Merge branch 'main' into alp/teams_scripts

This commit is contained in:
Alexander Petric
2025-01-31 11:48:52 -05:00
120 changed files with 2902 additions and 1749 deletions
+1 -1
View File
@@ -1 +1 @@
use nix
use flake
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
with:
deno-version: v1.x
- name: benchmark
timeout-minutes: 20
timeout-minutes: 30
run: deno run --unstable -A -r
https://raw.githubusercontent.com/windmill-labs/windmill/${GITHUB_REF##ref/head/}/benchmarks/benchmark_suite.ts
-c
+11 -2
View File
@@ -46,6 +46,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
fetch-depth: 0
- name: Read EE repo commit hash
@@ -108,6 +109,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
fetch-depth: 0
- name: Read EE repo commit hash
@@ -205,7 +207,6 @@ jobs:
ARCH: amd64
steps:
- uses: actions/checkout@v4
- run: |
# pulling docker image with desired arch so that actions-docker-extract doesn't do it
docker pull --platform "linux/$ARCH" ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.DEV_SHA }}
@@ -291,6 +292,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
fetch-depth: 0
- name: Prepare test run
if: ${{ ! startsWith(github.ref, 'refs/tags/v') }}
@@ -354,7 +356,7 @@ jobs:
verify_ee_image_vulnerabilities:
runs-on: ubicloud
needs: [tag_latest_ee]
if: ${{ startsWith(github.ref, 'refs/tags/v') }} && (github.event_name != 'workflow_dispatch')
if: startsWith(github.ref, 'refs/tags/v') && (github.event_name != 'workflow_dispatch')
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -403,6 +405,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }}
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v2
@@ -431,6 +434,11 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Update Dockerfile image reference
run: |
sed -i 's|FROM ghcr.io/windmill-labs/windmill-ee:dev|FROM ghcr.io/${{ env.IMAGE_NAME }}-ee:${{ env.DEV_SHA }}|' ./docker/DockerfileNsjail
cat ./docker/DockerfileNsjail | grep "FROM"
- name: Build and push publicly ee
uses: depot/build-push-action@v1
with:
@@ -542,6 +550,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }}
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v2
+65
View File
@@ -1,5 +1,70 @@
# Changelog
## [1.455.1](https://github.com/windmill-labs/windmill/compare/v1.455.0...v1.455.1) (2025-01-31)
### Bug Fixes
* pin malachite version ([b74d3fd](https://github.com/windmill-labs/windmill/commit/b74d3fd6e08ea991f7a7360b79d54a45a8ee58be))
## [1.455.0](https://github.com/windmill-labs/windmill/compare/v1.454.1...v1.455.0) (2025-01-31)
### Features
* **cli:** add --parallel option to push + display timings ([73dbb8f](https://github.com/windmill-labs/windmill/commit/73dbb8fb80f56942d35e5b5cda9715bba5d5c98f))
* duckdb connection settings with azure blob storage ([#5180](https://github.com/windmill-labs/windmill/issues/5180)) ([8ba0f3a](https://github.com/windmill-labs/windmill/commit/8ba0f3addd0a02dba87c30739b44f524019a5e5a))
* polars connection settings with azure blob storage ([#5175](https://github.com/windmill-labs/windmill/issues/5175)) ([8b65f1b](https://github.com/windmill-labs/windmill/commit/8b65f1bc3829d6b02bf38bf1449d3061ef62a08c))
### Bug Fixes
* capture panel never stopping + http saved config ([#5179](https://github.com/windmill-labs/windmill/issues/5179)) ([ff3339d](https://github.com/windmill-labs/windmill/commit/ff3339dc4e9ea4641b847fa5ef51bedcf6a3a429))
* restart zombie job correctly handle concurrency limits ([#5181](https://github.com/windmill-labs/windmill/issues/5181)) ([8a93418](https://github.com/windmill-labs/windmill/commit/8a93418c25d180859bda76b9218e750e4b407fed))
## [1.454.1](https://github.com/windmill-labs/windmill/compare/v1.454.0...v1.454.1) (2025-01-30)
### Bug Fixes
* fix codebase script execution ([b1155a9](https://github.com/windmill-labs/windmill/commit/b1155a9012c726c1b1c3f1a62f3aa96b8504681c))
## [1.454.0](https://github.com/windmill-labs/windmill/compare/v1.453.1...v1.454.0) (2025-01-30)
### Features
* history for flow inputs ([#5117](https://github.com/windmill-labs/windmill/issues/5117)) ([cd44056](https://github.com/windmill-labs/windmill/commit/cd440564d29aff39f6dc30ecbf0055696b07cf12))
### Bug Fixes
* fix app form password handling ([4812c0c](https://github.com/windmill-labs/windmill/commit/4812c0cc9e46fdb5944f7010efdf617f2fd23cff))
* improve codebase support ([c8cc9d2](https://github.com/windmill-labs/windmill/commit/c8cc9d2941dda2fedb959e1156c4f9b4f7dfab43))
## [1.453.1](https://github.com/windmill-labs/windmill/compare/v1.453.0...v1.453.1) (2025-01-30)
### Bug Fixes
* **frontend:** fetching job result (db schema) separatly if too big ([#5171](https://github.com/windmill-labs/windmill/issues/5171)) ([f29ee67](https://github.com/windmill-labs/windmill/commit/f29ee67d9d8547d7c4a79e14698b7d88a722e730))
* **typescript-client:** fix typescript client mocked api behavior ([7f516f0](https://github.com/windmill-labs/windmill/commit/7f516f08682876f5a7ca409ec002cba6ec0c5c5b))
## [1.453.0](https://github.com/windmill-labs/windmill/compare/v1.452.1...v1.453.0) (2025-01-29)
### Features
* custom response headers ([#5156](https://github.com/windmill-labs/windmill/issues/5156)) ([5544d42](https://github.com/windmill-labs/windmill/commit/5544d42a2db652660051b5f2e05f8db0102400fa))
* **python:** add `custom_wheels` directory to `PYTHONPATH` ([#5169](https://github.com/windmill-labs/windmill/issues/5169)) ([c60f8da](https://github.com/windmill-labs/windmill/commit/c60f8dacf191c5255ef8fca462d6d3d571b86b07))
* websocket trigger allow returning messages ([#5168](https://github.com/windmill-labs/windmill/issues/5168)) ([487c273](https://github.com/windmill-labs/windmill/commit/487c273bfbc5bf1731947c922f32cd026cca47f9))
### Bug Fixes
* oracledb tag for native workers + client not working on arm systems ([#5162](https://github.com/windmill-labs/windmill/issues/5162)) ([a1579c1](https://github.com/windmill-labs/windmill/commit/a1579c1654a2ddf9f6fc8f05f62b37bb09303752))
* **python:** fix uv can't find ssl certificates ([#5157](https://github.com/windmill-labs/windmill/issues/5157)) ([680fb18](https://github.com/windmill-labs/windmill/commit/680fb1852a192e384e23e2f5ceea473383f9f5aa))
## [1.452.1](https://github.com/windmill-labs/windmill/compare/v1.452.0...v1.452.1) (2025-01-27)
@@ -1,40 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n EXISTS(SELECT 1 FROM websocket_trigger WHERE workspace_id = $1) as \"websocket_used!\", \n EXISTS(SELECT 1 FROM http_trigger WHERE workspace_id = $1) as \"http_routes_used!\",\n EXISTS(SELECT 1 FROM kafka_trigger WHERE workspace_id = $1) as \"kafka_used!\",\n EXISTS(SELECT 1 FROM nats_trigger WHERE workspace_id = $1) as \"nats_used!\"",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "websocket_used!",
"type_info": "Bool"
},
{
"ordinal": 1,
"name": "http_routes_used!",
"type_info": "Bool"
},
{
"ordinal": 2,
"name": "kafka_used!",
"type_info": "Bool"
},
{
"ordinal": 3,
"name": "nats_used!",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null,
null,
null,
null
]
},
"hash": "1ef48cc430870ab6c062b046bdbf0a2db057141619e9818ddb904d7c242efec3"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id, created_at, trigger_kind as \"trigger_kind: _\", payload as \"payload: _\", trigger_extra as \"trigger_extra: _\"\n FROM capture\n WHERE workspace_id = $1\n AND path = $2 AND is_flow = $3\n AND ($4::trigger_kind IS NULL OR trigger_kind = $4)\n ORDER BY created_at DESC\n OFFSET $5\n LIMIT $6",
"query": "SELECT id, created_at, trigger_kind as \"trigger_kind: _\", CASE WHEN pg_column_size(payload) < 40000 THEN payload ELSE '\"WINDMILL_TOO_BIG\"'::jsonb END as \"payload!: _\", trigger_extra as \"trigger_extra: _\"\n FROM capture\n WHERE workspace_id = $1\n AND path = $2 AND is_flow = $3\n AND ($4::trigger_kind IS NULL OR trigger_kind = $4)\n ORDER BY created_at DESC\n OFFSET $5\n LIMIT $6",
"describe": {
"columns": [
{
@@ -34,7 +34,7 @@
},
{
"ordinal": 3,
"name": "payload: _",
"name": "payload!: _",
"type_info": "Jsonb"
},
{
@@ -71,9 +71,9 @@
false,
false,
false,
false,
null,
true
]
},
"hash": "e08dddf6af2656b561c453460ff928e7a158bd29d9035bee136f9952fb96bad4"
"hash": "5c1de8473e0e96c1063a9a735a064c5a91e3ed8d9260c72b783fc12542b88fbd"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE websocket_trigger SET url = $1, script_path = $2, path = $3, is_flow = $4, filters = $5, initial_messages = $6, url_runnable_args = $7, edited_by = $8, email = $9, edited_at = now(), server_id = NULL, error = NULL\n WHERE workspace_id = $10 AND path = $11",
"query": "UPDATE websocket_trigger SET url = $1, script_path = $2, path = $3, is_flow = $4, filters = $5, initial_messages = $6, url_runnable_args = $7, edited_by = $8, email = $9, can_return_message = $10, edited_at = now(), server_id = NULL, error = NULL\n WHERE workspace_id = $11 AND path = $12",
"describe": {
"columns": [],
"parameters": {
@@ -14,11 +14,12 @@
"Jsonb",
"Varchar",
"Varchar",
"Bool",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "561b7935d687f6b9f3d6488f8489f55e6737ffa2d3716f803d32f8b68cc1e915"
"hash": "63b42286804a3f3977235935d7ac200a07c5a4ad3a50051b73f382385b89466e"
}
@@ -1,34 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE queue SET running = false, started_at = null\n WHERE last_ping < now() - ($1 || ' seconds')::interval\n AND running = true AND job_kind NOT IN ('flow', 'flowpreview', 'flownode', 'singlescriptflow') AND same_worker = false RETURNING id, workspace_id, last_ping",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "last_ping",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
true
]
},
"hash": "84576f6add15108ade2ed3a88185d67db17904859e5f01f1d578af4e1d062a85"
}
@@ -0,0 +1,34 @@
{
"db_name": "PostgreSQL",
"query": "WITH zombie_jobs AS (\n UPDATE queue SET running = false, started_at = null\n WHERE last_ping < now() - ($1 || ' seconds')::interval\n AND running = true AND job_kind NOT IN ('flow', 'flowpreview', 'flownode', 'singlescriptflow') AND same_worker = false \n RETURNING id, workspace_id, last_ping\n ),\n update_concurrency AS (\n UPDATE concurrency_counter cc\n SET job_uuids = job_uuids - zj.id::text\n FROM zombie_jobs zj\n INNER JOIN concurrency_key ck ON ck.job_id = zj.id\n WHERE cc.concurrency_id = ck.key\n )\n SELECT id, workspace_id, last_ping FROM zombie_jobs",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "last_ping",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
true
]
},
"hash": "9a9b639611459659ae355a43f219c6da2c3d1e04d49306adaaaaf06e54ee8357"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT content AS \"content!: String\",\n lock AS \"lock: String\", language AS \"language: Option<ScriptLang>\", envs AS \"envs: Vec<String>\", codebase AS \"codebase: String\" FROM script WHERE hash = $1 LIMIT 1",
"query": "SELECT content AS \"content!: String\",\n lock AS \"lock: String\", language AS \"language: Option<ScriptLang>\", envs AS \"envs: Vec<String>\", codebase LIKE '%.tar' as use_tar FROM script WHERE hash = $1 LIMIT 1",
"describe": {
"columns": [
{
@@ -52,8 +52,8 @@
},
{
"ordinal": 4,
"name": "codebase: String",
"type_info": "Varchar"
"name": "use_tar",
"type_info": "Bool"
}
],
"parameters": {
@@ -66,8 +66,8 @@
true,
false,
true,
true
null
]
},
"hash": "0bf123446bebbc357c58a53a9319f4954dbf3225e91cbe999e5b264c1a747664"
"hash": "d55eabd42559893bdf91c5fdab1ae58006013be26fcb9367b3c6830f38749799"
}
@@ -0,0 +1,61 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id, created_at, trigger_kind as \"trigger_kind: _\", payload as \"payload!: _\", trigger_extra as \"trigger_extra: _\" FROM capture WHERE id = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 2,
"name": "trigger_kind: _",
"type_info": {
"Custom": {
"name": "trigger_kind",
"kind": {
"Enum": [
"webhook",
"http",
"websocket",
"kafka",
"email",
"nats"
]
}
}
}
},
{
"ordinal": 3,
"name": "payload!: _",
"type_info": "Jsonb"
},
{
"ordinal": 4,
"name": "trigger_extra: _",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Int8",
"Text"
]
},
"nullable": [
false,
false,
false,
false,
true
]
},
"hash": "e17ec84003e2ec414622d100f5dfdda86bee33f31835317df512a20c805b35d7"
}
@@ -1,29 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value, is_secret\n FROM variable\n WHERE path = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "value",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "is_secret",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false
]
},
"hash": "f3d3d105b76ec2eded521c3bb3735a3765c9aa2d103e3df85c245d85ba32b3b9"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT flow_version.value->>'early_return' as early_return\n FROM flow \n LEFT JOIN flow_version\n ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]\n WHERE flow.path = $1 and flow.workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "early_return",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
null
]
},
"hash": "fc243af1bc70f04e28c006364d6dfdfc49981568e8c496fb6a163c499c3e4ad1"
}
+111 -99
View File
@@ -718,9 +718,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sso"
version = "1.56.0"
version = "1.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12e057fdcb8842de9b83592a70f5b4da0ee10bc0ad278247da1425a742a444d7"
checksum = "c54bab121fe1881a74c338c5f723d1592bf3b53167f80268a1274f404e1acc38"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -740,9 +740,9 @@ dependencies = [
[[package]]
name = "aws-sdk-ssooidc"
version = "1.57.1"
version = "1.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a120ade4a44691b3c5c2ff2fa61b14ed331fdc218397f61ab48d66593012ae2a"
checksum = "8c8234fd024f7ac61c4e44ea008029bde934250f371efe7d4a39708397b1080c"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -762,9 +762,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sts"
version = "1.57.0"
version = "1.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "115fd4fb663817ed595a5ee4f1649d7aacd861d47462323cb37576ce89271b93"
checksum = "ba60e1d519d6f23a9df712c04fdeadd7872ac911c84b2f62a8bda92e129b7962"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -962,7 +962,7 @@ dependencies = [
"http 1.2.0",
"http-body 1.0.1",
"http-body-util",
"hyper 1.5.2",
"hyper 1.6.0",
"hyper-util",
"itoa",
"matchit",
@@ -1282,7 +1282,7 @@ dependencies = [
"hex",
"http 1.2.0",
"http-body-util",
"hyper 1.5.2",
"hyper 1.6.0",
"hyper-named-pipe",
"hyper-util",
"hyperlocal",
@@ -1388,9 +1388,9 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.16.0"
version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
dependencies = [
"allocator-api2",
]
@@ -1708,9 +1708,9 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "cmake"
version = "0.1.52"
version = "0.1.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c682c223677e0e5b6b7f63a64b9351844c3f1b1678a68b7ee617e30fb082620e"
checksum = "e24a03c8b52922d68a1589ad61032f2c1aa5a8158d2aa0d93c6e9534944bbad6"
dependencies = [
"cc",
]
@@ -1941,7 +1941,7 @@ checksum = "5877d3fbf742507b66bc2a1945106bd30dd8504019d596901ddd012a4dd01740"
dependencies = [
"chrono",
"once_cell",
"winnow 0.6.25",
"winnow 0.6.26",
]
[[package]]
@@ -2641,7 +2641,7 @@ dependencies = [
"hickory-resolver",
"http 1.2.0",
"http-body-util",
"hyper 1.5.2",
"hyper 1.6.0",
"hyper-rustls 0.27.5",
"hyper-util",
"ipnet",
@@ -2777,7 +2777,7 @@ checksum = "688175eed35e7b3053ec114227894ef24786855405d8844058a48bffa997d85a"
dependencies = [
"deno_core",
"deno_native_certs",
"rustls 0.23.21",
"rustls 0.23.22",
"rustls-pemfile 2.2.0",
"rustls-tokio-stream",
"rustls-webpki 0.102.8",
@@ -3766,9 +3766,9 @@ dependencies = [
[[package]]
name = "getrandom"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71393ecc86efbf00e4ca13953979ba8b94cfe549a4b74cc26d8b62f4d8feac2b"
checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
dependencies = [
"cfg-if",
"libc",
@@ -3958,9 +3958,9 @@ dependencies = [
[[package]]
name = "hashify"
version = "0.2.4"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a194e6d22f060dada750b0c33c9c3d01dce0ba5212ea81e0a46834e065e932c"
checksum = "f208758247e68e239acaa059e72e4ce1f30f2a4b6523f19c1b923d25b7e9cceb"
dependencies = [
"proc-macro2",
"quote",
@@ -4201,9 +4201,9 @@ dependencies = [
[[package]]
name = "httparse"
version = "1.9.5"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946"
checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a"
[[package]]
name = "httpdate"
@@ -4243,9 +4243,9 @@ dependencies = [
[[package]]
name = "hyper"
version = "1.5.2"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0"
checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
dependencies = [
"bytes",
"futures-channel",
@@ -4269,7 +4269,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278"
dependencies = [
"hex",
"hyper 1.5.2",
"hyper 1.6.0",
"hyper-util",
"pin-project-lite",
"tokio",
@@ -4301,9 +4301,9 @@ checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2"
dependencies = [
"futures-util",
"http 1.2.0",
"hyper 1.5.2",
"hyper 1.6.0",
"hyper-util",
"rustls 0.23.21",
"rustls 0.23.22",
"rustls-native-certs 0.8.1",
"rustls-pki-types",
"tokio",
@@ -4318,7 +4318,7 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3203a961e5c83b6f5498933e78b6b263e208c197b63e9c6c53cc82ffd3f63793"
dependencies = [
"hyper 1.5.2",
"hyper 1.6.0",
"hyper-util",
"pin-project-lite",
"tokio",
@@ -4346,7 +4346,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"http-body-util",
"hyper 1.5.2",
"hyper 1.6.0",
"hyper-util",
"native-tls",
"tokio",
@@ -4365,7 +4365,7 @@ dependencies = [
"futures-util",
"http 1.2.0",
"http-body 1.0.1",
"hyper 1.5.2",
"hyper 1.6.0",
"pin-project-lite",
"socket2",
"tokio",
@@ -4382,7 +4382,7 @@ checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7"
dependencies = [
"hex",
"http-body-util",
"hyper 1.5.2",
"hyper 1.6.0",
"hyper-util",
"pin-project-lite",
"tokio",
@@ -4587,9 +4587,9 @@ dependencies = [
[[package]]
name = "indicatif"
version = "0.17.9"
version = "0.17.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbf675b85ed934d3c67b5c5469701eec7db22689d0a2139d856e0925fa28b281"
checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235"
dependencies = [
"console",
"number_prefix",
@@ -5060,9 +5060,9 @@ dependencies = [
[[package]]
name = "mail-parser"
version = "0.10.1"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d8f64d96d3235bb1390e3d8efd745deb66b85595ce4db431a3ddc0e841a6015"
checksum = "187a2b93c4c8c32f552ee06c2d99915e575de2fc7e04b07891c9edfee5b8edd6"
dependencies = [
"encoding_rs",
"hashify",
@@ -5078,7 +5078,7 @@ dependencies = [
"base64 0.22.1",
"gethostname",
"mail-builder",
"rustls 0.23.21",
"rustls 0.23.22",
"rustls-pki-types",
"smtp-proto",
"tokio",
@@ -5099,9 +5099,9 @@ dependencies = [
[[package]]
name = "malachite-base"
version = "0.4.18"
version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06f6d078bb4dad5b76f6a85737e6c9113667d199612a73eef375c56e75d5d4d5"
checksum = "cc7d99f6446827fb878a005ea5cf7db1d78d236391733012216a9b8eb5dbd824"
dependencies = [
"hashbrown 0.14.5",
"itertools 0.11.0",
@@ -5124,9 +5124,9 @@ dependencies = [
[[package]]
name = "malachite-nz"
version = "0.4.18"
version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad61a09cde72dcdb8b2baaf3e71430c5bd94fb6753da89f17301a459f3e466db"
checksum = "f36157fa6b969ca652feefe159e202ebd85683e57463074492a7ec3eaa468c97"
dependencies = [
"itertools 0.11.0",
"libm",
@@ -5135,9 +5135,9 @@ dependencies = [
[[package]]
name = "malachite-q"
version = "0.4.18"
version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4a9dfffb87667ae94e8320213d3f5419e3e37311bc6bf8f4d2ab9b44f3a535"
checksum = "3dde29a4fcf7025f482745247951f7c5d0459671f59aedf09035b2e2c3746fdf"
dependencies = [
"itertools 0.11.0",
"malachite-base",
@@ -5666,7 +5666,7 @@ dependencies = [
"chrono",
"futures",
"humantime",
"hyper 1.5.2",
"hyper 1.6.0",
"itertools 0.13.0",
"md-5 0.10.6",
"parking_lot",
@@ -6427,7 +6427,7 @@ version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b"
dependencies = [
"toml_edit 0.22.22",
"toml_edit 0.22.23",
]
[[package]]
@@ -6710,7 +6710,7 @@ dependencies = [
"quinn-proto",
"quinn-udp",
"rustc-hash 2.1.0",
"rustls 0.23.21",
"rustls 0.23.22",
"socket2",
"thiserror 2.0.11",
"tokio",
@@ -6728,7 +6728,7 @@ dependencies = [
"rand 0.8.5",
"ring 0.17.8",
"rustc-hash 2.1.0",
"rustls 0.23.21",
"rustls 0.23.22",
"rustls-pki-types",
"slab",
"thiserror 2.0.11",
@@ -6855,7 +6855,7 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff"
dependencies = [
"getrandom 0.3.0",
"getrandom 0.3.1",
"zerocopy 0.8.14",
]
@@ -7118,7 +7118,7 @@ dependencies = [
"http 1.2.0",
"http-body 1.0.1",
"http-body-util",
"hyper 1.5.2",
"hyper 1.6.0",
"hyper-rustls 0.27.5",
"hyper-tls 0.6.0",
"hyper-util",
@@ -7131,7 +7131,7 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls 0.23.21",
"rustls 0.23.22",
"rustls-native-certs 0.8.1",
"rustls-pemfile 2.2.0",
"rustls-pki-types",
@@ -7397,9 +7397,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.21"
version = "0.23.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8"
checksum = "9fb9263ab4eb695e42321db096e3b8fbd715a59b154d5c88d82db2175b681ba7"
dependencies = [
"log",
"once_cell",
@@ -7481,7 +7481,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22557157d7395bc30727745b365d923f1ecc230c4c80b176545f3f4f08c46e33"
dependencies = [
"futures",
"rustls 0.23.21",
"rustls 0.23.22",
"socket2",
"tokio",
]
@@ -7572,9 +7572,9 @@ checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
[[package]]
name = "ryu"
version = "1.0.18"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
[[package]]
name = "ryu-js"
@@ -7842,9 +7842,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.137"
version = "1.0.138"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b"
checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
dependencies = [
"indexmap 2.7.1",
"itoa",
@@ -8377,7 +8377,7 @@ dependencies = [
"memchr",
"once_cell",
"percent-encoding",
"rustls 0.23.21",
"rustls 0.23.22",
"rustls-pemfile 2.2.0",
"serde",
"serde_json",
@@ -9299,13 +9299,13 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.15.0"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704"
checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91"
dependencies = [
"cfg-if",
"fastrand 2.3.0",
"getrandom 0.2.15",
"getrandom 0.3.1",
"once_cell",
"rustix",
"windows-sys 0.59.0",
@@ -9681,7 +9681,7 @@ version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37"
dependencies = [
"rustls 0.23.21",
"rustls 0.23.22",
"tokio",
]
@@ -9793,7 +9793,7 @@ dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit 0.22.22",
"toml_edit 0.22.23",
]
[[package]]
@@ -9820,15 +9820,15 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.22.22"
version = "0.22.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee"
dependencies = [
"indexmap 2.7.1",
"serde",
"serde_spanned",
"toml_datetime",
"winnow 0.6.25",
"winnow 0.7.0",
]
[[package]]
@@ -9846,7 +9846,7 @@ dependencies = [
"http 1.2.0",
"http-body 1.0.1",
"http-body-util",
"hyper 1.5.2",
"hyper 1.6.0",
"hyper-timeout",
"hyper-util",
"percent-encoding",
@@ -10346,9 +10346,9 @@ checksum = "2f322b60f6b9736017344fa0635d64be2f458fbc04eef65f6be22976dd1ffd5b"
[[package]]
name = "unicode-ident"
version = "1.0.15"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11cd88e12b17c6494200a9c1b683a04fcac9573ed74cd1b62aeb2727c5592243"
checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034"
[[package]]
name = "unicode-normalization"
@@ -10455,7 +10455,7 @@ dependencies = [
"log",
"native-tls",
"once_cell",
"rustls 0.23.21",
"rustls 0.23.22",
"rustls-pki-types",
"serde",
"serde_json",
@@ -10779,9 +10779,9 @@ dependencies = [
[[package]]
name = "webpki-roots"
version = "0.26.7"
version = "0.26.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e"
checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9"
dependencies = [
"rustls-pki-types",
]
@@ -10860,7 +10860,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill"
version = "1.452.1"
version = "1.455.1"
dependencies = [
"anyhow",
"axum",
@@ -10890,6 +10890,7 @@ dependencies = [
"tracing",
"url",
"uuid 1.12.1",
"v8",
"windmill-api",
"windmill-api-client",
"windmill-autoscaling",
@@ -10902,7 +10903,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.452.1"
version = "1.455.1"
dependencies = [
"anyhow",
"argon2",
@@ -10932,7 +10933,7 @@ dependencies = [
"hf-hub",
"hmac",
"http 1.2.0",
"hyper 1.5.2",
"hyper 1.6.0",
"itertools 0.14.0",
"jsonwebtoken",
"lazy_static",
@@ -10995,7 +10996,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.452.1"
version = "1.455.1"
dependencies = [
"base64 0.22.1",
"chrono",
@@ -11013,7 +11014,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.452.1"
version = "1.455.1"
dependencies = [
"chrono",
"serde",
@@ -11026,7 +11027,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.452.1"
version = "1.455.1"
dependencies = [
"anyhow",
"serde",
@@ -11040,7 +11041,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.452.1"
version = "1.455.1"
dependencies = [
"anyhow",
"async-stream",
@@ -11060,7 +11061,7 @@ dependencies = [
"git-version",
"hex",
"hmac",
"hyper 1.5.2",
"hyper 1.6.0",
"indexmap 2.7.1",
"itertools 0.14.0",
"lazy_static",
@@ -11099,7 +11100,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.452.1"
version = "1.455.1"
dependencies = [
"regex",
"serde",
@@ -11113,7 +11114,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.452.1"
version = "1.455.1"
dependencies = [
"anyhow",
"bytes",
@@ -11136,7 +11137,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.452.1"
version = "1.455.1"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -11148,7 +11149,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.452.1"
version = "1.455.1"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -11157,7 +11158,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.452.1"
version = "1.455.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -11169,7 +11170,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.452.1"
version = "1.455.1"
dependencies = [
"anyhow",
"serde_json",
@@ -11181,7 +11182,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.452.1"
version = "1.455.1"
dependencies = [
"anyhow",
"gosyn",
@@ -11193,7 +11194,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.452.1"
version = "1.455.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -11205,7 +11206,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.452.1"
version = "1.455.1"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -11216,7 +11217,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.452.1"
version = "1.455.1"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -11227,12 +11228,14 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.452.1"
version = "1.455.1"
dependencies = [
"anyhow",
"async-recursion",
"itertools 0.14.0",
"lazy_static",
"malachite",
"malachite-bigint",
"phf",
"regex",
"regex-lite",
@@ -11245,7 +11248,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.452.1"
version = "1.455.1"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -11262,7 +11265,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.452.1"
version = "1.455.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -11274,7 +11277,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.452.1"
version = "1.455.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -11292,7 +11295,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wasm"
version = "1.452.1"
version = "1.455.1"
dependencies = [
"anyhow",
"getrandom 0.2.15",
@@ -11314,7 +11317,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.452.1"
version = "1.455.1"
dependencies = [
"anyhow",
"serde_json",
@@ -11324,7 +11327,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.452.1"
version = "1.455.1"
dependencies = [
"anyhow",
"async-recursion",
@@ -11357,7 +11360,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.452.1"
version = "1.455.1"
dependencies = [
"wasm-bindgen",
"wasm-bindgen-test",
@@ -11367,7 +11370,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.452.1"
version = "1.455.1"
dependencies = [
"anyhow",
"async-recursion",
@@ -11642,9 +11645,18 @@ dependencies = [
[[package]]
name = "winnow"
version = "0.6.25"
version = "0.6.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad699df48212c6cc6eb4435f35500ac6fd3b9913324f938aea302022ce19d310"
checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28"
dependencies = [
"memchr",
]
[[package]]
name = "winnow"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e49d2d35d3fad69b39b94139037ecfb4f359f08958b9c11e7315ce770462419"
dependencies = [
"memchr",
]
+8 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.452.1"
version = "1.455.1"
authors.workspace = true
edition.workspace = true
@@ -30,7 +30,7 @@ members = [
]
[workspace.package]
version = "1.452.1"
version = "1.455.1"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -61,7 +61,7 @@ cloud = ["windmill-queue/cloud", "windmill-worker/cloud"]
jemalloc = ["windmill-common/jemalloc", "dep:tikv-jemallocator", "dep:tikv-jemalloc-sys", "dep:tikv-jemalloc-ctl"]
tantivy = ["dep:windmill-indexer", "windmill-api/tantivy", "windmill-indexer/enterprise", "windmill-indexer/parquet", "enterprise", "parquet"]
sqlx = ["windmill-worker/sqlx"]
deno_core = ["windmill-worker/deno_core", "dep:deno_core"]
deno_core = ["windmill-worker/deno_core", "dep:deno_core", "dep:v8"]
kafka = ["windmill-api/kafka"]
nats = ["windmill-api/nats"]
otel = ["windmill-common/otel", "windmill-worker/otel"]
@@ -116,6 +116,7 @@ deno_core = { workspace = true, optional = true }
object_store = { workspace = true, optional = true }
quote.workspace = true
memchr.workspace = true
v8 = { workspace = true, optional = true }
[target.'cfg(not(target_env = "msvc"))'.dependencies]
@@ -133,6 +134,7 @@ windmill-api-client.workspace = true
deno_core = { workspace = true, features = ["include_js_files_for_snapshotting", "unsafe_use_unprotected_platform"] }
[workspace.dependencies]
windmill-api = { path = "./windmill-api", default-features = false }
windmill-queue = { path = "./windmill-queue" }
@@ -157,6 +159,7 @@ windmill-parser-graphql = { path = "./parsers/windmill-parser-graphql" }
windmill-parser-php = { path = "./parsers/windmill-parser-php" }
windmill-api-client = { path = "./windmill-api-client" }
v8 = "=130.0.7" # Exact version
memchr = "2.7.4"
axum = { version = "^0.7", features = ["multipart"] }
headers = "^0"
@@ -188,6 +191,8 @@ rand = "^0"
rand_core = { version = "^0", features = ["std"] }
magic-crypt = "^3"
git-version = "^0"
malachite = "=0.4.18"
malachite-bigint = "=0.2.0"
rustpython-parser = "^0"
php-parser-rs = { git = "https://github.com/php-rust-tools/parser", rev = "ec4cb411dec09450946ef57920b7ffced7f6495d" }
cron = "^0"
+1 -1
View File
@@ -1 +1 @@
35402ea9fc459245d8b734b50ed98923e3061514
7cc103e5827b1ec91180a5939a9c022a2a1856c6
@@ -0,0 +1 @@
ALTER TABLE cloud_workspace_settings DROP COLUMN last_warning_sent;
@@ -0,0 +1 @@
ALTER TABLE cloud_workspace_settings ADD COLUMN last_warning_sent TIMESTAMP NULL;
@@ -0,0 +1 @@
-- Add down migration script here
@@ -0,0 +1,2 @@
-- Add up migration script here
UPDATE config set config = jsonb_set(config, '{worker_tags}', config->'worker_tags' || '["oracledb"]'::jsonb) where name = 'worker__native' and config @> '{"worker_tags": ["nativets", "postgresql", "mysql", "graphql", "snowflake", "bigquery", "mssql"]}'::jsonb AND NOT config->'worker_tags' @> '"oracledb"'::jsonb;
@@ -0,0 +1 @@
ALTER TABLE websocket_trigger DROP COLUMN can_return_message;
@@ -0,0 +1 @@
ALTER TABLE websocket_trigger ADD COLUMN can_return_message BOOLEAN NOT NULL DEFAULT FALSE;
@@ -0,0 +1 @@
DELETE FROM global_settings WHERE name = 'otel';
@@ -0,0 +1 @@
INSERT INTO global_settings (name, value) VALUES ('otel', '{}');
@@ -18,6 +18,8 @@ regex.workspace = true
windmill-parser.workspace = true
windmill-common.workspace = true
rustpython-parser.workspace = true
malachite.workspace = true
malachite-bigint.workspace = true
phf.workspace = true
itertools.workspace = true
serde_json.workspace = true
@@ -37,6 +37,7 @@ static PYTHON_IMPORTS_REPLACEMENT: phf::Map<&'static str, &'static str> = phf_ma
"smb" => "pysmb",
"PIL" => "Pillow",
"googleapiclient" => "google-api-python-client",
"googlecloudbigquery" => "google-cloud-bigquery",
"dateutil" => "python-dateutil",
"mailparser" => "mail-parser",
"mailparser-reply" => "mail-parser-reply",
+95
View File
@@ -0,0 +1,95 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1737885589,
"narHash": "sha256-Zf0hSrtzaM1DEz8//+Xs51k/wdSajticVrATqDrfQjg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "852ff1d9e153d8875a83602e03fdef8a63f0ecf8",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-unstable",
"type": "indirect"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1736320768,
"narHash": "sha256-nIYdTAiKIGnFNugbomgBJR+Xv5F1ZQU+HfaBqJKroC0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4bc9c909d9ac828a039f288cf872d16d38185db8",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1738117527,
"narHash": "sha256-GFviGfaezjGLFUlxdv3zyC7rSZvTXqwcG/YsF6MDkOw=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "6a3dc6ce4132bd57359214d986db376f2333c14d",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
@@ -0,0 +1,37 @@
{
inputs = {
nixpkgs.url = "nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
rust-overlay.url = "github:oxalica/rust-overlay";
};
outputs = {
nixpkgs,
flake-utils,
rust-overlay,
...
}:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs {
inherit system;
overlays = [(import rust-overlay)];
};
rust = pkgs.rust-bin.nightly.latest.default.override {
extensions = [
"rust-src"
];
targets = ["wasm32-unknown-unknown"];
};
in {
devShell = pkgs.mkShell {
buildInputs = with pkgs; [
rust
nodejs
wasm-pack
sccache
];
RUSTC_WRAPPER = "${pkgs.sccache}/bin/sccache";
CARGO_PATH = "${rust}/bin/cargo";
};
});
}
+34 -4
View File
@@ -1506,9 +1506,20 @@ pub async fn reload_base_url_setting(db: &DB) -> error::Result<()> {
async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, worker_name: &str) {
if *RESTART_ZOMBIE_JOBS {
let restarted = sqlx::query!(
"UPDATE queue SET running = false, started_at = null
WHERE last_ping < now() - ($1 || ' seconds')::interval
AND running = true AND job_kind NOT IN ('flow', 'flowpreview', 'flownode', 'singlescriptflow') AND same_worker = false RETURNING id, workspace_id, last_ping",
"WITH zombie_jobs AS (
UPDATE queue SET running = false, started_at = null
WHERE last_ping < now() - ($1 || ' seconds')::interval
AND running = true AND job_kind NOT IN ('flow', 'flowpreview', 'flownode', 'singlescriptflow') AND same_worker = false
RETURNING id, workspace_id, last_ping
),
update_concurrency AS (
UPDATE concurrency_counter cc
SET job_uuids = job_uuids - zj.id::text
FROM zombie_jobs zj
INNER JOIN concurrency_key ck ON ck.job_id = zj.id
WHERE cc.concurrency_id = ck.key
)
SELECT id, workspace_id, last_ping FROM zombie_jobs",
*ZOMBIE_JOB_TIMEOUT,
)
.fetch_all(db)
@@ -1652,12 +1663,31 @@ async fn handle_zombie_flows(db: &DB) -> error::Result<()> {
tracing::error!(error_message);
report_critical_error(error_message, db.clone(), Some(&flow.workspace_id), None).await;
// if the flow hasn't started and is a zombie, we can simply restart it
let mut tx = db.begin().await?;
let concurrency_key =
sqlx::query_scalar!("SELECT key FROM concurrency_key WHERE job_id = $1", flow.id)
.fetch_optional(&mut *tx)
.await?;
if let Some(key) = concurrency_key {
sqlx::query!(
"UPDATE concurrency_counter SET job_uuids = job_uuids - $2 WHERE concurrency_id = $1",
key,
flow.id.hyphenated().to_string()
)
.execute(&mut *tx)
.await?;
}
sqlx::query!(
"UPDATE queue SET running = false, started_at = null WHERE id = $1 AND canceled = false",
flow.id
)
.execute(db)
.execute(&mut *tx)
.await?;
tx.commit().await?;
} else {
let id = flow.id.clone();
let last_ping = flow.last_ping.clone();
+34 -2
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.452.1
version: 1.455.1
title: Windmill API
contact:
@@ -7994,8 +7994,11 @@ paths:
type: string
url_runnable_args:
$ref: "#/components/schemas/ScriptArgs"
can_return_message:
type: boolean
required:
- url
- can_return_message
responses:
"200":
description: successfuly connected to websocket
@@ -9855,6 +9858,25 @@ paths:
$ref: "#/components/schemas/Capture"
/w/{workspace}/capture/{id}:
get:
summary: get a capture
operationId: getCapture
tags:
- capture
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: id
in: path
required: true
schema:
type: integer
responses:
"200":
description: capture
content:
application/json:
schema:
$ref: "#/components/schemas/Capture"
delete:
summary: delete a capture
operationId: deleteCapture
@@ -10123,6 +10145,8 @@ paths:
properties:
connection_settings_str:
type: string
azure_container_path:
type: string
required:
- connection_settings_str
@@ -13151,6 +13175,8 @@ components:
$ref: "#/components/schemas/WebsocketTriggerInitialMessage"
url_runnable_args:
$ref: "#/components/schemas/ScriptArgs"
can_return_message:
type: boolean
required:
- path
@@ -13164,6 +13190,7 @@ components:
- workspace_id
- enabled
- filters
- can_return_message
NewWebsocketTrigger:
type: object
@@ -13195,6 +13222,8 @@ components:
$ref: "#/components/schemas/WebsocketTriggerInitialMessage"
url_runnable_args:
$ref: "#/components/schemas/ScriptArgs"
can_return_message:
type: boolean
required:
- path
@@ -13202,6 +13231,7 @@ components:
- url
- is_flow
- filters
- can_return_message
EditWebsocketTrigger:
type: object
@@ -13231,6 +13261,8 @@ components:
$ref: "#/components/schemas/WebsocketTriggerInitialMessage"
url_runnable_args:
$ref: "#/components/schemas/ScriptArgs"
can_return_message:
type: boolean
required:
- path
@@ -13238,7 +13270,7 @@ components:
- url
- is_flow
- filters
- can_return_message
WebsocketTriggerInitialMessage:
anyOf:
- type: object
+20 -1
View File
@@ -56,6 +56,7 @@ pub fn workspaced_service() -> Router {
.route("/get_configs/:runnable_kind/*path", get(get_configs))
.route("/list/:runnable_kind/*path", get(list_captures))
.route("/:id", delete(delete_capture))
.route("/:id", get(get_capture))
}
pub fn workspaced_unauthed_service() -> Router {
@@ -280,7 +281,7 @@ async fn list_captures(
let captures = sqlx::query_as!(
Capture,
r#"SELECT id, created_at, trigger_kind as "trigger_kind: _", payload as "payload: _", trigger_extra as "trigger_extra: _"
r#"SELECT id, created_at, trigger_kind as "trigger_kind: _", CASE WHEN pg_column_size(payload) < 40000 THEN payload ELSE '"WINDMILL_TOO_BIG"'::jsonb END as "payload!: _", trigger_extra as "trigger_extra: _"
FROM capture
WHERE workspace_id = $1
AND path = $2 AND is_flow = $3
@@ -303,6 +304,24 @@ async fn list_captures(
Ok(Json(captures))
}
async fn get_capture(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, id)): Path<(String, i64)>,
) -> JsonResult<Capture> {
let mut tx = user_db.begin(&authed).await?;
let capture = sqlx::query_as!(
Capture,
r#"SELECT id, created_at, trigger_kind as "trigger_kind: _", payload as "payload!: _", trigger_extra as "trigger_extra: _" FROM capture WHERE id = $1 AND workspace_id = $2"#,
id,
&w_id,
)
.fetch_one(&mut *tx)
.await?;
tx.commit().await?;
Ok(Json(capture))
}
async fn delete_capture(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
+123 -99
View File
@@ -1968,20 +1968,34 @@ async fn resume_suspended_job_internal(
resume_immediately_if_relevant(parent_flow_info, job_id, &mut tx).await?;
}
let approver = approver.unwrap_or_else(|| "anonymous".to_string());
let audit_author = match authed {
Some(authed) => (&authed).into(),
None => {
let approver = approver.unwrap_or_else(|| "anonymous".to_string());
AuditAuthor { email: approver.clone(), username: approver, username_override: None }
}
None => AuditAuthor {
email: approver.clone(),
username: approver.clone(),
username_override: None,
},
};
audit_log(
&mut *tx,
&audit_author,
"jobs.approved",
"jobs.suspend_resume",
ActionKind::Update,
&w_id,
Some(&serde_json::json!({"approved": approved, "job_id": job_id}).to_string()),
Some(
&serde_json::json!({
"approved": approved,
"job_id": job_id,
"details": if approved {
format!("Approved by {}", &approver)
} else {
format!("Cancelled by {}", &approver)
}
})
.to_string(),
),
None,
)
.await?;
@@ -3527,13 +3541,14 @@ pub struct WindmillCompositeResult {
windmill_headers: Option<HashMap<String, String>>,
result: Option<Box<RawValue>>,
}
pub async fn run_wait_result(
pub async fn run_wait_result_internal(
db: &DB,
uuid: Uuid,
w_id: String,
node_id_for_empty_return: Option<String>,
username: &str,
) -> error::Result<Response> {
) -> error::Result<(Box<RawValue>, bool)> {
let mut result = None;
let mut success = false;
let timeout = TIMEOUT_WAIT_RESULT.read().await.clone().unwrap_or(600);
@@ -3605,104 +3620,113 @@ pub async fn run_wait_result(
};
tokio::time::sleep(core::time::Duration::from_millis(delay)).await;
}
if let Some(result) = result {
g.done = true;
let composite_result = serde_json::from_str::<WindmillCompositeResult>(result.get());
match composite_result {
Ok(WindmillCompositeResult {
windmill_status_code,
windmill_content_type,
windmill_headers,
result: result_value,
}) => {
if windmill_content_type.is_none()
&& windmill_status_code.is_none()
&& windmill_headers.is_none()
{
return Ok((
if success {
StatusCode::OK
} else {
StatusCode::INTERNAL_SERVER_ERROR
},
Json(result),
)
.into_response());
}
let status_code_or_default = windmill_status_code
.map(|val| match StatusCode::from_u16(val) {
Ok(sc) => Ok(sc),
Err(_) => Err(Error::ExecutionErr("Invalid status code".to_string())),
})
.unwrap_or_else(|| {
if !success {
Ok(StatusCode::INTERNAL_SERVER_ERROR)
} else if result_value.is_some() {
Ok(StatusCode::OK)
} else {
Ok(StatusCode::NO_CONTENT)
}
})?;
let mut headers = HeaderMap::new();
if let Some(windmill_headers) = windmill_headers {
for (k, v) in windmill_headers {
let k = HeaderName::from_str(k.as_str()).map_err(|err| {
Error::InternalErr(format!("Invalid header name {k}: {err}"))
})?;
let v = HeaderValue::from_str(v.as_str()).map_err(|err| {
Error::InternalErr(format!("Invalid header value {v}: {err}"))
})?;
headers.insert(k, v);
}
}
if let Some(content_type) = windmill_content_type {
let serialized_json_result = result_value
.map(|val| val.get().to_owned())
.unwrap_or_else(String::new);
// if the `result` was just a single string, the below removes the surrounding quotes by parsing it as a string.
// it falls back to the original serialized JSON if it doesn't work.
let serialized_result =
serde_json::from_str::<String>(serialized_json_result.as_str())
.ok()
.unwrap_or(serialized_json_result);
headers.insert(
http::header::CONTENT_TYPE,
HeaderValue::from_str(content_type.as_str()).map_err(|err| {
Error::InternalErr(format!(
"Invalid content type {content_type}: {err}"
))
})?,
);
return Ok((status_code_or_default, headers, serialized_result).into_response());
}
if let Some(result_value) = result_value {
return Ok(
(status_code_or_default, headers, Json(result_value)).into_response()
);
} else {
Ok((status_code_or_default, headers).into_response())
}
}
_ => Ok((
if success {
StatusCode::OK
} else {
StatusCode::INTERNAL_SERVER_ERROR
},
Json(result),
)
.into_response()),
}
Ok((result, success))
} else {
Err(Error::ExecutionErr(format!("timeout after {}s", timeout)))
}
}
pub async fn run_wait_result(
db: &DB,
uuid: Uuid,
w_id: String,
node_id_for_empty_return: Option<String>,
username: &str,
) -> error::Result<Response> {
let (result, success) =
run_wait_result_internal(db, uuid, w_id, node_id_for_empty_return, username).await?;
let composite_result = serde_json::from_str::<WindmillCompositeResult>(result.get());
match composite_result {
Ok(WindmillCompositeResult {
windmill_status_code,
windmill_content_type,
windmill_headers,
result: result_value,
}) => {
if windmill_content_type.is_none()
&& windmill_status_code.is_none()
&& windmill_headers.is_none()
{
return Ok((
if success {
StatusCode::OK
} else {
StatusCode::INTERNAL_SERVER_ERROR
},
Json(result),
)
.into_response());
}
let status_code_or_default = windmill_status_code
.map(|val| match StatusCode::from_u16(val) {
Ok(sc) => Ok(sc),
Err(_) => Err(Error::ExecutionErr("Invalid status code".to_string())),
})
.unwrap_or_else(|| {
if !success {
Ok(StatusCode::INTERNAL_SERVER_ERROR)
} else if result_value.is_some() {
Ok(StatusCode::OK)
} else {
Ok(StatusCode::NO_CONTENT)
}
})?;
let mut headers = HeaderMap::new();
if let Some(windmill_headers) = windmill_headers {
for (k, v) in windmill_headers {
let k = HeaderName::from_str(k.as_str()).map_err(|err| {
Error::InternalErr(format!("Invalid header name {k}: {err}"))
})?;
let v = HeaderValue::from_str(v.as_str()).map_err(|err| {
Error::InternalErr(format!("Invalid header value {v}: {err}"))
})?;
headers.insert(k, v);
}
}
if let Some(content_type) = windmill_content_type {
let serialized_json_result = result_value
.map(|val| val.get().to_owned())
.unwrap_or_else(String::new);
// if the `result` was just a single string, the below removes the surrounding quotes by parsing it as a string.
// it falls back to the original serialized JSON if it doesn't work.
let serialized_result =
serde_json::from_str::<String>(serialized_json_result.as_str())
.ok()
.unwrap_or(serialized_json_result);
headers.insert(
http::header::CONTENT_TYPE,
HeaderValue::from_str(content_type.as_str()).map_err(|err| {
Error::InternalErr(format!("Invalid content type {content_type}: {err}"))
})?,
);
return Ok((status_code_or_default, headers, serialized_result).into_response());
}
if let Some(result_value) = result_value {
return Ok((status_code_or_default, headers, Json(result_value)).into_response());
} else {
Ok((status_code_or_default, headers).into_response())
}
}
_ => Ok((
if success {
StatusCode::OK
} else {
StatusCode::INTERNAL_SERVER_ERROR
},
Json(result),
)
.into_response()),
}
}
async fn delete_job_metadata_after_use(db: &DB, job_uuid: Uuid) -> Result<(), Error> {
sqlx::query!(
"UPDATE completed_job
+2 -2
View File
@@ -23,7 +23,7 @@ use windmill_common::{
error::{self, Error},
jobs::JobKind,
scripts::ScriptHash,
variables::{build_crypt, decrypt_value_with_mc},
variables::{build_crypt, decrypt},
};
#[derive(Deserialize, Debug)]
@@ -853,7 +853,7 @@ async fn get_slack_token(db: &DB, slack_resource_path: &str, w_id: &str) -> anyh
if slack_token.is_secret {
let mc = build_crypt(&db, w_id).await?;
let bot_token = decrypt_value_with_mc(slack_token.value, mc).await?;
let bot_token = decrypt(&mc, slack_token.value)?;
Ok(bot_token)
} else {
Ok(slack_token.value)
+1
View File
@@ -1718,6 +1718,7 @@ pub async fn create_session_token<'c>(
if COOKIE_DOMAIN.is_some() {
cookie.set_domain(COOKIE_DOMAIN.clone().unwrap());
}
let mut expire: OffsetDateTime = time::OffsetDateTime::now_utc();
expire += time::Duration::days(3);
cookie.set_expires(expire);
+318 -224
View File
@@ -33,7 +33,9 @@ use windmill_queue::PushArgsOwned;
use crate::{
capture::{insert_capture_payload, TriggerKind, WebsocketTriggerConfig},
db::{ApiAuthed, DB},
jobs::{run_flow_by_path_inner, run_script_by_path_inner, RunJobQuery},
jobs::{
run_flow_by_path_inner, run_script_by_path_inner, run_wait_result_internal, RunJobQuery,
},
users::fetch_api_authed,
};
@@ -61,6 +63,7 @@ struct NewWebsocketTrigger {
filters: Vec<Box<RawValue>>,
initial_messages: Option<Vec<Box<RawValue>>>,
url_runnable_args: Option<Box<RawValue>>,
can_return_message: bool,
}
#[derive(Deserialize)]
@@ -101,6 +104,7 @@ pub struct WebsocketTrigger {
filters: Vec<SqlxJson<Box<RawValue>>>,
initial_messages: Option<Vec<SqlxJson<Box<RawValue>>>>,
url_runnable_args: Option<SqlxJson<Box<RawValue>>>,
can_return_message: bool,
}
#[derive(Deserialize)]
@@ -112,6 +116,7 @@ struct EditWebsocketTrigger {
filters: Vec<Box<RawValue>>,
initial_messages: Option<Vec<Box<RawValue>>>,
url_runnable_args: Option<Box<RawValue>>,
can_return_message: bool,
}
#[derive(Deserialize)]
@@ -189,7 +194,7 @@ async fn create_websocket_trigger(
) -> error::Result<(StatusCode, String)> {
if *CLOUD_HOSTED {
return Err(error::Error::BadRequest(
"Websocket triggers are not supported on multi-tenant cloud, use dedicated cloud or self-host".to_string(),
"WebSocket triggers are not supported on multi-tenant cloud, use dedicated cloud or self-host".to_string(),
));
}
@@ -203,7 +208,7 @@ async fn create_websocket_trigger(
.map(SqlxJson)
.collect_vec();
sqlx::query_as::<_, WebsocketTrigger>(
"INSERT INTO websocket_trigger (workspace_id, path, url, script_path, is_flow, enabled, filters, initial_messages, url_runnable_args, edited_by, email, edited_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, now()) RETURNING *",
"INSERT INTO websocket_trigger (workspace_id, path, url, script_path, is_flow, enabled, filters, initial_messages, url_runnable_args, edited_by, can_return_message, email, edited_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, now()) RETURNING *",
)
.bind(&w_id)
.bind(&ct.path)
@@ -215,6 +220,7 @@ async fn create_websocket_trigger(
.bind(initial_messages.as_slice())
.bind(ct.url_runnable_args.map(SqlxJson))
.bind(&authed.username)
.bind(ct.can_return_message)
.bind(&authed.email)
.fetch_one(&mut *tx).await?;
@@ -253,8 +259,8 @@ async fn update_websocket_trigger(
// important to update server_id to NULL to stop current websocket listener
sqlx::query!(
"UPDATE websocket_trigger SET url = $1, script_path = $2, path = $3, is_flow = $4, filters = $5, initial_messages = $6, url_runnable_args = $7, edited_by = $8, email = $9, edited_at = now(), server_id = NULL, error = NULL
WHERE workspace_id = $10 AND path = $11",
"UPDATE websocket_trigger SET url = $1, script_path = $2, path = $3, is_flow = $4, filters = $5, initial_messages = $6, url_runnable_args = $7, edited_by = $8, email = $9, can_return_message = $10, edited_at = now(), server_id = NULL, error = NULL
WHERE workspace_id = $11 AND path = $12",
ct.url,
ct.script_path,
ct.path,
@@ -264,6 +270,7 @@ async fn update_websocket_trigger(
ct.url_runnable_args.map(SqlxJson) as Option<SqlxJson<Box<RawValue>>>,
&authed.username,
&authed.email,
ct.can_return_message,
w_id,
path,
)
@@ -310,7 +317,7 @@ pub async fn set_enabled(
w_id,
).fetch_optional(&mut *tx).await?;
not_found_if_none(one_o.flatten(), "Websocket trigger", path)?;
not_found_if_none(one_o.flatten(), "WebSocket trigger", path)?;
audit_log(
&mut *tx,
@@ -326,7 +333,7 @@ pub async fn set_enabled(
tx.commit().await?;
Ok(format!(
"succesfully updated websocket trigger at path {} to status {}",
"succesfully updated WebSocket trigger at path {} to status {}",
path, payload.enabled
))
}
@@ -359,7 +366,7 @@ async fn delete_websocket_trigger(
tx.commit().await?;
Ok(format!("Websocket trigger {path} deleted"))
Ok(format!("WebSocket trigger {path} deleted"))
}
async fn exists_websocket_trigger(
@@ -409,7 +416,7 @@ async fn test_websocket_connection(
)
} else {
return Err(error::Error::BadConfig(format!(
"Invalid websocket runnable path: {}",
"Invalid WebSocket runnable path: {}",
url
)));
}
@@ -419,7 +426,7 @@ async fn test_websocket_connection(
connect_async(connect_url.as_ref()).await.map_err(|err| {
error::Error::BadConfig(format!(
"Error connecting to websocket: {}",
"Error connecting to WebSocket: {}",
err.to_string()
))
})?;
@@ -430,7 +437,7 @@ async fn test_websocket_connection(
tokio::time::timeout(tokio::time::Duration::from_secs(30), connect_f)
.await
.map_err(|_| {
error::Error::BadConfig(format!("Timeout connecting to websocket after 30 seconds"))
error::Error::BadConfig(format!("Timeout connecting to WebSocket after 30 seconds"))
})??;
Ok(())
@@ -455,7 +462,7 @@ async fn listen_to_unlistened_websockets(
}
}
Err(err) => {
tracing::error!("Error fetching websocket triggers: {:?}", err);
tracing::error!("Error fetching WebSocket triggers: {:?}", err);
}
};
@@ -473,7 +480,7 @@ async fn listen_to_unlistened_websockets(
}
}
Err(err) => {
tracing::error!("Error fetching capture websocket triggers: {:?}", err);
tracing::error!("Error fetching capture WebSocket triggers: {:?}", err);
}
}
}
@@ -561,16 +568,9 @@ where
deserializer.deserialize_map(SupersetVisitor { key, value_to_check })
}
async fn wait_runnable_result(
path: String,
is_flow: bool,
fn raw_value_to_args_hashmap(
args: Option<&Box<RawValue>>,
authed: ApiAuthed,
db: &DB,
workspace_id: &str,
) -> error::Result<String> {
let user_db = UserDB::new(db.clone());
) -> error::Result<HashMap<String, Box<RawValue>>> {
let args = if let Some(args) = args {
serde_json::from_str::<Option<HashMap<String, Box<RawValue>>>>(args.get())
.map_err(|e| error::Error::BadRequest(format!("invalid json: {}", e)))?
@@ -578,84 +578,89 @@ async fn wait_runnable_result(
} else {
HashMap::new()
};
Ok(args)
}
let (_, job_id) = if is_flow {
run_flow_by_path_inner(
async fn wait_runnable_result(
path: String,
is_flow: bool,
args: PushArgsOwned,
authed: ApiAuthed,
db: &DB,
workspace_id: &str,
) -> error::Result<String> {
let user_db = UserDB::new(db.clone());
let username = authed.display_username().to_owned();
let (job_id, early_return) = if is_flow {
let (_, job_id) = run_flow_by_path_inner(
authed,
db.clone(),
user_db,
workspace_id.to_string(),
StripPath(path.clone()),
RunJobQuery::default(),
PushArgsOwned { args, extra: None },
args,
None,
)
.await?;
let early_return = sqlx::query_scalar!(
r#"SELECT flow_version.value->>'early_return' as early_return
FROM flow
LEFT JOIN flow_version
ON flow_version.id = flow.versions[array_upper(flow.versions, 1)]
WHERE flow.path = $1 and flow.workspace_id = $2"#,
path,
workspace_id,
)
.fetch_optional(db)
.await?
.flatten();
(job_id, early_return)
} else {
run_script_by_path_inner(
let (_, job_id) = run_script_by_path_inner(
authed,
db.clone(),
user_db,
workspace_id.to_string(),
StripPath(path.clone()),
RunJobQuery::default(),
PushArgsOwned { args, extra: None },
args,
None,
)
.await?
.await?;
(job_id, None)
};
let start_time = tokio::time::Instant::now();
loop {
if start_time.elapsed() > tokio::time::Duration::from_secs(300) {
return Err(anyhow::anyhow!(
"Timed out after 5m waiting for {} {} to complete",
if is_flow { "flow" } else { "script" },
path
)
.into());
}
#[derive(sqlx::FromRow)]
struct RawResult {
result: Option<SqlxJson<Box<RawValue>>>,
success: bool,
}
let result = sqlx::query_as::<_, RawResult>(
"SELECT result, success FROM completed_job WHERE id = $1 AND workspace_id = $2",
let (result, success) = run_wait_result_internal(
db,
Uuid::parse_str(&job_id).unwrap(),
workspace_id.to_string(),
early_return,
&username,
)
.await
.with_context(|| {
format!(
"Error fetching job result for {} {}",
if is_flow { "flow" } else { "script" },
path
)
.bind(Uuid::parse_str(&job_id).unwrap())
.bind(workspace_id)
.fetch_optional(db)
.await;
})?;
match result {
Ok(Some(r)) => {
if !r.success {
return Err(anyhow::anyhow!(
"{} {path} failed: {:?}",
if is_flow { "Flow" } else { "Script" },
r.result
)
.into());
} else {
return Ok(r.result.map(|r| r.get().to_owned()).unwrap_or_default());
}
}
Ok(None) => {
// not yet done, wait for 5s and check again
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
}
Err(err) => {
return Err(anyhow::anyhow!(
"Error fetching job result for {} {path}: {err}",
if is_flow { "flow" } else { "script" },
)
.into());
}
}
if !success {
Err(anyhow::anyhow!(
"{} {path} failed: {:?}",
if is_flow { "Flow" } else { "Script" },
result
)
.into())
} else {
Ok(result.get().to_owned())
}
}
@@ -677,23 +682,30 @@ async fn get_url_from_runnable(
workspace_id: &str,
) -> error::Result<String> {
tracing::info!(
"Running {} {} to get websocket URL",
"Running {} {} to get WebSocket URL",
if is_flow { "flow" } else { "script" },
path
);
let result =
wait_runnable_result(path.to_string(), is_flow, args, authed, db, workspace_id).await?;
let args = raw_value_to_args_hashmap(args)?;
if result.starts_with("\"") && result.ends_with("\"") {
Ok(result[1..result.len() - 1].to_string())
} else {
Err(error::Error::BadConfig(format!(
let result = wait_runnable_result(
path.to_string(),
is_flow,
PushArgsOwned { args, extra: None },
authed,
db,
workspace_id,
)
.await?;
serde_json::from_str::<String>(result.as_str()).map_err(|_| {
error::Error::BadConfig(format!(
"{} {} did not return a string",
if is_flow { "Flow" } else { "Script" },
path
)))
}
path,
))
})
}
impl WebsocketTrigger {
@@ -712,11 +724,11 @@ impl WebsocketTrigger {
if has_lock.flatten().unwrap_or(false) {
tokio::spawn(listen_to_websocket(WebsocketEnum::Trigger(self), db, killpill_rx));
} else {
tracing::info!("Websocket {} already being listened to", self.url);
tracing::info!("WebSocket {} already being listened to", self.url);
}
},
Err(err) => {
tracing::error!("Error acquiring lock for websocket {}: {:?}", self.path, err);
tracing::error!("Error acquiring lock for WebSocket {}: {:?}", self.path, err);
}
};
}
@@ -737,12 +749,12 @@ impl WebsocketTrigger {
self.workspace_id,
self.path,
).execute(db).await.ok();
tracing::info!("Websocket {} changed, disabled, or deleted, stopping...", self.url);
tracing::info!("WebSocket {} changed, disabled, or deleted, stopping...", self.url);
return None;
}
},
Err(err) => {
tracing::warn!("Error updating ping of websocket {}: {:?}", self.url, err);
tracing::warn!("Error updating ping of WebSocket {}: {:?}", self.url, err);
}
};
@@ -758,11 +770,11 @@ impl WebsocketTrigger {
)
.execute(db).await {
Ok(_) => {
report_critical_error(format!("Disabling websocket {} because of error: {}", self.url, error), db.clone(), Some(&self.workspace_id), None).await;
report_critical_error(format!("Disabling WebSocket {} because of error: {}", self.url, error), db.clone(), Some(&self.workspace_id), None).await;
},
Err(disable_err) => {
report_critical_error(
format!("Could not disable websocket {} with err {}, disabling because of error {}", self.path, disable_err, error),
format!("Could not disable WebSocket {} with err {}, disabling because of error {}", self.path, disable_err, error),
db.clone(),
Some(&self.workspace_id),
None,
@@ -790,7 +802,7 @@ impl WebsocketTrigger {
async fn send_initial_messages(
&self,
mut writer: SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>,
writer: &mut SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>,
db: &DB,
) -> error::Result<()> {
let initial_messages: Vec<InitialMessage> = self
@@ -810,7 +822,7 @@ impl WebsocketTrigger {
msg
};
tracing::info!(
"Sending raw message initial message to websocket {}: {}",
"Sending raw message initial message to WebSocket {}: {}",
self.url,
msg
);
@@ -822,16 +834,18 @@ impl WebsocketTrigger {
}
InitialMessage::RunnableResult { path, is_flow, args } => {
tracing::info!(
"Running {} {} for initial message to websocket {}",
"Running {} {} for initial message to WebSocket {}",
if is_flow { "flow" } else { "script" },
path,
self.url,
);
let args = raw_value_to_args_hashmap(Some(&args))?;
let result = wait_runnable_result(
path.clone(),
is_flow,
Some(&args),
PushArgsOwned { args, extra: None },
self.fetch_authed(db).await?,
db,
&self.workspace_id,
@@ -839,17 +853,15 @@ impl WebsocketTrigger {
.await?;
tracing::info!(
"Sending {} {} result to websocket {}",
"Sending {} {} result to WebSocket {}",
if is_flow { "flow" } else { "script" },
path,
self.url
);
let result = if result.starts_with("\"") && result.ends_with("\"") {
result[1..result.len() - 1].to_string()
} else {
result
};
// if the `result` was just a single string, the below removes the surrounding quotes by parsing it as a string.
// it falls back to the original serialized JSON if it doesn't work.
let result = serde_json::from_str::<String>(result.as_str()).unwrap_or(result);
writer
.send(tokio_tungstenite::tungstenite::Message::Text(result))
@@ -869,11 +881,16 @@ impl WebsocketTrigger {
Ok(())
}
async fn handle(&self, db: &DB, args: PushArgsOwned) -> () {
if let Err(err) = run_job(db, self, args).await {
async fn handle(
&self,
db: &DB,
args: PushArgsOwned,
return_message_channels: Option<ReturnMessageChannels>,
) -> () {
if let Err(err) = run_job(db, self, args, return_message_channels).await {
report_critical_error(
format!(
"Failed to trigger job from websocket {}: {:?}",
"Failed to trigger job from WebSocket {}: {:?}",
self.url, err
),
db.clone(),
@@ -923,11 +940,11 @@ impl CaptureConfigForWebsocket {
if has_lock.flatten().unwrap_or(false) {
tokio::spawn(listen_to_websocket(WebsocketEnum::Capture(self), db, killpill_rx));
} else {
tracing::info!("Websocket {} already being listened to", self.trigger_config.url);
tracing::info!("WebSocket {} already being listened to", self.trigger_config.url);
}
},
Err(err) => {
tracing::error!("Error acquiring lock for capture websocket {}: {:?}", self.path, err);
tracing::error!("Error acquiring lock for capture WebSocket {}: {:?}", self.path, err);
}
};
}
@@ -950,12 +967,12 @@ impl CaptureConfigForWebsocket {
self.path,
self.is_flow,
).execute(db).await.ok();
tracing::info!("Websocket capture {} changed, disabled, or deleted, stopping...", self.trigger_config.url);
tracing::info!("WebSocket capture {} changed, disabled, or deleted, stopping...", self.trigger_config.url);
return None;
}
},
Err(err) => {
tracing::warn!("Error updating ping of capture websocket {}: {:?}", self.trigger_config.url, err);
tracing::warn!("Error updating ping of capture WebSocket {}: {:?}", self.trigger_config.url, err);
}
};
@@ -1021,7 +1038,7 @@ impl CaptureConfigForWebsocket {
self.is_flow,
)
.execute(db).await {
tracing::error!("Could not disable websocket capture {} ({}) with err {}, disabling because of error {}", self.path, self.workspace_id, err, error);
tracing::error!("Could not disable WebSocket capture {} ({}) with err {}, disabling because of error {}", self.path, self.workspace_id, err, error);
}
}
@@ -1069,6 +1086,20 @@ impl WebsocketEnum {
}
}
struct ReturnMessageChannels {
send_message_tx: tokio::sync::mpsc::Sender<String>,
killpill_rx: tokio::sync::broadcast::Receiver<()>,
}
impl Clone for ReturnMessageChannels {
fn clone(&self) -> Self {
Self {
send_message_tx: self.send_message_tx.clone(),
killpill_rx: self.killpill_rx.resubscribe(),
}
}
}
async fn listen_to_websocket(
ws: WebsocketEnum,
db: DB,
@@ -1097,7 +1128,7 @@ async fn listen_to_websocket(
return;
},
_ = loop_ping(&db, &ws, Some(
"Waiting on runnable to return websocket URL..."
"Waiting on runnable to return WebSocket URL..."
)) => {
return;
},
@@ -1106,7 +1137,7 @@ async fn listen_to_websocket(
Ok(url) => Cow::Owned(url),
Err(err) => {
ws.disable_with_error(&db, format!(
"Error getting websocket URL from runnable after 5 tries: {:?}",
"Error getting WebSocket URL from runnable after 5 tries: {:?}",
err
),
)
@@ -1116,7 +1147,7 @@ async fn listen_to_websocket(
},
}
} else {
ws.disable_with_error(&db, format!("Invalid websocket runnable path: {}", url))
ws.disable_with_error(&db, format!("Invalid WebSocket runnable path: {}", url))
.await;
return;
}
@@ -1135,8 +1166,8 @@ async fn listen_to_websocket(
connection = connect_async(connect_url.as_ref()) => {
match connection {
Ok((ws_stream, _)) => {
tracing::info!("Connected to websocket {}", url);
let (writer, mut reader) = ws_stream.split();
tracing::info!("Connected to WebSocket {}", url);
let (mut writer, mut reader) = ws_stream.split();
// send initial messages
match &ws {
@@ -1149,110 +1180,134 @@ async fn listen_to_websocket(
_ = loop_ping(&db, &ws, Some("Sending initial messages...")) => {
return;
},
result = ws_trigger.send_initial_messages(writer, &db) => {
result = ws_trigger.send_initial_messages(&mut writer, &db) => {
if let Err(err) = result {
ws_trigger.disable_with_error(&db, format!("Error sending initial messages: {:?}", err)).await;
return
} else {
tracing::debug!("Initial messages sent successfully to websocket {}", url);
tracing::debug!("Initial messages sent successfully to WebSocket {}", url);
}
}
}
},
_ => {}
_ => {
}
}
loop {
tokio::select! {
biased;
_ = killpill_rx.recv() => {
return;
},
_ = loop_ping(&db, &ws, None) => {
return;
},
_ = async {
loop {
if let Some(msg) = reader.next().await {
match msg {
Ok(msg) => {
match msg {
tokio_tungstenite::tungstenite::Message::Text(text) => {
tracing::debug!("Received text message from websocket {}: {}", url, text);
let mut should_handle = true;
for filter in &filters {
match filter {
Filter::JsonFilter(JsonFilter { key, value }) => {
let mut deserializer = serde_json::Deserializer::from_str(text.as_str());
should_handle = match is_value_superset(&mut deserializer, key, &value) {
Ok(filter_match) => {
filter_match
},
Err(err) => {
tracing::warn!("Error deserializing filter for websocket {}: {:?}", url, err);
false
}
};
}
}
if !should_handle {
break;
}
}
if should_handle {
let args = HashMap::from([("msg".to_string(), to_raw_value(&text))]);
let extra = Some(HashMap::from([(
"wm_trigger".to_string(),
to_raw_value(&serde_json::json!({"kind": "websocket", "websocket": { "url": url }})),
)]));
let args = PushArgsOwned { args, extra };
match &ws {
WebsocketEnum::Trigger(ws_trigger) => {
ws_trigger.handle(&db, args).await;
},
WebsocketEnum::Capture(capture) => {
capture.handle(&db, args).await;
},
}
}
},
a @ _ => {
tracing::debug!("Received non text-message from websocket {}: {:?}", url, a);
}
}
},
Err(err) => {
tracing::error!("Error reading from websocket {}: {:?}", url, err);
}
}
} else {
tracing::error!("Websocket {} closed", url);
if let None = ws.update_ping(&db, Some("Websocket closed")).await {
return;
}
return;
let (return_message_channels, message_sender_handle) = match &ws {
WebsocketEnum::Trigger(ws_trigger) if ws_trigger.can_return_message => {
let (send_message_tx, mut rx) = tokio::sync::mpsc::channel::<String>(100);
let w_id = ws_trigger.workspace_id.clone();
let url = ws_trigger.url.clone();
let db = db.clone();
let handle = tokio::spawn(async move {
while let Some(message) = rx.recv().await {
if let Err(err) = writer.send(tokio_tungstenite::tungstenite::Message::Text(message)).await {
report_critical_error(format!("Could not send runnable result to WebSocket {} because of error: {}", url, err), db.clone(), Some(&w_id), None).await;
}
}
} => {
return;
});
let killpill_rx = killpill_rx.resubscribe();
let return_message_channels = ReturnMessageChannels {
send_message_tx,
killpill_rx
};
(Some(return_message_channels), Some(handle))
},
_ => (None, None)
};
tokio::select! {
biased;
_ = killpill_rx.recv() => {},
_ = loop_ping(&db, &ws, None) => {},
_ = async {
loop {
if let Some(msg) = reader.next().await {
match msg {
Ok(msg) => {
match msg {
tokio_tungstenite::tungstenite::Message::Text(text) => {
tracing::debug!("Received text message from WebSocket {}: {}", url, text);
let mut should_handle = true;
for filter in &filters {
match filter {
Filter::JsonFilter(JsonFilter { key, value }) => {
let mut deserializer = serde_json::Deserializer::from_str(text.as_str());
should_handle = match is_value_superset(&mut deserializer, key, &value) {
Ok(filter_match) => {
filter_match
},
Err(err) => {
tracing::warn!("Error deserializing filter for WebSocket {}: {:?}", url, err);
false
}
};
}
}
if !should_handle {
break;
}
}
if should_handle {
let args = HashMap::from([("msg".to_string(), to_raw_value(&text))]);
let extra = Some(HashMap::from([(
"wm_trigger".to_string(),
to_raw_value(&serde_json::json!({"kind": "websocket", "websocket": { "url": url }})),
)]));
let args = PushArgsOwned { args, extra };
match &ws {
WebsocketEnum::Trigger(ws_trigger) => {
ws_trigger.handle(&db, args, return_message_channels.clone()).await;
},
WebsocketEnum::Capture(capture) => {
capture.handle(&db, args).await;
},
}
}
},
a @ _ => {
tracing::debug!("Received non text-message from WebSocket {}: {:?}", url, a);
}
}
},
Err(err) => {
tracing::error!("Error reading from WebSocket {}: {:?}", url, err);
}
}
} else {
tracing::error!("WebSocket {} closed", url);
ws.update_ping(&db, Some("WebSocket closed")).await;
break;
}
}
}
} => {}
}
// make sure to stop return message handler
if let Some(message_sender_handle) = message_sender_handle {
message_sender_handle.abort();
}
}
Err(err) => {
tracing::error!("Error connecting to websocket {}: {:?}", url, err);
if let None = ws.update_ping(&db, Some(err.to_string().as_str())).await {
return;
}
tracing::error!("Error connecting to WebSocket {}: {:?}", url, err);
ws.update_ping(&db, Some(err.to_string().as_str())).await;
}
}
}
}
}
async fn run_job(db: &DB, trigger: &WebsocketTrigger, args: PushArgsOwned) -> anyhow::Result<()> {
async fn run_job(
db: &DB,
trigger: &WebsocketTrigger,
args: PushArgsOwned,
return_message_channels: Option<ReturnMessageChannels>,
) -> anyhow::Result<()> {
let authed = fetch_api_authed(
trigger.edited_by.clone(),
trigger.email.clone(),
@@ -1262,34 +1317,73 @@ async fn run_job(db: &DB, trigger: &WebsocketTrigger, args: PushArgsOwned) -> an
)
.await?;
let user_db = UserDB::new(db.clone());
if let Some(ReturnMessageChannels { send_message_tx, mut killpill_rx }) =
return_message_channels
{
let db_ = db.clone();
let url = trigger.url.clone();
let script_path = trigger.script_path.clone();
let is_flow = trigger.is_flow;
let w_id = trigger.workspace_id.clone();
let handle_response_f = async move {
tokio::select! {
_ = killpill_rx.recv() => {
return;
},
result = wait_runnable_result(
script_path,
is_flow,
args,
authed,
&db_,
&w_id,
) => {
if let Ok(result) = result {
// only send the result if it's not null
if result != "null" {
tracing::info!("Sending job result to WebSocket {}", url);
// if the `result` was just a single string, the below removes the surrounding quotes by parsing it as a string.
// it falls back to the original serialized JSON if it doesn't work.
let result = serde_json::from_str::<String>(result.as_str()).unwrap_or(result);
if let Err(err) = send_message_tx.send(result).await {
report_critical_error(format!("Could not send runnable result to WebSocket {} because of error: {}", url, err), db_.clone(), Some(&w_id), None).await;
}
}
}
}
};
};
let run_query = RunJobQuery::default();
if trigger.is_flow {
run_flow_by_path_inner(
authed,
db.clone(),
user_db,
trigger.workspace_id.clone(),
StripPath(trigger.script_path.to_owned()),
run_query,
args,
None,
)
.await?;
tokio::spawn(handle_response_f);
} else {
run_script_by_path_inner(
authed,
db.clone(),
user_db,
trigger.workspace_id.clone(),
StripPath(trigger.script_path.to_owned()),
run_query,
args,
None,
)
.await?;
let user_db = UserDB::new(db.clone());
let run_query = RunJobQuery::default();
let runnable_path = StripPath(trigger.script_path.to_owned());
if trigger.is_flow {
run_flow_by_path_inner(
authed,
db.clone(),
user_db,
trigger.workspace_id.clone(),
runnable_path,
run_query,
args,
None,
)
.await?;
} else {
run_script_by_path_inner(
authed,
db.clone(),
user_db,
trigger.workspace_id.clone(),
runnable_path,
run_query,
args,
None,
)
.await?;
}
}
Ok(())
+11 -2
View File
@@ -529,7 +529,7 @@ pub mod script {
lock AS \"lock: String\", \
language AS \"language: Option<ScriptLang>\", \
envs AS \"envs: Vec<String>\", \
codebase AS \"codebase: String\" \
codebase LIKE '%.tar' as use_tar \
FROM script WHERE hash = $1 LIMIT 1",
hash.0
)
@@ -543,7 +543,16 @@ pub mod script {
meta: Some(ScriptMetadata {
language: r.language,
envs: r.envs,
codebase: r.codebase,
codebase: if let Some(use_tar) = r.use_tar {
let sh = hash.to_string();
if use_tar {
Some(format!("{sh}.tar"))
} else {
Some(sh)
}
} else {
None
},
}),
})
});
+2 -13
View File
@@ -6,12 +6,11 @@
* LICENSE-AGPL for a copy of the license.
*/
use crate::error::Result;
use crate::error;
use crate::{worker::WORKER_GROUP, BASE_URL, DB};
use chrono::{SecondsFormat, Utc};
use magic_crypt::{MagicCrypt256, MagicCryptError, MagicCryptTrait};
use serde::{Deserialize, Serialize};
use crate::error;
lazy_static::lazy_static! {
pub static ref SECRET_SALT: Option<String> = std::env::var("SECRET_SALT").ok();
@@ -134,7 +133,7 @@ pub async fn get_secret_value_as_admin(
let value = variable.value;
if !value.is_empty() {
let mc = build_crypt(db, w_id).await?;
decrypt_value_with_mc(value, mc).await?
decrypt(&mc, value)?
} else {
"".to_string()
}
@@ -145,16 +144,6 @@ pub async fn get_secret_value_as_admin(
Ok(r)
}
pub async fn decrypt_value_with_mc(value: String, mc: MagicCrypt256) -> Result<String> {
mc.decrypt_base64_to_string(value).map_err(|e| match e {
MagicCryptError::DecryptError(_) => crate::error::Error::InternalErr(
"Could not decrypt value. The value may have been encrypted with a different key."
.to_string(),
),
_ => crate::error::Error::InternalErr(e.to_string()),
})
}
pub fn encrypt(mc: &MagicCrypt256, value: &str) -> String {
mc.encrypt_str_to_base64(value)
}
@@ -123,6 +123,13 @@ mount {
is_bind: true
}
mount {
src: "{GLOBAL_SITE_PACKAGES}"
dst: "{GLOBAL_SITE_PACKAGES}"
is_bind: true
mandatory: false
}
{SHARED_MOUNT}
{SHARED_DEPENDENCIES}
+4 -6
View File
@@ -18,7 +18,7 @@ use windmill_common::jobs::ENTRYPOINT_OVERRIDE;
use windmill_common::s3_helpers::{
get_etag_or_empty, LargeFileStorage, ObjectStoreResource, S3Object,
};
use windmill_common::variables::{build_crypt_with_key_suffix, decrypt_value_with_mc};
use windmill_common::variables::{build_crypt_with_key_suffix, decrypt};
use windmill_common::worker::{
to_raw_value, write_file, CLOUD_HOSTED, ROOT_CACHE_DIR, WORKER_CONFIG,
};
@@ -255,11 +255,9 @@ pub async fn transform_json_value(
let encrypted = y.strip_prefix("$encrypted:").unwrap();
let mc =
build_crypt_with_key_suffix(&db, &job.workspace_id, &job.id.to_string()).await?;
decrypt_value_with_mc(encrypted.to_string(), mc)
.await
.and_then(|x| {
serde_json::from_str(&x).map_err(|e| Error::InternalErr(e.to_string()))
})
decrypt(&mc, encrypted.to_string()).and_then(|x| {
serde_json::from_str(&x).map_err(|e| Error::InternalErr(e.to_string()))
})
// let path = y.strip_prefix("$res:").unwrap();
}
+64 -25
View File
@@ -36,36 +36,38 @@ use windmill_common::{
use windmill_common::variables::get_secret_value_as_admin;
use windmill_queue::{append_logs, CanceledBy};
use std::env::var;
lazy_static::lazy_static! {
static ref PYTHON_PATH: String =
std::env::var("PYTHON_PATH").unwrap_or_else(|_| "/usr/local/bin/python3".to_string());
var("PYTHON_PATH").unwrap_or_else(|_| "/usr/local/bin/python3".to_string());
static ref UV_PATH: String =
std::env::var("UV_PATH").unwrap_or_else(|_| "/usr/local/bin/uv".to_string());
var("UV_PATH").unwrap_or_else(|_| "/usr/local/bin/uv".to_string());
static ref PY_CONCURRENT_DOWNLOADS: usize =
std::env::var("PY_CONCURRENT_DOWNLOADS").ok().map(|flag| flag.parse().unwrap_or(20)).unwrap_or(20);
var("PY_CONCURRENT_DOWNLOADS").ok().map(|flag| flag.parse().unwrap_or(20)).unwrap_or(20);
static ref FLOCK_PATH: String =
std::env::var("FLOCK_PATH").unwrap_or_else(|_| "/usr/bin/flock".to_string());
var("FLOCK_PATH").unwrap_or_else(|_| "/usr/bin/flock".to_string());
static ref NON_ALPHANUM_CHAR: Regex = regex::Regex::new(r"[^0-9A-Za-z=.-]").unwrap();
static ref PIP_TRUSTED_HOST: Option<String> = std::env::var("PIP_TRUSTED_HOST").ok();
static ref PIP_INDEX_CERT: Option<String> = std::env::var("PIP_INDEX_CERT").ok();
static ref TRUSTED_HOST: Option<String> = var("PY_TRUSTED_HOST").ok().or(var("PIP_TRUSTED_HOST").ok());
static ref INDEX_CERT: Option<String> = var("PY_INDEX_CERT").ok().or(var("PIP_INDEX_CERT").ok());
static ref NATIVE_CERT: bool = var("PY_NATIVE_CERT").ok().or(var("UV_NATIVE_TLS").ok()).map(|flag| flag == "true").unwrap_or(false);
pub static ref USE_SYSTEM_PYTHON: bool = std::env::var("USE_SYSTEM_PYTHON")
pub static ref USE_SYSTEM_PYTHON: bool = var("USE_SYSTEM_PYTHON")
.ok().map(|flag| flag == "true").unwrap_or(false);
pub static ref USE_PIP_COMPILE: bool = std::env::var("USE_PIP_COMPILE")
pub static ref USE_PIP_COMPILE: bool = var("USE_PIP_COMPILE")
.ok().map(|flag| flag == "true").unwrap_or(false);
pub static ref USE_PIP_INSTALL: bool = std::env::var("USE_PIP_INSTALL")
pub static ref USE_PIP_INSTALL: bool = var("USE_PIP_INSTALL")
.ok().map(|flag| flag == "true").unwrap_or(false);
static ref RELATIVE_IMPORT_REGEX: Regex = Regex::new(r#"(import|from)\s(((u|f)\.)|\.)"#).unwrap();
static ref EPHEMERAL_TOKEN_CMD: Option<String> = std::env::var("EPHEMERAL_TOKEN_CMD").ok();
static ref EPHEMERAL_TOKEN_CMD: Option<String> = var("EPHEMERAL_TOKEN_CMD").ok();
}
const NSJAIL_CONFIG_DOWNLOAD_PY_CONTENT: &str = include_str!("../nsjail/download.py.config.proto");
@@ -532,10 +534,10 @@ pub async fn uv_pip_compile(
args.extend(["--index-url", url, "--no-emit-index-url"]);
pip_args.push(format!("--index-url {}", url));
}
if let Some(host) = PIP_TRUSTED_HOST.as_ref() {
if let Some(host) = TRUSTED_HOST.as_ref() {
args.extend(["--trusted-host", host]);
}
if let Some(cert_path) = PIP_INDEX_CERT.as_ref() {
if let Some(cert_path) = INDEX_CERT.as_ref() {
args.extend(["--cert", cert_path]);
}
let pip_args_str = pip_args.join(" ");
@@ -621,12 +623,15 @@ pub async fn uv_pip_compile(
if let Some(url) = pip_index_url.as_ref() {
args.extend(["--index-url", url]);
}
if let Some(host) = PIP_TRUSTED_HOST.as_ref() {
if let Some(host) = TRUSTED_HOST.as_ref() {
args.extend(["--trusted-host", host]);
}
if let Some(cert_path) = PIP_INDEX_CERT.as_ref() {
if let Some(cert_path) = INDEX_CERT.as_ref() {
args.extend(["--cert", cert_path]);
}
if *NATIVE_CERT {
args.extend(["--native-tls"]);
}
tracing::error!("uv args: {:?}", args);
#[cfg(windows)]
@@ -897,6 +902,8 @@ pub async fn handle_python_job(
tracing::debug!("Finished deps postinstall stage");
}
if no_uv {
append_logs(
&job.id,
@@ -1045,7 +1052,27 @@ except BaseException as e:
let client = client.get_authed().await;
let mut reserved_variables = get_reserved_variables(job, &client.token, db).await?;
let additional_python_paths_folders = additional_python_paths.iter().join(":");
// Add /tmp/windmill/cache/python_xyz/global-site-packages to PYTHONPATH.
// Usefull if certain wheels needs to be preinstalled before execution.
let global_site_packages_path = py_version.to_cache_dir() + "/global-site-packages";
let additional_python_paths_folders = {
let mut paths= additional_python_paths.clone();
if std::fs::metadata(&global_site_packages_path).is_ok() {
// We want global_site_packages_path to be included in additonal_python_paths_folders, but
// we don't want it to be included in global_site_packages_path.
// The reason for this is that additional_python_paths_folders is used to fill PYTHONPATH env variable for jailed script
// When global_site_packages_path used to place mount point of wheels to the jail config.
// Since we handle mount of global_site_packages on our own, we don't want it to be mounted automatically.
// We do this because existence of every wheel in cache is mandatory and if it is not there and nsjail expects it, it is a bug.
// On the other side global_site_packages is purely optional.
// NOTE: This behaviour can be changed in future, so verification of wheels can be offloaded from nsjail to windmill
paths.insert(0, global_site_packages_path.clone());
// ^^^^^^^^
// We also want this be priorotized, that's why we insert it to the beginning
}
paths.iter().join(":")
};
#[cfg(windows)]
let additional_python_paths_folders = additional_python_paths_folders.replace(":", ";");
@@ -1075,6 +1102,7 @@ mount {{
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{SHARED_MOUNT}", shared_mount)
.replace("{SHARED_DEPENDENCIES}", shared_deps.as_str())
.replace("{GLOBAL_SITE_PACKAGES}", &global_site_packages_path)
.replace("{MAIN}", format!("{dirs}/{last}").as_str())
.replace(
"{ADDITIONAL_PYTHON_PATHS}",
@@ -1572,12 +1600,15 @@ async fn spawn_uv_install(
if let Some(url) = pip_index_url.as_ref() {
vars.push(("INDEX_URL", url));
}
if let Some(cert_path) = PIP_INDEX_CERT.as_ref() {
vars.push(("PIP_INDEX_CERT", cert_path));
if let Some(cert_path) = INDEX_CERT.as_ref() {
vars.push(("SSL_CERT_FILE", cert_path));
}
if let Some(host) = PIP_TRUSTED_HOST.as_ref() {
if let Some(host) = TRUSTED_HOST.as_ref() {
vars.push(("TRUSTED_HOST", host));
}
if *NATIVE_CERT {
vars.push(("UV_NATIVE_TLS", "true"));
}
let _owner;
if let Some(py_path) = py_path.as_ref() {
_owner = format!(
@@ -1674,18 +1705,26 @@ async fn spawn_uv_install(
});
}
let mut envs = vec![("PATH", PATH_ENV.as_str())];
envs.push(("HOME", HOME_ENV.as_str()));
if let Some(url) = pip_index_url.as_ref() {
command_args.extend(["--index-url", url]);
}
if let Some(cert_path) = PIP_INDEX_CERT.as_ref() {
command_args.extend(["--cert", cert_path]);
}
if let Some(host) = PIP_TRUSTED_HOST.as_ref() {
if let Some(host) = TRUSTED_HOST.as_ref() {
command_args.extend(["--trusted-host", &host]);
}
let mut envs = vec![("PATH", PATH_ENV.as_str())];
envs.push(("HOME", HOME_ENV.as_str()));
if *NATIVE_CERT {
command_args.extend(["--native-tls"]);
}
// TODO:
// Track https://github.com/astral-sh/uv/issues/6715
if let Some(cert_path) = INDEX_CERT.as_ref() {
// Once merged --cert can be used instead
//
// command_args.extend(["--cert", cert_path]);
envs.push(("SSL_CERT_FILE", cert_path));
}
tracing::debug!("uv pip install command: {:?}", command_args);
@@ -62,6 +62,8 @@ use windmill_queue::{
type DB = sqlx::Pool<sqlx::Postgres>;
use windmill_audit::audit_ee::{audit_log, AuditAuthor};
use windmill_audit::ActionKind;
use windmill_queue::{canceled_job_to_result, push};
// #[instrument(level = "trace", skip_all)]
@@ -1894,6 +1896,12 @@ async fn push_next_flow_job(
))
&& suspend.continue_on_disapprove_timeout.unwrap_or(false);
let audit_author = AuditAuthor {
username: flow_job.permissioned_as.trim_start_matches("u/").to_string(),
email: flow_job.email.clone(),
username_override: None,
};
if can_be_resumed || disapproved_or_timeout_but_continue {
if disapproved_or_timeout_but_continue {
let js = if let Some(disapproved) = is_disapproved.as_ref() {
@@ -1903,7 +1911,18 @@ async fn push_next_flow_job(
};
resume_messages.push(to_raw_value(&js));
audit_log(
&mut *tx,
&audit_author,
"jobs.suspend_resume",
ActionKind::Update,
&flow_job.workspace_id,
Some(&serde_json::json!({"approved": false, "job_id": flow_job.id, "details": "Suspend timed out without approval but can continue".to_string()}).to_string()),
None,
)
.await?;
}
sqlx::query(
"UPDATE queue
SET flow_status = JSONB_SET(flow_status, ARRAY['modules', $1::TEXT, 'approvers'], $2)
@@ -1971,6 +1990,18 @@ async fn push_next_flow_job(
/* cancelled or we're WaitingForEvents but we don't have enough messages (timed out) */
} else {
if is_disapproved.is_none() {
audit_log(
&mut *tx,
&audit_author,
"jobs.suspend_resume",
ActionKind::Update,
&flow_job.workspace_id,
Some(&serde_json::json!({"approved": false, "job_id": flow_job.id, "details": "Suspend timed out without approval and is cancelled".to_string()}).to_string()),
None,
)
.await?;
}
tx.commit().await?;
let (logs, error_name) = if let Some(disapprover) = is_disapproved {
+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.452.1";
export const VERSION = "v1.455.1";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({
+1
View File
@@ -22,6 +22,7 @@ export interface SyncOptions {
excludes?: string[];
defaultTs?: "bun" | "deno";
codebases?: Codebase[];
parallel?: number;
}
export interface Codebase {
+1 -1
View File
@@ -60,7 +60,7 @@ export {
// }
// });
export const VERSION = "1.452.1";
export const VERSION = "1.455.1";
const command = new Command()
.name("wmill")
+26 -23
View File
@@ -193,11 +193,12 @@ export async function handleFile(
bundleContent = execSync(
codebase.customBundler + " " + path
).toString();
log.info("Custom bundler executed");
log.info("Custom bundler executed for " + path);
} else {
const esbuild = await import("npm:esbuild");
log.info(`Starting building the bundle for ${path}`);
log.info(`Started bundling ${path} ...`);
const startTime = performance.now();
const out = await esbuild.build({
entryPoints: [path],
format: "cjs",
@@ -210,19 +211,20 @@ export async function handleFile(
packages: "bundle",
target: "node20.15.1",
});
const endTime = performance.now();
bundleContent = out.outputFiles[0].text;
log.info(
"Bundle size: " + (bundleContent.length / 1024).toFixed(0) + "kB"
`Finished bundling ${path}: ${(bundleContent.length / 1024).toFixed(0)}kB (${(endTime - startTime).toFixed(0)}ms)`
);
}
if (Array.isArray(codebase.assets) && codebase.assets.length > 0) {
const archiveNpm = await import("npm:@ayonli/jsext/archive");
log.info(
`Using the following asset configuration: ${JSON.stringify(
`Using the following asset configuration for ${path}: ${JSON.stringify(
codebase.assets
)}`
);
const startTime = performance.now();
const tarball = new archiveNpm.Tarball();
tarball.append(
new File([bundleContent], "main.js", { type: "text/plain" })
@@ -233,10 +235,10 @@ export async function handleFile(
const file = new File([blob], asset.to);
tarball.append(file);
}
log.info("Tarball size: " + (tarball.size / 1024).toFixed(0) + "kB");
const endTime = performance.now();
log.info(`Finished creating tarball for ${path}: ${(tarball.size / 1024).toFixed(0)}kB (${(endTime - startTime).toFixed(0)}ms)`);
bundleContent = tarball;
}
log.info(`Finished building the bundle for ${path}`);
}
let typed =
opts?.skipScriptsMetadata ? undefined :
@@ -364,24 +366,23 @@ export async function handleFile(
}
}
log.info(
colors.yellow.bold(`Creating script with a parent ${remotePath}`)
);
log.info(`Updating script ${remotePath} ...`);
const body = {
...requestBodyCommon,
parent_hash: remote.hash,
};
await createScript(bundleContent, workspaceId, body, workspace);
const execTime = await createScript(bundleContent, workspaceId, body, workspace);
log.info(colors.yellow.bold(`Updated script ${remotePath} (${execTime.toFixed(0)}ms)`));
} else {
log.info(
colors.yellow.bold(`Creating script without parent ${remotePath}`)
);
log.info(`Creating new script ${remotePath} ...`);
const body = {
...requestBodyCommon,
parent_hash: undefined,
};
await createScript(bundleContent, workspaceId, body, workspace);
const execTime = await createScript(bundleContent, workspaceId, body, workspace);
log.info(colors.yellow.bold(`Created new script ${remotePath} (${execTime.toFixed(0)}ms)`));
}
return true;
}
@@ -416,7 +417,8 @@ async function createScript(
workspaceId: string,
body: NewScript,
workspace: Workspace
) {
): Promise<number> {
const start = performance.now();
if (!bundleContent) {
try {
// no parent hash
@@ -427,7 +429,7 @@ async function createScript(
} catch (e: any) {
throw Error(
`Script creation for ${body.path} with parent ${body.parent_hash
} was not successful: ${e.body ?? e.message}`
} was not successful: ${e.body ?? e.message} `
);
}
} else {
@@ -447,16 +449,17 @@ async function createScript(
"/scripts/create_snapshot";
const req = await fetch(url, {
method: "POST",
headers: { Authorization: `Bearer ${workspace.token}` },
headers: { Authorization: `Bearer ${workspace.token} ` },
body: form,
});
if (req.status != 201) {
throw Error(
`Script snapshot creation was not successful: ${req.status} - ${req.statusText
} - ${await req.text()}`
} - ${await req.text()} `
);
}
}
return performance.now() - start;
}
export async function findContentFile(filePath: string) {
@@ -868,7 +871,7 @@ async function generateMetadata(
} & SyncOptions,
scriptPath: string | undefined
) {
log.info("This command only works for workspace scripts, for flows inline scripts use `wmill flow generate-locks`");
log.info("This command only works for workspace scripts, for flows inline scripts use `wmill flow generate - locks`");
if (scriptPath == "") {
scriptPath = undefined;
}
@@ -924,7 +927,7 @@ async function generateMetadata(
);
if (candidate) {
hasAny = true;
log.info(colors.green(`+ ${candidate}`));
log.info(colors.green(`+ ${candidate} `));
}
}
if (hasAny) {
@@ -987,7 +990,7 @@ const command = new Command()
.action(bootstrap as any)
.command(
"generate-metadata",
"re-generate the metadata file updating the lock and the script schema (for flows, use `wmill flow generate-locks`)"
"re-generate the metadata file updating the lock and the script schema (for flows, use `wmill flow generate - locks`)"
)
.arguments("[script:file]")
.option("--yes", "Skip confirmation prompt")
+286 -227
View File
@@ -1390,244 +1390,299 @@ export async function push(opts: GlobalOptions & SyncOptions) {
return;
}
const start = performance.now();
log.info(colors.gray(`Applying changes to files ...`));
const alreadySynced: string[] = [];
for await (const change of changes) {
const stateTarget = path.join(Deno.cwd(), ".wmill", change.path);
let stateExists = true;
let stateful = opts.stateful;
if (stateful) {
try {
await Deno.stat(stateTarget);
await Deno.stat(path.join(Deno.cwd(), ".wmill"));
} catch {
stateExists = false;
stateful = false;
}
}
// Group changes by base path (before first dot)
const groupedChanges = new Map<string, typeof changes>();
for (const change of changes) {
const basePath = change.path.split('.')[0];
if (!groupedChanges.has(basePath)) {
groupedChanges.set(basePath, []);
}
groupedChanges.get(basePath)!.push(change);
}
let parallelizationFactor = opts.parallel ?? 1;
if (parallelizationFactor <= 0) {
parallelizationFactor = 1;
}
const groupedChangesArray = Array.from(groupedChanges.entries());
log.info(`found changes for ${groupedChangesArray.length} items with a total of ${groupedChangesArray.reduce((acc, [_, changes]) => acc + changes.length, 0)} files to process`);
if (parallelizationFactor > 1) {
log.info(`Parallelizing ${parallelizationFactor} changes at a time`);
}
// Create a pool of workers that processes items as they become available
const pool = new Set();
const queue = [...groupedChangesArray];
while (queue.length > 0 || pool.size > 0) {
// Fill the pool until we reach parallelizationFactor
while (pool.size < parallelizationFactor && queue.length > 0) {
const [_basePath, changes] = queue.shift()!;
const promise = (async () => {
const alreadySynced: string[] = [];
for await (const change of changes) {
let stateTarget = undefined;
if (stateful) {
try {
stateTarget = path.join(Deno.cwd(), ".wmill", change.path);
await Deno.stat(stateTarget);
} catch {
stateTarget = undefined;
}
}
if (change.name === "edited") {
if (
await handleScriptMetadata(
change.path,
workspace,
alreadySynced,
opts.message,
globalDeps,
codebases,
opts
)
) {
if (stateTarget) {
await Deno.writeTextFile(stateTarget, change.after);
}
continue;
} else if (
await handleFile(
change.path,
workspace,
alreadySynced,
opts.message,
opts,
globalDeps,
codebases
)
) {
if (stateTarget) {
await Deno.writeTextFile(stateTarget, change.after);
}
continue;
}
if (stateTarget) {
await ensureDir(path.dirname(stateTarget));
log.info(`Editing ${getTypeStrFromPath(change.path)} ${change.path}`);
}
if (isFileResource(change.path)) {
const resourceFilePath = await findResourceFile(change.path);
if (!alreadySynced.includes(resourceFilePath)) {
alreadySynced.push(resourceFilePath);
const newObj = parseFromPath(
resourceFilePath,
await Deno.readTextFile(resourceFilePath)
);
await pushResource(
workspace.workspaceId,
resourceFilePath,
undefined,
newObj
);
if (stateTarget) {
await Deno.writeTextFile(stateTarget, change.after);
}
continue;
}
}
const oldObj = parseFromPath(change.path, change.before);
const newObj = parseFromPath(change.path, change.after);
await pushObj(
workspace.workspaceId,
change.path,
oldObj,
newObj,
opts.plainSecrets ?? false,
alreadySynced,
opts.message
);
if (stateTarget) {
await Deno.writeTextFile(stateTarget, change.after);
}
} else if (change.name === "added") {
if (
change.path.endsWith(".script.json") ||
change.path.endsWith(".script.yaml") ||
change.path.endsWith(".lock") ||
isFileResource(change.path)
) {
continue;
} else if (
await handleFile(
change.path,
workspace,
alreadySynced,
opts.message,
opts,
globalDeps,
codebases
)
) {
continue;
}
if (stateTarget) {
await ensureDir(path.dirname(stateTarget));
log.info(`Adding ${getTypeStrFromPath(change.path)} ${change.path}`);
}
const obj = parseFromPath(change.path, change.content);
await pushObj(
workspace.workspaceId,
change.path,
undefined,
obj,
opts.plainSecrets ?? false,
[],
opts.message
);
if (stateTarget) {
await Deno.writeTextFile(stateTarget, change.content);
}
} else if (change.name === "deleted") {
if (change.path.endsWith(".lock")) {
continue;
}
const typ = getTypeStrFromPath(change.path);
if (typ == "script") {
log.info(`Archiving ${typ} ${change.path}`);
} else {
log.info(`Deleting ${typ} ${change.path}`);
}
const workspaceId = workspace.workspaceId;
const target = change.path.replaceAll(SEP, "/");
switch (typ) {
case "script": {
const script = await wmill.getScriptByPath({
workspace: workspaceId,
path: removeExtensionToPath(target),
});
await wmill.archiveScriptByHash({
workspace: workspaceId,
hash: script.hash,
});
break;
}
case "folder":
await wmill.deleteFolder({
workspace: workspaceId,
name: change.path.split(SEP)[1],
});
break;
case "resource":
await wmill.deleteResource({
workspace: workspaceId,
path: removeSuffix(target, ".resource.json"),
});
break;
case "resource-type":
await wmill.deleteResourceType({
workspace: workspaceId,
path: removeSuffix(target, ".resource-type.json"),
});
break;
case "flow":
await wmill.deleteFlowByPath({
workspace: workspaceId,
path: removeSuffix(target, ".flow/flow.json"),
});
break;
case "app":
await wmill.deleteApp({
workspace: workspaceId,
path: removeSuffix(target, ".app/app.json"),
});
break;
case "schedule":
await wmill.deleteSchedule({
workspace: workspaceId,
path: removeSuffix(target, ".schedule.json"),
});
break;
case "variable":
await wmill.deleteVariable({
workspace: workspaceId,
path: removeSuffix(target, ".variable.json"),
});
break;
case "user": {
const users = await wmill.listUsers({
workspace: workspaceId,
});
const email = removeSuffix(
removePathPrefix(change.path, "users"),
".user.json"
);
const user = users.find((u) => u.email === email);
if (!user) {
throw new Error(`User ${email} not found`);
}
await wmill.deleteUser({
workspace: workspaceId,
username: user.username,
});
break;
}
case "group":
await wmill.deleteGroup({
workspace: workspaceId,
name: removeSuffix(
removePathPrefix(change.path, "groups"),
".group.json"
),
});
break;
default:
break;
}
if (stateTarget) {
try {
await Deno.remove(stateTarget);
} catch {
// state target may not exist already
}
}
}
}
})();
pool.add(promise);
// Remove from pool when complete
promise.then(() => pool.delete(promise));
}
if (change.name === "edited") {
if (
await handleScriptMetadata(
change.path,
workspace,
alreadySynced,
opts.message,
globalDeps,
codebases,
opts
)
) {
if (opts.stateful && stateExists) {
await Deno.writeTextFile(stateTarget, change.after);
}
continue;
} else if (
await handleFile(
change.path,
workspace,
alreadySynced,
opts.message,
opts,
globalDeps,
codebases
)
) {
if (opts.stateful && stateExists) {
await Deno.writeTextFile(stateTarget, change.after);
}
continue;
}
if (opts.stateful) {
await ensureDir(path.dirname(stateTarget));
log.info(`Editing ${getTypeStrFromPath(change.path)} ${change.path}`);
}
if (isFileResource(change.path)) {
const resourceFilePath = await findResourceFile(change.path);
if (!alreadySynced.includes(resourceFilePath)) {
alreadySynced.push(resourceFilePath);
const newObj = parseFromPath(
resourceFilePath,
await Deno.readTextFile(resourceFilePath)
);
await pushResource(
workspace.workspaceId,
resourceFilePath,
undefined,
newObj
);
if (opts.stateful && stateExists) {
await Deno.writeTextFile(stateTarget, change.after);
}
continue;
}
}
const oldObj = parseFromPath(change.path, change.before);
const newObj = parseFromPath(change.path, change.after);
await pushObj(
workspace.workspaceId,
change.path,
oldObj,
newObj,
opts.plainSecrets ?? false,
alreadySynced,
opts.message
);
if (opts.stateful && stateExists) {
await Deno.writeTextFile(stateTarget, change.after);
}
} else if (change.name === "added") {
if (
change.path.endsWith(".script.json") ||
change.path.endsWith(".script.yaml") ||
change.path.endsWith(".lock") ||
isFileResource(change.path)
) {
continue;
} else if (
await handleFile(
change.path,
workspace,
alreadySynced,
opts.message,
opts,
globalDeps,
codebases
)
) {
continue;
}
if (opts.stateful && stateExists) {
await ensureDir(path.dirname(stateTarget));
log.info(`Adding ${getTypeStrFromPath(change.path)} ${change.path}`);
}
const obj = parseFromPath(change.path, change.content);
await pushObj(
workspace.workspaceId,
change.path,
undefined,
obj,
opts.plainSecrets ?? false,
[],
opts.message
);
if (opts.stateful && stateExists) {
await Deno.writeTextFile(stateTarget, change.content);
}
} else if (change.name === "deleted") {
if (change.path.endsWith(".lock")) {
continue;
}
const typ = getTypeStrFromPath(change.path);
if (typ == "script") {
log.info(`Archiving ${typ} ${change.path}`);
} else {
log.info(`Deleting ${typ} ${change.path}`);
}
const workspaceId = workspace.workspaceId;
const target = change.path.replaceAll(SEP, "/");
switch (typ) {
case "script": {
const script = await wmill.getScriptByPath({
workspace: workspaceId,
path: removeExtensionToPath(target),
});
await wmill.archiveScriptByHash({
workspace: workspaceId,
hash: script.hash,
});
break;
}
case "folder":
await wmill.deleteFolder({
workspace: workspaceId,
name: change.path.split(SEP)[1],
});
break;
case "resource":
await wmill.deleteResource({
workspace: workspaceId,
path: removeSuffix(target, ".resource.json"),
});
break;
case "resource-type":
await wmill.deleteResourceType({
workspace: workspaceId,
path: removeSuffix(target, ".resource-type.json"),
});
break;
case "flow":
await wmill.deleteFlowByPath({
workspace: workspaceId,
path: removeSuffix(target, ".flow/flow.json"),
});
break;
case "app":
await wmill.deleteApp({
workspace: workspaceId,
path: removeSuffix(target, ".app/app.json"),
});
break;
case "schedule":
await wmill.deleteSchedule({
workspace: workspaceId,
path: removeSuffix(target, ".schedule.json"),
});
break;
case "variable":
await wmill.deleteVariable({
workspace: workspaceId,
path: removeSuffix(target, ".variable.json"),
});
break;
case "user": {
const users = await wmill.listUsers({
workspace: workspaceId,
});
const email = removeSuffix(
removePathPrefix(change.path, "users"),
".user.json"
);
const user = users.find((u) => u.email === email);
if (!user) {
throw new Error(`User ${email} not found`);
}
await wmill.deleteUser({
workspace: workspaceId,
username: user.username,
});
break;
}
case "group":
await wmill.deleteGroup({
workspace: workspaceId,
name: removeSuffix(
removePathPrefix(change.path, "groups"),
".group.json"
),
});
break;
default:
break;
}
try {
await Deno.remove(stateTarget);
} catch {
// state target may not exist already
}
// Wait for at least one task to complete before continuing
if (pool.size > 0) {
await Promise.race(pool);
}
}
log.info(
colors.bold.green.underline(
`\nDone! All ${changes.length} changes pushed to the remote workspace ${workspace.workspaceId} named ${workspace.name}.`
`\nDone! All ${changes.length} changes pushed to the remote workspace ${workspace.workspaceId} named ${workspace.name} (${(performance.now() - start).toFixed(0)}ms)`
)
);
}
@@ -1699,6 +1754,10 @@ const command = new Command()
"--message <message:string>",
"Include a message that will be added to all scripts/flows/apps updated during this push"
)
.option(
"--parallel <number>",
"Number of changes to process in parallel"
)
// deno-lint-ignore no-explicit-any
.action(push as any);
+12 -4
View File
@@ -15,7 +15,7 @@ export function deepEqual<T>(a: T, b: T): boolean {
if (Array.isArray(a)) {
length = a.length;
if (length != b.length) return false;
for (i = length; i-- !== 0; ) {
for (i = length; i-- !== 0;) {
if (!deepEqual(a[i], b[i])) return false;
}
return true;
@@ -43,7 +43,7 @@ export function deepEqual<T>(a: T, b: T): boolean {
if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) {
length = a.length;
if (length != b.length) return false;
for (i = length; i-- !== 0; ) {
for (i = length; i-- !== 0;) {
if (a[i] !== b[i]) return false;
}
return true;
@@ -66,11 +66,11 @@ export function deepEqual<T>(a: T, b: T): boolean {
length = keys.length;
if (length !== Object.keys(b).length) return false;
for (i = length; i-- !== 0; ) {
for (i = length; i-- !== 0;) {
if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false;
}
for (i = length; i-- !== 0; ) {
for (i = length; i-- !== 0;) {
const key = keys[i];
if (!deepEqual(a[key], b[key])) return false;
}
@@ -142,3 +142,11 @@ export function isFileResource(path: string): boolean {
splitPath[2] == "file"
);
}
export function printSync(input: string | Uint8Array, to = Deno.stdout) {
let bytesWritten = 0
const bytes = typeof input === 'string' ? new TextEncoder().encode(input) : input
while (bytesWritten < bytes.length) {
bytesWritten += to.writeSync(bytes.subarray(bytesWritten))
}
}
+21 -1
View File
@@ -1,3 +1,22 @@
FROM alpine:3.14 AS oracledb-client
ARG TARGETPLATFORM
# Oracle DB Client for amd64
COPY --from=ghcr.io/oracle/oraclelinux9-instantclient:23 /usr/lib/oracle/23/client64/lib /opt/oracle/23/amd64/lib
# Oracle DB Client for arm64
RUN mkdir -p /opt/oracle/23/arm64 \
&& cd /opt/oracle/23/arm64 \
&& wget https://download.oracle.com/otn_software/linux/instantclient/instantclient-basiclite-linux-arm64.zip \
&& unzip instantclient-basiclite-linux-arm64.zip && rm instantclient-basiclite-linux-arm64.zip && mv instantclient* ./lib
RUN if [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
mv /opt/oracle/23/arm64/lib /opt/oracle/23/lib; \
else \
mv /opt/oracle/23/amd64/lib /opt/oracle/23/lib; \
fi
FROM ghcr.io/windmill-labs/windmill-ee:dev
COPY --from=rust:1.81.0 /usr/local/cargo /usr/local/cargo
@@ -5,11 +24,12 @@ COPY --from=rust:1.81.0 /usr/local/rustup /usr/local/rustup
RUN pip3 install ansible
# dotnet SDK
COPY --from=bitnami/dotnet-sdk:9.0.101-debian-12-r0 /opt/bitnami/dotnet-sdk /opt/dotnet-sdk
RUN ln -s /opt/dotnet-sdk/bin/dotnet /usr/bin/dotnet
ENV DOTNET_ROOT="/opt/dotnet-sdk/bin"
# Oracle DB Client
COPY --from=ghcr.io/oracle/oraclelinux9-instantclient:23 /usr/lib/oracle/23/client64/lib /opt/oracle/23/lib
COPY --from=oracledb-client /opt/oracle/23/lib /opt/oracle/23/lib
RUN apt-get -y update && apt-get install -y libaio1
RUN echo /opt/oracle/23/lib > /etc/ld.so.conf.d/oracle-instantclient.conf && ldconfig
+21 -1
View File
@@ -21,6 +21,25 @@ RUN apt-get -y update \
RUN git clone -b master --single-branch https://github.com/google/nsjail.git . && git checkout dccf911fd2659e7b08ce9507c25b2b38ec2c5800
RUN make
FROM alpine:3.14 AS oracledb-client
ARG TARGETPLATFORM
# Oracle DB Client for amd64
COPY --from=ghcr.io/oracle/oraclelinux9-instantclient:23 /usr/lib/oracle/23/client64/lib /opt/oracle/23/amd64/lib
# Oracle DB Client for arm64
RUN mkdir -p /opt/oracle/23/arm64 \
&& cd /opt/oracle/23/arm64 \
&& wget https://download.oracle.com/otn_software/linux/instantclient/instantclient-basiclite-linux-arm64.zip \
&& unzip instantclient-basiclite-linux-arm64.zip && rm instantclient-basiclite-linux-arm64.zip && mv instantclient* ./lib
RUN if [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
mv /opt/oracle/23/arm64/lib /opt/oracle/23/lib; \
else \
mv /opt/oracle/23/amd64/lib /opt/oracle/23/lib; \
fi
FROM ghcr.io/windmill-labs/windmill-ee:dev
RUN apt-get update && apt-get install -y libprotobuf-dev libnl-route-3-dev
@@ -30,12 +49,13 @@ COPY --from=rust:1.80.1 /usr/local/rustup /usr/local/rustup
RUN pip3 install ansible
# dotnet SDK
COPY --from=bitnami/dotnet-sdk:9.0.101-debian-12-r0 /opt/bitnami/dotnet-sdk /opt/dotnet-sdk
RUN ln -s /opt/dotnet-sdk/bin/dotnet /usr/bin/dotnet
ENV DOTNET_ROOT="/opt/dotnet-sdk/bin"
# Oracle DB Client
COPY --from=ghcr.io/oracle/oraclelinux9-instantclient:23 /usr/lib/oracle/23/client64/lib /opt/oracle/23/lib
COPY --from=oracledb-client /opt/oracle/23/lib /opt/oracle/23/lib
RUN apt-get -y update && apt-get install -y libaio1
RUN echo /opt/oracle/23/lib > /etc/ld.so.conf.d/oracle-instantclient.conf && ldconfig
+6 -2
View File
@@ -10,6 +10,7 @@
let
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = true;
overlays = [ (import rust-overlay) ];
};
rust = pkgs.rust-bin.stable.latest.default.override {
@@ -49,7 +50,7 @@
devShell = pkgs.mkShell {
buildInputs = buildInputs ++ (with pkgs; [
git xcaddy sqlx-cli flock sccache
deno python3 python3Packages.pip go bun uv
deno python3 python3Packages.pip go bun uv dotnet-sdk_9 oracle-instantclient
]);
packages = [
(pkgs.writeScriptBin "wm-caddy" ''
@@ -102,12 +103,15 @@
UV_PATH = "${pkgs.uv}/bin/uv";
FLOCK_PATH = "${pkgs.flock}/bin/flock";
CARGO_PATH = "${rust}/bin/cargo";
DOTNET_PATH = "${pkgs.dotnet-sdk_9}/bin/dotnet";
DOTNET_ROOT = "${pkgs.dotnet-sdk_9}/share/dotnet";
ORACLE_LIB_DIR = "${pkgs.oracle-instantclient.lib}/lib";
};
packages.default = self.packages.${system}.windmill;
packages.windmill-client = pkgs.buildNpmPackage {
name = "windmill-client";
version = (pkgs.lib.strings.trim (builtins.readFile ./version.txt));
src = pkgs.nix-gitignore.gitignoreSource [] ./frontend;
nativeBuildInputs = with pkgs; [ pkg-config ];
buildInputs = with pkgs; [ nodejs pixman cairo pango ];
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.452.1",
"version": "1.455.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.452.1",
"version": "1.455.1",
"license": "AGPL-3.0",
"dependencies": {
"@anthropic-ai/sdk": "^0.32.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.452.1",
"version": "1.455.1",
"scripts": {
"dev": "vite dev",
"build": "vite build",
+1 -1
View File
@@ -381,11 +381,11 @@
? 'border-2 border-green-500 bg-surface'
: ''
)}
data-schema-picker
>
{#if diffStatus && typeof diffStatus === 'object' && diffStatus.diff !== 'same'}
<div
class="absolute top-0 right-2 rounded-md rounded-t-none flex flex-row overflow-hidden bg-surface"
data-schema-picker
>
<button
class="p-1 bg-green-500 text-white hover:bg-green-600"
@@ -1,18 +0,0 @@
<script lang="ts">
import CaptureTable from '$lib/components/triggers/CaptureTable.svelte'
export let flowPath: string | null = null
export let headless: boolean = true
export let addButton: boolean = false
</script>
<div class="h-full">
<CaptureTable
path={flowPath ?? ''}
on:select
isFlow={true}
{headless}
{addButton}
on:openTriggers
/>
</div>
@@ -51,6 +51,7 @@
export let diff: Record<string, SchemaDiff> = {}
export let disableDnd: boolean = false
export let shouldDispatchChanges: boolean = false
export let isValid: boolean = true
const dispatch = createEventDispatcher()
@@ -303,6 +304,7 @@
dispatch('change', schema)
}}
{shouldDispatchChanges}
bind:isValid
/>
<slot name="runButton" />
@@ -1,32 +1,25 @@
<script lang="ts">
import { type Job, JobService, type Flow, type RestartedFrom, type OpenFlow } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { Badge, Button, Drawer, Popup } from './common'
import { Badge, Button, Popup } from './common'
import { createEventDispatcher, getContext } from 'svelte'
import type { FlowEditorContext } from './flows/types'
import { runFlowPreview } from './flows/utils'
import SchemaForm from './SchemaForm.svelte'
import SchemaFormWithArgPicker from './SchemaFormWithArgPicker.svelte'
import FlowStatusViewer from '../components/FlowStatusViewer.svelte'
import FlowProgressBar from './flows/FlowProgressBar.svelte'
import {
AlertTriangle,
ArrowRight,
CornerDownLeft,
Play,
RefreshCw,
X,
ArrowLeftIcon
} from 'lucide-svelte'
import { AlertTriangle, ArrowRight, CornerDownLeft, Play, RefreshCw, X } from 'lucide-svelte'
import { emptyString, sendUserToast } from '$lib/utils'
import DrawerContent from './common/drawer/DrawerContent.svelte'
import SavedInputs from './SavedInputs.svelte'
import { dfs } from './flows/dfs'
import { sliceModules } from './flows/flowStateUtils'
import CapturesInputs from '$lib/components/CapturesInputs.svelte'
import ObjectViewer from './propertyPicker/ObjectViewer.svelte'
import InputSelectedBadge from './schema/InputSelectedBadge.svelte'
import Toggle from './Toggle.svelte'
import JsonInputs from './JsonInputs.svelte'
export let previewMode: 'upTo' | 'whole'
export let open: boolean
export let preventEscape = false
export let jobId: string | undefined = undefined
export let job: Job | undefined = undefined
@@ -39,6 +32,10 @@
let isRunning: boolean = false
let jobProgressReset: () => void
let jsonView: boolean = false
let jsonEditor: JsonInputs
let schemaHeight = 0
let isValid: boolean = true
export function test() {
renderCount++
@@ -83,6 +80,10 @@
const newFlow = extractFlow(previewMode)
jobId = await runFlowPreview(args, newFlow, $pathStore, restartedFrom)
isRunning = true
if (inputSelected) {
savedArgs = $previewArgs
inputSelected = undefined
}
} catch (e) {
sendUserToast('Could not run preview', true, undefined, e.toString())
isRunning = false
@@ -99,6 +100,14 @@
runPreview($previewArgs, undefined)
}
break
case 'Escape':
if (preventEscape) {
selectInput(undefined)
event.preventDefault()
event.stopPropagation
}
break
}
}
}
@@ -124,97 +133,40 @@
}
}
let savedArgs = $previewArgs
let inputSelected: 'captures' | 'history' | 'saved' | undefined = undefined
async function selectInput(input, type?: 'captures' | 'history' | 'saved' | undefined) {
if (!input) {
$previewArgs = savedArgs
inputSelected = undefined
setTimeout(() => {
preventEscape = false
}, 100)
} else {
$previewArgs = input
inputSelected = type
preventEscape = true
jsonEditor?.setCode(JSON.stringify($previewArgs ?? {}, null, '\t'))
}
}
export function refresh() {
renderCount++
}
$: if (job?.type === 'CompletedJob') {
isRunning = false
}
$: selectedJobStep !== undefined && onSelectedJobStepChange()
let inputLibraryDrawer: Drawer
let captureLibraryDrawer: Drawer
let renderCount: number = 0
let selectedCapture: Record<string, any> | undefined = undefined
</script>
<svelte:window on:keydown={onKeyDown} />
<Drawer bind:this={inputLibraryDrawer}>
<DrawerContent title="Input library {initialPath}" on:close={inputLibraryDrawer?.toggleDrawer}>
<SavedInputs
flowPath={initialPath}
isValid={true}
args={$previewArgs}
on:selected_args={(e) => {
$previewArgs = JSON.parse(JSON.stringify(e.detail))
inputLibraryDrawer?.closeDrawer()
renderCount++
}}
/>
</DrawerContent>
</Drawer>
<Drawer bind:this={captureLibraryDrawer}>
<DrawerContent
title="Trigger captures library {initialPath}"
on:close={captureLibraryDrawer?.toggleDrawer}
>
<div class="h-full flex flex-col gap-2">
<div class="min-h-0 grow h-full">
<CapturesInputs
flowPath={$pathStore}
headless={false}
addButton={true}
on:select={(e) => {
selectedCapture = e.detail
}}
on:openTriggers={(e) => {
dispatch('openTriggers', e.detail)
captureLibraryDrawer?.closeDrawer()
}}
/>
</div>
<div class="w-full flex flex-col gap-2 px-2 py-2 h-[50%]">
<div class="w-full flex flex-col">
<Button
color="blue"
btnClasses="w-full"
size="sm"
spacingSize="xl"
on:click={async () => {
$previewArgs = JSON.parse(JSON.stringify(selectedCapture))
captureLibraryDrawer?.closeDrawer()
renderCount++
}}
disabled={!selectedCapture}
>
<ArrowLeftIcon class="w-4 h-4 mr-2" />
Use input
</Button>
</div>
<div class="w-full min-h-0 grow overflow-auto">
{#if typeof selectedCapture == 'string' && selectedCapture == 'WINDMILL_TOO_BIG'}
<div class="text-secondary mt-2">
Payload too big to preview but can still be loaded</div
>
{:else if Object.keys(selectedCapture || {}).length > 0}
<div class=" overflow-auto h-full p-2">
<ObjectViewer json={selectedCapture} />
</div>
{:else}
<div class="text-center text-tertiary">
Select an Input to preview scripts arguments
</div>
{/if}
</div>
</div>
</div>
</DrawerContent>
</Drawer>
<div class="flex flex-col space-y-2 h-screen bg-surface px-6 py-2 w-full" id="flow-preview-content">
<div class="flex flex-row justify-between w-full items-center gap-x-2">
<div class="flex flex-col space-y-2 h-screen bg-surface px-4 py-2 w-full" id="flow-preview-content">
<div class="flex flex-row w-full items-center gap-x-2">
<div class="w-8">
<Button
on:click={() => dispatch('close')}
@@ -227,28 +179,30 @@
</div>
{#if isRunning}
<Button
color="red"
on:click={async () => {
isRunning = false
try {
jobId &&
(await JobService.cancelQueuedJob({
workspace: $workspaceStore ?? '',
id: jobId,
requestBody: {}
}))
} catch {}
}}
size="sm"
btnClasses="w-full max-w-lg"
loading={true}
clickableWhileLoading
>
Cancel
</Button>
<div class="mx-auto">
<Button
color="red"
on:click={async () => {
isRunning = false
try {
jobId &&
(await JobService.cancelQueuedJob({
workspace: $workspaceStore ?? '',
id: jobId,
requestBody: {}
}))
} catch {}
}}
size="sm"
btnClasses="w-full max-w-lg"
loading={true}
clickableWhileLoading
>
Cancel
</Button>
</div>
{:else}
<div class="flex flex-row gap-4">
<div class="grow justify-center flex flex-row gap-4">
{#if jobId !== undefined && selectedJobStep !== undefined && selectedJobStepIsTopLevel}
{#if selectedJobStepType == 'single'}
<Button
@@ -352,26 +306,6 @@
</Button>
</div>
{/if}
<div class="flex gap-2">
<Button
btnClasses="h-full truncate"
size="sm"
variant="border"
on:click={() => {
captureLibraryDrawer?.openDrawer()
}}>Trigger captures library</Button
>
{#if initialPath != ''}
<Button
btnClasses="h-full truncate"
size="sm"
variant="border"
on:click={() => {
inputLibraryDrawer?.openDrawer()
}}>Past runs/Input library</Button
>
{/if}
</div>
</div>
<div class="w-full flex flex-col gap-y-1">
{#if lastPreviewFlow && JSON.stringify($flowStore) != lastPreviewFlow}
@@ -387,17 +321,68 @@
<FlowProgressBar {job} bind:reset={jobProgressReset} />
</div>
<div class="overflow-y-auto grow flex flex-col pr-4">
<div class="overflow-y-auto grow flex flex-col pt-4">
<div class="border-b">
{#key renderCount}
<SchemaForm
noVariablePicker
compact
class="py-4 max-w-3xl"
schema={$flowStore.schema}
bind:args={$previewArgs}
/>
{/key}
<SchemaFormWithArgPicker
runnableId={initialPath == '' ? $pathStore : initialPath}
runnableType={'FlowPath'}
previewArgs={$previewArgs}
on:openTriggers
on:select={(e) => {
selectInput(e.detail.payload, e.detail?.type)
}}
{isValid}
{jsonView}
>
<div class="w-full flex flex-row justify-between">
<InputSelectedBadge {inputSelected} />
<div class="flex flex-row gap-2">
<Toggle
bind:checked={jsonView}
label="JSON View"
size="xs"
options={{
right: 'JSON',
rightTooltip: 'Fill args from JSON'
}}
lightMode
on:change={(e) => {
jsonEditor?.setCode(JSON.stringify($previewArgs ?? {}, null, '\t'))
refresh()
}}
/>
</div>
</div>
{#if jsonView}
<div class="py-2" style="height: {Math.max(schemaHeight, 100)}px" data-schema-picker>
<JsonInputs
bind:this={jsonEditor}
on:select={(e) => {
if (e.detail) {
$previewArgs = e.detail
}
}}
updateOnBlur={false}
placeholder={`Write args as JSON.<br/><br/>Example:<br/><br/>{<br/>&nbsp;&nbsp;"foo": "12"<br/>}`}
/>
</div>
{:else}
{#key renderCount}
<div bind:clientHeight={schemaHeight} class="min-h-[40vh]">
<SchemaForm
noVariablePicker
compact
schema={$flowStore.schema}
bind:args={$previewArgs}
on:change={() => {
savedArgs = $previewArgs
}}
bind:isValid
/>
</div>
{/key}
{/if}
</SchemaFormWithArgPicker>
</div>
<div class="pt-4 flex flex-col grow">
{#if jobId}
@@ -821,7 +821,7 @@
As a non logged in user, you can only see jobs ran by anonymous users like you
</Alert>
{:else if job}
<div class="flow-root w-full space-y-4 {wideResults ? '' : 'max-w-7xl'} mx-auto px-4">
<div class="flow-root w-full space-y-4 {wideResults ? '' : 'max-w-7xl px-4'} mx-auto">
<!-- {#if innerModules.length > 0 && true}
<h3 class="text-md leading-6 font-bold text-primay border-b pb-2">Flow result</h3>
{:else}
@@ -145,7 +145,7 @@
{#if job?.raw_flow?.modules?.[approvalStep]?.suspend?.resume_form?.schema}
<div class="w-full border rounded-lg p-2">
<SchemaForm bind:args={default_payload} {defaultValues} {schema} />
<SchemaForm onlyMaskPassword bind:args={default_payload} {defaultValues} {schema} />
</div>
{/if}
<Tooltip
@@ -10,32 +10,20 @@
import { clickOutside } from '$lib/utils'
import InfiniteList from './InfiniteList.svelte'
export let scriptHash: string | null = null
export let scriptPath: string | null = null
export let flowPath: string | null = null
export let runnableId: string | undefined = undefined
export let runnableType: RunnableType | undefined = undefined
export let loading: boolean = false
const dispatch = createEventDispatcher()
let jobs: Job[] = []
let loading: boolean = false
let hasMoreCurrentRuns = false
let page = 1
let infiniteList: InfiniteList | undefined = undefined
let loadInputsPageFn: ((page: number, perPage: number) => Promise<any>) | undefined = undefined
let runnableType: RunnableType | undefined = undefined
$: runnableType = scriptHash
? 'ScriptHash'
: scriptPath
? 'ScriptPath'
: flowPath
? 'FlowPath'
: undefined
let runnableId: string | undefined = undefined
let cachedArgs: Record<string, any> = {}
function initLoadInputs() {
runnableId = scriptHash || scriptPath || flowPath || undefined
loadInputsPageFn = async (page: number, perPage: number) => {
const inputs = await InputService.getInputHistory({
workspace: $workspaceStore!,
@@ -48,7 +36,21 @@
const inputsWithPayload = await Promise.all(
inputs.map(async (input) => {
if (cachedArgs[input.id]) {
return {
...input,
payloadData: cachedArgs[input.id]
}
}
const payloadData = await loadArgsFromHistory(input.id, undefined, false)
if (payloadData === 'WINDMILL_TOO_BIG') {
return {
...input,
payloadData: 'WINDMILL_TOO_BIG',
getFullPayload: () => loadArgsFromHistory(input.id, undefined, true)
}
}
cachedArgs[input.id] = payloadData
return {
...input,
payloadData
@@ -60,21 +62,24 @@
infiniteList?.setLoader(loadInputsPageFn)
}
function handleSelected(data: any) {
async function handleSelected(data: any) {
if (selected === data.id) {
selected = undefined
dispatch('select', undefined)
resetSelected(true)
return
}
selected = data.id
dispatch('select', data.payloadData)
if (data.payloadData === 'WINDMILL_TOO_BIG') {
const fullPayload = await data.getFullPayload?.()
dispatch('select', fullPayload)
} else {
dispatch('select', structuredClone(data.payloadData))
}
}
let selected: string | undefined = undefined
onDestroy(() => {
selected = undefined
dispatch('select', undefined)
resetSelected(true)
})
async function getPropPickerElements(): Promise<HTMLElement[]> {
@@ -100,8 +105,7 @@
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape' && selected) {
selected = undefined
dispatch('select', undefined)
resetSelected(true)
event.stopPropagation()
event.preventDefault()
}
@@ -115,14 +119,21 @@
let jobHovered: string | undefined = undefined
function refresh() {
export function refresh() {
if (infiniteList) {
infiniteList.loadData('refresh')
}
}
export function resetSelected(dispatchEvent?: boolean) {
selected = undefined
if (dispatchEvent) {
dispatch('select', undefined)
}
}
$: !loading && refresh()
$: $workspaceStore && (scriptHash || scriptPath || flowPath) && infiniteList && initLoadInputs()
$: $workspaceStore && runnableId && runnableType && infiniteList && initLoadInputs()
</script>
<svelte:window on:keydown={handleKeydown} />
@@ -153,19 +164,18 @@
use:clickOutside={{ capture: false, exclude: getPropPickerElements }}
on:click_outside={() => {
if (selected) {
selected = undefined
dispatch('select', undefined)
resetSelected(true)
}
}}
>
<div class="grow-0">
<div class="grow-0" data-schema-picker>
<DataTable size="xs" bind:currentPage={page} hasMore={hasMoreCurrentRuns} tableFixed={true}>
{#if loading && (jobs == undefined || jobs?.length == 0)}
<div class="text-center text-tertiary text-xs py-2">Loading current runs...</div>
{:else if jobs?.length > 0}
<colgroup>
<col class="w-8" />
<col class="w-20" />
<col class="w-16" />
<col />
</colgroup>
@@ -190,7 +200,7 @@
</DataTable>
</div>
<div class="min-h-0 grow">
<div class="min-h-0 grow" data-schema-picker>
<InfiniteList
bind:this={infiniteList}
selectedItemId={selected}
@@ -200,7 +210,7 @@
<svelte:fragment slot="columns">
<colgroup>
<col class="w-8" />
<col class="w-20" />
<col class="w-16" />
<col />
</colgroup>
</svelte:fragment>
@@ -213,7 +223,9 @@
/>
</svelte:fragment>
<svelte:fragment slot="empty">
<div class="text-center text-tertiary text-xs py-2">No previous Runs</div>
<div class="text-center text-tertiary text-xs py-2">
{runnableId ? 'No previous inputs' : 'Save draft to see previous runs'}
</div>
</svelte:fragment>
</InfiniteList>
</div>
@@ -56,10 +56,14 @@
isNew: false
}))
}
}, 1000)
}, 2000)
page = Math.floor(items.length / perPage) + 1
hasMore = items.length === perPage * (page - 1)
page = Math.ceil(items.length / perPage)
hasMore = items.length === perPage * page
if (hasMore) {
const potentialNewItems = await loadInputs(page + 1, perPage)
hasMore = potentialNewItems.length > 0
}
initLoad = true
isEmpty = items.length === 0
length = items.length
@@ -112,6 +116,7 @@
on:loadMore={() => {
loadData()
}}
{loading}
>
<slot name="columns" />
@@ -0,0 +1,70 @@
<script lang="ts">
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
import { createEventDispatcher } from 'svelte'
import { clickOutside } from '$lib/utils'
const dispatch = createEventDispatcher()
export let updateOnBlur = true
export let placeholder =
'Write a JSON payload. The input schema will be inferred.<br/><br/>Example:<br/><br/>{<br/>&nbsp;&nbsp;"foo": "12"<br/>}'
let pendingJson = ''
let simpleEditor: SimpleEditor | undefined = undefined
function updatePayloadFromJson(jsonInput: string) {
if (jsonInput === undefined || jsonInput === null || jsonInput.trim() === '') {
dispatch('select', undefined)
return
}
try {
const parsed = JSON.parse(jsonInput)
dispatch('select', parsed)
} catch (error) {
dispatch('select', undefined)
}
}
export function setCode(code: string) {
simpleEditor?.setCode(code)
}
async function getPropPickerElements(): Promise<HTMLElement[]> {
const elements = document.querySelectorAll('[data-schema-picker], [data-schema-picker] *')
return elements ? (Array.from(elements) as HTMLElement[]) : []
}
</script>
<div
class="h-full"
use:clickOutside={{ capture: false, exclude: getPropPickerElements }}
on:click_outside={() => {
if (updateOnBlur) {
updatePayloadFromJson('')
}
}}
>
<SimpleEditor
bind:this={simpleEditor}
on:focus={() => {
if (updateOnBlur) {
dispatch('focus')
updatePayloadFromJson(pendingJson)
}
}}
on:blur={async () => {
if (updateOnBlur) {
dispatch('blur')
}
}}
on:change={(e) => {
if (e.detail?.code !== undefined) {
updatePayloadFromJson(e.detail.code)
}
}}
bind:code={pendingJson}
lang="json"
class="h-full"
{placeholder}
/>
</div>
+40 -17
View File
@@ -18,6 +18,7 @@
import RunFormAdvancedPopup from './RunFormAdvancedPopup.svelte'
import { page } from '$app/stores'
import { replaceState } from '$app/navigation'
import JsonInputs from '$lib/components/JsonInputs.svelte'
export let runnable:
| {
@@ -54,9 +55,12 @@
export let overrideTag: string | undefined
export let args: Record<string, any> = {}
export let jsonView = false
let reloadArgs = 0
let blockPopupOpen = false
let jsonEditor: JsonInputs | undefined = undefined
let schemaHeight = 0
export async function setArgs(nargs: Record<string, any>) {
args = nargs
@@ -89,6 +93,10 @@
console.error('Impossible to set hash in args', e)
}
}
export function setCode(code: string) {
jsonEditor?.setCode(code)
}
</script>
<div class="max-w-3xl">
@@ -137,7 +145,7 @@
{#if topButton}
<Button
btnClasses="!px-6 !py-1 w-full"
disabled={!isValid}
disabled={!isValid || jsonView}
on:click={() => runAction(undefined, args, invisible_to_owner, overrideTag)}
>
{buttonText}
@@ -147,22 +155,37 @@
<div class="my-2" />
{#if !runnable.schema.properties || Object.keys(runnable.schema.properties).length === 0}
<div class="text-sm py-4 italic">No arguments</div>
{:else if jsonView}
<div class="py-2" style="height: {schemaHeight}px" data-schema-picker>
<JsonInputs
bind:this={jsonEditor}
on:select={(e) => {
if (e.detail) {
args = e.detail
}
}}
updateOnBlur={false}
placeholder={`Write args as JSON.<br/><br/>Example:<br/><br/>{<br/>&nbsp;&nbsp;"foo": "12"<br/>}`}
/>
</div>
{:else}
{#key reloadArgs}
<SchemaForm
helperScript={runnable.hash
? {
type: 'hash',
hash: runnable.hash
}
: undefined}
prettifyHeader
{noVariablePicker}
{autofocus}
schema={runnable.schema}
bind:isValid
bind:args
/>
<div bind:clientHeight={schemaHeight}>
<SchemaForm
helperScript={runnable.hash
? {
type: 'hash',
hash: runnable.hash
}
: undefined}
prettifyHeader
{noVariablePicker}
{autofocus}
schema={runnable.schema}
bind:isValid
bind:args
/>
</div>
{/key}
{/if}
{:else}
@@ -176,7 +199,7 @@
{loading}
color="dark"
btnClasses="!px-6 !py-1 !h-8 inline-flex gap-2"
disabled={!isValid}
disabled={!isValid || jsonView}
on:click={() => runAction(scheduledForStr, args, invisible_to_owner, overrideTag)}
shortCut={{ Icon: CornerDownLeft, hide: !viewKeybinding }}
>
@@ -230,7 +253,7 @@
{:else if !topButton}
<Button
btnClasses="!px-6 !py-1 w-full"
disabled={!isValid}
disabled={!isValid || jsonView}
on:click={() => runAction(undefined, args, invisible_to_owner, overrideTag)}
shortCut={{ Icon: CornerDownLeft, hide: !viewKeybinding }}
>
@@ -136,7 +136,7 @@
</div>
</div>
<CronInput bind:schedule={$primarySchedule.cron} bind:timezone={$primarySchedule.timezone} />
<SchemaForm {schema} bind:args={$primarySchedule.args} />
<SchemaForm onlyMaskPassword {schema} bind:args={$primarySchedule.args} />
{#if emptyString($primarySchedule.cron)}
<p class="text-xs text-tertiary mt-10">Define a schedule frequency first</p>
{/if}
@@ -4,7 +4,7 @@
import { userStore, workspaceStore } from '$lib/stores.js'
import { sendUserToast } from '$lib/utils.js'
import { createEventDispatcher, onDestroy } from 'svelte'
import { Edit, Trash2, Save } from 'lucide-svelte'
import { Edit, Trash2, Save, Eye } from 'lucide-svelte'
import Toggle from './Toggle.svelte'
import { Cell } from './table/index'
import { clickOutside } from '$lib/utils'
@@ -12,15 +12,23 @@
import Popover from '$lib/components/Popover.svelte'
import InfiniteList from './InfiniteList.svelte'
import { twMerge } from 'tailwind-merge'
import PopoverV2 from '$lib/components/meltComponents/Popover.svelte'
import ObjectViewer from '$lib/components/propertyPicker/ObjectViewer.svelte'
export let flowPath: string | null = null
export let previewArgs: any = undefined
export let runnableId: string | undefined = undefined
export let runnableType: RunnableType | undefined = undefined
export let isValid: boolean = false
export let noButton: boolean = false
export let jsonView: boolean = false
interface EditableInput extends Input {
isEditing?: boolean
isSaving?: boolean
isNew?: boolean
isDeleting?: boolean
payloadData?: any
getFullPayload?: () => Promise<any>
}
let infiniteList: InfiniteList | null = null
@@ -30,10 +38,6 @@
const dispatch = createEventDispatcher()
$: runnableId = flowPath || undefined
let runnableType: RunnableType | undefined = undefined
$: runnableType = flowPath ? 'FlowPath' : undefined
async function updateInput(input: EditableInput | null) {
if (!input) return
input.isSaving = true
@@ -57,13 +61,30 @@
function initLoadInputs() {
const loadInputsPageFn = async (page: number, perPage: number) => {
return await InputService.listInputs({
const inputs = await InputService.listInputs({
workspace: $workspaceStore!,
runnableId,
runnableType,
page,
perPage
})
const inputsWithPayload = await Promise.all(
inputs.map(async (input) => {
const payloadData = await loadLargeArgs(input.id, undefined, false)
if (payloadData === 'WINDMILL_TOO_BIG') {
return {
...input,
payloadData: 'WINDMILL_TOO_BIG',
getFullPayload: () => loadLargeArgs(input.id, undefined, true)
}
}
return {
...input,
payloadData
}
})
)
return inputsWithPayload
}
infiniteList?.setLoader(loadInputsPageFn)
@@ -95,20 +116,21 @@
if (input.isEditing) return
if (selectedInput === input.id) {
selectedInput = null
selectedArgs = undefined
dispatch('select', undefined)
resetSelected(true)
} else {
selectedInput = input.id
selectedArgs = await loadLargeArgs(input.id, true, false)
dispatch('select', selectedArgs)
if (input.payloadData === 'WINDMILL_TOO_BIG') {
const fullPayload = await input.getFullPayload?.()
dispatch('select', fullPayload)
} else {
selectedArgs = structuredClone(input.payloadData ?? {})
dispatch('select', selectedArgs)
}
}
}
onDestroy(() => {
selectedInput = null
selectedArgs = undefined
dispatch('select', undefined)
resetSelected(true)
})
async function getPropPickerElements(): Promise<HTMLElement[]> {
@@ -122,9 +144,7 @@
event.stopPropagation()
event.preventDefault()
} else if (event.key === 'Escape' && selectedInput) {
selectedInput = null
selectedArgs = undefined
dispatch('select', undefined)
resetSelected(true)
event.stopPropagation()
event.preventDefault()
}
@@ -151,42 +171,57 @@
}
}
$: $workspaceStore && flowPath && (infiniteList && initLoadInputs(), (draft = false))
export function refresh() {
infiniteList?.loadData('refresh')
}
export function resetSelected(dispatchEvent?: boolean) {
selectedInput = null
selectedArgs = undefined
if (dispatchEvent) {
dispatch('select', undefined)
}
}
$: $workspaceStore &&
runnableId &&
runnableType &&
(infiniteList && initLoadInputs(), (draft = false))
</script>
<svelte:window on:keydown={handleKeydown} />
<div
class="w-full flex flex-col gap-1 h-full overflow-y-auto p"
class="w-full flex flex-col gap-1 h-full overflow-y-auto"
use:clickOutside={{ capture: false, exclude: getPropPickerElements }}
on:click_outside={() => {
selectedInput = null
selectedArgs = undefined
dispatch('select', undefined)
resetSelected(true)
}}
>
<div>
<Popover class="w-full" placement="bottom" disablePopup={flowPath && previewArgs}>
<svelte:fragment slot="text">
{#if !flowPath}
Save draft first before you can save inputs
{:else if !previewArgs}
Add inputs before saving
{/if}
</svelte:fragment>
<SaveInputsButton
{runnableId}
{runnableType}
args={previewArgs ?? {}}
disabled={!previewArgs || !flowPath}
on:update={() => {
infiniteList?.loadData('refresh')
}}
showTooltip={true}
/>
</Popover>
</div>
<div class="grow min-h-0">
{#if !noButton}
<div>
<Popover class="w-full" placement="bottom" disablePopup={runnableId && previewArgs}>
<svelte:fragment slot="text">
{#if !runnableId}
Save draft first before you can save inputs
{:else if !previewArgs}
Add inputs before saving
{/if}
</svelte:fragment>
<SaveInputsButton
{runnableId}
{runnableType}
args={previewArgs ?? {}}
disabled={!previewArgs || !runnableId || !isValid || jsonView}
on:update={() => {
refresh()
}}
showTooltip={true}
/>
</Popover>
</div>
{/if}
<div class="grow min-h-0" data-schema-picker>
{#if !draft}
<InfiniteList
bind:this={infiniteList}
@@ -208,9 +243,9 @@
</Cell>
<Cell>
<div
class="w-full flex items-center text-sm justify-between gap-4 px-4 text-left transition-all"
class="w-full flex items-center text-sm justify-between gap-4 py-1 text-left transition-all"
>
<div class="w-full h-full items-center justify-between flex gap-1 min-w-0 p-1">
<div class="w-full h-full items-center justify-between flex gap-1 min-w-0">
{#if isEditing && isEditing.id === item.id}
<form
on:submit={() => {
@@ -230,6 +265,25 @@
{/if}
{#if item.created_by == $userStore?.username || $userStore?.is_admin || $userStore?.is_super_admin}
<div class="items-center flex gap-2">
<PopoverV2 displayArrow={false} closeButton={false} openOnHover={true}>
<svelte:fragment slot="trigger">
<Eye class="w-4 h-4 group-hover:block hidden" />
</svelte:fragment>
<svelte:fragment slot="content">
<div class="p-2">
{#if item.payloadData === 'WINDMILL_TOO_BIG'}
<div class="text-center text-tertiary text-xs">
Payload too big to preview but can still be loaded
</div>
{:else}
<div class="max-w-60 overflow-auto">
<ObjectViewer json={item.payloadData} />
</div>
{/if}
</div>
</svelte:fragment>
</PopoverV2>
{#if !isEditing || isEditing?.id !== item.id}
<div class="group-hover:block hidden -my-2">
<Toggle
@@ -0,0 +1,99 @@
<script lang="ts">
import SavedInputsPicker from '$lib/components/SavedInputsPicker.svelte'
import { createEventDispatcher } from 'svelte'
import HistoricInputs from '$lib/components/HistoricInputs.svelte'
import { type RunnableType } from '$lib/gen/types.gen.js'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import Section from '$lib/components/Section.svelte'
import SaveInputsButton from '$lib/components/SaveInputsButton.svelte'
const dispatch = createEventDispatcher()
export let scriptHash: string | null = null
export let scriptPath: string | null = null
export let flowPath: string | null = null
export let inputSelected: 'saved' | 'history' | undefined = undefined
export let jsonView: boolean = false
// Are the current Inputs valid and able to be saved?
export let isValid: boolean
export let args: object
let savedArgs: any = undefined
let runnableType: RunnableType | undefined = undefined
let savedInputsPicker: SavedInputsPicker | undefined = undefined
let historicInputs: HistoricInputs | undefined = undefined
$: runnableId = scriptHash || scriptPath || flowPath || undefined
$: runnableType = scriptHash
? 'ScriptHash'
: scriptPath
? 'ScriptPath'
: flowPath
? 'FlowPath'
: undefined
function selectArgs(selected_args: any, type: 'saved' | 'history' | undefined) {
if (selected_args) {
if (!inputSelected) {
savedArgs = args
}
inputSelected = type
dispatch('selected_args', selected_args)
} else if (savedArgs) {
inputSelected = type
dispatch('selected_args', savedArgs)
}
}
</script>
<div class="min-w-[300px] h-full flex flex-col">
<Splitpanes horizontal={true}>
<Pane class="px-4 py-2 h-full">
<Section
label="Saved Inputs"
tooltip="Shared inputs are available to anyone with access to the script"
wrapperClass="h-full"
small={true}
>
<svelte:fragment slot="action">
<SaveInputsButton
{args}
disabled={!isValid || jsonView}
{runnableId}
{runnableType}
on:update={() => {
savedInputsPicker?.refresh()
}}
/>
</svelte:fragment>
<SavedInputsPicker
bind:this={savedInputsPicker}
previewArgs={args}
{runnableId}
{runnableType}
{isValid}
on:select={(e) => {
if (e.detail) historicInputs?.resetSelected()
selectArgs(e.detail, e.detail ? 'saved' : undefined)
}}
noButton={true}
/>
</Section>
</Pane>
<Pane class="px-4 py-4 h-full">
<Section label="History" wrapperClass="h-full" small={true}>
<HistoricInputs
bind:this={historicInputs}
{runnableId}
{runnableType}
on:select={(e) => {
if (e.detail) savedInputsPicker?.resetSelected()
selectArgs(e.detail, e.detail ? 'history' : undefined)
}}
/>
</Section>
</Pane>
</Splitpanes>
</div>
@@ -0,0 +1,160 @@
<script lang="ts">
import { Pane, Splitpanes } from 'svelte-splitpanes'
import FlowInputEditor from '$lib/components/flows/content/FlowInputEditor.svelte'
import HistoricInputs from '$lib/components/HistoricInputs.svelte'
import SideBarTab from '$lib/components/meltComponents/SideBarTab.svelte'
import { History, Save } from 'lucide-svelte'
import CaptureIcon from '$lib/components/triggers/CaptureIcon.svelte'
import CaptureButton from './triggers/CaptureButton.svelte'
import SavedInputsPicker from './SavedInputsPicker.svelte'
import { createEventDispatcher } from 'svelte'
import CaptureTable from './triggers/CaptureTable.svelte'
import RefreshButton from './common/button/RefreshButton.svelte'
export let runnableId: string = ''
export let runnableType: any
export let previewArgs: any
export let isValid: boolean = true
export let jsonView: boolean = false
const dispatch = createEventDispatcher()
const getDropdownItems = () => {
return [
{
label: 'History',
onClick: () => {
if (selectedTab === 'history') {
selectedTab = undefined
rightPanelOpen = false
} else {
selectedTab = 'history'
rightPanelOpen = true
}
},
icon: History,
selected: selectedTab === 'history'
},
{
label: 'Saved inputs',
onClick: () => {
if (selectedTab === 'saved_inputs') {
selectedTab = undefined
rightPanelOpen = false
} else {
selectedTab = 'saved_inputs'
rightPanelOpen = true
}
},
icon: Save,
selected: selectedTab === 'saved_inputs'
},
{
label: 'Trigger captures',
onClick: () => {
if (selectedTab === 'captures') {
selectedTab = undefined
rightPanelOpen = false
} else {
selectedTab = 'captures'
rightPanelOpen = true
}
},
icon: CaptureIcon,
selected: selectedTab === 'captures'
}
]
}
let rightHeight = 0
let selectedTab: 'history' | 'saved_inputs' | 'captures' | undefined = undefined
let dropdownItems: any
let rightPanelOpen = false
let loading = false
let historicInputs: HistoricInputs | undefined = undefined
$: selectedTab, (dropdownItems = getDropdownItems())
</script>
<div class="h-fit">
<Splitpanes class={!rightPanelOpen ? 'splitter-hidden' : ''}>
<Pane class="!overflow-visible" size={70} minSize={30}>
<div class="relative w-full h-fit pr-12 pb-4 overflow-hidden" bind:clientHeight={rightHeight}>
<div class="absolute right-[1px] -top-[1px] z-50">
<SideBarTab
expandRight={rightPanelOpen}
{dropdownItems}
fullMenu={true}
noTrigger={true}
/>
</div>
<slot />
</div>
</Pane>
<Pane minSize={rightPanelOpen ? 30 : 0}>
{#if rightPanelOpen}
<div style="height: {rightHeight}px" class="border-t border-r pb-2">
{#if selectedTab === 'history'}
<FlowInputEditor title="History">
<svelete:fragment slot="action">
<div class="center-center">
<RefreshButton {loading} on:click={() => historicInputs?.refresh()} />
</div>
</svelete:fragment>
<HistoricInputs
bind:loading
bind:this={historicInputs}
{runnableId}
{runnableType}
on:select={(e) => {
dispatch('select', { payload: e.detail, type: 'history' })
}}
/>
</FlowInputEditor>
{:else if selectedTab === 'saved_inputs'}
<FlowInputEditor title="Saved inputs">
<SavedInputsPicker
{isValid}
{runnableId}
{runnableType}
{previewArgs}
on:select={(e) => {
dispatch('select', { payload: e.detail, type: 'saved' })
}}
{jsonView}
/>
</FlowInputEditor>
{:else if selectedTab === 'captures'}
<FlowInputEditor title="Trigger captures">
<svelete:fragment slot="action">
<div class="center-center">
<CaptureButton on:openTriggers small={true} />
</div>
</svelete:fragment>
<div class="h-full">
<CaptureTable
path={runnableId}
on:select={(e) => {
dispatch('select', { payload: e.detail, type: 'captures' })
}}
isFlow={true}
headless={true}
addButton={false}
/>
</div>
</FlowInputEditor>
{/if}
</div>
{/if}
</Pane>
</Splitpanes>
</div>
<style>
:global(.splitter-hidden .splitpanes__splitter) {
background-color: transparent !important;
border: none !important;
opacity: 0 !important;
}
</style>
+2 -1
View File
@@ -9,13 +9,14 @@
export let documentationLink: string | undefined = undefined
export let eeOnly = false
export let small: boolean = false
export let wrapperClass: string = ''
export let collapsable: boolean = false
export let collapsed: boolean = true
export let headless: boolean = false
</script>
<div class="w-full flex flex-col">
<div class={twMerge('w-full flex flex-col', wrapperClass)}>
{#if !headless}
<div class="flex flex-row justify-between items-center mb-2">
<h2
@@ -151,6 +151,7 @@
!$connectingInput.opened && selectId(e, id, selectedComponent, $app)}
>
<SchemaForm
onlyMaskPassword
defaultValues={resolvedConfig.defaultValues}
dynamicEnums={resolvedConfig.dynamicEnums}
schema={result}
@@ -156,5 +156,5 @@
</script>
{#if schema}
<SchemaForm {schema} bind:args />
<SchemaForm onlyMaskPassword {schema} bind:args />
{/if}
@@ -477,12 +477,27 @@ export async function getDbSchemas(
if (!testResult.success) {
console.error(testResult.result?.['error']?.['message'])
} else {
if (testResult.result === 'WINDMILL_TOO_BIG') {
console.info('Result is too big, fetching result separately')
const data = await JobService.getCompletedJobResult({
workspace,
id: job
})
testResult.result = data
}
if (resourceType !== undefined) {
if (resourceType !== 'graphql') {
const { processingFn } = scripts[resourceType]
const schema: any =
processingFn !== undefined ? processingFn(testResult.result) : testResult.result
let schema: any
try {
schema =
processingFn !== undefined ? processingFn(testResult.result) : testResult.result
} catch (e) {
console.error(e)
errorCallback('Error processing schema')
resolve()
return
}
const dbSchema = {
lang: resourceTypeToLang(resourceType) as SQLSchema['lang'],
schema,
@@ -769,6 +769,7 @@
{#if render && (autoRefresh || forceSchemaDisplay) && schemaStripped && Object.keys(schemaStripped?.properties ?? {}).length > 0}
<div class="px-2 h-fit min-h-0">
<SchemaForm
onlyMaskPassword
schema={schemaStripped}
appPath={defaultIfEmptyString($appPath, `u/${$userStore?.username ?? 'unknown'}/newapp`)}
{computeS3ForceViewerPolicies}
@@ -650,6 +650,7 @@ export async function main(app_path: string, startup_duration = 5, kind: 'pdf' |
{#if selectedTab !== 'custom' || customPath !== undefined}
{#key selectedTab + JSON.stringify(customPathSchema ?? {})}
<SchemaForm
onlyMaskPassword
bind:isValid={areArgsValid}
bind:args
schema={selectedTab !== 'custom'
@@ -1,5 +1,5 @@
<script lang="ts">
import type { AlertType } from './model'
import { type AlertType, classes } from './model'
import Tooltip from '$lib/components/Tooltip.svelte'
import {
AlertCircle,
@@ -38,34 +38,6 @@
success: CheckCircle2
}
const classes: Record<AlertType, Record<string, string>> = {
info: {
bgClass: 'bg-blue-50 border-blue-200 border dark:bg-blue-900/40 dark:border-blue-700/40',
iconClass: 'text-blue-500 dark:text-blue-300',
titleClass: 'text-blue-800 dark:text-blue-100',
descriptionClass: 'text-blue-700 dark:text-blue-100'
},
warning: {
bgClass:
'bg-yellow-50 border-yellow-200 border dark:bg-yellow-900/40 dark:border-yellow-700/40',
iconClass: 'text-yellow-500 dark:text-yellow-300/90',
titleClass: 'text-yellow-800 dark:text-yellow-100/90',
descriptionClass: 'text-yellow-700 dark:text-yellow-100/90'
},
error: {
bgClass: 'bg-red-50 border-red-200 border dark:bg-red-900/40 dark:border-red-700/40',
iconClass: 'text-red-500 dark:text-red-300/90',
titleClass: 'text-red-800 dark:text-red-100/90',
descriptionClass: 'text-red-700 dark:text-red-100/90'
},
success: {
bgClass: 'bg-green-50 border-green-200 border dark:bg-green-900/40 dark:border-green-700/40',
iconClass: 'text-green-500 dark:text-green-300/90',
titleClass: 'text-green-800 dark:text-green-100/90',
descriptionClass: 'text-green-700 dark:text-green-100/90'
}
}
function toggleCollapse() {
if (collapsible) {
isCollapsed = !isCollapsed
@@ -1 +1,29 @@
export type AlertType = 'success' | 'error' | 'warning' | 'info'
export const classes: Record<AlertType, Record<string, string>> = {
info: {
bgClass: 'bg-blue-50 border-blue-200 border dark:bg-blue-900/40 dark:border-blue-700/40',
iconClass: 'text-blue-500 dark:text-blue-300',
titleClass: 'text-blue-800 dark:text-blue-100',
descriptionClass: 'text-blue-700 dark:text-blue-100'
},
warning: {
bgClass:
'bg-yellow-50 border-yellow-200 border dark:bg-yellow-900/40 dark:border-yellow-700/40',
iconClass: 'text-yellow-500 dark:text-yellow-300/90',
titleClass: 'text-yellow-800 dark:text-yellow-100/90',
descriptionClass: 'text-yellow-700 dark:text-yellow-100/90'
},
error: {
bgClass: 'bg-red-50 border-red-200 border dark:bg-red-900/40 dark:border-red-700/40',
iconClass: 'text-red-500 dark:text-red-300/90',
titleClass: 'text-red-800 dark:text-red-100/90',
descriptionClass: 'text-red-700 dark:text-red-100/90'
},
success: {
bgClass: 'bg-green-50 border-green-200 border dark:bg-green-900/40 dark:border-green-700/40',
iconClass: 'text-green-500 dark:text-green-300/90',
titleClass: 'text-green-800 dark:text-green-100/90',
descriptionClass: 'text-green-700 dark:text-green-100/90'
}
}
@@ -9,6 +9,7 @@
export let open = false
export let id = (Math.random() + 1).toString(36).substring(10)
export let preventEscape = false
if (open) {
openedDrawers.push(id)
@@ -63,7 +64,10 @@
if (open) {
switch (event.key) {
case 'Escape':
if (id == openedDrawers[openedDrawers.length - 1] || openedDrawers.length == 0) {
if (
(id == openedDrawers[openedDrawers.length - 1] || openedDrawers.length == 0) &&
!preventEscape
) {
openedDrawers.pop()
event.preventDefault()
event.stopPropagation()
@@ -11,6 +11,7 @@
export let alwaysOpen = false
export let shouldUsePortal: boolean = true
export let offset: number = 0
export let preventEscape = false
let disposable: Disposable | undefined = undefined
@@ -71,6 +72,7 @@
bind:this={disposable}
on:open
on:close
{preventEscape}
>
<aside
class="drawer windmill-app windmill-drawer {$$props.class ?? ''} {$$props.positionClass ??
@@ -28,7 +28,7 @@
<div class="flex flex-col h-full">
<Tabs bind:selected wrapperClass="flex-none w-full">
<Tab value="saved_inputs">Saved Inputs</Tab>
<Tab value="saved_inputs">Inputs library</Tab>
{#if !isOperator}
<Tab value="triggers">Triggers</Tab>
{/if}
@@ -64,7 +64,7 @@
<Tab value="websockets">
<span class="flex flex-row gap-2 items-center text-xs">
<Unplug size={12} />
Websockets
WebSockets
</span>
</Tab>
<Tab value="postgres">
@@ -5,7 +5,7 @@
import FlowCard from '../common/FlowCard.svelte'
import type { FlowEditorContext } from '../types'
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
import JsonInputs from '$lib/components/JsonInputs.svelte'
import { convert } from '@redocly/json-to-json-schema'
import { sendUserToast } from '$lib/toast'
import EditableSchemaForm from '$lib/components/EditableSchemaForm.svelte'
@@ -29,7 +29,6 @@
import CaptureIcon from '$lib/components/triggers/CaptureIcon.svelte'
import FlowPreviewContent from '$lib/components/FlowPreviewContent.svelte'
import FlowInputEditor from './FlowInputEditor.svelte'
import CapturesInputs from '$lib/components/CapturesInputs.svelte'
import { twMerge } from 'tailwind-merge'
import CaptureButton from '$lib/components/triggers/CaptureButton.svelte'
import {
@@ -42,6 +41,7 @@
applyDiff
} from '$lib/components/schema/schemaUtils'
import SideBarTab from '$lib/components/meltComponents/SideBarTab.svelte'
import CaptureTable from '$lib/components/triggers/CaptureTable.svelte'
export let noEditor: boolean
export let disabled: boolean
@@ -49,7 +49,6 @@
const { flowStore, previewArgs, pathStore, initialPath, flowInputEditorState } =
getContext<FlowEditorContext>('FlowEditorContext')
let pendingJson: string
let addProperty: AddPropertyV2 | undefined = undefined
let previewSchema: Record<string, any> | undefined = undefined
let payloadData: Record<string, any> | undefined = undefined
@@ -65,6 +64,8 @@
let runDisabled: boolean = false
let editableSchemaForm: EditableSchemaForm | undefined = undefined
let savedPreviewArgs: Record<string, any> | undefined = undefined
let preventEscape = false
let isValid = true
function updateEditPanelSize(size: number | undefined) {
if (!$flowInputEditorState) return
@@ -244,19 +245,6 @@
previewArguments = structuredClone(payloadData)
}
function updatePayloadFromJson(jsonInput: string) {
if (jsonInput === undefined || jsonInput === null || jsonInput.trim() === '') {
updatePreviewSchemaAndArgs(undefined)
return
}
try {
const parsed = JSON.parse(jsonInput)
updatePreviewSchemaAndArgs(parsed)
} catch (error) {
updatePreviewSchemaAndArgs(undefined)
}
}
let tabButtonWidth = 0
let connectFirstNode: () => void = () => {}
@@ -325,7 +313,7 @@
<!-- Add svelte:window to listen for keyboard events -->
<svelte:window on:keydown={handleKeydown} />
<Drawer bind:open={previewOpen} alwaysOpen size="75%">
<Drawer bind:open={previewOpen} alwaysOpen size="75%" {preventEscape}>
<FlowPreviewContent
bind:this={flowPreviewContent}
open={previewOpen}
@@ -333,6 +321,7 @@
on:close={() => {
previewOpen = false
}}
bind:preventEscape
/>
</Drawer>
@@ -378,6 +367,7 @@
on:schemaChange={() => {
resetArgs()
}}
bind:isValid
>
<svelte:fragment slot="openEditTab">
<div class={twMerge('flex flex-row divide-x', ButtonType.ColorVariants.blue.divider)}>
@@ -470,9 +460,8 @@
}}
>
<HistoricInputs
scriptHash={null}
scriptPath={null}
flowPath={$pathStore}
runnableId={initialPath ?? undefined}
runnableType={$pathStore ? 'FlowPath' : undefined}
on:select={(e) => {
updatePreviewSchemaAndArgs(e.detail ?? undefined)
}}
@@ -490,12 +479,17 @@
<CaptureButton on:openTriggers small={true} />
</div>
</svelete:fragment>
<CapturesInputs
on:select={(e) => {
updatePreviewSchemaAndArgs(e.detail ?? undefined)
}}
flowPath={$pathStore}
/>
<div class="h-full">
<CaptureTable
path={$pathStore}
on:select={(e) => {
updatePreviewSchemaAndArgs(e.detail ?? undefined)
}}
isFlow={true}
headless={true}
addButton={false}
/>
</div>
</FlowInputEditor>
{:else if $flowInputEditorState?.selectedTab === 'savedInputs'}
<FlowInputEditor
@@ -505,7 +499,8 @@
title="Saved inputs"
>
<SavedInputsPicker
flowPath={initialPath}
runnableId={initialPath ?? undefined}
runnableType={$pathStore ? 'FlowPath' : undefined}
on:select={(e) => {
updatePreviewSchemaAndArgs(e.detail ?? undefined)
}}
@@ -513,6 +508,7 @@
preventEnter = e.detail
}}
previewArgs={previewArguments}
{isValid}
/>
</FlowInputEditor>
{:else if $flowInputEditorState?.selectedTab === 'json'}
@@ -522,26 +518,16 @@
}}
title="Json payload"
>
<SimpleEditor
<JsonInputs
on:focus={() => {
preventEnter = true
updatePayloadFromJson(pendingJson)
}}
on:blur={async () => {
preventEnter = false
setTimeout(() => {
if (payloadData) {
updatePayloadFromJson('')
}
}, 100)
}}
on:change={(e) => {
updatePayloadFromJson(e.detail.code)
on:select={(e) => {
updatePreviewSchemaAndArgs(e.detail ?? undefined)
}}
bind:code={pendingJson}
lang="json"
class="h-full"
placeholder={'Write a JSON payload. The input schema will be inferred.<br/><br/>Example:<br/><br/>{<br/>&nbsp;&nbsp;"foo": "12"<br/>}'}
/>
</FlowInputEditor>
{:else if $flowInputEditorState?.selectedTab === 'firstStepInputs'}
@@ -575,7 +561,7 @@
<Button
color="dark"
btnClasses="w-fit"
disabled={runDisabled}
disabled={runDisabled || !isValid}
size="xs"
shortCut={{ Icon: CornerDownLeft, hide: false }}
on:click={() => {
@@ -17,6 +17,7 @@
export async function openPreview(test: boolean = false) {
if (!previewOpen) {
previewOpen = true
flowPreviewContent?.refresh()
if (!test) return
}
flowPreviewContent?.test()
@@ -27,6 +28,7 @@
let flowPreviewContent: FlowPreviewContent
let jobId: string | undefined = undefined
let job: Job | undefined = undefined
let preventEscape = false
$: upToDisabled =
$selectedId == undefined ||
@@ -79,6 +81,9 @@
on:click={() => {
previewMode = 'whole'
previewOpen = !previewOpen
if (previewOpen) {
flowPreviewContent?.refresh()
}
}}
startIcon={{ icon: Play }}
id="flow-editor-test-flow"
@@ -86,7 +91,7 @@
Test flow
</Button>
<Drawer bind:open={previewOpen} alwaysOpen size="75%">
<Drawer bind:open={previewOpen} alwaysOpen size="75%" {preventEscape}>
<FlowPreviewContent
bind:this={flowPreviewContent}
open={previewOpen}
@@ -100,5 +105,6 @@
previewOpen = false
dispatch('openTriggers', e.detail)
}}
bind:preventEscape
/>
</Drawer>
@@ -7,6 +7,8 @@
export let closeButton: boolean = true
export let displayArrow: boolean = false
export let placement: any = 'bottom'
export let disablePopup: boolean = false
export let openOnHover: boolean = false
const {
elements: { trigger, content, arrow, close },
@@ -22,7 +24,14 @@
$: sync.open(open, (v) => (open = v))
</script>
<button class="w-full h-full" type="button" use:melt={$trigger} aria-label="Popup button">
<button
class="w-full h-full"
type="button"
use:melt={$trigger}
aria-label="Popup button"
on:mouseenter={() => (openOnHover ? (open = true) : null)}
on:mouseleave={() => (openOnHover ? (open = false) : null)}
>
<slot name="trigger" />
</button>
@@ -1,4 +1,6 @@
<script lang="ts">
import { twMerge } from 'tailwind-merge'
export let dropdownItems: Array<{
label: string
onClick: () => void
@@ -7,6 +9,8 @@
}> = []
export let fullMenu: boolean = false
export let noTrigger: boolean = false
export let expandRight = false
let open = false
let timeout: NodeJS.Timeout | null = null
@@ -25,21 +29,31 @@
}
open = false
}
let hasCloseButton = $$slots['close button']
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="flex flex-col relative !overflow-visible"
class="flex flex-col relative !overflow-visible {expandRight ? 'pr-2 -mr-2' : ''}"
on:mouseenter={handleMouseEnter}
on:mouseleave={handleMouseLeave}
>
<slot name="close button" />
{#if hasCloseButton}
<slot name="close button" />
{:else}
<div class="w-[31px]" />
{/if}
{#if fullMenu}
<div
class="absolute flex-col top-[30px] left-0 z-50 bg-surface border-l border-b {open
? 'rounded-md rounded-tl-none overflow-hidden shadow-md'
: 'rounded-bl-md'}"
class={twMerge(
'absolute flex-col left-0 z-50 bg-surface border-l border-b overflow-hidden',
hasCloseButton ? 'top-[30px]' : 'top-[1px]',
open ? 'rounded-md rounded-tl-none shadow-md' : 'rounded-bl-md',
hasCloseButton ? '' : 'rounded-tl-md border-t',
noTrigger && !dropdownItems.some((item) => item.selected) ? 'rounded-md border' : ''
)}
>
{#each dropdownItems as item}
<button
@@ -0,0 +1,30 @@
<script lang="ts">
import { classes } from '$lib/components/common/alert/model'
import { twMerge } from 'tailwind-merge'
import Button from '$lib/components/common/button/Button.svelte'
import { X } from 'lucide-svelte'
export let inputSelected: 'history' | 'captures' | 'saved' | undefined = undefined
</script>
<div class="min-h-[34px]">
<div
class="rounded-md flex flex-row gap-2 items-center py-1 px-2 w-fit {classes['info']
.bgClass} {inputSelected ? '' : 'hidden'}"
>
<p class={twMerge(classes['info'].descriptionClass, 'text-xs px-2')}>
Using {inputSelected === 'history'
? 'historic'
: inputSelected === 'captures'
? 'captures'
: 'saved'} input arguments
</p>
<Button
color="light"
size="xs2"
startIcon={{ icon: X }}
shortCut={{ key: 'esc', withoutModifier: true }}
nonCaptureEvent
/>
</div>
</div>
@@ -19,6 +19,7 @@
export let shouldDispatchChanges: boolean = false
export let diff: Record<string, SchemaDiff> = {}
export let nestedClasses = ''
export let isValid: boolean = true
const dispatch = createEventDispatcher()
const flipDurationMs = 200
@@ -98,6 +99,7 @@
{diff}
{nestedParent}
{shouldDispatchChanges}
bind:isValid
>
<svelte:fragment slot="actions">
{#if !disableDnd}
@@ -5,7 +5,7 @@
import { twMerge } from 'tailwind-merge'
import Cell from '$lib/components/table/Cell.svelte'
export let payloadData: Record<string, any>
export let payloadData: Record<string, any> | string
export let date: string | undefined
export let selected = false
export let hovering = false
@@ -58,7 +58,7 @@
<Cell
wrap
class="text-2xs font-normal text-left p-2 rounded-md overflow-auto text-ellipsis scrollbar-none"
class="text-2xs font-normal text-left py-2 rounded-md overflow-auto text-ellipsis scrollbar-none"
title={formatDate(date)}
>
{formatDateShort(date)}
@@ -70,7 +70,7 @@
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class={twMerge(
'text-xs border w-full font-normal text-left p-1 rounded-md whitespace-nowrap overflow-hidden text-ellipsis',
'text-xs border w-full font-normal text-tertiary text-left p-1 rounded-md whitespace-nowrap overflow-hidden text-ellipsis',
hovering && 'border-surface'
)}
on:click={() => {
@@ -82,7 +82,15 @@
{JSON.stringify(payloadData)}
</div>
<svelte:fragment slot="overlay">
<ObjectViewer json={payloadData} />
{#if payloadData === 'WINDMILL_TOO_BIG'}
<div class="text-center text-tertiary text-xs">
Payload too big to preview but can still be loaded
</div>
{:else}
<div class="max-w-60 overflow-auto">
<ObjectViewer json={payloadData} />
</div>
{/if}
</svelte:fragment>
</CustomPopover>
@@ -268,10 +268,10 @@
/>
{/if}
</div>
<p class="italic text-xs"
>An email notification will be sent to {premiumInfo.owner} if the specified threshold amount
is reached during a given month.</p
>
<p class="italic text-xs">
An email notification will be sent to {premiumInfo.owner} if the specified threshold amount
is exceeded during a given month.
</p>
</div>
{/if}
<div class="w-full">
@@ -95,7 +95,7 @@
kind: 'http'
},
{
label: 'Websockets',
label: 'WebSockets',
href: '/websocket_triggers',
icon: Unplug,
disabled: $userStore?.operator,
@@ -31,7 +31,7 @@
let tableHeight: number = 0
const dispatch = createEventDispatcher()
let tableContainer: HTMLDivElement
let isLoading = false
export let loading = false
setContext<DatatableContext>('datatable', {
size
@@ -40,23 +40,19 @@
$: contentHeight = tableHeight - footerHeight
function checkScrollStatus() {
if (!infiniteScroll || isLoading) return
if (!infiniteScroll || loading) return
const hasScrollbar = tableContainer.scrollHeight > tableContainer.clientHeight
if (!hasScrollbar && hasMore) {
isLoading = true
dispatch('loadMore')
setTimeout(() => {
isLoading = false
}, 200)
}
}
function handleScroll() {
if (!infiniteScroll || isLoading) {
if (isLoading) {
if (!infiniteScroll || loading) {
if (loading) {
const checkAgain = () => {
if (!isLoading) {
if (!loading) {
handleScroll()
}
}
@@ -67,15 +63,11 @@
const { scrollTop, scrollHeight, clientHeight } = tableContainer
if (scrollHeight - (scrollTop + clientHeight) < 50) {
isLoading = true
dispatch('loadMore')
setTimeout(() => {
isLoading = false
}, 1000)
}
}
$: if (tableContainer && hasMore && !isLoading) {
$: if (tableContainer && hasMore && !loading) {
checkScrollStatus()
}
</script>
@@ -30,7 +30,7 @@
export let captureTable: CaptureTable | undefined
const dispatch = createEventDispatcher<{
captureToggle: undefined
captureToggle: { disableOnly?: boolean }
updateSchema: { payloadData: Record<string, any>; redirect: boolean }
}>()
@@ -38,7 +38,10 @@
onDestroy(() => {
if (captureInfo.active) {
dispatch('captureToggle')
dispatch('captureToggle', {
// this on destroy can be called after capturing has already been stopped (aka after on destroy of the wrapper), make sure we do not start it again
disableOnly: true
})
}
})
@@ -70,7 +73,7 @@
<AnimatedButton animate={captureInfo.active} baseRadius="6px" wrapperClasses="ml-[-2px]">
<Button
size="xs2"
on:click={() => dispatch('captureToggle')}
on:click={() => dispatch('captureToggle', {})}
variant="border"
{disabled}
color="light"
@@ -7,7 +7,6 @@
import CustomPopover from '../CustomPopover.svelte'
import { Webhook, Route, Unplug, Mail, Play } from 'lucide-svelte'
import KafkaIcon from '$lib/components/icons/KafkaIcon.svelte'
import { isObject } from '$lib/utils'
import { createEventDispatcher, onDestroy } from 'svelte'
import { type TriggerKind } from '../triggers'
import { CaptureService } from '$lib/gen'
@@ -15,9 +14,10 @@
import { type CaptureTriggerKind } from '$lib/gen'
import CaptureButton from '$lib/components/triggers/CaptureButton.svelte'
import InfiniteList from '../InfiniteList.svelte'
import { sendUserToast } from '$lib/utils'
import { isObject, sendUserToast } from '$lib/utils'
import SchemaPickerRow from '$lib/components/schema/SchemaPickerRow.svelte'
import { clickOutside } from '$lib/utils'
import type { Capture } from '$lib/gen'
export let path: string
export let hasPreprocessor = false
@@ -57,6 +57,11 @@
testWithArgs: any
}>()
interface CaptureWithPayload extends Capture {
getFullCapture?: () => Promise<any>
payloadData?: any
}
export async function loadCaptures(refresh: boolean = false) {
await infiniteList?.loadData(refresh ? 'refresh' : 'loadMore')
}
@@ -71,19 +76,36 @@
page,
perPage
})
let capturesWithPayload = captures.map((capture) => {
const payload = isObject(capture.payload) ? capture.payload : {}
const triggerExtra = isObject(capture.trigger_extra) ? capture.trigger_extra : {}
return {
...capture,
payloadData:
testKind === 'preprocessor'
? {
...payload,
...triggerExtra
}
: { ...payload }
let capturesWithPayload: CaptureWithPayload[] = captures.map((capture) => {
let newCapture: CaptureWithPayload = { ...capture }
if (capture.payload === 'WINDMILL_TOO_BIG') {
newCapture = {
...capture,
getFullCapture: () =>
CaptureService.getCapture({
workspace: $workspaceStore!,
id: capture.id
})
}
}
const trigger_extra = isObject(capture.trigger_extra) ? capture.trigger_extra : {}
newCapture.payloadData =
testKind === 'preprocessor'
? capture.payload === 'WINDMILL_TOO_BIG'
? {
payload: capture.payload,
...trigger_extra
}
: typeof capture.payload === 'object'
? {
...capture.payload,
...trigger_extra
}
: trigger_extra
: capture.payload
return newCapture
})
return capturesWithPayload
}
@@ -98,15 +120,33 @@
infiniteList?.setDeleteItemFn(deleteInputFn)
}
function handleSelect(capture: any) {
async function handleSelect(capture: any) {
if (selected === capture.id) {
deselect()
} else {
const payloadData = await getPayload(capture)
selected = capture.id
dispatch('select', capture.payloadData)
dispatch('select', structuredClone(payloadData))
}
}
async function getPayload(capture: any) {
let payloadData = {}
if (capture.getFullCapture) {
const fullCapture = await capture.getFullCapture()
payloadData =
testKind === 'preprocessor'
? {
...(typeof fullCapture.payload === 'object' ? fullCapture.payload : {}),
...(typeof fullCapture.trigger_extra === 'object' ? fullCapture.trigger_extra : {})
}
: fullCapture.payload
} else {
payloadData = structuredClone(capture.payloadData)
}
return payloadData
}
function deselect() {
selected = undefined
dispatch('select', undefined)
@@ -209,7 +249,7 @@
<svelte:fragment slot="columns">
<colgroup>
<col class="w-8" />
<col class="w-20" />
<col class="w-16" />
<col />
</colgroup>
</svelte:fragment>
@@ -265,9 +305,10 @@
dropdownItems={[
{
label: 'Use as input schema',
onClick: () => {
onClick: async () => {
const payloadData = await getPayload(item)
dispatch('updateSchema', {
payloadData: item.payloadData,
payloadData,
redirect: true,
args: true
})
@@ -275,20 +316,23 @@
disabled: !isFlow || testKind !== 'main'
}
].filter((item) => !item.disabled)}
on:click={() => {
on:click={async () => {
const payloadData = await getPayload(item)
if (isFlow && testKind === 'main') {
dispatch('testWithArgs', item.payloadData)
dispatch('testWithArgs', payloadData)
} else {
dispatch('applyArgs', {
kind: testKind,
args: item.payloadData
args: payloadData
})
}
}}
disabled={testKind === 'preprocessor' && !hasPreprocessor}
title={isFlow && testKind === 'main'
? 'Test flow with args'
: 'Apply args to preprocessor'}
: testKind === 'preprocessor'
? 'Apply args to preprocessor'
: 'Apply args to inputs'}
startIcon={isFlow && testKind === 'main' ? { icon: Play } : {}}
>
{isFlow && testKind === 'main' ? 'Test' : 'Apply args'}
@@ -110,12 +110,12 @@
)
}
export async function handleCapture() {
if (!captureActive) {
export async function handleCapture(e: CustomEvent<{ disableOnly?: boolean }>) {
if (captureActive || e.detail.disableOnly) {
captureActive = false
} else {
await setConfig()
capture()
} else {
captureActive = false
}
}
@@ -178,9 +178,7 @@
on:applyArgs
on:updateSchema
on:addPreprocessor
on:captureToggle={() => {
handleCapture()
}}
on:captureToggle={handleCapture}
on:testWithArgs
/>
{:else if captureType === 'webhook'}
@@ -197,9 +195,7 @@
on:applyArgs
on:updateSchema
on:addPreprocessor
on:captureToggle={() => {
handleCapture()
}}
on:captureToggle={handleCapture}
on:testWithArgs
/>
{:else if captureType === 'http'}
@@ -209,16 +205,15 @@
{showCapture}
can_write={true}
runnableArgs={data?.args}
bind:args
bind:route_path={args.route_path}
bind:http_method={args.http_method}
headless
{captureInfo}
bind:captureTable
on:applyArgs
on:updateSchema
on:addPreprocessor
on:captureToggle={() => {
handleCapture()
}}
on:captureToggle={handleCapture}
on:testWithArgs
/>
{:else if captureType === 'email'}
@@ -235,9 +230,7 @@
on:applyArgs
on:updateSchema
on:addPreprocessor
on:captureToggle={() => {
handleCapture()
}}
on:captureToggle={handleCapture}
on:testWithArgs
/>
{:else if captureType === 'kafka'}
@@ -252,9 +245,7 @@
on:applyArgs
on:updateSchema
on:addPreprocessor
on:captureToggle={() => {
handleCapture()
}}
on:captureToggle={handleCapture}
on:testWithArgs
/>
{:else if captureType === 'nats'}
@@ -269,9 +260,7 @@
on:applyArgs
on:updateSchema
on:addPreprocessor
on:captureToggle={() => {
handleCapture()
}}
on:captureToggle={handleCapture}
/>
{/if}
</div>
@@ -13,7 +13,7 @@
export let args: Record<string, any>
const kindToName: { [key: string]: string } = {
websocket: 'Websocket',
websocket: 'WebSocket',
nats: 'NATS server(s)',
kafka: 'Kafka broker(s)'
}
@@ -47,7 +47,6 @@
await promise
sendUserToast(`Successfully connected to ${kindToName[kind]}`)
} catch (err) {
if (!promise?.isCancelled) {
sendUserToast(`Error testing ${kindToName[kind]}: ${err?.body ?? 'Unknown error'}`, true)
}
@@ -53,7 +53,7 @@
<Tab value="webhooks" selectedClass="text-primary font-semibold">Webhooks</Tab>
<Tab value="schedules" selectedClass="text-primary text-sm font-semibold">Schedules</Tab>
<Tab value="routes" selectedClass="text-primary text-sm font-semibold">HTTP</Tab>
<Tab value="websockets" selectedClass="text-primary text-sm font-semibold">Websockets</Tab>
<Tab value="websockets" selectedClass="text-primary text-sm font-semibold">WebSockets</Tab>
<Tab value="postgres" selectedClass="text-primary text-sm font-semibold">Postgres</Tab>
<Tab
value="kafka"
@@ -24,9 +24,9 @@
const captureTypeLabels: Record<CaptureTriggerKind, string> = {
http: 'New custom HTTP route',
websocket: 'New websocket trigger',
websocket: 'New WebSocket trigger',
webhook: 'Webhook',
kafka: 'New kafka trigger',
kafka: 'New Kafka trigger',
email: 'Email trigger',
nats: 'NATS trigger'
}
@@ -44,7 +44,8 @@
<RouteEditorConfigSection
showCapture={false}
can_write={true}
bind:args
bind:route_path={args.route_path}
bind:http_method={args.http_method}
headless
{isFlow}
{path}
@@ -19,10 +19,9 @@
export let isFlow: boolean
export let path: string
export let args: Record<string, any> = { route_path: '', http_method: 'get' }
export let dirtyRoutePath: boolean = false
export let route_path = ''
export let http_method: 'get' | 'post' | 'put' | 'patch' | 'delete' = 'post'
export let route_path: string | undefined
export let http_method: 'get' | 'post' | 'put' | 'patch' | 'delete' | undefined
export let can_write: boolean = false
export let static_asset_config: { s3: string; storage?: string; filename?: string } | undefined =
undefined
@@ -36,12 +35,15 @@
let validateTimeout: NodeJS.Timeout | undefined = undefined
let routeError: string = ''
async function validateRoute(path: string, method: typeof http_method): Promise<void> {
async function validateRoute(
path: string | undefined,
method: typeof http_method
): Promise<void> {
if (validateTimeout) {
clearTimeout(validateTimeout)
}
validateTimeout = setTimeout(async () => {
if (!/^[\w-:]+(\/[\w-:]+)*$/.test(path)) {
if (!path || !method || !/^[\w-:]+(\/[\w-:]+)*$/.test(path)) {
routeError = 'Endpoint not valid'
} else if (initialRoutePath !== path && (await routeExists(path, method))) {
routeError = 'Endpoint already taken'
@@ -52,7 +54,7 @@
}, 500)
}
async function routeExists(route_path: string, method: typeof http_method) {
async function routeExists(route_path: string, method: Exclude<typeof http_method, undefined>) {
return await HttpTriggerService.existsRoute({
workspace: $workspaceStore!,
requestBody: {
@@ -67,9 +69,9 @@
}/${path.replaceAll('/', '.')}/${route_path}`
function getHttpRoute(route_path: string | undefined) {
return `${location.origin}${base}/api/r/${
isCloudHosted() ? $workspaceStore + '/' : ''
}${route_path}`
return `${location.origin}${base}/api/r/${isCloudHosted() ? $workspaceStore + '/' : ''}${
route_path ?? ''
}`
}
$: validateRoute(route_path, http_method)
@@ -78,13 +80,8 @@
$: fullRoute = getHttpRoute(route_path)
$: showCapture && (http_method = 'post')
function updateArgs(route_path: string, http_method: string) {
args && ((args.route_path = route_path), (args.http_method = http_method))
}
$: updateArgs(route_path, http_method)
$: !http_method && (http_method = 'post')
$: route_path === undefined && (route_path = '')
</script>
<div>
@@ -108,7 +105,7 @@
<CopyableCodeBlock
disabled={!captureInfo.active}
code={`curl \\
-X ${http_method.toUpperCase()} ${captureURL} \\
-X ${(http_method ?? 'post').toUpperCase()} ${captureURL} \\
-H 'Content-Type: application/json' \\
-d '${JSON.stringify(runnableArgs ?? {}, null, 2)}'`}
language={bash}
@@ -166,10 +166,6 @@
let drawer: Drawer
let dirtyPath = false
let args: Record<string, any> = { route_path: '' }
$: args && (route_path = args.route_path)
</script>
{#if static_asset_config}
@@ -227,7 +223,6 @@
isFlow={is_flow}
{path}
bind:route_path
bind:args
bind:isValid
bind:dirtyRoutePath
bind:http_method
@@ -46,7 +46,7 @@
dirtyPath = false
await loadTrigger()
} catch (err) {
sendUserToast(`Could not load kafka trigger: ${err}`, true)
sendUserToast(`Could not load Kafka trigger: ${err}`, true)
} finally {
drawerLoading = false
}
@@ -154,9 +154,9 @@
<DrawerContent
title={edit
? can_write
? `Edit kafka trigger ${initialPath}`
? `Edit Kafka trigger ${initialPath}`
: `Kafka trigger ${initialPath}`
: 'New kafka trigger'}
: 'New Kafka trigger'}
on:close={drawer.closeDrawer}
>
<svelte:fragment slot="actions">
@@ -173,7 +173,7 @@
workspace: $workspaceStore ?? '',
requestBody: { enabled: e.detail }
})
sendUserToast(`${e.detail ? 'enabled' : 'disabled'} kafka trigger ${initialPath}`)
sendUserToast(`${e.detail ? 'enabled' : 'disabled'} Kafka trigger ${initialPath}`)
}}
/>
</div>
@@ -51,7 +51,7 @@
})
$triggersCount = { ...($triggersCount ?? {}), kafka_count: kafkaTriggers?.length }
} catch (e) {
console.error('impossible to load kafka triggers', e)
console.error('impossible to load Kafka triggers', e)
}
}
@@ -108,7 +108,7 @@
{#if kafkaTriggers}
<Section label="Kafka triggers">
{#if kafkaTriggers.length == 0}
<div class="text-xs text-secondary text-center"> No kafka triggers </div>
<div class="text-xs text-secondary text-center"> No Kafka triggers </div>
{:else}
<div class="flex flex-col divide-y pt-2">
{#each kafkaTriggers as kafkaTrigger (kafkaTrigger.path)}
@@ -56,7 +56,8 @@
publication: publication_name
})
items = items.filter((item) => item != publication_name)
table_to_track = []
relations = []
transaction_to_track = ['Insert', 'Update', 'Delete']
publication_name = ''
sendUserToast(message)
} catch (error) {

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