Compare commits
15 Commits
supabase-c
...
rf/diff3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
020e5f7b32 | ||
|
|
1414030afe | ||
|
|
92bf928b78 | ||
|
|
0bcc036a84 | ||
|
|
6277188c1b | ||
|
|
65c9d43419 | ||
|
|
cc5744ee2d | ||
|
|
1e796881b3 | ||
|
|
a55d99a8b7 | ||
|
|
07477f9e35 | ||
|
|
9409d5e266 | ||
|
|
2d71ebbe09 | ||
|
|
c1673ac036 | ||
|
|
8ca54e02c0 | ||
|
|
53ddf013df |
20
.github/workflows/deploy_to_windmill.yml
vendored
Normal file
20
.github/workflows/deploy_to_windmill.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: Deploy to windmill.dev
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "community/**"
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Deploy to windmill.dev
|
||||
uses: windmill-labs/windmill-gh-action-deploy@v2.0.0
|
||||
with:
|
||||
dry_run: false
|
||||
input_dir: community
|
||||
windmill_workspace: starter
|
||||
windmill_token: ${{ secrets.WINDMILL_API_TOKEN }}
|
||||
68
.github/workflows/docker-image.yml
vendored
68
.github/workflows/docker-image.yml
vendored
@@ -109,38 +109,38 @@ jobs:
|
||||
${{ steps.meta-ee-public.outputs.labels }}
|
||||
org.opencontainers.image.licenses=Windmill-Enterprise-License
|
||||
|
||||
# disabled until we make it 100% reliable and add more meaningful tests
|
||||
# playwright:
|
||||
# runs-on: [self-hosted, new]
|
||||
# needs: [build]
|
||||
# services:
|
||||
# postgres:
|
||||
# image: postgres
|
||||
# env:
|
||||
# POSTGRES_DB: windmill
|
||||
# POSTGRES_USER: admin
|
||||
# POSTGRES_PASSWORD: changeme
|
||||
# ports:
|
||||
# - 5432:5432
|
||||
# options: >-
|
||||
# --health-cmd pg_isready
|
||||
# --health-interval 10s
|
||||
# --health-timeout 5s
|
||||
# --health-retries 5
|
||||
# steps:
|
||||
# - uses: actions/checkout@v3
|
||||
# - name: "Docker"
|
||||
# run: echo "::set-output name=id::$(docker run --network=host --rm -d -p 8000:8000 --privileged -it -e DATABASE_URL=postgres://admin:changeme@localhost:5432/windmill -e BASE_INTERNAL_URL=http://localhost:8000 ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest)"
|
||||
# id: docker-container
|
||||
# - uses: actions/setup-node@v3
|
||||
# with:
|
||||
# node-version: 16
|
||||
# - name: "Playwright run"
|
||||
# timeout-minutes: 2
|
||||
# run: cd frontend && npm ci @playwright/test && npx playwright install && export BASE_URL=http://localhost:8000 && npm run test
|
||||
# - name: "Clean up"
|
||||
# run: docker kill ${{ steps.docker-container.outputs.id }}
|
||||
# if: always()
|
||||
|
||||
playwright:
|
||||
runs-on: [self-hosted, new]
|
||||
needs: [build]
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
env:
|
||||
POSTGRES_DB: windmill
|
||||
POSTGRES_USER: admin
|
||||
POSTGRES_PASSWORD: changeme
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: "Docker"
|
||||
run: echo "::set-output name=id::$(docker run --network=host --rm -d -p 8000:8000 --privileged -it -e DATABASE_URL=postgres://admin:changeme@localhost:5432/windmill -e BASE_INTERNAL_URL=http://localhost:8000 ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest)"
|
||||
id: docker-container
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
- name: "Playwright run"
|
||||
timeout-minutes: 2
|
||||
run: cd frontend && npm ci @playwright/test && npx playwright install && export BASE_URL=http://localhost:8000 && npm run test
|
||||
- name: "Clean up"
|
||||
run: docker kill ${{ steps.docker-container.outputs.id }}
|
||||
if: always()
|
||||
|
||||
|
||||
publish_privately_heavy:
|
||||
@@ -182,7 +182,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push privately
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v3
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
context: .
|
||||
@@ -222,7 +222,7 @@ jobs:
|
||||
password: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
|
||||
- name: Build and push privately
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v3
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
context: .
|
||||
|
||||
2
.github/workflows/pypi_on_release.yml
vendored
2
.github/workflows/pypi_on_release.yml
vendored
@@ -65,7 +65,7 @@ jobs:
|
||||
password: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
|
||||
- name: Build and push publicly
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: "{{defaultContext}}:lsp"
|
||||
push: true
|
||||
|
||||
@@ -25,12 +25,12 @@ jobs:
|
||||
run: echo "UUID_TAG_APP=$(uuidgen)" >> $GITHUB_ENV
|
||||
- name: Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: registry.uffizzi.com/${{ env.UUID_TAG_APP }}
|
||||
tags: type=raw,value=60d
|
||||
- name: Build and Push Image to registry.uffizzi.com ephemeral registry
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
push: true
|
||||
context: ./
|
||||
168
CHANGELOG.md
168
CHANGELOG.md
@@ -1,174 +1,6 @@
|
||||
# Changelog
|
||||
|
||||
|
||||
## [1.78.0](https://github.com/windmill-labs/windmill/compare/v1.77.0...v1.78.0) (2023-03-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **frontend:** app textcomponent editable + tooltip ([11567d6](https://github.com/windmill-labs/windmill/commit/11567d6280ea60f1a8c3c6607c724179775cbbe3))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **backend:** whitelist for include_header was ignored in some cases ([183a459](https://github.com/windmill-labs/windmill/commit/183a4591df700ab4720de6e92a83631256940089))
|
||||
* **frontend:** improve rendering performance after component moving ([6f890f2](https://github.com/windmill-labs/windmill/commit/6f890f2120885f90d986fbd655096b45bf9de539))
|
||||
* **frontend:** remove staticOutputs from apps ([dbdfd62](https://github.com/windmill-labs/windmill/commit/dbdfd626386398180ecba7976714f86365eeccd8))
|
||||
|
||||
## [1.77.0](https://github.com/windmill-labs/windmill/compare/v1.76.0...v1.77.0) (2023-03-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **apps:** state can be used as input in apps ([2f0acb9](https://github.com/windmill-labs/windmill/commit/2f0acb9ffa8dace4a886527dcee49809d019b271))
|
||||
* **apps:** tabs can be made pages or invisible + better frontend scripts reactivity ([cd645d0](https://github.com/windmill-labs/windmill/commit/cd645d0935f2d06e0ff71f14d2cf63accd378ff3))
|
||||
* **deno:** add support for custom npm repo ([#1291](https://github.com/windmill-labs/windmill/issues/1291)) ([944795f](https://github.com/windmill-labs/windmill/commit/944795f6eeaa7d01ab1a35a80570a55c363723e6))
|
||||
* **frontend:** add setTab to frontend scripts ([c2a97c5](https://github.com/windmill-labs/windmill/commit/c2a97c53cfff0fdb35dd8bc249490566eebdc1a9))
|
||||
* **frontend:** app components output panel ([#1283](https://github.com/windmill-labs/windmill/issues/1283)) ([751edcf](https://github.com/windmill-labs/windmill/commit/751edcf9b8e0976a1d073603c9eff5dc6e714490))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **backend:** do not cache reference to workspace scripts ([eb73f2a](https://github.com/windmill-labs/windmill/commit/eb73f2a687f6faad301b9038ab8585450bec7481))
|
||||
* **frontend:** fix app tabs ([#1288](https://github.com/windmill-labs/windmill/issues/1288)) ([c71a577](https://github.com/windmill-labs/windmill/commit/c71a577fead90c9cd01a736b54d859ec4f0b7807))
|
||||
* **frontend:** fix container deletion ([#1287](https://github.com/windmill-labs/windmill/issues/1287)) ([bc870bd](https://github.com/windmill-labs/windmill/commit/bc870bd03eb76cb8bc0e0c861f6cd8a9c661186b))
|
||||
* **frontend:** Update setting accordion ([#1285](https://github.com/windmill-labs/windmill/issues/1285)) ([dea12e8](https://github.com/windmill-labs/windmill/commit/dea12e8870ece998bb6607723cbaab9b9a958f22))
|
||||
|
||||
## [1.76.0](https://github.com/windmill-labs/windmill/compare/v1.75.0...v1.76.0) (2023-03-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **frontend:** add frontend (JS) scripts to apps ([f0b1b1f](https://github.com/windmill-labs/windmill/commit/f0b1b1f752731ba434b960a75624118152f53c00))
|
||||
* **frontend:** Copy, Cut and Paste ([#1279](https://github.com/windmill-labs/windmill/issues/1279)) ([82c139e](https://github.com/windmill-labs/windmill/commit/82c139ed0992be401e250cfb7ecc0fca61b76772))
|
||||
* **frontend:** disabled for action buttons can now depend on row ([75f87e7](https://github.com/windmill-labs/windmill/commit/75f87e7e1117a9c12afcf626379e94b134a9a493))
|
||||
* **frontend:** improve drag-n-drop behavior ([cfd489a](https://github.com/windmill-labs/windmill/commit/cfd489a55059e7b6843f99bab261c70b3852e6a2))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **backend:** improve worker ping api ([c958480](https://github.com/windmill-labs/windmill/commit/c958480ce83844a989f58dd5a70eb288582e2194))
|
||||
* **frontend:** General fixes and updates ([#1281](https://github.com/windmill-labs/windmill/issues/1281)) ([3e5a179](https://github.com/windmill-labs/windmill/commit/3e5a179eb8cd8001f49c92305141dade1571e20f))
|
||||
|
||||
## [1.75.0](https://github.com/windmill-labs/windmill/compare/v1.74.2...v1.75.0) (2023-03-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add filter jobs by args or result ([3b44f9a](https://github.com/windmill-labs/windmill/commit/3b44f9a72ca0466a44963a4b9657a0ee59b44753))
|
||||
* **apps:** add resource picker ([8681e83](https://github.com/windmill-labs/windmill/commit/8681e83b574141acbf7e5a389a9e8a4f340336d1))
|
||||
* **bash:** add default argument handling for bash ([1d5c194](https://github.com/windmill-labs/windmill/commit/1d5c194f09ffba963d52e418c5954843d84ae337))
|
||||
* **frontend-apps:** add variable picker for static string input on apps ([bc440f8](https://github.com/windmill-labs/windmill/commit/bc440f8d4154ce464c0e027d93b7a0a3b76d782e))
|
||||
* **frontend:** make runs filters synced with query args ([61a5e1f](https://github.com/windmill-labs/windmill/commit/61a5e1f1accc988628b785b3b9be04c4ea719874))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **backend:** add killpill for lines reading ([7c825c2](https://github.com/windmill-labs/windmill/commit/7c825c212dd0f1e8be427eabd9a9756303241d1b))
|
||||
* **cli:** many small fixes ([ce32370](https://github.com/windmill-labs/windmill/commit/ce323709a94d27fb24214719180ea1aafc66d646))
|
||||
|
||||
## [1.74.2](https://github.com/windmill-labs/windmill/compare/v1.74.1...v1.74.2) (2023-03-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **frontend:** fix splitpanes navigation ([#1276](https://github.com/windmill-labs/windmill/issues/1276)) ([8d5c5b8](https://github.com/windmill-labs/windmill/commit/8d5c5b88a35d7a3bad1d8ddf2d940026825241eb))
|
||||
|
||||
## [1.74.1](https://github.com/windmill-labs/windmill/compare/v1.74.0...v1.74.1) (2023-03-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **apps:** proper reactivity for non rendered static components ([ae53baf](https://github.com/windmill-labs/windmill/commit/ae53bafaf6777f928113f84b2c6ed6a2ed341844))
|
||||
* **ci:** make windmill compile again by pinning swc deps ([2ea15d5](https://github.com/windmill-labs/windmill/commit/2ea15d5035e5e15473968db3c0501a4dddff5cd0))
|
||||
|
||||
## [1.74.0](https://github.com/windmill-labs/windmill/compare/v1.73.1...v1.74.0) (2023-03-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add delete by path for scripts ([0c2cf92](https://github.com/windmill-labs/windmill/commit/0c2cf92dd3df9610e649f15e23921a4ca0d94e6a))
|
||||
* **frontend:** Add color picker input to app ([#1270](https://github.com/windmill-labs/windmill/issues/1270)) ([88e537a](https://github.com/windmill-labs/windmill/commit/88e537ad1fb4c207f38fbe951c82106bef6491a3))
|
||||
* **frontend:** add expand ([#1268](https://github.com/windmill-labs/windmill/issues/1268)) ([b854ee3](https://github.com/windmill-labs/windmill/commit/b854ee34393534bde104e2e6f606108fd66d38dc))
|
||||
* **frontend:** add hash to ctx in apps ([b1a45b1](https://github.com/windmill-labs/windmill/commit/b1a45b1e708aa6f19f8be9c949507083e044f2d8))
|
||||
* **frontend:** Add key navigation in app editor ([#1273](https://github.com/windmill-labs/windmill/issues/1273)) ([6b0fb75](https://github.com/windmill-labs/windmill/commit/6b0fb75d23e2151c88b07814139d203c1bd0578d))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cli:** improve visibility of the active workspace ([e6344da](https://github.com/windmill-labs/windmill/commit/e6344dac6d1be04b46231fa8ef8579fd12ca8f37))
|
||||
* **frontend:** add confirmation modal to delete script/flow/app ([a4adcb5](https://github.com/windmill-labs/windmill/commit/a4adcb5192c11f7bf47a0d259825e474779378d7))
|
||||
* **frontend:** Clean up app editor ([#1267](https://github.com/windmill-labs/windmill/issues/1267)) ([0a5e181](https://github.com/windmill-labs/windmill/commit/0a5e181a3aa966fb8211bee0d9174fc16353b31f))
|
||||
* **frontend:** Minor changes ([#1272](https://github.com/windmill-labs/windmill/issues/1272)) ([3b6ae0c](https://github.com/windmill-labs/windmill/commit/3b6ae0cc49461b858d9cfff79eae9a7569465235))
|
||||
* **frontend:** simplify input bindings ([b2de531](https://github.com/windmill-labs/windmill/commit/b2de531a46e4b120d7106d361b727746bec516dd))
|
||||
|
||||
## [1.73.1](https://github.com/windmill-labs/windmill/compare/v1.73.0...v1.73.1) (2023-03-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **frontend:** load flow is not initialized ([719d475](https://github.com/windmill-labs/windmill/commit/719d4752621d462b1cfaa0d27930fba7586be779))
|
||||
|
||||
## [1.73.0](https://github.com/windmill-labs/windmill/compare/v1.72.0...v1.73.0) (2023-03-07)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **frontend:** add a way to automatically resize ([#1259](https://github.com/windmill-labs/windmill/issues/1259)) ([24f58ef](https://github.com/windmill-labs/windmill/commit/24f58efd9994a2201c1b1d9bbfb11734c57068e3))
|
||||
* **frontend:** add ability to move nodes ([614fb50](https://github.com/windmill-labs/windmill/commit/614fb5022aa7d5428fb96b7ee3a20794edd1e9d3))
|
||||
* **frontend:** Add app PDF viewer ([#1254](https://github.com/windmill-labs/windmill/issues/1254)) ([3e5d09e](https://github.com/windmill-labs/windmill/commit/3e5d09ef0b5619186bee5ec6d442cbfd12a6e8d5))
|
||||
* **frontend:** add fork/save buttons + consistent styling for slider/range ([9e9f8ef](https://github.com/windmill-labs/windmill/commit/9e9f8efb8ee389ea75e99b67ef720756959ca737))
|
||||
* **frontend:** add history to flows and apps ([9e4d90a](https://github.com/windmill-labs/windmill/commit/9e4d90ad37a57ff1f515eea0c82cf603649e915d))
|
||||
* **frontend:** Fix object viewer style ([#1255](https://github.com/windmill-labs/windmill/issues/1255)) ([94f1aad](https://github.com/windmill-labs/windmill/commit/94f1aadef2b09ac1962478f11b27cc708b8328f1))
|
||||
* **frontend:** refactor entire flow builder UX ([2ac51b0](https://github.com/windmill-labs/windmill/commit/2ac51b0af08bdef7ce3c7e874e9983b9fc00478a))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **frontend:** arginput + apppreview fixes ([e2c4545](https://github.com/windmill-labs/windmill/commit/e2c45452401022b00285b21551ffaf35a114be33))
|
||||
* **frontend:** fix app map reactivity ([#1260](https://github.com/windmill-labs/windmill/issues/1260)) ([2557e13](https://github.com/windmill-labs/windmill/commit/2557e136bd0df1a023819b7d9b2235e30d7140b6))
|
||||
* **frontend:** fix branch deletion ([#1261](https://github.com/windmill-labs/windmill/issues/1261)) ([a999eb2](https://github.com/windmill-labs/windmill/commit/a999eb21121a7c0010621448324e0c77caf2b3f6))
|
||||
* **frontend:** Side menu z-index issue ([#1265](https://github.com/windmill-labs/windmill/issues/1265)) ([c638897](https://github.com/windmill-labs/windmill/commit/c638897fdcd58f55b0929f91641b21a6f9d25ead))
|
||||
|
||||
## [1.72.0](https://github.com/windmill-labs/windmill/compare/v1.71.0...v1.72.0) (2023-03-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **backend:** get_result_by_id do a downward pass to find node at any depth ([#1249](https://github.com/windmill-labs/windmill/issues/1249)) ([4c913dc](https://github.com/windmill-labs/windmill/commit/4c913dc4b6be03571a015c97a13829adffb61479))
|
||||
* **frontend:** Add app map component ([#1251](https://github.com/windmill-labs/windmill/issues/1251)) ([ed25d9f](https://github.com/windmill-labs/windmill/commit/ed25d9f186d9925f75404cb193a025d8a41c4540))
|
||||
* **frontend:** app splitpanes ([#1248](https://github.com/windmill-labs/windmill/issues/1248)) ([f4d79ee](https://github.com/windmill-labs/windmill/commit/f4d79ee2633e6cdab0fa2410108b31cfa77e10da))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **backend:** improve result retrieval ([c4463bb](https://github.com/windmill-labs/windmill/commit/c4463bb029907f3c8d77abb194f872aae7876bf6))
|
||||
* **backend:** incorrect get_result_by_id for list_result job ([2a75cd2](https://github.com/windmill-labs/windmill/commit/2a75cd250ea5e01849fc8bbb69bf44f147d0acb8))
|
||||
* **cli:** fix workspace option + run script/flow + whoami ([35ea2b2](https://github.com/windmill-labs/windmill/commit/35ea2b27b12159c68c8507ec1f8686028c975387))
|
||||
* **frontend:** background script not showing inputs ([55eb48c](https://github.com/windmill-labs/windmill/commit/55eb48c55332431304cedbf3bcbbbcff61ec3645))
|
||||
* **frontend:** fix table bindings ([2679386](https://github.com/windmill-labs/windmill/commit/2679386bf87a56352269911bd89e52df5ee9f314))
|
||||
* **frontend:** rework app reactivity ([94b20d2](https://github.com/windmill-labs/windmill/commit/94b20d2f5e3b551974c57ea82b6e3dc16e97b9b8))
|
||||
* **frontend:** rework app reactivity ([1753cb7](https://github.com/windmill-labs/windmill/commit/1753cb7da658f47be974c15da82c71a8e19309a6))
|
||||
|
||||
## [1.71.0](https://github.com/windmill-labs/windmill/compare/v1.70.1...v1.71.0) (2023-02-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **backend:** use counter for sleep/execution/pull durations ([e568690](https://github.com/windmill-labs/windmill/commit/e56869092a03fec4703ddd9ef65c89edb8122962))
|
||||
* **cli:** add autocompletions ([287b2db](https://github.com/windmill-labs/windmill/commit/287b2db22f7b56e90bcd0c4727c00096695c2e0d))
|
||||
* **frontend:** App drawer ([#1246](https://github.com/windmill-labs/windmill/issues/1246)) ([8a0d115](https://github.com/windmill-labs/windmill/commit/8a0d1158c4d7e970cb91e1adf4838e5efdbb39ff))
|
||||
* **frontend:** drawer for editing workspace scripts in flows ([6adc875](https://github.com/windmill-labs/windmill/commit/6adc87561070d8aceaba1838008cd7e6be2e2660))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **frontend:** Add more app custom css ([#1229](https://github.com/windmill-labs/windmill/issues/1229)) ([a4e4d18](https://github.com/windmill-labs/windmill/commit/a4e4d188ad10443dd0b7f104389594efc768dc59))
|
||||
* **frontend:** Add more app custom css ([#1247](https://github.com/windmill-labs/windmill/issues/1247)) ([1bb5ed9](https://github.com/windmill-labs/windmill/commit/1bb5ed9ae01fd7998b06833b6222e5dd5d774d35))
|
||||
* **frontend:** display currently selected filter even if not in list ([42d1cd6](https://github.com/windmill-labs/windmill/commit/42d1cd6456620ba917c560c87d736dc93634adff))
|
||||
* **frontend:** Fix deeply nested move ([#1245](https://github.com/windmill-labs/windmill/issues/1245)) ([a67f10e](https://github.com/windmill-labs/windmill/commit/a67f10eeb6fdb44bbb3a510badcc5ad0ae187a2b))
|
||||
* **frontend:** invisible subgrids have h-0 + app policies fix ([2244e83](https://github.com/windmill-labs/windmill/commit/2244e83b9da803a4cf46ab0825d7cb6cb0e24872))
|
||||
|
||||
## [1.70.1](https://github.com/windmill-labs/windmill/compare/v1.70.0...v1.70.1) (2023-02-27)
|
||||
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ ARG features=""
|
||||
|
||||
COPY --from=planner /windmill/recipe.json recipe.json
|
||||
|
||||
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true RUST_BACKTRACE=1 cargo chef cook --release --features "$features" --recipe-path recipe.json
|
||||
RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo chef cook --release --features "$features" --recipe-path recipe.json
|
||||
|
||||
COPY ./openflow.openapi.yaml /openflow.openapi.yaml
|
||||
COPY ./backend ./
|
||||
@@ -86,7 +86,6 @@ RUN CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release --features "$feature
|
||||
|
||||
|
||||
FROM python:3.11.2-slim-buster
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
ARG APP=/usr/src/app
|
||||
|
||||
@@ -130,10 +129,6 @@ COPY --from=nsjail /nsjail/nsjail /bin/nsjail
|
||||
|
||||
COPY --from=denoland/deno:latest /usr/bin/deno /usr/bin/deno
|
||||
|
||||
# docker does not support conditional COPY and we want to use the same Dockerfile for both amd64 and arm64 and privilege the official image
|
||||
COPY --from=lukechannings/deno:latest /usr/bin/deno /usr/bin/deno-arm
|
||||
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then rm /usr/bin/deno-arm; elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then mv /usr/bin/deno-arm /usr/bin/deno; fi
|
||||
|
||||
RUN mkdir -p ${APP}
|
||||
|
||||
WORKDIR ${APP}
|
||||
|
||||
8
LICENSE
8
LICENSE
@@ -8,9 +8,5 @@ or belonging to one of the below cases:
|
||||
|
||||
The files under backend/ are AGPL Licensed.
|
||||
The files under frontend/ are AGPL Licensed.
|
||||
The files under python-client/ deno-client/ go-client/ are Apache 2.0 Licensed.
|
||||
|
||||
The openapi files, including the OpenFlow spec is Apache 2.0 Licensed.
|
||||
|
||||
All third party components incorporated into the Windmill Software are licensed under the
|
||||
original license provided by the owner of the applicable component.
|
||||
The files under python-client/ are Apache 2.0 Licensed.
|
||||
The files under community/ are Apache 2.0 Licensed.
|
||||
|
||||
@@ -284,7 +284,6 @@ you to have it being synced automatically everyday.
|
||||
| ------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
|
||||
| DATABASE_URL | | The Postgres database url. | All |
|
||||
| DISABLE_NSJAIL | true | Disable Nsjail Sandboxing | Worker |
|
||||
| SERVER_BIND_ADDR | 0.0.0.0 | IP Address on which to bind listening socket | Server |
|
||||
| PORT | 8000 | Exposed port | Server | |
|
||||
| NUM_WORKERS | 3 | The number of worker per Worker instance (set to 1 on Eks to have 1 pod = 1 worker, set to 0 for an API only instance) | Worker |
|
||||
| DISABLE_SERVER | false | Binary would operate as a worker only instance | Worker |
|
||||
@@ -316,7 +315,6 @@ you to have it being synced automatically everyday.
|
||||
| QUEUE_LIMIT_WAIT_RESULT | None | The number of max jobs in the queue before rejecting immediately the request in 'run_wait_result' endpoint. Takes precedence on the query arg. If none is specified, there are no limit. | Worker |
|
||||
| DENO_AUTH_TOKENS | None | Custom DENO_AUTH_TOKENS to pass to worker to allow the use of private modules | Worker |
|
||||
| DENO_FLAGS | None | Override the flags passed to deno (default --allow-all) to tighten permissions. Minimum permissions needed are "--allow-read=args.json --allow-write=result.json" | Worker |
|
||||
| NPM_CONFIG_REGISTRY | None | Registry to use for NPM dependencies, set if you have a private repository you need to use instead of the default public NPM registry | Worker |
|
||||
| PIP_LOCAL_DEPENDENCIES | None | Specify dependencies that are installed locally and do not need to be solved nor installed again | |
|
||||
| ADDITIONAL_PYTHON_PATHS | None | Specify python paths (separated by a :) to be appended to the PYTHONPATH of the python jobs. To be used with PIP_LOCAL_DEPENDENCIES to use python codebases within Windmill | Worker |
|
||||
| INCLUDE_HEADERS | None | Whitelist of headers that are passed to jobs as args (separated by a comma) | Server |
|
||||
|
||||
473
backend/Cargo.lock
generated
473
backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.78.0"
|
||||
version = "1.70.1"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -19,7 +19,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.78.0"
|
||||
version = "1.70.1"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -123,8 +123,8 @@ regex = "^1"
|
||||
deno_core = "^0"
|
||||
async-recursion = "^1"
|
||||
swc_common = "^0"
|
||||
swc_ecma_parser = "0.128.2"
|
||||
swc_ecma_ast = "0.98.1"
|
||||
swc_ecma_parser = "^0"
|
||||
swc_ecma_ast = "^0"
|
||||
base64 = "0.21.0"
|
||||
unicode-general-category = "^0"
|
||||
hmac = "0.12.1"
|
||||
|
||||
@@ -1165,3 +1165,18 @@ VALUES
|
||||
}') RETURNING id)
|
||||
UPDATE app SET versions = ARRAY((select id from _insert)), policy = '{ "execution_mode": "viewer", "triggerables": {} }'
|
||||
WHERE workspace_id = 'admins' AND path = 'g/all/setup_app';
|
||||
|
||||
UPDATE script SET content = 'import wmill from "https://deno.land/x/wmill@v1.69.3/main.ts";
|
||||
export async function main() {
|
||||
await run(
|
||||
"workspace", "add", "__automation", "admins", Deno.env.get("BASE_INTERNAL_URL") + "/", "--token", Deno.env.get("WM_TOKEN"));
|
||||
|
||||
await run("hub", "pull");
|
||||
}
|
||||
|
||||
async function run(...cmd: string[]) {
|
||||
console.log("Running \"" + cmd.join('' '') + "\"");
|
||||
await wmill.parse(cmd);
|
||||
}', summary = 'Synchronize Hub Resource types with admins workspace',
|
||||
description = 'Basic administrative script to sync latest resource types from hub to share to every workspace. Recommended to run at least once. On a schedule by default.'
|
||||
WHERE hash = -28028598712388162 AND workspace_id = 'admins';
|
||||
@@ -1 +0,0 @@
|
||||
-- Add down migration script here
|
||||
@@ -1,16 +0,0 @@
|
||||
-- Add up migration script here
|
||||
|
||||
UPDATE script SET content = 'import wmill from "https://deno.land/x/wmill@v1.70.1/main.ts";
|
||||
export async function main() {
|
||||
await run(
|
||||
"workspace", "add", "__automation", "admins", Deno.env.get("BASE_INTERNAL_URL") + "/", "--token", Deno.env.get("WM_TOKEN"));
|
||||
|
||||
await run("hub", "pull");
|
||||
}
|
||||
|
||||
async function run(...cmd: string[]) {
|
||||
console.log("Running \"" + cmd.join('' '') + "\"");
|
||||
await wmill.parse(cmd);
|
||||
}', summary = 'Synchronize Hub Resource types with admins workspace',
|
||||
description = 'Basic administrative script to sync latest resource types from hub to share to every workspace. Recommended to run at least once. On a schedule by default.'
|
||||
WHERE hash = -28028598712388162 AND workspace_id = 'admins';
|
||||
@@ -1 +0,0 @@
|
||||
-- Add down migration script here
|
||||
@@ -1,3 +0,0 @@
|
||||
-- Add up migration script here
|
||||
ALTER TABLE queue ADD COLUMN root_job uuid;
|
||||
ALTER TABLE queue ADD COLUMN leaf_jobs jsonb;
|
||||
@@ -16,5 +16,4 @@ unicode-general-category.workspace = true
|
||||
itertools.workspace = true
|
||||
anyhow.workspace = true
|
||||
regex.workspace = true
|
||||
lazy_static.workspace = true
|
||||
serde_json.workspace = true
|
||||
lazy_static.workspace = true
|
||||
@@ -1,8 +1,6 @@
|
||||
#![allow(non_snake_case)] // TODO: switch to parse_* function naming
|
||||
|
||||
use anyhow::anyhow;
|
||||
use regex::Regex;
|
||||
use serde_json::json;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use windmill_parser::{Arg, MainArgSignature, Typ};
|
||||
@@ -19,32 +17,19 @@ pub fn parse_bash_sig(code: &str) -> windmill_common::error::Result<MainArgSigna
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref RE: Regex = Regex::new(r#"(?m)^(\w+)="\$(?:(\d+)|\{(\d+):-(.*)\})"$"#).unwrap();
|
||||
}
|
||||
|
||||
fn parse_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
|
||||
let mut hm: HashMap<i32, (String, Option<String>)> = HashMap::new();
|
||||
for cap in RE.captures_iter(code) {
|
||||
hm.insert(
|
||||
cap.get(2)
|
||||
.or(cap.get(3))
|
||||
.and_then(|x| x.as_str().parse::<i32>().ok())
|
||||
.ok_or_else(|| anyhow!("Impossible to parse arg digit"))?,
|
||||
(
|
||||
cap[1].to_string(),
|
||||
cap.get(4).map(|x| x.as_str().to_string()),
|
||||
),
|
||||
);
|
||||
let mut hm = HashMap::new();
|
||||
let re = Regex::new(r#"(?m)^(\w+)="\$(\d+)"$"#).unwrap();
|
||||
for cap in re.captures_iter(code) {
|
||||
hm.insert(cap[2].parse::<i32>()?, cap[1].to_string());
|
||||
}
|
||||
let mut args = vec![];
|
||||
for i in 1..20 {
|
||||
if hm.contains_key(&i) {
|
||||
let (name, default) = hm.get(&i).unwrap();
|
||||
args.push(Arg {
|
||||
name: name.clone(),
|
||||
name: hm[&i].clone(),
|
||||
typ: Typ::Str(None),
|
||||
default: default.clone().map(|x| json!(x)),
|
||||
default: None,
|
||||
otyp: None,
|
||||
has_default: false,
|
||||
});
|
||||
@@ -58,8 +43,6 @@ fn parse_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
@@ -67,7 +50,8 @@ mod tests {
|
||||
let code = r#"
|
||||
token="$1"
|
||||
image="$2"
|
||||
digest="${3:-latest with spaces}"
|
||||
digest="${3:-latest}"
|
||||
foo="$4"
|
||||
|
||||
"#;
|
||||
//println!("{}", serde_json::to_string()?);
|
||||
@@ -90,13 +74,6 @@ digest="${3:-latest with spaces}"
|
||||
typ: Typ::Str(None),
|
||||
default: None,
|
||||
has_default: false
|
||||
},
|
||||
Arg {
|
||||
otyp: None,
|
||||
name: "digest".to_string(),
|
||||
typ: Typ::Str(None),
|
||||
default: Some(json!("latest with spaces")),
|
||||
has_default: false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -468,27 +468,6 @@
|
||||
},
|
||||
"query": "DELETE FROM workspace_settings WHERE workspace_id = $1"
|
||||
},
|
||||
"0e7d95f4913e5775651971d741a3b5c1ef5dfe079be5325abe2866d39a7fe5fb": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "path",
|
||||
"ordinal": 0,
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "DELETE FROM script WHERE path = $1 AND workspace_id = $2 RETURNING path"
|
||||
},
|
||||
"11b1586acdfc180c5a077861ee1f7201fcbcec9d0ebada464f9d952c9c3e400d": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -571,6 +550,21 @@
|
||||
},
|
||||
"query": "SELECT * FROM workspace LIMIT $1 OFFSET $2"
|
||||
},
|
||||
"15de975d9be141c9ed9647935a508492aabbbddbf986d5c5c0f0c415293c432d": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO variable\n (workspace_id, path, value, is_secret, description)\n VALUES ($1, 'g/all/pretty_secret', $2, true, 'This item is secret'), \n ($3, 'g/all/not_secret', $4, false, 'This item is not secret')"
|
||||
},
|
||||
"163f00eb8b1a489d5f382cdba22a5744e88a8e6f1532d7cb02af560f5f5d49f7": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -826,6 +820,75 @@
|
||||
},
|
||||
"query": "\n SELECT id, flow_status, suspend, script_path\n FROM queue\n WHERE id = $1\n "
|
||||
},
|
||||
"1e35c39bc786d638252e5483ca4efae9a041f7e845341f8bfd715ddd9e899499": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Uuid",
|
||||
"Bool",
|
||||
"Uuid",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Timestamptz",
|
||||
"Int8",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text",
|
||||
"Jsonb",
|
||||
{
|
||||
"Custom": {
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"script",
|
||||
"preview",
|
||||
"flow",
|
||||
"dependencies",
|
||||
"flowpreview",
|
||||
"script_hub",
|
||||
"identity",
|
||||
"flowdependencies"
|
||||
]
|
||||
},
|
||||
"name": "job_kind"
|
||||
}
|
||||
},
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb",
|
||||
"Bool",
|
||||
{
|
||||
"Custom": {
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"python3",
|
||||
"deno",
|
||||
"go",
|
||||
"bash"
|
||||
]
|
||||
},
|
||||
"name": "script_lang"
|
||||
}
|
||||
},
|
||||
"Bool",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Bool"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO queue\n (workspace_id, id, running, parent_job, created_by, permissioned_as, scheduled_for, \n script_hash, script_path, raw_code, raw_lock, args, job_kind, schedule_path, raw_flow, flow_status, is_flow_step, language, started_at, same_worker, pre_run_error, email, visible_to_owner)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, CASE WHEN $3 THEN now() END, $19, $20, $21, $22) RETURNING id"
|
||||
},
|
||||
"1eaf8d677d520c7f2f303a731de6b6d939918e41ad0d1c748d80db3fd33cb9d3": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -1107,20 +1170,6 @@
|
||||
},
|
||||
"query": "SELECT set_config('session.folders_read', $1, true)"
|
||||
},
|
||||
"2a3ebe1b0eae5b2164894321e138cc4dc0293788aeb98d05d95d18dfc708d6a6": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Jsonb",
|
||||
"Uuid"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n UPDATE queue\n SET leaf_jobs = JSONB_SET(coalesce(leaf_jobs, '{}'::jsonb), ARRAY[$1::TEXT], $2)\n WHERE COALESCE((SELECT root_job FROM queue WHERE id = $3), $3) = id\n "
|
||||
},
|
||||
"2a4be8334db7d39f3d954193a8b0169cc4a4a07e081d2fa61d8764879d6a8ff5": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -1134,6 +1183,33 @@
|
||||
},
|
||||
"query": "UPDATE script SET archived = true WHERE hash = $1 AND workspace_id = $2"
|
||||
},
|
||||
"2be0cfd075df9624ccbcbe5fd645e0a5c25460c2d01493f86dcdd9b2b71f6181": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "flow_status",
|
||||
"ordinal": 0,
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"name": "parent_job",
|
||||
"ordinal": 1,
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
null,
|
||||
null
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT flow_status, parent_job FROM completed_job WHERE id = $1 AND workspace_id = $2 UNION ALL SELECT flow_status, parent_job FROM queue WHERE id = $1 AND workspace_id = $2 "
|
||||
},
|
||||
"2e4115bb2e6c8c85ad1492ad135d6b0454b342126cb5fa17e58caf71b32ee755": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -2501,57 +2577,6 @@
|
||||
},
|
||||
"query": "SELECT set_config('session.user', $1, true)"
|
||||
},
|
||||
"6d50a8dc9cfc040b6f37b58053daa0709671e008a10ab4189114ceff66efe603": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "worker",
|
||||
"ordinal": 0,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "worker_instance",
|
||||
"ordinal": 1,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "last_ping",
|
||||
"ordinal": 2,
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"name": "started_at",
|
||||
"ordinal": 3,
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"name": "ip",
|
||||
"ordinal": 4,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "jobs_executed",
|
||||
"ordinal": 5,
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT worker, worker_instance, EXTRACT(EPOCH FROM (now() - ping_at))::integer as last_ping, started_at, ip, jobs_executed FROM worker_ping ORDER BY ping_at desc LIMIT $1 OFFSET $2"
|
||||
},
|
||||
"6dfd1ac7168f098a9e7c92d530a470c0a156041d5081053d43d9efb00d81c464": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -2573,6 +2598,57 @@
|
||||
},
|
||||
"query": "SELECT policy from app WHERE path = $1 AND workspace_id = $2"
|
||||
},
|
||||
"6fc2cfae9df83eb24ea33e4c9567740100f4dd2285afc3ef474fc70041b0567b": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "worker",
|
||||
"ordinal": 0,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "worker_instance",
|
||||
"ordinal": 1,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "ping_at",
|
||||
"ordinal": 2,
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"name": "started_at",
|
||||
"ordinal": 3,
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"name": "ip",
|
||||
"ordinal": 4,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "jobs_executed",
|
||||
"ordinal": 5,
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT * FROM worker_ping ORDER BY ping_at desc LIMIT $1 OFFSET $2"
|
||||
},
|
||||
"701f215eb14ba67a79afea15d7effc0dd394ba6c4a72c95d4560c5a377015d4e": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -2984,6 +3060,20 @@
|
||||
},
|
||||
"query": "UPDATE group_ SET summary = $1 WHERE name = $2 AND workspace_id = $3"
|
||||
},
|
||||
"83fc9bf32b9b40987feff61fea82e20613e74efb7bf8ba52485566667ccf2284": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "UPDATE resource SET path = $1 WHERE path = $2 AND workspace_id = $3"
|
||||
},
|
||||
"8543f029d9784234e4c6a6dcd7b03e62d544b98be261334ee210594e0bb839f2": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -3154,21 +3244,6 @@
|
||||
},
|
||||
"query": "INSERT INTO usage (id, is_workspace, month_, usage) \n VALUES ($1, false, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 0) \n ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + 1 \n RETURNING usage.usage"
|
||||
},
|
||||
"8c0131a9cc61f2daa258d49767242bcaab6bb34a977ff7fb0c18aa9202d11f47": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "UPDATE resource SET path = $1, value = $2 WHERE path = $3 AND workspace_id = $4"
|
||||
},
|
||||
"8c11511a74a41a65f448249a00ebe6964a61d00c2f7b4875a55e64741bf1f0ca": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -3928,76 +4003,6 @@
|
||||
},
|
||||
"query": "SELECT content FROM script WHERE path = $1 AND workspace_id = $2 AND\n created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND archived = false AND workspace_id = $2)"
|
||||
},
|
||||
"a1c41bbeb2d64fa1e7dfd2ed053191a1de5d786ae8c22e225e450865ecac94e9": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Uuid",
|
||||
"Bool",
|
||||
"Uuid",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Timestamptz",
|
||||
"Int8",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text",
|
||||
"Jsonb",
|
||||
{
|
||||
"Custom": {
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"script",
|
||||
"preview",
|
||||
"flow",
|
||||
"dependencies",
|
||||
"flowpreview",
|
||||
"script_hub",
|
||||
"identity",
|
||||
"flowdependencies"
|
||||
]
|
||||
},
|
||||
"name": "job_kind"
|
||||
}
|
||||
},
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Jsonb",
|
||||
"Bool",
|
||||
{
|
||||
"Custom": {
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"python3",
|
||||
"deno",
|
||||
"go",
|
||||
"bash"
|
||||
]
|
||||
},
|
||||
"name": "script_lang"
|
||||
}
|
||||
},
|
||||
"Bool",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Uuid"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO queue\n (workspace_id, id, running, parent_job, created_by, permissioned_as, scheduled_for, \n script_hash, script_path, raw_code, raw_lock, args, job_kind, schedule_path, raw_flow, flow_status, is_flow_step, language, started_at, same_worker, pre_run_error, email, visible_to_owner, root_job)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, CASE WHEN $3 THEN now() END, $19, $20, $21, $22, $23) RETURNING id"
|
||||
},
|
||||
"a227548b6604c56bfc15eb780bd8ee72a89dc6701a50f5048e928bd87baa7b9a": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -4108,28 +4113,6 @@
|
||||
},
|
||||
"query": "UPDATE flow SET dependency_job = $1 WHERE path = $2 AND workspace_id = $3"
|
||||
},
|
||||
"a5f9fb82791103e2bbaf9cb6d87e8c50495d12d87f8ed83382068203a8dd7a67": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "?column?",
|
||||
"ordinal": 0,
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
null
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT leaf_jobs->$1::text FROM queue WHERE COALESCE((SELECT root_job FROM queue WHERE id = $2), $2) = id AND workspace_id = $3"
|
||||
},
|
||||
"a6145b0482c9e5da245059a80b1563cad20318fd2dd8aef33f9ca97de1826b8b": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -4698,27 +4681,6 @@
|
||||
},
|
||||
"query": "SELECT null FROM queue WHERE id = $1 FOR UPDATE"
|
||||
},
|
||||
"be1de9116b18a40681cb8c5ec7578fd7c5f7ce77f7af63f766c2899d85fe0bef": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "value",
|
||||
"ordinal": 0,
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
true
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT value FROM resource WHERE path = $1 AND workspace_id = $2"
|
||||
},
|
||||
"be7a99a5bb6858323ca61dd51077010f51ba58ae76b9a413339255024dcb524d": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -4907,6 +4869,27 @@
|
||||
},
|
||||
"query": "SELECT result FROM completed_job WHERE id = $1"
|
||||
},
|
||||
"c2d0e44faab6981a21ca28dfd6f4eef9dfcafb471852e701c0bbc8ae11344325": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "parent_job",
|
||||
"ordinal": 0,
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
null
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT parent_job FROM completed_job WHERE id = $1 AND workspace_id = $2 UNION ALL SELECT parent_job FROM queue WHERE id = $1 AND workspace_id = $2"
|
||||
},
|
||||
"c2d6cb56c1dea4498e2aab9ea9301dbbaa127602a38f57f5add4108fdc209b1a": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -5062,18 +5045,6 @@
|
||||
},
|
||||
"query": "\n SELECT id, flow_status, suspend, script_path\n FROM queue\n WHERE id = ( SELECT parent_job FROM queue WHERE id = $1 UNION ALL SELECT parent_job FROM completed_job WHERE id = $1)\n FOR UPDATE\n "
|
||||
},
|
||||
"c9d97800eb0ec87df8e8959b283dacb2c6cce422365ed394375641488ceb6b65": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "UPDATE worker_ping SET ping_at = now() WHERE worker = $1"
|
||||
},
|
||||
"cac594031a21b4806de9c4616317d3541522ef9712a83ecff7bd8b5f6e870748": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use git_version::git_version;
|
||||
use sqlx::{Pool, Postgres};
|
||||
@@ -15,7 +15,6 @@ use windmill_common::utils::rd_string;
|
||||
const GIT_VERSION: &str = git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
|
||||
const DEFAULT_NUM_WORKERS: usize = 3;
|
||||
const DEFAULT_PORT: u16 = 8000;
|
||||
const DEFAULT_SERVER_BIND_ADDR: Ipv4Addr = Ipv4Addr::new(0, 0, 0, 0);
|
||||
|
||||
mod ee;
|
||||
|
||||
@@ -40,11 +39,6 @@ async fn main() -> anyhow::Result<()> {
|
||||
.transpose()?
|
||||
.flatten();
|
||||
|
||||
let server_bind_address: IpAddr = std::env::var("SERVER_BIND_ADDR")
|
||||
.ok()
|
||||
.and_then(|x| x.parse().ok() )
|
||||
.unwrap_or(IpAddr::from(DEFAULT_SERVER_BIND_ADDR));
|
||||
|
||||
let port: u16 = std::env::var("PORT")
|
||||
.ok()
|
||||
.and_then(|x| x.parse::<u16>().ok())
|
||||
@@ -66,64 +60,8 @@ async fn main() -> anyhow::Result<()> {
|
||||
let (tx, rx) = tokio::sync::broadcast::channel::<()>(3);
|
||||
let shutdown_signal = windmill_common::shutdown_signal(tx);
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
tracing::info!(
|
||||
"
|
||||
##############################
|
||||
Windmill Enterprise Edition {GIT_VERSION}
|
||||
##############################"
|
||||
);
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
tracing::info!(
|
||||
"
|
||||
##############################
|
||||
Windmill Community Edition {GIT_VERSION}
|
||||
##############################"
|
||||
);
|
||||
|
||||
display_config(vec![
|
||||
"DISABLE_NSJAIL",
|
||||
"DISABLE_SERVER",
|
||||
"NUM_WORKERS",
|
||||
"METRICS_ADDR",
|
||||
"JSON_FMT",
|
||||
"BASE_URL",
|
||||
"BASE_INTERNAL_URL",
|
||||
"TIMEOUT",
|
||||
"SLEEP_QUEUE",
|
||||
"MAX_LOG_SIZE",
|
||||
"SERVER_BIND_ADDR",
|
||||
"PORT",
|
||||
"KEEP_JOB_DIR",
|
||||
"S3_CACHE_BUCKET",
|
||||
"TAR_CACHE_RATE",
|
||||
"COOKIE_DOMAIN",
|
||||
"PYTHON_PATH",
|
||||
"DENO_PATH",
|
||||
"GO_PATH",
|
||||
"PIP_INDEX_URL",
|
||||
"PIP_EXTRA_INDEX_URL",
|
||||
"PIP_TRUSTED_HOST",
|
||||
"PATH",
|
||||
"HOME",
|
||||
"DATABASE_CONNECTIONS",
|
||||
"TIMEOUT_WAIT_RESULT",
|
||||
"QUEUE_LIMIT_WAIT_RESULT",
|
||||
"DENO_AUTH_TOKENS",
|
||||
"DENO_FLAGS",
|
||||
"NPM_CONFIG_REGISTRY",
|
||||
"PIP_LOCAL_DEPENDENCIES",
|
||||
"ADDITIONAL_PYTHON_PATHS",
|
||||
"INCLUDE_HEADERS",
|
||||
"WHITELIST_WORKSPACES",
|
||||
"BLACKLIST_WORKSPACES",
|
||||
"NEW_USER_WEBHOOK",
|
||||
"CLOUD_HOSTED",
|
||||
]);
|
||||
|
||||
if server_mode || num_workers > 0 {
|
||||
let addr = SocketAddr::from((server_bind_address, port));
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||
|
||||
let server_f = async {
|
||||
if server_mode {
|
||||
@@ -134,6 +72,60 @@ Windmill Community Edition {GIT_VERSION}
|
||||
|
||||
let workers_f = async {
|
||||
if num_workers > 0 {
|
||||
#[cfg(feature = "enterprise")]
|
||||
tracing::info!(
|
||||
"
|
||||
##############################
|
||||
Windmill Enterprise Edition {GIT_VERSION}
|
||||
##############################"
|
||||
);
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
tracing::info!(
|
||||
"
|
||||
##############################
|
||||
Windmill Community Edition {GIT_VERSION}
|
||||
##############################"
|
||||
);
|
||||
|
||||
display_config(vec![
|
||||
"DISABLE_NSJAIL",
|
||||
"DISABLE_SERVER",
|
||||
"NUM_WORKERS",
|
||||
"METRICS_ADDR",
|
||||
"JSON_FMT",
|
||||
"BASE_URL",
|
||||
"BASE_INTERNAL_URL",
|
||||
"TIMEOUT",
|
||||
"SLEEP_QUEUE",
|
||||
"MAX_LOG_SIZE",
|
||||
"PORT",
|
||||
"KEEP_JOB_DIR",
|
||||
"S3_CACHE_BUCKET",
|
||||
"TAR_CACHE_RATE",
|
||||
"COOKIE_DOMAIN",
|
||||
"PYTHON_PATH",
|
||||
"DENO_PATH",
|
||||
"GO_PATH",
|
||||
"PIP_INDEX_URL",
|
||||
"PIP_EXTRA_INDEX_URL",
|
||||
"PIP_TRUSTED_HOST",
|
||||
"PATH",
|
||||
"HOME",
|
||||
"DATABASE_CONNECTIONS",
|
||||
"TIMEOUT_WAIT_RESULT",
|
||||
"QUEUE_LIMIT_WAIT_RESULT",
|
||||
"DENO_AUTH_TOKENS",
|
||||
"DENO_FLAGS",
|
||||
"PIP_LOCAL_DEPENDENCIES",
|
||||
"ADDITIONAL_PYTHON_PATHS",
|
||||
"INCLUDE_HEADERS",
|
||||
"WHITELIST_WORKSPACES",
|
||||
"BLACKLIST_WORKSPACES",
|
||||
"NEW_USER_WEBHOOK",
|
||||
"CLOUD_HOSTED",
|
||||
]);
|
||||
|
||||
run_workers(
|
||||
db.clone(),
|
||||
rx.resubscribe(),
|
||||
|
||||
@@ -836,7 +836,6 @@ impl RunJob {
|
||||
/* scheduled_for_o */ None,
|
||||
/* schedule_path */ None,
|
||||
/* parent_job */ None,
|
||||
/* root job */ None,
|
||||
/* is_flow_step */ false,
|
||||
/* running */ false,
|
||||
None,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.78.0
|
||||
version: 1.70.1
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -204,6 +204,7 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
|
||||
/w/{workspace}/users/is_owner/{path}:
|
||||
get:
|
||||
summary: is owner of path
|
||||
@@ -1548,7 +1549,7 @@ paths:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
value: {}
|
||||
@@ -2102,6 +2103,7 @@ paths:
|
||||
items:
|
||||
type: string
|
||||
|
||||
|
||||
/w/{workspace}/scripts/create:
|
||||
post:
|
||||
summary: create script
|
||||
@@ -2276,7 +2278,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)
|
||||
operationId: deleteScriptByHash
|
||||
tags:
|
||||
- script
|
||||
@@ -2291,23 +2293,6 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Script"
|
||||
|
||||
/w/{workspace}/scripts/delete/p/{path}:
|
||||
post:
|
||||
summary: delete all scripts at a given path (require admin)
|
||||
operationId: deleteScriptByPath
|
||||
tags:
|
||||
- script
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/ScriptPath"
|
||||
responses:
|
||||
"200":
|
||||
description: script path
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/scripts/get/p/{path}:
|
||||
get:
|
||||
summary: get script by path
|
||||
@@ -2553,6 +2538,11 @@ paths:
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: skip_direct
|
||||
description: Skip checking that the node is part of the given flow.
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
responses:
|
||||
"200":
|
||||
description: job result
|
||||
@@ -2833,6 +2823,7 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AppWithLastVersion"
|
||||
|
||||
|
||||
/w/{workspace}/apps/secret_of/{path}:
|
||||
get:
|
||||
summary: get public secret of app
|
||||
@@ -3165,14 +3156,12 @@ paths:
|
||||
- $ref: "#/components/parameters/ScriptExactPath"
|
||||
- $ref: "#/components/parameters/ScriptStartPath"
|
||||
- $ref: "#/components/parameters/ScriptExactHash"
|
||||
- $ref: "#/components/parameters/StartedBefore"
|
||||
- $ref: "#/components/parameters/StartedAfter"
|
||||
- $ref: "#/components/parameters/CreatedBefore"
|
||||
- $ref: "#/components/parameters/CreatedAfter"
|
||||
- $ref: "#/components/parameters/Success"
|
||||
- $ref: "#/components/parameters/JobKinds"
|
||||
- $ref: "#/components/parameters/Suspended"
|
||||
- $ref: "#/components/parameters/Running"
|
||||
- $ref: "#/components/parameters/ArgsFilter"
|
||||
- $ref: "#/components/parameters/ResultFilter"
|
||||
responses:
|
||||
"200":
|
||||
description: All available queued jobs
|
||||
@@ -3197,12 +3186,10 @@ paths:
|
||||
- $ref: "#/components/parameters/ScriptExactPath"
|
||||
- $ref: "#/components/parameters/ScriptStartPath"
|
||||
- $ref: "#/components/parameters/ScriptExactHash"
|
||||
- $ref: "#/components/parameters/StartedBefore"
|
||||
- $ref: "#/components/parameters/StartedAfter"
|
||||
- $ref: "#/components/parameters/CreatedBefore"
|
||||
- $ref: "#/components/parameters/CreatedAfter"
|
||||
- $ref: "#/components/parameters/Success"
|
||||
- $ref: "#/components/parameters/JobKinds"
|
||||
- $ref: "#/components/parameters/ArgsFilter"
|
||||
- $ref: "#/components/parameters/ResultFilter"
|
||||
- name: is_skipped
|
||||
description: is the job skipped
|
||||
in: query
|
||||
@@ -3236,11 +3223,9 @@ paths:
|
||||
- $ref: "#/components/parameters/ScriptExactPath"
|
||||
- $ref: "#/components/parameters/ScriptStartPath"
|
||||
- $ref: "#/components/parameters/ScriptExactHash"
|
||||
- $ref: "#/components/parameters/StartedBefore"
|
||||
- $ref: "#/components/parameters/StartedAfter"
|
||||
- $ref: "#/components/parameters/CreatedBefore"
|
||||
- $ref: "#/components/parameters/CreatedAfter"
|
||||
- $ref: "#/components/parameters/JobKinds"
|
||||
- $ref: "#/components/parameters/ArgsFilter"
|
||||
- $ref: "#/components/parameters/ResultFilter"
|
||||
- name: is_skipped
|
||||
description: is the job skipped
|
||||
in: query
|
||||
@@ -3352,22 +3337,6 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CompletedJob"
|
||||
|
||||
/w/{workspace}/jobs/completed/get_result/{id}:
|
||||
get:
|
||||
summary: get completed job result
|
||||
operationId: getCompletedJobResult
|
||||
tags:
|
||||
- job
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/JobId"
|
||||
responses:
|
||||
"200":
|
||||
description: result
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
|
||||
/w/{workspace}/jobs/completed/delete/{id}:
|
||||
post:
|
||||
summary: delete completed job (erase content but keep run id)
|
||||
@@ -3441,7 +3410,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
|
||||
@@ -3467,7 +3436,7 @@ paths:
|
||||
properties:
|
||||
approvalPage:
|
||||
type: string
|
||||
resume:
|
||||
resume:
|
||||
type: string
|
||||
cancel:
|
||||
type: string
|
||||
@@ -4112,7 +4081,7 @@ paths:
|
||||
type: string
|
||||
owners:
|
||||
type: array
|
||||
items:
|
||||
items:
|
||||
type: string
|
||||
extra_perms:
|
||||
additionalProperties:
|
||||
@@ -4146,7 +4115,7 @@ paths:
|
||||
properties:
|
||||
owners:
|
||||
type: array
|
||||
items:
|
||||
items:
|
||||
type: string
|
||||
extra_perms:
|
||||
additionalProperties:
|
||||
@@ -4317,8 +4286,7 @@ paths:
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
[script, group_, resource, schedule, variable, flow, folder, app]
|
||||
enum: [script, group_, resource, schedule, variable, flow, folder, app]
|
||||
responses:
|
||||
"200":
|
||||
description: acls
|
||||
@@ -4343,8 +4311,7 @@ paths:
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
[script, group_, resource, schedule, variable, flow, folder, app]
|
||||
enum: [script, group_, resource, schedule, variable, flow, folder, app]
|
||||
requestBody:
|
||||
description: acl to add
|
||||
required: true
|
||||
@@ -4380,8 +4347,7 @@ paths:
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
[script, group_, resource, schedule, variable, flow, folder, app]
|
||||
enum: [script, group_, resource, schedule, variable, flow, folder, app]
|
||||
requestBody:
|
||||
description: acl to add
|
||||
required: true
|
||||
@@ -4401,18 +4367,19 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
|
||||
/w/{workspace}/capture_u/{path}:
|
||||
post:
|
||||
summary: update flow preview capture
|
||||
operationId: updateCapture
|
||||
tags:
|
||||
- capture
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/Path"
|
||||
responses:
|
||||
"204":
|
||||
description: flow preview captured
|
||||
post:
|
||||
summary: update flow preview capture
|
||||
operationId: updateCapture
|
||||
tags:
|
||||
- capture
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/Path"
|
||||
responses:
|
||||
"204":
|
||||
description: flow preview captured
|
||||
|
||||
/w/{workspace}/capture/{path}:
|
||||
put:
|
||||
@@ -4619,15 +4586,15 @@ components:
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
StartedBefore:
|
||||
name: started_before
|
||||
CreatedBefore:
|
||||
name: created_before
|
||||
description: filter on created before (inclusive) timestamp
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
StartedAfter:
|
||||
name: started_after
|
||||
CreatedAfter:
|
||||
name: created_after
|
||||
description: filter on created after (exclusive) timestamp
|
||||
in: query
|
||||
schema:
|
||||
@@ -4651,19 +4618,6 @@ components:
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
ArgsFilter:
|
||||
name: args
|
||||
description: filter on jobs containing those args as a json subset (@> in postgres)
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
|
||||
ResultFilter:
|
||||
name: result
|
||||
description: filter on jobs containing those result as a json subset (@> in postgres)
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
After:
|
||||
name: after
|
||||
description: filter on created after (exclusive) timestamp
|
||||
@@ -5568,8 +5522,9 @@ components:
|
||||
type: string
|
||||
worker_instance:
|
||||
type: string
|
||||
last_ping:
|
||||
type: number
|
||||
ping_at:
|
||||
type: string
|
||||
format: date-time
|
||||
started_at:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
@@ -313,13 +313,10 @@ async fn create_app(
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(mut app): Json<CreateApp>,
|
||||
Json(app): Json<CreateApp>,
|
||||
) -> Result<(StatusCode, String)> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
app.policy.on_behalf_of = Some(username_to_permissioned_as(&authed.username));
|
||||
app.policy.on_behalf_of_email = Some(authed.email);
|
||||
|
||||
let id = sqlx::query_scalar!(
|
||||
"INSERT INTO app
|
||||
(workspace_id, path, summary, policy, versions)
|
||||
@@ -466,9 +463,7 @@ async fn update_app(
|
||||
sqlb.set_str("summary", nsummary);
|
||||
}
|
||||
|
||||
if let Some(mut npolicy) = ns.policy {
|
||||
npolicy.on_behalf_of = Some(username_to_permissioned_as(&authed.username));
|
||||
npolicy.on_behalf_of_email = Some(authed.email);
|
||||
if let Some(npolicy) = ns.policy {
|
||||
sqlb.set(
|
||||
"policy",
|
||||
&format!(
|
||||
@@ -675,7 +670,6 @@ async fn execute_component(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
@@ -733,9 +727,6 @@ fn build_args(
|
||||
path: String,
|
||||
args: &Map<String, Value>,
|
||||
) -> Result<Map<String, Value>> {
|
||||
// disallow var and res access in args coming from the user for security reasons
|
||||
args.into_iter()
|
||||
.try_for_each(|x| disallow_var_res_access(x.1))?;
|
||||
let static_args = policy
|
||||
.triggerables
|
||||
.get(&path)
|
||||
@@ -756,20 +747,3 @@ fn build_args(
|
||||
}
|
||||
Ok(args)
|
||||
}
|
||||
|
||||
fn disallow_var_res_access(args: &serde_json::Value) -> Result<()> {
|
||||
match args {
|
||||
Value::Object(v) => v.into_iter().try_for_each(|x| disallow_var_res_access(x.1)),
|
||||
Value::Array(arr) => arr.into_iter().try_for_each(|v| disallow_var_res_access(v)),
|
||||
Value::String(s) => {
|
||||
if s.starts_with("$var:") || s.starts_with("$res:") {
|
||||
Err(Error::BadRequest(format!(
|
||||
"For security reasons, variable or resource access is not allowed as dynamic argument"
|
||||
)))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,11 @@
|
||||
*/
|
||||
|
||||
use axum::{
|
||||
extract::{Extension, Path, Query},
|
||||
extract::{Extension, Path},
|
||||
routing::{get, post, put},
|
||||
Json, Router,
|
||||
};
|
||||
use hyper::{HeaderMap, StatusCode};
|
||||
use serde::Deserialize;
|
||||
use hyper::StatusCode;
|
||||
use windmill_common::{
|
||||
error::{JsonResult, Result},
|
||||
utils::{not_found_if_none, StripPath},
|
||||
@@ -20,7 +19,6 @@ use windmill_common::{
|
||||
|
||||
use crate::{
|
||||
db::{UserDB, DB},
|
||||
jobs::add_include_headers,
|
||||
users::Authed,
|
||||
};
|
||||
|
||||
@@ -85,21 +83,13 @@ pub async fn new_payload(
|
||||
Ok(StatusCode::CREATED)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub struct IncludeHeaderQuery {
|
||||
include_header: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn update_payload(
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
Query(run_query): Query<IncludeHeaderQuery>,
|
||||
headers: HeaderMap,
|
||||
Json(args): Json<Option<serde_json::Map<String, serde_json::Value>>>,
|
||||
Json(payload): Json<serde_json::Value>,
|
||||
) -> Result<StatusCode> {
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
let args = add_include_headers(&run_query.include_header, headers, args.unwrap_or_default());
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE capture
|
||||
@@ -109,7 +99,7 @@ pub async fn update_payload(
|
||||
",
|
||||
&w_id,
|
||||
&path.to_path(),
|
||||
serde_json::json!(args),
|
||||
&payload,
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
|
||||
@@ -237,7 +237,6 @@ async fn create_flow(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
@@ -394,7 +393,6 @@ async fn update_flow(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
@@ -640,6 +638,8 @@ mod tests {
|
||||
"type": "script",
|
||||
"path": "test"
|
||||
},
|
||||
"stop_after_if": null,
|
||||
"summary": null
|
||||
},
|
||||
{
|
||||
"id": "b",
|
||||
@@ -648,12 +648,15 @@ mod tests {
|
||||
"input_transforms": {},
|
||||
"type": "rawscript",
|
||||
"content": "test",
|
||||
"lock": null,
|
||||
"path": null,
|
||||
"language": "deno"
|
||||
},
|
||||
"stop_after_if": {
|
||||
"expr": "foo = 'bar'",
|
||||
"skip_if_stopped": false
|
||||
}
|
||||
},
|
||||
"summary": null
|
||||
},
|
||||
{
|
||||
"id": "c",
|
||||
@@ -675,7 +678,8 @@ mod tests {
|
||||
"stop_after_if": {
|
||||
"expr": "previous.isEmpty()",
|
||||
"skip_if_stopped": false,
|
||||
}
|
||||
},
|
||||
"summary": null
|
||||
}
|
||||
],
|
||||
"failure_module": {
|
||||
@@ -689,7 +693,8 @@ mod tests {
|
||||
"stop_after_if": {
|
||||
"expr": "previous.isEmpty()",
|
||||
"skip_if_stopped": false
|
||||
}
|
||||
},
|
||||
"summary": null
|
||||
}
|
||||
});
|
||||
assert_eq!(dbg!(serde_json::json!(fv)), dbg!(expect));
|
||||
|
||||
@@ -16,9 +16,10 @@ use axum::{
|
||||
routing::{delete, get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use windmill_audit::{audit_log, ActionKind};
|
||||
use windmill_common::{
|
||||
error::{self, to_anyhow, Error, JsonResult, Result},
|
||||
error::{self, Error, JsonResult, Result},
|
||||
users::username_to_permissioned_as,
|
||||
utils::{not_found_if_none, paginate, Pagination},
|
||||
};
|
||||
@@ -261,26 +262,13 @@ async fn update_folder(
|
||||
sqlb.and_where_eq("workspace_id", "?".bind(&w_id));
|
||||
|
||||
if let Some(display_name) = ng.display_name {
|
||||
sqlb.set("display_name", "?".bind(&display_name));
|
||||
sqlb.set("display_name", display_name);
|
||||
}
|
||||
if let Some(owners) = ng.owners {
|
||||
sqlb.set(
|
||||
"owners",
|
||||
"?".bind(&format!(
|
||||
"{{{}}}",
|
||||
owners
|
||||
.iter()
|
||||
.map(|x| format!("\"{x}\""))
|
||||
.collect::<Vec<_>>()
|
||||
.join(","),
|
||||
)),
|
||||
);
|
||||
sqlb.set_str("owners", format!("{{{}}}", owners.into_iter().join(",")));
|
||||
}
|
||||
if let Some(extra_perms) = ng.extra_perms {
|
||||
sqlb.set(
|
||||
"extra_perms",
|
||||
"?".bind(&serde_json::to_string(&extra_perms).map_err(to_anyhow)?),
|
||||
);
|
||||
sqlb.set_str("extra_perms", extra_perms.to_string());
|
||||
}
|
||||
|
||||
sqlb.returning("*");
|
||||
|
||||
@@ -102,9 +102,10 @@ pub fn global_service() -> Router {
|
||||
|
||||
async fn get_result_by_id(
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, flow_id, node_id)): Path<(String, Uuid, String)>,
|
||||
Query(ResultByIdQuery { skip_direct }): Query<ResultByIdQuery>,
|
||||
Path((w_id, flow_id, node_id)): Path<(String, String, String)>,
|
||||
) -> windmill_common::error::JsonResult<serde_json::Value> {
|
||||
let res = windmill_queue::get_result_by_id(db, w_id, flow_id, node_id).await?;
|
||||
let res = windmill_queue::get_result_by_id(db, skip_direct, w_id, flow_id, node_id).await?;
|
||||
Ok(Json(res))
|
||||
}
|
||||
|
||||
@@ -176,6 +177,11 @@ async fn get_job(
|
||||
Ok(Json(job))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ResultByIdQuery {
|
||||
pub skip_direct: bool,
|
||||
}
|
||||
|
||||
pub async fn get_job_by_id<'c>(
|
||||
mut tx: Transaction<'c, Postgres>,
|
||||
w_id: &str,
|
||||
@@ -289,51 +295,41 @@ impl RunJobQuery {
|
||||
fn add_include_headers(
|
||||
&self,
|
||||
headers: HeaderMap,
|
||||
args: serde_json::Map<String, serde_json::Value>,
|
||||
mut args: serde_json::Map<String, serde_json::Value>,
|
||||
) -> serde_json::Map<String, serde_json::Value> {
|
||||
return add_include_headers(&self.include_header, headers, args);
|
||||
let whitelist = self
|
||||
.include_header
|
||||
.as_ref()
|
||||
.map(|s| s.split(",").map(|s| s.to_string()).collect::<Vec<_>>())
|
||||
.unwrap_or_default();
|
||||
whitelist
|
||||
.iter()
|
||||
.chain(INCLUDE_HEADERS.iter())
|
||||
.for_each(|h| {
|
||||
if let Some(v) = headers.get(h) {
|
||||
args.insert(
|
||||
h.to_string().to_lowercase().replace('-', "_"),
|
||||
serde_json::Value::String(v.to_str().unwrap().to_string()),
|
||||
);
|
||||
}
|
||||
});
|
||||
args
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_include_headers(
|
||||
include_header: &Option<String>,
|
||||
headers: HeaderMap,
|
||||
mut args: serde_json::Map<String, serde_json::Value>,
|
||||
) -> serde_json::Map<String, serde_json::Value> {
|
||||
let whitelist = include_header
|
||||
.as_ref()
|
||||
.map(|s| s.split(",").map(|s| s.to_string()).collect::<Vec<_>>())
|
||||
.unwrap_or_default();
|
||||
|
||||
whitelist
|
||||
.iter()
|
||||
.chain(INCLUDE_HEADERS.iter())
|
||||
.for_each(|h| {
|
||||
if let Some(v) = headers.get(h) {
|
||||
args.insert(
|
||||
h.to_string().to_lowercase().replace('-', "_"),
|
||||
serde_json::Value::String(v.to_str().unwrap().to_string()),
|
||||
);
|
||||
}
|
||||
});
|
||||
args
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ListQueueQuery {
|
||||
pub script_path_start: Option<String>,
|
||||
pub script_path_exact: Option<String>,
|
||||
pub script_hash: Option<String>,
|
||||
pub created_by: Option<String>,
|
||||
pub started_before: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub started_after: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub created_before: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub created_after: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub running: Option<bool>,
|
||||
pub parent_job: Option<String>,
|
||||
pub order_desc: Option<bool>,
|
||||
pub job_kinds: Option<String>,
|
||||
pub suspended: Option<bool>,
|
||||
// filter by matching a subset of the args using base64 encoded json subset
|
||||
pub args: Option<String>,
|
||||
}
|
||||
|
||||
fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> SqlBuilder {
|
||||
@@ -362,11 +358,11 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq
|
||||
if let Some(pj) = &lq.parent_job {
|
||||
sqlb.and_where_eq("parent_job", "?".bind(pj));
|
||||
}
|
||||
if let Some(dt) = &lq.started_before {
|
||||
sqlb.and_where_le("started_at", format!("to_timestamp({})", dt.timestamp()));
|
||||
if let Some(dt) = &lq.created_before {
|
||||
sqlb.and_where_lt("created_at", format!("to_timestamp({})", dt.timestamp()));
|
||||
}
|
||||
if let Some(dt) = &lq.started_after {
|
||||
sqlb.and_where_ge("started_at", format!("to_timestamp({})", dt.timestamp()));
|
||||
if let Some(dt) = &lq.created_after {
|
||||
sqlb.and_where_gt("created_at", format!("to_timestamp({})", dt.timestamp()));
|
||||
}
|
||||
|
||||
if let Some(s) = &lq.suspended {
|
||||
@@ -376,7 +372,6 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq
|
||||
sqlb.and_where_eq("suspend", 0);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(jk) = &lq.job_kinds {
|
||||
sqlb.and_where_in(
|
||||
"job_kind",
|
||||
@@ -384,10 +379,6 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(args) = &lq.args {
|
||||
sqlb.and_where("args @> ?".bind(&args.replace("'", "''")));
|
||||
}
|
||||
|
||||
sqlb
|
||||
}
|
||||
|
||||
@@ -460,14 +451,13 @@ async fn list_jobs(
|
||||
script_path_exact: lq.script_path_exact,
|
||||
script_hash: lq.script_hash,
|
||||
created_by: lq.created_by,
|
||||
started_before: lq.started_before,
|
||||
started_after: lq.started_after,
|
||||
created_before: lq.created_before,
|
||||
created_after: lq.created_after,
|
||||
running: None,
|
||||
parent_job: lq.parent_job,
|
||||
order_desc: Some(true),
|
||||
job_kinds: lq.job_kinds,
|
||||
suspended: lq.suspended,
|
||||
args: lq.args,
|
||||
},
|
||||
&[
|
||||
"'QueuedJob' as typ",
|
||||
@@ -1078,8 +1068,6 @@ impl From<UnifiedJob> for Job {
|
||||
visible_to_owner: uj.visible_to_owner,
|
||||
suspend: uj.suspend,
|
||||
mem_peak: uj.mem_peak,
|
||||
root_job: None,
|
||||
leaf_jobs: None,
|
||||
}),
|
||||
t => panic!("job type {} not valid", t),
|
||||
}
|
||||
@@ -1142,14 +1130,14 @@ where
|
||||
struct InPayload {
|
||||
payload: Option<String>,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_payload<D: DeserializeOwned, T: AsRef<[u8]>>(t: T) -> anyhow::Result<D> {
|
||||
let vec = base64::engine::general_purpose::URL_SAFE
|
||||
.decode(t)
|
||||
.context("invalid base64")?;
|
||||
serde_json::from_slice(vec.as_slice()).context("invalid json")
|
||||
fn decode_payload<D: DeserializeOwned, T: AsRef<[u8]>>(t: T) -> anyhow::Result<D> {
|
||||
let vec = base64::engine::general_purpose::URL_SAFE
|
||||
.decode(t)
|
||||
.context("invalid base64")?;
|
||||
serde_json::from_slice(vec.as_slice()).context("invalid json")
|
||||
}
|
||||
}
|
||||
}
|
||||
pub async fn run_flow_by_path(
|
||||
authed: Authed,
|
||||
@@ -1175,7 +1163,6 @@ pub async fn run_flow_by_path(
|
||||
scheduled_for,
|
||||
None,
|
||||
run_query.parent_job,
|
||||
run_query.parent_job,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
@@ -1211,7 +1198,6 @@ pub async fn run_job_by_path(
|
||||
scheduled_for,
|
||||
None,
|
||||
run_query.parent_job,
|
||||
run_query.parent_job,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
@@ -1364,7 +1350,6 @@ pub async fn run_wait_result_job_by_path(
|
||||
scheduled_for,
|
||||
None,
|
||||
run_query.parent_job,
|
||||
run_query.parent_job,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
@@ -1411,7 +1396,6 @@ pub async fn run_wait_result_job_by_hash(
|
||||
scheduled_for,
|
||||
None,
|
||||
run_query.parent_job,
|
||||
run_query.parent_job,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
@@ -1457,7 +1441,6 @@ pub async fn run_wait_result_flow_by_path(
|
||||
scheduled_for,
|
||||
None,
|
||||
run_query.parent_job,
|
||||
run_query.parent_job,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
@@ -1520,7 +1503,6 @@ async fn run_preview_job(
|
||||
scheduled_for,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
@@ -1554,7 +1536,6 @@ async fn run_preview_flow_job(
|
||||
scheduled_for,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
@@ -1590,7 +1571,6 @@ pub async fn run_job_by_hash(
|
||||
scheduled_for,
|
||||
None,
|
||||
run_query.parent_job,
|
||||
run_query.parent_job,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
@@ -1693,11 +1673,11 @@ fn list_completed_jobs_query(
|
||||
if let Some(pj) = &lq.parent_job {
|
||||
sqlb.and_where_eq("parent_job", "?".bind(pj));
|
||||
}
|
||||
if let Some(dt) = &lq.started_before {
|
||||
sqlb.and_where_le("started_at", format!("to_timestamp({})", dt.timestamp()));
|
||||
if let Some(dt) = &lq.created_before {
|
||||
sqlb.and_where_lt("created_at", format!("to_timestamp({})", dt.timestamp()));
|
||||
}
|
||||
if let Some(dt) = &lq.started_after {
|
||||
sqlb.and_where_ge("started_at", format!("to_timestamp({})", dt.timestamp()));
|
||||
if let Some(dt) = &lq.created_after {
|
||||
sqlb.and_where_gt("created_at", format!("to_timestamp({})", dt.timestamp()));
|
||||
}
|
||||
if let Some(sk) = &lq.is_skipped {
|
||||
sqlb.and_where_eq("is_skipped", sk);
|
||||
@@ -1712,14 +1692,6 @@ fn list_completed_jobs_query(
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(args) = &lq.args {
|
||||
sqlb.and_where("args @> ?".bind(&args.replace("'", "''")));
|
||||
}
|
||||
|
||||
if let Some(result) = &lq.result {
|
||||
sqlb.and_where("result @> ?".bind(&result.replace("'", "''")));
|
||||
}
|
||||
|
||||
sqlb
|
||||
}
|
||||
#[derive(Deserialize, Clone)]
|
||||
@@ -1728,8 +1700,8 @@ pub struct ListCompletedQuery {
|
||||
pub script_path_exact: Option<String>,
|
||||
pub script_hash: Option<String>,
|
||||
pub created_by: Option<String>,
|
||||
pub started_before: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub started_after: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub created_before: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub created_after: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub success: Option<bool>,
|
||||
pub parent_job: Option<String>,
|
||||
pub order_desc: Option<bool>,
|
||||
@@ -1737,10 +1709,6 @@ pub struct ListCompletedQuery {
|
||||
pub is_skipped: Option<bool>,
|
||||
pub is_flow_step: Option<bool>,
|
||||
pub suspended: Option<bool>,
|
||||
// filter by matching a subset of the args using base64 encoded json subset
|
||||
pub args: Option<String>,
|
||||
// filter by matching a subset of the result using base64 encoded json subset
|
||||
pub result: Option<String>,
|
||||
}
|
||||
|
||||
async fn list_completed_jobs(
|
||||
@@ -1807,7 +1775,6 @@ async fn get_completed_job(
|
||||
.fetch_optional(&db)
|
||||
.await?;
|
||||
|
||||
tracing::info!("job_o: {:?}", job_o);
|
||||
let job = not_found_if_none(job_o, "Completed Job", id.to_string())?;
|
||||
Ok(Json(job))
|
||||
}
|
||||
|
||||
@@ -734,10 +734,14 @@ async fn slack_command(
|
||||
.map_err(|_| error::Error::BadRequest("invalid payload".to_string()))?;
|
||||
|
||||
let body = String::from_utf8_lossy(&body);
|
||||
if let Some(sv) = SLACK_SIGNING_SECRET.as_ref() {
|
||||
if sv.verify(&ts, &body, &sig).ok().is_none() {
|
||||
return Err(error::Error::BadRequest("verification failed".to_owned()));
|
||||
}
|
||||
if SLACK_SIGNING_SECRET
|
||||
.as_ref()
|
||||
.as_ref()
|
||||
.map(|sv| sv.verify(&ts, &body, &sig).ok())
|
||||
.flatten()
|
||||
.is_none()
|
||||
{
|
||||
return Err(error::Error::BadRequest("verification failed".to_owned()));
|
||||
}
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
@@ -781,7 +785,6 @@ async fn slack_command(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
|
||||
@@ -70,7 +70,6 @@ pub fn workspaced_service() -> Router {
|
||||
.route("/exists/p/*path", get(exists_script_by_path))
|
||||
.route("/archive/h/:hash", post(archive_script_by_hash))
|
||||
.route("/delete/h/:hash", post(delete_script_by_hash))
|
||||
.route("/delete/p/*path", post(delete_script_by_path))
|
||||
.route("/get/h/:hash", get(get_script_by_hash))
|
||||
.route("/raw/h/:hash", get(raw_script_by_hash))
|
||||
.route("/deployment_status/h/:hash", get(get_deployment_status))
|
||||
@@ -124,7 +123,6 @@ async fn list_scripts(
|
||||
AND workspace_id = ?)"
|
||||
.bind(&w_id),
|
||||
);
|
||||
sqlb.and_where_eq("archived", true);
|
||||
} else {
|
||||
sqlb.and_where_eq("archived", false);
|
||||
}
|
||||
@@ -379,7 +377,6 @@ async fn create_script(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
@@ -721,46 +718,6 @@ async fn delete_script_by_hash(
|
||||
Ok(Json(script))
|
||||
}
|
||||
|
||||
async fn delete_script_by_path(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(webhook): Extension<WebhookShared>,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> JsonResult<String> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let path = path.to_path();
|
||||
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
let script = sqlx::query_scalar!(
|
||||
"DELETE FROM script WHERE path = $1 AND workspace_id = $2 RETURNING path",
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await
|
||||
.map_err(|e| Error::InternalErr(format!("deleting script by path {w_id}: {e}")))?;
|
||||
|
||||
audit_log(
|
||||
&mut tx,
|
||||
&authed.username,
|
||||
"scripts.delete",
|
||||
ActionKind::Delete,
|
||||
&w_id,
|
||||
Some(&path),
|
||||
Some([("workspace", w_id.as_str())].into()),
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
webhook.send_message(
|
||||
w_id.clone(),
|
||||
WebhookMessage::DeleteScriptPath { workspace: w_id, path: path.to_string() },
|
||||
);
|
||||
|
||||
Ok(Json(script))
|
||||
}
|
||||
|
||||
async fn parse_python_code_to_jsonschema(
|
||||
Json(code): Json<String>,
|
||||
) -> JsonResult<windmill_parser::MainArgSignature> {
|
||||
|
||||
@@ -1311,9 +1311,7 @@ async fn create_user(
|
||||
if let Some(new_user_webhook) = NEW_USER_WEBHOOK.clone() {
|
||||
let _ = HTTP_CLIENT
|
||||
.post(&new_user_webhook)
|
||||
.json(
|
||||
&serde_json::json!({"email" : &nu.email, "name": &nu.name, "event": "global_add"}),
|
||||
)
|
||||
.json(&serde_json::json!({"email" : &nu.email, "name": &nu.name, "event": "new_user"}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| tracing::error!("Error sending new user webhook: {}", e.to_string()));
|
||||
|
||||
@@ -26,7 +26,6 @@ use axum::{
|
||||
Json, Router,
|
||||
};
|
||||
use hyper::StatusCode;
|
||||
use serde_json::Value;
|
||||
use windmill_audit::{audit_log, ActionKind};
|
||||
use windmill_common::{
|
||||
error::{Error, JsonResult, Result},
|
||||
@@ -379,27 +378,9 @@ async fn update_variable(
|
||||
if !authed.is_admin {
|
||||
require_owner_of_path(&w_id, &authed.username, &authed.groups, &path, &db).await?;
|
||||
}
|
||||
let mut v = sqlx::query_scalar!(
|
||||
"SELECT value FROM resource WHERE path = $1 AND workspace_id = $2",
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
.fetch_optional(&mut tx)
|
||||
.await?
|
||||
.flatten();
|
||||
|
||||
if let Some(old_v) = v {
|
||||
v = Some(replace_path(
|
||||
old_v,
|
||||
&format!("$var:{path}"),
|
||||
&format!("$var:{npath}"),
|
||||
))
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE resource SET path = $1, value = $2 WHERE path = $3 AND workspace_id = $4",
|
||||
"UPDATE resource SET path = $1 WHERE path = $2 AND workspace_id = $3",
|
||||
npath,
|
||||
v,
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
@@ -438,23 +419,6 @@ async fn update_variable(
|
||||
Ok(format!("variable {} updated (npath: {:?})", path, npath))
|
||||
}
|
||||
|
||||
fn replace_path(v: serde_json::Value, path: &str, npath: &str) -> Value {
|
||||
match v {
|
||||
Value::Object(v) => Value::Object(
|
||||
v.into_iter()
|
||||
.map(|(k, v)| (k, replace_path(v, path, npath)))
|
||||
.collect(),
|
||||
),
|
||||
Value::Array(arr) => Value::Array(
|
||||
arr.into_iter()
|
||||
.map(|v| replace_path(v, path, npath))
|
||||
.collect(),
|
||||
),
|
||||
Value::String(s) if s == path => Value::String(npath.to_owned()),
|
||||
_ => v,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn build_crypt<'c>(
|
||||
db: &mut Transaction<'c, Postgres>,
|
||||
w_id: &str,
|
||||
|
||||
@@ -37,7 +37,6 @@ pub enum WebhookMessage {
|
||||
CreateScript { workspace: String, path: String, hash: String },
|
||||
UpdateScript { workspace: String, path: String, hash: String },
|
||||
DeleteScript { workspace: String, hash: String },
|
||||
DeleteScriptPath { workspace: String, path: String },
|
||||
CreateVariable { workspace: String, path: String },
|
||||
UpdateVariable { workspace: String, old_path: String, new_path: String },
|
||||
DeleteVariable { workspace: String, path: String },
|
||||
|
||||
@@ -28,7 +28,7 @@ pub fn global_service() -> Router {
|
||||
struct WorkerPing {
|
||||
worker: String,
|
||||
worker_instance: String,
|
||||
last_ping: Option<i32>,
|
||||
ping_at: chrono::DateTime<chrono::Utc>,
|
||||
started_at: chrono::DateTime<chrono::Utc>,
|
||||
ip: String,
|
||||
jobs_executed: i32,
|
||||
@@ -45,7 +45,7 @@ async fn list_worker_pings(
|
||||
|
||||
let rows = sqlx::query_as!(
|
||||
WorkerPing,
|
||||
"SELECT worker, worker_instance, EXTRACT(EPOCH FROM (now() - ping_at))::integer as last_ping, started_at, ip, jobs_executed FROM worker_ping ORDER BY ping_at desc LIMIT $1 OFFSET $2",
|
||||
"SELECT * FROM worker_ping ORDER BY ping_at desc LIMIT $1 OFFSET $2",
|
||||
per_page as i64,
|
||||
offset as i64
|
||||
)
|
||||
|
||||
@@ -965,7 +965,7 @@ async fn invite_user(
|
||||
if let Some(new_user_webhook) = NEW_USER_WEBHOOK.clone() {
|
||||
let _ = &HTTP_CLIENT
|
||||
.post(&new_user_webhook)
|
||||
.json(&serde_json::json!({"email" : &nu.email, "event": "workspace_invite"}))
|
||||
.json(&serde_json::json!({"email" : &nu.email, "event": "new_invite"}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| tracing::error!("Error sending new user webhook: {}", e.to_string()));
|
||||
@@ -1002,15 +1002,6 @@ async fn add_user(
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
if let Some(new_user_webhook) = NEW_USER_WEBHOOK.clone() {
|
||||
let _ = HTTP_CLIENT
|
||||
.post(&new_user_webhook)
|
||||
.json(&serde_json::json!({"email" : &nu.email, "event": "workspace_add"}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| tracing::error!("Error sending new user webhook: {}", e.to_string()));
|
||||
}
|
||||
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
format!("user with email {} added", nu.email),
|
||||
@@ -1073,7 +1064,6 @@ struct ScriptMetadata {
|
||||
schema: Option<Schema>,
|
||||
is_template: bool,
|
||||
lock: Vec<String>,
|
||||
kind: String,
|
||||
}
|
||||
|
||||
enum ArchiveImpl {
|
||||
@@ -1236,7 +1226,6 @@ async fn tarball_workspace(
|
||||
description: script.description,
|
||||
schema: script.schema,
|
||||
is_template: script.is_template,
|
||||
kind: script.kind.to_string(),
|
||||
lock,
|
||||
};
|
||||
let metadata_str = serde_json::to_string_pretty(&metadata).unwrap();
|
||||
|
||||
@@ -125,7 +125,7 @@ pub enum FlowStatusModule {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum JobResult {
|
||||
SingleJob(Uuid),
|
||||
ListJob(Vec<Uuid>),
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
use std::{
|
||||
fmt::{self, Display},
|
||||
fmt::Display,
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
|
||||
@@ -103,18 +103,6 @@ pub enum ScriptKind {
|
||||
Approval,
|
||||
}
|
||||
|
||||
impl Display for ScriptKind {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
fmt.write_str(match self {
|
||||
ScriptKind::Trigger => "trigger",
|
||||
ScriptKind::Failure => "failure",
|
||||
ScriptKind::Script => "script",
|
||||
ScriptKind::Approval => "approval",
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
|
||||
pub struct Script {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
use anyhow::Context;
|
||||
use reqwest::Client;
|
||||
@@ -152,24 +152,55 @@ pub async fn pull(
|
||||
|
||||
pub async fn get_result_by_id(
|
||||
db: Pool<Postgres>,
|
||||
mut skip_direct: bool,
|
||||
w_id: String,
|
||||
flow_id: Uuid,
|
||||
flow_id: String,
|
||||
node_id: String,
|
||||
) -> error::Result<serde_json::Value> {
|
||||
let job_result: Option<JobResult> = sqlx::query_scalar!(
|
||||
"SELECT leaf_jobs->$1::text FROM queue WHERE COALESCE((SELECT root_job FROM queue WHERE id = $2), $2) = id AND workspace_id = $3",
|
||||
node_id,
|
||||
flow_id,
|
||||
w_id,
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?
|
||||
.flatten()
|
||||
.map(|x| serde_json::from_value(x).ok())
|
||||
.flatten();
|
||||
|
||||
let mut result_id: Option<JobResult> = None;
|
||||
let mut parent_id = Uuid::from_str(&flow_id).ok();
|
||||
while result_id.is_none() && parent_id.is_some() {
|
||||
if !skip_direct {
|
||||
let r = sqlx::query!(
|
||||
"SELECT flow_status, parent_job FROM completed_job WHERE id = $1 AND workspace_id = $2 UNION ALL SELECT flow_status, parent_job FROM queue WHERE id = $1 AND workspace_id = $2 ",
|
||||
parent_id.unwrap(),
|
||||
w_id,
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?;
|
||||
if let Some(r) = r {
|
||||
let value = r
|
||||
.flow_status
|
||||
.as_ref()
|
||||
.ok_or_else(|| Error::InternalErr(format!("requiring a flow status value")))?
|
||||
.to_owned();
|
||||
parent_id = r.parent_job;
|
||||
let status_o = serde_json::from_value::<FlowStatus>(value).ok();
|
||||
result_id = status_o.and_then(|status| {
|
||||
status
|
||||
.modules
|
||||
.iter()
|
||||
.find(|m| m.id() == node_id)
|
||||
.and_then(|m| m.job_result())
|
||||
});
|
||||
} else {
|
||||
parent_id = None;
|
||||
}
|
||||
} else {
|
||||
let q_parent = sqlx::query_scalar!(
|
||||
"SELECT parent_job FROM completed_job WHERE id = $1 AND workspace_id = $2 UNION ALL SELECT parent_job FROM queue WHERE id = $1 AND workspace_id = $2",
|
||||
parent_id.unwrap(),
|
||||
w_id,
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?
|
||||
.flatten();
|
||||
parent_id = q_parent;
|
||||
skip_direct = false
|
||||
}
|
||||
}
|
||||
let result_id = windmill_common::utils::not_found_if_none(
|
||||
job_result,
|
||||
result_id,
|
||||
"Flow result by id",
|
||||
format!("{}, {}", flow_id, node_id),
|
||||
)?;
|
||||
@@ -251,7 +282,6 @@ pub async fn push<'c>(
|
||||
scheduled_for_o: Option<chrono::DateTime<chrono::Utc>>,
|
||||
schedule_path: Option<String>,
|
||||
parent_job: Option<Uuid>,
|
||||
root_job: Option<Uuid>,
|
||||
is_flow_step: bool,
|
||||
mut same_worker: bool,
|
||||
pre_run_error: Option<&windmill_common::error::Error>,
|
||||
@@ -504,13 +534,12 @@ pub async fn push<'c>(
|
||||
.unwrap_or_else(|| (None, None));
|
||||
|
||||
let flow_status = raw_flow.as_ref().map(FlowStatus::new);
|
||||
|
||||
let uuid = sqlx::query_scalar!(
|
||||
"INSERT INTO queue
|
||||
(workspace_id, id, running, parent_job, created_by, permissioned_as, scheduled_for,
|
||||
script_hash, script_path, raw_code, raw_lock, args, job_kind, schedule_path, raw_flow, \
|
||||
flow_status, is_flow_step, language, started_at, same_worker, pre_run_error, email, visible_to_owner, root_job)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, CASE WHEN $3 THEN now() END, $19, $20, $21, $22, $23) \
|
||||
flow_status, is_flow_step, language, started_at, same_worker, pre_run_error, email, visible_to_owner)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, CASE WHEN $3 THEN now() END, $19, $20, $21, $22) \
|
||||
RETURNING id",
|
||||
workspace_id,
|
||||
job_id,
|
||||
@@ -533,8 +562,7 @@ pub async fn push<'c>(
|
||||
same_worker,
|
||||
pre_run_error.map(|e| e.to_string()),
|
||||
email,
|
||||
visible_to_owner,
|
||||
root_job
|
||||
visible_to_owner
|
||||
)
|
||||
.fetch_one(&mut tx)
|
||||
.await
|
||||
@@ -647,10 +675,6 @@ pub struct QueuedJob {
|
||||
pub suspend: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mem_peak: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub root_job: Option<Uuid>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub leaf_jobs: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl QueuedJob {
|
||||
|
||||
@@ -85,7 +85,6 @@ pub async fn push_scheduled_job<'c>(
|
||||
Some(next),
|
||||
Some(schedule.path.clone()),
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
|
||||
@@ -40,7 +40,7 @@ pub async fn eval_timeout(
|
||||
let (sender, mut receiver) = oneshot::channel::<IsolateHandle>();
|
||||
let base_internal_url: String = base_internal_url.to_string();
|
||||
timeout(
|
||||
std::time::Duration::from_millis(3000),
|
||||
std::time::Duration::from_millis(2000),
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let mut ops = vec![];
|
||||
|
||||
@@ -104,7 +104,7 @@ pub async fn eval_timeout(
|
||||
isolate.terminate_execution();
|
||||
};
|
||||
Error::ExecutionErr(format!(
|
||||
"The expression of evaluation `{expr2}` took too long to execute (>3000ms)"
|
||||
"The expression of evaluation `{expr2}` took too long to execute (>2000ms)"
|
||||
))
|
||||
})??
|
||||
}
|
||||
@@ -145,7 +145,7 @@ fn add_closing_bracket(s: &str) -> String {
|
||||
s
|
||||
}
|
||||
|
||||
const SPLIT_PAT: &str = ";";
|
||||
const SPLIT_PAT: &str = ";\n";
|
||||
async fn eval(
|
||||
context: &mut JsRuntime,
|
||||
expr: &str,
|
||||
@@ -154,21 +154,14 @@ async fn eval(
|
||||
by_id: Option<IdContext>,
|
||||
base_internal_url: &str,
|
||||
) -> anyhow::Result<serde_json::Value> {
|
||||
let exprs = expr
|
||||
.trim()
|
||||
.split(SPLIT_PAT)
|
||||
.map(|x| x.trim())
|
||||
.filter(|x| !x.is_empty())
|
||||
.collect::<Vec<&str>>();
|
||||
let expr = if exprs.is_empty() {
|
||||
"return undefined;".to_string()
|
||||
} else {
|
||||
format!(
|
||||
"{};\n return {};",
|
||||
exprs.iter().take(exprs.len() - 1).join(";\n"),
|
||||
exprs.last().unwrap()
|
||||
)
|
||||
};
|
||||
let expr = expr.trim();
|
||||
let expr = format!(
|
||||
"{}\nreturn {};",
|
||||
expr.split(SPLIT_PAT)
|
||||
.take(expr.split(SPLIT_PAT).count() - 1)
|
||||
.join("\n"),
|
||||
expr.split(SPLIT_PAT).last().unwrap_or_else(|| "")
|
||||
);
|
||||
let (api_code, by_id_code) = if let Some(EvalCreds { workspace, token }) = creds {
|
||||
let by_id_code = if let Some(by_id) = by_id {
|
||||
format!(
|
||||
@@ -205,12 +198,12 @@ const results = new Proxy({{}}, {{
|
||||
.into_iter()
|
||||
.map(|(k, v)| {
|
||||
let v_str = match v {
|
||||
JobResult::SingleJob(x) => format!("\"{x}\""),
|
||||
JobResult::SingleJob(x) => x.to_string(),
|
||||
JobResult::ListJob(x) => {
|
||||
format!("[{}]", x.iter().map(|x| format!("\"{x}\"")).join(","))
|
||||
format!("[{}]", x.iter().map(|x| x.to_string()).join(","))
|
||||
}
|
||||
};
|
||||
format!("\"{k}\": {v_str}")
|
||||
format!("\"{k}\": \"{v_str}\"")
|
||||
})
|
||||
.join(","),
|
||||
by_id.previous_id,
|
||||
@@ -299,8 +292,9 @@ async fn op_get_result(args: Vec<String>) -> Result<serde_json::Value, anyhow::E
|
||||
let base_url = &args[3];
|
||||
let client = windmill_api_client::create_client(base_url, token.clone());
|
||||
let result = client
|
||||
.get_completed_job_result(workspace, &id.parse()?)
|
||||
.get_completed_job(workspace, &id.parse()?)
|
||||
.await?
|
||||
.result
|
||||
.clone();
|
||||
Ok(serde_json::json!(result))
|
||||
}
|
||||
@@ -315,7 +309,7 @@ async fn op_get_id(args: Vec<String>) -> Result<Option<serde_json::Value>, anyho
|
||||
|
||||
let client = windmill_api_client::create_client(base_url, token.clone());
|
||||
let result = client
|
||||
.result_by_id(workspace, flow_job_id, node_id)
|
||||
.result_by_id(workspace, flow_job_id, node_id, Some(true))
|
||||
.await
|
||||
.map_or(None, |e| Some(e.into_inner()));
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ use tokio::{
|
||||
io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader},
|
||||
process::{Child, Command},
|
||||
sync::{
|
||||
mpsc::{self, Sender}, watch, broadcast,
|
||||
mpsc::{self, Sender}, watch,
|
||||
},
|
||||
time::{interval, sleep, Instant, MissedTickBehavior},
|
||||
};
|
||||
@@ -236,7 +236,8 @@ pub async fn create_token_for_owner<'c>(
|
||||
) -> error::Result<(Transaction<'c, Postgres>, String)> {
|
||||
// TODO: Bad implementation. We should not have access to this DB here.
|
||||
let token: String = rd_string(30);
|
||||
let is_super_admin = sqlx::query_scalar!("SELECT super_admin FROM password WHERE email = $1", email)
|
||||
let is_super_admin = owner.contains('@')
|
||||
&& sqlx::query_scalar!("SELECT super_admin FROM password WHERE email = $1", owner)
|
||||
.fetch_optional(&mut tx)
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
@@ -263,7 +264,7 @@ const ROOT_CACHE_DIR: &str = "/tmp/windmill/cache/";
|
||||
const PIP_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "pip");
|
||||
const DENO_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "deno");
|
||||
const GO_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "go");
|
||||
const NUM_SECS_PING: u64 = 5;
|
||||
const NUM_SECS_ENV_CHECK: u64 = 15;
|
||||
const NUM_SECS_SYNC: u64 = 60 * 10;
|
||||
|
||||
const INCLUDE_DEPS_PY_SH_CONTENT: &str = include_str!("../nsjail/download_deps.py.sh");
|
||||
@@ -329,7 +330,7 @@ lazy_static::lazy_static! {
|
||||
.ok()
|
||||
.map(|x| format!(";{x}"))
|
||||
.unwrap_or_else(|| String::new());
|
||||
static ref NPM_CONFIG_REGISTRY: Option<String> = std::env::var("NPM_CONFIG_REGISTRY").ok();
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -477,7 +478,7 @@ pub async fn run_worker(
|
||||
|
||||
|
||||
let worker_sleep_duration_counter = prometheus::register_counter!(prometheus::opts!(
|
||||
"worker_sleep_duration_counter",
|
||||
"worker_execution_sleep_counter",
|
||||
"Total number of seconds spent sleeping between pulling jobs from the queue"
|
||||
)
|
||||
.const_label("name", &worker_name))
|
||||
@@ -492,7 +493,7 @@ pub async fn run_worker(
|
||||
.expect("register prometheus metric");
|
||||
|
||||
let worker_pull_duration_counter = prometheus::register_counter!(prometheus::opts!(
|
||||
"worker_pull_duration_counter",
|
||||
"worker_pull_sleep_counter",
|
||||
"Total number of seconds spent pulling jobs (if growing large the db is undersized)"
|
||||
)
|
||||
.const_label("name", &worker_name))
|
||||
@@ -552,7 +553,7 @@ pub async fn run_worker(
|
||||
|
||||
|
||||
let do_break = async {
|
||||
if last_ping.elapsed().as_secs() > NUM_SECS_PING {
|
||||
if last_ping.elapsed().as_secs() > NUM_SECS_ENV_CHECK {
|
||||
sqlx::query!(
|
||||
"UPDATE worker_ping SET ping_at = now(), jobs_executed = $1 WHERE worker = $2",
|
||||
jobs_executed,
|
||||
@@ -839,7 +840,7 @@ async fn handle_queued_job(
|
||||
job_dir: &str,
|
||||
metrics: Metrics,
|
||||
same_worker_tx: Sender<Uuid>,
|
||||
base_internal_url: &str,
|
||||
base_internal_url: &str
|
||||
) -> windmill_common::error::Result<()> {
|
||||
if job.canceled {
|
||||
return Err(Error::JsonErr(canceled_job_to_result(&job)))?;
|
||||
@@ -887,10 +888,10 @@ async fn handle_queued_job(
|
||||
logs.push_str(&format!("job {} on worker {}\n", &job.id, &worker_name));
|
||||
let result = match job.job_kind {
|
||||
JobKind::Dependencies => {
|
||||
handle_dependency_job(&job, &mut logs, job_dir, db, worker_name).await
|
||||
handle_dependency_job(&job, &mut logs, job_dir, db).await
|
||||
}
|
||||
JobKind::FlowDependencies => {
|
||||
handle_flow_dependency_job(&job, &mut logs, job_dir, db, worker_name)
|
||||
handle_flow_dependency_job(&job, &mut logs, job_dir, db)
|
||||
.await
|
||||
.map(|()| Value::Null)
|
||||
}
|
||||
@@ -911,8 +912,7 @@ async fn handle_queued_job(
|
||||
job_dir,
|
||||
worker_dir,
|
||||
&mut logs,
|
||||
base_internal_url,
|
||||
worker_name
|
||||
base_internal_url
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -1064,9 +1064,7 @@ async fn handle_code_execution_job(
|
||||
job_dir: &str,
|
||||
worker_dir: &str,
|
||||
logs: &mut String,
|
||||
base_internal_url: &str,
|
||||
worker_name: &str
|
||||
|
||||
base_internal_url: &str
|
||||
) -> error::Result<serde_json::Value> {
|
||||
let (inner_content, requirements_o, language) = match job.job_kind {
|
||||
JobKind::Preview | JobKind::Script_Hub => (
|
||||
@@ -1088,6 +1086,7 @@ async fn handle_code_execution_job(
|
||||
"handle_code_execution_job should never be reachable with a non-code execution job"
|
||||
),
|
||||
};
|
||||
let worker_name = worker_dir.split("/").last().unwrap_or("unknown");
|
||||
let lang_str = job
|
||||
.language
|
||||
.as_ref()
|
||||
@@ -1137,7 +1136,7 @@ mount {{
|
||||
token,
|
||||
&inner_content,
|
||||
&shared_mount,
|
||||
base_internal_url,
|
||||
base_internal_url
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -1152,8 +1151,7 @@ mount {{
|
||||
&inner_content,
|
||||
&shared_mount,
|
||||
requirements_o,
|
||||
base_internal_url,
|
||||
worker_name
|
||||
base_internal_url
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -1168,8 +1166,7 @@ mount {{
|
||||
job_dir,
|
||||
requirements_o,
|
||||
&shared_mount,
|
||||
base_internal_url,
|
||||
worker_name
|
||||
base_internal_url
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -1182,8 +1179,7 @@ mount {{
|
||||
&inner_content,
|
||||
job_dir,
|
||||
&shared_mount,
|
||||
base_internal_url,
|
||||
worker_name
|
||||
base_internal_url
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -1212,7 +1208,6 @@ async fn handle_go_job(
|
||||
requirements_o: Option<String>,
|
||||
shared_mount: &str,
|
||||
base_internal_url: &str,
|
||||
worker_name: &str,
|
||||
) -> Result<serde_json::Value, Error> {
|
||||
//go does not like executing modules at temp root
|
||||
let job_dir = &format!("{job_dir}/go");
|
||||
@@ -1241,7 +1236,6 @@ async fn handle_go_job(
|
||||
db,
|
||||
true,
|
||||
skip_go_mod,
|
||||
worker_name
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1359,7 +1353,7 @@ func Run(req Req) (interface{{}}, error){{
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
handle_child(&job.id, db, logs, build_go, false, worker_name).await?;
|
||||
handle_child(&job.id, db, logs, build_go, false).await?;
|
||||
|
||||
Command::new(NSJAIL_PATH.as_str())
|
||||
.current_dir(job_dir)
|
||||
@@ -1385,7 +1379,7 @@ func Run(req Req) (interface{{}}, error){{
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?
|
||||
};
|
||||
handle_child(&job.id, db, logs, child, !*DISABLE_NSJAIL, worker_name).await?;
|
||||
handle_child(&job.id, db, logs, child, !*DISABLE_NSJAIL).await?;
|
||||
read_result(job_dir).await
|
||||
}
|
||||
|
||||
@@ -1399,7 +1393,6 @@ async fn handle_bash_job(
|
||||
job_dir: &str,
|
||||
shared_mount: &str,
|
||||
base_internal_url: &str,
|
||||
worker_name: &str,
|
||||
) -> Result<serde_json::Value, Error> {
|
||||
logs.push_str("\n\n--- BASH CODE EXECUTION ---\n");
|
||||
set_logs(logs, &job.id, db).await;
|
||||
@@ -1463,7 +1456,7 @@ async fn handle_bash_job(
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?
|
||||
};
|
||||
handle_child(&job.id, db, logs, child, !*DISABLE_NSJAIL, worker_name).await?;
|
||||
handle_child(&job.id, db, logs, child, !*DISABLE_NSJAIL).await?;
|
||||
//for now bash jobs have an empty result object
|
||||
Ok(serde_json::json!(logs
|
||||
.lines()
|
||||
@@ -1480,26 +1473,6 @@ fn capitalize(s: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_common_deno_proc_envs(token: &str, base_internal_url: &str) -> HashMap<String, String> {
|
||||
let hostname_base = BASE_URL.split("://").last().unwrap_or("localhost");
|
||||
let hostname_internal = base_internal_url.split("://").last().unwrap_or("localhost");
|
||||
let deno_auth_tokens_base = DENO_AUTH_TOKENS.as_str();
|
||||
let deno_auth_tokens =
|
||||
format!("{token}@{hostname_base};{token}@{hostname_internal}{deno_auth_tokens_base}",);
|
||||
|
||||
let mut deno_envs: HashMap<String, String> = HashMap::from([
|
||||
(String::from("PATH"), PATH_ENV.clone()),
|
||||
(String::from("DENO_AUTH_TOKENS"), deno_auth_tokens),
|
||||
(String::from("BASE_INTERNAL_URL"), base_internal_url.to_string()),
|
||||
(String::from("NO_COLOR"), String::from("true")),
|
||||
]);
|
||||
|
||||
if let Some(ref s) = *NPM_CONFIG_REGISTRY {
|
||||
deno_envs.insert(String::from("NPM_CONFIG_REGISTRY"), s.clone());
|
||||
}
|
||||
return deno_envs;
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
async fn handle_deno_job(
|
||||
logs: &mut String,
|
||||
@@ -1511,8 +1484,7 @@ async fn handle_deno_job(
|
||||
inner_content: &String,
|
||||
shared_mount: &str,
|
||||
lockfile: Option<String>,
|
||||
base_internal_url: &str,
|
||||
worker_name: &str
|
||||
base_internal_url: &str
|
||||
) -> error::Result<serde_json::Value> {
|
||||
logs.push_str("\n\n--- DENO CODE EXECUTION ---\n");
|
||||
set_logs(logs, &job.id, db).await;
|
||||
@@ -1565,9 +1537,11 @@ run().catch(async (e) => {{
|
||||
let mut reserved_variables = get_reserved_variables(job, &token, db).await?;
|
||||
reserved_variables.insert("RUST_LOG".to_string(), "info".to_string());
|
||||
|
||||
let common_deno_proc_envs = get_common_deno_proc_envs(&token, base_internal_url);
|
||||
//do not cache local dependencies
|
||||
let reload = format!("--reload={base_internal_url}");
|
||||
let hostname_base = BASE_URL.split("://").last().unwrap_or("localhost");
|
||||
let hostname_internal = base_internal_url.split("://").last().unwrap_or("localhost");
|
||||
let deno_auth_tokens_base = DENO_AUTH_TOKENS.as_str();
|
||||
let deno_auth_tokens =
|
||||
format!("{token}@{hostname_base};{token}@{hostname_internal}{deno_auth_tokens_base}",);
|
||||
let child = async {
|
||||
Ok(if !*DISABLE_NSJAIL {
|
||||
let _ = write_file(
|
||||
@@ -1591,7 +1565,6 @@ run().catch(async (e) => {{
|
||||
}
|
||||
args.push("--import-map");
|
||||
args.push("/tmp/import_map.json");
|
||||
args.push(&reload);
|
||||
args.push("--unstable");
|
||||
if let Some(deno_flags) = DENO_FLAGS.as_ref() {
|
||||
for flag in deno_flags {
|
||||
@@ -1606,7 +1579,9 @@ run().catch(async (e) => {{
|
||||
.current_dir(job_dir)
|
||||
.env_clear()
|
||||
.envs(reserved_variables)
|
||||
.envs(common_deno_proc_envs)
|
||||
.env("PATH", PATH_ENV.as_str())
|
||||
.env("DENO_AUTH_TOKENS", deno_auth_tokens)
|
||||
.env("BASE_INTERNAL_URL", base_internal_url)
|
||||
.args(args)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
@@ -1618,7 +1593,6 @@ run().catch(async (e) => {{
|
||||
args.push("run");
|
||||
args.push("--import-map");
|
||||
args.push(&import_map_path);
|
||||
args.push(&reload);
|
||||
args.push("--unstable");
|
||||
if let Some(deno_flags) = DENO_FLAGS.as_ref() {
|
||||
for flag in deno_flags {
|
||||
@@ -1632,8 +1606,11 @@ run().catch(async (e) => {{
|
||||
.current_dir(job_dir)
|
||||
.env_clear()
|
||||
.envs(reserved_variables)
|
||||
.envs(common_deno_proc_envs)
|
||||
.env("PATH", PATH_ENV.as_str())
|
||||
.env("DENO_DIR", DENO_CACHE_DIR)
|
||||
.env("DENO_AUTH_TOKENS", deno_auth_tokens)
|
||||
.env("BASE_INTERNAL_URL", base_internal_url)
|
||||
.env("NO_COLOR", "true")
|
||||
.args(args)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
@@ -1642,7 +1619,7 @@ run().catch(async (e) => {{
|
||||
}
|
||||
.instrument(trace_span!("create_deno_jail"))
|
||||
.await?;
|
||||
handle_child(&job.id, db, logs, child, !*DISABLE_NSJAIL, worker_name).await?;
|
||||
handle_child(&job.id, db, logs, child, !*DISABLE_NSJAIL).await?;
|
||||
read_result(job_dir).await
|
||||
}
|
||||
|
||||
@@ -1680,7 +1657,7 @@ async fn handle_python_job(
|
||||
token: String,
|
||||
inner_content: &String,
|
||||
shared_mount: &str,
|
||||
base_internal_url: &str,
|
||||
base_internal_url: &str
|
||||
) -> error::Result<serde_json::Value> {
|
||||
create_dependencies_dir(job_dir).await;
|
||||
|
||||
@@ -1694,7 +1671,7 @@ async fn handle_python_job(
|
||||
if requirements.is_empty() {
|
||||
"".to_string()
|
||||
} else {
|
||||
pip_compile(&job.id, &requirements, logs, job_dir, db, worker_name)
|
||||
pip_compile(&job.id, &requirements, logs, job_dir, db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
Error::ExecutionErr(format!("pip compile failed: {}", e.to_string()))
|
||||
@@ -1911,7 +1888,7 @@ mount {{
|
||||
.spawn()?
|
||||
};
|
||||
|
||||
handle_child(&job.id, db, logs, child, !*DISABLE_NSJAIL, worker_name).await?;
|
||||
handle_child(&job.id, db, logs, child, !*DISABLE_NSJAIL).await?;
|
||||
read_result(job_dir).await
|
||||
}
|
||||
|
||||
@@ -1941,7 +1918,6 @@ async fn handle_dependency_job(
|
||||
logs: &mut String,
|
||||
job_dir: &str,
|
||||
db: &sqlx::Pool<sqlx::Postgres>,
|
||||
worker_name: &str,
|
||||
) -> error::Result<serde_json::Value> {
|
||||
let content = capture_dependency_job(
|
||||
&job.id,
|
||||
@@ -1956,8 +1932,7 @@ async fn handle_dependency_job(
|
||||
.unwrap_or_else(|| "no raw code"),
|
||||
logs,
|
||||
job_dir,
|
||||
db,
|
||||
worker_name
|
||||
db
|
||||
)
|
||||
.await;
|
||||
match content {
|
||||
@@ -1992,7 +1967,6 @@ async fn handle_flow_dependency_job(
|
||||
logs: &mut String,
|
||||
job_dir: &str,
|
||||
db: &sqlx::Pool<sqlx::Postgres>,
|
||||
worker_name: &str,
|
||||
) -> error::Result<()> {
|
||||
let path = job.script_path.clone().ok_or_else(|| {
|
||||
error::Error::InternalErr(
|
||||
@@ -2023,7 +1997,6 @@ async fn handle_flow_dependency_job(
|
||||
logs,
|
||||
job_dir,
|
||||
db,
|
||||
worker_name
|
||||
)
|
||||
.await;
|
||||
match new_lock {
|
||||
@@ -2138,13 +2111,12 @@ async fn capture_dependency_job(
|
||||
job_raw_code: &str,
|
||||
logs: &mut String,
|
||||
job_dir: &str,
|
||||
db: &sqlx::Pool<sqlx::Postgres>,
|
||||
worker_name: &str
|
||||
db: &sqlx::Pool<sqlx::Postgres>
|
||||
) -> error::Result<String> {
|
||||
match job_language {
|
||||
ScriptLang::Python3 => {
|
||||
create_dependencies_dir(job_dir).await;
|
||||
pip_compile(job_id, job_raw_code, logs, job_dir, db, worker_name).await
|
||||
pip_compile(job_id, job_raw_code, logs, job_dir, db ).await
|
||||
}
|
||||
ScriptLang::Go => {
|
||||
install_go_dependencies(
|
||||
@@ -2155,7 +2127,6 @@ async fn capture_dependency_job(
|
||||
db,
|
||||
false,
|
||||
false,
|
||||
worker_name
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -2173,7 +2144,6 @@ async fn pip_compile(
|
||||
logs: &mut String,
|
||||
job_dir: &str,
|
||||
db: &Pool<Postgres>,
|
||||
worker_name: &str
|
||||
) -> error::Result<String> {
|
||||
logs.push_str(&format!("\nresolving dependencies..."));
|
||||
set_logs(logs, job_id, db).await;
|
||||
@@ -2206,7 +2176,7 @@ async fn pip_compile(
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
handle_child(job_id, db, logs, child, false, worker_name)
|
||||
handle_child(job_id, db, logs, child, false)
|
||||
.await
|
||||
.map_err(|e| Error::ExecutionErr(format!("Lock file generation failed: {e:?}")))?;
|
||||
let path_lock = format!("{job_dir}/requirements.txt");
|
||||
@@ -2229,7 +2199,6 @@ async fn install_go_dependencies(
|
||||
db: &sqlx::Pool<sqlx::Postgres>,
|
||||
preview: bool,
|
||||
skip_go_mod: bool,
|
||||
worker_name: &str
|
||||
) -> error::Result<String> {
|
||||
if !skip_go_mod {
|
||||
gen_go_mymod(code, job_dir).await?;
|
||||
@@ -2240,7 +2209,7 @@ async fn install_go_dependencies(
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
handle_child(job_id, db, logs, child, false, worker_name).await?;
|
||||
handle_child(job_id, db, logs, child, false).await?;
|
||||
}
|
||||
let child = Command::new(GO_PATH.as_str())
|
||||
.current_dir(job_dir)
|
||||
@@ -2248,7 +2217,7 @@ async fn install_go_dependencies(
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
handle_child(job_id, db, logs, child, false, worker_name)
|
||||
handle_child(job_id, db, logs, child, false)
|
||||
.await
|
||||
.map_err(|e| Error::ExecutionErr(format!("Lock file generation failed: {e:?}")))?;
|
||||
|
||||
@@ -2354,7 +2323,6 @@ async fn handle_child(
|
||||
logs: &mut String,
|
||||
mut child: Child,
|
||||
nsjail: bool,
|
||||
worker_name: &str,
|
||||
) -> error::Result<()> {
|
||||
let update_job_interval = Duration::from_millis(500);
|
||||
let write_logs_delay = Duration::from_millis(500);
|
||||
@@ -2368,14 +2336,11 @@ async fn handle_child(
|
||||
tracing::info!("could not get child pid");
|
||||
}
|
||||
let (set_too_many_logs, mut too_many_logs) = watch::channel::<bool>(false);
|
||||
let (tx, mut rx) = broadcast::channel::<()>(3);
|
||||
let mut rx2 = tx.subscribe();
|
||||
|
||||
|
||||
let output = child_joined_output_stream(&mut child);
|
||||
|
||||
let job_id = job_id.clone();
|
||||
|
||||
let (tx, mut rx) = mpsc::channel::<()>(1);
|
||||
|
||||
/* the cancellation future is polled on by `wait_on_child` while
|
||||
* waiting for the child to exit normally */
|
||||
@@ -2385,22 +2350,10 @@ async fn handle_child(
|
||||
let mut interval = interval(update_job_interval);
|
||||
interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||||
|
||||
let mut i = 1;
|
||||
loop {
|
||||
tokio::select!(
|
||||
_ = rx.recv() => break,
|
||||
_ = interval.tick() => {
|
||||
// update the last_ping column every 5 seconds
|
||||
i+=1;
|
||||
if i % 10 == 0 {
|
||||
sqlx::query!(
|
||||
"UPDATE worker_ping SET ping_at = now() WHERE worker = $1",
|
||||
&worker_name
|
||||
)
|
||||
.execute(&db)
|
||||
.await
|
||||
.expect("update worker ping");
|
||||
}
|
||||
let mem_peak = get_mem_peak(pid, nsjail).await;
|
||||
tracing::info!("{job_id} still running. mem peak: {}kB", mem_peak);
|
||||
let mem_peak = if mem_peak > 0 { Some(mem_peak) } else { None };
|
||||
@@ -2434,10 +2387,10 @@ async fn handle_child(
|
||||
biased;
|
||||
result = child.wait() => return result.map(Ok),
|
||||
Ok(()) = too_many_logs.changed() => KillReason::TooManyLogs,
|
||||
_ = sleep(*TIMEOUT_DURATION) => KillReason::Timeout,
|
||||
_ = update_job => KillReason::Cancelled,
|
||||
_ = sleep(*TIMEOUT_DURATION) => KillReason::Timeout,
|
||||
};
|
||||
tx.send(()).expect("rx should never be dropped");
|
||||
tx.send(()).await.expect("rx should never be dropped");
|
||||
drop(tx);
|
||||
|
||||
let set_reason = async {
|
||||
@@ -2460,7 +2413,7 @@ async fn handle_child(
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/* send SIGKILL and reap child process */
|
||||
let (_, kill) = future::join(set_reason, child.kill()).await;
|
||||
kill.map(|()| Err(kill_reason))
|
||||
@@ -2476,13 +2429,12 @@ async fn handle_child(
|
||||
/* log_remaining is zero when output limit was reached */
|
||||
let mut log_remaining = max_log_size.saturating_sub(logs.chars().count());
|
||||
let mut result = io::Result::Ok(());
|
||||
let mut output = output.take_until(rx2.recv()).boxed();
|
||||
let mut output = output;
|
||||
/* `do_write` resolves the task, but does not contain the Result.
|
||||
* It's useful to know if the task completed. */
|
||||
let (mut do_write, mut write_result) = tokio::spawn(ready(())).remote_handle();
|
||||
|
||||
while let Some(line) = output.by_ref().next().await {
|
||||
|
||||
while let Some(line) = output.by_ref().next().await {
|
||||
let do_write_ = do_write.shared();
|
||||
|
||||
let mut read_lines = stream::once(async { line })
|
||||
@@ -2497,14 +2449,11 @@ async fn handle_child(
|
||||
let mut joined = String::new();
|
||||
|
||||
while let Some(line) = read_lines.next().await {
|
||||
|
||||
match line {
|
||||
Ok(_) if log_remaining == 0 => (),
|
||||
Ok(line) => {
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
append_with_limit(&mut joined, &line, &mut log_remaining);
|
||||
|
||||
if log_remaining == 0 {
|
||||
tracing::info!(%job_id, "Too many logs lines for job {job_id}");
|
||||
let _ = set_too_many_logs.send(true);
|
||||
@@ -2524,7 +2473,6 @@ async fn handle_child(
|
||||
|
||||
logs.push_str(&joined);
|
||||
|
||||
|
||||
/* Ensure the last flush completed before starting a new one.
|
||||
*
|
||||
* This shouldn't pause since `take_until()` reads lines until `do_write`
|
||||
@@ -2541,7 +2489,8 @@ async fn handle_child(
|
||||
panic::resume_unwind(p);
|
||||
}
|
||||
|
||||
(do_write, write_result) = tokio::spawn(append_logs(job_id, joined, db.clone())).remote_handle();
|
||||
(do_write, write_result) =
|
||||
tokio::spawn(append_logs(job_id, joined, db.clone())).remote_handle();
|
||||
|
||||
if let Err(err) = result {
|
||||
tracing::error!(%job_id, %err, "error reading output for job {job_id}: {err}");
|
||||
@@ -2551,7 +2500,6 @@ async fn handle_child(
|
||||
if *set_too_many_logs.borrow() {
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* drop our end of the pipe */
|
||||
@@ -2865,7 +2813,7 @@ async fn handle_python_reqs(
|
||||
.spawn()?
|
||||
};
|
||||
|
||||
let child = handle_child(&job.id, db, logs, child, false, worker_name).await;
|
||||
let child = handle_child(&job.id, db, logs, child, false).await;
|
||||
tracing::info!(
|
||||
worker_name = %worker_name,
|
||||
job_id = %job.id,
|
||||
|
||||
@@ -326,21 +326,6 @@ pub async fn update_flow_status_after_job_completion(
|
||||
)
|
||||
.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?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1252,7 +1237,6 @@ 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 (uuid, inner_tx) = push(
|
||||
tx,
|
||||
&flow_job.workspace_id,
|
||||
@@ -1264,7 +1248,6 @@ async fn push_next_flow_job(
|
||||
scheduled_for_o,
|
||||
flow_job.schedule_path.clone(),
|
||||
Some(flow_job.id),
|
||||
root_job,
|
||||
true,
|
||||
continue_on_same_worker,
|
||||
err,
|
||||
|
||||
30
cli/apps.ts
30
cli/apps.ts
@@ -28,18 +28,14 @@ export class AppFile implements Resource, PushDiffs {
|
||||
remotePath: string,
|
||||
diffs: Difference[],
|
||||
): Promise<void> {
|
||||
let app: AppWithLastVersion | undefined = undefined;
|
||||
try {
|
||||
app = await AppService.getAppByPath({ workspace, path: remotePath });
|
||||
} catch (e) {}
|
||||
|
||||
if (app) {
|
||||
if (await AppService.existsApp({ workspace, path: remotePath })) {
|
||||
console.log(
|
||||
colors.bold.yellow(
|
||||
`Applying ${diffs.length} diffs to existing app... ${remotePath}`,
|
||||
`Applying ${diffs.length} diffs to existing app...`,
|
||||
),
|
||||
);
|
||||
const changeset: {
|
||||
path?: string | undefined;
|
||||
summary?: string | undefined;
|
||||
value?: any;
|
||||
policy?: Policy | undefined;
|
||||
@@ -50,7 +46,7 @@ export class AppFile implements Resource, PushDiffs {
|
||||
(
|
||||
diff.path[0] !== "value" && diff.path[0] !== "policy" && (
|
||||
diff.path.length !== 1 ||
|
||||
!["summary"].includes(
|
||||
!["path", "summary"].includes(
|
||||
diff.path[0] as string,
|
||||
)
|
||||
)
|
||||
@@ -65,13 +61,6 @@ export class AppFile implements Resource, PushDiffs {
|
||||
}
|
||||
}
|
||||
|
||||
if ((!changeset?.policy || JSON.stringify(changeset?.policy) == JSON.stringify(app.policy))
|
||||
&& (!changeset?.value || JSON.stringify(changeset?.value) == JSON.stringify(app.value))
|
||||
&& (!changeset?.summary || changeset.summary == app.summary)) {
|
||||
console.log(colors.yellow(`No changes to push for app ${remotePath}, skipping`))
|
||||
return;
|
||||
}
|
||||
|
||||
const hasChanges = Object.values(changeset).some((v) =>
|
||||
v !== null && typeof v !== "undefined"
|
||||
);
|
||||
@@ -98,10 +87,19 @@ export class AppFile implements Resource, PushDiffs {
|
||||
}
|
||||
}
|
||||
async push(workspace: string, remotePath: string): Promise<void> {
|
||||
let existing: AppWithLastVersion | undefined;
|
||||
try {
|
||||
existing = await AppService.getAppByPath({
|
||||
workspace: workspace,
|
||||
path: remotePath,
|
||||
});
|
||||
} catch {
|
||||
existing = undefined;
|
||||
}
|
||||
await this.pushDiffs(
|
||||
workspace,
|
||||
remotePath,
|
||||
microdiff({}, this, { cyclesFix: false }),
|
||||
microdiff(existing ?? {}, this, { cyclesFix: false }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,12 +18,12 @@ export type Context = {
|
||||
};
|
||||
|
||||
async function tryResolveWorkspace(
|
||||
opts: GlobalOptions
|
||||
opts: GlobalOptions,
|
||||
): Promise<
|
||||
{ isError: false; value: Workspace } | { isError: true; error: string }
|
||||
> {
|
||||
const cache = (opts as any).__secret_workspace;
|
||||
if (cache) return { isError: false, value: cache };
|
||||
if (cache) return cache;
|
||||
|
||||
if (opts.workspace) {
|
||||
const e = await getWorkspaceByName(opts.workspace);
|
||||
@@ -49,23 +49,21 @@ async function tryResolveWorkspace(
|
||||
}
|
||||
|
||||
export async function resolveWorkspace(
|
||||
opts: GlobalOptions
|
||||
opts: GlobalOptions,
|
||||
): Promise<Workspace> {
|
||||
const res = await tryResolveWorkspace(opts);
|
||||
if (res.isError) {
|
||||
console.log(colors.red.bold(res.error));
|
||||
console.log(res.error);
|
||||
return Deno.exit(-1);
|
||||
} else {
|
||||
return res.value;
|
||||
}
|
||||
}
|
||||
|
||||
export async function requireLogin(
|
||||
opts: GlobalOptions
|
||||
): Promise<GlobalUserInfo> {
|
||||
export async function requireLogin(opts: GlobalOptions): Promise<GlobalUserInfo> {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
let token = await tryGetLoginInfo(opts);
|
||||
|
||||
let token = await tryGetLoginInfo(opts);
|
||||
if (!token) {
|
||||
token = workspace.token;
|
||||
}
|
||||
@@ -76,26 +74,26 @@ export async function requireLogin(
|
||||
return await UserService.globalWhoami();
|
||||
} catch {
|
||||
console.log(
|
||||
"! Could not reach API given existing credentials. Attempting to reauth..."
|
||||
"! Could not reach API given existing credentials. Attempting to reauth...",
|
||||
);
|
||||
const newToken = await loginInteractive(workspace.remote);
|
||||
if (!newToken) {
|
||||
throw new Error("Could not reauth");
|
||||
}
|
||||
removeWorkspace(workspace.name, false, opts);
|
||||
removeWorkspace(workspace.name);
|
||||
workspace.token = newToken;
|
||||
addWorkspace(workspace, opts);
|
||||
addWorkspace(workspace);
|
||||
|
||||
setClient(
|
||||
token,
|
||||
workspace.remote.substring(0, workspace.remote.length - 1)
|
||||
workspace.remote.substring(0, workspace.remote.length - 1),
|
||||
);
|
||||
return await UserService.globalWhoami();
|
||||
}
|
||||
}
|
||||
|
||||
export async function tryResolveVersion(
|
||||
opts: GlobalOptions
|
||||
opts: GlobalOptions,
|
||||
): Promise<number | undefined> {
|
||||
if ((opts as any).__cache_version) {
|
||||
return (opts as any).__cache_version;
|
||||
@@ -105,12 +103,12 @@ export async function tryResolveVersion(
|
||||
if (workspaceRes.isError) return undefined;
|
||||
|
||||
const response = await fetch(
|
||||
new URL(new URL(workspaceRes.value.remote).origin + "/api/version")
|
||||
new URL(new URL(workspaceRes.value.remote).origin + "/api/version"),
|
||||
);
|
||||
const version = await response.text();
|
||||
try {
|
||||
return Number.parseInt(
|
||||
version.split("-", 1)[0].replaceAll(".", "").replace("v", "")
|
||||
version.split("-", 1)[0].replaceAll(".", "").replace("v", ""),
|
||||
);
|
||||
} catch {
|
||||
return undefined;
|
||||
@@ -119,7 +117,7 @@ export async function tryResolveVersion(
|
||||
|
||||
export async function validatePath(
|
||||
opts: GlobalOptions,
|
||||
path: string
|
||||
path: string,
|
||||
): Promise<boolean> {
|
||||
const backendVersion = await tryResolveVersion(opts);
|
||||
if (path.startsWith("f")) {
|
||||
@@ -127,16 +125,19 @@ export async function validatePath(
|
||||
return true;
|
||||
}
|
||||
console.log(
|
||||
`Attempting to use folders, but the current remote does not have support. Remote version is ${backendVersion} but folders are supported from 1560.`
|
||||
`Attempting to use folders, but the current remote does not have support. Remote version is ${backendVersion} but folders are supported from 1560.`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(path.startsWith("g") || path.startsWith("u"))) {
|
||||
if (
|
||||
!(path.startsWith("g") ||
|
||||
path.startsWith("u"))
|
||||
) {
|
||||
console.log(
|
||||
colors.red(
|
||||
"Given remote path looks invalid. Remote paths are typically of the form <u|g|f>/<username|group|folder>/..."
|
||||
)
|
||||
"Given remote path looks invalid. Remote paths are typically of the form <u|g|f>/<username|group|folder>/...",
|
||||
),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export {
|
||||
DenoLandProvider,
|
||||
UpgradeCommand,
|
||||
} from "https://deno.land/x/cliffy@v0.25.7/command/upgrade/mod.ts";
|
||||
export { CompletionsCommand } from "https://deno.land/x/cliffy@v0.25.7/command/completions/mod.ts";
|
||||
|
||||
// std
|
||||
export * as path from "https://deno.land/std@0.176.0/path/mod.ts";
|
||||
export { ensureDir } from "https://deno.land/std@0.176.0/fs/ensure_dir.ts";
|
||||
|
||||
22
cli/flow.ts
22
cli/flow.ts
@@ -94,7 +94,6 @@ export class FlowFile implements Resource, PushDiffs {
|
||||
...changeset,
|
||||
...base_changeset,
|
||||
}
|
||||
|
||||
await FlowService.updateFlow({
|
||||
workspace: workspace,
|
||||
path: remotePath,
|
||||
@@ -115,11 +114,20 @@ export class FlowFile implements Resource, PushDiffs {
|
||||
}
|
||||
}
|
||||
async push(workspace: string, remotePath: string): Promise<void> {
|
||||
let remote: Flow | undefined;
|
||||
try {
|
||||
remote = await FlowService.getFlowByPath({
|
||||
workspace,
|
||||
path: remotePath,
|
||||
});
|
||||
} catch {
|
||||
|
||||
remote = undefined;
|
||||
}
|
||||
await this.pushDiffs(
|
||||
workspace,
|
||||
remotePath,
|
||||
microdiff({}, this, { cyclesFix: false }),
|
||||
microdiff(remote ?? {}, this, { cyclesFix: false }),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -184,7 +192,7 @@ async function list(opts: GlobalOptions & { showArchived?: boolean }) {
|
||||
}
|
||||
async function run(
|
||||
opts: GlobalOptions & {
|
||||
data?: string;
|
||||
input: string[];
|
||||
silent: boolean;
|
||||
},
|
||||
path: string,
|
||||
@@ -192,8 +200,7 @@ async function run(
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
const input = opts.data ? await resolve(opts.data) : {};
|
||||
|
||||
const input = await resolve(opts.input);
|
||||
|
||||
const id = await JobService.runFlowByPath({
|
||||
workspace: workspace.workspaceId,
|
||||
@@ -229,7 +236,6 @@ async function run(
|
||||
|
||||
if (!opts.silent) {
|
||||
console.log(colors.green.underline.bold("Flow ran to completion"));
|
||||
console.log()
|
||||
}
|
||||
const jobInfo = await JobService.getCompletedJob({
|
||||
workspace: workspace.workspaceId,
|
||||
@@ -251,8 +257,8 @@ const command = new Command()
|
||||
.command("run", "run a flow by path.")
|
||||
.arguments("<path:string>")
|
||||
.option(
|
||||
"-d --data <data:string>",
|
||||
"Inputs specified as a JSON string or a file using @<filename> or stdin using @-.",
|
||||
"-i --input [inputs...:string]",
|
||||
"Inputs specified as JSON objects or simply as <name>=<value>. Supports file inputs using @<filename> and stdin using @- these also need to be formatted as JSON. Later inputs override earlier ones.",
|
||||
)
|
||||
.option(
|
||||
"-s --silent",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { colors, Command, FolderService, microdiff } from "./deps.ts";
|
||||
import { colors, Command, Folder, FolderService, microdiff } from "./deps.ts";
|
||||
import { requireLogin, resolveWorkspace, validatePath } from "./context.ts";
|
||||
import {
|
||||
Difference,
|
||||
@@ -20,16 +20,8 @@ import {
|
||||
export class FolderFile implements Resource, PushDiffs {
|
||||
@property(array(() => String))
|
||||
owners: Array<string> | undefined;
|
||||
@property(
|
||||
map(
|
||||
() => String,
|
||||
() => Boolean,
|
||||
{ shape: MapShape.Object }
|
||||
)
|
||||
)
|
||||
@property(map(() => String, () => Boolean, { shape: MapShape.Object }))
|
||||
extra_perms: Map<string, boolean> | undefined;
|
||||
@property(() => String)
|
||||
display_name: string | undefined;
|
||||
|
||||
async push(workspace: string, remotePath: string): Promise<void> {
|
||||
if (remotePath.startsWith("/")) {
|
||||
@@ -39,17 +31,23 @@ export class FolderFile implements Resource, PushDiffs {
|
||||
remotePath = remotePath.substring(2);
|
||||
}
|
||||
|
||||
let existing: Folder | undefined;
|
||||
try {
|
||||
existing = await FolderService.getFolder({ workspace, name: remotePath });
|
||||
} catch {
|
||||
existing = undefined;
|
||||
}
|
||||
await this.pushDiffs(
|
||||
workspace,
|
||||
remotePath,
|
||||
microdiff({}, this, { cyclesFix: false })
|
||||
microdiff(existing ?? {}, this, { cyclesFix: false }),
|
||||
);
|
||||
}
|
||||
|
||||
async pushDiffs(
|
||||
workspace: string,
|
||||
remotePath: string,
|
||||
diffs: Difference[]
|
||||
diffs: Difference[],
|
||||
): Promise<void> {
|
||||
if (remotePath.startsWith("/")) {
|
||||
remotePath = remotePath.substring(1);
|
||||
@@ -61,34 +59,29 @@ export class FolderFile implements Resource, PushDiffs {
|
||||
// TODO: Support this in backend
|
||||
let exists: boolean;
|
||||
try {
|
||||
exists = !!(await FolderService.getFolder({
|
||||
workspace,
|
||||
name: remotePath,
|
||||
}));
|
||||
exists = !!await FolderService.getFolder({ workspace, name: remotePath });
|
||||
} catch {
|
||||
exists = false;
|
||||
}
|
||||
if (exists) {
|
||||
console.log(
|
||||
colors.bold.yellow(
|
||||
`Applying ${diffs.length} diffs to existing folder... ${remotePath}`
|
||||
)
|
||||
`Applying ${diffs.length} diffs to existing folder...`,
|
||||
),
|
||||
);
|
||||
|
||||
const changeset: {
|
||||
owners?: string[] | undefined;
|
||||
extra_perms?: any;
|
||||
display_name?: string | undefined;
|
||||
} = {};
|
||||
for (const diff of diffs) {
|
||||
if (
|
||||
diff.type !== "REMOVE" &&
|
||||
(diff.path.length !== 1 ||
|
||||
!["owners", "extra_perms", "display_name"].includes(
|
||||
diff.path[0] as string
|
||||
))
|
||||
(
|
||||
diff.path.length !== 1 ||
|
||||
!["owners", "extra_perms"].includes(diff.path[0] as string)
|
||||
)
|
||||
) {
|
||||
console.log(diff.path);
|
||||
throw new Error("Invalid folder diff with path " + diff.path);
|
||||
}
|
||||
if (diff.type === "CREATE" || diff.type === "CHANGE") {
|
||||
@@ -98,27 +91,18 @@ export class FolderFile implements Resource, PushDiffs {
|
||||
}
|
||||
}
|
||||
|
||||
const hasChanges = Object.values(changeset).some(
|
||||
(v) => v !== null && typeof v !== "undefined"
|
||||
const hasChanges = Object.values(changeset).some((v) =>
|
||||
v !== null && typeof v !== "undefined"
|
||||
);
|
||||
if (!hasChanges) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await FolderService.updateFolder({
|
||||
workspace: workspace,
|
||||
name: remotePath,
|
||||
requestBody: {
|
||||
...changeset,
|
||||
extra_perms: changeset.extra_perms
|
||||
? Object.fromEntries(this.extra_perms?.entries() ?? [])
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(colors.red.bold(e.body));
|
||||
throw e;
|
||||
}
|
||||
|
||||
await FolderService.updateFolder({
|
||||
workspace: workspace,
|
||||
name: remotePath,
|
||||
requestBody: changeset,
|
||||
});
|
||||
} else {
|
||||
console.log(colors.bold.yellow("Creating new folder: " + remotePath));
|
||||
await FolderService.createFolder({
|
||||
@@ -137,7 +121,7 @@ async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
if (!(await validatePath(opts, remotePath))) {
|
||||
if (!await validatePath(opts, remotePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -155,11 +139,11 @@ async function push(opts: GlobalOptions, filePath: string, remotePath: string) {
|
||||
export async function pushFolder(
|
||||
workspace: string,
|
||||
filePath: string,
|
||||
remotePath: string
|
||||
remotePath: string,
|
||||
) {
|
||||
const data = decoverto
|
||||
.type(FolderFile)
|
||||
.rawToInstance(await Deno.readTextFile(filePath));
|
||||
const data = decoverto.type(FolderFile).rawToInstance(
|
||||
await Deno.readTextFile(filePath),
|
||||
);
|
||||
data.push(workspace, remotePath);
|
||||
}
|
||||
|
||||
@@ -167,7 +151,7 @@ const command = new Command()
|
||||
.description("resource related commands")
|
||||
.command(
|
||||
"push",
|
||||
"push a local folder spec. This overrides any remote versions."
|
||||
"push a local folder spec. This overrides any remote versions.",
|
||||
)
|
||||
.arguments("<file_path:string> <remote_path:string>")
|
||||
.action(push as any);
|
||||
|
||||
23
cli/main.ts
23
cli/main.ts
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
Command,
|
||||
CompletionsCommand,
|
||||
DenoLandProvider,
|
||||
UpgradeCommand,
|
||||
} from "./deps.ts";
|
||||
import { Command, DenoLandProvider, UpgradeCommand } from "./deps.ts";
|
||||
import flow from "./flow.ts";
|
||||
import script from "./script.ts";
|
||||
import workspace from "./workspace.ts";
|
||||
@@ -18,18 +13,19 @@ import sync from "./sync.ts";
|
||||
import { tryResolveVersion } from "./context.ts";
|
||||
import { GlobalOptions } from "./types.ts";
|
||||
|
||||
export const VERSION = "v1.77.0";
|
||||
const VERSION = "v1.70.1";
|
||||
|
||||
let command: any = new Command()
|
||||
.name("wmill")
|
||||
.description("A simple CLI tool for windmill.")
|
||||
.action(() => command.showHelp())
|
||||
.globalOption(
|
||||
"--workspace <workspace:string>",
|
||||
"Specify the target workspace. This overrides the default workspace."
|
||||
"Specify the target workspace. This overrides the default workspace.",
|
||||
)
|
||||
.globalOption(
|
||||
"--token <token:string>",
|
||||
"Specify an API token. This will override any stored token."
|
||||
"Specify an API token. This will override any stored token.",
|
||||
)
|
||||
.version(VERSION)
|
||||
.command("flow", flow)
|
||||
@@ -63,12 +59,13 @@ let command: any = new Command()
|
||||
"--unstable",
|
||||
],
|
||||
provider: new DenoLandProvider({ name: "wmill" }),
|
||||
})
|
||||
)
|
||||
.command("completions", new CompletionsCommand());
|
||||
}),
|
||||
);
|
||||
|
||||
if (Number.parseInt(VERSION.replace("v", "").replace(".", "")) > 1700) {
|
||||
command = command.command("push", push).command("pull", pull);
|
||||
command = command
|
||||
.command("push", push)
|
||||
.command("pull", pull);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
EditResourceType,
|
||||
microdiff,
|
||||
ResourceService,
|
||||
ResourceType,
|
||||
Table,
|
||||
} from "./deps.ts";
|
||||
import { Any, decoverto, model, property } from "./decoverto.ts";
|
||||
@@ -25,10 +26,19 @@ export class ResourceTypeFile implements ResourceI, PushDiffs {
|
||||
description?: string;
|
||||
|
||||
async push(workspace: string, remotePath: string): Promise<void> {
|
||||
await this.pushDiffs(
|
||||
let existing: ResourceType | undefined;
|
||||
try {
|
||||
existing = await ResourceService.getResourceType({
|
||||
workspace,
|
||||
path: remotePath,
|
||||
});
|
||||
} catch {
|
||||
existing = undefined;
|
||||
}
|
||||
this.pushDiffs(
|
||||
workspace,
|
||||
remotePath,
|
||||
microdiff({}, this, { cyclesFix: false }),
|
||||
microdiff(existing ?? {}, this, { cyclesFix: false }),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -55,8 +65,8 @@ export class ResourceTypeFile implements ResourceI, PushDiffs {
|
||||
return;
|
||||
}
|
||||
console.log(
|
||||
colors.yellow.bold(
|
||||
`Applying ${diffs.length} diffs to existing resource type... ${remotePath}`,
|
||||
colors.yellow(
|
||||
`Applying ${diffs.length} diffs to existing resource type...`,
|
||||
),
|
||||
);
|
||||
const changeset: EditResourceType = {};
|
||||
|
||||
@@ -43,7 +43,7 @@ export class ResourceFile implements Resource2, PushDiffs {
|
||||
})
|
||||
) {
|
||||
console.log(
|
||||
colors.yellow.bold(`Applying ${diffs.length} diffs to existing resource... ${remotePath}`),
|
||||
colors.yellow(`Applying ${diffs.length} diffs to existing resource...`),
|
||||
);
|
||||
|
||||
const changeset: EditResource = {
|
||||
@@ -51,7 +51,11 @@ export class ResourceFile implements Resource2, PushDiffs {
|
||||
};
|
||||
for (const diff of diffs) {
|
||||
if (diff.path[0] === "is_oauth") {
|
||||
//is_oauth is not updatable
|
||||
console.log(
|
||||
colors.yellow(
|
||||
"! is_oauth has been removed in newer versions. Ignoring.",
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
@@ -60,10 +64,9 @@ export class ResourceFile implements Resource2, PushDiffs {
|
||||
diff.path[0] !== "value" && (
|
||||
diff.path.length !== 1 ||
|
||||
diff.path[0] !== "description"
|
||||
) && diff.path[0] !== "resource_type"
|
||||
)
|
||||
)
|
||||
) {
|
||||
console.log(colors.red("Invalid variable diff with path " + diff.path));
|
||||
throw new Error("Invalid folder diff with path " + diff.path);
|
||||
}
|
||||
if (diff.type === "CREATE" || diff.type === "CHANGE") {
|
||||
@@ -107,10 +110,19 @@ export class ResourceFile implements Resource2, PushDiffs {
|
||||
}
|
||||
}
|
||||
async push(workspace: string, remotePath: string): Promise<void> {
|
||||
let existing: Resource | undefined;
|
||||
try {
|
||||
existing = await ResourceService.getResource({
|
||||
workspace,
|
||||
path: remotePath,
|
||||
});
|
||||
} catch {
|
||||
existing = undefined;
|
||||
}
|
||||
await this.pushDiffs(
|
||||
workspace,
|
||||
remotePath,
|
||||
microdiff({}, this, { cyclesFix: false }),
|
||||
microdiff(existing ?? {}, this, { cyclesFix: false }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
Table,
|
||||
} from "./deps.ts";
|
||||
import { Any, array, decoverto, model, property } from "./decoverto.ts";
|
||||
import { writeAllSync } from "https://deno.land/std@0.176.0/streams/mod.ts";
|
||||
|
||||
@model()
|
||||
export class ScriptFile {
|
||||
@@ -42,7 +41,6 @@ export class ScriptFile {
|
||||
},
|
||||
toPlain: (data) => data,
|
||||
})
|
||||
@property(() => String)
|
||||
kind?: "script" | "failure" | "trigger" | "command" | "approval";
|
||||
|
||||
constructor(summary: string, description: string) {
|
||||
@@ -106,21 +104,11 @@ export async function handleFile(path: string, content: string, workspace: strin
|
||||
} catch { }
|
||||
const language = inferContentTypeFromFilePath(path);
|
||||
|
||||
let remote = undefined
|
||||
try {
|
||||
remote = await ScriptService.getScriptByPath({
|
||||
const remote = await ScriptService.getScriptByPath({
|
||||
workspace,
|
||||
path: remotePath,
|
||||
});
|
||||
} catch { }
|
||||
|
||||
if (remote) {
|
||||
if (typed.description === remote.description && content === remote.content && typed.summary === remote.summary && typed.is_template === remote.is_template && typed.kind == remote.kind && remote?.lock == typed.lock?.join('\n') && JSON.stringify(typed.schema) == JSON.stringify(remote.schema)) {
|
||||
console.log(colors.yellow(`No change to push for script ${remotePath}, skipping`))
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
await ScriptService.createScript({
|
||||
workspace,
|
||||
requestBody: {
|
||||
@@ -134,11 +122,10 @@ export async function handleFile(path: string, content: string, workspace: strin
|
||||
lock: typed.lock,
|
||||
parent_hash: remote.hash,
|
||||
schema: typed.schema,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
console.log(colors.yellow.bold(`Creating script with a parent ${remotePath}`))
|
||||
} else {
|
||||
} catch {
|
||||
// no parent hash
|
||||
await ScriptService.createScript({
|
||||
workspace: workspace,
|
||||
@@ -156,6 +143,7 @@ export async function handleFile(path: string, content: string, workspace: strin
|
||||
},
|
||||
});
|
||||
console.log(colors.yellow.bold(`Creating script without parent ${remotePath}`))
|
||||
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -292,27 +280,50 @@ async function list(opts: GlobalOptions & { showArchived?: boolean }) {
|
||||
.render();
|
||||
}
|
||||
|
||||
export async function resolve(input: string): Promise<Record<string, any>> {
|
||||
if (!input) {
|
||||
throw new Error("No data given");
|
||||
export async function resolve(inputs: string[]): Promise<Record<string, any>> {
|
||||
let result = {};
|
||||
|
||||
if (!inputs) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (input == "@-") {
|
||||
input = new TextDecoder().decode(await readAll(Deno.stdin));
|
||||
} if (input[0] == "@") {
|
||||
input = await Deno.readTextFile(input.substring(1));
|
||||
}
|
||||
try {
|
||||
return JSON.parse(input);
|
||||
} catch (e) {
|
||||
console.error("Impossible to parse input as JSON", input)
|
||||
throw e
|
||||
for (const input of inputs) {
|
||||
let data: string;
|
||||
if (input.startsWith("@")) {
|
||||
if (input == "@-") {
|
||||
data = new TextDecoder().decode(await readAll(Deno.stdin));
|
||||
} else {
|
||||
data = await Deno.readTextFile(input.substring(1));
|
||||
}
|
||||
} else {
|
||||
if (input.startsWith("{")) {
|
||||
data = input;
|
||||
} else {
|
||||
const key = input.split("=", 1)[0];
|
||||
const value = input.substring(key.length + 1);
|
||||
let o;
|
||||
try {
|
||||
o = JSON.parse(value);
|
||||
} catch {
|
||||
o = value;
|
||||
}
|
||||
data = JSON.stringify(Object.fromEntries([[key, o]]));
|
||||
}
|
||||
}
|
||||
let jsonObj;
|
||||
try {
|
||||
jsonObj = JSON.parse(data);
|
||||
} catch {
|
||||
jsonObj = data;
|
||||
}
|
||||
result = { ...result, ...jsonObj };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function run(
|
||||
opts: GlobalOptions & {
|
||||
data?: string;
|
||||
input: string[];
|
||||
silent: boolean;
|
||||
},
|
||||
path: string,
|
||||
@@ -320,8 +331,7 @@ async function run(
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
|
||||
const input = opts.data ? await resolve(opts.data) : {};
|
||||
const input = await resolve(opts.input);
|
||||
const id = await JobService.runScriptByPath({
|
||||
workspace: workspace.workspaceId,
|
||||
path,
|
||||
@@ -354,9 +364,7 @@ export async function track_job(workspace: string, id: string) {
|
||||
const result = await JobService.getCompletedJob({ workspace, id });
|
||||
|
||||
console.log(result.logs);
|
||||
console.log()
|
||||
console.log(colors.bold.underline.green("Job Completed"));
|
||||
console.log()
|
||||
return;
|
||||
} catch {
|
||||
/* ignore */
|
||||
@@ -395,7 +403,7 @@ export async function track_job(workspace: string, id: string) {
|
||||
}
|
||||
|
||||
if (updates.new_logs) {
|
||||
writeAllSync(Deno.stdout, new TextEncoder().encode(updates.new_logs));
|
||||
console.log(updates.new_logs);
|
||||
logOffset += updates.new_logs.length;
|
||||
}
|
||||
|
||||
@@ -418,15 +426,12 @@ export async function track_job(workspace: string, id: string) {
|
||||
if ((final_job.logs?.length ?? -1) > logOffset) {
|
||||
console.log(final_job.logs!.substring(logOffset));
|
||||
}
|
||||
console.log("\n")
|
||||
|
||||
if (final_job.success) {
|
||||
console.log(colors.bold.underline.green("Job Completed"));
|
||||
|
||||
} else {
|
||||
console.log(colors.bold.underline.red("Job Completed"));
|
||||
}
|
||||
console.log()
|
||||
|
||||
} catch {
|
||||
console.log("Job appears to have completed, but no data can be retrieved");
|
||||
}
|
||||
@@ -461,8 +466,8 @@ const command = new Command()
|
||||
.command("run", "run a script by path")
|
||||
.arguments("<path:string>")
|
||||
.option(
|
||||
"-d --data <data:string>",
|
||||
"Inputs specified as a JSON string or a file using @<filename> or stdin using @-.",
|
||||
"-i --input [inputs...:string]",
|
||||
"Inputs specified as JSON objects or simply as <name>=<value>. Supports file inputs using @<filename> and stdin using @- these also need to be formatted as JSON. Later inputs override earlier ones.",
|
||||
)
|
||||
.option(
|
||||
"-s --silent",
|
||||
|
||||
484
cli/sync.ts
484
cli/sync.ts
@@ -13,7 +13,7 @@ import {
|
||||
ResourceService,
|
||||
VariableService,
|
||||
AppService,
|
||||
FlowService,
|
||||
FlowService
|
||||
} from "./deps.ts";
|
||||
import {
|
||||
Difference,
|
||||
@@ -25,13 +25,16 @@ import {
|
||||
import { downloadZip } from "./pull.ts";
|
||||
import { FolderFile } from "./folder.ts";
|
||||
import { ResourceTypeFile } from "./resource-type.ts";
|
||||
import { handleScriptMetadata, ScriptFile } from "./script.ts";
|
||||
import {
|
||||
handleScriptMetadata,
|
||||
ScriptFile,
|
||||
} from "./script.ts";
|
||||
import { ResourceFile } from "./resource.ts";
|
||||
import { FlowFile } from "./flow.ts";
|
||||
import { VariableFile } from "./variable.ts";
|
||||
import { handleFile } from "./script.ts";
|
||||
import { equal } from "https://deno.land/x/equal@v1.5.0/mod.ts";
|
||||
import { diffCharacters } from "https://deno.land/x/diff@v0.3.5/mod.ts";
|
||||
import { equal } from "https://deno.land/x/equal/mod.ts";
|
||||
import { diffCharacters } from "https://deno.land/x/diff/mod.ts";
|
||||
type DynFSElement = {
|
||||
isDirectory: boolean;
|
||||
path: string;
|
||||
@@ -41,7 +44,10 @@ type DynFSElement = {
|
||||
};
|
||||
|
||||
async function FSFSElement(p: string): Promise<DynFSElement> {
|
||||
function _internal_element(localP: string, isDir: boolean): DynFSElement {
|
||||
function _internal_element(
|
||||
localP: string,
|
||||
isDir: boolean,
|
||||
): DynFSElement {
|
||||
return {
|
||||
isDirectory: isDir,
|
||||
path: localP.substring(p.length + 1),
|
||||
@@ -107,14 +113,16 @@ function ZipFSElement(zip: JSZip): DynFSElement {
|
||||
|
||||
async function* readDirRecursiveWithIgnore(
|
||||
ignore: (path: string, isDirectory: boolean) => boolean,
|
||||
root: DynFSElement
|
||||
): AsyncGenerator<{
|
||||
path: string;
|
||||
ignored: boolean;
|
||||
isDirectory: boolean;
|
||||
getContentBytes(): Promise<Uint8Array>;
|
||||
getContentText(): Promise<string>;
|
||||
}> {
|
||||
root: DynFSElement,
|
||||
): AsyncGenerator<
|
||||
{
|
||||
path: string;
|
||||
ignored: boolean;
|
||||
isDirectory: boolean;
|
||||
getContentBytes(): Promise<Uint8Array>;
|
||||
getContentText(): Promise<string>;
|
||||
}
|
||||
> {
|
||||
const stack: {
|
||||
path: string;
|
||||
isDirectory: boolean;
|
||||
@@ -122,20 +130,18 @@ async function* readDirRecursiveWithIgnore(
|
||||
c(): AsyncIterable<DynFSElement>;
|
||||
getContentBytes(): Promise<Uint8Array>;
|
||||
getContentText(): Promise<string>;
|
||||
}[] = [
|
||||
{
|
||||
path: root.path,
|
||||
ignored: ignore(root.path, root.isDirectory),
|
||||
isDirectory: root.isDirectory,
|
||||
c: root.getChildren,
|
||||
getContentBytes(): Promise<Uint8Array> {
|
||||
throw undefined;
|
||||
},
|
||||
getContentText(): Promise<string> {
|
||||
throw undefined;
|
||||
},
|
||||
}[] = [{
|
||||
path: root.path,
|
||||
ignored: ignore(root.path, root.isDirectory),
|
||||
isDirectory: root.isDirectory,
|
||||
c: root.getChildren,
|
||||
getContentBytes(): Promise<Uint8Array> {
|
||||
throw undefined;
|
||||
},
|
||||
];
|
||||
getContentText(): Promise<string> {
|
||||
throw undefined;
|
||||
},
|
||||
}];
|
||||
|
||||
while (stack.length > 0) {
|
||||
const e = stack.pop()!;
|
||||
@@ -155,17 +161,17 @@ async function* readDirRecursiveWithIgnore(
|
||||
}
|
||||
|
||||
type Added = { name: "added"; path: string; content: string };
|
||||
type Deleted = { name: "deleted"; path: string };
|
||||
type Edit = { name: "edited"; path: string; before: string; after: string };
|
||||
type Deleted = { name: "deleted"; path: string; };
|
||||
type Edit = { name: "edited"; path: string; before: string; after: string; };
|
||||
|
||||
type Change = Added | Deleted | Edit;
|
||||
|
||||
async function elementsToMap(
|
||||
els: DynFSElement,
|
||||
ignore: (path: string, isDirectory: boolean) => boolean
|
||||
): Promise<{ [key: string]: string }> {
|
||||
async function elementsToMap(els: DynFSElement, ignore: (path: string, isDirectory: boolean) => boolean): Promise<{ [key: string]: string }> {
|
||||
const map: { [key: string]: string } = {};
|
||||
for await (const entry of readDirRecursiveWithIgnore(ignore, els)) {
|
||||
for await (const entry of readDirRecursiveWithIgnore(
|
||||
ignore,
|
||||
els,
|
||||
)) {
|
||||
if (entry.isDirectory || entry.ignored) continue;
|
||||
const content = await entry.getContentText();
|
||||
map[entry.path] = content;
|
||||
@@ -173,26 +179,20 @@ async function elementsToMap(
|
||||
return map;
|
||||
}
|
||||
async function compareDynFSElement(
|
||||
els1: DynFSElement,
|
||||
els2: DynFSElement | undefined,
|
||||
ignore: (path: string, isDirectory: boolean) => boolean
|
||||
els1: DynFSElement, els2: DynFSElement,
|
||||
ignore: (path: string, isDirectory: boolean) => boolean,
|
||||
raw: boolean
|
||||
): Promise<Change[]> {
|
||||
const [m1, m2] = els2
|
||||
? await Promise.all([
|
||||
elementsToMap(els1, ignore),
|
||||
elementsToMap(els2, ignore),
|
||||
])
|
||||
: [await elementsToMap(els1, ignore), {}];
|
||||
|
||||
const [m1, m2] = raw ? [await elementsToMap(els1, ignore), {}] :
|
||||
await Promise.all([elementsToMap(els1, ignore), elementsToMap(els2, ignore)]);
|
||||
|
||||
const changes: Change[] = [];
|
||||
|
||||
for (const [k, v] of Object.entries(m1)) {
|
||||
if (m2[k] === undefined) {
|
||||
changes.push({ name: "added", path: k, content: v });
|
||||
} else if (
|
||||
m2[k] != v &&
|
||||
(!k.endsWith(".json") || !equal(JSON.parse(v), JSON.parse(m2[k])))
|
||||
) {
|
||||
} else if (m2[k] != v && (!k.endsWith(".json") || !equal(JSON.parse(v), JSON.parse(m2[k])))) {
|
||||
// await Deno.writeTextFile("/tmp/k", m2[k])
|
||||
// await Deno.writeTextFile("/tmp/v", v)
|
||||
// console.log(k)
|
||||
@@ -208,54 +208,54 @@ async function compareDynFSElement(
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
return changes
|
||||
}
|
||||
|
||||
const isNotWmillFile = (p: string, isDirectory: boolean) => {
|
||||
if (p.endsWith("/")) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
if (isDirectory) {
|
||||
return !p.startsWith("u/") && !p.startsWith("f/") && !p.startsWith("g/");
|
||||
return !p.startsWith("u/") && !p.startsWith("f/") && !p.startsWith("g/")
|
||||
}
|
||||
|
||||
try {
|
||||
const typ = getTypeStrFromPath(p);
|
||||
if (typ == "resource-type") {
|
||||
return p.includes("/");
|
||||
const typ = getTypeStrFromPath(p)
|
||||
if (typ == 'resource-type') {
|
||||
return p.includes('/')
|
||||
} else {
|
||||
return !p.startsWith("u/") && !p.startsWith("f/") && !p.startsWith("g/");
|
||||
return !p.startsWith("u/") && !p.startsWith("f/") && !p.startsWith("g/")
|
||||
}
|
||||
} catch {
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const isWhitelisted = (p: string) => {
|
||||
return p == "./" || p == "" || p == "u" || p == "f" || p == "g";
|
||||
};
|
||||
return p == "./" || p == "" || p == "u" || p == "f" || p == "g"
|
||||
}
|
||||
async function ignoreF() {
|
||||
try {
|
||||
const ignore: {
|
||||
accepts(file: string): boolean;
|
||||
denies(file: string): boolean;
|
||||
} = gitignore_parser.compile(await Deno.readTextFile(".wmillignore"));
|
||||
} = gitignore_parser.compile(
|
||||
await Deno.readTextFile(".wmillignore"),
|
||||
);
|
||||
|
||||
return (p: string, isDirectory: boolean) => {
|
||||
return (
|
||||
!isWhitelisted(p) &&
|
||||
(isNotWmillFile(p, isDirectory) || ignore.denies(p))
|
||||
);
|
||||
};
|
||||
return !isWhitelisted(p) && (isNotWmillFile(p, isDirectory) || ignore.denies(p));
|
||||
}
|
||||
} catch (e) {
|
||||
return (p: string, isDirectory: boolean) =>
|
||||
!isWhitelisted(p) && isNotWmillFile(p, isDirectory);
|
||||
return (p: string, isDirectory: boolean) => !isWhitelisted(p) && isNotWmillFile(p, isDirectory)
|
||||
}
|
||||
}
|
||||
|
||||
async function pull(
|
||||
opts: GlobalOptions & { raw: boolean; yes: boolean; failConflicts: boolean }
|
||||
opts: GlobalOptions & { raw: boolean; yes: boolean, failConflicts: boolean },
|
||||
) {
|
||||
|
||||
|
||||
if (!opts.raw) {
|
||||
await ensureDir(path.join(Deno.cwd(), ".wmill"));
|
||||
}
|
||||
@@ -263,96 +263,74 @@ async function pull(
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
console.log(
|
||||
colors.gray(
|
||||
"Computing the files to update locally to match remote (taking .wmillignore into account)"
|
||||
)
|
||||
);
|
||||
const remote = ZipFSElement((await downloadZip(workspace))!);
|
||||
const local = opts.raw
|
||||
? undefined
|
||||
: await FSFSElement(path.join(Deno.cwd(), opts.raw ? "" : ".wmill"));
|
||||
const changes = await compareDynFSElement(remote, local, await ignoreF());
|
||||
|
||||
console.log(colors.gray("Computing the files to update locally to match remote (taking .wmillignore into account)"));
|
||||
const remote = ZipFSElement((await downloadZip(workspace))!)
|
||||
const local = await FSFSElement(path.join(Deno.cwd(), opts.raw ? "" : ".wmill"))
|
||||
const changes = await compareDynFSElement(remote, local, await ignoreF(), opts.raw)
|
||||
|
||||
|
||||
console.log(`remote -> local: ${changes.length} changes to apply`);
|
||||
if (changes.length > 0) {
|
||||
prettyChanges(changes);
|
||||
|
||||
prettyChanges(changes)
|
||||
if (
|
||||
!opts.yes &&
|
||||
!opts.raw &&
|
||||
!(await Confirm.prompt({
|
||||
message: `Do you want to apply these ${changes.length} changes?`,
|
||||
default: true,
|
||||
}))
|
||||
!opts.yes && !opts.raw && !(await Confirm.prompt({ message: `Do you want to apply these ${changes.length} changes?`, default: true }))
|
||||
) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const conflicts = [];
|
||||
const conflicts = []
|
||||
console.log(colors.gray(`Applying changes to files ...`));
|
||||
for await (const change of changes) {
|
||||
const target = path.join(Deno.cwd(), change.path);
|
||||
const stateTarget = path.join(Deno.cwd(), ".wmill", change.path);
|
||||
const stateTarget = path.join(Deno.cwd(), ".wmill", change.path)
|
||||
if (change.name === "edited") {
|
||||
|
||||
try {
|
||||
const currentLocal = await Deno.readTextFile(target);
|
||||
if (currentLocal !== change.before && currentLocal !== change.after) {
|
||||
console.log(
|
||||
colors.red(
|
||||
`Conflict detected on ${change.path}\nBoth local and remote have been modified.`
|
||||
)
|
||||
);
|
||||
const currentLocal = await Deno.readTextFile(target)
|
||||
if (currentLocal !== change.before) {
|
||||
console.log(colors.red(`Conflict detected on ${change.path}\nBoth local and remote have been modified.`))
|
||||
if (opts.failConflicts) {
|
||||
conflicts.push({
|
||||
local: currentLocal,
|
||||
change,
|
||||
path: change.path,
|
||||
});
|
||||
conflicts.push({ local: currentLocal, change, path: change.path })
|
||||
continue;
|
||||
} else if (opts.yes) {
|
||||
console.log(
|
||||
colors.red(
|
||||
`Override local version with remote since --yes was passed and no --fail-conflicts.`
|
||||
)
|
||||
);
|
||||
} else {
|
||||
showConflict(change.path, currentLocal, change.after);
|
||||
if (
|
||||
await Confirm.prompt(
|
||||
"Preserve local (push to change remote and avoid seeing this again)?"
|
||||
)
|
||||
) {
|
||||
console.log(colors.red(`Override local version with remote since --yes was passed and no --fail-conflicts.`))
|
||||
}
|
||||
else {
|
||||
showConflict(change.path, currentLocal, change.after)
|
||||
if (await Confirm.prompt("Preserve local (push to change remote and avoid seeing this again)?")) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
} catch { }
|
||||
if (change.path.endsWith(".json")) {
|
||||
const diffs = microdiff(
|
||||
JSON.parse(change.before),
|
||||
JSON.parse(change.after),
|
||||
{ cyclesFix: false }
|
||||
);
|
||||
const diffs =
|
||||
microdiff(
|
||||
JSON.parse(change.before),
|
||||
JSON.parse(change.after),
|
||||
{ cyclesFix: false },
|
||||
)
|
||||
|
||||
console.log(
|
||||
`Editing ${getTypeStrFromPath(change.path)} json ${change.path}`
|
||||
console.log(`Editing ${getTypeStrFromPath(change.path)} json ${change.path}`)
|
||||
await applyDiff(
|
||||
diffs,
|
||||
target,
|
||||
);
|
||||
await applyDiff(diffs, target);
|
||||
} else {
|
||||
console.log(`Editing script ${change.path}`);
|
||||
console.log(`Editing script ${change.path}`)
|
||||
await Deno.writeTextFile(target, change.after);
|
||||
}
|
||||
if (!opts.raw) {
|
||||
await ensureDir(path.dirname(stateTarget));
|
||||
await ensureDir(path.dirname(stateTarget))
|
||||
await Deno.copyFile(target, stateTarget);
|
||||
}
|
||||
} else if (change.name === "added") {
|
||||
await ensureDir(path.dirname(target));
|
||||
await ensureDir(path.dirname(target))
|
||||
if (!opts.raw) {
|
||||
await ensureDir(path.dirname(stateTarget));
|
||||
console.log(
|
||||
`Adding ${getTypeStrFromPath(change.path)} ${change.path}`
|
||||
);
|
||||
await ensureDir(path.dirname(stateTarget))
|
||||
console.log(`Adding ${getTypeStrFromPath(change.path)} ${change.path}`)
|
||||
}
|
||||
await Deno.writeTextFile(target, change.content);
|
||||
if (!opts.raw) {
|
||||
@@ -360,10 +338,8 @@ async function pull(
|
||||
}
|
||||
} else if (change.name === "deleted") {
|
||||
try {
|
||||
console.log(
|
||||
`Deleting ${getTypeStrFromPath(change.path)} ${change.path}`
|
||||
);
|
||||
await Deno.remove(target);
|
||||
console.log(`Deleting ${getTypeStrFromPath(change.path)} ${change.path}`)
|
||||
await Deno.remove(target)
|
||||
if (!opts.raw) {
|
||||
await Deno.remove(stateTarget);
|
||||
}
|
||||
@@ -376,29 +352,24 @@ async function pull(
|
||||
}
|
||||
if (opts.failConflicts) {
|
||||
if (conflicts.length > 0) {
|
||||
console.error(colors.red(`Conflicts were found`));
|
||||
console.log("Conflicts:");
|
||||
console.error(colors.red(`Conflicts were found`))
|
||||
console.log("Conflicts:")
|
||||
for (const conflict of conflicts) {
|
||||
showConflict(conflict.path, conflict.local, conflict.change.after);
|
||||
showConflict(conflict.path, conflict.local, conflict.change.after)
|
||||
}
|
||||
console.log(
|
||||
colors.red(`Please resolve theses conflicts manually by either:
|
||||
console.log(colors.red(`Please resolve theses conflicts manually by either:
|
||||
- reverting the content back to its remote (\`wmill pull\` and refuse to preserve local when prompted)
|
||||
- pushing the changes with \`wmill push --skip-pull\` to override wmill with all your local changes
|
||||
`)
|
||||
);
|
||||
Deno.exit(1);
|
||||
`))
|
||||
Deno.exit(1)
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
colors.green.underline(
|
||||
`Done! All ${changes.length} changes applied locally.`
|
||||
)
|
||||
);
|
||||
console.log(colors.green.underline(`Done! All ${changes.length} changes applied locally.`));
|
||||
}
|
||||
|
||||
|
||||
function showConflict(path: string, local: string, remote: string) {
|
||||
console.log(colors.yellow(`- ${path}`));
|
||||
console.log(colors.yellow(`- ${path}`))
|
||||
|
||||
let finalString = "";
|
||||
for (const character of diffCharacters(local, remote)) {
|
||||
@@ -414,8 +385,8 @@ async function pull(
|
||||
}
|
||||
}
|
||||
console.log(finalString);
|
||||
console.log("\x1b[31mlocal\x1b[31m - \x1b[32mremote\x1b[32m");
|
||||
console.log();
|
||||
console.log("\x1b[31mlocal\x1b[31m - \x1b[32mremote\x1b[32m")
|
||||
console.log()
|
||||
}
|
||||
async function applyDiff(diffs: Difference[], file: string) {
|
||||
ensureDir(path.dirname(file));
|
||||
@@ -444,20 +415,17 @@ async function pull(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
function prettyChanges(changes: Change[]) {
|
||||
for (const change of changes) {
|
||||
if (change.name === "added") {
|
||||
console.log(
|
||||
colors.green(`+ ${getTypeStrFromPath(change.path)} ` + change.path)
|
||||
);
|
||||
console.log(colors.green(`+ ${getTypeStrFromPath(change.path)} ` + change.path));
|
||||
} else if (change.name === "deleted") {
|
||||
console.log(
|
||||
colors.red(`- ${getTypeStrFromPath(change.path)} ` + change.path)
|
||||
);
|
||||
console.log(colors.red(`- ${getTypeStrFromPath(change.path)} ` + change.path));
|
||||
} else if (change.name === "edited") {
|
||||
console.log(
|
||||
colors.yellow(`~ ${getTypeStrFromPath(change.path)} ` + change.path)
|
||||
);
|
||||
console.log(colors.yellow(`~ ${getTypeStrFromPath(change.path)} ` + change.path));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -484,208 +452,138 @@ function prettyDiff(diffs: Difference[]) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function removeSuffix(str: string, suffix: string) {
|
||||
return str.slice(0, str.length - suffix.length);
|
||||
}
|
||||
|
||||
async function push(
|
||||
opts: GlobalOptions & {
|
||||
raw: boolean;
|
||||
yes: boolean;
|
||||
skipPull: boolean;
|
||||
failConflicts: boolean;
|
||||
}
|
||||
) {
|
||||
async function push(opts: GlobalOptions & { raw: boolean, yes: boolean, skipPull: boolean, failConflicts: boolean }) {
|
||||
|
||||
|
||||
if (!opts.raw) {
|
||||
if (!opts.skipPull) {
|
||||
console.log(
|
||||
colors.gray("You need to be up-to-date before pushing, pulling first.")
|
||||
);
|
||||
await pull(opts);
|
||||
console.log(colors.green("Pull done, now pushing."));
|
||||
console.log();
|
||||
console.log(colors.gray("You need to be up-to-date before pushing, pulling first."))
|
||||
await pull(opts)
|
||||
console.log(colors.green("Pull done, now pushing."))
|
||||
console.log()
|
||||
}
|
||||
}
|
||||
|
||||
const workspace = await resolveWorkspace(opts);
|
||||
await requireLogin(opts);
|
||||
|
||||
console.log(
|
||||
colors.gray(
|
||||
"Computing the files to update on the remote to match local (taking .wmillignore into account)"
|
||||
)
|
||||
);
|
||||
const remote = opts.raw
|
||||
? undefined
|
||||
: ZipFSElement((await downloadZip(workspace))!);
|
||||
const local = await FSFSElement(path.join(Deno.cwd(), ""));
|
||||
const changes = await compareDynFSElement(local, remote, await ignoreF());
|
||||
|
||||
console.log(colors.gray("Computing the files to update on the remote to match local (taking .wmillignore into account)"));
|
||||
const remote = ZipFSElement((await downloadZip(workspace))!)
|
||||
const local = await FSFSElement(path.join(Deno.cwd(), ""))
|
||||
const changes = await compareDynFSElement(local, remote, await ignoreF(), opts.raw)
|
||||
|
||||
console.log(`remote <- local: ${changes.length} changes to apply`);
|
||||
if (changes.length > 0) {
|
||||
prettyChanges(changes);
|
||||
|
||||
prettyChanges(changes)
|
||||
if (
|
||||
!opts.yes &&
|
||||
!(await Confirm.prompt({
|
||||
message: `Do you want to apply these ${changes.length} changes?`,
|
||||
default: true,
|
||||
}))
|
||||
!opts.yes && !(await Confirm.prompt({ message: `Do you want to apply these ${changes.length} changes?`, default: true }))
|
||||
) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
console.log(colors.gray(`Applying changes to files ...`));
|
||||
const alreadySynced: string[] = [];
|
||||
const alreadySynced: string[] = []
|
||||
for await (const change of changes) {
|
||||
const stateTarget = path.join(Deno.cwd(), ".wmill", change.path);
|
||||
const stateTarget = path.join(Deno.cwd(), ".wmill", change.path)
|
||||
if (change.name === "edited") {
|
||||
if (
|
||||
await handleScriptMetadata(
|
||||
change.path,
|
||||
workspace.workspaceId,
|
||||
alreadySynced
|
||||
)
|
||||
) {
|
||||
if (await handleScriptMetadata(change.path, workspace.workspaceId, alreadySynced)) {
|
||||
if (!opts.raw) {
|
||||
await Deno.writeTextFile(stateTarget, change.after);
|
||||
}
|
||||
continue;
|
||||
} else if (
|
||||
await handleFile(
|
||||
change.path,
|
||||
change.after,
|
||||
workspace.workspaceId,
|
||||
alreadySynced
|
||||
)
|
||||
) {
|
||||
continue
|
||||
} else if (await handleFile(change.path, change.after, workspace.workspaceId, alreadySynced)) {
|
||||
if (!opts.raw) {
|
||||
await Deno.writeTextFile(stateTarget, change.after);
|
||||
}
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
if (!opts.raw) {
|
||||
await ensureDir(path.dirname(stateTarget));
|
||||
console.log(
|
||||
`Editing ${getTypeStrFromPath(change.path)} ${change.path}`
|
||||
);
|
||||
await ensureDir(path.dirname(stateTarget))
|
||||
console.log(`Editing ${getTypeStrFromPath(change.path)} ${change.path}`)
|
||||
}
|
||||
const obj = inferTypeFromPath(change.path, JSON.parse(change.after));
|
||||
const obj = inferTypeFromPath(change.path, JSON.parse(change.after))
|
||||
|
||||
const diff = microdiff(
|
||||
inferTypeFromPath(change.path, JSON.parse(change.before)),
|
||||
obj,
|
||||
{ cyclesFix: false }
|
||||
);
|
||||
const diff = microdiff(inferTypeFromPath(change.path, JSON.parse(change.before)), obj, { cyclesFix: false });
|
||||
await applyDiff(
|
||||
workspace.workspaceId,
|
||||
change.path.split(".")[0],
|
||||
obj,
|
||||
diff
|
||||
diff,
|
||||
);
|
||||
if (!opts.raw) {
|
||||
await Deno.writeTextFile(stateTarget, change.after);
|
||||
}
|
||||
} else if (change.name === "added") {
|
||||
if (change.path.endsWith(".script.json")) {
|
||||
continue;
|
||||
} else if (
|
||||
await handleFile(
|
||||
change.path,
|
||||
change.content,
|
||||
workspace.workspaceId,
|
||||
alreadySynced
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
continue
|
||||
} else if (await handleFile(change.path, change.content, workspace.workspaceId, alreadySynced)) {
|
||||
continue
|
||||
}
|
||||
if (!opts.raw) {
|
||||
await ensureDir(path.dirname(stateTarget));
|
||||
console.log(
|
||||
`Adding ${getTypeStrFromPath(change.path)} ${change.path}`
|
||||
);
|
||||
await ensureDir(path.dirname(stateTarget))
|
||||
console.log(`Adding ${getTypeStrFromPath(change.path)} ${change.path}`)
|
||||
}
|
||||
const obj = inferTypeFromPath(change.path, JSON.parse(change.content));
|
||||
const obj = inferTypeFromPath(change.path, JSON.parse(change.content))
|
||||
const diff = microdiff({}, obj, { cyclesFix: false });
|
||||
await applyDiff(
|
||||
workspace.workspaceId,
|
||||
change.path.split(".")[0],
|
||||
obj,
|
||||
diff
|
||||
diff,
|
||||
);
|
||||
if (!opts.raw) {
|
||||
await Deno.writeTextFile(stateTarget, change.content);
|
||||
}
|
||||
} else if (change.name === "deleted") {
|
||||
if (!change.path.includes(".json")) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
console.log(
|
||||
`Deleting ${getTypeStrFromPath(change.path)} ${change.path}`
|
||||
);
|
||||
const typ = getTypeStrFromPath(change.path);
|
||||
console.log(`Deleting ${getTypeStrFromPath(change.path)} ${change.path}`)
|
||||
const typ = getTypeStrFromPath(change.path)
|
||||
const workspaceId = workspace.workspaceId;
|
||||
switch (typ) {
|
||||
case "script": {
|
||||
const script = await ScriptService.getScriptByPath({
|
||||
workspace: workspaceId,
|
||||
path: removeSuffix(change.path, ".script.json"),
|
||||
});
|
||||
await ScriptService.deleteScriptByHash({
|
||||
workspace: workspaceId,
|
||||
hash: script.hash,
|
||||
});
|
||||
const script = await ScriptService.getScriptByPath({ workspace: workspaceId, path: removeSuffix(change.path, ".script.json") })
|
||||
await ScriptService.deleteScriptByHash({ workspace: workspaceId, hash: script.hash })
|
||||
break;
|
||||
}
|
||||
case "folder":
|
||||
await FolderService.deleteFolder({
|
||||
workspace: workspaceId,
|
||||
name: change.path.split("/")[1],
|
||||
});
|
||||
await FolderService.deleteFolder({ workspace: workspaceId, name: change.path.split('/')[1] })
|
||||
break;
|
||||
case "resource":
|
||||
await ResourceService.deleteResource({
|
||||
workspace: workspaceId,
|
||||
path: removeSuffix(change.path, ".resource.json"),
|
||||
});
|
||||
await ResourceService.deleteResource({ workspace: workspaceId, path: removeSuffix(change.path, ".resource.json") })
|
||||
break;
|
||||
case "resource-type":
|
||||
await ResourceService.deleteResourceType({
|
||||
workspace: workspaceId,
|
||||
path: removeSuffix(change.path, ".resource-type.json"),
|
||||
});
|
||||
break;
|
||||
await ResourceService.deleteResourceType({ workspace: workspaceId, path: removeSuffix(change.path, ".resource-type.json") })
|
||||
break
|
||||
case "flow":
|
||||
await FlowService.deleteFlowByPath({
|
||||
workspace: workspaceId,
|
||||
path: removeSuffix(change.path, ".flow.json"),
|
||||
});
|
||||
break;
|
||||
await FlowService.deleteFlowByPath({ workspace: workspaceId, path: removeSuffix(change.path, ".flow.json") })
|
||||
break
|
||||
case "app":
|
||||
await AppService.deleteApp({
|
||||
workspace: workspaceId,
|
||||
path: removeSuffix(change.path, ".app.json"),
|
||||
});
|
||||
break;
|
||||
await AppService.deleteApp({ workspace: workspaceId, path: removeSuffix(change.path, ".app.json") })
|
||||
break
|
||||
case "variable":
|
||||
await VariableService.deleteVariable({
|
||||
workspace: workspaceId,
|
||||
path: removeSuffix(change.path, ".variable.json"),
|
||||
});
|
||||
break;
|
||||
await VariableService.deleteVariable({ workspace: workspaceId, path: removeSuffix(change.path, ".variable.json") })
|
||||
break
|
||||
default:
|
||||
break;
|
||||
}
|
||||
try {
|
||||
await Deno.remove(stateTarget);
|
||||
} catch {}
|
||||
Deno.remove(stateTarget)
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
colors.green.underline(
|
||||
`Done! All ${changes.length} changes pushed to the remote workspace.`
|
||||
)
|
||||
);
|
||||
console.log(colors.green.underline(`Done! All ${changes.length} changes pushed to the remote workspace.`));
|
||||
|
||||
}
|
||||
|
||||
|
||||
async function applyDiff(
|
||||
workspace: string,
|
||||
remotePath: string,
|
||||
@@ -696,11 +594,11 @@ async function push(
|
||||
| ResourceFile
|
||||
| ResourceTypeFile
|
||||
| FolderFile,
|
||||
diffs: Difference[]
|
||||
diffs: Difference[],
|
||||
) {
|
||||
if (file instanceof ScriptFile) {
|
||||
throw new Error(
|
||||
"This code path should be unreachable - we should never generate diffs for scripts"
|
||||
"This code path should be unreachable - we should never generate diffs for scripts",
|
||||
);
|
||||
} else if (file instanceof FolderFile) {
|
||||
const parts = remotePath.split("/");
|
||||
@@ -711,14 +609,14 @@ async function push(
|
||||
}
|
||||
}
|
||||
if (diffs.length === 0) {
|
||||
console.log("No diffs to apply to " + remotePath);
|
||||
console.log("No diffs to apply to " + remotePath)
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await file.pushDiffs(workspace, remotePath, diffs);
|
||||
} catch (e) {
|
||||
console.error("Failing to apply diffs to " + remotePath);
|
||||
console.error(JSON.stringify(e));
|
||||
console.error("Failing to apply diffs to " + remotePath)
|
||||
console.error(e.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -726,24 +624,18 @@ async function push(
|
||||
const command = new Command()
|
||||
.command("pull")
|
||||
.description(
|
||||
"Pull any remote changes and apply them locally. Use --raw for usage without local state tracking."
|
||||
)
|
||||
.option(
|
||||
"--fail-conflicts",
|
||||
"Error on conflicts (both remote and local have changes on the same item)"
|
||||
"Pull any remote changes and apply them locally. Use --raw for usage without local state tracking.",
|
||||
)
|
||||
.option("--fail-conflicts", "Error on conflicts (both remote and local have changes on the same item)")
|
||||
.option("--yes", "Pull without needing confirmation")
|
||||
.option("--raw", "Pull without using state, just overwrite.")
|
||||
.action(pull as any)
|
||||
.command("push")
|
||||
.description(
|
||||
"Push any local changes and apply them remotely. Use --raw for usage without local state tracking."
|
||||
"Push any local changes and apply them remotely. Use --raw for usage without local state tracking.",
|
||||
)
|
||||
.option(
|
||||
"--fail-conflicts",
|
||||
"Error on conflicts (both remote and local have changes on the same item)"
|
||||
)
|
||||
.option("--skip-pull", "Push without pulling first (you have pulled prior)")
|
||||
.option("--fail-conflicts", "Error on conflicts (both remote and local have changes on the same item)")
|
||||
.option("--skip-pull", "Push without pulling first")
|
||||
.option("--yes", "Push without needing confirmation")
|
||||
.option("--raw", "Push without using state, just overwrite.")
|
||||
.action(push as any);
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
colors,
|
||||
Command,
|
||||
EditVariable,
|
||||
ListableVariable,
|
||||
microdiff,
|
||||
Table,
|
||||
VariableService,
|
||||
@@ -66,7 +67,7 @@ export class VariableFile implements Resource, PushDiffs {
|
||||
if (await VariableService.existsVariable({ workspace, path: remotePath })) {
|
||||
console.log(
|
||||
colors.bold.yellow(
|
||||
`Applying ${diffs.length} diffs to existing variable... ${remotePath}`,
|
||||
`Applying ${diffs.length} diffs to existing variable...`,
|
||||
),
|
||||
);
|
||||
const changeset: EditVariable = {};
|
||||
@@ -75,12 +76,11 @@ export class VariableFile implements Resource, PushDiffs {
|
||||
diff.type !== "REMOVE" &&
|
||||
(
|
||||
diff.path.length !== 1 ||
|
||||
!["path", "value", "is_secret", "description", "account", "is_oauth"].includes(
|
||||
!["path", "value", "is_secret", "description"].includes(
|
||||
diff.path[0] as string,
|
||||
)
|
||||
)
|
||||
) {
|
||||
console.log(colors.red("Invalid variable diff with path " + diff.path));
|
||||
throw new Error("Invalid variable diff with path " + diff.path);
|
||||
}
|
||||
if (diff.type === "CREATE" || diff.type === "CHANGE") {
|
||||
@@ -96,7 +96,6 @@ export class VariableFile implements Resource, PushDiffs {
|
||||
if (!hasChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
await VariableService.updateVariable({
|
||||
workspace,
|
||||
path: remotePath,
|
||||
@@ -104,6 +103,7 @@ export class VariableFile implements Resource, PushDiffs {
|
||||
requestBody: changeset,
|
||||
});
|
||||
|
||||
console.log(changeset);
|
||||
} else {
|
||||
console.log(colors.yellow.bold("Creating new variable..."));
|
||||
await VariableService.createVariable({
|
||||
@@ -121,10 +121,19 @@ export class VariableFile implements Resource, PushDiffs {
|
||||
}
|
||||
}
|
||||
async push(workspace: string, remotePath: string): Promise<void> {
|
||||
let existing: ListableVariable | undefined;
|
||||
try {
|
||||
existing = await VariableService.getVariable({
|
||||
workspace: workspace,
|
||||
path: remotePath,
|
||||
});
|
||||
} catch {
|
||||
existing = undefined;
|
||||
}
|
||||
await this.pushDiffs(
|
||||
workspace,
|
||||
remotePath,
|
||||
microdiff({}, this, { cyclesFix: false }),
|
||||
microdiff(existing ?? {}, this, { cyclesFix: false }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,9 @@ import {
|
||||
Input,
|
||||
setClient,
|
||||
Table,
|
||||
UserService,
|
||||
WorkspaceService,
|
||||
} from "./deps.ts";
|
||||
import { decoverto, model, property } from "./decoverto.ts";
|
||||
import { requireLogin } from "./context.ts";
|
||||
|
||||
@model()
|
||||
export class Workspace {
|
||||
@@ -139,8 +137,6 @@ async function list(opts: GlobalOptions) {
|
||||
}),
|
||||
)
|
||||
.render();
|
||||
|
||||
console.log('Active: ' + colors.green.bold(activeName || 'none'))
|
||||
}
|
||||
|
||||
async function switchC(opts: GlobalOptions, workspaceName: string) {
|
||||
@@ -312,20 +308,12 @@ async function remove(_opts: GlobalOptions, name: string) {
|
||||
await removeWorkspace(name, false, _opts);
|
||||
}
|
||||
|
||||
async function whoami(_opts: GlobalOptions) {
|
||||
await requireLogin(_opts)
|
||||
console.log(await UserService.globalWhoami())
|
||||
const activeName = await getActiveWorkspaceName(_opts);
|
||||
console.log('Active: ' + colors.green.bold(activeName || 'none'))
|
||||
}
|
||||
|
||||
const command = new Command()
|
||||
.description("workspace related commands")
|
||||
.action(list as any)
|
||||
.command("switch")
|
||||
.complete("workspace", async () => (await allWorkspaces()).map((x) => x.name))
|
||||
.description("Switch to another workspace")
|
||||
.arguments("<workspace_name:string:workspace>")
|
||||
.arguments("<workspace_name:string>")
|
||||
.action(switchC as any)
|
||||
.command("add")
|
||||
.description("Add a workspace")
|
||||
@@ -346,9 +334,6 @@ const command = new Command()
|
||||
.command("remove")
|
||||
.description("Remove a workspace")
|
||||
.arguments("<workspace_name:string>")
|
||||
.action(remove as any)
|
||||
.command("whoami")
|
||||
.description("Show the currently active user")
|
||||
.action(whoami as any);
|
||||
.action(remove as any);
|
||||
|
||||
export default command;
|
||||
|
||||
@@ -38,9 +38,7 @@ services:
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
# volumes:
|
||||
# - ./oauth.json/:/usr/src/app/oauth.json
|
||||
|
||||
|
||||
windmill_worker:
|
||||
image: ghcr.io/windmill-labs/windmill:main
|
||||
deploy:
|
||||
@@ -63,6 +61,7 @@ services:
|
||||
# to mount the worker folder to debug,, KEEP_JOB_DIR=true and mount /tmp/windmill
|
||||
volumes:
|
||||
- worker_dependency_cache:/tmp/windmill/cache
|
||||
# - ./oauth.json/:/usr/src/app/oauth.json
|
||||
|
||||
lsp:
|
||||
image: ghcr.io/windmill-labs/windmill-lsp:latest
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
VITE_DEFAULT_WEBHOOK_TYPE=async
|
||||
1
frontend/.gitignore
vendored
1
frontend/.gitignore
vendored
@@ -7,4 +7,3 @@ node_modules
|
||||
CaddyfileRemoteRuben
|
||||
tests-out/
|
||||
storageState.json
|
||||
.env.production
|
||||
@@ -1,6 +1,6 @@
|
||||
# Developing
|
||||
|
||||
## Starting The Development Server
|
||||
## Starting the Development Server
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or
|
||||
`pnpm install` or `yarn`), start a development server:
|
||||
@@ -17,7 +17,7 @@ In the root folder:
|
||||
|
||||
```bash
|
||||
docker build . -t windmill
|
||||
docker compose up db windmill_server windmill_worker
|
||||
docker-compose up db server
|
||||
```
|
||||
|
||||
### 2. Backend is run by cargo
|
||||
@@ -25,9 +25,9 @@ docker compose up db windmill_server windmill_worker
|
||||
**Prerequisites**
|
||||
|
||||
- Install Rust [as explained on the website](https://www.rust-lang.org/tools/install).
|
||||
- Install llvm
|
||||
- Install llvm
|
||||
|
||||
**On OSX:**
|
||||
**on OSX:**
|
||||
```bash
|
||||
brew install llvm caddy gsed
|
||||
|
||||
@@ -36,20 +36,7 @@ docker compose up db windmill_server windmill_worker
|
||||
|
||||
# now, restart your shell. You should now have the `lld` binary on your PATH.
|
||||
```
|
||||
- To test that you have Rust and Cargo installed run `cargo --version`
|
||||
|
||||
- In your terminal, go to the backend directory and run `cargo build`
|
||||
- Run `cargo run`
|
||||
|
||||
**Known issue on M1 Mac while running `cargo build`**
|
||||
- You may encounter `linking with cc failed` build time error.
|
||||
- To solve this run:
|
||||
```bash
|
||||
echo 'export RUSTFLAGS="-L/opt/homebrew/opt/libomp/lib"' >> ~/.zshrc
|
||||
source ~/.zshrc
|
||||
```
|
||||
|
||||
|
||||
|
||||
**Do a Frontend Build**
|
||||
|
||||
In order to run the backend, you need to have a frontend build inside `frontend/build/`.
|
||||
@@ -69,14 +56,6 @@ npm run build
|
||||
# now, you'll have a `frontend/build` folder.
|
||||
```
|
||||
|
||||
**Known issue while running `npm run build`**
|
||||
- You may encounter `FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory` error.
|
||||
- To solve this run:
|
||||
```bash
|
||||
export NODE_OPTIONS=--max_old_space_size=8096
|
||||
```
|
||||
- run `npm run build` again
|
||||
|
||||
In the root folder:
|
||||
|
||||
```bash
|
||||
@@ -103,7 +82,7 @@ sudo caddy run --config ./Caddyfile
|
||||
|
||||
and then go to <http://localhost>
|
||||
|
||||
### 3. Backend is run by remote!
|
||||
### Backend is run by remote!
|
||||
|
||||
```bash
|
||||
sudo caddy run --config ./CaddyfileRemote
|
||||
@@ -140,30 +119,8 @@ Recommended config for VS Code:
|
||||
|
||||
```json
|
||||
"[svelte]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
```
|
||||
|
||||
- turn _format on save_ on
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
The project is built with [SvelteKit](https://kit.svelte.dev/) and uses as output static files.
|
||||
There are others adapters for sveltekit, but we use the static adapter.
|
||||
|
||||
To build the frontend as static assets, use:
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
The output is in the `build` folder.
|
||||
|
||||
The default build assume you serve every non static files as the 200.html file which is catchall. If you prefer a normal layout, you can use:
|
||||
```
|
||||
NOTCATCHALL=true npm run build
|
||||
```
|
||||
which will generate an index.html and allow you to serve the frontend with any static server.
|
||||
|
||||
Env variables used for build are set in .env file. See [https://vitejs.dev/guide/env-and-mode.html#env-files](https://vitejs.dev/guide/env-and-mode.html#env-files) for more details.
|
||||
|
||||
|
||||
2163
frontend/package-lock.json
generated
2163
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill",
|
||||
"version": "1.78.0",
|
||||
"version": "1.70.1",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
@@ -15,18 +15,18 @@
|
||||
"test": "playwright test --config=tests-out/playwright.config.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.31.1",
|
||||
"@playwright/test": "^1.29.2",
|
||||
"@sveltejs/adapter-static": "^1.0.0",
|
||||
"@sveltejs/kit": "^1.0.0-next.589",
|
||||
"@sveltejs/package": "^1.0.2",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tailwindcss/typography": "^0.5.8",
|
||||
"@types/d3": "^7.4.0",
|
||||
"@types/d3-zoom": "^3.0.2",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/vscode": "~1.74.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.49.0",
|
||||
"@typescript-eslint/parser": "^5.48.0",
|
||||
"@windmill-labs/svelte-grid": "^5.1.4",
|
||||
"@windmill-labs/svelvet": "^4.0.20",
|
||||
"@zerodevx/svelte-toast": "^0.8.1",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"cssnano": "^5.1.14",
|
||||
@@ -34,10 +34,8 @@
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
"ol": "^7.2.2",
|
||||
"openapi-typescript-codegen": "^0.23.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"pdfjs-dist": "^3.4.120",
|
||||
"postcss": "^8.4.18",
|
||||
"postcss-load-config": "^4.0.1",
|
||||
"prettier": "^2.8.3",
|
||||
@@ -46,7 +44,6 @@
|
||||
"stylelint-config-recommended": "^9.0.0",
|
||||
"svelte": "^3.55.1",
|
||||
"svelte-awesome": "^3.0.0",
|
||||
"svelte-awesome-color-picker": "^2.4.1",
|
||||
"svelte-check": "^3.0.2",
|
||||
"svelte-highlight": "^6.2.1",
|
||||
"svelte-overlay": "^1.4.1",
|
||||
@@ -74,7 +71,6 @@
|
||||
"chart.js": "^3.9.1",
|
||||
"chartjs-adapter-date-fns": "^3.0.0",
|
||||
"chartjs-plugin-zoom": "^2.0.0",
|
||||
"d3-zoom": "^3.0.0",
|
||||
"date-fns": "^2.29.3",
|
||||
"fast-equals": "^4.0.3",
|
||||
"highlight.js": "^11.7.0",
|
||||
@@ -86,7 +82,7 @@
|
||||
"svelte-autosize": "^1.0.1",
|
||||
"svelte-chartjs": "^3.1.0",
|
||||
"svelte-portal": "^2.2.0",
|
||||
"svelte-select": "^5.3.1",
|
||||
"svelte-select": "^5.0.2",
|
||||
"tailwind-merge": "^1.9.1",
|
||||
"vscode-ws-jsonrpc": "^2.0.1"
|
||||
},
|
||||
|
||||
76
frontend/src/global.d.ts
vendored
76
frontend/src/global.d.ts
vendored
@@ -1,2 +1,78 @@
|
||||
/// <reference types="@sveltejs/kit" />
|
||||
|
||||
declare type Item = import('svelte-dnd-action').Item
|
||||
declare type DndEvent<ItemType = Item> = import('svelte-dnd-action').DndEvent<ItemType>
|
||||
declare namespace svelte.JSX {
|
||||
interface HTMLAttributes<T> {
|
||||
onconsider?: (event: CustomEvent<DndEvent<ItemType>> & { target: EventTarget & T }) => void
|
||||
onfinalize?: (event: CustomEvent<DndEvent<ItemType>> & { target: EventTarget & T }) => void
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@windmill-labs/svelte-grid' {
|
||||
import type { SvelteComponentTyped } from 'svelte'
|
||||
|
||||
export interface Size {
|
||||
w: number
|
||||
h: number
|
||||
}
|
||||
|
||||
export interface Positon {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
interface ItemLayout extends Size, Positon {
|
||||
fixed?: boolean
|
||||
resizable?: boolean
|
||||
draggable?: boolean
|
||||
customDragger?: boolean
|
||||
customResizer?: boolean
|
||||
min?: Size
|
||||
max?: Size
|
||||
}
|
||||
|
||||
export type Item<T> = T & { [width: number]: ItemLayout; data: any }
|
||||
export type FilledItem<T> = T & { [width: number]: Required<ItemLayout>; data: any }
|
||||
|
||||
export interface Props<T> {
|
||||
fillSpace?: boolean
|
||||
items: FilledItem<T>[]
|
||||
rowHeight: number
|
||||
cols: [number, number][]
|
||||
gap?: [number, number]
|
||||
fastStart?: boolean
|
||||
throttleUpdate?: number
|
||||
throttleResize?: number
|
||||
onTopId?: string
|
||||
scroller?: undefined
|
||||
sensor?: number
|
||||
}
|
||||
|
||||
export interface Slots<T> {
|
||||
default: { item: ItemLayout; dataItem: Item<T> }
|
||||
}
|
||||
|
||||
export default class Grid<T = {}> extends SvelteComponentTyped<
|
||||
Props<T>,
|
||||
{
|
||||
pointerup: CustomEvent<{ id: string }>
|
||||
mount: CustomEvent<>
|
||||
},
|
||||
Slots<T>
|
||||
> { }
|
||||
}
|
||||
|
||||
declare module '@windmill-labs/svelte-grid/build/helper/index.mjs' {
|
||||
import { ItemLayout } from '@windmill-labs/svelte-grid'
|
||||
|
||||
const x: {
|
||||
normalize(items: any[], col: any): unknown[]
|
||||
adjust(items: any[], col: any): unknown[]
|
||||
findSpace(item: any, items: any, cols: any): unknown
|
||||
|
||||
item<T>(obj: ItemLayout): Required<ItemLayout>
|
||||
}
|
||||
|
||||
export default x
|
||||
}
|
||||
|
||||
@@ -50,23 +50,12 @@
|
||||
.Template-editor span.mtk20 {
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(155, 155, 155, 0.5);
|
||||
border: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* Flow graph viewer -> Svelvet library internal class overwrite */
|
||||
.Node {
|
||||
display: flex !important;
|
||||
cursor: pointer !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
$: key =
|
||||
apiTokenApps[resource_type]?.key ??
|
||||
(args != undefined
|
||||
? Object.keys(args).filter((x) => ['token', 'password', 'api_key', 'key'].includes(x))[0]
|
||||
? Object.keys(args).filter((x) => ['token', 'password', 'api_key'].includes(x))[0]
|
||||
: undefined)
|
||||
|
||||
let scopes: string[] = []
|
||||
@@ -259,7 +259,6 @@
|
||||
args['token'] == '' &&
|
||||
args['password'] == '' &&
|
||||
args['api_key'] == '' &&
|
||||
args['key'] == '' &&
|
||||
key != undefined) ||
|
||||
(step == 3 && pathError != '') ||
|
||||
!isValid
|
||||
@@ -397,7 +396,7 @@
|
||||
bind:error={pathError}
|
||||
bind:path
|
||||
initialPath=""
|
||||
namePlaceholder={resource_type}
|
||||
namePlaceholder="{resource_type}"
|
||||
kind="resource"
|
||||
/>
|
||||
|
||||
@@ -434,7 +433,7 @@
|
||||
{:else}
|
||||
<Path
|
||||
initialPath=""
|
||||
namePlaceholder={resource_type}
|
||||
namePlaceholder="{resource_type}"
|
||||
bind:error={pathError}
|
||||
bind:path
|
||||
kind="resource"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
faPlus
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import { setInputCat as computeInputCat } from '$lib/utils'
|
||||
import { setInputCat as computeInputCat, type InputCat } from '$lib/utils'
|
||||
import { Badge, Button } from './common'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Icon from 'svelte-awesome'
|
||||
@@ -152,8 +152,6 @@
|
||||
value = enum_?.[0]
|
||||
} else if (inputCat == 'boolean') {
|
||||
value = false
|
||||
} else if (inputCat == 'list') {
|
||||
value = []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -290,7 +288,7 @@
|
||||
on:click={() => {
|
||||
value = value.filter((el) => el != v)
|
||||
if (value.length == 0) {
|
||||
value = []
|
||||
value = undefined
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -452,6 +450,19 @@
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if !required && inputCat != 'resource-object'}
|
||||
<!-- <Tooltip placement="bottom" content="Reset to default value">
|
||||
<Button
|
||||
on:click={() => (value = undefined)}
|
||||
{disabled}
|
||||
color="alternative"
|
||||
size="sm"
|
||||
class="h-8"
|
||||
>
|
||||
<Icon data={faArrowRotateLeft} />
|
||||
</Button>
|
||||
</Tooltip> -->
|
||||
{/if}
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
{#if !compact || (error && error != '')}
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
import { Highlight } from 'svelte-highlight'
|
||||
import { json } from 'svelte-highlight/languages'
|
||||
import TableCustom from './TableCustom.svelte'
|
||||
import { copyToClipboard, truncate } from '$lib/utils'
|
||||
import { Button, Drawer, DrawerContent } from './common'
|
||||
import { truncate } from '$lib/utils'
|
||||
import { Button } from './common'
|
||||
import autosize from 'svelte-autosize'
|
||||
import { ClipboardCopy } from 'lucide-svelte'
|
||||
|
||||
export let result: any
|
||||
export let requireHtmlApproval = false
|
||||
@@ -87,36 +86,16 @@
|
||||
return 'json'
|
||||
}
|
||||
let payload = ''
|
||||
|
||||
let jsonViewer: Drawer
|
||||
</script>
|
||||
|
||||
<Drawer bind:this={jsonViewer} size="900px">
|
||||
<DrawerContent title="Expanded Result" on:close={jsonViewer.closeDrawer}>
|
||||
<svelte:fragment slot="actions">
|
||||
<Button
|
||||
on:click={() => copyToClipboard(JSON.stringify(result, null, 4))}
|
||||
color="light"
|
||||
size="xs"
|
||||
>
|
||||
<div class="flex gap-2 items-center">Copy to clipboard <ClipboardCopy /> </div>
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
<Highlight language={json} code={JSON.stringify(result, null, 4).replace(/\\n/g, '\n')} />
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<div class="inline-highlight">
|
||||
{#if result != undefined}
|
||||
{#if resultKind && resultKind != 'json'}
|
||||
<div class="mb-2 text-gray-500 text-sm bg-gray-50/20">
|
||||
as JSON <input class="windmillapp" type="checkbox" bind:checked={forceJson} /></div
|
||||
as JSON <input type="checkbox" bind:checked={forceJson} /></div
|
||||
>{/if}{#if typeof result == 'object' && Object.keys(result).length > 0}<div
|
||||
class="mb-2 min-w-[400px] 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">
|
||||
<button on:click={jsonViewer.openDrawer}>Expand JSON</button>
|
||||
</div></div
|
||||
class="mb-2 text-sm text-gray-700"
|
||||
>The result keys are: <b>{truncate(Object.keys(result).join(', '), 50)}</b></div
|
||||
>{/if}{#if !forceJson && resultKind == 'table-col'}<div
|
||||
class="grid grid-flow-col-dense border border-gray-200 rounded-md "
|
||||
>
|
||||
@@ -244,10 +223,12 @@
|
||||
><a rel="noreferrer" target="_blank" href={result['approvalPage']}>Approval Page</a></div
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<Highlight language={json} code={JSON.stringify(result, null, 4).replace(/\\n/g, '\n')} />
|
||||
{:else}<Highlight
|
||||
language={json}
|
||||
code={JSON.stringify(result, null, 4).replace(/\\n/g, '\n')}
|
||||
/>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="text-gray-500 text-sm">No result: {JSON.stringify(result)}</div>
|
||||
<div class="text-gray-500 text-sm">No result</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
import type { DropdownItem } from '$lib/utils'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { MoreVertical } from 'lucide-svelte'
|
||||
import { MoreHorizontal } from 'lucide-svelte'
|
||||
import { Button, Menu } from './common'
|
||||
import { goto } from '$app/navigation'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
type Alignment = 'start' | 'end'
|
||||
type Side = 'top' | 'bottom'
|
||||
@@ -14,24 +13,15 @@
|
||||
export let dropdownItems: DropdownItem[]
|
||||
export let name: string | undefined = undefined
|
||||
export let placement: Placement = 'bottom-start'
|
||||
export let btnClasses = ''
|
||||
$: buttonClass = twMerge('!border-0 bg-transparent !p-[6px]', btnClasses)
|
||||
export let btnClasses = '!text-blue-500 bg-transparent'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<Menu {placement} let:close>
|
||||
<Button
|
||||
nonCaptureEvent
|
||||
color="dark"
|
||||
variant="border"
|
||||
size="xs"
|
||||
btnClasses={buttonClass}
|
||||
{...$$restProps}
|
||||
slot="trigger"
|
||||
>
|
||||
<Button nonCaptureEvent color="light" size="xs" {btnClasses} {...$$restProps} slot="trigger">
|
||||
{#if !$$slots.default}
|
||||
<MoreVertical size={20} />
|
||||
<MoreHorizontal size={20} />
|
||||
{:else}
|
||||
<slot />
|
||||
{/if}
|
||||
|
||||
@@ -11,21 +11,17 @@
|
||||
| undefined = undefined
|
||||
</script>
|
||||
|
||||
<div class="inline-flex flex-row items-center truncated">
|
||||
<div class="inline-flex flex-row items-center">
|
||||
<span class="font-semibold">
|
||||
{label}
|
||||
</span>
|
||||
<Required {required} class="!ml-0" />
|
||||
|
||||
{#if format && format != ''}
|
||||
<span class="text-sm italic ml-1 text-indigo-800">
|
||||
({format})
|
||||
</span>
|
||||
{:else}
|
||||
<span class="text-sm italic ml-1 text-indigo-800">
|
||||
({type ?? 'any'}{contentEncoding && contentEncoding != ''
|
||||
? `, encoding: ${contentEncoding}`
|
||||
: ''})</span
|
||||
>
|
||||
{/if}
|
||||
<span class="text-sm italic ml-1 text-indigo-800">
|
||||
({type ?? 'any'}{contentEncoding && contentEncoding != ''
|
||||
? `, encoding: ${contentEncoding}`
|
||||
: ''}{format && format != '' ? `, format: ${format}` : ''}{itemsType?.type
|
||||
? ` of ${itemsType?.type}s`
|
||||
: ''})</span
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation'
|
||||
import { FlowService, ScheduleService, type Flow, type FlowModule } from '$lib/gen'
|
||||
import { initHistory, redo, undo } from '$lib/history'
|
||||
import { page } from '$app/stores'
|
||||
import { FlowService, ScheduleService, type Flow } from '$lib/gen'
|
||||
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, faEye, faPen, faSave } from '@fortawesome/free-solid-svg-icons'
|
||||
import { setContext } from 'svelte'
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import { writable } from 'svelte/store'
|
||||
import CenteredPage from './CenteredPage.svelte'
|
||||
import { Button, ButtonPopup, ButtonPopupItem, UndoRedo } from './common'
|
||||
import { Button, Drawer, DrawerContent } from './common'
|
||||
import { dirtyStore } from './common/confirmationModal/dirtyStore'
|
||||
import UnsavedConfirmationModal from './common/confirmationModal/UnsavedConfirmationModal.svelte'
|
||||
import { OFFSET } from './CronInput.svelte'
|
||||
import ScriptEditorDrawer from './flows/content/ScriptEditorDrawer.svelte'
|
||||
import FlowGraphViewer from './FlowGraphViewer.svelte'
|
||||
import FlowEditor from './flows/FlowEditor.svelte'
|
||||
import type { FlowState } from './flows/flowState'
|
||||
import { dfs } from './flows/flowStore'
|
||||
import { flowStateStore } from './flows/flowState'
|
||||
import { flowStore } from './flows/flowStore'
|
||||
import FlowImportExportMenu from './flows/header/FlowImportExportMenu.svelte'
|
||||
import FlowPreviewButtons from './flows/header/FlowPreviewButtons.svelte'
|
||||
import { loadFlowSchedule, type Schedule } from './flows/scheduleUtils'
|
||||
@@ -25,8 +26,8 @@
|
||||
export let selectedId: string | undefined
|
||||
export let initialArgs: Record<string, any> = {}
|
||||
export let loading = false
|
||||
export let flowStore: Writable<Flow>
|
||||
export let flowStateStore: Writable<FlowState>
|
||||
|
||||
let pathError = ''
|
||||
|
||||
async function createSchedule(path: string) {
|
||||
const { cron, args, enabled } = $scheduleStore
|
||||
@@ -49,10 +50,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
let loadingSave = false
|
||||
|
||||
async function saveFlow(leave: boolean): Promise<void> {
|
||||
loadingSave = true
|
||||
async function saveFlow(): Promise<void> {
|
||||
const flow = cleanInputs($flowStore)
|
||||
const { cron, args, enabled } = $scheduleStore
|
||||
$dirtyStore = false
|
||||
@@ -114,12 +112,8 @@
|
||||
await createSchedule(flow.path)
|
||||
}
|
||||
}
|
||||
loadingSave = false
|
||||
if (leave) {
|
||||
goto(`/flows/get/${$flowStore.path}?workspace_id=${$workspaceStore}`)
|
||||
} else if (initialPath !== $flowStore.path) {
|
||||
goto(`/flows/edit/${$flowStore.path}?workspace_id=${$workspaceStore}`)
|
||||
}
|
||||
sendUserToast(`Flow saved at ${$flowStore.path}`)
|
||||
goto(`/flows/get/${$flowStore.path}?workspace_id=${$workspaceStore}`)
|
||||
}
|
||||
|
||||
let timeout: NodeJS.Timeout | undefined = undefined
|
||||
@@ -147,15 +141,10 @@
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const selectedIdStore = writable<string>(selectedId ?? 'settings-metadata')
|
||||
const selectedIdStore = writable<string>(selectedId)
|
||||
|
||||
const scheduleStore = writable<Schedule>({ args: {}, cron: '', enabled: false })
|
||||
const previewArgsStore = writable<Record<string, any>>(initialArgs)
|
||||
const scriptEditorDrawer = writable<ScriptEditorDrawer | undefined>(undefined)
|
||||
const moving = writable<{ module: FlowModule; modules: FlowModule[] } | undefined>(undefined)
|
||||
const history = initHistory($flowStore)
|
||||
|
||||
const testStepStore = writable<Record<string, any>>({})
|
||||
|
||||
function select(selectedId: string) {
|
||||
selectedIdStore.set(selectedId)
|
||||
@@ -164,13 +153,8 @@
|
||||
setContext<FlowEditorContext>('FlowEditorContext', {
|
||||
selectedId: selectedIdStore,
|
||||
schedule: scheduleStore,
|
||||
previewArgs: previewArgsStore,
|
||||
scriptEditorDrawer,
|
||||
moving,
|
||||
history,
|
||||
flowStateStore,
|
||||
flowStore,
|
||||
testStepStore
|
||||
select,
|
||||
previewArgs: previewArgsStore
|
||||
})
|
||||
|
||||
async function loadSchedule() {
|
||||
@@ -193,82 +177,38 @@
|
||||
|
||||
loadHubScripts()
|
||||
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
switch (event.key) {
|
||||
case 'Z':
|
||||
if (event.ctrlKey) {
|
||||
$flowStore = redo(history)
|
||||
event.preventDefault()
|
||||
}
|
||||
break
|
||||
case 'z':
|
||||
if (event.ctrlKey) {
|
||||
$flowStore = undo(history, $flowStore)
|
||||
$selectedIdStore = 'Input'
|
||||
event.preventDefault()
|
||||
}
|
||||
break
|
||||
case 's':
|
||||
if (event.ctrlKey) {
|
||||
saveFlow(false)
|
||||
event.preventDefault()
|
||||
}
|
||||
break
|
||||
case 'ArrowDown': {
|
||||
let ids = generateIds()
|
||||
let idx = ids.indexOf($selectedIdStore)
|
||||
if (idx > -1 && idx < ids.length - 1) {
|
||||
$selectedIdStore = ids[idx + 1]
|
||||
event.preventDefault()
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'ArrowUp': {
|
||||
let ids = generateIds()
|
||||
let idx = ids.indexOf($selectedIdStore)
|
||||
if (idx > 0 && idx < ids.length) {
|
||||
$selectedIdStore = ids[idx - 1]
|
||||
event.preventDefault()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function generateIds() {
|
||||
return [
|
||||
'settings-metadata',
|
||||
'constants',
|
||||
...dfs($flowStore.value.modules, (module) => module.id)
|
||||
]
|
||||
}
|
||||
let flowViewer: Drawer
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
|
||||
{#if !$userStore?.operator}
|
||||
<ScriptEditorDrawer bind:this={$scriptEditorDrawer} />
|
||||
<UnsavedConfirmationModal />
|
||||
|
||||
<Drawer bind:this={flowViewer} size="75%">
|
||||
<DrawerContent title="View Graph" on:close={flowViewer.closeDrawer} noPadding>
|
||||
<div class="overflow-hidden h-full w-full">
|
||||
<FlowGraphViewer flow={$flowStore} />
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<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"
|
||||
>
|
||||
<div class="flex flex-row gap-4 items-center">
|
||||
<div class="flex flex-row">
|
||||
<FlowImportExportMenu />
|
||||
<UndoRedo
|
||||
undoProps={{ disabled: $history.index === 0 }}
|
||||
redoProps={{ disabled: $history.index === $history.history.length - 1 }}
|
||||
on:undo={() => {
|
||||
$flowStore = undo(history, $flowStore)
|
||||
$selectedIdStore = 'Input'
|
||||
}}
|
||||
on:redo={() => {
|
||||
$flowStore = redo(history)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
btnClasses="inline-flex"
|
||||
startIcon={{ icon: faEye }}
|
||||
variant="border"
|
||||
color="light"
|
||||
size="sm"
|
||||
on:click={flowViewer.openDrawer}
|
||||
>
|
||||
Graph
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="gap-1 flex-row hidden md:flex shrink overflow-hidden">
|
||||
{#if $scheduleStore.enabled}
|
||||
<Button
|
||||
@@ -315,22 +255,12 @@
|
||||
<div class="flex flex-row space-x-2">
|
||||
<FlowPreviewButtons />
|
||||
<div class="center-center">
|
||||
<ButtonPopup
|
||||
loading={loadingSave}
|
||||
size="sm"
|
||||
<Button
|
||||
disabled={pathError != ''}
|
||||
startIcon={{ icon: faSave }}
|
||||
on:click={() => saveFlow(false)}
|
||||
size="sm"
|
||||
on:click={saveFlow}>Save</Button
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -79,16 +79,17 @@
|
||||
</Drawer>
|
||||
<div class="grid grid-cols-3 w-full">
|
||||
<div
|
||||
bind:clientHeight={topHeight}
|
||||
class="{noSide
|
||||
? 'col-span-3'
|
||||
: 'sm:col-span-2 col-span-3'} w-full border border-gray-400 max-h-screen"
|
||||
: 'sm:col-span-2 col-span-3'} w-full border border-gray-400 h-screen"
|
||||
class:overflow-auto={overflowAuto}
|
||||
>
|
||||
<FlowGraph
|
||||
minHeight={400}
|
||||
minHeight={topHeight}
|
||||
modules={flow?.value?.modules}
|
||||
failureModule={flow?.value?.failure_module}
|
||||
on:select={(e) => (stepDetail = e.detail)}
|
||||
on:click={(e) => (stepDetail = e.detail)}
|
||||
/>
|
||||
</div>
|
||||
{#if !noSide}
|
||||
@@ -96,9 +97,7 @@
|
||||
class="w-full border-r border-b border-t border-gray-400 min-h-[150px] p-2 overflow-auto hidden sm:block"
|
||||
>
|
||||
{#if stepDetail == undefined}
|
||||
<SchemaViewer schema={flow?.schema} />
|
||||
|
||||
<span class="font-black text-lg w-full my-4 mt-14">
|
||||
<span class="font-black text-lg w-full my-4">
|
||||
<span>Click on a step to see its details</span>
|
||||
</span>
|
||||
{:else if stepDetail == 'Input'}
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
|
||||
<div
|
||||
class:border={!noBorder}
|
||||
class="grid {!col ? 'grid-cols-2' : 'grid-rows-2'} shadow border-gray-400 h-full max-h-screen"
|
||||
class="grid {!col ? 'grid-cols-2' : 'grid-rows-2'} shadow border-gray-400 h-full"
|
||||
>
|
||||
<div class="bg-white {col ? '' : 'max-h-80'} h-full p-1 overflow-auto relative">
|
||||
<div class="bg-white max-h-80 h-full p-1 overflow-auto relative">
|
||||
<span class="text-gray-500">Result</span>
|
||||
{#if result}
|
||||
<DisplayResult {result} />
|
||||
@@ -24,7 +24,7 @@
|
||||
<div class="text-gray-400">No result (result is undefined)</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="overflow-auto {col ? '' : 'max-h-80'} h-full relative">
|
||||
<div class="overflow-auto max-h-80 h-full relative">
|
||||
<LogViewer content={logs ?? ''} isLoading={false} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,12 +5,13 @@
|
||||
import { Button, Kbd } from './common'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { dfs } from './flows/flowStore'
|
||||
import { dfs, flowStore } from './flows/flowStore'
|
||||
import type { FlowEditorContext } from './flows/types'
|
||||
import { runFlowPreview } from './flows/utils'
|
||||
import SchemaForm from './SchemaForm.svelte'
|
||||
import FlowStatusViewer from '../components/FlowStatusViewer.svelte'
|
||||
import FlowProgressBar from './flows/FlowProgressBar.svelte'
|
||||
import { flowStateStore } from './flows/flowState'
|
||||
import CapturePayload from './flows/content/CapturePayload.svelte'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
|
||||
@@ -20,11 +21,11 @@
|
||||
|
||||
export let jobId: string | undefined = undefined
|
||||
export let job: Job | undefined = undefined
|
||||
let isValid: boolean = true
|
||||
let isRunning: boolean = false
|
||||
let jobProgressReset: () => void
|
||||
|
||||
const { selectedId, previewArgs, flowStateStore, flowStore } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
const { selectedId, previewArgs } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function sliceModules(modules: FlowModule[], upTo: number, idOrders: string[]): FlowModule[] {
|
||||
@@ -104,6 +105,7 @@
|
||||
|
||||
{#if isRunning}
|
||||
<Button
|
||||
disabled={!isValid}
|
||||
color="red"
|
||||
on:click={async () => {
|
||||
isRunning = false
|
||||
@@ -130,6 +132,7 @@
|
||||
color="blue"
|
||||
size="sm"
|
||||
btnClasses="w-full max-w-lg"
|
||||
disabled={!isValid}
|
||||
on:click={() => runPreview($previewArgs)}
|
||||
>
|
||||
Test flow <Kbd class="ml-2">Ctrl+Enter</Kbd>
|
||||
@@ -146,13 +149,14 @@
|
||||
</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 ">
|
||||
<div class="max-h-1/2 overflow-auto border-b border-gray-700">
|
||||
<SchemaForm
|
||||
noVariablePicker
|
||||
compact
|
||||
class="py-4 max-w-3xl"
|
||||
schema={$flowStore.schema}
|
||||
bind:isValid
|
||||
bind:args={$previewArgs}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
localFlowModuleStates?.[innerModules?.[i - 1]?.id ?? '']?.type ==
|
||||
FlowStatusModule.type.SUCCESS
|
||||
) {
|
||||
localFlowModuleStates[mod.id ?? ''] = { type: mod.type, args: job?.args }
|
||||
localFlowModuleStates[mod.id ?? ''] = { type: mod.type }
|
||||
} else if (
|
||||
mod.type === FlowStatusModule.type.WAITING_FOR_EXECUTOR &&
|
||||
localFlowModuleStates[mod.id ?? '']?.scheduled_for == undefined
|
||||
@@ -109,8 +109,7 @@
|
||||
type: mod.type,
|
||||
scheduled_for: 'scheduled for ' + displayDate(job?.['scheduled_for'], true),
|
||||
job_id: job?.id,
|
||||
parent_module: mod['parent_module'],
|
||||
args: job?.args
|
||||
parent_module: mod['parent_module']
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -330,12 +329,10 @@
|
||||
type: FlowStatusModule.type.IN_PROGRESS,
|
||||
logs: e.detail.logs,
|
||||
job_id: e.detail.id,
|
||||
args: e.detail.args,
|
||||
iteration_total: flowJobIds?.flowJobs.length
|
||||
}
|
||||
} else {
|
||||
localFlowModuleStates[flowJobIds.moduleId] = {
|
||||
args: e.detail.args,
|
||||
type: e.detail.success
|
||||
? FlowStatusModule.type.SUCCESS
|
||||
: FlowStatusModule.type.FAILURE,
|
||||
@@ -400,12 +397,10 @@
|
||||
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,
|
||||
@@ -452,17 +447,13 @@
|
||||
<FlowGraph
|
||||
success={isSuccess(job?.['success'])}
|
||||
flowModuleStates={localFlowModuleStates}
|
||||
on:select={(e) => {
|
||||
if (typeof e.detail == 'string') {
|
||||
if (e.detail == 'Input') {
|
||||
selectedNode = 'start'
|
||||
} else if (e.detail == 'Result') {
|
||||
selectedNode = 'end'
|
||||
} else {
|
||||
selectedNode = e.detail
|
||||
}
|
||||
} else {
|
||||
on:click={(e) => {
|
||||
if (e.detail.id) {
|
||||
selectedNode = e.detail.id
|
||||
} else if (e.detail == 'Result') {
|
||||
selectedNode = 'end'
|
||||
} else if (e.detail == 'Input') {
|
||||
selectedNode = 'start'
|
||||
}
|
||||
}}
|
||||
modules={job.raw_flow?.modules ?? []}
|
||||
@@ -506,10 +497,6 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="px-1 border-b border-black">
|
||||
<JobArgs args={node.args} />
|
||||
</div>
|
||||
|
||||
<FlowJobResult
|
||||
loading={job['running'] == true}
|
||||
noBorder
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
|
||||
export let name: string
|
||||
export let tabular = false
|
||||
export let order = ['scripts', 'flows', 'apps', 'schedules', 'variables', 'resources']
|
||||
|
||||
$: $workspaceStore && loadUsage()
|
||||
|
||||
@@ -15,18 +13,10 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if tabular}
|
||||
{#each order as key}
|
||||
<td class="text-center">
|
||||
{usage[key] ?? ''}
|
||||
</td>
|
||||
<div class="flex flex-col text-xs text-gray-600">
|
||||
{#each Object.entries(usage) as [k, v]}
|
||||
<div>
|
||||
{k}: {v}
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="flex flex-col text-xs text-gray-600">
|
||||
{#each Object.entries(usage) as [k, v]}
|
||||
<div>
|
||||
{k}: {v}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -234,14 +234,6 @@
|
||||
name,
|
||||
requestBody: { username: member_name }
|
||||
})
|
||||
await GranularAclService.removeGranularAcls({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: name,
|
||||
kind: 'group_',
|
||||
requestBody: {
|
||||
owner: 'u/' + member_name
|
||||
}
|
||||
})
|
||||
loadGroup()
|
||||
}}>remove</button
|
||||
>
|
||||
|
||||
@@ -82,10 +82,16 @@
|
||||
}
|
||||
|
||||
function connectProperty(rawValue: string) {
|
||||
arg.expr = getDefaultExpr(undefined, previousModuleId, rawValue)
|
||||
arg.type = 'javascript'
|
||||
propertyType = 'javascript'
|
||||
monaco?.setCode(arg.expr)
|
||||
if (isStaticTemplate(inputCat)) {
|
||||
arg.value = `\$\{${rawValue}}`
|
||||
setPropertyType(arg.value)
|
||||
monacoTemplate?.setCode(arg.value)
|
||||
} else {
|
||||
arg.expr = getDefaultExpr(undefined, previousModuleId, rawValue)
|
||||
arg.type = 'javascript'
|
||||
propertyType = 'javascript'
|
||||
monaco?.setCode(arg.expr)
|
||||
}
|
||||
}
|
||||
|
||||
function onFocus() {
|
||||
@@ -148,7 +154,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
{#if !noDynamicToggle}
|
||||
<div class="flex flex-row gap-x-4 gap-y-1 flex-wrap z-10">
|
||||
<div class="flex flex-row gap-x-4 gap-y-1 flex-wrap">
|
||||
<ToggleButtonGroup
|
||||
bind:selected={propertyType}
|
||||
on:selected={(e) => {
|
||||
@@ -190,10 +196,10 @@
|
||||
>
|
||||
{#if isStaticTemplate(inputCat)}
|
||||
<ToggleButton light position="left" value="static" size="xs">
|
||||
{'${} '}
|
||||
<Tooltip
|
||||
>Write text or surround javascript with "{openBracket}" and "{closeBracket}". Use
|
||||
`result` to connect to another node's output.
|
||||
{'${} '}Template <Tooltip
|
||||
>Write javascript expressions between "{openBracket}" and "{closeBracket}". You may
|
||||
refer to contextual objects like 'flow_input', or 'result' or functions like
|
||||
'resource' and 'variable'
|
||||
</Tooltip></ToggleButton
|
||||
>
|
||||
{:else}
|
||||
@@ -206,11 +212,9 @@
|
||||
value="javascript"
|
||||
startIcon={{ icon: faCode }}
|
||||
size="xs"
|
||||
> <Tooltip
|
||||
>Write javascript expressions directly, using 'flow_input' or 'result'. You can use
|
||||
multiline javascript.
|
||||
</Tooltip></ToggleButton
|
||||
>
|
||||
Dynamic (JS)
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -301,4 +305,6 @@
|
||||
Not recognized input type {argName}
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-sm text-gray-700">Argument at {argName} is undefined</p>
|
||||
{/if}
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { Schema } from '$lib/common'
|
||||
import { VariableService, type InputTransform } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { allTrue } from '$lib/utils'
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Button } from './common'
|
||||
import InputTransformForm from './InputTransformForm.svelte'
|
||||
import ItemPicker from './ItemPicker.svelte'
|
||||
import VariableEditor from './VariableEditor.svelte'
|
||||
|
||||
export let schema: Schema
|
||||
export let args: Record<string, InputTransform | any> = {}
|
||||
|
||||
export let isValid: boolean = true
|
||||
export let extraLib: string = 'missing extraLib'
|
||||
export let previousModuleId: string | undefined = undefined
|
||||
|
||||
export let filter: string[] | undefined = undefined
|
||||
export let noDynamicToggle = false
|
||||
|
||||
let clazz: string = ''
|
||||
export { clazz as class }
|
||||
|
||||
let inputCheck: { [id: string]: boolean } = {}
|
||||
$: isValid = allTrue(inputCheck) ?? false
|
||||
|
||||
$: if (args == undefined || typeof args !== 'object') {
|
||||
args = {}
|
||||
}
|
||||
|
||||
function removeExtraKey() {
|
||||
const nargs = {}
|
||||
Object.keys(args ?? {}).forEach((key) => {
|
||||
if (keys.includes(key)) {
|
||||
nargs[key] = args[key]
|
||||
}
|
||||
})
|
||||
args = nargs
|
||||
}
|
||||
|
||||
let pickForField: string | undefined
|
||||
let itemPicker: ItemPicker | undefined = undefined
|
||||
let variableEditor: VariableEditor | undefined = undefined
|
||||
|
||||
let keys: string[] = []
|
||||
$: {
|
||||
let lkeys = Object.keys(schema?.properties ?? {})
|
||||
if (schema?.properties && JSON.stringify(lkeys) != JSON.stringify(keys)) {
|
||||
keys = lkeys
|
||||
removeExtraKey()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="w-full {clazz}">
|
||||
{#if keys.length > 0}
|
||||
{#each keys as argName, i (argName)}
|
||||
{#if (!filter || filter.includes(argName)) && Object.keys(schema.properties ?? {}).includes(argName)}
|
||||
<div class="z-10">
|
||||
<InputTransformForm
|
||||
{previousModuleId}
|
||||
bind:arg={args[argName]}
|
||||
bind:schema
|
||||
bind:argName
|
||||
bind:inputCheck={inputCheck[argName]}
|
||||
bind:extraLib
|
||||
{variableEditor}
|
||||
{itemPicker}
|
||||
bind:pickForField
|
||||
{noDynamicToggle}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="text-gray-500 text-sm">No inputs</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<ItemPicker
|
||||
bind:this={itemPicker}
|
||||
pickCallback={(path, _) => {
|
||||
if (pickForField) {
|
||||
args[pickForField].value = '$var:' + path
|
||||
}
|
||||
}}
|
||||
itemName="Variable"
|
||||
extraField="path"
|
||||
loadItems={async () =>
|
||||
(await VariableService.listVariable({ workspace: $workspaceStore ?? '' })).map((x) => ({
|
||||
name: x.path,
|
||||
...x
|
||||
}))}
|
||||
>
|
||||
<div
|
||||
slot="submission"
|
||||
class="flex flex-row-reverse w-full bg-white border-t border-gray-200 rounded-bl-lg rounded-br-lg"
|
||||
>
|
||||
<Button
|
||||
variant="border"
|
||||
color="blue"
|
||||
size="sm"
|
||||
startIcon={{ icon: faPlus }}
|
||||
on:click={() => {
|
||||
variableEditor?.initNew?.()
|
||||
}}
|
||||
>
|
||||
New variable
|
||||
</Button>
|
||||
</div>
|
||||
</ItemPicker>
|
||||
|
||||
<VariableEditor bind:this={variableEditor} />
|
||||
@@ -1,331 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { 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 ResourcePicker from './ResourcePicker.svelte'
|
||||
import type { SchemaProperty } from '$lib/common'
|
||||
import autosize from 'svelte-autosize'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import Range from './Range.svelte'
|
||||
import LightweightSchemaForm from './LightweightSchemaForm.svelte'
|
||||
|
||||
export let label: string = ''
|
||||
export let value: any
|
||||
|
||||
export let defaultValue: any = undefined
|
||||
|
||||
export let description: string = ''
|
||||
export let format: string = ''
|
||||
export let contentEncoding: 'base64' | 'binary' | undefined = undefined
|
||||
export let type: string | undefined = undefined
|
||||
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 itemsType:
|
||||
| { type?: 'string' | 'number' | 'bytes'; contentEncoding?: 'base64' }
|
||||
| undefined = undefined
|
||||
export let displayHeader = true
|
||||
export let properties: { [name: string]: SchemaProperty } | undefined = undefined
|
||||
export let extra: Record<string, any> = {}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: maxHeight = maxRows ? `${1 + maxRows * 1.2}em` : `auto`
|
||||
|
||||
$: validateInput(pattern, value)
|
||||
|
||||
let error: string = ''
|
||||
|
||||
let el: HTMLTextAreaElement | undefined = undefined
|
||||
|
||||
let rawValue: string | undefined = undefined
|
||||
|
||||
$: {
|
||||
if (rawValue) {
|
||||
try {
|
||||
value = JSON.parse(rawValue)
|
||||
error = ''
|
||||
} catch (err) {
|
||||
error = err.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
error = ''
|
||||
if (inputCat === 'object') {
|
||||
evalValueToRaw()
|
||||
validateInput(pattern, value)
|
||||
}
|
||||
}
|
||||
|
||||
export function evalValueToRaw() {
|
||||
if (value) {
|
||||
rawValue = JSON.stringify(value, null, 4)
|
||||
}
|
||||
}
|
||||
|
||||
function fileChanged(e: any, cb: (v: string | undefined) => void) {
|
||||
let t = e.target
|
||||
if (t && 'files' in t && t.files.length > 0) {
|
||||
let reader = new FileReader()
|
||||
reader.onload = (e: any) => {
|
||||
cb(e.target.result.split('base64,')[1])
|
||||
}
|
||||
reader.readAsDataURL(t.files[0])
|
||||
} else {
|
||||
cb(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
export function focus() {
|
||||
el?.focus()
|
||||
if (el) {
|
||||
el.style.height = '5px'
|
||||
el.style.height = el.scrollHeight + 50 + 'px'
|
||||
}
|
||||
}
|
||||
|
||||
function validateInput(pattern: string | undefined, v: any): void {
|
||||
if (required && (v == undefined || v == null || v === '')) {
|
||||
error = 'This field is required'
|
||||
valid = false
|
||||
} else {
|
||||
if (pattern && !testRegex(pattern, v)) {
|
||||
error = `Should match ${pattern}`
|
||||
valid = false
|
||||
} else {
|
||||
error = ''
|
||||
valid = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function testRegex(pattern: string, value: any): boolean {
|
||||
try {
|
||||
const regex = new RegExp(pattern)
|
||||
return regex.test(value)
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if (value == undefined || value == null) {
|
||||
value = defaultValue
|
||||
if (defaultValue === undefined || defaultValue === null) {
|
||||
if (inputCat === 'string') {
|
||||
value = ''
|
||||
} else if (inputCat == 'enum') {
|
||||
value = enum_?.[0]
|
||||
} else if (inputCat == 'boolean') {
|
||||
value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: inputCat = computeInputCat(type, format, itemsType?.type, enum_, contentEncoding)
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col w-full min-w-[250px]">
|
||||
<div>
|
||||
{#if displayHeader}
|
||||
<FieldHeader {label} {required} {type} {contentEncoding} {format} {itemsType} />
|
||||
{/if}
|
||||
|
||||
{#if description}
|
||||
<div class="text-sm italic pb-1">
|
||||
{description}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex space-x-1">
|
||||
{#if inputCat == 'number'}
|
||||
{#if extra['min'] != undefined && extra['max'] != undefined}
|
||||
<div class="flex w-full gap-1">
|
||||
<span>{extra['min']}</span>
|
||||
<div class="grow">
|
||||
<Range bind:value min={extra['min']} max={extra['max']} />
|
||||
</div>
|
||||
<span>{extra['max']}</span>
|
||||
<span class="mx-2"><Badge large color="blue">{value}</Badge></span>
|
||||
</div>
|
||||
{:else}
|
||||
<input
|
||||
on:focus={(e) => {
|
||||
window.dispatchEvent(new Event('pointerup'))
|
||||
dispatch('focus')
|
||||
}}
|
||||
type="number"
|
||||
class={valid
|
||||
? ''
|
||||
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-30 bg-red-100'}
|
||||
placeholder={defaultValue ?? ''}
|
||||
bind:value
|
||||
min={extra['min']}
|
||||
max={extra['max']}
|
||||
on:input={() => dispatch('input', { value, isRaw: true })}
|
||||
/>
|
||||
{/if}
|
||||
{:else if inputCat == 'boolean'}
|
||||
<Toggle
|
||||
on:pointerdown={(e) => {
|
||||
e?.stopPropagation()
|
||||
window.dispatchEvent(new Event('pointerup'))
|
||||
}}
|
||||
class={valid
|
||||
? ''
|
||||
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-30 bg-red-100'}
|
||||
bind:checked={value}
|
||||
/>
|
||||
{#if type == 'boolean' && value == undefined}
|
||||
<span> Not set</span>
|
||||
{/if}
|
||||
{:else if inputCat == 'list'}
|
||||
<div>
|
||||
<div>
|
||||
{#each value ?? [] as v, i}
|
||||
<div class="flex flex-row max-w-md mt-1">
|
||||
{#if itemsType?.type == 'number'}
|
||||
<input type="number" bind:value={v} />
|
||||
{:else if itemsType?.type == 'string' && itemsType?.contentEncoding == 'base64'}
|
||||
<input
|
||||
type="file"
|
||||
class="my-6"
|
||||
on:change={(x) => fileChanged(x, (val) => (value[i] = val))}
|
||||
multiple={false}
|
||||
/>
|
||||
{:else}
|
||||
<input type="text" bind:value={v} />
|
||||
{/if}
|
||||
<Button
|
||||
variant="border"
|
||||
color="red"
|
||||
size="sm"
|
||||
btnClasses="mx-6"
|
||||
on:click={() => {
|
||||
value = value.filter((el) => el != v)
|
||||
if (value.length == 0) {
|
||||
value = undefined
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon data={faMinus} />
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<Button
|
||||
variant="border"
|
||||
color="blue"
|
||||
size="sm"
|
||||
btnClasses="mt-1"
|
||||
on:click={() => {
|
||||
if (value == undefined || !Array.isArray(value)) {
|
||||
value = []
|
||||
}
|
||||
value = value.concat('')
|
||||
}}
|
||||
>
|
||||
<Icon data={faPlus} class="mr-2" />
|
||||
Add item
|
||||
</Button>
|
||||
<span class="ml-2">
|
||||
{(value ?? []).length} item{(value ?? []).length > 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
{:else if inputCat == 'resource-object'}
|
||||
<ObjectResourceInput {format} bind:value />
|
||||
{:else if inputCat == 'object'}
|
||||
{#if properties && Object.keys(properties).length > 0}
|
||||
<div class="p-4 pl-8 border rounded w-full">
|
||||
<LightweightSchemaForm
|
||||
schema={{ properties, $schema: '', required: [], type: 'object' }}
|
||||
bind:args={value}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<textarea
|
||||
bind:this={el}
|
||||
on:focus={(e) => {
|
||||
window.dispatchEvent(new Event('pointerup'))
|
||||
dispatch('focus')
|
||||
}}
|
||||
use:autosize
|
||||
style="max-height: {maxHeight}"
|
||||
on:input={() => {
|
||||
dispatch('input', { rawValue: value, isRaw: false })
|
||||
}}
|
||||
class="col-span-10 {valid
|
||||
? ''
|
||||
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-30 bg-red-100'}"
|
||||
placeholder={defaultValue ? JSON.stringify(defaultValue, null, 4) : ''}
|
||||
bind:value={rawValue}
|
||||
/>
|
||||
{/if}
|
||||
{:else if inputCat == 'enum'}
|
||||
<select
|
||||
on:focus={(e) => {
|
||||
window.dispatchEvent(new Event('pointerup'))
|
||||
dispatch('focus')
|
||||
}}
|
||||
class="px-6"
|
||||
bind:value
|
||||
>
|
||||
{#each enum_ ?? [] as e}
|
||||
<option>{e}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else if inputCat == 'date'}
|
||||
<input class="inline-block" type="datetime-local" bind:value />
|
||||
{:else if inputCat == 'base64'}
|
||||
<input
|
||||
type="file"
|
||||
class="my-6"
|
||||
on:change={(x) => fileChanged(x, (val) => (value = val))}
|
||||
multiple={false}
|
||||
/>
|
||||
{:else if inputCat == 'resource-string'}
|
||||
<ResourcePicker
|
||||
bind:value
|
||||
resourceType={format.split('-').length > 1
|
||||
? format.substring('resource-'.length)
|
||||
: undefined}
|
||||
/>
|
||||
{:else if inputCat == 'string'}
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex flex-row w-full items- justify-between">
|
||||
<textarea
|
||||
rows="1"
|
||||
bind:this={el}
|
||||
on:focus={(e) => {
|
||||
window.dispatchEvent(new Event('pointerup'))
|
||||
dispatch('focus')
|
||||
}}
|
||||
on:blur={() => dispatch('blur')}
|
||||
use:autosize
|
||||
type="text"
|
||||
class="col-span-10 {valid
|
||||
? ''
|
||||
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-30 bg-red-100'}"
|
||||
placeholder={defaultValue ?? ''}
|
||||
bind:value
|
||||
on:input={() => {
|
||||
dispatch('input', { rawValue: value, isRaw: false })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,35 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { Schema } from '$lib/common'
|
||||
import LightweightArgInput from './LightweightArgInput.svelte'
|
||||
|
||||
export let schema: Schema
|
||||
export let args: Record<string, any> | undefined = undefined
|
||||
|
||||
$: if (args === undefined) {
|
||||
args = {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="w-full">
|
||||
{#each Object.keys(schema.properties ?? {}) as argName, i (argName)}
|
||||
<div>
|
||||
{#if typeof args == 'object' && schema?.properties[argName] && args}
|
||||
<LightweightArgInput
|
||||
label={argName}
|
||||
description={schema.properties[argName].description}
|
||||
bind:value={args[argName]}
|
||||
type={schema.properties[argName].type}
|
||||
required={schema.required.includes(argName)}
|
||||
pattern={schema.properties[argName].pattern}
|
||||
defaultValue={schema.properties[argName].default}
|
||||
enum_={schema.properties[argName].enum}
|
||||
format={schema.properties[argName].format}
|
||||
contentEncoding={schema.properties[argName].contentEncoding}
|
||||
properties={schema.properties[argName].properties}
|
||||
itemsType={schema.properties[argName].items}
|
||||
extra={schema.properties[argName]}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -42,7 +42,7 @@
|
||||
<button on:click={logViewer.openDrawer}>Expand</button>
|
||||
<div class="py-2 pr-2 text-xs flex gap-2 items-center">
|
||||
Auto scroll
|
||||
<input class="windmillapp" type="checkbox" bind:checked={scroll} />
|
||||
<input type="checkbox" bind:checked={scroll} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,17 +8,14 @@
|
||||
import LogViewer from './LogViewer.svelte'
|
||||
import DisplayResult from './DisplayResult.svelte'
|
||||
import Button from './common/button/Button.svelte'
|
||||
import { flowStateStore, testStepStore } from './flows/flowState'
|
||||
import { flowStore } from './flows/flowStore'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import type { FlowEditorContext } from './flows/types'
|
||||
|
||||
export let mod: FlowModule
|
||||
export let schema: Schema
|
||||
|
||||
const { flowStore, flowStateStore, testStepStore } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
// Test
|
||||
let testJobLoader: TestJobLoader
|
||||
let testIsLoading = false
|
||||
@@ -60,7 +57,6 @@
|
||||
if (testJob && !testJob.canceled && testJob.type == 'CompletedJob' && `result` in testJob) {
|
||||
if ($flowStateStore[mod.id]?.previewResult) {
|
||||
$flowStateStore[mod.id].previewResult = testJob.result
|
||||
$flowStateStore = $flowStateStore
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,7 +86,6 @@
|
||||
detailed={false}
|
||||
topButton
|
||||
bind:args={stepArgs}
|
||||
isFlow={false}
|
||||
/>
|
||||
{#if testIsLoading}
|
||||
<Button on:click={testJobLoader?.cancelJob} btnClasses="w-full mt-4" color="red" size="sm">
|
||||
|
||||
@@ -167,7 +167,7 @@
|
||||
{#if showOptions}
|
||||
<ul
|
||||
class="options"
|
||||
transition:fly|local={{ duration: 200, y: 5 }}
|
||||
transition:fly={{ duration: 200, y: 5 }}
|
||||
on:mousedown|preventDefault={handleOptionMousedown}
|
||||
>
|
||||
{#each filtered as option}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<span class="flex items-center space-x-2 mb-2">
|
||||
<h1>{title}</h1>
|
||||
{#if tooltip}
|
||||
<Tooltip scale={0.9}>{tooltip}</Tooltip>
|
||||
<Tooltip>{tooltip}</Tooltip>
|
||||
{/if}
|
||||
</span>
|
||||
{:else}
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
import FolderEditor from './FolderEditor.svelte'
|
||||
import GroupEditor from './GroupEditor.svelte'
|
||||
import { random_adj } from './random_positive_adjetive'
|
||||
import Badge from './common/badge/Badge.svelte'
|
||||
|
||||
type PathKind = 'resource' | 'script' | 'variable' | 'flow' | 'schedule' | 'app'
|
||||
let meta: Meta | undefined = undefined
|
||||
@@ -200,10 +199,6 @@
|
||||
}
|
||||
|
||||
function initPath() {
|
||||
if (path != undefined && path != '') {
|
||||
meta = pathToMeta(path)
|
||||
return
|
||||
}
|
||||
if (initialPath == undefined || initialPath == '') {
|
||||
reset()
|
||||
} else {
|
||||
@@ -282,19 +277,15 @@
|
||||
folderCreated = undefined
|
||||
}}
|
||||
>
|
||||
{#if !folderCreated}
|
||||
<div class="flex flex-row">
|
||||
<input class="mr-2" placeholder="New folder name" bind:value={newFolderName} />
|
||||
<Button
|
||||
size="md"
|
||||
startIcon={{ icon: faPlus }}
|
||||
disabled={!newFolderName}
|
||||
on:click={addFolder}
|
||||
>
|
||||
New folder
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-row">
|
||||
<input class="mr-2" placeholder="New folder name" bind:value={newFolderName} />
|
||||
<Button size="md" startIcon={{ icon: faPlus }} disabled={!newFolderName} on:click={addFolder}>
|
||||
New folder
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if folderCreated}
|
||||
<div class="mt-8" />
|
||||
<FolderEditor name={folderCreated} />
|
||||
{/if}
|
||||
</DrawerContent>
|
||||
@@ -311,7 +302,7 @@
|
||||
{#if meta != undefined}
|
||||
<div class="flex gap-4 shrink">
|
||||
<label class="block">
|
||||
<span class="text-gray-700 text-sm whitespace-nowrap">Owner</span>
|
||||
<span class="text-gray-700 text-sm whitespace-nowrap"> </span>
|
||||
|
||||
<ToggleButtonGroup
|
||||
class="mt-0.5"
|
||||
@@ -414,26 +405,8 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col w-full mt-4">
|
||||
<div class="flex justify-start w-full">
|
||||
<Badge
|
||||
color="gray"
|
||||
class="center-center !bg-gray-300 !text-gray-600 !w-[70px] !h-[24px] rounded-r-none"
|
||||
>
|
||||
Full path
|
||||
</Badge>
|
||||
<input
|
||||
type="text"
|
||||
readonly
|
||||
value={path}
|
||||
size={path?.length || 50}
|
||||
class="font-mono !text-xs max-w-[calc(100%-70px)] !w-auto !h-[24px] !py-0 !border-l-0 !rounded-l-none"
|
||||
on:focus={({ currentTarget }) => {
|
||||
currentTarget.select()
|
||||
}}
|
||||
/>
|
||||
<!-- <span class="font-mono text-sm break-all">{path}</span> -->
|
||||
</div>
|
||||
<div class="flex-row flex justify-between">
|
||||
<div><span class="font-mono text-sm">{path}</span></div>
|
||||
<div class="text-red-600 text-2xs">{error}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -49,11 +49,22 @@
|
||||
</script>
|
||||
|
||||
{#if notClickable}
|
||||
<span use:popperRef on:mouseenter={open} on:mouseleave={close} class={$$props.class}>
|
||||
<span
|
||||
use:popperRef
|
||||
on:mouseenter={open}
|
||||
on:mouseleave={close}
|
||||
class={$$props.class}
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
{:else}
|
||||
<button use:popperRef on:mouseenter={open} on:mouseleave={close} on:click class={$$props.class}>
|
||||
<button
|
||||
use:popperRef
|
||||
on:mouseenter={open}
|
||||
on:mouseleave={close}
|
||||
on:click
|
||||
class={$$props.class}
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -8,41 +8,19 @@
|
||||
import Select from 'svelte-select'
|
||||
import AppConnect from './AppConnect.svelte'
|
||||
import ResourceEditor from './ResourceEditor.svelte'
|
||||
import { SELECT_INPUT_DEFAULT_STYLE } from '../defaults'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let resources: Resource[] = []
|
||||
|
||||
export let initialValue: string | undefined = undefined
|
||||
export let value: string | undefined = initialValue
|
||||
export let resourceType: string | undefined = undefined
|
||||
|
||||
let valueSelect =
|
||||
initialValue || value
|
||||
? {
|
||||
value: value ?? initialValue,
|
||||
label: value ?? initialValue
|
||||
}
|
||||
: undefined
|
||||
|
||||
let collection = [valueSelect]
|
||||
|
||||
async function loadResources(resourceType: string | undefined) {
|
||||
const nc = (
|
||||
await ResourceService.listResource({
|
||||
workspace: $workspaceStore!,
|
||||
resourceType
|
||||
})
|
||||
).map((x) => ({
|
||||
value: x.path,
|
||||
label: x.path
|
||||
}))
|
||||
|
||||
if (!nc.find((x) => x.value == value) && (initialValue || value)) {
|
||||
nc.push({ value: value ?? initialValue!, label: value ?? initialValue! })
|
||||
}
|
||||
collection = nc
|
||||
const v = value
|
||||
resources = await ResourceService.listResource({ workspace: $workspaceStore!, resourceType })
|
||||
value = v
|
||||
}
|
||||
|
||||
$: {
|
||||
if ($workspaceStore) {
|
||||
loadResources(resourceType)
|
||||
@@ -50,6 +28,10 @@
|
||||
}
|
||||
$: dispatch('change', value)
|
||||
|
||||
$: collection = resources.map((x) => ({
|
||||
value: x.path,
|
||||
label: x.path
|
||||
}))
|
||||
let appConnect: AppConnect
|
||||
let resourceEditor: ResourceEditor
|
||||
</script>
|
||||
@@ -58,7 +40,6 @@
|
||||
on:refresh={async (e) => {
|
||||
await loadResources(resourceType)
|
||||
value = e.detail
|
||||
valueSelect = { value: e.detail, label: e.detail }
|
||||
}}
|
||||
newPageOAuth
|
||||
bind:this={appConnect}
|
||||
@@ -68,29 +49,22 @@
|
||||
bind:this={resourceEditor}
|
||||
on:refresh={async (e) => {
|
||||
await loadResources(resourceType)
|
||||
console.log(e)
|
||||
if (e.detail) {
|
||||
value = e.detail
|
||||
valueSelect = { value: e.detail, label: e.detail }
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="flex flex-row gap-x-1 w-full">
|
||||
<Select
|
||||
value={valueSelect}
|
||||
on:change={(e) => {
|
||||
value = e.detail.value
|
||||
valueSelect = e.detail
|
||||
}}
|
||||
on:clear={() => {
|
||||
value = undefined
|
||||
valueSelect = undefined
|
||||
}}
|
||||
listAutoWidth={false}
|
||||
--height="34px"
|
||||
value={collection.find((x) => x.value == value)}
|
||||
bind:justValue={value}
|
||||
items={collection}
|
||||
class="text-clip grow min-w-0"
|
||||
placeholder="{resourceType ?? 'any'} resource"
|
||||
inputStyles={SELECT_INPUT_DEFAULT_STYLE.inputStyles}
|
||||
containerStyles={SELECT_INPUT_DEFAULT_STYLE.containerStyles}
|
||||
placeholder="{resourceType} resource"
|
||||
/>
|
||||
{#if value && value != ''}
|
||||
<Button variant="border" size="xs" on:click={() => resourceEditor?.initEdit?.(value ?? '')}>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
label: 'Failed',
|
||||
data:
|
||||
failed?.map((job) => ({
|
||||
x: job.started_at as any,
|
||||
x: job.created_at as any,
|
||||
y: job.duration_ms,
|
||||
id: job.id,
|
||||
path: job.script_path
|
||||
@@ -57,7 +57,7 @@
|
||||
label: 'Successful',
|
||||
data:
|
||||
success?.map((job) => ({
|
||||
x: job.started_at as any,
|
||||
x: job.created_at as any,
|
||||
y: job.duration_ms,
|
||||
id: job.id,
|
||||
path: job.script_path
|
||||
@@ -71,10 +71,7 @@
|
||||
enabled: true,
|
||||
modifierKey: 'ctrl' as 'ctrl',
|
||||
onPanComplete: ({ chart }) => {
|
||||
dispatch('zoom', {
|
||||
min: addSeconds(new Date(chart.scales.x.min), -1),
|
||||
max: addSeconds(new Date(chart.scales.x.max), 1)
|
||||
})
|
||||
dispatch('zoom', { min: new Date(chart.scales.x.min), max: new Date(chart.scales.x.max) })
|
||||
}
|
||||
},
|
||||
zoom: {
|
||||
@@ -83,10 +80,7 @@
|
||||
},
|
||||
mode: 'x' as 'x',
|
||||
onZoom: ({ chart }) => {
|
||||
dispatch('zoom', {
|
||||
min: addSeconds(new Date(chart.scales.x.min), -1),
|
||||
max: addSeconds(new Date(chart.scales.x.max), 1)
|
||||
})
|
||||
dispatch('zoom', { min: new Date(chart.scales.x.min), max: new Date(chart.scales.x.max) })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,22 +88,6 @@
|
||||
function getPath(x: any): string {
|
||||
return x.path
|
||||
}
|
||||
|
||||
$: minTime = getMinTime(jobs)
|
||||
|
||||
function addSeconds(date: Date, seconds: number): Date {
|
||||
date.setTime(date.getTime() + seconds * 1000)
|
||||
return date
|
||||
}
|
||||
function getMinTime(jobs: CompletedJob[] | undefined): Date {
|
||||
return addSeconds(new Date(jobs?.[jobs?.length - 1]?.started_at ?? new Date().toString()), -15)
|
||||
}
|
||||
|
||||
$: maxTime = getMaxTime(jobs)
|
||||
|
||||
function getMaxTime(jobs: CompletedJob[] | undefined): Date {
|
||||
return addSeconds(new Date(jobs?.[0]?.started_at ?? new Date().toString()), 15)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Scatter
|
||||
@@ -137,8 +115,7 @@
|
||||
display: false
|
||||
},
|
||||
type: 'time',
|
||||
min: minTime,
|
||||
max: maxTime
|
||||
min: jobs?.[jobs?.length - 1]?.created_at ?? new Date().toString()
|
||||
},
|
||||
y: {
|
||||
grid: {
|
||||
|
||||
@@ -49,7 +49,6 @@
|
||||
export let loading = false
|
||||
export let noVariablePicker = false
|
||||
export let viewCliRun = false
|
||||
export let isFlow: boolean
|
||||
|
||||
export let args: Record<string, any> = decodeArgs($page.url.searchParams.get('args') ?? undefined)
|
||||
|
||||
@@ -65,9 +64,9 @@
|
||||
let scheduledForStr: string | undefined
|
||||
let invisible_to_owner: false
|
||||
|
||||
$: cliCommand = `wmill ${isFlow ? 'flow' : 'script'} run ${runnable?.path} -d '${JSON.stringify(
|
||||
args
|
||||
)}'`
|
||||
$: cliCommand = `wmill ${runnable?.kind} run ${runnable?.path} ${Object.entries(args)
|
||||
.map(([k, v]) => `-i ${k}=${JSON.stringify(v)}`)
|
||||
.join(' ')}`
|
||||
</script>
|
||||
|
||||
<div class="max-w-6xl">
|
||||
@@ -146,7 +145,7 @@
|
||||
</Button>
|
||||
</div>
|
||||
{#if viewOptions}
|
||||
<div transition:slide|local class="mt-6">
|
||||
<div transition:slide class="mt-6">
|
||||
<div class="border rounded-md p-3 pt-4">
|
||||
<div class="flex flex-row items-end">
|
||||
<div class="w-max md:w-2/3 mt-2 mb-1">
|
||||
@@ -214,14 +213,14 @@
|
||||
<div class="my-10" />
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
size="sm"
|
||||
endIcon={{ icon: viewCliOptions ? faChevronUp : faChevronDown }}
|
||||
on:click={() => (viewCliOptions = !viewCliOptions)}
|
||||
>
|
||||
Run it from the CLI
|
||||
</Button>
|
||||
{#if viewCliOptions}
|
||||
<div transition:slide|local class="mt-2 px-4 pt-2">
|
||||
<div transition:slide class="mt-2 px-4 pt-2">
|
||||
<InlineCodeCopy content={cliCommand} />
|
||||
<CliHelpBox />
|
||||
</div>
|
||||
|
||||
@@ -1,27 +1,34 @@
|
||||
<script lang="ts">
|
||||
import type { Schema } from '$lib/common'
|
||||
import { VariableService } from '$lib/gen'
|
||||
import { VariableService, type InputTransform } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { allTrue } from '$lib/utils'
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import { slide } from 'svelte/transition'
|
||||
import ArgInput from './ArgInput.svelte'
|
||||
import { Button } from './common'
|
||||
import InputTransformForm from './InputTransformForm.svelte'
|
||||
import ItemPicker from './ItemPicker.svelte'
|
||||
import VariableEditor from './VariableEditor.svelte'
|
||||
|
||||
export let inputTransform = false
|
||||
export let schema: Schema
|
||||
export let args: Record<string, any> = {}
|
||||
export let args: Record<string, InputTransform | any> = {}
|
||||
export let disabledArgs: string[] = []
|
||||
export let disabled = false
|
||||
|
||||
export let editableSchema = false
|
||||
export let isValid: boolean = true
|
||||
export let extraLib: string = 'missing extraLib'
|
||||
export let autofocus = false
|
||||
export let previousModuleId: string | undefined = undefined
|
||||
|
||||
export let shouldHideNoInputs: boolean = false
|
||||
export let compact = false
|
||||
export let password: string | undefined = undefined
|
||||
export let noVariablePicker = false
|
||||
export let filter: string[] | undefined = undefined
|
||||
export let noDynamicToggle = false
|
||||
export let flexWrap = false
|
||||
export let noDelete = false
|
||||
|
||||
@@ -36,19 +43,17 @@
|
||||
}
|
||||
|
||||
function removeExtraKey() {
|
||||
const nargs = {}
|
||||
Object.keys(args ?? {}).forEach((key) => {
|
||||
if (keys.includes(key)) {
|
||||
nargs[key] = args[key]
|
||||
if (!keys.includes(key)) {
|
||||
delete args[key]
|
||||
delete inputCheck[key]
|
||||
}
|
||||
})
|
||||
args = nargs
|
||||
}
|
||||
|
||||
let pickForField: string | undefined
|
||||
let itemPicker: ItemPicker | undefined = undefined
|
||||
let variableEditor: VariableEditor | undefined = undefined
|
||||
|
||||
let keys: string[] = []
|
||||
$: {
|
||||
let lkeys = Object.keys(schema?.properties ?? {})
|
||||
@@ -64,60 +69,48 @@
|
||||
<div class="w-full {clazz} {flexWrap ? 'flex flex-row flex-wrap gap-x-6 gap-y-2' : ''}">
|
||||
{#if keys.length > 0}
|
||||
{#each keys as argName, i (argName)}
|
||||
{#if Object.keys(schema.properties ?? {}).includes(argName)}
|
||||
<div>
|
||||
{#if typeof args == 'object' && schema?.properties[argName]}
|
||||
{#if editableSchema}
|
||||
<ArgInput
|
||||
autofocus={i == 0 && autofocus}
|
||||
label={argName}
|
||||
bind:description={schema.properties[argName].description}
|
||||
bind:value={args[argName]}
|
||||
type={schema.properties[argName].type}
|
||||
required={schema.required.includes(argName)}
|
||||
bind:pattern={schema.properties[argName].pattern}
|
||||
bind:valid={inputCheck[argName]}
|
||||
defaultValue={schema.properties[argName].default}
|
||||
bind:enum_={schema.properties[argName].enum}
|
||||
bind:format={schema.properties[argName].format}
|
||||
contentEncoding={schema.properties[argName].contentEncoding}
|
||||
properties={schema.properties[argName].properties}
|
||||
bind:itemsType={schema.properties[argName].items}
|
||||
disabled={disabledArgs.includes(argName) || disabled}
|
||||
{editableSchema}
|
||||
{compact}
|
||||
password={argName == password}
|
||||
{variableEditor}
|
||||
{itemPicker}
|
||||
bind:pickForField
|
||||
bind:extra={schema.properties[argName]}
|
||||
/>
|
||||
{:else}
|
||||
<ArgInput
|
||||
autofocus={i == 0 && autofocus}
|
||||
label={argName}
|
||||
description={schema.properties[argName].description}
|
||||
bind:value={args[argName]}
|
||||
type={schema.properties[argName].type}
|
||||
required={schema.required.includes(argName)}
|
||||
pattern={schema.properties[argName].pattern}
|
||||
bind:valid={inputCheck[argName]}
|
||||
defaultValue={schema.properties[argName].default}
|
||||
enum_={schema.properties[argName].enum}
|
||||
format={schema.properties[argName].format}
|
||||
contentEncoding={schema.properties[argName].contentEncoding}
|
||||
properties={schema.properties[argName].properties}
|
||||
itemsType={schema.properties[argName].items}
|
||||
disabled={disabledArgs.includes(argName) || disabled}
|
||||
{editableSchema}
|
||||
{compact}
|
||||
password={argName == password}
|
||||
{variableEditor}
|
||||
{itemPicker}
|
||||
bind:pickForField
|
||||
extra={schema.properties[argName]}
|
||||
/>
|
||||
{/if}
|
||||
{#if !filter || filter.includes(argName)}
|
||||
<div transition:slide|local>
|
||||
{#if inputTransform}
|
||||
<InputTransformForm
|
||||
{previousModuleId}
|
||||
bind:arg={args[argName]}
|
||||
bind:schema
|
||||
bind:argName
|
||||
bind:inputCheck={inputCheck[argName]}
|
||||
bind:extraLib
|
||||
{variableEditor}
|
||||
{itemPicker}
|
||||
bind:pickForField
|
||||
{noDynamicToggle}
|
||||
/>
|
||||
{:else if typeof args == 'object'}
|
||||
<ArgInput
|
||||
autofocus={i == 0 && autofocus}
|
||||
label={argName}
|
||||
bind:description={schema.properties[argName].description}
|
||||
bind:value={args[argName]}
|
||||
type={schema.properties[argName].type}
|
||||
required={schema.required.includes(argName)}
|
||||
bind:pattern={schema.properties[argName].pattern}
|
||||
bind:valid={inputCheck[argName]}
|
||||
defaultValue={schema.properties[argName].default}
|
||||
bind:enum_={schema.properties[argName].enum}
|
||||
bind:format={schema.properties[argName].format}
|
||||
contentEncoding={schema.properties[argName].contentEncoding}
|
||||
properties={schema.properties[argName].properties}
|
||||
bind:itemsType={schema.properties[argName].items}
|
||||
disabled={disabledArgs.includes(argName) || disabled}
|
||||
{editableSchema}
|
||||
{compact}
|
||||
password={argName == password}
|
||||
{variableEditor}
|
||||
{itemPicker}
|
||||
bind:pickForField
|
||||
bind:extra={schema.properties[argName]}
|
||||
/>
|
||||
{:else}
|
||||
Expected argument to be an object, got {JSON.stringify(args)} instead
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -132,7 +125,11 @@
|
||||
bind:this={itemPicker}
|
||||
pickCallback={(path, _) => {
|
||||
if (pickForField) {
|
||||
args[pickForField] = '$var:' + path
|
||||
if (inputTransform) {
|
||||
args[pickForField].value = '$var:' + path
|
||||
} else {
|
||||
args[pickForField] = '$var:' + path
|
||||
}
|
||||
}
|
||||
}}
|
||||
itemName="Variable"
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { Script, ScriptService } from '$lib/gen'
|
||||
|
||||
import { goto } from '$app/navigation'
|
||||
import { page } from '$app/stores'
|
||||
import { inferArgs } from '$lib/infer'
|
||||
import { initialCode } from '$lib/script_helpers'
|
||||
import { initialCode, isInitialCode } from '$lib/script_helpers'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { emptySchema, encodeState, sendUserToast, setQueryWithoutLoad } from '$lib/utils'
|
||||
import Path from './Path.svelte'
|
||||
import RadioButton from './RadioButton.svelte'
|
||||
import ScriptEditor from './ScriptEditor.svelte'
|
||||
import ScriptSchema from './ScriptSchema.svelte'
|
||||
import CenteredPage from './CenteredPage.svelte'
|
||||
import UnsavedConfirmationModal from './common/confirmationModal/UnsavedConfirmationModal.svelte'
|
||||
import { dirtyStore } from './common/confirmationModal/dirtyStore'
|
||||
import { Button, ButtonPopup, ButtonPopupItem, Kbd } from './common'
|
||||
import { faPen, faSave } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Button, Kbd } from './common'
|
||||
import { faChevronDown, faChevronUp, faPen, faSave } from '@fortawesome/free-solid-svg-icons'
|
||||
import Breadcrumb from './common/breadcrumb/Breadcrumb.svelte'
|
||||
import LanguageIcon from './common/languageIcons/LanguageIcon.svelte'
|
||||
import type { SupportedLanguage } from '$lib/common'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import SettingSection from './SettingSection.svelte'
|
||||
|
||||
export let script: Script
|
||||
export let initialPath: string = ''
|
||||
@@ -31,34 +32,14 @@
|
||||
['Go', Script.language.GO],
|
||||
['Bash', Script.language.BASH]
|
||||
]
|
||||
const scriptKindOptions: { value: Script.kind; title: string; desc?: string }[] = [
|
||||
{
|
||||
value: Script.kind.SCRIPT,
|
||||
title: 'Action'
|
||||
},
|
||||
{
|
||||
value: Script.kind.TRIGGER,
|
||||
title: 'Trigger',
|
||||
desc: 'First module of flows to trigger them based on external changes. These kind of scripts are usually running on a schedule to periodically look for changes.'
|
||||
},
|
||||
{
|
||||
value: Script.kind.APPROVAL,
|
||||
title: 'Approval',
|
||||
desc: 'Send notifications externally to ask for approval to continue a flow.'
|
||||
},
|
||||
{
|
||||
value: Script.kind.FAILURE,
|
||||
title: 'Error Handler',
|
||||
desc: 'Handle errors in flows after all retry attempts have been exhausted.'
|
||||
}
|
||||
]
|
||||
let viewScriptKind = script.kind !== Script.kind.SCRIPT
|
||||
|
||||
let pathError = ''
|
||||
|
||||
let summaryC: HTMLInputElement | undefined = undefined
|
||||
let pathC: Path | undefined = undefined
|
||||
let loadingSave = false
|
||||
|
||||
$: setQueryWithoutLoad($page.url, [{ key: 'state', value: encodeState(script) }])
|
||||
$: setQueryWithoutLoad($page.url, 'state', encodeState(script))
|
||||
$: step = Number($page.url.searchParams.get('step')) || 1
|
||||
|
||||
if (script.content == '') {
|
||||
@@ -73,8 +54,7 @@
|
||||
script.content = initialCode(language, kind, template)
|
||||
}
|
||||
|
||||
async function editScript(leave: boolean): Promise<void> {
|
||||
loadingSave = true
|
||||
async function editScript(): Promise<void> {
|
||||
try {
|
||||
$dirtyStore = false
|
||||
localStorage.removeItem(script.path)
|
||||
@@ -103,17 +83,12 @@
|
||||
kind: script.kind
|
||||
}
|
||||
})
|
||||
if (leave) {
|
||||
history.replaceState(history.state, '', `/scripts/edit/${newHash}?step=2`)
|
||||
goto(`/scripts/get/${newHash}?workspace_id=${$workspaceStore}`)
|
||||
} else {
|
||||
await goto(`/scripts/edit/${newHash}?step=2`)
|
||||
script.hash = newHash
|
||||
}
|
||||
sendUserToast(`New script created at hash ${newHash}`)
|
||||
history.replaceState(history.state, '', `/scripts/edit/${newHash}?step=2`)
|
||||
goto(`/scripts/get/${newHash}?workspace_id=${$workspaceStore}`)
|
||||
} catch (error) {
|
||||
sendUserToast(`Impossible to save the script: ${error.body || error.message}`, true)
|
||||
sendUserToast(`Impossible to save the script: ${error.body}`, true)
|
||||
}
|
||||
loadingSave = false
|
||||
}
|
||||
|
||||
async function changeStep(step: number) {
|
||||
@@ -139,7 +114,10 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
|
||||
{#if !$userStore?.operator}
|
||||
<UnsavedConfirmationModal />
|
||||
<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">
|
||||
@@ -205,24 +183,14 @@
|
||||
>
|
||||
Next {#if step == 1}<Kbd>Enter</Kbd>{/if}
|
||||
</Button>
|
||||
<ButtonPopup
|
||||
loading={loadingSave}
|
||||
<Button
|
||||
size="sm"
|
||||
variant={step == 1 ? 'border' : 'contained'}
|
||||
disabled={step === 1 && pathError !== ''}
|
||||
btnClasses={step == 1 && initialPath == '' ? 'invisible' : ''}
|
||||
startIcon={{ icon: faSave }}
|
||||
on:click={() => editScript(false)}
|
||||
on:click={editScript}>Save</Button
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -230,111 +198,132 @@
|
||||
<!-- 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}
|
||||
<h2 class="border-b pb-1 mt-8 mb-2">Path</h2>
|
||||
<Path
|
||||
bind:this={pathC}
|
||||
bind:error={pathError}
|
||||
bind:path={script.path}
|
||||
{initialPath}
|
||||
on:enter={() => changeStep(2)}
|
||||
namePlaceholder="script"
|
||||
kind="script"
|
||||
/>
|
||||
<h2 class="border-b pb-1 mt-12 mb-4">Summary</h2>
|
||||
<input
|
||||
type="text"
|
||||
bind:this={summaryC}
|
||||
bind:value={script.summary}
|
||||
placeholder="Short summary to be displayed when listed"
|
||||
/>
|
||||
|
||||
<h2 class="border-b pb-1 mt-12 mb-6">Language</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={template == 'pgsql' ? 'blue' : 'dark'}
|
||||
btnClasses={template == 'pgsql' ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
|
||||
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}
|
||||
on:click={() => {
|
||||
script.language = Script.language.DENO
|
||||
template = 'pgsql'
|
||||
initContent(script.language, script.kind, template)
|
||||
}}
|
||||
>
|
||||
<LanguageIcon lang="pgsql" /><span class="ml-2 py-2">PostgreSQL</span>
|
||||
<LanguageIcon {lang} /><span class="ml-2 py-4">{label}</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>
|
||||
</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
|
||||
>
|
||||
<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={() => {
|
||||
{/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={() => {
|
||||
script.language = Script.language.DENO
|
||||
template = 'pgsql'
|
||||
initContent(script.language, script.kind, template)
|
||||
}}
|
||||
>
|
||||
<LanguageIcon lang="pgsql" /><span class="ml-2 py-4">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-4">MySQL</span>
|
||||
</Button> -->
|
||||
</div>
|
||||
<div class="mt-16 mb-4">
|
||||
<Button
|
||||
color="light"
|
||||
size="sm"
|
||||
endIcon={{ icon: viewScriptKind ? faChevronUp : faChevronDown }}
|
||||
on:click={() => (viewScriptKind = !viewScriptKind)}
|
||||
>
|
||||
Tag this script as having a specific purpose inside flows
|
||||
</Button>
|
||||
</div>
|
||||
{#if viewScriptKind}
|
||||
<div class="max-w-lg">
|
||||
<RadioButton
|
||||
label="Script Type"
|
||||
options={[
|
||||
['Action', Script.kind.SCRIPT],
|
||||
[
|
||||
{
|
||||
title: 'Trigger',
|
||||
desc: `First module of flows to trigger them based on watching changes external periodically using an internal state`
|
||||
},
|
||||
Script.kind.TRIGGER
|
||||
],
|
||||
[
|
||||
{
|
||||
title: 'Approval',
|
||||
desc: `Send notification externally to ask for approval to continue a flow`
|
||||
},
|
||||
Script.kind.APPROVAL
|
||||
],
|
||||
[
|
||||
{
|
||||
title: 'Error Handler',
|
||||
desc: `Handle errors for flows after all retries attempts have been exhausted`
|
||||
},
|
||||
Script.kind.FAILURE
|
||||
]
|
||||
|
||||
// ['Command Handler', Script.kind.COMMAND]
|
||||
]}
|
||||
on:change={(e) => {
|
||||
if (isInitialCode(script.content)) {
|
||||
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}
|
||||
initContent(script.language, e.detail, template)
|
||||
}
|
||||
}}
|
||||
bind:value={script.kind}
|
||||
/>
|
||||
</div>
|
||||
</SettingSection>
|
||||
{/if}
|
||||
<!-- <div class="ml-3">
|
||||
<Toggle
|
||||
bind:checked={script.is_template}
|
||||
options={{ right: 'Save as a workspace template' }}
|
||||
/>
|
||||
</div> -->
|
||||
</CenteredPage>
|
||||
{:else if step === 2}
|
||||
<ScriptEditor
|
||||
@@ -354,5 +343,3 @@
|
||||
{:else}
|
||||
Script Builder not available to operators
|
||||
{/if}
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
import { Button, Drawer, DrawerContent } from './common'
|
||||
import HighlightCode from './HighlightCode.svelte'
|
||||
import FlowPathViewer from './flows/content/FlowPathViewer.svelte'
|
||||
import { SELECT_INPUT_DEFAULT_STYLE } from '../defaults'
|
||||
|
||||
export let initialPath: string | undefined = undefined
|
||||
export let scriptPath: string | undefined = undefined
|
||||
@@ -88,8 +87,6 @@
|
||||
bind:justValue={scriptPath}
|
||||
{items}
|
||||
placeholder="Pick a {itemKind}"
|
||||
inputStyles={SELECT_INPUT_DEFAULT_STYLE.inputStyles}
|
||||
containerStyles={SELECT_INPUT_DEFAULT_STYLE.containerStyles}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { ChevronDown } from 'lucide-svelte'
|
||||
import { slide } from 'svelte/transition'
|
||||
import type { IntRange } from '../common'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
|
||||
export let title: string
|
||||
export let tooltip: string = ''
|
||||
export let element: `h${IntRange<1, 6>}` = 'h2'
|
||||
export let accordion = false
|
||||
let showContent = !accordion
|
||||
</script>
|
||||
|
||||
<div class="border-b [&:has(button:hover)]:border-gray-400 duration-200 pb-1 mt-8 mb-2">
|
||||
{#if accordion}
|
||||
<button
|
||||
class="flex w-full justify-start items-center"
|
||||
on:click={() => (showContent = !showContent)}
|
||||
>
|
||||
<span class="rounded-full hover:bg-gray-100 focus:bg-gray-100 p-1 mr-1">
|
||||
<ChevronDown size={22} class="rotate-0 duration-300 {showContent ? '!-rotate-180' : ''}" />
|
||||
</span>
|
||||
<svelte:element this={element}>
|
||||
{title}
|
||||
{#if tooltip}
|
||||
<Tooltip scale={0.9} class="mb-0.5">
|
||||
{tooltip}
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</svelte:element>
|
||||
</button>
|
||||
{:else}
|
||||
<svelte:element this={element}>
|
||||
{title}
|
||||
{#if tooltip}
|
||||
<Tooltip scale={0.9} class="mb-0.5">
|
||||
{tooltip}
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</svelte:element>
|
||||
{/if}
|
||||
</div>
|
||||
{#if showContent}
|
||||
<div transition:slide={{ duration: 300 }}>
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
@@ -99,7 +99,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
export function format() {
|
||||
function format() {
|
||||
if (editor) {
|
||||
code = getCode()
|
||||
editor.getAction('editor.action.formatDocument').run()
|
||||
@@ -110,10 +110,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
export function focus() {
|
||||
editor?.focus()
|
||||
}
|
||||
|
||||
let width = 0
|
||||
async function loadMonaco() {
|
||||
model = meditor.createModel(code, lang, mUri.parse(uri))
|
||||
@@ -209,7 +205,7 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<div bind:this={divEl} class="{$$props.class ?? ''} editor" bind:clientWidth={width} />
|
||||
<div bind:this={divEl} class="{$$props.class} editor" bind:clientWidth={width} />
|
||||
|
||||
<style>
|
||||
.editor {
|
||||
|
||||
@@ -2,20 +2,15 @@
|
||||
import { ChevronDown, ChevronUp } from 'lucide-svelte'
|
||||
import { slide } from 'svelte/transition'
|
||||
import Button from './common/button/Button.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
|
||||
export let text: string
|
||||
export let tooltip: string | undefined = undefined
|
||||
export let view = false
|
||||
</script>
|
||||
|
||||
<Button color="light" on:click={() => (view = !view)} variant="border"
|
||||
>{text}
|
||||
{#if tooltip}
|
||||
<Tooltip wrapperClass="mx-1">{tooltip}</Tooltip>
|
||||
{/if}
|
||||
{#if !view}<ChevronDown />{:else}<ChevronUp />{/if}</Button
|
||||
>
|
||||
{#if view}
|
||||
<div class="my-4 px-2" transition:slide|local><slot /></div>
|
||||
<div class="my-4 px-2" transition:slide><slot /></div>
|
||||
{/if}
|
||||
|
||||
@@ -577,10 +577,6 @@
|
||||
})
|
||||
}
|
||||
|
||||
export function focus() {
|
||||
editor?.focus()
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
loadMonaco()
|
||||
@@ -600,7 +596,7 @@
|
||||
<div
|
||||
bind:this={divEl}
|
||||
style="height: 18px;"
|
||||
class="{$$props.class ?? ''} template rounded-lg min-h-4 mx-0.5 overflow-clip"
|
||||
class="{$$props.class} template rounded-lg min-h-4 mx-0.5"
|
||||
bind:clientWidth={width}
|
||||
/>
|
||||
|
||||
|
||||
@@ -186,9 +186,9 @@
|
||||
syncIteration++
|
||||
await loadTestJob(id)
|
||||
let nextIteration = 50
|
||||
if (syncIteration > ITERATIONS_BEFORE_SLOW_REFRESH) {
|
||||
if (syncIteration == ITERATIONS_BEFORE_SLOW_REFRESH) {
|
||||
nextIteration = 500
|
||||
} else if (syncIteration > ITERATIONS_BEFORE_SUPER_SLOW_REFRESH) {
|
||||
} else if (syncIteration == ITERATIONS_BEFORE_SUPER_SLOW_REFRESH) {
|
||||
nextIteration = 2000
|
||||
}
|
||||
setTimeout(() => syncer(id), nextIteration)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let options: {
|
||||
left?: string
|
||||
@@ -8,8 +7,6 @@
|
||||
} = {}
|
||||
export let checked: boolean = false
|
||||
export let disabled = false
|
||||
export let textClass = ''
|
||||
export let textStyle = ''
|
||||
|
||||
export let size: 'sm' | 'xs' = 'sm'
|
||||
const id = (Math.random() + 1).toString(36).substring(10)
|
||||
@@ -25,13 +22,7 @@
|
||||
>
|
||||
{#if Boolean(options?.left)}
|
||||
<span
|
||||
class={twMerge(
|
||||
'ml-2 font-medium duration-200',
|
||||
disabled ? 'text-gray-500' : 'text-gray-900',
|
||||
size === 'xs' ? 'text-xs' : 'text-sm',
|
||||
textClass
|
||||
)}
|
||||
style={textStyle}
|
||||
class="mr-2 text-sm font-medium duration-200 {disabled ? 'text-gray-600' : 'text-gray-900'}"
|
||||
>
|
||||
{options?.left}
|
||||
</span>
|
||||
@@ -60,13 +51,9 @@
|
||||
</div>
|
||||
{#if Boolean(options?.right)}
|
||||
<span
|
||||
class={twMerge(
|
||||
'ml-2 font-medium duration-200',
|
||||
disabled ? 'text-gray-500' : 'text-gray-900',
|
||||
size === 'xs' ? 'text-xs' : 'text-sm',
|
||||
textClass
|
||||
)}
|
||||
style={textStyle}
|
||||
class="ml-2 text-sm font-medium duration-200
|
||||
{disabled ? 'text-gray-500' : 'text-gray-900'}
|
||||
{size === 'xs' ? 'text-xs' : 'text-sm'}"
|
||||
>
|
||||
{options?.right}
|
||||
</span>
|
||||
|
||||
@@ -13,12 +13,10 @@
|
||||
<Popover notClickable {placement} class={wrapperClass}>
|
||||
<Icon
|
||||
class="{light
|
||||
? 'text-gray-400'
|
||||
? 'text-gray-300'
|
||||
: ' text-gray-500'} font-thin inline-block align-middle w-4 {$$props.class}"
|
||||
data={faInfoCircle}
|
||||
{scale}
|
||||
/>
|
||||
<svelte:fragment slot="text">
|
||||
<slot />
|
||||
</svelte:fragment>
|
||||
<span slot="text"> <slot /> </span>
|
||||
</Popover>
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
export function closeDrawer() {
|
||||
drawer?.closeDrawer()
|
||||
const index = $page.url.href.lastIndexOf('#')
|
||||
if (index === -1) return
|
||||
const hashRemoved = $page.url.href.slice(0, index)
|
||||
goto(hashRemoved)
|
||||
}
|
||||
|
||||
@@ -57,8 +57,7 @@
|
||||
decryptSecret: false
|
||||
})
|
||||
can_write =
|
||||
getV.workspace_id == $workspaceStore &&
|
||||
canWrite(edit_path, getV.extra_perms ?? {}, $userStore)
|
||||
getV.workspace_id == $workspaceStore && canWrite(path, getV.extra_perms ?? {}, $userStore)
|
||||
|
||||
variable = {
|
||||
value: getV.value ?? '',
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
...new Set(
|
||||
workers
|
||||
.filter((worker) => {
|
||||
return worker.ip != 'unretrievable IP' && worker.last_ping && worker.last_ping < 300
|
||||
const date = new Date().getTime() - 300 * 60
|
||||
const ping_at = new Date(worker.ping_at).getTime()
|
||||
return worker.ip != 'unretrievable IP' && ping_at > date
|
||||
})
|
||||
.map((worker) => worker.ip)
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user