Compare commits

...
38 Commits
Author SHA1 Message Date
Ruben Fiszelandrubenfiszel f6a9fc2b87 chore(main): release 1.473.1 (#5416)
* chore(main): release 1.473.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2025-03-04 00:49:11 +01:00
pyranota 2e7910c0d3 nit: print more information in frontend if something goes wrong with uv (#5411)
* nit: print to frontend logs if there is fallback to system python

* print to logs if fallback
2025-03-04 00:36:11 +01:00
HugoCasa 844edd1117 fix(backend): copilot info exists_ai_resource (#5415) 2025-03-04 00:32:47 +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
178 changed files with 3197 additions and 2196 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
@@ -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: |
+66
View File
@@ -1,5 +1,71 @@
# Changelog
## [1.473.1](https://github.com/windmill-labs/windmill/compare/v1.473.0...v1.473.1) (2025-03-03)
### Bug Fixes
* **backend:** copilot info exists_ai_resource ([#5415](https://github.com/windmill-labs/windmill/issues/5415)) ([844edd1](https://github.com/windmill-labs/windmill/commit/844edd1117bc5fdcc108adf53aec3974a7c66384))
* improve cancel performance ([fba9e7e](https://github.com/windmill-labs/windmill/commit/fba9e7ef03b91d3e2c78ff833ddcc5207b7436d2))
## [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)
+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
@@ -130,28 +130,28 @@
},
{
"ordinal": 25,
"name": "ai_models",
"type_info": "VarcharArray"
},
{
"ordinal": 26,
"name": "code_completion_model",
"type_info": "Varchar"
},
{
"ordinal": 27,
"name": "teams_command_script",
"type_info": "Text"
},
{
"ordinal": 28,
"ordinal": 26,
"name": "teams_team_id",
"type_info": "Text"
},
{
"ordinal": 29,
"ordinal": 27,
"name": "teams_team_name",
"type_info": "Text"
},
{
"ordinal": 28,
"name": "ai_models",
"type_info": "VarcharArray"
},
{
"ordinal": 29,
"name": "code_completion_model",
"type_info": "Varchar"
}
],
"parameters": {
@@ -185,10 +185,10 @@
true,
true,
true,
true,
true,
true,
false,
true,
true,
true,
true
]
},
@@ -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"
}
@@ -130,28 +130,28 @@
},
{
"ordinal": 25,
"name": "ai_models",
"type_info": "VarcharArray"
},
{
"ordinal": 26,
"name": "code_completion_model",
"type_info": "Varchar"
},
{
"ordinal": 27,
"name": "teams_command_script",
"type_info": "Text"
},
{
"ordinal": 28,
"ordinal": 26,
"name": "teams_team_id",
"type_info": "Text"
},
{
"ordinal": 29,
"ordinal": 27,
"name": "teams_team_name",
"type_info": "Text"
},
{
"ordinal": 28,
"name": "ai_models",
"type_info": "VarcharArray"
},
{
"ordinal": 29,
"name": "code_completion_model",
"type_info": "Varchar"
}
],
"parameters": {
@@ -185,10 +185,10 @@
true,
true,
true,
true,
true,
true,
false,
true,
true,
true,
true
]
},
@@ -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,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"
}
+544 -751
View File
File diff suppressed because it is too large Load Diff
+16 -16
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.470.0"
version = "1.473.1"
authors.workspace = true
edition.workspace = true
@@ -30,7 +30,7 @@ members = [
]
[workspace.package]
version = "1.470.0"
version = "1.473.1"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -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 @@
d1dd29ec1aba66dd105e6193db9f430476bf5dfc
a165bb74c6c33bdcf618d564498fd4cbb5742451
@@ -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);
@@ -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 = "
+19
View File
@@ -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())
+1 -1
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,
+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,
+107 -1
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.470.0
version: 1.473.1
title: Windmill API
contact:
@@ -5292,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
@@ -6212,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>,
+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?;
+1 -1
View File
@@ -796,7 +796,7 @@ async fn get_copilot_info(
let (ai_provider, exists_ai_resource) = if let Some(ai_resource) = record.ai_resource {
let ai_resource = serde_json::from_value::<AIResource>(ai_resource)?;
(Some(ai_resource.provider), true)
(Some(ai_resource.provider), ai_resource.path.is_some())
} else {
(None, false)
};
@@ -189,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,
+2
View File
@@ -61,6 +61,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)]
+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
)
+21 -18
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,
};
@@ -660,7 +660,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 +802,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)?;
@@ -1088,6 +1090,7 @@ try {{
let build_cache = !has_bundle_cache
&& !annotation.nobundling
&& !*DISABLE_BUNDLING
&& !codebase.is_some()
&& (requirements_o.is_some() || annotation.native);
+21 -8
View File
@@ -96,19 +96,23 @@ pub enum PyVersion {
}
impl PyVersion {
pub async fn from_instance_version() -> Self {
match INSTANCE_PYTHON_VERSION.read().await.clone() {
pub async fn from_instance_version(job_id: &Uuid, w_id: &str, db: &Pool<Postgres>) -> Self {
let mut err = None;
let pyv = match INSTANCE_PYTHON_VERSION.read().await.clone() {
Some(v) => PyVersion::from_string_with_dots(&v).unwrap_or_else(|| {
let v = PyVersion::default();
tracing::error!(
"Cannot parse INSTANCE_PYTHON_VERSION ({:?}), fallback to latest_stable ({v:?})",
*INSTANCE_PYTHON_VERSION
);
err = Some(format!("\nCannot parse INSTANCE_PYTHON_VERSION ({:?}), fallback to latest_stable ({v:?})", *INSTANCE_PYTHON_VERSION));
v
}),
// Use latest stable
None => PyVersion::default(),
};
if let Some(msg) = err {
append_logs(job_id, w_id, &msg, db).await;
tracing::error!(msg);
}
pyv
}
/// e.g.: `/tmp/windmill/cache/python_3xy`
pub fn to_cache_dir(&self) -> String {
@@ -223,9 +227,18 @@ impl PyVersion {
if let Err(ref e) = res {
tracing::error!(
"worker_name: {worker_name}, w_id: {w_id}, job_id: {job_id}\n
"worker_name: {worker_name}, w_id: {w_id}, job_id: {job_id}\n
Error while getting python from uv, falling back to system python: {e:?}"
);
append_logs(
job_id,
w_id,
format!(
"\nError while getting python from uv, falling back to system python: {e:?}"
),
db,
)
.await;
}
res
}
@@ -1430,7 +1443,7 @@ async fn handle_python_deps(
let mut annotated_pyv = None;
let mut annotated_pyv_numeric = None;
let is_deployed = requirements_o.is_some();
let instance_pyv = PyVersion::from_instance_version().await;
let instance_pyv = PyVersion::from_instance_version(job_id, w_id, db).await;
let annotations = windmill_common::worker::PythonAnnotations::parse(inner_content);
let requirements = match requirements_o {
Some(r) => r,
+7 -19
View File
@@ -11,8 +11,9 @@
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::{
@@ -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,17 +243,10 @@ 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))
}
@@ -785,7 +773,7 @@ pub async fn run_worker(
worker_dir.clone(),
);
tokio::spawn(async move {
if let Err(e) = PyVersion::from_instance_version()
if let Err(e) = PyVersion::from_instance_version(&Uuid::nil(), "", &db)
.await
.get_python(&Uuid::nil(), &mut 0, &db, &worker_name, "", &mut None)
.await
@@ -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);
@@ -1673,7 +1701,7 @@ async fn python_dep(
let final_version = annotated_pyv_numeric
.and_then(|pyv| PyVersion::from_numeric(pyv))
.unwrap_or(PyVersion::from_instance_version().await);
.unwrap_or(PyVersion::from_instance_version(job_id, w_id, db).await);
let req: std::result::Result<String, Error> = uv_pip_compile(
job_id,
+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.470.0";
export const VERSION = "v1.473.1";
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.470.0";
export const VERSION = "1.473.1";
const command = new Command()
.name("wmill")
+6 -6
View File
@@ -1,15 +1,15 @@
{
"name": "windmill-components",
"version": "1.470.0",
"version": "1.473.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.470.0",
"version": "1.473.1",
"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",
@@ -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",
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.470.0",
"version": "1.473.1",
"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",
-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
}
}
+4 -6
View File
@@ -92,26 +92,24 @@
{/if}
<span class="text-xs mb-1 pt-2 leading-6">Role</span>
<ToggleButtonGroup bind:selected class="mb-4">
<ToggleButtonGroup bind:selected class="mb-4" let:item>
<ToggleButton
value="operator"
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."
{item}
/>
<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)."
{item}
/>
<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."
{item}
/>
</ToggleButtonGroup>
<Button
+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}
@@ -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>
@@ -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>
@@ -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>
@@ -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"
@@ -93,7 +93,13 @@
{#if $$slots.buttonReplacement}
<slot name="buttonReplacement" />
{:else}
<Button nonCaptureEvent size="xs" color="light" startIcon={{ icon: MoreVertical }} />
<Button
nonCaptureEvent
size="xs"
color="light"
startIcon={{ icon: MoreVertical }}
btnClasses="bg-transparent"
/>
{/if}
</button>
@@ -23,6 +23,7 @@
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}
@@ -465,6 +468,7 @@
<ToggleButtonGroup
tabListClass="flex-wrap"
class="h-auto"
let:item
bind:selected
on:selected={(e) => {
const isS3 = e.detail == 'S3'
@@ -545,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'}
@@ -18,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
@@ -172,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({
@@ -183,6 +187,7 @@
page: 1,
perPage: 1
})
// console.log('previousJobId', previousJobId, module.id)
if (previousJobId.length > 0) {
const getJobResult = await JobService.getCompletedJobResultMaybe({
@@ -184,9 +184,13 @@
</Alert>
<div class="flex items-center gap-1">
<div>
<ToggleButtonGroup bind:selected={ownerKind} on:selected={() => (ownerItem = '')}>
<ToggleButton value="user" size="xs" label="User" />
<ToggleButton value="group" size="xs" label="Group" />
<ToggleButtonGroup
bind:selected={ownerKind}
on:selected={() => (ownerItem = '')}
let:item
>
<ToggleButton value="user" label="User" {item} />
<ToggleButton value="group" label="Group" {item} />
</ToggleButtonGroup>
</div>
@@ -194,7 +198,9 @@
<AutoComplete
required
noInputStyles
items={ownerKind === 'user' ? usernames.filter((x) => !perms?.map((y) => y.owner_name).includes('u/'+x)) : groups.filter((x) => !perms?.map((y) => y.owner_name).includes('g/'+x))}
items={ownerKind === 'user'
? usernames.filter((x) => !perms?.map((y) => y.owner_name).includes('u/' + x))
: groups.filter((x) => !perms?.map((y) => y.owner_name).includes('g/' + x))}
bind:selectedItem={ownerItem}
/>
{#if ownerKind == 'group'}
@@ -271,6 +277,7 @@
<div>
<ToggleButtonGroup
selected={role}
let:item
on:selected={async (e) => {
const role = e.detail
// const wasInFolder = (folder?.owners ?? []).includes(folder)
@@ -309,25 +316,25 @@
>
<ToggleButton
value="viewer"
size="xs"
label="Viewer"
tooltip="A viewer of a folder has read-only access to all the elements (scripts/flows/apps/schedules/resources/variables) inside the folder"
{item}
/>
<ToggleButton
position="center"
value="writer"
size="xs"
label="Writer"
tooltip="A writer of a folder has read AND write access to all the elements (scripts/flows/apps/schedules/resources/variables) inside the folder"
{item}
/>
<ToggleButton
position="right"
value="admin"
size="xs"
label="Admin"
tooltip="An admin of a folder has read AND write access to all the elements inside the folders and can manage the permissions as well as add new admins"
{item}
/>
</ToggleButtonGroup>
</div>
@@ -150,6 +150,7 @@
<div>
<ToggleButtonGroup
selected={role}
let:item
on:selected={async (e) => {
const role = e.detail
// const wasInGroup = (group?.members ?? []).includes(group)
@@ -215,6 +216,7 @@
size="xs"
label="Member"
tooltip="A Member of a group can see everything the group can see, write to everything the group can write, and generally act on behalf of the group"
{item}
/>
<ToggleButton
position="right"
@@ -222,6 +224,7 @@
size="xs"
label="Admin"
tooltip="An admin of a group is a member of a group that can also add and remove members to the group, or make them admin."
{item}
/>
{#if role === 'manager'}
<ToggleButton
@@ -230,6 +233,7 @@
label="Manager"
tooltip="A manager of a group can manage the group, adding and removing users and
change their roles. Being a manager does not make you a member"
{item}
/>
{/if}
</ToggleButtonGroup>
@@ -358,6 +358,7 @@
<div>
<ToggleButtonGroup
selected={propertyType}
let:item
on:selected={(e) => {
if (e.detail == propertyType) return
const staticTemplate = isStaticTemplate(inputCat)
@@ -417,9 +418,10 @@
value="static"
size="xs2"
label={'${}'}
{item}
/>
{:else}
<ToggleButton small label="Static" value="static" />
<ToggleButton small label="Static" value="static" {item} />
{/if}
{#if codeInjectionDetected && propertyType == 'static'}
@@ -441,6 +443,7 @@
tooltip="JavaScript expression ('flow_input' or 'results')."
value="javascript"
icon={FunctionSquare}
{item}
/>
{/if}
</ToggleButtonGroup>
@@ -185,12 +185,13 @@
</span>
{/if}
</label>
<ToggleButtonGroup bind:selected={$values[setting.key]}>
<ToggleButtonGroup bind:selected={$values[setting.key]} let:item={toggleButton}>
{#each setting.select_items ?? [] as item}
<ToggleButton
value={item.value ?? item.label}
label={item.label}
tooltip={item.tooltip}
item={toggleButton}
/>
{/each}
</ToggleButtonGroup>
@@ -80,28 +80,26 @@
bind:value={email}
class="mr-4"
/>
<ToggleButtonGroup bind:selected>
<ToggleButtonGroup bind:selected let:item>
<ToggleButton
value="operator"
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."
{item}
/>
<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)."
{item}
/>
<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."
{item}
/>
</ToggleButtonGroup>
<Button
@@ -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">
+3
View File
@@ -374,6 +374,7 @@
}
}
}}
let:item
>
<ToggleButton
icon={User}
@@ -383,6 +384,7 @@
value="user"
position="left"
label="User"
{item}
/>
<!-- <ToggleButton light size="xs" value="group" position="center">Group</ToggleButton> -->
<ToggleButton
@@ -393,6 +395,7 @@
value="folder"
position="right"
label="Folder"
{item}
/>
</ToggleButtonGroup>
</div>
+1 -19
View File
@@ -13,7 +13,6 @@
import TimeAgo from './TimeAgo.svelte'
import Popover from './meltComponents/Popover.svelte'
import { autoPlacement } from '@floating-ui/core'
import { Calendar, CornerDownLeft } from 'lucide-svelte'
import RunFormAdvancedPopup from './RunFormAdvancedPopup.svelte'
import { page } from '$app/stores'
@@ -205,24 +204,7 @@
{scheduledForStr ? 'Schedule to run later' : buttonText}
</Button>
<div>
<Popover
floatingConfig={{
middleware: [
autoPlacement({
allowedPlacements: [
'bottom-start',
'bottom-end',
'top-start',
'top-end',
'top',
'bottom'
]
})
]
}}
closeButton
usePointerDownOutside
>
<Popover placement="bottom" closeButton usePointerDownOutside>
<svelte:fragment slot="trigger">
<Button nonCaptureEvent startIcon={{ icon: Calendar }} size="xs" color="light">
Advanced
@@ -29,6 +29,7 @@
let initialPrimarySchedule: Writable<ScheduleTrigger | false | undefined> = writable(undefined)
async function updateSchedules(forceRefresh: boolean) {
const loadPrimarySchedule = true
loadSchedules(
forceRefresh,
path,
@@ -37,7 +38,8 @@
primarySchedule,
initialPrimarySchedule,
$workspaceStore ?? '',
triggersCount
triggersCount,
loadPrimarySchedule
)
}
@@ -335,27 +335,30 @@
{#if linkedSecretCandidates?.includes(argName)}
<div>
<ToggleButtonGroup
selected={linkedSecret == argName}
selected={linkedSecret == argName ? 'secret' : 'inlined'}
on:selected={(e) => {
if (e.detail) {
if (e.detail === 'secret') {
linkedSecret = argName
} else if (linkedSecret == argName) {
linkedSecret = undefined
}
}}
let:item
>
<ToggleButton
value={false}
value="inlined"
size="sm"
label="Inlined"
tooltip="The value is inlined in the resource and thus has no special treatment."
{item}
/>
<ToggleButton
position="right"
value={true}
value="secret"
size="sm"
label="Secret"
tooltip="The value will be stored in a newly created linked secret variable at the same path. That variable can be permissioned differently, will be treated as a secret the UI, operators will not be able to load it and every access will generate a corresponding audit log."
{item}
/>
</ToggleButtonGroup>
</div>{/if}</svelte:fragment
+139 -77
View File
@@ -11,7 +11,6 @@
} from '$lib/gen'
import { inferArgs } from '$lib/infer'
import { initialCode } from '$lib/script_helpers'
import { page } from '$app/stores'
import { defaultScripts, enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
import {
cleanValueProperties,
@@ -64,7 +63,7 @@
import { writable } from 'svelte/store'
import { defaultScriptLanguages, processLangs } from '$lib/scripts'
import DefaultScripts from './DefaultScripts.svelte'
import { createEventDispatcher, setContext } from 'svelte'
import { createEventDispatcher, onMount, setContext } from 'svelte'
import CustomPopover from './CustomPopover.svelte'
import Summary from './Summary.svelte'
import type { ScriptBuilderWhitelabelCustomUi } from './custom_ui'
@@ -77,6 +76,7 @@
} from '$lib/script_helpers'
import CaptureTable from './triggers/CaptureTable.svelte'
import type { SavedAndModifiedValue } from './common/confirmationModal/unsavedTypes'
import type { ScriptBuilderFunctionExports } from './scriptBuilder'
export let script: NewScript
export let fullyLoaded: boolean = true
@@ -94,6 +94,8 @@
window.history.replaceState(null, '', url)
export let customUi: ScriptBuilderWhitelabelCustomUi = {}
export let savedPrimarySchedule: ScheduleTrigger | undefined = undefined
export let functionExports: ((exports: ScriptBuilderFunctionExports) => void) | undefined =
undefined
export function getInitialAndModifiedValues(): SavedAndModifiedValue {
return {
@@ -139,6 +141,25 @@
$: initialPath != '' && loadTriggers()
onMount(() => {
if (functionExports) {
console.log('functionExports set')
functionExports({
setPreviewArgs: (args: Record<string, any>) => {
scriptEditor?.setArgs(args)
},
runPreview: async () => await scriptEditor?.runTest(),
setCode: (code: string, language?: Script['language']) => {
if (language) {
script.language = language
}
editor?.setCode(code)
},
getCode: () => editor?.getCode() ?? ''
})
}
})
async function loadTriggers() {
$triggersCount = await ScriptService.getTriggersCountOfScript({
workspace: $workspaceStore!,
@@ -180,9 +201,13 @@
$defaultScripts?.order ?? Object.keys(defaultScriptLanguages)
)
.map((l) => [defaultScriptLanguages[l], l])
.filter(
(x) => $defaultScripts?.hidden == undefined || !$defaultScripts.hidden.includes(x[1])
) as [string, SupportedLanguage | 'docker' | 'bunnative'][]
.filter((x) => $defaultScripts?.hidden == undefined || !$defaultScripts.hidden.includes(x[1]))
.filter((x) => {
if (customUi?.settingsPanel?.metadata?.languages === undefined) {
return true
}
return customUi.settingsPanel.metadata.languages.includes(x[1] as SupportedLanguage)
}) as [string, SupportedLanguage | 'docker' | 'bunnative'][]
const scriptKindOptions: {
value: Script['kind']
@@ -245,16 +270,17 @@
}
async function isTemplateScript() {
let getInitBlockTemplate = $page.url.searchParams.get('id')
if (getInitBlockTemplate === null) {
const params = new URLSearchParams(window.location.search)
const templateId = params.get('id')
if (templateId === null) {
return undefined
}
try {
getInitBlockTemplate = await PostgresTriggerService.getTemplateScript({
const templateScript = await PostgresTriggerService.getTemplateScript({
workspace: $workspaceStore!,
id: getInitBlockTemplate as string
id: templateId
})
return getInitBlockTemplate
return templateScript
} catch (error) {
sendUserToast(
'An error occured when trying to load your template script, please try again later',
@@ -654,7 +680,24 @@
let path: Path | undefined = undefined
let dirtyPath = false
let selectedTab: 'metadata' | 'runtime' | 'ui' | 'triggers' = 'metadata'
let selectedTab: 'metadata' | 'runtime' | 'ui' | 'triggers' = (() => {
if (customUi?.settingsPanel?.disableMetadata !== true) {
// first option: either no custom UI or metadata is enabled
return 'metadata'
}
if (customUi?.settingsPanel?.disableRuntime !== true) {
return 'runtime'
}
if (customUi?.settingsPanel?.disableGeneratedUi !== true) {
return 'ui'
}
if (customUi?.settingsPanel?.disableTriggers !== true) {
return 'triggers'
}
return 'metadata'
})()
setContext('disableTooltips', customUi?.disableTooltips === true)
let deploymentMsg = ''
let msgInput: HTMLInputElement | undefined = undefined
@@ -728,23 +771,31 @@
<!-- svelte-ignore a11y-autofocus -->
<div class="flex flex-col h-full">
<Tabs bind:selected={selectedTab} wrapperClass="flex-none w-full">
<Tab value="metadata">Metadata</Tab>
<Tab value="runtime">Runtime</Tab>
<Tab value="ui">
Generated UI
<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/json_schema_and_parsing"
>
The arguments are synced with the main signature but you may refine the parts that
cannot be inferred from the type directly.
</Tooltip>
</Tab>
<Tab value="triggers">
Triggers
<Tooltip documentationLink="https://www.windmill.dev/docs/getting_started/triggers">
Configure how this script will be triggered.
</Tooltip>
</Tab>
{#if customUi?.settingsPanel?.disableMetadata !== true}
<Tab value="metadata">Metadata</Tab>
{/if}
{#if customUi?.settingsPanel?.disableRuntime !== true}
<Tab value="runtime">Runtime</Tab>
{/if}
{#if customUi?.settingsPanel?.disableGeneratedUi !== true}
<Tab value="ui">
Generated UI
<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/json_schema_and_parsing"
>
The arguments are synced with the main signature but you may refine the parts that
cannot be inferred from the type directly.
</Tooltip>
</Tab>
{/if}
{#if customUi?.settingsPanel?.disableTriggers !== true}
<Tab value="triggers">
Triggers
<Tooltip documentationLink="https://www.windmill.dev/docs/getting_started/triggers">
Configure how this script will be triggered.
</Tooltip>
</Tab>
{/if}
<svelte:fragment slot="content">
<div class="min-h-0 grow overflow-y-auto">
@@ -752,14 +803,16 @@
<div class="flex flex-col gap-8 px-4 py-2">
<Section label="Metadata">
<svelte:fragment slot="action">
<div class="flex flex-row items-center gap-2">
<ErrorHandlerToggleButton
kind="script"
scriptOrFlowPath={script.path}
bind:errorHandlerMuted={script.ws_error_handler_muted}
iconOnly={false}
/>
</div>
{#if customUi?.settingsPanel?.metadata?.disableMute !== true}
<div class="flex flex-row items-center gap-2">
<ErrorHandlerToggleButton
kind="script"
scriptOrFlowPath={script.path}
bind:errorHandlerMuted={script.ws_error_handler_muted}
iconOnly={false}
/>
</div>
{/if}
</svelte:fragment>
<div class="flex flex-col gap-4">
<Label label="Summary">
@@ -893,36 +946,40 @@
</div>
</Section>
<Section label="Script kind">
<svelte:fragment slot="header">
<Tooltip
documentationLink="https://www.windmill.dev/docs/script_editor/script_kinds"
{#if customUi?.settingsPanel?.metadata?.disableScriptKind !== true}
<Section label="Script kind">
<svelte:fragment slot="header">
<Tooltip
documentationLink="https://www.windmill.dev/docs/script_editor/script_kinds"
>
Tag this script's purpose within flows such that it is available as the
corresponding action.
</Tooltip>
</svelte:fragment>
<ToggleButtonGroup
class="h-10"
selected={script.kind}
on:selected={({ detail }) => {
template = 'script'
script.kind = detail
initContent(script.language, detail, template)
}}
let:item
>
Tag this script's purpose within flows such that it is available as the
corresponding action.
</Tooltip>
</svelte:fragment>
<ToggleButtonGroup
class="h-10"
selected={script.kind}
on:selected={({ detail }) => {
template = 'script'
script.kind = detail
initContent(script.language, detail, template)
}}
>
{#each scriptKindOptions as { value, title, desc, documentationLink, Icon }}
<ToggleButton
label={title}
{value}
tooltip={desc}
{documentationLink}
icon={Icon}
showTooltipIcon={Boolean(desc)}
/>
{/each}
</ToggleButtonGroup>
</Section>
{#each scriptKindOptions as { value, title, desc, documentationLink, Icon }}
<ToggleButton
label={title}
{value}
tooltip={desc}
{documentationLink}
icon={Icon}
showTooltipIcon={Boolean(desc)}
{item}
/>
{/each}
</ToggleButtonGroup>
</Section>
{/if}
</div>
</TabContent>
<TabContent value="runtime">
@@ -1321,7 +1378,10 @@
</div>
</TabContent>
<TabContent value="ui" class="h-full p-4">
<ScriptSchema bind:schema={script.schema} />
<ScriptSchema
bind:schema={script.schema}
customUi={customUi?.settingsPanel?.metadata?.editableSchemaForm}
/>
</TabContent>
<TabContent value="triggers">
<TriggersEditor
@@ -1396,18 +1456,20 @@
{#if customUi?.topBar?.path != false}
<div class="flex justify-start w-full border rounded-md overflow-hidden">
<div>
<button
on:click={async () => {
metadataOpen = true
}}
>
<Badge
color="gray"
class="center-center !bg-surface-secondary !text-tertiary !h-[28px] !w-[70px] rounded-none hover:!bg-surface-hover transition-all"
{#if customUi?.topBar?.editablePath != false}
<button
on:click={async () => {
metadataOpen = true
}}
>
<Pen size={12} class="mr-2" /> Path
</Badge>
</button>
<Badge
color="gray"
class="center-center !bg-surface-secondary !text-tertiary !h-[28px] !w-[70px] rounded-none hover:!bg-surface-hover transition-all"
>
<Pen size={12} class="mr-2" /> Path
</Badge>
</button>
{/if}
</div>
<input
type="text"
@@ -30,6 +30,7 @@
import { slide } from 'svelte/transition'
import CaptureTable from '$lib/components/triggers/CaptureTable.svelte'
import CaptureButton from './triggers/CaptureButton.svelte'
import { setContext } from 'svelte'
// Exported
export let schema: Schema | any = emptySchema()
@@ -50,7 +51,7 @@
export let noHistory = false
export let saveToWorkspace = false
export let watchChanges = false
export let customUi: ScriptEditorWhitelabelCustomUi = {}
export let customUi: ScriptEditorWhitelabelCustomUi | undefined = undefined
export let args: Record<string, any> = initialArgs
export let selectedTab: 'main' | 'preprocessor' = 'main'
export let hasPreprocessor = false
@@ -107,11 +108,15 @@
}
}
function runTest() {
export function setArgs(nargs: Record<string, any>) {
args = nargs
}
export async function runTest() {
// Not defined if JobProgressBar not loaded
if (jobProgressReset) jobProgressReset()
//@ts-ignore
testJobLoader.runPreview(
let job = await testJobLoader.runPreview(
path,
code,
lang,
@@ -119,6 +124,7 @@
tag
)
setFocusToLogs()
return job
}
async function loadPastTests(): Promise<void> {
@@ -258,6 +264,8 @@
}
let setFocusToLogs = () => {}
setContext('disableTooltips', customUi?.disableTooltips === true)
</script>
<TestJobLoader
@@ -407,7 +415,7 @@
/>
Cancel
</Button>
{:else}
{:else if customUi?.previewPanel?.disableTriggerButton !== true}
<div class="flex flex-row divide-x divide-gray-800 dark:divide-gray-300 items-stretch">
<Button
color="dark"
@@ -430,6 +438,28 @@
</Button>
<CaptureButton on:openTriggers />
</div>
{:else}
<div class="flex flex-row divide-x divide-gray-800 dark:divide-gray-300 items-stretch">
<Button
color="dark"
on:click={() => {
runTest()
}}
btnClasses="w-full"
size="xs"
startIcon={{
icon: Play,
classes: 'animate-none'
}}
shortCut={{ Icon: CornerDownLeft, hide: testIsLoading }}
>
{#if testIsLoading}
Running
{:else}
Test
{/if}
</Button>
</div>
{/if}
</div>
<Splitpanes horizontal class="!max-h-[calc(100%-43px)]">
@@ -448,6 +478,7 @@
{schema}
bind:args
bind:isValid
noVariablePicker={customUi?.previewPanel?.disableVariablePicker === true}
showSchemaExplorer
/>
{/key}
@@ -465,6 +496,7 @@
{diffEditor}
{args}
showCaptures={true}
customUi={customUi?.previewPanel}
>
{#if scriptProgress}
<!-- Put to the slot in logpanel -->
@@ -50,7 +50,7 @@
items = (
await ScriptService.listScripts({
workspace: $workspaceStore!,
kinds: kinds.join(','),
kinds: kinds.join(',')
})
).map((script) => ({
value: script.path,
@@ -85,9 +85,9 @@
<div class="flex flex-row items-center gap-4 w-full mt-2">
{#if options.length > 1}
<div>
<ToggleButtonGroup bind:selected={itemKind}>
<ToggleButtonGroup bind:selected={itemKind} let:item>
{#each options as [label, value, icon, selectedColor]}
<ToggleButton {icon} {disabled} {value} {label} {selectedColor} />
<ToggleButton {icon} {disabled} {value} {label} {selectedColor} {item} />
{/each}
</ToggleButtonGroup>
</div>
@@ -1,13 +1,15 @@
<script lang="ts">
import type { Schema } from '$lib/common'
import type { EditableSchemaFormUi } from '$lib/components/custom_ui'
import EditableSchemaForm from './EditableSchemaForm.svelte'
export let schema: Schema | any
export let customUi: EditableSchemaFormUi | undefined = undefined
export function setSchema(newSchema: Schema) {
schema = newSchema
}
</script>
<EditableSchemaForm bind:schema uiOnly editTab="inputEditor" />
<EditableSchemaForm bind:schema uiOnly {customUi} editTab="inputEditor" />
@@ -190,9 +190,15 @@
{#if selectedVersionIndex !== undefined && versions?.slice(selectedVersionIndex + 1).length}
<div class="p-2 flex flex-row items-center gap-2 h-8">
<div class="w-min">
<ToggleButtonGroup bind:selected={showDiff}>
<ToggleButton light small value={false} label="Code" icon={Code} />
<ToggleButton light small value={true} label="Diff" icon={Diff} />
<ToggleButtonGroup
selected={showDiff ? 'diff' : 'code'}
let:item
on:selected={({ detail }) => {
showDiff = detail === 'diff'
}}
>
<ToggleButton light small value="code" label="Code" icon={Code} {item} />
<ToggleButton light small value="diff" label="Diff" icon={Diff} {item} />
</ToggleButtonGroup>
</div>
+10 -5
View File
@@ -123,9 +123,13 @@
{#if own}
<div class="flex flex-row flex-wrap gap-2 items-center">
<div>
<ToggleButtonGroup bind:selected={ownerKind} on:selected={() => (owner = '')}>
<ToggleButton value="user" size="xs" label="User" />
<ToggleButton value="group" size="xs" label="Group" />
<ToggleButtonGroup
bind:selected={ownerKind}
on:selected={() => (owner = '')}
let:item
>
<ToggleButton value="user" size="xs" label="User" {item} />
<ToggleButton value="group" size="xs" label="Group" {item} />
</ToggleButtonGroup>
</div>
{#key ownerKind}
@@ -163,9 +167,10 @@
}
loadAcls()
}}
let:item
>
<ToggleButton value="viewer" size="xs" label="Viewer" />
<ToggleButton value="writer" size="xs" label="Writer" />
<ToggleButton value="viewer" size="xs" label="Viewer" {item} />
<ToggleButton value="writer" size="xs" label="Writer" {item} />
</ToggleButtonGroup>
</div>
{:else}{write}{/if}</td
@@ -133,9 +133,16 @@
disableVariablePicker = undefined
}
}}
let:item
>
{#each FIELD_SETTINGS as x}
<ToggleButton value={x[1]} label={x[0]} tooltip={x[2]} showTooltipIcon={Boolean(x[2])} />
<ToggleButton
value={x[1]}
label={x[0]}
tooltip={x[2]}
showTooltipIcon={Boolean(x[2])}
{item}
/>
{/each}
</ToggleButtonGroup>
{/if}
@@ -292,15 +292,22 @@
sendUserToast('User updated')
listUsers(activeOnly)
}}
let:item
>
<ToggleButton value={'user'} size="xs" label="User" />
<ToggleButton value={'user'} size="xs" label="User" {item} />
<ToggleButton
value={'devops'}
size="xs"
label="Devops"
tooltip="Devops is a role that grants visibilty similar to that of a super admin, but without giving all rights. For example devops users can see service logs and crtical alerts. You can think of it as a 'readonly' super admin"
{item}
/>
<ToggleButton
value={'super_admin'}
size="xs"
label="Superadmin"
{item}
/>
<ToggleButton value={'super_admin'} size="xs" label="Superadmin" />
</ToggleButtonGroup>
</td>
<td>
@@ -7,8 +7,8 @@
</script>
<div class="max-w-min">
<ToggleButtonGroup bind:selected>
<ToggleButton value="hub" icon={WindmillIcon} label="Hub" />
<ToggleButton value="workspace" icon={Building} label="Workspace" />
<ToggleButtonGroup bind:selected let:item>
<ToggleButton value="hub" icon={WindmillIcon} label="Hub" {item} />
<ToggleButton value="workspace" icon={Building} label="Workspace" {item} />
</ToggleButtonGroup>
</div>
@@ -7,9 +7,9 @@
</script>
<div class="max-w-min">
<ToggleButtonGroup id="flow-editor-flow-providers" bind:selected>
<ToggleButton value="all" label="All" light small />
<ToggleButton value="hub" icon={WindmillIcon2} label="Hub" light small />
<ToggleButton value="workspace" icon={Building} label="Workspace" light small />
<ToggleButtonGroup id="flow-editor-flow-providers" bind:selected let:item>
<ToggleButton value="all" label="All" light small {item} />
<ToggleButton value="hub" icon={WindmillIcon2} label="Hub" light small {item} />
<ToggleButton value="workspace" icon={Building} label="Workspace" light small {item} />
</ToggleButtonGroup>
</div>
+37 -30
View File
@@ -4,6 +4,7 @@
import Popover from './Popover.svelte'
import { ExternalLink, InfoIcon } from 'lucide-svelte'
import { gfmPlugin } from 'svelte-exmarkdown/gfm'
import { getContext, hasContext } from 'svelte'
export let light = false
export let wrapperClass = ''
export let placement: PopoverPlacement | undefined = undefined
@@ -12,37 +13,43 @@
export let markdownTooltip: string | undefined = undefined
export let customSize: string = '100%'
const plugins = [gfmPlugin()]
const disableTooltips = hasContext('disableTooltips')
? getContext('disableTooltips') === true
: false
</script>
<Popover
notClickable
{placement}
class={wrapperClass}
style="transform: scale({parseFloat(customSize) / 100});"
>
<div
class="inline-flex w-3 mx-0.5 h-3 {light
? 'text-tertiary-inverse'
: 'text-tertiary'} {$$props.class} relative"
{#if disableTooltips !== true}
<Popover
notClickable
{placement}
class={wrapperClass}
style="transform: scale({parseFloat(customSize) / 100});"
>
<InfoIcon class="{small ? 'bottom-0' : '-bottom-0.5'} absolute" size={small ? 12 : 14} />
</div>
<svelte:fragment slot="text">
{#if markdownTooltip}
<div class="prose-sm">
<Markdown md={markdownTooltip} {plugins} />
</div>
{:else}
<slot />
{/if}
{#if documentationLink}
<a href={documentationLink} target="_blank" class="text-blue-300 text-xs">
<div class="flex flex-row gap-2 mt-4">
See documentation
<ExternalLink size="16" />
<div
class="inline-flex w-3 mx-0.5 h-3 {light
? 'text-tertiary-inverse'
: 'text-tertiary'} {$$props.class} relative"
>
<InfoIcon class="{small ? 'bottom-0' : '-bottom-0.5'} absolute" size={small ? 12 : 14} />
</div>
<svelte:fragment slot="text">
{#if markdownTooltip}
<div class="prose-sm">
<Markdown md={markdownTooltip} {plugins} />
</div>
</a>
{/if}
</svelte:fragment>
</Popover>
{:else}
<slot />
{/if}
{#if documentationLink}
<a href={documentationLink} target="_blank" class="text-blue-300 text-xs">
<div class="flex flex-row gap-2 mt-4">
See documentation
<ExternalLink size="16" />
</div>
</a>
{/if}
</svelte:fragment>
</Popover>
{/if}
@@ -183,10 +183,10 @@
>{/if}{/if}
</div>
<div class="flex flex-col gap-2">
<ToggleButtonGroup bind:selected={editorKind}>
<ToggleButton value="plain" label="Plain" />
<ToggleButton value="json" label="Json" />
<ToggleButton value="yaml" label="YAML" />
<ToggleButtonGroup bind:selected={editorKind} let:item>
<ToggleButton value="plain" label="Plain" {item} />
<ToggleButton value="json" label="Json" {item} />
<ToggleButton value="yaml" label="YAML" {item} />
</ToggleButtonGroup>
{#if editorKind == 'plain'}
<textarea
+12 -3
View File
@@ -263,13 +263,21 @@
}
}}
class="mb-4"
let:item
>
<ToggleButton position="left" value="normal" size="sm" label="Any jobs within worker tags" />
<ToggleButton
position="left"
value="normal"
size="sm"
label="Any jobs within worker tags"
{item}
/>
<ToggleButton
position="dedicated"
value="dedicated"
size="sm"
label="Dedicated to a script/flow"
{item}
/>
</ToggleButtonGroup>
{#if selected == 'normal'}
@@ -682,9 +690,10 @@
envvar.value = undefined
}
}}
let:item
>
<ToggleButton position="left" value="dynamic" label="Dynamic" />
<ToggleButton position="right" value="static" label="Static" />
<ToggleButton value="dynamic" label="Dynamic" {item} />
<ToggleButton value="static" label="Static" {item} />
</ToggleButtonGroup>
<input
type="text"
@@ -5,12 +5,14 @@
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
import { initCss } from '../../utils'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
import Markdown from 'svelte-exmarkdown'
import { Markdown, type Plugin } from 'svelte-exmarkdown'
import { gfmPlugin } from 'svelte-exmarkdown/gfm'
import rehypeRaw from 'rehype-raw'
import { rehypeGithubAlerts } from 'rehype-github-alerts'
import { classNames } from '$lib/utils'
import { components } from '../../editor/component'
import ResolveConfig from '../helpers/ResolveConfig.svelte'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
import { gfmPlugin } from 'svelte-exmarkdown/gfm'
export let id: string
export let componentInput: AppInput | undefined
export let initializing: boolean | undefined = undefined
@@ -18,6 +20,11 @@
export let render: boolean
export let configuration: RichConfigurations
const plugins: Plugin[] = [
gfmPlugin(),
{ rehypePlugin: [rehypeRaw] },
{ rehypePlugin: [rehypeGithubAlerts] }
]
const { app, worldStore, mode } = getContext<AppViewerContext>('AppViewerContext')
const resolvedConfig = initConfig(
@@ -86,7 +93,7 @@
{id}
bind:initializing
bind:result
>{#if result}{#key result}<Markdown md={result} plugins={[gfmPlugin()]} />{/key}{/if}
>{#if result}{#key result}<Markdown md={result} {plugins} />{/key}{/if}
</RunnableWrapper>
</div>
{:else}
@@ -143,4 +150,12 @@
.wm-markdown h4:last-child {
margin-bottom: 0;
}
.wm-markdown code:not([class~="not-prose"]):not(.not-prose *)::before,
.wm-markdown code:not([class~="not-prose"]):not(.not-prose *)::after {
content: none !important;
}
.wm-markdown.prose code::before,
.wm-markdown.prose code::after {
content: none !important;
}
</style>
@@ -144,15 +144,24 @@
runnable && runnable.type === 'runnableByName' ? runnable.inlineScript?.refreshOn ?? [] : []
function refreshIfAutoRefresh(src: 'arg changed' | 'static changed') {
// console.log(
// 'refreshIfAutoRefresh',
// src,
// id,
// iterContext ? $iterContext : undefined,
// $rowContext ? $rowContext : undefined,
// firstRefresh
// )
if (firstRefresh) {
firstRefresh = false
if (
src == 'arg changed' &&
args == undefined &&
result != undefined &&
Object.keys(runnableInputValues ?? {}).length == 0 &&
Object.keys(extraQueryParams ?? {}).length == 0
) {
// console.debug(`Skipping refreshing ${id} because ${_src} (first)`)
console.log('skipping refresh because first refresh')
return
}
}
@@ -2,7 +2,7 @@
import { getContext, onMount } from 'svelte'
import type { AppInput } from '../../inputType'
import type { Output } from '../../rx'
import type { AppViewerContext } from '../../types'
import type { AppViewerContext, ListContext } from '../../types'
import { isScriptByNameDefined, isScriptByPathDefined } from '../../utils'
import NonRunnableComponent from './NonRunnableComponent.svelte'
import RunnableComponent from './RunnableComponent.svelte'
@@ -98,6 +98,8 @@
const { staticExporter, initialized, noBackend, componentControl, runnableComponents } =
getContext<AppViewerContext>('AppViewerContext')
const iterContext = getContext<ListContext>('ListWrapperContext')
const rowContext = getContext<ListContext>('RowWrapperContext')
if (noBackend && componentInput?.type == 'runnable') {
result = componentInput?.['value']
@@ -113,15 +115,24 @@
}
})
const fullId = id + (extraKey ?? '')
if (!(initializing && componentInput?.type === 'runnable' && isRunnableDefined(componentInput))) {
initializing = false
} else {
initializing =
initializing == undefined && Object.keys($initialized?.runnableInitialized ?? {}).includes(id)
? false
: undefined
if (result == undefined && !initializing) {
result = $initialized.runnableInitialized?.[id]
if (
(initializing == undefined || initializing == true) &&
Object.keys($initialized?.runnableInitialized ?? {}).includes(fullId)
) {
initializing = false
}
if (
result == undefined &&
!initializing &&
iterContext == undefined &&
rowContext == undefined
) {
result = $initialized.runnableInitialized?.[fullId]
}
}
@@ -291,9 +302,12 @@
on:argsChanged
on:resultSet={(e) => {
const res = e.detail
if (initializing !== false) {
if ($initialized?.runnableInitialized?.[fullId] === undefined) {
console.log('resultSet', id)
$initialized.runnableInitialized = { ...$initialized.runnableInitialized, [id]: res }
$initialized.runnableInitialized = {
...($initialized.runnableInitialized ?? {}),
[fullId]: res
}
}
initializing = false
@@ -132,6 +132,7 @@
: undefined}
pathTransformer={resolvedConfig?.type?.configuration?.s3?.pathTemplate}
allowMultiple={resolvedConfigS3.allowMultiple}
allowDelete={resolvedConfigS3.allowDelete}
containerText={resolvedConfigS3.text}
customResourcePath={resolvedConfigS3.resource}
customResourceType="s3"
@@ -90,6 +90,10 @@
// 32px (2rem) of top and bottom margin
wrapperHeight - headerHeight - 64
)
async function getMenuElements(): Promise<HTMLElement[]> {
return Array.from(document.querySelectorAll('[data-menu]')) as HTMLElement[]
}
</script>
<InitializeComponent {id} />
@@ -176,7 +180,11 @@
<div
style={css?.popup?.style}
class={twMerge('mx-24 mt-8 bg-surface rounded-lg relative', css?.popup?.class)}
use:clickOutside={false}
use:clickOutside={{
capture: false,
stopPropagation: false,
exclude: getMenuElements
}}
on:click_outside={(e) => {
if ($mode !== 'dnd' && !unclickableOutside) {
handleClickAway(e)
@@ -824,7 +824,21 @@
on:hideRightPanel={() => hideRightPanel()}
on:hideBottomPanel={() => hideBottomPanel()}
on:showBottomPanel={() => showBottomPanel()}
/>
>
<svelte:fragment
slot="unsavedConfirmationModal"
let:diffDrawer
let:additionalExitAction
let:getInitialAndModifiedValues
>
<slot
name="unsavedConfirmationModal"
{diffDrawer}
{additionalExitAction}
{getInitialAndModifiedValues}
/>
</svelte:fragment>
</AppEditorHeader>
{#if $mode === 'preview'}
<SplitPanesWrapper>
<div
@@ -62,7 +62,6 @@
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
import UnsavedConfirmationModal from '$lib/components/common/confirmationModal/UnsavedConfirmationModal.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import { Sha256 } from '@aws-crypto/sha256-js'
import { sendUserToast } from '$lib/toast'
@@ -947,23 +946,26 @@
<svelte:window on:keydown={onKeyDown} />
<TestJobLoader bind:this={testJobLoader} bind:isLoading={testIsLoading} bind:job />
<UnsavedConfirmationModal
{diffDrawer}
getInitialAndModifiedValues={() => ({
savedValue: savedApp,
modifiedValue: {
summary: $summary,
value: $app,
path: newEditedPath || savedApp?.draft?.path || savedApp?.path,
policy,
custom_path: customPath
}
})}
additionalExitAction={() => {
setTheme(priorDarkMode)
}}
/>
{#if $$slots.unsavedConfirmationModal}
<slot
name="unsavedConfirmationModal"
{diffDrawer}
additionalExitAction={() => {
setTheme(priorDarkMode)
}}
getInitialAndModifiedValues={() => ({
savedValue: savedApp,
modifiedValue: {
summary: $summary,
value: $app,
path: newEditedPath || savedApp?.draft?.path || savedApp?.path,
policy,
custom_path: customPath
}
})}
/>
{/if}
<DeployOverrideConfirmationModal
bind:deployedBy
bind:confirmCallback
@@ -1553,18 +1555,21 @@
on:selected={({ detail }) => {
$app.fullscreen = detail === 'true'
}}
let:item
>
<ToggleButton
icon={AlignHorizontalSpaceAround}
value={'false'}
tooltip="The max width is 1168px and the content stay centered instead of taking the full page width"
iconProps={{ size: 16 }}
{item}
/>
<ToggleButton
tooltip="The width is of the app if the full width of its container"
icon={Expand}
value={'true'}
iconProps={{ size: 16 }}
{item}
/>
</ToggleButtonGroup>
{/if}
@@ -1578,40 +1583,46 @@
setTheme(theme)
}}
selected={$app.darkMode === undefined ? 'auto' : $app.darkMode ? 'dark' : 'sun'}
let:item
>
<ToggleButton
icon={SunMoon}
value={'auto'}
tooltip="The app mode between dark/light is automatic"
iconProps={{ size: 16 }}
{item}
/>
<ToggleButton
icon={Sun}
value={'sun'}
tooltip="Force light mode"
iconProps={{ size: 16 }}
{item}
/>
<ToggleButton
tooltip="Force dark mode"
icon={Moon}
value={'dark'}
iconProps={{ size: 16 }}
{item}
/>
</ToggleButtonGroup>
{/if}
<div class="flex flex-row gap-2">
<ToggleButtonGroup class="h-[30px]" bind:selected={$breakpoint}>
<ToggleButtonGroup class="h-[30px]" bind:selected={$breakpoint} let:item>
<ToggleButton
tooltip="Computer View"
icon={Laptop2}
value={'lg'}
iconProps={{ size: 16 }}
{item}
/>
<ToggleButton
tooltip="Mobile View"
icon={Smartphone}
value={'sm'}
iconProps={{ size: 16 }}
{item}
/>
{#if $breakpoint === 'sm'}
<Toggle
@@ -18,6 +18,7 @@
jobsById.set({})
jobs.set([])
}}
let:item
>
<ToggleButton
label="Editor"
@@ -26,6 +27,7 @@
disabled={loading}
iconProps={{ size: 16 }}
class="gap-2"
{item}
/>
<ToggleButton
label="Preview"
@@ -36,5 +38,6 @@
id="app-editor-preview-toggle"
iconProps={{ size: 16 }}
class="gap-2"
{item}
/>
</ToggleButtonGroup>

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