Compare commits

..
Author SHA1 Message Date
Pyra f40a3b420b Merge branch 'main' into debouncing-tests 2026-03-06 14:42:25 +01:00
centdix c40ad129bc rename config file (#8230) 2026-03-06 05:03:41 +00:00
wendrul 7859bca6ae fix: cli: support deleting linked resources-variables without throwing (#8248) 2026-03-05 20:09:59 +00:00
wendrul 1ac391a795 fix: wmill workspace whoami output (#8246) 2026-03-05 18:12:21 +00:00
Diego ImbertandClaude Opus 4.6 5d79f33590 Final Svelte 5 migration (#8211)
* Remove $$props.field usage

* Rename slots to ensure no hyphen

* _props

* _trigger

* OnSelectedIteration type correct capitalization

* rename _content

* Remove afterUpdate

* Migrate everything to svelte 5

* array bind

* Fix popover

* type never

* nit fixes

* Fixed many trivial errors

* onClick

* Fix errors

* use let:

* nit typing

* fix: wrap state_referenced_locally vars with untrack()

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add untrack import

* Fix all syntax errors due to untrack migration

* Fix undefined errors

* Fix more undefined errors

* untrack(() => initialOpen)

* svelte-ignore

* Fix state_descriptors_fixed error in Chart.svelte

Use $state.snapshot() to pass plain copies of data/options to Chart.js
instead of $state proxies. Chart.js's listenArrayEvents tries to define
property descriptors on data arrays, which Svelte 5 proxies reject.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* nit typing

* Merge issue

* Fix "path is not set" error in resource picker / editor

* Fix InputTransformForm error when rerunning some flows

* fix npm run check

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-05 18:11:40 +01:00
Ruben Fiszelandrubenfiszel 86065aaac8 chore(main): release 1.651.1 (#8242)
* chore(main): release 1.651.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-03-05 14:51:38 +00:00
Ruben Fiszel e3f4130c68 nits 2026-03-05 14:36:51 +00:00
Ruben FiszelandClaude Opus 4.6 2e582b1bc1 fix: prevent slow loading toast interval from leaking on promise cancellation (#8240)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 14:23:36 +00:00
Ruben FiszelandClaude Opus 4.6 2d583826dc fix: suppress unused variable warnings on windows builds (#8241)
* fix: suppress unused variable warnings on windows builds

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: update ee-repo-ref.txt to merged commit

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:17:51 +01:00
Ruben Fiszelandrubenfiszel 972ae7aa29 chore(main): release 1.651.0 (#8235)
* chore(main): release 1.651.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-03-05 13:42:07 +00:00
Ruben FiszelandClaude Opus 4.5 d46913b74a fix: write fallback package.json for codebase mode nsjail (#8239)
* fix: write fallback package.json for codebase mode to fix nsjail ERR_INVALID_PACKAGE_CONFIG

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test: add e2e tests for codebase mode with and without nsjail

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-05 13:35:53 +00:00
Roderik-WU 90f4c64ee1 fix(python-client): add delete_s3_object (#8216)
* Implement remove_s3_file method

Add method to permanently delete a file from S3 bucket.

* Add test for removing S3 file

Added a test case to verify removal of a file from S3.

* Add remove_s3_file function to delete S3 files

Added a function to permanently delete a file from the S3 bucket.

* Rename remove_s3_file to remove_3_object

* Rename remove_3_object to remove_s3_object

* Rename test method and update S3 object handling

* Rename remove_s3_object to delete_s3_object

* Rename test_remove_s3_object to test_delete_s3_object and remove_s3_object to delete_s3_object
2026-03-05 12:49:59 +00:00
hugocasaandClaude Opus 4.6 a8cbe9396f fix: update CLI bun template to match UI template (#8238)
* fix: update CLI bun template to match UI template

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: simplify CLI bun template, only add mode comments

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 12:04:38 +00:00
centdixandClaude Opus 4.6 ce041e8a5e feat: hash-based MCP tool names for long paths (#8133)
* feat: replace _TRUNC with hash-based MCP tool names (50 char limit)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: reduce MCP tool name limit from 50 to 40 chars

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: use path prefix filtering instead of separate DB query for hashed name resolution

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: remove long path warning from MCP token creation (hashing handles long names)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: unify tool prefix parsing and fix extract_path_prefix_from_hashed for Hs- names

- Replace `is_hashed_name` + `parse_hashed_name` with unified `parse_tool_prefix`
  that returns `(type_str, is_hub, is_hashed)` in one call
- Fix `extract_path_prefix_from_hashed` to dynamically determine prefix length
  (3 for `Hs-`, 2 for `S-`/`F-`) instead of hardcoding index 2
- Simplify `reverse_transform` to reuse `parse_tool_prefix`
- Add tests for invalid prefixes and `Hs-` prefix handling

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: escape LIKE wildcards in MCP hashed name path prefix query

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: respect favorites scope in hashed tool name resolution

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: deduplicate MCP tool name resolution and rename get_path_or_id

- Extract `unescape_path` helper in transform.rs to deduplicate the
  3-step placeholder unescape logic
- Extract `find_matching_path` helper in runner.rs to deduplicate
  script/flow candidate matching via ToolableItem trait
- Remove verbose tracing::info! logs from hashed tool resolution hot path
- Fix doc comment referencing nonexistent `is_hashed_name` function
- Rename `get_path_or_id` to `get_transformed_path` for clarity

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: update stale doc comments to reflect MAX_PATH_LENGTH=40

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-05 12:04:20 +00:00
65082159d8 tighten volume limits (#8236)
* feat: add volume limits info in CE volumes drawer

Show an info alert in the volumes drawer when running in Community
Edition, mentioning the 20 volumes per workspace and 50 MB per file
limits. Update ee-repo-ref for companion EE changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: update ee-repo-ref

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: update ee-repo-ref to a61366dd4d9e9b1f98a421aaa6d3f63194615275

This commit updates the EE repository reference after PR #438 was merged in windmill-ee-private.

Previous ee-repo-ref: 05385738e36e81f5bc51d15c0ca60bba30457c21

New ee-repo-ref: a61366dd4d9e9b1f98a421aaa6d3f63194615275

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
2026-03-05 06:44:32 +00:00
Pyra 993fbcde59 Merge branch 'main' into debouncing-tests 2026-03-04 17:40:57 +01:00
pyranota b1142421b8 nit: add more tests
Signed-off-by: pyranota <pyra@duck.com>
2026-03-04 17:39:54 +01:00
490 changed files with 5763 additions and 3102 deletions
View File
+23
View File
@@ -1,5 +1,28 @@
# Changelog
## [1.651.1](https://github.com/windmill-labs/windmill/compare/v1.651.0...v1.651.1) (2026-03-05)
### Bug Fixes
* prevent slow loading toast interval from leaking on promise cancellation ([#8240](https://github.com/windmill-labs/windmill/issues/8240)) ([2e582b1](https://github.com/windmill-labs/windmill/commit/2e582b1bc1c299388a3c97cfddff9d0eb92858f2))
* suppress unused variable warnings on windows builds ([#8241](https://github.com/windmill-labs/windmill/issues/8241)) ([2d58382](https://github.com/windmill-labs/windmill/commit/2d583826dc065c05684d4cd1d1510f0d1f2d9ae9))
## [1.651.0](https://github.com/windmill-labs/windmill/compare/v1.650.0...v1.651.0) (2026-03-05)
### Features
* add sandbox annotations, volume mounts, for AI sandbox starting with claude ([#8058](https://github.com/windmill-labs/windmill/issues/8058)) ([5f0ef93](https://github.com/windmill-labs/windmill/commit/5f0ef936d1d5d07d01c8e07e26ec254feebef8fb))
* hash-based MCP tool names for long paths ([#8133](https://github.com/windmill-labs/windmill/issues/8133)) ([ce041e8](https://github.com/windmill-labs/windmill/commit/ce041e8a5e7ff105df389875d9981f3843d4ce39))
### Bug Fixes
* **python-client:** add delete_s3_object ([#8216](https://github.com/windmill-labs/windmill/issues/8216)) ([90f4c64](https://github.com/windmill-labs/windmill/commit/90f4c64ee12e1d04ce846ff88d6658f667e194e0))
* update CLI bun template to match UI template ([#8238](https://github.com/windmill-labs/windmill/issues/8238)) ([a8cbe93](https://github.com/windmill-labs/windmill/commit/a8cbe9396ffc51140dce5582d57f4dc59873304e))
* write fallback package.json for codebase mode nsjail ([#8239](https://github.com/windmill-labs/windmill/issues/8239)) ([d46913b](https://github.com/windmill-labs/windmill/commit/d46913b74a0ffd41d2323e0355cc81954f09e29d))
## [1.650.0](https://github.com/windmill-labs/windmill/compare/v1.649.0...v1.650.0) (2026-03-05)
@@ -15,7 +15,7 @@
]
},
"nullable": [
true
false
]
},
"hash": "2d6607b3c38fe72b5663c32de58dacbabed4c5ae28101e3ae2694f96fd055a91"
@@ -15,7 +15,7 @@
]
},
"nullable": [
null
true
]
},
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"
@@ -15,7 +15,7 @@
]
},
"nullable": [
true
false
]
},
"hash": "eba16eb819e2644284fb073c891706d78a6f24cb0e614d7d81ba1b643805bf06"
+72 -71
View File
@@ -15741,7 +15741,7 @@ dependencies = [
[[package]]
name = "windmill"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"async-nats",
@@ -15773,6 +15773,7 @@ dependencies = [
"sql-builder",
"sqlx",
"strum 0.27.2",
"tar",
"tempfile",
"tikv-jemalloc-ctl",
"tikv-jemalloc-sys",
@@ -15807,7 +15808,7 @@ dependencies = [
[[package]]
name = "windmill-alerting"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15820,7 +15821,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"argon2",
@@ -15959,7 +15960,7 @@ dependencies = [
[[package]]
name = "windmill-api-agent-workers"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15982,7 +15983,7 @@ dependencies = [
[[package]]
name = "windmill-api-assets"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -15995,7 +15996,7 @@ dependencies = [
[[package]]
name = "windmill-api-auth"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16021,7 +16022,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"reqwest 0.12.28",
"serde",
@@ -16031,7 +16032,7 @@ dependencies = [
[[package]]
name = "windmill-api-configs"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16048,7 +16049,7 @@ dependencies = [
[[package]]
name = "windmill-api-debug"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"axum 0.7.9",
"base64 0.22.1",
@@ -16071,7 +16072,7 @@ dependencies = [
[[package]]
name = "windmill-api-embeddings"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16094,7 +16095,7 @@ dependencies = [
[[package]]
name = "windmill-api-flow-conversations"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16110,7 +16111,7 @@ dependencies = [
[[package]]
name = "windmill-api-flows"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16130,7 +16131,7 @@ dependencies = [
[[package]]
name = "windmill-api-groups"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16150,7 +16151,7 @@ dependencies = [
[[package]]
name = "windmill-api-inputs"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16164,7 +16165,7 @@ dependencies = [
[[package]]
name = "windmill-api-integration-tests"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"async-nats",
@@ -16191,7 +16192,7 @@ dependencies = [
[[package]]
name = "windmill-api-jobs"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16216,7 +16217,7 @@ dependencies = [
[[package]]
name = "windmill-api-npm-proxy"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"axum 0.7.9",
"flate2",
@@ -16234,7 +16235,7 @@ dependencies = [
[[package]]
name = "windmill-api-openapi"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16255,7 +16256,7 @@ dependencies = [
[[package]]
name = "windmill-api-schedule"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16275,7 +16276,7 @@ dependencies = [
[[package]]
name = "windmill-api-scripts"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16305,7 +16306,7 @@ dependencies = [
[[package]]
name = "windmill-api-settings"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16332,7 +16333,7 @@ dependencies = [
[[package]]
name = "windmill-api-sse"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"lazy_static",
"serde",
@@ -16344,7 +16345,7 @@ dependencies = [
[[package]]
name = "windmill-api-users"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"argon2",
"axum 0.7.9",
@@ -16367,7 +16368,7 @@ dependencies = [
[[package]]
name = "windmill-api-workers"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16381,7 +16382,7 @@ dependencies = [
[[package]]
name = "windmill-api-workspaces"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"axum 0.7.9",
"chrono",
@@ -16412,7 +16413,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"chrono",
"lazy_static",
@@ -16426,7 +16427,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -16445,7 +16446,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"aes-gcm",
"anyhow",
@@ -16544,7 +16545,7 @@ dependencies = [
[[package]]
name = "windmill-dep-map"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"chrono",
"itertools 0.14.0",
@@ -16563,7 +16564,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"regex",
"serde",
@@ -16578,7 +16579,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -16602,7 +16603,7 @@ dependencies = [
[[package]]
name = "windmill-jseval"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"futures",
@@ -16619,7 +16620,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -16635,7 +16636,7 @@ dependencies = [
[[package]]
name = "windmill-mcp"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"async-trait",
@@ -16656,7 +16657,7 @@ dependencies = [
[[package]]
name = "windmill-native-triggers"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"async-trait",
@@ -16687,7 +16688,7 @@ dependencies = [
[[package]]
name = "windmill-oauth"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"async-oauth2",
@@ -16711,7 +16712,7 @@ dependencies = [
[[package]]
name = "windmill-object-store"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"async-stream",
@@ -16745,7 +16746,7 @@ dependencies = [
[[package]]
name = "windmill-operator"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"futures",
@@ -16763,7 +16764,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -16772,7 +16773,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -16784,7 +16785,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"serde_json",
@@ -16796,7 +16797,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"gosyn",
@@ -16808,7 +16809,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -16820,7 +16821,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"serde_json",
@@ -16832,7 +16833,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"nu-parser",
@@ -16843,7 +16844,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -16854,7 +16855,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -16867,7 +16868,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"async-recursion",
@@ -16891,7 +16892,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -16905,7 +16906,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -16922,7 +16923,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -16937,7 +16938,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -16956,7 +16957,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"serde",
@@ -16967,7 +16968,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"async-recursion",
@@ -17004,7 +17005,7 @@ dependencies = [
[[package]]
name = "windmill-runtime-nativets"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"const_format",
@@ -17042,7 +17043,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"getrandom 0.3.4",
"wasm-bindgen",
@@ -17053,7 +17054,7 @@ dependencies = [
[[package]]
name = "windmill-store"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"async-recursion",
@@ -17082,7 +17083,7 @@ dependencies = [
[[package]]
name = "windmill-test-utils"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -17105,7 +17106,7 @@ dependencies = [
[[package]]
name = "windmill-trigger"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"async-trait",
@@ -17138,7 +17139,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-email"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"async-trait",
@@ -17158,7 +17159,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-gcp"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"async-trait",
@@ -17192,7 +17193,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-http"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"async-trait",
@@ -17227,7 +17228,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-kafka"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"async-trait",
@@ -17250,7 +17251,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-mqtt"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"async-trait",
@@ -17274,7 +17275,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-nats"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"async-nats",
@@ -17298,7 +17299,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-postgres"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"async-trait",
@@ -17333,7 +17334,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-sqs"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"async-trait",
@@ -17361,7 +17362,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-websocket"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"async-trait",
@@ -17384,7 +17385,7 @@ dependencies = [
[[package]]
name = "windmill-types"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"bitflags 2.9.4",
@@ -17402,7 +17403,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"anyhow",
"async-once-cell",
@@ -17508,7 +17509,7 @@ dependencies = [
[[package]]
name = "windmill-worker-volumes"
version = "1.650.0"
version = "1.651.1"
dependencies = [
"bytes",
"futures",
+3 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.650.0"
version = "1.651.1"
authors.workspace = true
edition.workspace = true
@@ -77,7 +77,7 @@ members = [
exclude = ["./windmill-duckdb-ffi-internal"]
[workspace.package]
version = "1.650.0"
version = "1.651.1"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -257,6 +257,7 @@ axum.workspace = true
serde.workspace = true
windmill-api-client.workspace = true
tempfile.workspace = true
tar.workspace = true
windmill-parser-ts.workspace = true
rumqttc.workspace = true
rdkafka.workspace = true
+1 -1
View File
@@ -1 +1 @@
05385738e36e81f5bc51d15c0ca60bba30457c21
f9549c813b3dba5324ea9d1edacc8756a6d699bf
+238
View File
@@ -1,5 +1,6 @@
use sqlx::postgres::Postgres;
use sqlx::Pool;
use uuid::Uuid;
use windmill_common::jobs::{JobPayload, RawCode};
use windmill_common::scripts::ScriptLang;
use windmill_test_utils::*;
@@ -1448,3 +1449,240 @@ export function main() { return { a, b }; }
);
}
}
// ============================================================================
// Codebase Mode Tests
// ============================================================================
/// Create a TAR archive in memory containing a single `main.js` file.
fn create_codebase_tar(main_js_content: &str) -> Vec<u8> {
let mut builder = tar::Builder::new(Vec::new());
let content = main_js_content.as_bytes();
let mut header = tar::Header::new_gnu();
header.set_path("main.js").unwrap();
header.set_size(content.len() as u64);
header.set_mode(0o644);
header.set_cksum();
builder.append(&header, content).unwrap();
builder.into_inner().unwrap()
}
/// Place a TAR codebase at the expected cache path for the given job ID and hash.
fn place_codebase_in_cache(job_id: &Uuid, tar_bytes: &[u8], is_esm: bool) {
let codebase_id = if is_esm {
format!("{}.esm.tar", job_id)
} else {
format!("{}.tar", job_id)
};
let bundle_path = format!("script_bundle/test-workspace/{}", codebase_id);
let cache_path = format!(
"{}/{}.tar",
*windmill_common::worker::ROOT_CACHE_NOMOUNT_DIR,
bundle_path,
);
let parent = std::path::Path::new(&cache_path).parent().unwrap();
std::fs::create_dir_all(parent).unwrap();
std::fs::write(&cache_path, tar_bytes).unwrap();
}
#[sqlx::test(fixtures("base"))]
async fn test_cjs_codebase_tar(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let main_js = r#"
module.exports.main = function() {
return "cjs codebase ok";
};
"#;
let inner_content = r#"export function main() { return "cjs codebase ok"; }"#;
let job_id = Uuid::new_v4();
let tar_bytes = create_codebase_tar(main_js);
place_codebase_in_cache(&job_id, &tar_bytes, false);
let job = JobPayload::Code(RawCode {
hash: Some(-43), // PREVIEW_IS_TAR_CODEBASE_HASH
content: inner_content.to_string(),
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings: Default::default(),
debouncing_settings: Default::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
});
let result = RunJob::from(job)
.job_id(job_id)
.run_until_complete(&db, false, port)
.await
.json_result()
.unwrap();
assert_eq!(result, serde_json::json!("cjs codebase ok"));
Ok(())
}
#[sqlx::test(fixtures("base"))]
async fn test_esm_codebase_tar(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let main_js = r#"
export function main() {
return "esm codebase ok";
}
"#;
let inner_content = r#"export function main() { return "esm codebase ok"; }"#;
let job_id = Uuid::new_v4();
let tar_bytes = create_codebase_tar(main_js);
place_codebase_in_cache(&job_id, &tar_bytes, true);
let job = JobPayload::Code(RawCode {
hash: Some(-45), // PREVIEW_IS_TAR_ESM_CODEBASE_HASH
content: inner_content.to_string(),
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings: Default::default(),
debouncing_settings: Default::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
});
let result = RunJob::from(job)
.job_id(job_id)
.run_until_complete(&db, false, port)
.await
.json_result()
.unwrap();
assert_eq!(result, serde_json::json!("esm codebase ok"));
Ok(())
}
#[sqlx::test(fixtures("base"))]
async fn test_cjs_codebase_tar_nsjail(db: Pool<Postgres>) -> anyhow::Result<()> {
if std::process::Command::new("nsjail")
.arg("--help")
.output()
.is_err()
{
eprintln!("nsjail not found, skipping test");
return Ok(());
}
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let main_js = r#"
module.exports.main = function() {
return "cjs nsjail ok";
};
"#;
let inner_content = r#"export function main() { return "cjs nsjail ok"; }"#;
let job_id = Uuid::new_v4();
let tar_bytes = create_codebase_tar(main_js);
place_codebase_in_cache(&job_id, &tar_bytes, false);
let job = JobPayload::Code(RawCode {
hash: Some(-43),
content: inner_content.to_string(),
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings: Default::default(),
debouncing_settings: Default::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
});
use std::sync::atomic::Ordering;
windmill_worker::JOB_ISOLATION.store(
windmill_worker::JobIsolationLevel::NsjailSandboxing as u8,
Ordering::Relaxed,
);
let result = RunJob::from(job)
.job_id(job_id)
.run_until_complete(&db, false, port)
.await;
windmill_worker::JOB_ISOLATION.store(
windmill_worker::JobIsolationLevel::Undefined as u8,
Ordering::Relaxed,
);
let json = result.json_result().unwrap();
assert_eq!(json, serde_json::json!("cjs nsjail ok"));
Ok(())
}
#[sqlx::test(fixtures("base"))]
async fn test_esm_codebase_tar_nsjail(db: Pool<Postgres>) -> anyhow::Result<()> {
if std::process::Command::new("nsjail")
.arg("--help")
.output()
.is_err()
{
eprintln!("nsjail not found, skipping test");
return Ok(());
}
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let main_js = r#"
export function main() {
return "esm nsjail ok";
}
"#;
let inner_content = r#"export function main() { return "esm nsjail ok"; }"#;
let job_id = Uuid::new_v4();
let tar_bytes = create_codebase_tar(main_js);
place_codebase_in_cache(&job_id, &tar_bytes, true);
let job = JobPayload::Code(RawCode {
hash: Some(-45),
content: inner_content.to_string(),
path: None,
language: ScriptLang::Bun,
lock: None,
concurrency_settings: Default::default(),
debouncing_settings: Default::default(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
});
use std::sync::atomic::Ordering;
windmill_worker::JOB_ISOLATION.store(
windmill_worker::JobIsolationLevel::NsjailSandboxing as u8,
Ordering::Relaxed,
);
let result = RunJob::from(job)
.job_id(job_id)
.run_until_complete(&db, false, port)
.await;
windmill_worker::JOB_ISOLATION.store(
windmill_worker::JobIsolationLevel::Undefined as u8,
Ordering::Relaxed,
);
let json = result.json_result().unwrap();
assert_eq!(json, serde_json::json!("esm nsjail ok"));
Ok(())
}
+7 -17
View File
@@ -9,23 +9,7 @@ export async function main() {
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/leafs/ts', 500001, 'nativets', '');
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'test-workspace',
'test-user',
'
package main
import "fmt"
func main() {
fmt.Println("Go leaf")
}',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/leafs/go', 500002, 'go', '');
'f/leafs/ts', 500001, 'bun', '');
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'test-workspace',
@@ -52,3 +36,9 @@ function main() {
'',
'f/leafs/php', 500004, 'php', '');
-- Link scripts to named workspace dependencies (name: "test")
INSERT INTO dependency_map (workspace_id, importer_path, importer_kind, imported_path, importer_node_id) VALUES
('test-workspace', 'f/leafs/ts', 'script', 'dependencies/test.package.json', ''),
('test-workspace', 'f/leafs/python', 'script', 'dependencies/test.requirements.in', ''),
('test-workspace', 'f/leafs/php', 'script', 'dependencies/test.composer.json', '');
+571 -60
View File
@@ -1,21 +1,17 @@
mod workspace_dependencies {
use windmill_test_utils::in_test_worker;
use windmill_test_utils::init_client;
use windmill_test_utils::listen_for_completed_jobs;
use sqlx::{Pool, Postgres};
use tokio_stream::StreamExt;
use windmill_common::scripts::ScriptLang;
use windmill_common::workspace_dependencies::WorkspaceDependencies;
use windmill_dep_map::workspace_dependencies::NewWorkspaceDependencies;
use windmill_test_utils::in_test_worker;
use windmill_test_utils::init_client;
use windmill_test_utils::listen_for_completed_jobs;
mod deps {
pub const REQUIREMENTS_IN: &'static str = "tiny==0.1.3";
// pub const GO_MOD: &'static str = r##"
// module example.com/project
// go 1.20
// require github.com/gin-gonic/gin v1.8.1
// "##;
pub const REQUIREMENTS_IN_V2: &'static str = "tiny==0.2.0";
pub const PACKAGE_JSON: &'static str = r##"
{
@@ -25,6 +21,18 @@ mod workspace_dependencies {
"express": "^4.17.1"
}
}
"##;
#[allow(dead_code)]
pub const PACKAGE_JSON_V2: &'static str = r##"
{
"name": "example-project",
"version": "2.0.0",
"dependencies": {
"express": "^4.18.0",
"axios": "^1.0.0"
}
}
"##;
pub const COMPOSER_JSON: &'static str = r##"
@@ -37,9 +45,510 @@ mod workspace_dependencies {
"##;
}
// =========================================================================
// CRUD Tests
// =========================================================================
/// Test: Create workspace dependencies and verify they are stored correctly.
#[sqlx::test(fixtures("base"))]
async fn test_create_workspace_dependencies(db: Pool<Postgres>) -> anyhow::Result<()> {
let id = NewWorkspaceDependencies {
workspace_id: "test-workspace".into(),
language: ScriptLang::Python3,
content: deps::REQUIREMENTS_IN.into(),
name: Some("test-deps".to_owned()),
description: Some("Test dependencies".to_owned()),
}
.create(
(
"test@test.com".to_owned(),
"u/test".to_owned(),
"test".to_owned(),
),
db.clone(),
)
.await?;
assert!(id > 0, "Should return a valid ID");
// Verify it was stored correctly
let stored = WorkspaceDependencies::get(id, "test-workspace".to_owned(), &db).await?;
assert_eq!(stored.name, Some("test-deps".to_owned()));
assert_eq!(stored.content, deps::REQUIREMENTS_IN);
assert_eq!(stored.language, ScriptLang::Python3);
Ok(())
}
/// Test: Create unnamed (default) workspace dependencies.
#[sqlx::test(fixtures("base"))]
async fn test_create_unnamed_workspace_dependencies(db: Pool<Postgres>) -> anyhow::Result<()> {
let id = NewWorkspaceDependencies {
workspace_id: "test-workspace".into(),
language: ScriptLang::Bun,
content: deps::PACKAGE_JSON.into(),
name: None, // Unnamed = default
description: None,
}
.create(
(
"test@test.com".to_owned(),
"u/test".to_owned(),
"test".to_owned(),
),
db.clone(),
)
.await?;
assert!(id > 0, "Should return a valid ID");
// Verify it was stored correctly
let stored = WorkspaceDependencies::get(id, "test-workspace".to_owned(), &db).await?;
assert_eq!(stored.name, None);
assert_eq!(stored.language, ScriptLang::Bun);
Ok(())
}
/// Test: List workspace dependencies returns all active entries.
#[sqlx::test(fixtures("base"))]
async fn test_list_workspace_dependencies(db: Pool<Postgres>) -> anyhow::Result<()> {
// Create multiple workspace dependencies
for (lang, content, name) in [
(ScriptLang::Python3, deps::REQUIREMENTS_IN, Some("python-deps")),
(ScriptLang::Bun, deps::PACKAGE_JSON, Some("bun-deps")),
(ScriptLang::Bun, deps::PACKAGE_JSON, None), // Default bun deps
] {
NewWorkspaceDependencies {
workspace_id: "test-workspace".into(),
language: lang,
content: content.into(),
name: name.map(|s| s.to_owned()),
description: None,
}
.create(
(
"test@test.com".to_owned(),
"u/test".to_owned(),
"test".to_owned(),
),
db.clone(),
)
.await?;
}
let list = WorkspaceDependencies::list("test-workspace", &db).await?;
assert_eq!(list.len(), 3, "Should have 3 workspace dependencies");
Ok(())
}
/// Test: Archive workspace dependencies marks them as archived.
#[sqlx::test(fixtures("base"))]
async fn test_archive_workspace_dependencies(db: Pool<Postgres>) -> anyhow::Result<()> {
// Create workspace dependencies
let _id = NewWorkspaceDependencies {
workspace_id: "test-workspace".into(),
language: ScriptLang::Python3,
content: deps::REQUIREMENTS_IN.into(),
name: Some("to-archive".to_owned()),
description: None,
}
.create(
(
"test@test.com".to_owned(),
"u/test".to_owned(),
"test".to_owned(),
),
db.clone(),
)
.await?;
// Verify it exists
let list_before = WorkspaceDependencies::list("test-workspace", &db).await?;
assert_eq!(list_before.len(), 1);
// Archive it
WorkspaceDependencies::archive(
Some("to-archive".to_owned()),
ScriptLang::Python3,
"test-workspace",
&db,
)
.await?;
// Verify it's no longer in the active list
let list_after = WorkspaceDependencies::list("test-workspace", &db).await?;
assert_eq!(list_after.len(), 0, "Archived deps should not appear in list");
Ok(())
}
/// Test: Delete workspace dependencies permanently removes them.
#[sqlx::test(fixtures("base"))]
async fn test_delete_workspace_dependencies(db: Pool<Postgres>) -> anyhow::Result<()> {
// Create workspace dependencies
let id = NewWorkspaceDependencies {
workspace_id: "test-workspace".into(),
language: ScriptLang::Python3,
content: deps::REQUIREMENTS_IN.into(),
name: Some("to-delete".to_owned()),
description: None,
}
.create(
(
"test@test.com".to_owned(),
"u/test".to_owned(),
"test".to_owned(),
),
db.clone(),
)
.await?;
// Verify it exists
assert!(
WorkspaceDependencies::get(id, "test-workspace".to_owned(), &db)
.await
.is_ok()
);
// Delete it
WorkspaceDependencies::delete(
Some("to-delete".to_owned()),
ScriptLang::Python3,
"test-workspace",
&db,
)
.await?;
// Verify it's gone (should error)
let result = WorkspaceDependencies::get(id, "test-workspace".to_owned(), &db).await;
assert!(result.is_err(), "Deleted deps should not be retrievable");
Ok(())
}
// =========================================================================
// Version History Tests
// =========================================================================
/// Test: Creating new version archives the old one.
#[sqlx::test(fixtures("base"))]
async fn test_versioning_archives_previous(db: Pool<Postgres>) -> anyhow::Result<()> {
// Create first version
let id1 = NewWorkspaceDependencies {
workspace_id: "test-workspace".into(),
language: ScriptLang::Python3,
content: deps::REQUIREMENTS_IN.into(),
name: Some("versioned".to_owned()),
description: Some("Version 1".to_owned()),
}
.create(
(
"test@test.com".to_owned(),
"u/test".to_owned(),
"test".to_owned(),
),
db.clone(),
)
.await?;
// Create second version with same name
let id2 = NewWorkspaceDependencies {
workspace_id: "test-workspace".into(),
language: ScriptLang::Python3,
content: deps::REQUIREMENTS_IN_V2.into(),
name: Some("versioned".to_owned()),
description: Some("Version 2".to_owned()),
}
.create(
(
"test@test.com".to_owned(),
"u/test".to_owned(),
"test".to_owned(),
),
db.clone(),
)
.await?;
assert_ne!(id1, id2, "Should create a new entry");
// List should only show the active (latest) version
let list = WorkspaceDependencies::list("test-workspace", &db).await?;
assert_eq!(list.len(), 1, "Should only have 1 active entry");
assert_eq!(list[0].content, deps::REQUIREMENTS_IN_V2);
// History should show both versions
let history = WorkspaceDependencies::get_history(
Some("versioned".to_owned()),
ScriptLang::Python3,
"test-workspace",
&db,
)
.await?;
assert_eq!(history.len(), 2, "Should have 2 versions in history");
Ok(())
}
/// Test: Description is inherited from previous version if not provided.
#[sqlx::test(fixtures("base"))]
async fn test_description_inheritance(db: Pool<Postgres>) -> anyhow::Result<()> {
// Create first version with description
NewWorkspaceDependencies {
workspace_id: "test-workspace".into(),
language: ScriptLang::Python3,
content: deps::REQUIREMENTS_IN.into(),
name: Some("inherit-desc".to_owned()),
description: Some("Original description".to_owned()),
}
.create(
(
"test@test.com".to_owned(),
"u/test".to_owned(),
"test".to_owned(),
),
db.clone(),
)
.await?;
// Create second version without description
let id2 = NewWorkspaceDependencies {
workspace_id: "test-workspace".into(),
language: ScriptLang::Python3,
content: deps::REQUIREMENTS_IN_V2.into(),
name: Some("inherit-desc".to_owned()),
description: None, // Should inherit
}
.create(
(
"test@test.com".to_owned(),
"u/test".to_owned(),
"test".to_owned(),
),
db.clone(),
)
.await?;
let stored = WorkspaceDependencies::get(id2, "test-workspace".to_owned(), &db).await?;
assert_eq!(
stored.description,
Some("Original description".to_owned()),
"Description should be inherited from previous version"
);
Ok(())
}
// =========================================================================
// Workspace Isolation Tests
// =========================================================================
/// Test: Workspace dependencies are isolated between workspaces.
#[sqlx::test(fixtures("base"))]
async fn test_workspace_isolation(db: Pool<Postgres>) -> anyhow::Result<()> {
// Create another workspace
sqlx::query!(
"INSERT INTO workspace (id, name, owner) VALUES ('other-workspace', 'other', 'test-user')"
)
.execute(&db)
.await?;
sqlx::query!("INSERT INTO workspace_settings (workspace_id) VALUES ('other-workspace')")
.execute(&db)
.await?;
// Create deps in test-workspace
NewWorkspaceDependencies {
workspace_id: "test-workspace".into(),
language: ScriptLang::Python3,
content: deps::REQUIREMENTS_IN.into(),
name: Some("shared-name".to_owned()),
description: None,
}
.create(
(
"test@test.com".to_owned(),
"u/test".to_owned(),
"test".to_owned(),
),
db.clone(),
)
.await?;
// Create deps in other-workspace with same name
NewWorkspaceDependencies {
workspace_id: "other-workspace".into(),
language: ScriptLang::Python3,
content: deps::REQUIREMENTS_IN_V2.into(),
name: Some("shared-name".to_owned()),
description: None,
}
.create(
(
"test@test.com".to_owned(),
"u/test".to_owned(),
"test".to_owned(),
),
db.clone(),
)
.await?;
// Each workspace should have exactly 1 entry
let list1 = WorkspaceDependencies::list("test-workspace", &db).await?;
let list2 = WorkspaceDependencies::list("other-workspace", &db).await?;
assert_eq!(list1.len(), 1);
assert_eq!(list2.len(), 1);
// Content should be different
assert_eq!(list1[0].content, deps::REQUIREMENTS_IN);
assert_eq!(list2[0].content, deps::REQUIREMENTS_IN_V2);
Ok(())
}
// =========================================================================
// Language-specific Tests
// =========================================================================
/// Test: Different languages can have same-named workspace dependencies.
#[sqlx::test(fixtures("base"))]
async fn test_same_name_different_languages(db: Pool<Postgres>) -> anyhow::Result<()> {
// Create Python deps
NewWorkspaceDependencies {
workspace_id: "test-workspace".into(),
language: ScriptLang::Python3,
content: deps::REQUIREMENTS_IN.into(),
name: Some("common".to_owned()),
description: None,
}
.create(
(
"test@test.com".to_owned(),
"u/test".to_owned(),
"test".to_owned(),
),
db.clone(),
)
.await?;
// Create Bun deps with same name
NewWorkspaceDependencies {
workspace_id: "test-workspace".into(),
language: ScriptLang::Bun,
content: deps::PACKAGE_JSON.into(),
name: Some("common".to_owned()),
description: None,
}
.create(
(
"test@test.com".to_owned(),
"u/test".to_owned(),
"test".to_owned(),
),
db.clone(),
)
.await?;
let list = WorkspaceDependencies::list("test-workspace", &db).await?;
assert_eq!(list.len(), 2, "Should have 2 entries (different languages)");
let python_deps: Vec<_> = list
.iter()
.filter(|d| d.language == ScriptLang::Python3)
.collect();
let bun_deps: Vec<_> = list
.iter()
.filter(|d| d.language == ScriptLang::Bun)
.collect();
assert_eq!(python_deps.len(), 1);
assert_eq!(bun_deps.len(), 1);
Ok(())
}
/// Test: Nativets and Bunnative use Bun workspace dependencies.
#[sqlx::test(fixtures("base"))]
async fn test_nativets_uses_bun_deps(db: Pool<Postgres>) -> anyhow::Result<()> {
use windmill_common::worker::Connection;
// Create Bun deps (which Nativets should use)
NewWorkspaceDependencies {
workspace_id: "test-workspace".into(),
language: ScriptLang::Bun,
content: deps::PACKAGE_JSON.into(),
name: None,
description: None,
}
.create(
(
"test@test.com".to_owned(),
"u/test".to_owned(),
"test".to_owned(),
),
db.clone(),
)
.await?;
// Query for Nativets should return Bun deps
let result = WorkspaceDependencies::get_latest(
None,
ScriptLang::Nativets,
"test-workspace",
Connection::Sql(db.clone()),
)
.await?;
assert!(result.is_some(), "Nativets should find Bun deps");
assert_eq!(result.unwrap().language, ScriptLang::Bun);
Ok(())
}
// =========================================================================
// Path Generation Tests
// =========================================================================
/// Test: to_path generates correct paths for named and unnamed deps.
#[test]
fn test_to_path_generation() {
// Unnamed (default) deps
let path = WorkspaceDependencies::to_path(&None, ScriptLang::Python3).unwrap();
assert_eq!(path, "dependencies/requirements.in");
let path = WorkspaceDependencies::to_path(&None, ScriptLang::Bun).unwrap();
assert_eq!(path, "dependencies/package.json");
let path = WorkspaceDependencies::to_path(&None, ScriptLang::Php).unwrap();
assert_eq!(path, "dependencies/composer.json");
// Named deps
let path =
WorkspaceDependencies::to_path(&Some("custom".to_owned()), ScriptLang::Python3).unwrap();
assert_eq!(path, "dependencies/custom.requirements.in");
let path =
WorkspaceDependencies::to_path(&Some("custom".to_owned()), ScriptLang::Bun).unwrap();
assert_eq!(path, "dependencies/custom.package.json");
}
/// Test: to_path returns error for unsupported languages.
#[test]
fn test_to_path_unsupported_language() {
// Deno doesn't support workspace dependencies
let result = WorkspaceDependencies::to_path(&None, ScriptLang::Deno);
assert!(result.is_err(), "Deno should not support workspace deps");
}
/// Test E2E: Creating named workspace dependencies triggers re-lock jobs for dependent scripts.
///
/// This test:
/// 1. Uses fixture with Python, Bun, PHP scripts linked to named workspace deps via dependency_map
/// 2. Creates named workspace dependencies for each language
/// 3. Verifies dependency jobs are triggered for all linked scripts
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "workspace_dependencies_leafs"))]
#[ignore]
async fn basic_manual_named(db: Pool<Postgres>) -> anyhow::Result<()> {
let ((_client, port, _s), db, mut completed) = (
init_client(db.clone()).await,
@@ -47,67 +556,69 @@ mod workspace_dependencies {
listen_for_completed_jobs(&db).await,
);
for (idx, (l, c)) in [
// Create named workspace dependencies for Python, Bun, and PHP
// These will trigger dependency jobs for scripts linked via dependency_map
for (lang, content) in [
(ScriptLang::Python3, deps::REQUIREMENTS_IN),
(ScriptLang::Bun, deps::PACKAGE_JSON),
(ScriptLang::Php, deps::COMPOSER_JSON),
// (ScriptLang::Go, deps::GO_MOD),
]
.iter()
.enumerate()
{
let id = NewWorkspaceDependencies {
] {
NewWorkspaceDependencies {
workspace_id: "test-workspace".into(),
language: *l,
content: (*c).into(),
language: lang,
content: content.into(),
name: Some("test".to_owned()),
description: None,
}
.create(("".to_owned(), "".to_owned(), "".to_owned()), db.clone())
.await
.unwrap();
assert_eq!(idx + 1, id as usize);
.create(
(
"test@test.com".to_owned(),
"u/test-user".to_owned(),
"test-user".to_owned(),
),
db.clone(),
)
.await?;
}
// Wait for 4 jobs.
// Creating those dependencies will trigger redeployment of all scripts in workspace_dependencies_leafs.sql
in_test_worker(
db,
async {
completed.next().await;
completed.next().await;
completed.next().await;
// completed.next().await;
},
port,
)
.await;
// Wait for 3 dependency jobs (one per script in fixture)
let mut completed_paths = vec![];
for _ in 0..3 {
let job_id = in_test_worker(db, async { completed.next().await }, port)
.await
.expect("Expected a dependency job to complete");
// Verify all scripts have correct locks
// let mut langs = vec![];
// for r in sqlx::query!(
// r#"SELECT language AS "language: ScriptLang",lock FROM script WHERE archived = false"#
// )
// .fetch_all(db)
// .await
// .unwrap()
// {
// match r.language {
// ScriptLang::Python3 => assert_eq!("", &r.lock.unwrap()),
// ScriptLang::Go => todo!(),
// ScriptLang::Bun => todo!(),
// ScriptLang::Bunnative => todo!(),
// ScriptLang::Php => todo!(),
// _ => panic!("Unsupported language"),
// }
let job_path = sqlx::query_scalar!(
"SELECT runnable_path FROM v2_job WHERE id = $1",
job_id
)
.fetch_one(db)
.await?;
// langs.push(r.language);
// }
if let Some(path) = job_path {
completed_paths.push(path);
}
}
// langs.sort();
// // Just tiny additional verification for peace of mind.
// assert_eq!(langs.as_slice(), &[]);
// Verify all 3 scripts received dependency jobs
completed_paths.sort();
let expected = vec![
"f/leafs/php".to_string(),
"f/leafs/python".to_string(),
"f/leafs/ts".to_string(),
];
assert_eq!(
completed_paths, expected,
"All scripts should have received dependency jobs"
);
// Verify no extra jobs were created
let total_jobs = sqlx::query_scalar!("SELECT COUNT(*) FROM v2_job")
.fetch_one(db)
.await?;
assert_eq!(total_jobs, Some(3), "Should have exactly 3 jobs");
Ok(())
}
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.650.0
version: 1.651.1
title: Windmill API
contact:
+4 -2
View File
@@ -64,9 +64,10 @@ impl McpBackend for WindmillBackend {
auth: &ApiAuthed,
workspace_id: &str,
favorites_only: bool,
path_prefix: Option<&str>,
) -> BackendResult<Vec<ScriptInfo>> {
let scope_type = if favorites_only { "favorites" } else { "all" };
get_items::<ScriptInfo>(&self.user_db, auth, workspace_id, scope_type, "script")
get_items::<ScriptInfo>(&self.user_db, auth, workspace_id, scope_type, "script", path_prefix)
.await
.map_err(|e| ErrorData::internal_error(e.message, None))
}
@@ -76,9 +77,10 @@ impl McpBackend for WindmillBackend {
auth: &ApiAuthed,
workspace_id: &str,
favorites_only: bool,
path_prefix: Option<&str>,
) -> BackendResult<Vec<FlowInfo>> {
let scope_type = if favorites_only { "favorites" } else { "all" };
get_items::<FlowInfo>(&self.user_db, auth, workspace_id, scope_type, "flow")
get_items::<FlowInfo>(&self.user_db, auth, workspace_id, scope_type, "flow", path_prefix)
.await
.map_err(|e| ErrorData::internal_error(e.message, None))
}
+6
View File
@@ -136,6 +136,7 @@ pub async fn get_items<T: for<'a> sqlx::FromRow<'a, sqlx::postgres::PgRow> + Sen
workspace_id: &str,
scope_type: &str,
item_type: &str,
path_prefix: Option<&str>,
) -> Result<Vec<T>, ErrorData> {
let mut sqlb = SqlBuilder::select_from(&format!("{} as o", item_type));
let fields = vec!["o.path", "o.summary", "o.description", "o.schema"];
@@ -153,6 +154,11 @@ pub async fn get_items<T: for<'a> sqlx::FromRow<'a, sqlx::postgres::PgRow> + Sen
sqlb.and_where("(o.no_main_func IS NOT TRUE OR o.no_main_func IS NULL)");
}
if let Some(prefix) = path_prefix {
let escaped = prefix.replace('\\', "\\\\").replace('%', "\\%").replace('_', "\\_");
sqlb.and_where("o.path LIKE ? ESCAPE '\\'".bind(&format!("{}%", escaped)));
}
sqlb.order_by(
if item_type == "flow" {
"o.edited_at"
+3 -1
View File
@@ -11,6 +11,8 @@ pub mod types;
pub use schema::convert_schema_to_schema_type;
pub use scope::{is_resource_allowed, parse_mcp_scopes, McpScopeConfig};
pub use transform::{
apply_key_transformation, reverse_transform, reverse_transform_key, transform_path,
apply_key_transformation, extract_hub_version_id_from_hashed,
extract_path_prefix_from_hashed, parse_tool_prefix, reverse_transform, reverse_transform_key,
transform_hub_path, transform_path,
};
pub use types::*;
+314 -39
View File
@@ -4,9 +4,15 @@
//! to make them compatible with MCP tool naming requirements.
use super::types::SchemaType;
use windmill_common::utils::calculate_hash;
/// MCP clients do not allow names longer than 60 characters
const MAX_PATH_LENGTH: usize = 60;
/// Max tool name length. The MCP spec allows 64 chars, but some clients
/// (e.g. Cursor) prepend the server name to the tool name, so we use 40
/// to leave room for that prefix.
const MAX_PATH_LENGTH: usize = 40;
/// Length of the SHA256 hash suffix used for hashed names
const HASH_LEN: usize = 16;
/// Transform the path for workspace scripts/flows
///
@@ -14,19 +20,133 @@ const MAX_PATH_LENGTH: usize = 60;
/// path with the type prefix. This is used when listing, because we can't
/// have names with slashes. Because we replace slashes with underscores,
/// we also need to escape underscores.
///
/// For short names (≤40 chars): `s-{escaped_path}` or `f-{escaped_path}`
/// For long names (>40 chars): `S-{escaped[:22]}{sha256[:16]}` or `F-{escaped[:22]}{sha256[:16]}`
///
/// The uppercase prefix signals that the name is hashed.
pub fn transform_path(path: &str, type_str: &str) -> String {
let escaped_path = path.replace('_', "__").replace('/', "_");
// first letter of type_str is used as prefix, only one letter to avoid reaching 60 char name limit
let transformed_path = format!("{}-{}", &type_str[..1], escaped_path);
if transformed_path.len() > MAX_PATH_LENGTH {
let suffix = "_TRUNC";
return format!(
"{}{}",
&transformed_path[..MAX_PATH_LENGTH - suffix.len()],
suffix
);
let prefix_char = &type_str[..1];
let short_name = format!("{}-{}", prefix_char, escaped_path);
if short_name.len() <= MAX_PATH_LENGTH {
return short_name;
}
transformed_path
let upper_prefix = prefix_char.to_uppercase();
// Layout: "{Upper}-" (2 chars) + prefix_body (22 chars) + hash (16 chars) = 40
let prefix_body_len = MAX_PATH_LENGTH - 2 - HASH_LEN;
let hash = calculate_hash(&short_name);
let hash_suffix = &hash[..HASH_LEN];
let truncated = truncate_to_char_boundary(&escaped_path, prefix_body_len);
format!("{}-{}{}", upper_prefix, truncated, hash_suffix)
}
/// Transform the path for hub scripts
///
/// For short names (≤40 chars): `hs-{id}-{summary}`
/// For long names (>40 chars): `Hs-{id}-{summary[:N]}{sha256[:16]}`
pub fn transform_hub_path(version_id: u64, summary: &str) -> String {
let escaped_summary = summary.replace(' ', "_");
let short_name = format!("hs-{}-{}", version_id, escaped_summary);
if short_name.len() <= MAX_PATH_LENGTH {
return short_name;
}
let hash = calculate_hash(&short_name);
let hash_suffix = &hash[..HASH_LEN];
// "Hs-{id}-" prefix, then fill remaining with summary + hash
let fixed_prefix = format!("Hs-{}-", version_id);
let available = MAX_PATH_LENGTH - fixed_prefix.len() - HASH_LEN;
let truncated_summary = truncate_to_char_boundary(&escaped_summary, available);
format!("{}{}{}", fixed_prefix, truncated_summary, hash_suffix)
}
/// Parse the prefix of any tool name (both short and hashed).
/// Returns `(type_str, is_hub, is_hashed)`.
/// Hashed names use an uppercase first character as the signal.
pub fn parse_tool_prefix(name: &str) -> Result<(&str, bool, bool), String> {
let is_hashed = name.chars().next().map(|c| c.is_ascii_uppercase()).unwrap_or(false);
let lower = name.to_ascii_lowercase();
let (type_str, is_hub) = if lower.starts_with("hs-") {
("script", true)
} else if lower.starts_with("s-") {
("script", false)
} else if lower.starts_with("f-") {
("flow", false)
} else {
return Err(format!("Invalid tool name prefix: {}", name));
};
Ok((type_str, is_hub, is_hashed))
}
/// Extract the hub version_id from a hashed hub script name like `Hs-{id}-...`
pub fn extract_hub_version_id_from_hashed(name: &str) -> Result<String, String> {
let rest = name
.strip_prefix("Hs-")
.ok_or_else(|| format!("Not a hashed hub name: {}", name))?;
let id = rest
.split('-')
.next()
.ok_or_else(|| format!("No version_id in hashed hub name: {}", name))?;
if id.is_empty() {
return Err(format!("Empty version_id in hashed hub name: {}", name));
}
Ok(id.to_string())
}
/// Extract a safe original-path prefix from a hashed tool name.
///
/// Given `S-u_admin_engineering__te<hash16>`, extracts the escaped prefix between
/// the type prefix (`S-`, `F-`, or `Hs-`) and the hash, un-escapes it, and
/// returns a prefix suitable for `WHERE path LIKE '{prefix}%'`.
///
/// Returns `None` if the name is too short or has an unrecognized prefix.
pub fn extract_path_prefix_from_hashed(name: &str) -> Option<String> {
let prefix_len = if name.starts_with("Hs-") {
3
} else if name.starts_with("S-") || name.starts_with("F-") {
2
} else {
return None;
};
if name.len() <= prefix_len + HASH_LEN {
return None;
}
let escaped_prefix = &name[prefix_len..name.len() - HASH_LEN];
if escaped_prefix.is_empty() {
return None;
}
// Strip trailing underscores — they may be half of a `__` pair split by truncation
let trimmed = escaped_prefix.trim_end_matches('_');
if trimmed.is_empty() {
return None;
}
Some(unescape_path(trimmed))
}
/// Un-escape a mangled path segment: `__` → `_`, standalone `_` → `/`.
fn unescape_path(s: &str) -> String {
const TEMP_PLACEHOLDER: &str = "@@UNDERSCORE@@";
s.replace("__", TEMP_PLACEHOLDER)
.replace('_', "/")
.replace(TEMP_PLACEHOLDER, "_")
}
/// Truncate a string to at most `max_len` bytes, ensuring we don't split a UTF-8 character.
fn truncate_to_char_boundary(s: &str, max_len: usize) -> &str {
if s.len() <= max_len {
return s;
}
let mut end = max_len;
while end > 0 && !s.is_char_boundary(end) {
end -= 1;
}
&s[..end]
}
/// Reverse the transformation of a path
@@ -38,25 +158,22 @@ pub fn transform_path(path: &str, type_str: &str) -> String {
/// This is used in call_tool to get the original path, and the type of the item.
///
/// Returns: (type, original_path, is_hub)
///
/// Note: This only works for non-hashed (short) names. Hashed names must be
/// resolved via `parse_tool_prefix` + path enumeration in the runner.
pub fn reverse_transform(transformed_path: &str) -> Result<(&str, String, bool), String> {
let is_hub = transformed_path.starts_with("h");
let transformed_path = if is_hub {
transformed_path[1..].to_string()
} else {
transformed_path.to_string()
};
let type_str = if transformed_path.starts_with("s-") {
"script"
} else if transformed_path.starts_with("f-") {
"flow"
} else {
return Err(format!(
"Invalid prefix in transformed path: {}",
transformed_path
));
};
let (type_str, is_hub, is_hashed) = parse_tool_prefix(transformed_path)?;
let mangled_path = &transformed_path[2..];
if is_hashed {
return Err(
"Hashed names cannot be reverse-transformed directly; use path enumeration instead"
.to_string(),
);
}
// Strip the prefix: "hs-" (3 chars) for hub, "s-"/"f-" (2 chars) for others
let prefix_len = if is_hub { 3 } else { 2 };
let mangled_path = &transformed_path[prefix_len..];
let original_path = if is_hub {
let parts = mangled_path.split("-").collect::<Vec<&str>>();
@@ -65,11 +182,7 @@ pub fn reverse_transform(transformed_path: &str) -> Result<(&str, String, bool),
}
parts[0].to_string()
} else {
const TEMP_PLACEHOLDER: &str = "@@UNDERSCORE@@";
mangled_path
.replace("__", TEMP_PLACEHOLDER)
.replace('_', "/")
.replace(TEMP_PLACEHOLDER, "_")
unescape_path(mangled_path)
};
Ok((type_str, original_path, is_hub))
@@ -97,16 +210,13 @@ pub fn reverse_transform_key(transformed_key: &str, schema_obj: &Option<SchemaTy
let schema_obj = match schema_obj {
Some(s) => s,
None => {
// No schema available, return the key as is (best guess)
return transformed_key.to_string();
}
};
for original_key_in_schema in schema_obj.properties.keys() {
// Apply the SAME forward transformation to the schema key
let potential_transformed_key = apply_key_transformation(original_key_in_schema);
// If it matches the key we received, we found the likely original
if potential_transformed_key == transformed_key {
return original_key_in_schema.clone();
}
@@ -120,7 +230,7 @@ mod tests {
use super::*;
#[test]
fn test_transform_path() {
fn test_transform_path_short() {
assert_eq!(
transform_path("u/admin/script", "script"),
"s-u_admin_script"
@@ -130,7 +240,108 @@ mod tests {
}
#[test]
fn test_reverse_transform() {
fn test_transform_path_long_is_hashed() {
let long_path = "u/engineering/team/automation/very_long_script_name_that_exceeds_limit";
let result = transform_path(long_path, "script");
assert_eq!(result.len(), MAX_PATH_LENGTH);
assert!(result.starts_with("S-"));
let (_, _, is_hashed) = parse_tool_prefix(&result).unwrap();
assert!(is_hashed);
}
#[test]
fn test_transform_path_long_flow_is_hashed() {
let long_path = "f/engineering/team/automation/very_long_flow_name_that_exceeds_limit";
let result = transform_path(long_path, "flow");
assert_eq!(result.len(), MAX_PATH_LENGTH);
assert!(result.starts_with("F-"));
let (_, _, is_hashed) = parse_tool_prefix(&result).unwrap();
assert!(is_hashed);
}
#[test]
fn test_transform_path_hashing_is_deterministic() {
let path = "u/engineering/team/automation/very_long_script_name_that_exceeds_limit";
let a = transform_path(path, "script");
let b = transform_path(path, "script");
assert_eq!(a, b);
}
#[test]
fn test_transform_path_different_long_paths_differ() {
let a = transform_path(
"u/engineering/team/automation/very_long_script_name_that_exceeds_limit_a",
"script",
);
let b = transform_path(
"u/engineering/team/automation/very_long_script_name_that_exceeds_limit_b",
"script",
);
assert_ne!(a, b);
}
#[test]
fn test_transform_hub_path_short() {
let result = transform_hub_path(12345, "Send Slack Message");
assert_eq!(result, "hs-12345-Send_Slack_Message");
let (_, _, is_hashed) = parse_tool_prefix(&result).unwrap();
assert!(!is_hashed);
}
#[test]
fn test_transform_hub_path_long_is_hashed() {
let result = transform_hub_path(
12345,
"Send Slack Message To Channel With Very Long Description That Exceeds Limit",
);
assert_eq!(result.len(), MAX_PATH_LENGTH);
assert!(result.starts_with("Hs-12345-"));
let (_, _, is_hashed) = parse_tool_prefix(&result).unwrap();
assert!(is_hashed);
}
#[test]
fn test_extract_hub_version_id_from_hashed() {
let name = "Hs-12345-Send_Slack_Message_To_Ch9e8d7c6b5a4f3e2d";
let id = extract_hub_version_id_from_hashed(name).unwrap();
assert_eq!(id, "12345");
}
#[test]
fn test_parse_tool_prefix() {
let (t, hub, hashed) = parse_tool_prefix("S-something").unwrap();
assert_eq!(t, "script");
assert!(!hub);
assert!(hashed);
let (t, hub, hashed) = parse_tool_prefix("F-something").unwrap();
assert_eq!(t, "flow");
assert!(!hub);
assert!(hashed);
let (t, hub, hashed) = parse_tool_prefix("Hs-12345-something").unwrap();
assert_eq!(t, "script");
assert!(hub);
assert!(hashed);
let (t, hub, hashed) = parse_tool_prefix("s-u_admin_script").unwrap();
assert_eq!(t, "script");
assert!(!hub);
assert!(!hashed);
let (t, hub, hashed) = parse_tool_prefix("f-f_folder_flow").unwrap();
assert_eq!(t, "flow");
assert!(!hub);
assert!(!hashed);
let (t, hub, hashed) = parse_tool_prefix("hs-12345-summary").unwrap();
assert_eq!(t, "script");
assert!(hub);
assert!(!hashed);
}
#[test]
fn test_reverse_transform_short_names() {
let (type_str, path, is_hub) = reverse_transform("s-u_admin_script").unwrap();
assert_eq!(type_str, "script");
assert_eq!(path, "u/admin/script");
@@ -142,6 +353,70 @@ mod tests {
assert!(!is_hub);
}
#[test]
fn test_extract_path_prefix_from_hashed() {
// Generate a real hashed name and verify prefix extraction
let long_path = "u/admin/engineering/team/automation/very_long_script";
let hashed = transform_path(long_path, "script");
let (_, _, is_hashed) = parse_tool_prefix(&hashed).unwrap();
assert!(is_hashed);
let prefix = extract_path_prefix_from_hashed(&hashed).unwrap();
// The original path should start with the extracted prefix
assert!(
long_path.starts_with(&prefix),
"path '{}' should start with prefix '{}'",
long_path,
prefix
);
}
#[test]
fn test_extract_path_prefix_underscore_in_path() {
let long_path = "u/admin/my_team/automation/very_long_script_name_here";
let hashed = transform_path(long_path, "script");
let prefix = extract_path_prefix_from_hashed(&hashed).unwrap();
assert!(
long_path.starts_with(&prefix),
"path '{}' should start with prefix '{}'",
long_path,
prefix
);
}
#[test]
fn test_extract_path_prefix_rejects_invalid_prefix() {
assert!(extract_path_prefix_from_hashed("x-something").is_none());
assert!(extract_path_prefix_from_hashed("").is_none());
assert!(extract_path_prefix_from_hashed("S-").is_none());
}
#[test]
fn test_extract_path_prefix_handles_hs_prefix() {
// Hs- is 3 chars, not 2 — ensure the prefix is stripped correctly
let hashed = transform_hub_path(12345, "a]very long hub script summary that exceeds the limit");
let (_, is_hub, is_hashed) = parse_tool_prefix(&hashed).unwrap();
assert!(is_hub);
assert!(is_hashed);
let prefix = extract_path_prefix_from_hashed(&hashed);
// Should not start with 's' (leftover from Hs- if sliced at index 2)
if let Some(ref p) = prefix {
assert!(
!p.starts_with('s'),
"prefix '{}' should not start with 's' from mis-sliced Hs- prefix",
p
);
}
}
#[test]
fn test_reverse_transform_rejects_hashed_names() {
assert!(reverse_transform("S-something").is_err());
assert!(reverse_transform("F-something").is_err());
assert!(reverse_transform("Hs-12345-something").is_err());
}
#[test]
fn test_apply_key_transformation() {
assert_eq!(apply_key_transformation("my key"), "my_key");
+4 -2
View File
@@ -92,8 +92,10 @@ pub struct ItemSchema {
/// Trait for objects that can be converted to MCP tools
pub trait ToolableItem {
/// Get the path or identifier for this item (transformed for MCP compatibility)
fn get_path_or_id(&self) -> String;
/// Get the MCP-compatible tool name (path transformed with escaping/hashing)
fn get_transformed_path(&self) -> String;
/// Get the original full path of this item (for display in tool title)
fn get_full_path(&self) -> &str;
/// Get the summary/title of this item
fn get_summary(&self) -> &str;
/// Get the description of this item
+3 -3
View File
@@ -14,9 +14,9 @@ pub mod client;
// Re-export common types at crate root for convenience
pub use common::{
convert_schema_to_schema_type, is_resource_allowed, parse_mcp_scopes, transform_path, FlowInfo,
HubResponse, HubScriptInfo, ItemSchema, McpScopeConfig, ResourceInfo, ResourceType, SchemaType,
ScriptInfo, ToolableItem, WorkspaceId,
convert_schema_to_schema_type, is_resource_allowed, parse_mcp_scopes, transform_hub_path,
transform_path, FlowInfo, HubResponse, HubScriptInfo, ItemSchema, McpScopeConfig, ResourceInfo,
ResourceType, SchemaType, ScriptInfo, ToolableItem, WorkspaceId,
};
// Re-export client types at crate root for backward compatibility
+4 -2
View File
@@ -55,20 +55,22 @@ pub trait McpBackend: Send + Sync + Clone + 'static {
// Listing Operations
// ─────────────────────────────────────────────────────────────────
/// List scripts, optionally filtered to favorites only
/// List scripts, optionally filtered to favorites only and/or by path prefix
async fn list_scripts(
&self,
auth: &Self::Auth,
workspace_id: &str,
favorites_only: bool,
path_prefix: Option<&str>,
) -> BackendResult<Vec<ScriptInfo>>;
/// List flows, optionally filtered to favorites only
/// List flows, optionally filtered to favorites only and/or by path prefix
async fn list_flows(
&self,
auth: &Self::Auth,
workspace_id: &str,
favorites_only: bool,
path_prefix: Option<&str>,
) -> BackendResult<Vec<FlowInfo>>;
/// List resource types in workspace
+65 -18
View File
@@ -5,7 +5,10 @@
use crate::common::schema::extract_resource_types_from_schema;
use crate::common::scope::parse_mcp_scopes;
use crate::common::transform::{reverse_transform, reverse_transform_key};
use crate::common::transform::{
extract_hub_version_id_from_hashed, extract_path_prefix_from_hashed, parse_tool_prefix,
reverse_transform, reverse_transform_key,
};
use crate::common::types::{ResourceInfo, ToolableItem, WorkspaceId};
use crate::server::backend::{McpAuth, McpBackend};
use crate::server::endpoints::endpoint_tool_to_mcp_tool;
@@ -81,6 +84,13 @@ impl<B: McpBackend> Runner<B> {
}
}
fn find_matching_path<T: ToolableItem>(candidates: Vec<T>, request_name: &str) -> Option<String> {
candidates
.into_iter()
.find(|item| item.get_transformed_path() == request_name)
.map(|item| item.get_full_path().to_string())
}
impl<B: McpBackend> ServerHandler for Runner<B> {
fn get_info(&self) -> ServerInfo {
ServerInfo {
@@ -120,9 +130,9 @@ impl<B: McpBackend> ServerHandler for Runner<B> {
// Fetch all items concurrently
let (scripts, flows, resource_types, hub_scripts) = tokio::try_join!(
self.backend
.list_scripts(&auth, &workspace_id, favorites_only),
.list_scripts(&auth, &workspace_id, favorites_only, None),
self.backend
.list_flows(&auth, &workspace_id, favorites_only),
.list_flows(&auth, &workspace_id, favorites_only, None),
self.backend.list_resource_types(&auth, &workspace_id),
async {
if let Some(ref apps) = scope_config.hub_apps {
@@ -231,17 +241,6 @@ impl<B: McpBackend> ServerHandler for Runner<B> {
let scope_config =
parse_mcp_scopes(scopes).map_err(|e| ErrorData::internal_error(e, None))?;
// Handle truncated tool names
if request.name.ends_with("_TRUNC") {
return Ok(CallToolResult::error(vec![rmcp::model::Annotated::new(
rmcp::model::RawContent::Text(rmcp::model::RawTextContent {
text: "Tool path is too long. Consider shortening it to make it compatible with MCP.".to_string(),
meta: None,
}),
None,
)]));
}
let args = request.arguments.map(Value::Object).unwrap_or(Value::Null);
// Check if this is an endpoint tool
@@ -274,10 +273,58 @@ impl<B: McpBackend> ServerHandler for Runner<B> {
}
}
// Not an endpoint tool - parse as script/flow
let (tool_type, path, is_hub) = reverse_transform(&request.name).map_err(|e| {
ErrorData::internal_error(format!("Failed to parse tool name: {}", e), None)
})?;
// Resolve the tool name to (type, path, is_hub)
let (type_str, is_hub, is_hashed) =
parse_tool_prefix(&request.name).map_err(|e| {
ErrorData::internal_error(format!("Failed to parse tool name: {}", e), None)
})?;
let (tool_type, path, is_hub) = if !is_hashed {
reverse_transform(&request.name).map_err(|e| {
ErrorData::internal_error(format!("Failed to parse tool name: {}", e), None)
})?
} else if is_hub {
let version_id =
extract_hub_version_id_from_hashed(&request.name).map_err(|e| {
ErrorData::internal_error(
format!("Failed to extract hub version_id: {}", e),
None,
)
})?;
(type_str, version_id, true)
} else {
let path_prefix = extract_path_prefix_from_hashed(&request.name);
let favorites_only = scope_config.favorites;
let matched_path = if type_str == "script" {
find_matching_path(
self.backend
.list_scripts(&auth, &workspace_id, favorites_only, path_prefix.as_deref())
.await
.map_err(|e| ErrorData::internal_error(e.message, None))?,
&request.name,
)
} else {
find_matching_path(
self.backend
.list_flows(&auth, &workspace_id, favorites_only, path_prefix.as_deref())
.await
.map_err(|e| ErrorData::internal_error(e.message, None))?,
&request.name,
)
};
let matched_path = matched_path.ok_or_else(|| {
ErrorData::internal_error(
format!(
"No {} found matching hashed tool name '{}'",
type_str, request.name
),
None,
)
})?;
(type_str, matched_path, false)
};
// Validate script/flow scope
if !is_hub && scope_config.granular {
+29 -9
View File
@@ -9,7 +9,7 @@ use std::collections::HashMap;
use std::sync::Arc;
use crate::common::schema::{convert_schema_to_schema_type, make_schema_compatible};
use crate::common::transform::transform_path;
use crate::common::transform::{transform_hub_path, transform_path};
use crate::common::types::{
FlowInfo, HubScriptInfo, ResourceInfo, ResourceType, SchemaType, ScriptInfo, ToolableItem,
};
@@ -17,10 +17,14 @@ use crate::server::backend::McpBackend;
/// Implementation of ToolableItem for ScriptInfo
impl ToolableItem for ScriptInfo {
fn get_path_or_id(&self) -> String {
fn get_transformed_path(&self) -> String {
transform_path(&self.path, "script")
}
fn get_full_path(&self) -> &str {
&self.path
}
fn get_summary(&self) -> &str {
self.summary.as_deref().unwrap_or("No summary")
}
@@ -48,10 +52,14 @@ impl ToolableItem for ScriptInfo {
/// Implementation of ToolableItem for FlowInfo
impl ToolableItem for FlowInfo {
fn get_path_or_id(&self) -> String {
fn get_transformed_path(&self) -> String {
transform_path(&self.path, "flow")
}
fn get_full_path(&self) -> &str {
&self.path
}
fn get_summary(&self) -> &str {
self.summary.as_deref().unwrap_or("No summary")
}
@@ -79,10 +87,13 @@ impl ToolableItem for FlowInfo {
/// Implementation of ToolableItem for HubScriptInfo
impl ToolableItem for HubScriptInfo {
fn get_path_or_id(&self) -> String {
let id = self.version_id;
fn get_transformed_path(&self) -> String {
let summary = self.summary.as_deref().unwrap_or("No summary");
format!("hs-{}-{}", id, summary.replace(" ", "_"))
transform_hub_path(self.version_id, summary)
}
fn get_full_path(&self) -> &str {
self.summary.as_deref().unwrap_or("No summary")
}
fn get_summary(&self) -> &str {
@@ -124,7 +135,7 @@ pub fn create_tool_from_item<T: ToolableItem, B: McpBackend>(
resources_types: &[ResourceType],
) -> Tool {
let is_hub = item.is_hub();
let path = item.get_path_or_id();
let path = item.get_transformed_path();
let item_type = item.item_type();
let description = format!(
"This is a {} named `{}` with the following description: `{}`.{}",
@@ -170,15 +181,24 @@ pub fn create_tool_from_item<T: ToolableItem, B: McpBackend>(
}
};
let title = {
let summary = item.get_summary();
if summary == "No summary" {
item.get_full_path().to_string()
} else {
summary.to_string()
}
};
Tool {
name: Cow::Owned(path),
description: Some(Cow::Owned(description)),
input_schema: Arc::new(input_schema_map),
title: Some(item.get_summary().to_string()),
title: Some(title.clone()),
output_schema: None,
icons: None,
annotations: Some(ToolAnnotations {
title: Some(item.get_summary().to_string()),
title: Some(title),
read_only_hint: Some(false), // Can modify environment
destructive_hint: Some(true), // Can potentially be destructive
idempotent_hint: Some(false), // Are not guaranteed to be idempotent
+11 -4
View File
@@ -151,6 +151,7 @@ pub struct RunJob {
pub args: serde_json::Map<String, serde_json::Value>,
pub scheduled_for_o: Option<chrono::DateTime<chrono::Utc>>,
pub email: String,
pub job_id: Option<Uuid>,
}
impl From<JobPayload> for RunJob {
@@ -160,6 +161,7 @@ impl From<JobPayload> for RunJob {
args: Default::default(),
scheduled_for_o: None,
email: "test@windmill.dev".to_string(),
job_id: None,
}
}
}
@@ -183,8 +185,13 @@ impl RunJob {
self
}
pub fn job_id(mut self, id: Uuid) -> Self {
self.job_id = Some(id);
self
}
pub async fn push(self, db: &Pool<Postgres>) -> Uuid {
let RunJob { payload, args, scheduled_for_o, email } = self;
let RunJob { payload, args, scheduled_for_o, email, job_id } = self;
let mut hm_args = std::collections::HashMap::new();
for (k, v) in args {
hm_args.insert(k, windmill_common::worker::to_raw_value(&v));
@@ -206,7 +213,7 @@ impl RunJob {
/* parent_job */ None,
/* root job */ None,
/* flow_innermost_root_job */ None,
/* job_id */ None,
/* job_id */ job_id,
/* is_flow_step */ false,
/* same_worker */ false,
None,
@@ -230,7 +237,7 @@ impl RunJob {
/// Push the job as a specific user (for testing permissions)
pub async fn push_as(self, db: &Pool<Postgres>, username: &str, email: &str) -> Uuid {
let RunJob { payload, args, scheduled_for_o, .. } = self;
let RunJob { payload, args, scheduled_for_o, job_id, .. } = self;
let mut hm_args = std::collections::HashMap::new();
for (k, v) in args {
hm_args.insert(k, windmill_common::worker::to_raw_value(&v));
@@ -252,7 +259,7 @@ impl RunJob {
/* parent_job */ None,
/* root job */ None,
/* flow_innermost_root_job */ None,
/* job_id */ None,
/* job_id */ job_id,
/* is_flow_step */ false,
/* same_worker */ false,
None,
@@ -1036,6 +1036,12 @@ pub async fn handle_bun_job(
let _ = write_file(job_dir, "main.ts", inner_content)?;
} else if !annotation.native && codebase.is_none() {
let _ = write_file(job_dir, "package.json", r#"{ "type": "module" }"#)?;
} else if codebase.is_some() {
// Write a valid fallback package.json for codebase mode. Without this,
// nsjail creates an empty 0-byte file (from the mandatory: false mount)
// which Node.js fails to parse as JSON (ERR_INVALID_PACKAGE_CONFIG).
// If the codebase TAR includes a package.json, it will overwrite this.
let _ = write_file(job_dir, "package.json", "{}")?;
};
let common_bun_proc_envs: HashMap<String, String> =
+1
View File
@@ -4771,6 +4771,7 @@ mount {{
// Clean up absolute-path symlinks created by setup_volume_mount_paths
if !is_sandboxing_enabled() {
#[allow(unused_variables)] // state is only used on unix
for state in &volume_setup.states {
#[cfg(unix)]
if state.mount.target.starts_with('/') {
+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.650.0";
export const VERSION = "v1.651.1";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({
+4 -1
View File
@@ -39,7 +39,10 @@ export const scriptBootstrapCode = {
}
`,
bun: `export async function main() {
bun: `// there are multiple modes to add as header: //nobundling //native //npm //nodejs
// https://www.windmill.dev/docs/getting_started/scripts_quickstart/typescript#modes
export async function main() {
return "Hello world";
}
`,
+33 -10
View File
@@ -2626,6 +2626,7 @@ export async function push(
let [_basePath, changes] = queue.shift()!;
const promise = (async () => {
const alreadySynced: string[] = [];
const deletedVarsResPaths: string[] = [];
const isRawApp = isRawAppFile(changes[0].path);
if (isRawApp) {
const deleteRawApp = changes.find(
@@ -2870,12 +2871,23 @@ export async function push(
name: change.path.split(SEP)[1],
});
break;
case "resource":
await wmill.deleteResource({
workspace: workspaceId,
path: removeSuffix(target, ".resource.json"),
});
case "resource": {
const resourcePath = removeSuffix(target, ".resource.json");
try {
await wmill.deleteResource({
workspace: workspaceId,
path: resourcePath,
});
} catch (e: any) {
if (e?.status === 404 && deletedVarsResPaths.includes(resourcePath)) {
log.debug(`Resource ${resourcePath} already deleted by linked variable`);
} else {
throw e;
}
}
deletedVarsResPaths.push(resourcePath);
break;
}
case "resource-type":
await wmill.deleteResourceType({
workspace: workspaceId,
@@ -3012,12 +3024,23 @@ export async function push(
});
break;
}
case "variable":
await wmill.deleteVariable({
workspace: workspaceId,
path: removeSuffix(target, ".variable.json"),
});
case "variable": {
const variablePath = removeSuffix(target, ".variable.json");
try {
await wmill.deleteVariable({
workspace: workspaceId,
path: variablePath,
});
} catch (e: any) {
if (e?.status === 404 && deletedVarsResPaths.includes(variablePath)) {
log.debug(`Variable ${variablePath} already deleted by linked resource`);
} else {
throw e;
}
}
deletedVarsResPaths.push(variablePath);
break;
}
case "user": {
const users = await wmill.listUsers({
workspace: workspaceId,
+2 -1
View File
@@ -408,7 +408,8 @@ async function remove(_opts: GlobalOptions, name: string) {
async function whoami(_opts: GlobalOptions) {
await requireLogin(_opts);
log.info(await wmill.globalWhoami());
const whoamiInfo = await wmill.globalWhoami();
log.info(JSON.stringify(whoamiInfo, null, 2));
const activeName = await getActiveWorkspaceName(_opts);
log.info("Active: " + colors.green.bold(activeName || "none"));
}
+1 -1
View File
@@ -67,7 +67,7 @@ export {
workspaceAdd,
};
export const VERSION = "1.650.0";
export const VERSION = "1.651.1";
// Re-exported from constants.ts to maintain backwards compatibility
export { WM_FORK_PREFIX } from "./core/constants.ts";
+39
View File
@@ -0,0 +1,39 @@
# Svelte 5 Migration - Bug Report
# Testing started: 2026-03-02
## Warnings (not blocking but worth fixing)
1. [WARNING] binding_property_non_reactive in Grid.svelte:372:5
- `bind:this={moveResizes[item.id]}` is binding to a non-reactive property
- File: src/lib/components/apps/svelte-grid/Grid.svelte
- Appears multiple times in App editor
- Status: NOT FIXED (non-blocking warning)
2. [WARNING] legacy_recursive_reactive_block in RecomputeAllComponents.svelte
- Migrated `$:` reactive block that both accesses and updates the same reactive value
- File: src/lib/components/apps/editor/RecomputeAllComponents.svelte
- May cause recursive updates when converted to $effect
- Status: NOT FIXED (non-blocking warning)
3. [WARNING] ownership_invalid_mutation in SchemaForm.svelte:70:16
- Mutating unbound props (`schema`) is strongly discouraged
- Parent: src/lib/components/ApiConnectForm.svelte should use `bind:schema={...}`
- Appears when opening PostgreSQL resource creation form
- Status: NOT FIXED (non-blocking warning)
4. [WARNING] ownership_invalid_binding in InputTransformSchemaForm.svelte
- Passes `schema` to InputTransformForm.svelte with `bind:`, but parent Pane.svelte didn't declare `schema` as binding
- Appears in flow editor when adding a TypeScript step
- Status: NOT FIXED (non-blocking warning)
## Bugs
1. [BUG] state_descriptors_fixed in Chart.svelte (Queue metrics drawer)
- Error: "Property descriptors defined on `$state` objects must contain `value` and always be `enumerable`, `configurable` and `writable`."
- Triggered by: Clicking "Queue metrics" on /workers page
- File: src/lib/components/chartjs-wrappers/Chart.svelte
- Root cause: Chart.js's `listenArrayEvents` calls Object.defineProperty on data arrays that are Svelte 5 $state proxies, which reject non-standard property descriptors
- Fix: Use $state.snapshot() to pass plain copies of data and options to Chart.js
- Status: FIXED
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.650.0",
"version": "1.651.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.650.0",
"version": "1.651.1",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.650.0",
"version": "1.651.1",
"scripts": {
"dev": "vite dev",
"build": "vite build",
@@ -1,4 +1,6 @@
<script lang="ts">
import { run } from 'svelte/legacy'
import { createEventDispatcher } from 'svelte'
import { Button, Drawer } from './common'
import DrawerContent from './common/drawer/DrawerContent.svelte'
@@ -6,24 +8,26 @@
import AppConnectInner from './AppConnectInner.svelte'
import DarkModeObserver from './DarkModeObserver.svelte'
export let expressOAuthSetup = false
let drawer: Drawer
let resourceType = ''
let step = 1
let disabled = false
let isGoogleSignin = false
let manual = true
let appConnectInner: AppConnectInner | undefined = undefined
let rtToLoad: string | undefined = ''
export async function open(rt?: string) {
rtToLoad = rt
drawer.openDrawer?.()
interface Props {
expressOAuthSetup?: boolean
}
$: appConnectInner && onRtToLoadChange(rtToLoad)
let { expressOAuthSetup = false }: Props = $props()
let drawer: Drawer | undefined = $state()
let resourceType = $state('')
let step = $state(1)
let disabled = $state(false)
let isGoogleSignin = $state(false)
let manual = $state(true)
let appConnectInner: AppConnectInner | undefined = $state(undefined)
let rtToLoad: string | undefined = $state('')
export async function open(rt?: string) {
rtToLoad = rt
drawer?.openDrawer?.()
}
function onRtToLoadChange(rtToLoad: string | undefined) {
appConnectInner?.open(rtToLoad)
@@ -31,7 +35,10 @@
const dispatch = createEventDispatcher()
let darkMode: boolean = false
let darkMode: boolean = $state(false)
run(() => {
appConnectInner && onRtToLoadChange(rtToLoad)
})
</script>
<DarkModeObserver bind:darkMode />
@@ -47,7 +54,7 @@
<DrawerContent
title="Add a resource"
id="add-resource-drawer"
on:close={drawer.closeDrawer}
on:close={drawer?.closeDrawer}
tooltip="Resources represent connections to third party systems. Learn more on how to integrate external APIs."
documentationLink="https://www.windmill.dev/docs/integrations/integrations_on_windmill"
>
@@ -68,7 +75,7 @@
<Button variant="default" on:click={appConnectInner?.back ?? (() => {})}>Back</Button>
{/if}
{#if isGoogleSignin}
<button {disabled} on:click={appConnectInner?.next}>
<button {disabled} onclick={appConnectInner?.next}>
<img
class="h-10 w-auto object-contain"
src={darkMode ? '/google_signin_dark.png' : '/google_signin_light.png'}
@@ -405,6 +405,7 @@
}
} else {
if (!path) {
if (step == 2) return
throw Error('Path is not set')
}
let exists = await VariableService.existsVariable({
@@ -3,7 +3,7 @@
import DarkModeObserver from '$lib/components/DarkModeObserver.svelte'
import { Button } from '$lib/components/common'
import { workspaceStore } from '$lib/stores'
import { onMount } from 'svelte'
import { onMount, untrack } from 'svelte'
interface Props {
resourceType?: string | undefined
@@ -21,8 +21,8 @@
let darkMode: boolean = $state(false)
if (workspace) {
$workspaceStore = workspace
if (untrack(() => workspace)) {
$workspaceStore = untrack(() => workspace)
}
onMount(async () => {
@@ -1,10 +1,11 @@
<script lang="ts">
import { untrack } from 'svelte'
import AppEditor from './apps/editor/AppEditor.svelte'
import type { AppEditorProps } from './apps/types'
let { app: oldApp, ...props }: AppEditorProps = $props()
let app = $state(oldApp)
let app = $state(untrack(() => oldApp))
</script>
<AppEditor {app} {...props} />
@@ -1,16 +1,20 @@
<script lang="ts">
import { run } from 'svelte/legacy';
import IconedResourceType from './IconedResourceType.svelte'
import TextInput from './text_input/TextInput.svelte'
import Toggle from './Toggle.svelte'
import SettingCard from './instanceSettings/SettingCard.svelte'
export let value: any
interface Props {
value: any;
}
$: enabled = value != undefined
let { value = $bindable() }: Props = $props();
let org = ''
$: changeOrg(org)
let org = $state('')
function changeOrg(org) {
if (value) {
@@ -30,10 +34,14 @@
}
}
}
let enabled = $derived(value != undefined)
run(() => {
changeOrg(org)
});
</script>
<div class="flex flex-col gap-1">
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="text-xs font-semibold text-emphasis flex gap-4 items-center"
><div class="w-[120px]"><IconedResourceType name={'authelia'} after={true} /></div><Toggle
checked={enabled}
@@ -1,16 +1,18 @@
<script lang="ts">
import { run } from 'svelte/legacy';
import IconedResourceType from './IconedResourceType.svelte'
import Toggle from './Toggle.svelte'
import SettingCard from './instanceSettings/SettingCard.svelte'
export let value: any
interface Props {
value: any;
}
let { value = $bindable() }: Props = $props();
$: enabled = value != undefined
// Initialize org from existing auth_url
$: org = value?.connect_config?.auth_url?.replace('/application/o/authorize/', '') ?? ''
$: changeOrg(org)
function changeOrg(org) {
if (value && org) {
@@ -30,10 +32,16 @@
}
}
}
let enabled = $derived(value != undefined)
// Initialize org from existing auth_url
let org = $derived(value?.connect_config?.auth_url?.replace('/application/o/authorize/', '') ?? '')
run(() => {
changeOrg(org)
});
</script>
<div class="flex flex-col gap-1">
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="text-xs font-semibold text-emphasis flex gap-4 items-center"
><div class="w-[120px]"><IconedResourceType name={'authentik'} after={true} /></div><Toggle
checked={enabled}
@@ -1,38 +1,48 @@
<script lang="ts">
import { run } from 'svelte/legacy';
import { ExternalLink } from 'lucide-svelte'
import OauthScopes from './OauthScopes.svelte'
export let connect_config: {
interface Props {
connect_config?: {
scopes: string[]
auth_url: string
token_url: string
req_body_auth: boolean
extra_params: { tenant_id: string }
extra_params_callback: Record<string, any>
} = {
};
}
let { connect_config = $bindable({
scopes: ['offline_access'],
auth_url: '',
token_url: '',
req_body_auth: true,
extra_params: { tenant_id: '' },
extra_params_callback: {}
}
}) }: Props = $props();
$: if (!connect_config) {
connect_config = {
scopes: ['offline_access'],
auth_url: '',
token_url: '',
req_body_auth: true,
extra_params: { tenant_id: '' },
extra_params_callback: {}
run(() => {
if (!connect_config) {
connect_config = {
scopes: ['offline_access'],
auth_url: '',
token_url: '',
req_body_auth: true,
extra_params: { tenant_id: '' },
extra_params_callback: {}
}
}
}
});
$: if (connect_config.extra_params.tenant_id) {
connect_config.auth_url = `https://login.microsoftonline.com/${connect_config.extra_params.tenant_id}/oauth2/v2.0/authorize`
connect_config.token_url = `https://login.microsoftonline.com/${connect_config.extra_params.tenant_id}/oauth2/v2.0/token`
}
run(() => {
if (connect_config.extra_params.tenant_id) {
connect_config.auth_url = `https://login.microsoftonline.com/${connect_config.extra_params.tenant_id}/oauth2/v2.0/authorize`
connect_config.token_url = `https://login.microsoftonline.com/${connect_config.extra_params.tenant_id}/oauth2/v2.0/token`
}
});
</script>
<label class="flex flex-col gap-1" for="tenant-id">
+14 -4
View File
@@ -1,12 +1,22 @@
<script lang="ts">
import Tooltip from './Tooltip.svelte'
export let twBgColor = 'bg-blue-200'
export let twTextColor = 'text-secondary'
export let tooltip: string | undefined = undefined
interface Props {
twBgColor?: string;
twTextColor?: string;
tooltip?: string | undefined;
children?: import('svelte').Snippet;
}
let {
twBgColor = 'bg-blue-200',
twTextColor = 'text-secondary',
tooltip = undefined,
children
}: Props = $props();
</script>
<span class="{twBgColor} {twTextColor} text-2xs rounded px-1 whitespace-nowrap">
<slot />
{@render children?.()}
{#if tooltip && tooltip != ''}
<Tooltip>{tooltip}</Tooltip>
{/if}
@@ -5,12 +5,21 @@
import Alert from './common/alert/Alert.svelte'
import { createEventDispatcher } from 'svelte'
export let email: string
export let username: string
export let isConflict = false
export let noPadding = false
interface Props {
email: string;
username: string;
isConflict?: boolean;
noPadding?: boolean;
}
let loading = false
let {
email,
username = $bindable(),
isConflict = false,
noPadding = false
}: Props = $props();
let loading = $state(false)
let usernameInfo:
| {
@@ -20,7 +29,7 @@
username: string
}[]
}
| undefined = undefined
| undefined = $state(undefined)
function handleKeyUp(event: KeyboardEvent) {
const key = event.key
@@ -83,7 +92,7 @@
<input
type="text"
class="mb-4"
on:keyup={handleKeyUp}
onkeyup={handleKeyUp}
bind:value={username}
disabled={isConflict}
/>
@@ -13,25 +13,25 @@
const dispatch = createEventDispatcher()
let edit: boolean = false
let name: string = ''
let value: string = ''
let edit: boolean = $state(false)
let name: string = $state('')
let value: string = $state('')
export function initNew(): void {
edit = false
name = ''
value = ''
drawer.openDrawer()
drawer?.openDrawer()
}
export function editVariable(editName: string, editValue: string): void {
edit = true
name = editName
value = editValue
drawer.openDrawer()
drawer?.openDrawer()
}
let drawer: Drawer
let drawer: Drawer | undefined = $state()
async function updateVariable(): Promise<void> {
await WorkspaceService.setEnvironmentVariable({
@@ -48,7 +48,7 @@
)
dispatch('update')
drawer.closeDrawer()
drawer?.closeDrawer()
setTimeout(() => {
dispatch('update')
}, 5000)
@@ -58,7 +58,7 @@
<Drawer bind:this={drawer} size="900px">
<DrawerContent
title={edit ? `Update contextual variable ${name}` : 'Create a contextual variable'}
on:close={drawer.closeDrawer}
on:close={drawer?.closeDrawer}
>
<div class="flex flex-col gap-8">
{#if !edit}
+19 -15
View File
@@ -1,28 +1,32 @@
<script lang="ts">
import { run } from 'svelte/legacy';
import OauthExtraParams from './OauthExtraParams.svelte'
import OauthScopes from './OauthScopes.svelte'
import Toggle from './Toggle.svelte'
import Tooltip from './Tooltip.svelte'
export let connect_config = {
let { connect_config = $bindable({
scopes: [],
auth_url: '',
token_url: '',
req_body_auth: false,
extra_params: {},
extra_params_callback: {}
}
}) } = $props();
$: if (!connect_config) {
connect_config = {
scopes: [],
auth_url: '',
token_url: '',
req_body_auth: false,
extra_params: {},
extra_params_callback: {}
run(() => {
if (!connect_config) {
connect_config = {
scopes: [],
auth_url: '',
token_url: '',
req_body_auth: false,
extra_params: {},
extra_params_callback: {}
}
}
}
});
</script>
<div class="flex flex-col gap-6">
@@ -42,12 +46,12 @@
bind:value={connect_config.token_url}
/>
</label>
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="flex flex-col gap-1">
<span class="text-emphasis font-semibold text-xs">Scopes</span>
<OauthScopes bind:scopes={connect_config.scopes} />
</label>
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="flex flex-col gap-1">
<span class="text-emphasis font-semibold text-xs"
>Extra Query Args for Authorize Request&nbsp;<Tooltip
@@ -57,14 +61,14 @@
>
<OauthExtraParams bind:extra_params={connect_config.extra_params} />
</label>
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="flex flex-col gap-1">
<span class="text-emphasis font-semibold text-xs"
>Extra Query Args for Token request <Tooltip>Not needed in most cases</Tooltip></span
>
<OauthExtraParams bind:extra_params={connect_config.extra_params_callback} />
</label>
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="flex flex-col gap-1">
<span class="text-emphasis font-semibold text-xs"
>Payload <Tooltip
@@ -5,6 +5,7 @@
import { createPopperActions, type PopperOptions } from 'svelte-popperjs'
import type { PopoverPlacement } from './Popover.model'
import Portal from '$lib/components/Portal.svelte'
import { untrack } from 'svelte'
interface Props {
placement?: PopoverPlacement
@@ -33,10 +34,10 @@
children,
overlay
}: Props = $props()
const [popperRef, popperContent] = createPopperActions({ placement })
const [popperRef, popperContent] = createPopperActions({ placement: untrack(() => placement) })
const popperOptions: PopperOptions<{}> = {
placement,
placement: untrack(() => placement),
strategy: 'fixed',
modifiers: [
{ name: 'offset', options: { offset: [8, 8] } },
+20 -16
View File
@@ -1,10 +1,12 @@
<script lang="ts">
import { run } from 'svelte/legacy';
import OauthExtraParams from './OauthExtraParams.svelte'
import OauthScopes from './OauthScopes.svelte'
import Toggle from './Toggle.svelte'
import Tooltip from './Tooltip.svelte'
export let login_config = {
let { login_config = $bindable({
scopes: [],
auth_url: '',
token_url: '',
@@ -12,19 +14,21 @@
req_body_auth: false,
extra_params: {},
extra_params_callback: {}
}
}) } = $props();
$: if (!login_config) {
login_config = {
scopes: [],
auth_url: '',
token_url: '',
userinfo_url: '',
req_body_auth: false,
extra_params: {},
extra_params_callback: {}
run(() => {
if (!login_config) {
login_config = {
scopes: [],
auth_url: '',
token_url: '',
userinfo_url: '',
req_body_auth: false,
extra_params: {},
extra_params_callback: {}
}
}
}
});
</script>
<label class="block pb-6">
@@ -51,12 +55,12 @@
bind:value={login_config.userinfo_url}
/>
</label>
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="block pb-6">
<span class="text-primary font-semibold text-xs">Scopes</span>
<OauthScopes bind:scopes={login_config.scopes} />
</label>
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="block pb-6">
<span class="text-primary font-semibold text-xs"
>Extra Query Args for Authorize Request&nbsp;<Tooltip
@@ -66,14 +70,14 @@
>
<OauthExtraParams bind:extra_params={login_config.extra_params} />
</label>
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="block pb-6">
<span class="text-primary font-semibold text-xs"
>Extra Query Args for Token request <Tooltip>Not needed in most cases</Tooltip></span
>
<OauthExtraParams bind:extra_params={login_config.extra_params_callback} />
</label>
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="block pb-6">
<span class="text-primary font-semibold text-xs"
>Payload <Tooltip
@@ -1,4 +1,5 @@
<script lang="ts" module>
import { untrack } from 'svelte'
function validate(values: TableEditorValues, dbSchema?: DBSchema) {
const columnNamesErrs = values.columns.flatMap((column) => {
const isUnique = values.columns.filter((c) => c.name === column.name).length === 1
@@ -92,7 +93,7 @@
computePreview
}: Props = $props()
const columnTypes = DB_TYPES[dbType]
const columnTypes = DB_TYPES[untrack(() => dbType)]
const defaultColumnType = (
{
postgresql: 'BIGSERIAL',
@@ -102,10 +103,10 @@
mysql: 'varchar',
duckdb: 'string'
} satisfies Record<DbType, string>
)[dbType]
)[untrack(() => dbType)]
const values: TableEditorValues = $state(
$state.snapshot(initialValues) ?? {
$state.snapshot(untrack(() => initialValues)) ?? {
name: '',
columns: [],
foreignKeys: []
@@ -122,8 +123,8 @@
...(primaryKey && { primaryKey })
})
}
if (!initialValues) {
addColumn({ name: 'id', primaryKey: features?.primaryKeys })
if (!untrack(() => initialValues)) {
addColumn({ name: 'id', primaryKey: untrack(() => features)?.primaryKeys })
}
const errors: ReturnType<typeof validate> = $derived(validate(values, dbSchema))
@@ -69,6 +69,7 @@
let randomId = 'datetarget-' + Math.random().toString(36).substring(7)
</script>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="flex flex-row gap-1 items-center w-full"
id={randomId}
@@ -119,6 +119,7 @@
let randomId = 'datetarget-' + Math.random().toString(36).substring(7)
</script>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="flex flex-row gap-1 items-center w-full relative"
id={randomId}
@@ -6,16 +6,20 @@
import DrawerContent from './common/drawer/DrawerContent.svelte'
import DefaultScriptsInner from './DefaultScriptsInner.svelte'
let drawer: Drawer
export let placement: 'left' | 'right' = 'left'
interface Props {
placement?: 'left' | 'right'
size?: 'xs3' | 'xs2'
noText?: boolean
}
export let size: 'xs3' | 'xs2' = 'xs2'
export let noText = false
let { placement = 'left', size = 'xs2', noText = false }: Props = $props()
let drawer: Drawer | undefined = $state()
</script>
{#if $userStore?.is_admin || $userStore?.is_super_admin}
<Drawer bind:this={drawer} {placement}>
<DrawerContent title="Edit Default Scripts" on:close={drawer.closeDrawer}>
<DrawerContent title="Edit Default Scripts" on:close={drawer?.closeDrawer}>
<DefaultScriptsInner />
</DrawerContent>
</Drawer>
@@ -6,8 +6,11 @@
import { defaultScriptLanguages } from '$lib/scripts'
import Alert from './common/alert/Alert.svelte'
export let small = false
$: langs = computeLangs($defaultScripts)
interface Props {
small?: boolean;
}
let { small = false }: Props = $props();
function computeLangs(defaultScripts: WorkspaceDefaultScripts | undefined): Script['language'][] {
const allLangs = Object.keys(defaultScriptLanguages) as Script['language'][]
@@ -30,6 +33,7 @@
requestBody: $defaultScripts
})
}
let langs = $derived(computeLangs($defaultScripts))
</script>
<Alert title="Global to workspace" type="info" class="mb-4" size={small ? 'xs' : 'sm'}>
@@ -47,7 +51,7 @@
<div>
{#if i > 0}
<button
on:click={() => changePosition(i ?? 0, true)}
onclick={() => changePosition(i ?? 0, true)}
class={small ? 'mr-2 text-secondary text-sm' : 'text-lg mr-2'}
title="Move up"
>
@@ -56,7 +60,7 @@
{/if}
{#if i < langs.length - 1}
<button
on:click={() => changePosition(i ?? 0, false)}
onclick={() => changePosition(i ?? 0, false)}
class={small ? 'mr-2 text-secondary text-sm' : 'text-lg mr-2'}
title="Move down">&downarrow;</button
>
+11 -3
View File
@@ -2,11 +2,19 @@
import { ExternalLink } from 'lucide-svelte'
import { twMerge } from 'tailwind-merge'
export let link: string | undefined = undefined
interface Props {
link?: string | undefined;
class?: string;
children?: import('svelte').Snippet;
}
let { link = undefined, class: className = '', children }: Props = $props();
</script>
<div class={twMerge('text-xs text-primary font-normal', $$props.class)}>
<slot />
<div class={twMerge('text-xs text-primary font-normal', className)}>
{@render children?.()}
{#if link}
<a href={link} target="_blank" class="whitespace-nowrap"
>Learn more <ExternalLink size={12} class="inline-block" /></a
+10 -9
View File
@@ -22,7 +22,7 @@
import { Pane, Splitpanes } from 'svelte-splitpanes'
import { onDestroy, onMount, setContext, untrack } from 'svelte'
import DarkModeToggle from '$lib/components/sidebar/DarkModeToggle.svelte'
import { page } from '$app/stores'
import { page } from '$app/state'
import { getUserExt } from '$lib/user'
import FlowPreviewButtons from './flows/header/FlowPreviewButtons.svelte'
import FlowModuleSchemaMap from './flows/map/FlowModuleSchemaMap.svelte'
@@ -169,11 +169,12 @@
let loadingCodebaseButton = $state(false)
let lastCommandId = ''
if (initial) {
if (initial.type == 'script') {
replaceScript(initial.script)
} else if (initial.type == 'flow') {
replaceFlow(initial.flow)
const untrackedInitial = untrack(() => initial)
if (untrackedInitial) {
if (untrackedInitial.type == 'script') {
replaceScript(untrackedInitial.script)
} else if (untrackedInitial.type == 'flow') {
replaceFlow(untrackedInitial.flow)
}
modeInitialized = true
}
@@ -596,9 +597,9 @@
}
})
}
let token = $derived($page.url.searchParams.get('wm_token') ?? undefined)
let workspace = $derived($page.url.searchParams.get('workspace') ?? undefined)
let themeDarkRaw = $derived($page.url.searchParams.get('activeColorTheme'))
let token = $derived(page.url.searchParams.get('wm_token') ?? undefined)
let workspace = $derived(page.url.searchParams.get('workspace') ?? undefined)
let themeDarkRaw = $derived(page.url.searchParams.get('activeColorTheme'))
let themeDark = $derived(themeDarkRaw == '2' || themeDarkRaw == '4')
$effect.pre(() => {
@@ -1,4 +1,5 @@
<script lang="ts">
import { untrack } from 'svelte'
import MenuItem from '$lib/components/meltComponents/MenuItem.svelte'
import { melt } from '@melt-ui/svelte'
import { twMerge } from 'tailwind-merge'
@@ -18,7 +19,7 @@
const {
elements: { subTrigger, subMenu },
states: { subOpen }
} = builders.createSubmenu()
} = untrack(() => builders).createSubmenu()
let subItems = $derived((item.submenuItems ?? []).filter((i) => !i.hide))
</script>
@@ -76,7 +76,7 @@
ids: { menu: dropdownId }
} = createDropdownMenu({
positioning: {
placement
placement: untrack(() => placement)
},
loop: true,
onOpenChange: ({ next }) => {
@@ -4,9 +4,13 @@
import { Hourglass } from 'lucide-svelte'
import WaitTimeWarning from './common/waitTimeWarning/WaitTimeWarning.svelte'
export let duration_ms: number
export let self_wait_time_ms: number | undefined = undefined
export let aggregate_wait_time_ms: number | undefined = undefined
interface Props {
duration_ms: number;
self_wait_time_ms?: number | undefined;
aggregate_wait_time_ms?: number | undefined;
}
let { duration_ms, self_wait_time_ms = undefined, aggregate_wait_time_ms = undefined }: Props = $props();
</script>
<div>
@@ -113,10 +113,10 @@
}
})
let lastArgs = $state.snapshot(otherArgs)
let lastArgs = $state.snapshot(untrack(() => otherArgs))
let timeout: number | undefined = $state()
let nargs = $state($state.snapshot(otherArgs))
let nargs = $state($state.snapshot(untrack(() => otherArgs)))
$effect(() => {
otherArgs
untrack(() => clearTimeout(timeout))
@@ -286,7 +286,7 @@
}
}
let jsonView: boolean = $state(customUi?.jsonOnly == true)
let jsonView: boolean = $state(untrack(() => customUi)?.jsonOnly == true)
let schemaString: string = $state(JSON.stringify(schema, null, '\t'))
let error: string | undefined = $state(undefined)
let editor: SimpleEditor | undefined = $state(undefined)
@@ -296,8 +296,8 @@
editor?.setCode(schemaString)
}
const editTabDefaultSize = noPreview ? 100 : 50
editPanelSize = editTab ? (editPanelInitialSize ?? editTabDefaultSize) : 0
const editTabDefaultSize = untrack(() => noPreview) ? 100 : 50
editPanelSize = untrack(() => editTab) ? (untrack(() => editPanelInitialSize) ?? editTabDefaultSize) : 0
let inputPanelSize = $state(100 - editPanelSize)
let editPanelSizeSmooth = tweened(editPanelSize, {
duration: 150
@@ -592,7 +592,7 @@
{argName}
{#if !uiOnly}
<div onclick={stopPropagation(preventDefault(bubble('click')))}>
<Popover placement="bottom-end" containerClasses="p-4" closeButton>
<Popover placement="bottom-end" closeButton>
{#snippet trigger()}
<Button
variant="subtle"
+4 -4
View File
@@ -189,11 +189,11 @@
}
})
let lang = $state(scriptLangToEditorLang(scriptLang))
let lang = $state(scriptLangToEditorLang(untrack(() => scriptLang)))
let filePath = $state(computePath(path))
let filePath = $state(computePath(untrack(() => path)))
let initialPath: string | undefined = $state(path)
let initialPath: string | undefined = $state(untrack(() => path))
let websockets: WebSocket[] = []
let languageClients: MonacoLanguageClient[] = []
@@ -209,7 +209,7 @@
let destroyed = false
const uri = computeUri(
untrack(() => filePath),
scriptLang
untrack(() => scriptLang)
)
console.log('uri', uri)
@@ -1,32 +1,40 @@
<script lang="ts">
import { run } from 'svelte/legacy';
import { type Job } from '$lib/gen'
import { isScriptPreview } from '$lib/utils'
import { onDestroy } from 'svelte'
export let job: Job | undefined = undefined
/** Execution duration of current active job (in ms) */
export let executionDuration: number = 0
/** Is current job running more than specified value in `longDefinition` seconds */
export let longRunning: boolean = false
/** What do we count as "long" (in ms)*/
export let longDefinition: number = 30_000
/** How often component updates execution duration (in ms)
interface Props {
job?: Job | undefined;
/** Execution duration of current active job (in ms) */
executionDuration?: number;
/** Is current job running more than specified value in `longDefinition` seconds */
longRunning?: boolean;
/** What do we count as "long" (in ms)*/
longDefinition?: number;
/** How often component updates execution duration (in ms)
* Higher value -> more efficient component is, less accuracy it has
* Lower value -> less efficient component is, more accuracy it has
*/
export let updateResolution: number = 5_000
updateResolution?: number;
}
let {
job = undefined,
executionDuration = $bindable(0),
longRunning = $bindable(false),
longDefinition = 30_000,
updateResolution = 5_000
}: Props = $props();
let startedAt: number | undefined = undefined
let busy: boolean = false
let busy: boolean = $state(false)
let interval: number | undefined
// Detect when execution of job started
$: if (
!busy &&
job &&
'running' in job &&
(job.job_kind == 'script' || isScriptPreview(job?.job_kind))
)
start(job)
function start(job: Job) {
busy = true
@@ -50,4 +58,14 @@
// Clear the interval when the component is destroyed
clearInterval(interval)
})
// Detect when execution of job started
run(() => {
if (
!busy &&
job &&
'running' in job &&
(job.job_kind == 'script' || isScriptPreview(job?.job_kind))
)
start(job)
});
</script>
@@ -1,6 +1,7 @@
<!-- Used to avoid height jitter when loading monaco asynchronously -->
<script lang="ts">
import { untrack } from 'svelte'
import { getOS } from '$lib/utils'
import { MONACO_Y_PADDING } from './vscode'
@@ -45,7 +46,7 @@
const charWidth = 9 // try to match as closely as possible to monaco editor
const lineHeight = fontSize * GOLDEN_LINE_HEIGHT_RATIO
const lineHeight = untrack(() => fontSize) * GOLDEN_LINE_HEIGHT_RATIO
let [clientWidth, clientHeight] = $state([0, 0])
let showHorizontalScrollbar = $derived(
+36 -16
View File
@@ -5,19 +5,37 @@
import Tooltip from './meltComponents/Tooltip.svelte'
import { InfoIcon } from 'lucide-svelte'
export let label: string
export let format: string = ''
export let contentEncoding = ''
export let type: string | undefined = undefined
export let disabled: boolean = false
export let required = false
export let displayType: boolean = true
export let labelClass: string = ''
export let prettify = false
export let simpleTooltip: string | undefined = undefined
export let lightHeader = false
export let SimpleTooltipIcon = InfoIcon
export let simpleTooltipIconClass = ''
interface Props {
label: string;
format?: string;
contentEncoding?: string;
type?: string | undefined;
disabled?: boolean;
required?: boolean;
displayType?: boolean;
labelClass?: string;
prettify?: boolean;
simpleTooltip?: string | undefined;
lightHeader?: boolean;
SimpleTooltipIcon?: any;
simpleTooltipIconClass?: string;
}
let {
label,
format = '',
contentEncoding = '',
type = undefined,
disabled = false,
required = false,
displayType = true,
labelClass = '',
prettify = false,
simpleTooltip = undefined,
lightHeader = false,
SimpleTooltipIcon = InfoIcon,
simpleTooltipIconClass = ''
}: Props = $props();
</script>
<div class="inline-flex flex-row items-baseline truncated">
@@ -54,9 +72,11 @@
{#if !emptyString(simpleTooltip)}
<Tooltip class="ml-2" placement="bottom">
<SimpleTooltipIcon size="14" class={'-mb-0.5 ' + simpleTooltipIconClass} />
<span class="text-xs" slot="text">
{simpleTooltip}
</span>
{#snippet text()}
<span class="text-xs" >
{simpleTooltip}
</span>
{/snippet}
</Tooltip>
{/if}
</div>
+26 -21
View File
@@ -228,7 +228,9 @@
}
}
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(savedPrimarySchedule) // kept for legacy reasons
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(
untrack(() => savedPrimarySchedule)
) // kept for legacy reasons
const triggersCount = writable<TriggersCount | undefined>(undefined)
const simplifiedPoll = writable(false)
@@ -601,8 +603,8 @@
const selectionManager = new SelectionManager()
const selectedIdStore = $derived(selectionManager.getSelectedId())
// Initialize with selected id if provided
if (selectedId) {
selectionManager.selectId(selectedId)
if (untrack(() => selectedId)) {
selectionManager.selectId(untrack(() => selectedId) ?? '')
} else {
selectionManager.selectId('settings-metadata')
}
@@ -611,11 +613,11 @@
return selectedIdStore
}
const previewArgsStore = $state({ val: initialArgs })
const previewArgsStore = $state({ val: untrack(() => initialArgs) })
const scriptEditorDrawer = writable<ScriptEditorDrawer | undefined>(undefined)
const flowEditorDrawer = writable<FlowEditorDrawer | undefined>(undefined)
const history = initHistory(flowStore.val)
const pathStore = writable<string>(pathStoreInit ?? initialPath)
const history = initHistory(untrack(() => flowStore).val)
const pathStore = writable<string>(untrack(() => pathStoreInit) ?? initialPath)
const captureOn = writable<boolean>(false)
const showCaptureHint = writable<boolean | undefined>(undefined)
const flowInputEditorStateStore = writable<FlowInputEditorState>({
@@ -642,15 +644,15 @@
scriptEditorDrawer,
flowEditorDrawer,
history,
flowStateStore,
flowStore,
flowStateStore: untrack(() => flowStateStore),
flowStore: untrack(() => flowStore),
pathStore,
stepsInputArgs,
saveDraft,
initialPathStore,
fakeInitialPath,
flowInputsStore: writable<FlowInput>({}),
customUi,
customUi: untrack(() => customUi),
insertButtonOpen,
executionCount: writable(0),
flowInputEditorState: flowInputEditorStateStore,
@@ -661,10 +663,13 @@
})
// Set up NoteEditor context for note editing capabilities
const noteEditor = new NoteEditor(flowStore, () => {
// Enable notes display when a note is created
flowEditor?.enableNotes?.()
})
const noteEditor = new NoteEditor(
untrack(() => flowStore),
() => {
// Enable notes display when a note is created
flowEditor?.enableNotes?.()
}
)
setNoteEditorContext(noteEditor)
setContext(
@@ -678,9 +683,9 @@
[
{ type: 'webhook', path: '', isDraft: false },
{ type: 'default_email', path: '', isDraft: false },
...(draftTriggersFromUrl ?? savedFlow?.draft?.draft_triggers ?? [])
...(untrack(() => draftTriggersFromUrl) ?? savedFlow?.draft?.draft_triggers ?? [])
],
selectedTriggerIndexFromUrl,
untrack(() => selectedTriggerIndexFromUrl),
saveSessionDraft
)
)
@@ -804,7 +809,7 @@
onClick: () => void
}> = []
if (customUi.topBar?.extraDeployOptions != false) {
if (untrack(() => customUi).topBar?.extraDeployOptions != false) {
if (savedFlow?.draft_only === false || savedFlow?.draft_only === undefined) {
dropdownItems.push({
label: 'Exit & see details',
@@ -812,14 +817,14 @@
})
}
if (!newFlow) {
if (!untrack(() => newFlow)) {
dropdownItems.push({
label: 'Fork',
onClick: () => window.open(`/flows/add?template=${initialPath}`)
})
}
if (!newFlow && !isCloudHosted() && !isRuleActive('DisableWorkspaceForking')) {
if (!untrack(() => newFlow) && !isCloudHosted() && !isRuleActive('DisableWorkspaceForking')) {
dropdownItems.push({
label: 'Edit in workspace fork',
onClick: () => window.open(buildForkEditUrl('flow', initialPath))
@@ -1036,10 +1041,10 @@
}
let stepHistoryLoader = new StepHistoryLoader(
loadedFromHistoryFromUrl?.stepsState ?? {},
loadedFromHistoryFromUrl?.flowJobInitial,
untrack(() => loadedFromHistoryFromUrl)?.stepsState ?? {},
untrack(() => loadedFromHistoryFromUrl)?.flowJobInitial,
saveSessionDraft,
noInitial
untrack(() => noInitial)
)
setStepHistoryLoaderContext(stepHistoryLoader)
@@ -9,23 +9,38 @@
import { dfs } from './flows/dfs'
import { workspaceStore } from '$lib/stores'
export let flow: {
interface Props {
flow: {
summary: string
description?: string
value: FlowValue
schema?: any
path?: string
};
overflowAuto?: boolean;
noSide?: boolean;
download?: boolean;
noGraph?: boolean;
triggerNode?: boolean;
stepDetail?: FlowModule | string | undefined;
workspace?: string | undefined;
minHeight?: number;
noBorder?: boolean;
}
export let overflowAuto = false
export let noSide = false
export let download = false
export let noGraph = false
export let triggerNode = false
export let stepDetail: FlowModule | string | undefined = undefined
export let workspace: string | undefined = $workspaceStore
export let minHeight = 400
export let noBorder = false
let {
flow,
overflowAuto = false,
noSide = false,
download = false,
noGraph = false,
triggerNode = false,
stepDetail = $bindable(undefined),
workspace = $workspaceStore,
minHeight = 400,
noBorder = false
}: Props = $props();
const dispatch = createEventDispatcher()
</script>
@@ -19,17 +19,20 @@
import HighlightTheme from './HighlightTheme.svelte'
import LanguageIcon from './common/languageIcons/LanguageIcon.svelte'
export let schema: any | undefined = undefined
interface Props {
schema?: any | undefined
stepDetail?: FlowModule | string | undefined
jobScriptHash?: string | undefined
}
export let stepDetail: FlowModule | string | undefined = undefined
export let jobScriptHash: string | undefined = undefined
let codeViewer: Drawer
let { schema = undefined, stepDetail = undefined, jobScriptHash = undefined }: Props = $props()
let codeViewer: Drawer | undefined = $state()
</script>
<HighlightTheme />
<Drawer bind:this={codeViewer} size="900px">
<DrawerContent title={'Expanded Code'} on:close={codeViewer.closeDrawer}>
<DrawerContent title={'Expanded Code'} on:close={codeViewer?.closeDrawer}>
{#if stepDetail && typeof stepDetail != 'string'}
{#if stepDetail.value.type == 'script'}
<div class="mb-4">
@@ -183,7 +186,7 @@
<Button
unifiedSize="sm"
variant="subtle"
onClick={codeViewer.openDrawer}
onClick={codeViewer?.openDrawer}
startIcon={{ icon: Expand }}>Expand</Button
>
</div>
@@ -221,7 +224,7 @@
<Button
unifiedSize="sm"
variant="subtle"
onClick={codeViewer.openDrawer}
onClick={codeViewer?.openDrawer}
startIcon={{ icon: Expand }}>Expand</Button
>
</div>
@@ -3,7 +3,11 @@
import FieldHeader from './FieldHeader.svelte'
export let schema: Schema | { [key: string]: unknown } | undefined
interface Props {
schema: Schema | { [key: string]: unknown } | undefined;
}
let { schema }: Props = $props();
</script>
<ul class="my-2">
@@ -62,7 +62,7 @@
const timelineItems = $derived(timelineCompute?.items ?? undefined)
const timelineNow = $derived(timelineCompute?.now ?? Date.now())
let moduleTracker = new ChangeTracker($state.snapshot(job.raw_flow?.modules ?? []))
let moduleTracker = new ChangeTracker($state.snapshot(untrack(() => job).raw_flow?.modules ?? []))
$effect(() => {
readFieldsRecursively(job.raw_flow?.modules ?? [])
untrack(() => moduleTracker.track($state.snapshot(job.raw_flow?.modules ?? [])))
@@ -123,7 +123,7 @@
}
let timelineAvailableWidths = $state<Record<string, number>>({})
let lastJobId: string | undefined = $state(job.id)
let lastJobId: string | undefined = $state(untrack(() => job).id)
const timelinelWidth = $derived.by(() => {
const widths = Object.values(timelineAvailableWidths)
@@ -4,9 +4,13 @@
import AnimatedButton from './common/button/AnimatedButton.svelte'
import { twMerge } from 'tailwind-merge'
export let connecting: boolean
export let id: undefined | string = undefined
export let wrapperClasses = ''
interface Props {
connecting: boolean;
id?: undefined | string;
wrapperClasses?: string;
}
let { connecting, id = undefined, wrapperClasses = '' }: Props = $props();
</script>
<AnimatedButton animate={connecting} baseRadius="6px" animationDuration="2s" marginWidth="2px">
@@ -62,7 +62,7 @@
showLogsWithResult = false
}: Props = $props()
let lastJobId: string = jobId
let lastJobId: string = untrack(() => jobId)
let retryStatus = $state({ val: {} })
let globalRefreshes: Record<string, ((clear, root) => Promise<void>)[]> = $state({})
@@ -71,11 +71,11 @@
flowState,
suspendStatus,
retryStatus,
hideDownloadInGraph,
hideNodeDefinition,
hideTimeline,
hideJobId,
hideDownloadLogs
hideDownloadInGraph: untrack(() => hideDownloadInGraph),
hideNodeDefinition: untrack(() => hideNodeDefinition),
hideTimeline: untrack(() => hideTimeline),
hideJobId: untrack(() => hideJobId),
hideDownloadLogs: untrack(() => hideDownloadLogs)
})
function loadOwner(path: string) {
@@ -181,7 +181,7 @@
let resultStreams: Record<string, string | undefined> = $state({})
if (onResultStreamUpdate == undefined) {
if (untrack(() => onResultStreamUpdate) == undefined) {
onResultStreamUpdate = ({
jobId,
result_stream
@@ -234,7 +234,7 @@
})
let jobResults: any[] = $state(
flowJobIds?.flowJobs?.map((x, id) => `iter #${id + 1} not loaded by frontend yet`) ?? []
untrack(() => flowJobIds)?.flowJobs?.map((x, id) => `iter #${id + 1} not loaded by frontend yet`) ?? []
)
let retry_selected = $state('')
@@ -805,7 +805,7 @@
let destroyed = false
updateRecursiveRefresh(jobId)
updateRecursiveRefresh(untrack(() => jobId))
async function updateJobId() {
if (jobId !== job?.id || innerModules == undefined) {
@@ -1,4 +1,5 @@
<script lang="ts">
import { untrack } from 'svelte'
import { type FlowValue, FlowService } from '$lib/gen'
import { Tab, Tabs, TabContent } from './common'
import SchemaViewer from './SchemaViewer.svelte'
@@ -51,13 +52,14 @@
}: Props = $props()
let open: { [id: number]: boolean } = {}
if (initialOpen) {
open[initialOpen] = true
const untrackedInitialOpen = untrack(() => initialOpen)
if (untrackedInitialOpen) {
open[untrackedInitialOpen] = true
}
let previousVersionId: number | undefined = $state(undefined)
let previousFlow: PreviousFlow | undefined = $state(undefined)
let tab: TabValue = $state(initTab ?? 'diff')
let tab: TabValue = $state(untrack(() => initTab) ?? 'diff')
let previousFlowCache: Record<number, PreviousFlow> = {}
@@ -1,4 +1,5 @@
<script lang="ts">
import { untrack } from 'svelte'
import AiChatLayout from './copilot/chat/AiChatLayout.svelte'
import type { FlowBuilderProps } from './flow_builder'
import FlowBuilder from './FlowBuilder.svelte'
@@ -11,12 +12,12 @@
...props
}: FlowBuilderProps & { light?: boolean } = $props()
let flowStore = $state(oldFlowStore)
let flowStateStore = $state(oldFlowStateStore)
let flowStore = $state(untrack(() => oldFlowStore))
let flowStateStore = $state(untrack(() => oldFlowStateStore))
let trialRender = $state(true)
if (light) {
if (untrack(() => light)) {
setTimeout(() => {
trialRender = false
}, 1000 * 300)
@@ -270,12 +270,14 @@
{/if}
{#if perms}
<TableCustom>
<!-- @migration-task: migrate this slot by hand, `header-row` is an invalid identifier -->
<tr slot="header-row">
<th>user/group</th>
<th></th>
<th></th>
</tr>
{#snippet headerRow()}
<tr >
<th>user/group</th>
<th></th>
<th></th>
</tr>
{/snippet}
{#snippet body()}
<tbody>
{#each perms ?? [] as { owner_name, role }}<tr>
@@ -407,7 +409,7 @@
<p class="text-primary text-sm">No folder is managing this folder</p>
{:else}
<TableCustom>
<tr slot="header-row">
<tr slot="headerRow">
<th>folder</th>
<th />
</tr>
@@ -1,19 +1,27 @@
<script lang="ts">
import { run } from 'svelte/legacy';
import { FolderService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import Cell from './table/Cell.svelte'
export let name: string
export let tabular = false
export let order = ['scripts', 'flows', 'apps', 'schedules', 'variables', 'resources']
interface Props {
name: string;
tabular?: boolean;
order?: any;
}
$: $workspaceStore && loadUsage()
let { name, tabular = false, order = ['scripts', 'flows', 'apps', 'schedules', 'variables', 'resources'] }: Props = $props();
let usage: Record<string, number> = {}
let usage: Record<string, number> = $state({})
async function loadUsage() {
usage = await FolderService.getFolderUsage({ workspace: $workspaceStore!, name })
}
run(() => {
$workspaceStore && loadUsage()
});
</script>
{#if tabular}
@@ -5,10 +5,17 @@
import { onDestroy, onMount } from 'svelte'
let divEl: HTMLDivElement | null = null
let divEl: HTMLDivElement | null = $state(null)
let editor: meditor.IStandaloneCodeEditor
export let code: string = ''
interface Props {
code?: string;
class?: string;
}
let { code = '', class: className = '' }: Props = $props();
async function loadMonaco() {
editor = meditor.create(divEl as HTMLDivElement, {
@@ -43,4 +50,4 @@
})
</script>
<div bind:this={divEl} class="{$$props.class ?? ''} editor"></div>
<div bind:this={divEl} class="{className} editor"></div>
+14 -10
View File
@@ -159,12 +159,14 @@
{/if}
{#if members}
<TableCustom>
<!-- @migration-task: migrate this slot by hand, `header-row` is an invalid identifier -->
<tr slot="header-row">
<th>user</th>
<th></th>
<th></th>
</tr>
{#snippet headerRow()}
<tr >
<th>user</th>
<th></th>
<th></th>
</tr>
{/snippet}
{#snippet body()}
<tbody>
{#each members ?? [] as { member_name, role }}<tr>
@@ -301,10 +303,12 @@
{#if instance_group?.emails}
<h2 class="mt-6 text-emphasis text-xs font-semibold">Members from the instance group</h2>
<TableCustom>
<!-- @migration-task: migrate this slot by hand, `header-row` is an invalid identifier -->
<tr slot="header-row">
<th>user</th>
</tr>
{#snippet headerRow()}
<tr >
<th>user</th>
</tr>
{/snippet}
{#snippet body()}
<tbody>
{#each instance_group?.emails ?? [] as email}<tr>
@@ -1,4 +1,5 @@
<script lang="ts">
import { untrack } from 'svelte'
import { stopPropagation, createBubbler } from 'svelte/legacy'
const bubble = createBubbler()
@@ -34,7 +35,7 @@
}: Props = $props()
let error = $state('')
const regex = acceptUnderScores ? /^[a-zA-Z][a-zA-Z0-9_]*$/ : /^[a-zA-Z][a-zA-Z0-9]*$/
const regex = untrack(() => acceptUnderScores) ? /^[a-zA-Z][a-zA-Z0-9_]*$/ : /^[a-zA-Z][a-zA-Z0-9]*$/
function validateId(id: string, reservedIds: string[], reservedPrefixes: string[]) {
if (id == initialId) {
@@ -407,6 +407,7 @@
}
function updatePropsBeingEdited(focused: boolean) {
if (!exprBeingEdited) return
let newPropsBeingEdited = [...$exprBeingEdited]
if (focused) {
newPropsBeingEdited.push(argName)
@@ -7,8 +7,12 @@
import Cell from './table/Cell.svelte'
import Row from './table/Row.svelte'
export let inputTransforms: Record<string, InputTransform>
$: entries = Object.entries(inputTransforms)
interface Props {
inputTransforms: Record<string, InputTransform>;
}
let { inputTransforms }: Props = $props();
let entries = $derived(Object.entries(inputTransforms))
</script>
{#if entries.length}
@@ -1,4 +1,6 @@
<script lang="ts">
import { run } from 'svelte/legacy';
import { GroupService, type InstanceGroup } from '$lib/gen'
import { createEventDispatcher } from 'svelte'
import autosize from '$lib/autosize'
@@ -8,17 +10,18 @@
import { sendUserToast } from '$lib/toast'
import { Loader2 } from 'lucide-svelte'
export let name: string
interface Props {
name: string;
}
let email = ''
let instance_group: InstanceGroup | undefined
let members: { member_email: string }[] | undefined = undefined
let { name }: Props = $props();
let email = $state('')
let instance_group: InstanceGroup | undefined = $state()
let members: { member_email: string }[] | undefined = $state(undefined)
const dispatch = createEventDispatcher()
$: {
load()
}
async function load() {
return Promise.all([loadInstanceGroup()])
@@ -32,6 +35,9 @@
})
: []
}
run(() => {
load()
});
</script>
<div class="flex flex-col gap-6">
@@ -85,17 +91,20 @@
</div>
{#if members}
<TableCustom>
<tr slot="header-row">
<th>user</th>
<th></th>
</tr>
<tbody slot="body">
{#each members as { member_email }}<tr>
<td>{member_email}</td>
<td>
<button
class="ml-2 text-red-500"
on:click={async () => {
{#snippet headerRow()}
<tr >
<th>user</th>
<th></th>
</tr>
{/snippet}
{#snippet body()}
<tbody >
{#each members as { member_email }}<tr>
<td>{member_email}</td>
<td>
<button
class="ml-2 text-red-500"
onclick={async () => {
await GroupService.removeUserFromInstanceGroup({
name,
requestBody: { email: member_email }
@@ -104,10 +113,11 @@
sendUserToast('User removed')
loadInstanceGroup()
}}>remove</button
>
</td>
</tr>{/each}
</tbody>
>
</td>
</tr>{/each}
</tbody>
{/snippet}
</TableCustom>
{:else}
<div class="flex flex-col">
@@ -1,4 +1,7 @@
<script lang="ts">
import { stopPropagation, createBubbler } from 'svelte/legacy'
const bubble = createBubbler()
import { Pencil } from 'lucide-svelte'
import { createEventDispatcher } from 'svelte'
import Button from './common/button/Button.svelte'
@@ -9,13 +12,23 @@
import { sendUserToast } from '$lib/toast'
import TextInput from './text_input/TextInput.svelte'
export let value: string | undefined
export let email: string
export let username: string | undefined = undefined
export let automateUsernameCreation: boolean = false
export let login_type: string
interface Props {
value: string | undefined
email: string
username?: string | undefined
automateUsernameCreation?: boolean
login_type: string
}
let password: string = ''
let {
value = $bindable(),
email,
username = undefined,
automateUsernameCreation = false,
login_type = $bindable()
}: Props = $props()
let password: string = $state('')
const dispatch = createEventDispatcher()
@@ -46,12 +59,12 @@
}}
closeButton
>
<svelte:fragment slot="trigger">
{#snippet trigger()}
<Button unifiedSize="sm" nonCaptureEvent={true} variant="subtle" startIcon={{ icon: Pencil }}
>Edit</Button
>
</svelte:fragment>
<svelte:fragment slot="content">
{/snippet}
{#snippet content()}
<div class="flex flex-col gap-8 max-w-sm p-4">
{#if automateUsernameCreation && username}
<ChangeInstanceUsernameInner {email} {username} on:renamed noPadding />
@@ -96,10 +109,11 @@
type="password"
bind:value={password}
class="!w-auto grow"
on:click|stopPropagation={() => {}}
on:keydown|stopPropagation
on:keypress|stopPropagation={({ key }) => {
if (key === 'Enter') {
onclick={stopPropagation(() => {})}
onkeydown={stopPropagation(bubble('keydown'))}
onkeypress={(e) => {
e.stopPropagation()
if (e.key === 'Enter') {
savePassword()
}
}}
@@ -126,10 +140,11 @@
type="text"
bind:value={login_type}
class="!w-auto grow"
on:click|stopPropagation={() => {}}
on:keydown|stopPropagation
on:keypress|stopPropagation={({ key }) => {
if (key === 'Enter') {
onclick={stopPropagation(() => {})}
onkeydown={stopPropagation(bubble('keydown'))}
onkeypress={(e) => {
e.stopPropagation()
if (e.key === 'Enter') {
saveLoginType()
}
}}
@@ -153,5 +168,5 @@
</Button>
</label>
</div>
</svelte:fragment>
{/snippet}
</Popover>
@@ -8,13 +8,17 @@
import { generateRandomString } from '$lib/utils'
import { globalEmailInvite } from '$lib/stores'
export let close: (() => void) | undefined = undefined
interface Props {
close?: (() => void) | undefined;
}
let { close = undefined }: Props = $props();
const dispatch = createEventDispatcher()
let is_super_admin = false
let password: string = generateRandomString(10)
let name: string | undefined
let is_super_admin = $state(false)
let password: string = $state(generateRandomString(10))
let name: string | undefined = $state()
let company: string | undefined
async function addUser() {
+1 -1
View File
@@ -89,7 +89,7 @@
let lastStartedAt: number = Date.now()
let currentId: string | undefined = $state(undefined)
let noPingTimeout: number | undefined = undefined
let lastNoLogs = $state(noLogs)
let lastNoLogs = $state(untrack(() => noLogs))
let lastCompletedJobId = $state<string | undefined>(undefined)
let token = getContext<{ token?: string }>('AuthToken')
@@ -1,24 +1,22 @@
<script lang="ts">
import { run } from 'svelte/legacy';
import IconedResourceType from './IconedResourceType.svelte'
import TextInput from './text_input/TextInput.svelte'
import Toggle from './Toggle.svelte'
import SettingCard from './instanceSettings/SettingCard.svelte'
export let value: any
interface Props {
value: any;
}
let { value = $bindable() }: Props = $props();
const AUTH_URL_SUFFIX = '/ui/oauth2'
$: enabled = value != undefined
// If `baseUrl` is not already set in the form, try to parse it from the `auth_url` value
//
// The binding dance here allows us to avoid rendering the string 'undefined' in the input, and
// also allow lazy/async binding of the `value` prop.
$: derivedBaseUrl = value?.connect_config?.auth_url?.replace(AUTH_URL_SUFFIX, '')
let proxyUrlValue = undefined
$: baseUrl = proxyUrlValue ?? derivedBaseUrl ?? ''
let proxyUrlValue = $state(undefined)
$: changeValues({ baseUrl, id: value?.id ?? '' })
function changeValues({ baseUrl, id }) {
if (value) {
@@ -40,10 +38,20 @@
proxyUrlValue = baseUrl
}
}
let enabled = $derived(value != undefined)
// If `baseUrl` is not already set in the form, try to parse it from the `auth_url` value
//
// The binding dance here allows us to avoid rendering the string 'undefined' in the input, and
// also allow lazy/async binding of the `value` prop.
let derivedBaseUrl = $derived(value?.connect_config?.auth_url?.replace(AUTH_URL_SUFFIX, ''))
let baseUrl = $derived(proxyUrlValue ?? derivedBaseUrl ?? '')
run(() => {
changeValues({ baseUrl, id: value?.id ?? '' })
});
</script>
<div class="flex flex-col gap-1">
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y_label_has_associated_control -->
<label class="text-xs font-semibold text-emphasis flex gap-4 items-center"
><div class="w-[120px]"><IconedResourceType name={'kanidm'} after={true} /></div><Toggle
checked={enabled}
@@ -63,7 +63,10 @@
<span class="text-secondary font-normal text-xs"
>{'REALM_URL/protocol/openid-connect/auth'}</span
>
<TextInput inputProps={{ type: 'text', placeholder: 'yourorg' }} bind:value={value['org']} />
<TextInput
inputProps={{ type: 'text', placeholder: 'yourorg' }}
bind:value={value['org']}
/>
</label>
<label class="flex flex-col gap-1">
<span class="text-emphasis font-semibold text-xs">Custom Name</span>
+10 -2
View File
@@ -1,5 +1,13 @@
<script lang="ts">
export let id: string
import { run } from 'svelte/legacy';
$: id && console.log('updateJobId')
interface Props {
id: string;
}
let { id }: Props = $props();
run(() => {
id && console.log('updateJobId')
});
</script>
@@ -1,8 +1,14 @@
<script lang="ts">
import { untrack } from 'svelte'
import { AnsiUp } from 'ansi_up'
export let content: string
export let highlighted: any[]
interface Props {
content: string
highlighted: any[]
onClick?: () => void
}
let { content, highlighted, onClick }: Props = $props()
const ansi_up = new AnsiUp()
ansi_up.use_classes = true
@@ -27,10 +33,10 @@
return html2
}
let html = highlightSnippet(content)
let html = highlightSnippet(untrack(() => content))
</script>
<button on:click class="font-light !m-0 !p-0">
<button onclick={onClick} class="font-light !m-0 !p-0">
<pre
class="bg-surface-secondary hover:bg-surface px-2 py-1 text-secondary text-xs w-[100%] whitespace-pre border min-w-full text-start">
{@html html}
+2 -1
View File
@@ -1,4 +1,5 @@
<script lang="ts" module>
import { untrack } from 'svelte'
const s3LogPrefixes = [
'[windmill] Previous logs have been saved to object storage at logs/',
'[windmill] Previous logs have been saved to disk at logs/',
@@ -73,7 +74,7 @@
let LOG_INC = 10000
let LOG_LIMIT = $state(LOG_INC)
let lastJobId = $state(jobId)
let lastJobId = $state(untrack(() => jobId))
let loadedFromObjectStore = $state('')
@@ -5,6 +5,7 @@
import { fade } from 'svelte/transition'
import { twMerge } from 'tailwind-merge'
import { untrack } from 'svelte'
export async function refresh() {
await getInstance()?.update()
@@ -26,7 +27,9 @@
content
}: Props = $props()
const [popperRef, popperContent, getInstance] = createPopperActions({ placement })
const [popperRef, popperContent, getInstance] = createPopperActions({
placement: untrack(() => placement)
})
export function open() {
showTooltip = true
+44 -31
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import { run } from 'svelte/legacy';
import { Map, View, Feature } from 'ol'
import { Fill, Stroke, Style, Text } from 'ol/style.js'
import { useGeographic } from 'ol/proj.js'
@@ -18,17 +20,26 @@
strokeColor?: string
}
export let lon: number | undefined = undefined
export let lat: number | undefined = undefined
export let zoom: number | undefined = undefined
export let markers: Marker[] | string | undefined = undefined
interface Props {
lon?: number | undefined;
lat?: number | undefined;
zoom?: number | undefined;
markers?: Marker[] | string | undefined;
}
let {
lon = undefined,
lat = undefined,
zoom = undefined,
markers = undefined
}: Props = $props();
const LAYER_NAME = {
MARKER: 'Marker'
} as const
let map: Map | undefined = undefined
let mapElement: HTMLDivElement | undefined = undefined
let map: Map | undefined = $state(undefined)
let mapElement: HTMLDivElement | undefined = $state(undefined)
function getLayersByName(name: keyof typeof LAYER_NAME) {
return map
@@ -100,36 +111,38 @@
createMarkerLayers()?.forEach((l) => map?.addLayer(l))
}
$: if (!map && mapElement) {
useGeographic()
map = new Map({
target: mapElement,
layers: [
new TileLayer({
source: new OSM()
run(() => {
if (!map && mapElement) {
useGeographic()
map = new Map({
target: mapElement,
layers: [
new TileLayer({
source: new OSM()
}),
...(createMarkerLayers() || [])
],
view: new View({
center: [lon ?? 0, lat ?? 0],
zoom: zoom ?? 2
}),
...(createMarkerLayers() || [])
],
view: new View({
center: [lon ?? 0, lat ?? 0],
zoom: zoom ?? 2
}),
controls: defaultControls({
attribution: false
controls: defaultControls({
attribution: false
})
})
})
if (lat && lon) {
map.getView().setCenter([lon, lat])
}
if (lat && lon) {
map.getView().setCenter([lon, lat])
}
if (map && zoom) {
map.getView().setZoom(zoom)
}
if (map && zoom) {
map.getView().setZoom(zoom)
}
if (map && markers) {
updateMarkers()
if (map && markers) {
updateMarkers()
}
}
}
});
</script>
<div bind:this={mapElement} class="w-full h-[300px]"></div>
@@ -1,4 +1,6 @@
<script lang="ts">
import { run } from 'svelte/legacy';
import { type MetricDataPoint, MetricsService } from '$lib/gen'
import { displayTime } from '$lib/utils'
import { workspaceStore } from '$lib/stores'
@@ -17,16 +19,20 @@
ChartJS.register(Title, Tooltip, Legend, LineElement, LinearScale, PointElement, CategoryScale)
export let jobId: string
export let jobUpdateLastFetch: Date | undefined
interface Props {
jobId: string;
jobUpdateLastFetch: Date | undefined;
}
let { jobId, jobUpdateLastFetch }: Props = $props();
let jobMetricsLastFetch: Date | undefined = undefined
let jobMemoryStats: MetricDataPoint[] | undefined = undefined
let jobMemoryStats: MetricDataPoint[] | undefined = $state(undefined)
let data: {
x: number
y: number
}[] = []
}[] = $state([])
let labels: string[] = []
async function loadMetricsData() {
@@ -66,7 +72,9 @@
data = [...data]
}
$: jobUpdateLastFetch && loadMetricsData()
run(() => {
jobUpdateLastFetch && loadMetricsData()
});
</script>
<div class="relative max-h-100">
@@ -5,9 +5,13 @@
import { displayDate } from '$lib/utils'
import { Hourglass } from 'lucide-svelte'
export let type: FlowStatusModule['type']
export let scheduled_for: Date | undefined
export let skipped: boolean = false
interface Props {
type: FlowStatusModule['type'];
scheduled_for: Date | undefined;
skipped?: boolean;
}
let { type, scheduled_for, skipped = false }: Props = $props();
</script>
{#if type == 'WaitingForEvents'}
@@ -8,7 +8,7 @@
} from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { getScriptByPath } from '$lib/scripts'
import { getContext } from 'svelte'
import { getContext, untrack } from 'svelte'
import type { FlowEditorContext } from './flows/types'
import JobLoader, { type Callbacks } from './JobLoader.svelte'
import { getStepHistoryLoaderContext } from './stepHistoryLoader.svelte'
@@ -167,8 +167,8 @@
testJob = modulesTestStates.states?.[mod.id]?.testJob
})
modulesTestStates.states[mod.id] = {
...(modulesTestStates.states?.[mod.id] ?? { loading: false }),
modulesTestStates.states[untrack(() => mod).id] = {
...(modulesTestStates.states?.[untrack(() => mod).id] ?? { loading: false }),
loading: testIsLoading,
testJob: testJob
}
@@ -2,9 +2,13 @@
import { Button } from './common'
import { X, Plus } from 'lucide-svelte'
export let extra_params: Record<string, string> = {}
interface Props {
extra_params?: Record<string, string>;
}
let extra_params_vec: [string, string][] = Object.entries(extra_params)
let { extra_params = $bindable({}) }: Props = $props();
let extra_params_vec: [string, string][] = $state(Object.entries(extra_params))
function sync() {
extra_params = Object.fromEntries(extra_params_vec)
@@ -13,8 +17,8 @@
{#each extra_params_vec as o}
<div class="flex flex-row max-w-md mb-2 gap-2">
<input type="text" on:keyup={sync} bind:value={o[0]} />
<input type="text" on:keyup={sync} bind:value={o[1]} />
<input type="text" onkeyup={sync} bind:value={o[0]} />
<input type="text" onkeyup={sync} bind:value={o[1]} />
<Button
variant="subtle"
destructive
@@ -2,13 +2,17 @@
import { Button } from './common'
import { Minus, Plus } from 'lucide-svelte'
export let scopes: string[] = []
interface Props {
scopes?: string[]
}
let { scopes = $bindable([]) }: Props = $props()
</script>
{#if scopes && Array.isArray(scopes)}
{#each scopes as v}
{#each scopes as v, i}
<div class="flex flex-row max-w-md mb-2">
<input type="text" bind:value={v} />
<input type="text" bind:value={scopes[i]} />
<Button
variant="default"
size="xs"
@@ -119,61 +119,65 @@
</script>
<MeltPopover placement="bottom" on:openChange={(e) => e.detail && loadUsers()}>
<svelte:fragment slot="trigger">
<span class="inline-flex items-center gap-1">
<UserCog class="w-4 h-4 {selected ? 'text-green-500' : 'text-yellow-500'}" />
{#if selectedDisplayName}
<span class="text-xs truncate max-w-24">{selectedDisplayName}</span>
{#snippet trigger()}
<span class="inline-flex items-center gap-1">
<UserCog class="w-4 h-4 {selected ? 'text-green-500' : 'text-yellow-500'}" />
{#if selectedDisplayName}
<span class="text-xs truncate max-w-24">{selectedDisplayName}</span>
{/if}
</span>
{/snippet}
{#snippet content({ close: closePopover })}
<div class="p-3 flex flex-col gap-2 min-w-48">
<div class="text-xs font-medium text-secondary mb-1">{label}</div>
<!-- Target option -->
{#if targetEmail}
<button
class="flex items-center gap-2 px-2 py-1.5 rounded text-left text-xs hover:bg-surface-hover {!canPreserve
? 'opacity-50 cursor-not-allowed'
: ''}"
disabled={!canPreserve}
onclick={() => onSelect('target')}
>
<Check class="w-3 h-3 {selected === 'target' ? 'opacity-100' : 'opacity-0'}" />
<span class="truncate max-w-40">{targetUsername}</span>
<span class="text-xs text-tertiary">{isDeployment ? '(target)' : '(current)'}</span>
</button>
{/if}
</span>
</svelte:fragment>
<div slot="content" let:close={closePopover} class="p-3 flex flex-col gap-2 min-w-48">
<div class="text-xs font-medium text-secondary mb-1">{label}</div>
<!-- Target option -->
{#if targetEmail}
<!-- Me option -->
<button
class="flex items-center gap-2 px-2 py-1.5 rounded text-left text-xs hover:bg-surface-hover"
onclick={() => onSelect('me')}
>
<Check class="w-3 h-3 {selected === 'me' ? 'opacity-100' : 'opacity-0'}" />
<span class="truncate max-w-40">{$userStore?.username}</span>
<span class="text-xs text-tertiary">(me)</span>
</button>
<!-- Custom / Pick from workspace -->
<button
class="flex items-center gap-2 px-2 py-1.5 rounded text-left text-xs hover:bg-surface-hover {!canPreserve
? 'opacity-50 cursor-not-allowed'
: ''}"
disabled={!canPreserve}
onclick={() => onSelect('target')}
onclick={() => {
closePopover()
openModal()
}}
>
<Check class="w-3 h-3 {selected === 'target' ? 'opacity-100' : 'opacity-0'}" />
<span class="truncate max-w-40">{targetUsername}</span>
<span class="text-xs text-tertiary">{isDeployment ? '(target)' : '(current)'}</span>
{#if selected === 'custom' && customUsername}
<Check class="w-3 h-3 opacity-100" />
<span class="truncate max-w-40">{customUsername}</span>
<span class="text-xs text-tertiary">(custom)</span>
{:else}
<Check class="w-3 h-3 opacity-0" />
<Users class="w-3 h-3 text-tertiary" />
<span>Pick from workspace&hellip;</span>
{/if}
</button>
{/if}
<!-- Me option -->
<button
class="flex items-center gap-2 px-2 py-1.5 rounded text-left text-xs hover:bg-surface-hover"
onclick={() => onSelect('me')}
>
<Check class="w-3 h-3 {selected === 'me' ? 'opacity-100' : 'opacity-0'}" />
<span class="truncate max-w-40">{$userStore?.username}</span>
<span class="text-xs text-tertiary">(me)</span>
</button>
<!-- Custom / Pick from workspace -->
<button
class="flex items-center gap-2 px-2 py-1.5 rounded text-left text-xs hover:bg-surface-hover {!canPreserve
? 'opacity-50 cursor-not-allowed'
: ''}"
disabled={!canPreserve}
onclick={() => {
closePopover()
openModal()
}}
>
{#if selected === 'custom' && customUsername}
<Check class="w-3 h-3 opacity-100" />
<span class="truncate max-w-40">{customUsername}</span>
<span class="text-xs text-tertiary">(custom)</span>
{:else}
<Check class="w-3 h-3 opacity-0" />
<Users class="w-3 h-3 text-tertiary" />
<span>Pick from workspace&hellip;</span>
{/if}
</button>
</div>
</div>
{/snippet}
</MeltPopover>
<!-- User selection modal -->
+19 -7
View File
@@ -1,11 +1,23 @@
<script lang="ts">
import Tooltip from './Tooltip.svelte'
export let title: string
export let tooltip: string = ''
export let documentationLink: string | undefined = undefined
export let primary: boolean = true
export let childrenWrapperDivClasses: string = ''
interface Props {
title: string;
tooltip?: string;
documentationLink?: string | undefined;
primary?: boolean;
childrenWrapperDivClasses?: string;
children?: import('svelte').Snippet;
}
let {
title,
tooltip = '',
documentationLink = undefined,
primary = true,
childrenWrapperDivClasses = '',
children
}: Props = $props();
</script>
<div class="flex flex-row flex-wrap justify-between items-center pb-2 my-4 mr-2 min-h-16">
@@ -31,9 +43,9 @@
</span>
{/if}
{#if $$slots.default}
{#if children}
<div class="my-2 {childrenWrapperDivClasses}">
<slot />
{@render children?.()}
</div>
{/if}
</div>
@@ -1,4 +1,6 @@
<script lang="ts">
import { run } from 'svelte/legacy'
import { type GridApi, createGrid, type IDatasource } from 'ag-grid-community'
import 'ag-grid-community/styles/ag-grid.css'
@@ -14,15 +16,19 @@
// import 'ag-grid-community/dist/styles/ag-theme-alpine-dark.css'
let selectedRowIndex = -1
export let s3resource: string
export let storage: string | undefined
export let workspaceId: string | undefined
export let disable_download: boolean = false
interface Props {
s3resource: string
storage: string | undefined
workspaceId: string | undefined
disable_download?: boolean
}
let { s3resource, storage, workspaceId, disable_download = false }: Props = $props()
let lastSearch: string | undefined = undefined
let nbRows: number | undefined = undefined
let csvSeparatorChar: string = ','
let nbRows: number | undefined = $state(undefined)
let csvSeparatorChar: string = $state(',')
let datasource: IDatasource = {
rowCount: 0,
getRows: async function (params) {
@@ -95,11 +101,9 @@
toggleRow(rows[0])
}
let eGui: HTMLDivElement
let eGui: HTMLDivElement | undefined = $state()
$: eGui && mountGrid()
let error: string | undefined = undefined
let error: string | undefined = $state(undefined)
async function mountGrid() {
if (eGui) {
try {
@@ -170,7 +174,10 @@
}
}
let darkMode: boolean = false
let darkMode: boolean = $state(false)
run(() => {
eGui && mountGrid()
})
</script>
<DarkModeObserver bind:darkMode />
@@ -182,7 +189,7 @@
<label for="csvSeparatorChar" class="text-2xs text-secondary">Separator</label>
<div class="w-12 ml-2 mr-2">
<select class="h-8" bind:value={csvSeparatorChar} on:change={(e) => mountGrid()}>
<select class="h-8" bind:value={csvSeparatorChar} onchange={(e) => mountGrid()}>
<option value=",">,</option>
<option value=";">;</option>
<option value="\t">\t</option>
@@ -79,12 +79,14 @@
<p class="text-primary text-sm">No permission changes recorded yet</p>
{:else}
<TableCustom>
<tr slot="header-row">
<th>Changed By</th>
<th>Change Type</th>
<th>Affected</th>
<th>Date</th>
</tr>
{#snippet headerRow()}
<tr >
<th>Changed By</th>
<th>Change Type</th>
<th>Affected</th>
<th>Date</th>
</tr>
{/snippet}
{#snippet body()}
<tbody>
{#each history as change}
@@ -10,19 +10,19 @@
import { Hourglass, Loader2, Play, RefreshCw } from 'lucide-svelte'
let dispatch = createEventDispatcher()
let drawer: Drawer
let drawer: Drawer | undefined = $state()
let script: Script
let loadQueuedJobs = true
let queuedJobsLoading = false
let script: Script | undefined = $state()
let loadQueuedJobs = $state(true)
let queuedJobsLoading = $state(false)
let queuedJobs: {
status: 'running' | 'queued'
jobId: string
scheduledFor: string
scriptHash: string
}[] = []
}[] = $state([])
let cancellingInProgress = false
let cancellingInProgress = $state(false)
async function continuouslyLoadQueuedJobs() {
while (loadQueuedJobs) {
@@ -40,7 +40,7 @@
let qjs = await JobService.listQueue({
workspace: $workspaceStore ?? '',
orderDesc: false,
scriptPathExact: script.path
scriptPathExact: script?.path
})
let loadingQueuedJobs: {
status: 'running' | 'queued'
@@ -71,12 +71,12 @@
cancellingInProgress = true
await JobService.cancelPersistentQueuedJobs({
workspace: $workspaceStore ?? '',
path: script.path,
path: script?.path ?? '',
requestBody: {
reason: undefined
}
})
sendUserToast(`All jobs cancelled for ${script.path}`)
sendUserToast(`All jobs cancelled for ${script?.path}`)
cancellingInProgress = false
}
@@ -88,12 +88,12 @@
script = persistentScript!
loadQueuedJobs = true
continuouslyLoadQueuedJobs()
drawer.openDrawer?.()
drawer?.openDrawer?.()
}
async function exit() {
loadQueuedJobs = false
drawer.closeDrawer?.()
drawer?.closeDrawer?.()
}
onDestroy(() => {
@@ -117,51 +117,57 @@
>
<div class="flex gap-2 items-center justify-between">
<h2>
Queued jobs for {script.path}
Queued jobs for {script?.path}
</h2>
<Button size="md" btnClasses="w-full h-8" variant="default" on:click={loadQueuedJobsOnce}>
<RefreshCw class={queuedJobsLoading ? 'animate-spin' : ''} size={14} />
</Button>
</div>
<TableCustom>
<tr slot="header-row">
<th class="text-xs">Script Hash</th>
<th class="text-xs">Job ID</th>
<th class="text-xs">Status</th>
<th class="text-xs">Scheduled For</th>
</tr>
<tbody slot="body">
{#each queuedJobs as { jobId, status, scriptHash, scheduledFor }}
<tr class="">
<td class="text-xs">
<a
class="pr-3"
href="{base}/scripts/get/{scriptHash}?workspace={$workspaceStore}"
target="_blank"
>
{scriptHash}
</a>
</td>
<td class="text-xs">
<a class="pr-3" href="{base}/run/{jobId}?workspace={$workspaceStore}" target="_blank"
>{jobId.substring(24)}</a
>
</td>
<td class="text-xs">
{#if status === 'running'}
<Badge color="yellow" baseClass="!px-1.5">
<Play size={14} />
</Badge>
{:else}
<Badge baseClass="!px-1.5">
<Hourglass size={14} />
</Badge>
{/if}
</td>
<td class="text-xs">{scheduledFor}</td>
</tr>
{/each}
</tbody>
{#snippet headerRow()}
<tr>
<th class="text-xs">Script Hash</th>
<th class="text-xs">Job ID</th>
<th class="text-xs">Status</th>
<th class="text-xs">Scheduled For</th>
</tr>
{/snippet}
{#snippet body()}
<tbody>
{#each queuedJobs as { jobId, status, scriptHash, scheduledFor }}
<tr class="">
<td class="text-xs">
<a
class="pr-3"
href="{base}/scripts/get/{scriptHash}?workspace={$workspaceStore}"
target="_blank"
>
{scriptHash}
</a>
</td>
<td class="text-xs">
<a
class="pr-3"
href="{base}/run/{jobId}?workspace={$workspaceStore}"
target="_blank">{jobId.substring(24)}</a
>
</td>
<td class="text-xs">
{#if status === 'running'}
<Badge color="yellow" baseClass="!px-1.5">
<Play size={14} />
</Badge>
{:else}
<Badge baseClass="!px-1.5">
<Hourglass size={14} />
</Badge>
{/if}
</td>
<td class="text-xs">{scheduledFor}</td>
</tr>
{/each}
</tbody>
{/snippet}
</TableCustom>
{#snippet actions()}
+2 -2
View File
@@ -43,10 +43,10 @@
onClick
}: Props = $props()
const [popperRef, popperContent] = createPopperActions({ placement })
const [popperRef, popperContent] = createPopperActions({ placement: untrack(() => placement) })
const popperOptions: PopperOptions<{}> = {
placement,
placement: untrack(() => placement),
strategy: 'fixed',
modifiers: [
{ name: 'offset', options: { offset: [8, 8] } },

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