Compare commits

..
Author SHA1 Message Date
Ruben Fiszel 9007072274 all 2025-03-04 15:24:10 +01:00
Ruben Fiszel fba9e7ef03 fix: improve cancel performance 2025-03-03 23:50:13 +01:00
Ruben Fiszelandsmuun 72109b01d7 whitelabel improvements (#5405)
* hide resource

* more customizations

* disable tooltips globally & hide other elemetns

* hide variable picker in autogenerated ui too

* change to disableX and context

* all

* all

* all

* all

* all

* all

* fix

* fix

* fix

---------

Co-authored-by: smuun <silas@athenaintelligence.ai>
2025-03-03 22:35:05 +01:00
pyranota e7428bf84d nit: Pip index url -> UV index url (#5410) 2025-03-03 20:59:21 +01:00
Ruben Fiszelandrubenfiszel 56b417c669 chore(main): release 1.473.0 (#5399)
* chore(main): release 1.473.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-03-03 15:18:06 +01:00
Ruben Fiszel 8e1e37bf9e improve menu a hrefs 2025-03-03 14:58:49 +01:00
Guilhem 5e965aec15 prevent modal from closing when clicking inside popover (#5406) 2025-03-03 14:57:34 +01:00
pyranota 04ffbf8c26 fix: remove typings_extensions from python sdk
* fix: make python-client compatible with python >= 3.13

* remove completely

* remove sys import
2025-03-03 14:56:52 +01:00
Ruben Fiszel b601883bc0 fix sqlx 2025-03-03 14:51:51 +01:00
Ruben Fiszel 81ed965812 make PG_SCHEMA appear in logs 2025-03-03 14:44:03 +01:00
Ruben Fiszel e1ff00117c fix: improve db loads by adding index on audit 2025-03-03 14:23:35 +01:00
Ruben Fiszel 55d599d4b6 add PG_SCHEMA support 2025-03-03 11:25:20 +01:00
HugoCasa 46c784574a feat: app s3 input anonymous delete (#5401)
* feat: app s3 input anonymous delete

* tmp ee ref

* fix build
2025-02-27 16:28:56 +01:00
Ruben Fiszel 3063001491 fix: migrations do not refer to public schema anymore (#5400) 2025-02-27 14:37:39 +01:00
HugoCasaandellipsis-dev[bot] d30979d04e hub script fetch retry (#5379)
* hub script fetch retry

* use backon

* oups

* Update backend/windmill-common/src/scripts.rs

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* retry whole logic

* nits

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-02-27 11:29:14 +01:00
HugoCasa 7bf9e25ede feat: track workspace runnables used in flows (#5369)
* feat: track workspace runnables used in flows

* track script hash

* weird

* do it with lock

* Revert "feat: add support for | None and Optional in python (#5361)"

This reverts commit 9736355d5f.

* Revert "Revert "feat: add support for | None and Optional in python (#5361)""

This reverts commit bb8f709894.

* update openapi

* delete old in lock_modules + don't track hub scripts
2025-02-27 10:01:12 +01:00
Ruben Fiszel f406da9976 recover previous step in more cases in flow 2025-02-26 23:40:05 +01:00
Ruben Fiszelandrubenfiszel 07f91af535 chore(main): release 1.472.1 (#5397)
* chore(main): release 1.472.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-02-26 21:45:51 +01:00
HugoCasa cb559d6083 fix: disable bundling using env var (#5396) 2025-02-26 21:41:49 +01:00
Ruben Fiszel 63af3ce0b1 chore(main): release 1.472.0 (#5393) 2025-02-26 19:29:22 +01:00
Ruben Fiszel 3bc891a0dd fix compile 2025-02-26 19:23:55 +01:00
Ruben Fiszel 5569e4d495 fix: downgrade v8 to fix some rare panics 2025-02-26 19:12:24 +01:00
Alexander Petric 7f290bbf6a fix(frontend): markdown shows single backtick in single line code block (#5391) 2025-02-26 17:35:10 +01:00
69fc8a98ae feat(frontend): migrate toggle to melt (4/4) (#5329)
* use melt menu in sidebar

* stop keyboard navigation for disabled items

* use melt menu for FavoriteMenu and WorkspaceMenu

* fix popover placement for menuButton

* use melt menu for operator menu

* fix notification

* fix operator menu

* Use melt menu in FlowJobsMenu

* use melt menu for AppMenu

* clean code

* clean code

* add use clickOutside option to Menu

* use pointerdown_outside

* use pointerdown_outside

# Conflicts:
#	frontend/src/lib/components/meltComponents/Menu.svelte

* use pointerdown in menus

* add max-h to app dropdown menu

* keep more open in operator menu

* add a MenuItem component

* clean

* nit

* nit

* clean code

* put conditionalMelt as utility function

* remove unused Portal

* Add debounce effect in operator menu

* fix component jumping due to z-index

* format pages

* migrate dropdown to melt

* migrate toggle to melt

* migrate popup to melt popover

* fix missing toggle item

* feat: remove `pip` fallback option for python and ansible (#5186)

* refactor!: Remove `pip` fallback option for python and ansible

BREAKING CHANGE: pip was deprecated since 1.425.0 (2024-11-15)

* fix errors in main.rs

* fix tests

* remove nsjail for pip

* fix imports

* fix compilation error

* reinforce melt types

* fix racing condition issue in closing operator menu

* nit

* fix id conflix with melt element

* nit

* clean code

* use melt dropdown instead of menubar

* prevent modal from closing on click outside button in menu

* Apply automatic changes

* fix nit

* nit

* close dropdown when opening a new one

* replace MenuV2 with melt Menu (1/4) (#5214)

* use melt menu in sidebar

* stop keyboard navigation for disabled items

* use melt menu for FavoriteMenu and WorkspaceMenu

* fix popover placement for menuButton

* use melt menu for operator menu

* fix notification

* fix operator menu

* Use melt menu in FlowJobsMenu

* use melt menu for AppMenu

* clean code

* clean code

* add use clickOutside option to Menu

* use pointerdown_outside

* use pointerdown_outside

# Conflicts:
#	frontend/src/lib/components/meltComponents/Menu.svelte

* use pointerdown in menus

* add max-h to app dropdown menu

* keep more open in operator menu

* add a MenuItem component

* clean

* nit

* nit

* clean code

* put conditionalMelt as utility function

* remove unused Portal

* Add debounce effect in operator menu

* fix component jumping due to z-index

* feat: remove `pip` fallback option for python and ansible (#5186)

* refactor!: Remove `pip` fallback option for python and ansible

BREAKING CHANGE: pip was deprecated since 1.425.0 (2024-11-15)

* fix errors in main.rs

* fix tests

* remove nsjail for pip

* fix imports

* fix compilation error

* reinforce melt types

* fix racing condition issue in closing operator menu

* nit

* fix id conflix with melt element

* nit

* prevent modal from closing on click outside button in menu

---------

Co-authored-by: pyranota <92104930+pyranota@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
# Conflicts:
#	frontend/src/lib/components/meltComponents/MenuItem.svelte
#	frontend/src/lib/utils.ts

* clean

* fix z index and render

* fix initialize of dropdownmenu after melt migration

* feat: add support for | None and Optional in python (#5361)

* feat: add support for | None and Optional in python

* update python parser package

* add local rooting for MenuItem

* fix z index

* clean

* nit

* nit

* clean code

* nit

* nit

* clean code

* reinforce melt types

* wip

* reiforce instance select types for toggleButton

* nit

* fix double event

* fix selectedTable toggle

* fix sqs toggleButton

* fix potential issue with binding in toggleGroup

* Update frontend/src/routes/(root)/(logged)/runs/[...path]/+page.svelte

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

---------

Co-authored-by: pyranota <92104930+pyranota@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
Co-authored-by: HugoCasa <hugo@casademont.ch>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-02-26 17:32:40 +01:00
Alexander Petric eff2a4c496 set rust version for windows build (#5392)
* set rust version for windows build

* set rust version for windows build

* correct ation
2025-02-26 17:32:18 +01:00
Guilhem 5219062cc1 fix row picker (#5390) 2025-02-26 15:27:00 +01:00
Ruben Fiszelandrubenfiszel 318b654106 chore(main): release 1.471.1 (#5389)
* chore(main): release 1.471.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-02-26 13:07:47 +01:00
Guilhem 731299ee14 fix(frontend) load primary schedule args in detail trigger page (#5388)
* load primary schedule args in detail trigger page

* nit
2025-02-26 12:54:31 +01:00
Ruben Fiszel dde3c5bef3 fix compile issue 2025-02-26 12:51:11 +01:00
Ruben Fiszel 3ada264c4a fix: update to rust 1.86.0 2025-02-26 11:34:28 +01:00
Ruben Fiszelandrubenfiszel e2cd8dbdf0 chore(main): release 1.471.0 (#5385)
* chore(main): release 1.471.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-02-26 11:06:07 +01:00
HugoCasa 487d84bd7f feat: add support for claude sonnet 3.7 thinking (#5387)
* feat: add support for claude sonnet 3.7 thinking

* non streaming as well
2025-02-26 10:49:05 +01:00
Ruben Fiszel 82d37d6ace further reactivity for apps improvements 2025-02-26 10:23:15 +01:00
Guilhem a85ebfbbf4 fix(frontend): improve pagination handling and filter refreshes (#5378)
* fix pagination handling and filter refreshes

* wip

* fix window navigation
2025-02-26 09:42:00 +01:00
Ruben Fiszelandrubenfiszel 33b0e71523 chore(main): release 1.470.1 (#5383)
* chore(main): release 1.470.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-02-26 05:35:17 +01:00
Ruben Fiszel 630e54f65c fix: multiple app initializations fixes 2025-02-26 05:31:42 +01:00
Ruben Fiszelandrubenfiszel b26ebb3a79 chore(main): release 1.470.0 (#5381)
* chore(main): release 1.470.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-02-26 05:13:15 +01:00
Ruben Fiszel 9b3ec10c12 improve markdown styling 2025-02-26 05:08:00 +01:00
Ruben Fiszel ebe3582ced fix backticks handling in template 2025-02-26 04:59:56 +01:00
Ruben Fiszel 24b6003780 fix: multiple app initializations fixes 2025-02-26 04:44:14 +01:00
Alexander Petric 478d3fbf4a feat(frontend): set default app refesh interval (#5380) 2025-02-25 23:21:36 +01:00
Ruben Fiszelandrubenfiszel 47c8b334ef chore(main): release 1.469.0 (#5377)
* chore(main): release 1.469.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-02-25 21:51:55 +01:00
Ruben Fiszel 44b26d2cce fix: improve flow editor for vscode extension 2025-02-25 21:42:54 +01:00
Ruben Fiszel 4ab622deaa nits 2025-02-25 20:36:52 +01:00
Ruben Fiszel c13fa65354 fix nit 2025-02-25 20:00:03 +01:00
Ruben Fiszel 56d1da78fd fix: improve infinite grid behavior 2025-02-25 19:51:41 +01:00
Ruben Fiszel d0ee16b1b9 better table action rendering 2025-02-25 19:13:03 +01:00
Ruben Fiszel b175730324 improve delete component of tables 2025-02-25 17:57:31 +01:00
Ruben Fiszel 874dbd9dfa fix actions refresh 2025-02-25 17:32:28 +01:00
pyranotaandellipsis-dev[bot] 25f9369d5e fix dedicated worker multipython (#5338)
* fix dedicated worker multipython

* fix typo

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* fix dedublicate logic

* fix ellipsis complaints

* more factorization

* pass slice instead of ref to vector

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-02-25 17:22:03 +01:00
Ruben Fiszel 64d0c32443 run button paddingnits 2025-02-25 17:17:25 +01:00
Le Phat Tan 50607c7625 feat: python-client now support mocked api via WM_MOCKED_API_FILE env (#5372)
* feat: python-client now support mocked api via `WM_MOCKED_API_FILE` env

* fix: typo
2025-02-25 16:39:32 +01:00
Ruben Fiszel c8a959691c feat: limit the number of times a job can be restarted (3) after loss of pings 2025-02-25 16:35:29 +01:00
Guilhem edcf2d43a2 add catch (#5375) 2025-02-25 14:36:12 +01:00
Guilhem 3bf0bc64af check password input type (#5374) 2025-02-25 12:50:45 +01:00
d48b2dd886 migrate popup to melt popover (3/4) (#5328)
* use melt menu in sidebar

* stop keyboard navigation for disabled items

* use melt menu for FavoriteMenu and WorkspaceMenu

* fix popover placement for menuButton

* use melt menu for operator menu

* fix notification

* fix operator menu

* Use melt menu in FlowJobsMenu

* use melt menu for AppMenu

* clean code

* clean code

* add use clickOutside option to Menu

* use pointerdown_outside

* use pointerdown_outside

# Conflicts:
#	frontend/src/lib/components/meltComponents/Menu.svelte

* use pointerdown in menus

* add max-h to app dropdown menu

* keep more open in operator menu

* add a MenuItem component

* clean

* nit

* nit

* clean code

* put conditionalMelt as utility function

* remove unused Portal

* Add debounce effect in operator menu

* fix component jumping due to z-index

* format pages

* migrate dropdown to melt

* migrate popup to melt popover

* feat: remove `pip` fallback option for python and ansible (#5186)

* refactor!: Remove `pip` fallback option for python and ansible

BREAKING CHANGE: pip was deprecated since 1.425.0 (2024-11-15)

* fix errors in main.rs

* fix tests

* remove nsjail for pip

* fix imports

* fix compilation error

* reinforce melt types

* fix racing condition issue in closing operator menu

* nit

* fix id conflix with melt element

* nit

* clean code

* use melt dropdown instead of menubar

* prevent modal from closing on click outside button in menu

* fix nit

* nit

* close dropdown when opening a new one

* replace MenuV2 with melt Menu (1/4) (#5214)

* use melt menu in sidebar

* stop keyboard navigation for disabled items

* use melt menu for FavoriteMenu and WorkspaceMenu

* fix popover placement for menuButton

* use melt menu for operator menu

* fix notification

* fix operator menu

* Use melt menu in FlowJobsMenu

* use melt menu for AppMenu

* clean code

* clean code

* add use clickOutside option to Menu

* use pointerdown_outside

* use pointerdown_outside

# Conflicts:
#	frontend/src/lib/components/meltComponents/Menu.svelte

* use pointerdown in menus

* add max-h to app dropdown menu

* keep more open in operator menu

* add a MenuItem component

* clean

* nit

* nit

* clean code

* put conditionalMelt as utility function

* remove unused Portal

* Add debounce effect in operator menu

* fix component jumping due to z-index

* feat: remove `pip` fallback option for python and ansible (#5186)

* refactor!: Remove `pip` fallback option for python and ansible

BREAKING CHANGE: pip was deprecated since 1.425.0 (2024-11-15)

* fix errors in main.rs

* fix tests

* remove nsjail for pip

* fix imports

* fix compilation error

* reinforce melt types

* fix racing condition issue in closing operator menu

* nit

* fix id conflix with melt element

* nit

* prevent modal from closing on click outside button in menu

---------

Co-authored-by: pyranota <92104930+pyranota@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
# Conflicts:
#	frontend/src/lib/components/meltComponents/MenuItem.svelte
#	frontend/src/lib/utils.ts

* clean

* fix z index and render

* fix initialize of dropdownmenu after melt migration

* feat: add support for | None and Optional in python (#5361)

* feat: add support for | None and Optional in python

* update python parser package

* add local rooting for MenuItem

* fix z index

* clean

* nit

* nit

* clean code

* nit

---------

Co-authored-by: pyranota <92104930+pyranota@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
Co-authored-by: HugoCasa <hugo@casademont.ch>
2025-02-25 09:53:43 +01:00
Ruben Fiszelandrubenfiszel 03f6db0cb8 chore(main): release 1.468.0 (#5356)
* chore(main): release 1.468.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-02-24 23:14:49 +01:00
Ruben Fiszel 4ee4ff78d3 fix: improve custom component rendering 2025-02-24 22:43:36 +01:00
Ruben Fiszel d45c1f69d4 fix: handle better optional chaining operator 2025-02-24 20:08:13 +01:00
Ruben Fiszel 96597d3d6b fix: improve app markdown rendering 2025-02-24 19:33:14 +01:00
Henri Courdent 9a236ef3eb Changelogs 24 feb (#5363) 2025-02-24 19:01:09 +01:00
Ruben Fiszel 8806870b1b fix: add LOCALAPPDATA env variable to python execution on windows 2025-02-24 18:36:26 +01:00
Ruben Fiszel 11881b48cc nit 2025-02-24 17:53:46 +01:00
Ruben Fiszel 61983a5bbf fix confirmation modal 2025-02-24 17:50:54 +01:00
Ruben Fiszel 0456272e3f fix: improve webhook settings cache invalidation 2025-02-24 16:10:53 +01:00
Ruben Fiszel 1d03a95bef store auto-refresh setting in local storage 2025-02-24 15:47:06 +01:00
Ruben Fiszel 3028325615 fix: fix confirmation modal check on deploy 2025-02-24 15:42:55 +01:00
HugoCasa b3e00b7fdc feat: add audit logs scope filter in admins workspace (#5352)
* feat: add audit logs scope filter in admins workspace

* fix build

* disallow creating a workspace named global

* Update ee-repo-ref.txt
2025-02-24 15:25:28 +01:00
Ruben Fiszel 653241c43e unify variable add 2025-02-24 15:16:50 +01:00
Ruben Fiszel 7c842c88bf fix: handle better forced value propagation in apps 2025-02-24 15:16:33 +01:00
d7ef7fe360 migrate dropdown melt (2/4) (#5327)
* use melt menu in sidebar

* stop keyboard navigation for disabled items

* use melt menu for FavoriteMenu and WorkspaceMenu

* fix popover placement for menuButton

* use melt menu for operator menu

* fix notification

* fix operator menu

* Use melt menu in FlowJobsMenu

* use melt menu for AppMenu

* clean code

* clean code

* add use clickOutside option to Menu

* use pointerdown_outside

* use pointerdown_outside

# Conflicts:
#	frontend/src/lib/components/meltComponents/Menu.svelte

* use pointerdown in menus

* add max-h to app dropdown menu

* keep more open in operator menu

* add a MenuItem component

* clean

* nit

* nit

* clean code

* put conditionalMelt as utility function

* remove unused Portal

* Add debounce effect in operator menu

* fix component jumping due to z-index

* format pages

* migrate dropdown to melt

* feat: remove `pip` fallback option for python and ansible (#5186)

* refactor!: Remove `pip` fallback option for python and ansible

BREAKING CHANGE: pip was deprecated since 1.425.0 (2024-11-15)

* fix errors in main.rs

* fix tests

* remove nsjail for pip

* fix imports

* fix compilation error

* reinforce melt types

* fix racing condition issue in closing operator menu

* nit

* fix id conflix with melt element

* nit

* clean code

* use melt dropdown instead of menubar

* prevent modal from closing on click outside button in menu

* fix nit

* nit

* close dropdown when opening a new one

* clean

* fix z index and render

---------

Co-authored-by: pyranota <92104930+pyranota@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2025-02-24 14:53:50 +01:00
Ruben Fiszel a87b4a52ed nit saml 2025-02-24 14:43:27 +01:00
HugoCasa 9736355d5f feat: add support for | None and Optional in python (#5361)
* feat: add support for | None and Optional in python

* update python parser package
2025-02-24 14:12:29 +01:00
Ruben Fiszel 0329d91ee9 fix initialize of dropdownmenu after melt migration 2025-02-24 13:16:40 +01:00
7ef8100250 replace MenuV2 with melt Menu (1/4) (#5214)
* use melt menu in sidebar

* stop keyboard navigation for disabled items

* use melt menu for FavoriteMenu and WorkspaceMenu

* fix popover placement for menuButton

* use melt menu for operator menu

* fix notification

* fix operator menu

* Use melt menu in FlowJobsMenu

* use melt menu for AppMenu

* clean code

* clean code

* add use clickOutside option to Menu

* use pointerdown_outside

* use pointerdown_outside

# Conflicts:
#	frontend/src/lib/components/meltComponents/Menu.svelte

* use pointerdown in menus

* add max-h to app dropdown menu

* keep more open in operator menu

* add a MenuItem component

* clean

* nit

* nit

* clean code

* put conditionalMelt as utility function

* remove unused Portal

* Add debounce effect in operator menu

* fix component jumping due to z-index

* feat: remove `pip` fallback option for python and ansible (#5186)

* refactor!: Remove `pip` fallback option for python and ansible

BREAKING CHANGE: pip was deprecated since 1.425.0 (2024-11-15)

* fix errors in main.rs

* fix tests

* remove nsjail for pip

* fix imports

* fix compilation error

* reinforce melt types

* fix racing condition issue in closing operator menu

* nit

* fix id conflix with melt element

* nit

* prevent modal from closing on click outside button in menu

---------

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

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