mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-14 08:02:58 +00:00
Merge remote-tracking branch 'origin/main' into breadcrumb-leaf-navigation-popover
# Conflicts: # backend/ee-repo-ref.txt # frontend/src/lib/components/NavBreadcrumb.svelte # frontend/src/lib/components/PageHeaderBar.svelte # frontend/src/lib/components/PageHeaderContent.svelte # frontend/src/lib/components/details/DetailPageHeader.svelte # frontend/src/lib/components/pageHeaderRegistry.svelte.ts # frontend/src/lib/components/pageHeaderStages.test.ts # frontend/src/lib/components/pageHeaderStages.ts # frontend/src/lib/components/runs/RunDetail.svelte
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "1.831.0"
|
||||
".": "1.832.0"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# Changelog
|
||||
|
||||
## [1.832.0](https://github.com/windmill-labs/windmill/compare/v1.831.0...v1.832.0) (2026-10-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* record each instance's python version and start new ones on 3.13 ([#11656](https://github.com/windmill-labs/windmill/issues/11656)) ([7558ce2](https://github.com/windmill-labs/windmill/commit/7558ce27a9736bef1a9b9db9b0799150dccb11a5))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* badge the unavailable sign-in option in the oauth resource form ([#11728](https://github.com/windmill-labs/windmill/issues/11728)) ([9c44f5a](https://github.com/windmill-labs/windmill/commit/9c44f5a917fc628e870073c126bf9480c98c7f73))
|
||||
* serve s3 proxy objects with nosniff and a sandbox csp ([#11731](https://github.com/windmill-labs/windmill/issues/11731)) ([e29a0dc](https://github.com/windmill-labs/windmill/commit/e29a0dc5dfed4bb62b894e059c9506873d32a62c))
|
||||
* widen variable.value without breaking live servers' cached queries ([#11737](https://github.com/windmill-labs/windmill/issues/11737)) ([89e6bdb](https://github.com/windmill-labs/windmill/commit/89e6bdbcf50e065f6906e1ab93b3c4109aba3ab6))
|
||||
|
||||
## [1.831.0](https://github.com/windmill-labs/windmill/compare/v1.830.0...v1.831.0) (2026-10-13)
|
||||
|
||||
|
||||
|
||||
Generated
+79
-79
@@ -15486,7 +15486,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -15576,7 +15576,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-ai"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
@@ -15611,7 +15611,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-alerting"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"chrono",
|
||||
@@ -15624,7 +15624,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -15767,7 +15767,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-agent-workers"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"chrono",
|
||||
@@ -15790,7 +15790,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-assets"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"chrono",
|
||||
@@ -15807,7 +15807,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-auth"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -15833,7 +15833,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
@@ -15843,7 +15843,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-configs"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"chrono",
|
||||
@@ -15860,7 +15860,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-debug"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"base64 0.22.1",
|
||||
@@ -15882,7 +15882,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-embeddings"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -15905,7 +15905,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-flow-conversations"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"chrono",
|
||||
@@ -15921,7 +15921,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-flows"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"chrono",
|
||||
@@ -15943,7 +15943,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-groups"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"chrono",
|
||||
@@ -15965,7 +15965,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-inputs"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"chrono",
|
||||
@@ -15979,7 +15979,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-integration-tests"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -16015,7 +16015,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-jobs"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -16041,7 +16041,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-npm-proxy"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"chrono",
|
||||
@@ -16069,7 +16069,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-openapi"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -16091,7 +16091,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-schedule"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"chrono",
|
||||
@@ -16111,7 +16111,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-scripts"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -16153,7 +16153,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-settings"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -16182,7 +16182,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-sse"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"serde",
|
||||
@@ -16194,7 +16194,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-users"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"axum",
|
||||
@@ -16218,7 +16218,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-workers"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"chrono",
|
||||
@@ -16232,7 +16232,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-workspaces"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"chrono",
|
||||
@@ -16271,7 +16271,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"lazy_static",
|
||||
@@ -16286,7 +16286,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-autoscaling"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -16305,7 +16305,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"aho-corasick",
|
||||
@@ -16421,7 +16421,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-dep-map"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"futures",
|
||||
@@ -16441,7 +16441,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-git-sync"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -16458,7 +16458,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-indexer"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"astral-tokio-tar",
|
||||
@@ -16485,7 +16485,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-jseval"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
@@ -16502,7 +16502,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"lazy_static",
|
||||
@@ -16518,7 +16518,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-mcp"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16540,7 +16540,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-native-triggers"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16571,7 +16571,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-oauth"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -16596,7 +16596,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-object-store"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@@ -16632,7 +16632,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-operator"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
@@ -16651,7 +16651,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"serde",
|
||||
@@ -16660,7 +16660,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16672,7 +16672,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -16684,7 +16684,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -16696,7 +16696,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16708,7 +16708,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-java"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -16720,7 +16720,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-nu"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nu-parser",
|
||||
@@ -16731,7 +16731,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -16742,7 +16742,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -16754,7 +16754,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-asset"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"ruff_python_ast",
|
||||
@@ -16765,7 +16765,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -16788,7 +16788,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-r"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -16800,7 +16800,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ruby"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16814,7 +16814,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case 0.6.0",
|
||||
@@ -16831,7 +16831,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16844,7 +16844,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql-asset"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -16856,7 +16856,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16874,7 +16874,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts-asset"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde-wasm-bindgen",
|
||||
@@ -16890,7 +16890,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wac"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"ruff_python_ast",
|
||||
@@ -16907,7 +16907,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -16921,7 +16921,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -16960,7 +16960,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-runtime-nativets"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"const_format",
|
||||
@@ -16998,7 +16998,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-sql-datatype-parser-wasm"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"wasm-bindgen",
|
||||
@@ -17009,7 +17009,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-store"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -17044,7 +17044,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-test-utils"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17068,7 +17068,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17101,7 +17101,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-amqp"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17128,7 +17128,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-azure"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17161,7 +17161,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-email"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17181,7 +17181,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-gcp"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17215,7 +17215,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-http"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17252,7 +17252,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-kafka"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17275,7 +17275,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-mqtt"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17299,7 +17299,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-nats"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -17323,7 +17323,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-postgres"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17358,7 +17358,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-sqs"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17386,7 +17386,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-websocket"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17411,7 +17411,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-types"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.13.2",
|
||||
@@ -17430,7 +17430,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-once-cell",
|
||||
@@ -17549,7 +17549,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker-volumes"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -88,7 +88,7 @@ members = [
|
||||
exclude = ["./windmill-duckdb-ffi-internal", "./parsers/windmill-parser-wasm"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
9cd6a70486df465294f23071fbf383f21ef47ba9
|
||||
581b18cac39a4bee2671fd1aa86f91b9e702e8f5
|
||||
|
||||
@@ -446,7 +446,7 @@
|
||||
],
|
||||
"req_body_auth": true
|
||||
},
|
||||
"outlook": {
|
||||
"ms_outlook": {
|
||||
"req_body_auth": true,
|
||||
"scopes": ["offline_access", "User.Read", "Mail.ReadWrite", "Mail.Send", "Calendars.ReadWrite", "Contacts.ReadWrite"],
|
||||
"scope_options": [
|
||||
|
||||
+24
-24
@@ -6340,7 +6340,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"aho-corasick",
|
||||
@@ -6430,7 +6430,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -6442,7 +6442,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"serde",
|
||||
@@ -6451,7 +6451,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6463,7 +6463,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -6475,7 +6475,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -6487,7 +6487,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6499,7 +6499,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-java"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -6511,7 +6511,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-nu"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nu-parser",
|
||||
@@ -6522,7 +6522,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -6533,7 +6533,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -6545,7 +6545,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-asset"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"ruff_python_ast",
|
||||
@@ -6556,7 +6556,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -6579,7 +6579,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-r"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -6591,7 +6591,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ruby"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6605,7 +6605,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case",
|
||||
@@ -6622,7 +6622,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6635,7 +6635,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql-asset"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -6647,7 +6647,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6665,7 +6665,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts-asset"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde-wasm-bindgen",
|
||||
@@ -6681,7 +6681,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wac"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"ruff_python_ast",
|
||||
@@ -6698,7 +6698,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wasm"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"getrandom 0.2.17",
|
||||
@@ -6730,7 +6730,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -6744,7 +6744,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-types"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
|
||||
@@ -12,7 +12,7 @@ resolver = "2"
|
||||
members = ["."]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
edition = "2021"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
|
||||
|
||||
@@ -153,6 +153,9 @@ pub async fn connect(
|
||||
pool_options = pool_options.idle_timeout(Duration::from_secs(60));
|
||||
}
|
||||
pool_options
|
||||
.before_acquire(|_, meta| {
|
||||
Box::pin(async move { Ok(!windmill_common::db::connection_is_retired(meta.age)) })
|
||||
})
|
||||
.after_connect(move |conn, _| {
|
||||
if worker_mode {
|
||||
Box::pin(async move {
|
||||
|
||||
@@ -4469,12 +4469,14 @@ pub async fn monitor_db(
|
||||
|
||||
// Re-encrypt the legacy-format secret variables once new values are written in AES-256-GCM.
|
||||
// Every ~5 min until one pass has gone through every workspace; spawned so a large instance
|
||||
// does not hold up the join below.
|
||||
// does not hold up the join below. The column widening it starts with is tried every
|
||||
// iteration: a v2 value too long for the narrow column fails to save until it is done.
|
||||
let legacy_secret_upgrade_f = async {
|
||||
if server_mode
|
||||
&& iteration.is_some()
|
||||
&& iteration.as_ref().unwrap().should_run(30)
|
||||
&& windmill_common::variables::legacy_secret_upgrade_pending()
|
||||
&& (windmill_common::variables::variable_value_widen_pending()
|
||||
|| (iteration.as_ref().unwrap().should_run(30)
|
||||
&& windmill_common::variables::legacy_secret_upgrade_pending()))
|
||||
{
|
||||
if let Some(db) = conn.as_sql() {
|
||||
let db = db.clone();
|
||||
|
||||
@@ -1580,3 +1580,163 @@ async fn test_backups_fall_back_to_the_instance_storage(db: Pool<Postgres>) -> a
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Runs its closure when dropped, so process-wide state is put back even when an assertion
|
||||
/// fails.
|
||||
struct OnDrop<F: FnMut()>(F);
|
||||
impl<F: FnMut()> Drop for OnDrop<F> {
|
||||
fn drop(&mut self) {
|
||||
(self.0)()
|
||||
}
|
||||
}
|
||||
|
||||
/// Once every worker reads AES-256-GCM and this server has been up long enough for any server
|
||||
/// of the previous release to be gone, objects are written in it: a session backed up as CBC
|
||||
/// before the switch still restores whole with the GCM pieces added after it, a head at its
|
||||
/// cap and a push token still read back under the larger ciphertext, and relocated GCM
|
||||
/// ciphertext does not decrypt for another user.
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_backups_switch_to_gcm_once_every_worker_reads_it(
|
||||
db: Pool<Postgres>,
|
||||
) -> anyhow::Result<()> {
|
||||
use windmill_common::min_version::{
|
||||
MIN_VERSION, MIN_VERSION_SUPPORTS_AEAD_SECRETS, MIN_VERSION_SUPPORTS_JOB_TOKEN_SCOPES,
|
||||
};
|
||||
use windmill_common::workspace_crypt::V2_MAGIC;
|
||||
|
||||
initialize_tracing().await;
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let base = format!(
|
||||
"http://localhost:{}/api/w/test-workspace",
|
||||
server.addr.port()
|
||||
);
|
||||
let storage_dir = tempfile::tempdir()?;
|
||||
configure_primary_lfs(&db, &storage_dir.path().to_string_lossy()).await?;
|
||||
let before = MIN_VERSION.load_full();
|
||||
let _restore = OnDrop(move || MIN_VERSION.store(before.clone()));
|
||||
let _uptime = OnDrop(|| windmill_common::db::set_process_uptime_offset(Default::default()));
|
||||
let is_gcm = |b: &[u8]| b.starts_with(&V2_MAGIC);
|
||||
let object = |path: &str| -> Vec<u8> {
|
||||
std::fs::read(user_root(storage_dir.path(), "test@windmill.dev").join(path)).unwrap()
|
||||
};
|
||||
|
||||
MIN_VERSION.store(std::sync::Arc::new(
|
||||
MIN_VERSION_SUPPORTS_JOB_TOKEN_SCOPES.version().clone(),
|
||||
));
|
||||
let resp = push(
|
||||
&base,
|
||||
"SECRET_TOKEN",
|
||||
json!({
|
||||
"owner": "test@windmill.dev",
|
||||
"sessions": [{
|
||||
"id": "s1",
|
||||
"whole": true,
|
||||
"head": { "id": "s1", "workspace_id": "test-workspace", "createdAt": 1, "chatId": "c1" },
|
||||
"chats": [{ "id": "c1", "record": { "id": "c1", "title": "BEFORE" } }]
|
||||
}]
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(resp.status(), 200, "{}", resp.text().await?);
|
||||
let cbc_chat = object("sessions/s1/chats/c1.json");
|
||||
assert!(!cbc_chat.is_empty() && !is_gcm(&cbc_chat));
|
||||
|
||||
MIN_VERSION.store(std::sync::Arc::new(
|
||||
MIN_VERSION_SUPPORTS_AEAD_SECRETS.version().clone(),
|
||||
));
|
||||
// Every worker reads it, but this server was only just started: still CBC.
|
||||
let resp = push(
|
||||
&base,
|
||||
"SECRET_TOKEN",
|
||||
json!({
|
||||
"owner": "test@windmill.dev",
|
||||
"sessions": [{ "id": "s1", "chats": [{ "id": "c0", "record": { "id": "c0", "title": "FRESH" } }] }]
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(resp.status(), 200, "{}", resp.text().await?);
|
||||
let fresh_chat = object("sessions/s1/chats/c0.json");
|
||||
assert!(!fresh_chat.is_empty() && !is_gcm(&fresh_chat));
|
||||
|
||||
windmill_common::db::set_process_uptime_offset(
|
||||
windmill_common::db::PREVIOUS_RELEASE_ROLLOUT_UPTIME,
|
||||
);
|
||||
let resp = push(
|
||||
&base,
|
||||
"SECRET_TOKEN",
|
||||
json!({
|
||||
"owner": "test@windmill.dev",
|
||||
"sessions": [{
|
||||
"id": "s1",
|
||||
"chats": [{ "id": "c2", "record": { "id": "c2", "title": "AFTER" } }],
|
||||
"images": [{ "chat_id": "c2", "id": "i1", "data_url": "data:image/png;base64,AAAA" }]
|
||||
}]
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(resp.status(), 200, "{}", resp.text().await?);
|
||||
assert_eq!(
|
||||
object("sessions/s1/chats/c1.json"),
|
||||
cbc_chat,
|
||||
"not rewritten"
|
||||
);
|
||||
assert!(is_gcm(&object("sessions/s1/chats/c2.json")));
|
||||
assert!(is_gcm(&object("images/s1/c2/i1")));
|
||||
|
||||
let pulled = pull(&base, "SECRET_TOKEN", &["s1"]).await?;
|
||||
let mut titles: Vec<String> = pulled["sessions"][0]["chats"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|c| c["record"]["title"].as_str().unwrap().to_string())
|
||||
.collect();
|
||||
titles.sort();
|
||||
assert_eq!(titles, ["AFTER", "BEFORE", "FRESH"], "{pulled}");
|
||||
assert_eq!(
|
||||
pulled["sessions"][0]["images"][0]["data_url"],
|
||||
"data:image/png;base64,AAAA"
|
||||
);
|
||||
|
||||
// A head at its cap, pushed in two parts: the second reads back the push token.
|
||||
let mut head = json!({ "id": "s2", "workspace_id": "test-workspace", "createdAt": 2, "chatId": "c", "pad": "" });
|
||||
let pad = 1024 * 1024 - head.to_string().len();
|
||||
head["pad"] = json!("x".repeat(pad));
|
||||
assert_eq!(head.to_string().len(), 1024 * 1024);
|
||||
for part in [
|
||||
json!({ "id": "s2", "whole": true, "push": "p2", "opens": true, "partial": true, "head": head }),
|
||||
json!({ "id": "s2", "whole": true, "push": "p2", "chats": [{ "id": "c", "record": { "id": "c" } }] }),
|
||||
] {
|
||||
let resp = push(
|
||||
&base,
|
||||
"SECRET_TOKEN",
|
||||
json!({ "owner": "test@windmill.dev", "sessions": [part] }),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(resp.status(), 200, "{}", resp.text().await?);
|
||||
let pushed: Value = resp.json().await?;
|
||||
assert_eq!(pushed["results"], json!([{ "id": "s2" }]), "{pushed}");
|
||||
}
|
||||
assert!(is_gcm(&object("sessions/s2/head.json")));
|
||||
let pulled = pull(&base, "SECRET_TOKEN", &["s2"]).await?;
|
||||
assert_eq!(
|
||||
pulled["sessions"][0]["head"]["pad"].as_str().map(str::len),
|
||||
Some(pad)
|
||||
);
|
||||
assert_eq!(pulled["sessions"][0]["chats"][0]["id"], "c", "{pulled}");
|
||||
|
||||
let first = user_root(storage_dir.path(), "test@windmill.dev");
|
||||
copy_dir(
|
||||
&first,
|
||||
&first
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join(calculate_hash("test2@windmill.dev")),
|
||||
)?;
|
||||
assert_eq!(
|
||||
pull(&base, "SECRET_TOKEN_2", &["s1", "s2"]).await?["sessions"],
|
||||
json!([]),
|
||||
"relocated ciphertext must not decrypt for another user"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
//! Every s3_proxy response carries `nosniff` and the sandbox CSP, error responses included,
|
||||
//! since the presigned route serves workspace objects from the app origin without a login.
|
||||
#![cfg(all(feature = "enterprise", feature = "private", feature = "parquet"))]
|
||||
|
||||
use sqlx::{Pool, Postgres};
|
||||
use windmill_test_utils::*;
|
||||
|
||||
#[sqlx::test(fixtures("base"))]
|
||||
async fn test_s3_proxy_sets_content_security_headers(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
initialize_tracing().await;
|
||||
|
||||
let server = ApiServer::start(db.clone()).await?;
|
||||
let port = server.addr.port();
|
||||
|
||||
let resp = reqwest::Client::new()
|
||||
.get(format!(
|
||||
"http://localhost:{port}/api/w/test-workspace/s3_proxy/_default_/x.html?exp=9999999999&sig=00"
|
||||
))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
assert!(resp.status().is_client_error(), "status: {}", resp.status());
|
||||
assert_eq!(resp.headers()["x-content-type-options"], "nosniff");
|
||||
assert_eq!(
|
||||
resp.headers()["content-security-policy"],
|
||||
"sandbox allow-scripts allow-forms allow-popups allow-modals allow-downloads"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -132,6 +132,15 @@ async fn get_instance_ui(
|
||||
))
|
||||
}
|
||||
|
||||
/// Whether email notifications can go out, for the workspace admins who configure email
|
||||
/// handlers and cannot read `smtp_settings` itself (it holds the password). Asks the same
|
||||
/// resolver the senders use, so SMTP set through `SMTP_*` env vars counts too.
|
||||
async fn get_smtp_configured(Extension(db): Extension<DB>, _authed: ApiAuthed) -> JsonResult<bool> {
|
||||
Ok(Json(
|
||||
SMTP_ENABLED && windmill_common::server::is_smtp_configured(&db).await?,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn global_service() -> Router {
|
||||
#[warn(unused_mut)]
|
||||
let r = Router::new()
|
||||
@@ -152,6 +161,7 @@ pub fn global_service() -> Router {
|
||||
)
|
||||
.route("/instance_config/yaml", get(get_instance_config_yaml))
|
||||
.route("/test_smtp", post(test_email))
|
||||
.route("/smtp_configured", get(get_smtp_configured))
|
||||
.route("/test_license_key", post(test_license_key))
|
||||
.route("/send_stats", post(send_stats))
|
||||
.route("/get_stats", get(get_stats))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.831.0
|
||||
version: 1.832.0
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -1971,6 +1971,24 @@ paths:
|
||||
application/json:
|
||||
schema: {}
|
||||
|
||||
/settings/smtp_configured:
|
||||
get:
|
||||
summary: whether instance SMTP is configured
|
||||
description: |
|
||||
Readable by any authenticated user. True when this build can send email
|
||||
and SMTP is set, either in the smtp_settings instance setting or through
|
||||
the SMTP_* environment variables.
|
||||
operationId: getSmtpConfigured
|
||||
tags:
|
||||
- setting
|
||||
responses:
|
||||
"200":
|
||||
description: whether email notifications can be sent
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: boolean
|
||||
|
||||
/settings/test_smtp:
|
||||
post:
|
||||
summary: test smtp
|
||||
|
||||
@@ -24,7 +24,6 @@ use axum::{
|
||||
Extension, Json, Router,
|
||||
};
|
||||
use futures::{StreamExt, TryStreamExt};
|
||||
use magic_crypt::MagicCryptTrait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::value::RawValue;
|
||||
use std::sync::Arc;
|
||||
@@ -33,9 +32,11 @@ use windmill_api_workspaces::ai_session_backups::{
|
||||
fallback_store, generation_prefix, primary_store, storage_id, MAX_OBJECT_BYTES,
|
||||
};
|
||||
use windmill_api_workspaces::workspaces::sessions_retention_days;
|
||||
use windmill_common::db::{process_uptime, PREVIOUS_RELEASE_ROLLOUT_UPTIME};
|
||||
use windmill_common::error::{Error, JsonResult, Result};
|
||||
use windmill_common::utils::calculate_hash;
|
||||
use windmill_common::variables::{crypt_from_key_with_suffix, get_workspace_key, WorkspaceCrypt};
|
||||
use windmill_common::workspace_crypt::{CipherFormat, MAX_BYTES_OVERHEAD};
|
||||
use windmill_object_store::object_store_reexports::{
|
||||
ObjectStore, ObjectStoreError, Path as ObjectPath, PutPayload,
|
||||
};
|
||||
@@ -47,9 +48,9 @@ const PULL_BODY_LIMIT: usize = 64 * 1024;
|
||||
/// A pull answer larger than this hands the remaining ids back as `deferred`.
|
||||
const PULL_RESPONSE_BUDGET: usize = 32 * 1024 * 1024;
|
||||
const MAX_HEAD_BYTES: usize = 1024 * 1024;
|
||||
/// What the cipher adds to a plaintext at most (a block of padding): an object stored at a
|
||||
/// cap is that much larger than the cap when read back.
|
||||
const CIPHER_PADDING: usize = 16;
|
||||
/// What the cipher adds to a plaintext at most: an object stored at a cap is that much larger
|
||||
/// than the cap when read back.
|
||||
const CIPHER_PADDING: usize = MAX_BYTES_OVERHEAD;
|
||||
/// The browser bounds an image to a 1568 px edge and re-encodes past 700 KB; this is
|
||||
/// well above what that produces.
|
||||
const MAX_IMAGE_BYTES: usize = 4 * 1024 * 1024;
|
||||
@@ -120,6 +121,15 @@ pub fn workspaced_service() -> Router {
|
||||
)
|
||||
}
|
||||
|
||||
fn backup_format(current: CipherFormat, uptime: std::time::Duration) -> CipherFormat {
|
||||
// A previous-release server reads only CBC here: GCM waits for it to be rolled out.
|
||||
if current == CipherFormat::V2 && uptime >= PREVIOUS_RELEASE_ROLLOUT_UPTIME {
|
||||
CipherFormat::V2
|
||||
} else {
|
||||
CipherFormat::LegacyCbc
|
||||
}
|
||||
}
|
||||
|
||||
/// What reading an object yields. `Gone`: not there (deleted since the listing, or never
|
||||
/// pushed). `Grown`: larger than expected, so replaced since the listing (or planted), and
|
||||
/// left unread; a pull answers with a page ending before it rather than without it.
|
||||
@@ -214,7 +224,8 @@ impl Backend {
|
||||
}
|
||||
|
||||
fn seal(&self, plaintext: &[u8]) -> Vec<u8> {
|
||||
self.mc.encrypt_bytes_to_bytes(plaintext)
|
||||
let format = backup_format(CipherFormat::current(), process_uptime());
|
||||
self.mc.encrypt_bytes_in(plaintext, format)
|
||||
}
|
||||
|
||||
/// Bytes written.
|
||||
@@ -255,7 +266,7 @@ impl Backend {
|
||||
// apart beyond the cipher's padding check, which a wrong key passes now and then.
|
||||
match self
|
||||
.mc
|
||||
.decrypt_bytes_to_bytes(&bytes)
|
||||
.decrypt_bytes(&bytes)
|
||||
.ok()
|
||||
.and_then(|plaintext| String::from_utf8(plaintext).ok())
|
||||
{
|
||||
@@ -1632,3 +1643,32 @@ async fn push(
|
||||
results,
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn backups_are_written_in_gcm_only_once_the_previous_release_is_rolled_out() {
|
||||
let just_short = PREVIOUS_RELEASE_ROLLOUT_UPTIME - std::time::Duration::from_secs(1);
|
||||
for (current, uptime, expected) in [
|
||||
(
|
||||
CipherFormat::V2,
|
||||
PREVIOUS_RELEASE_ROLLOUT_UPTIME,
|
||||
CipherFormat::V2,
|
||||
),
|
||||
(CipherFormat::V2, just_short, CipherFormat::LegacyCbc),
|
||||
(
|
||||
CipherFormat::LegacyCbc,
|
||||
PREVIOUS_RELEASE_ROLLOUT_UPTIME * 4,
|
||||
CipherFormat::LegacyCbc,
|
||||
),
|
||||
] {
|
||||
assert_eq!(
|
||||
backup_format(current, uptime),
|
||||
expected,
|
||||
"{current:?} {uptime:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +113,12 @@ lazy_static::lazy_static! {
|
||||
(20260909163047, include_str!(
|
||||
"../../migrations/20260909163047_workspace_delete_cascade_indexes.up.sql"
|
||||
).replace("CREATE INDEX", "CREATE INDEX CONCURRENTLY").replace("DROP INDEX", "DROP INDEX CONCURRENTLY")),
|
||||
// The widening fails every query the previous release's still-running
|
||||
// servers prepared on the column; `variables::widen_variable_value` does it
|
||||
// once they are gone.
|
||||
(20261012213357, include_str!(
|
||||
"../../migrations/20261012213357_widen_variable_value.up.sql"
|
||||
).replace("ALTER TABLE variable ALTER COLUMN value TYPE VARCHAR(15040);", "")),
|
||||
].into_iter().collect();
|
||||
}
|
||||
|
||||
|
||||
@@ -290,3 +290,62 @@ impl<'b> DbExecutor<'b> for &'b mut PgConnection {
|
||||
&mut **self
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref PROCESS_START: std::time::Instant = std::time::Instant::now();
|
||||
}
|
||||
|
||||
/// Milliseconds after [`PROCESS_START`] before which every pooled connection is retired;
|
||||
/// 0 retires none.
|
||||
static RETIRE_CONNECTIONS_OPENED_BEFORE_MS: std::sync::atomic::AtomicU64 =
|
||||
std::sync::atomic::AtomicU64::new(0);
|
||||
|
||||
/// Postgres refuses a statement prepared before its result columns changed type (even a
|
||||
/// varchar length) with this error, on every execution, for as long as the connection lives.
|
||||
/// The routine is matched as well as the message, which `lc_messages` may translate.
|
||||
pub fn is_stale_cached_plan(e: &sqlx::Error) -> bool {
|
||||
let sqlx::Error::Database(d) = e else {
|
||||
return false;
|
||||
};
|
||||
d.code().as_deref() == Some("0A000")
|
||||
&& (d
|
||||
.try_downcast_ref::<sqlx::postgres::PgDatabaseError>()
|
||||
.is_some_and(|pg| pg.routine() == Some("RevalidateCachedQuery"))
|
||||
|| d.message()
|
||||
.contains("cached plan must not change result type"))
|
||||
}
|
||||
|
||||
/// Retire every pooled connection opened up to now, so a schema change that poisoned their
|
||||
/// prepared statements costs one failed query instead of failing until they expire.
|
||||
pub fn retire_connections_opened_before_now() {
|
||||
let now = PROCESS_START.elapsed().as_millis() as u64 + 1;
|
||||
RETIRE_CONNECTIONS_OPENED_BEFORE_MS.fetch_max(now, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// For the pool's `before_acquire`: whether a connection of this age must be closed.
|
||||
pub fn connection_is_retired(age: std::time::Duration) -> bool {
|
||||
let cutoff = RETIRE_CONNECTIONS_OPENED_BEFORE_MS.load(std::sync::atomic::Ordering::Relaxed);
|
||||
cutoff != 0 && (PROCESS_START.elapsed().saturating_sub(age).as_millis() as u64) < cutoff
|
||||
}
|
||||
|
||||
/// Servers report no version, so the worker version gate alone cannot see a server of the
|
||||
/// previous release; its rollout is assumed done once this server has been up this long.
|
||||
pub const PREVIOUS_RELEASE_ROLLOUT_UPTIME: std::time::Duration =
|
||||
std::time::Duration::from_secs(30 * 60);
|
||||
|
||||
static PROCESS_UPTIME_OFFSET_SECS: std::sync::atomic::AtomicU64 =
|
||||
std::sync::atomic::AtomicU64::new(0);
|
||||
|
||||
/// Time since this process first used its database pool.
|
||||
pub fn process_uptime() -> std::time::Duration {
|
||||
PROCESS_START.elapsed()
|
||||
+ std::time::Duration::from_secs(
|
||||
PROCESS_UPTIME_OFFSET_SECS.load(std::sync::atomic::Ordering::Relaxed),
|
||||
)
|
||||
}
|
||||
|
||||
/// For tests of what waits on [`process_uptime`]: adds `offset` to it.
|
||||
#[doc(hidden)]
|
||||
pub fn set_process_uptime_offset(offset: std::time::Duration) {
|
||||
PROCESS_UPTIME_OFFSET_SECS.store(offset.as_secs(), std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
@@ -153,6 +153,10 @@ impl From<anyhow::Error> for Error {
|
||||
impl From<sqlx::Error> for Error {
|
||||
#[track_caller]
|
||||
fn from(e: sqlx::Error) -> Self {
|
||||
if crate::db::is_stale_cached_plan(&e) {
|
||||
tracing::warn!("a schema change invalidated cached statements, reopening the database connections: {e}");
|
||||
crate::db::retire_connections_opened_before_now();
|
||||
}
|
||||
Self::SqlErr { error: e, location: prettify_location(std::panic::Location::caller()) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ pub const MIN_VERSION_SUPPORTS_JOB_TOKEN_SCOPES: VC = vc(1, 822, 0, "Restricted
|
||||
// stored secret variables. Reading it ships one release earlier (1.831), so a rollback to
|
||||
// that release still reads every secret; below it, secrets written after the switch stay
|
||||
// unreadable until rolled forward. Only worker versions are seen here, not servers.
|
||||
// Also gates widening `variable.value`, which needs every process to retire the connections
|
||||
// the widening poisons (`db::retire_connections_opened_before_now`): that also ships in 1.832.
|
||||
pub const MIN_VERSION_SUPPORTS_AEAD_SECRETS: VC = vc(1, 832, 0, "AES-GCM secret encryption");
|
||||
pub const MIN_VERSION_SUPPORTS_NODE_DEBOUNCING: VC = vc(1, 658, 0, "Flow node debouncing");
|
||||
pub const MIN_VERSION_SUPPORTS_TOKEN_HASH: VC = vc(1, 659, 0, "Token hash storage");
|
||||
|
||||
@@ -31,9 +31,24 @@ pub async fn load_smtp_config(db: &DB) -> error::Result<Option<Smtp>> {
|
||||
Ok(parse_smtp_config(value))
|
||||
}
|
||||
|
||||
/// Whether SMTP is set, without the "not configured" warning: any signed-in user can ask, and
|
||||
/// "no" is an ordinary answer there rather than something to log.
|
||||
pub async fn is_smtp_configured(db: &DB) -> error::Result<bool> {
|
||||
let value = crate::global_settings::load_value_from_global_settings(db, SMTP_SETTING).await?;
|
||||
Ok(resolve_smtp_config(value).is_some())
|
||||
}
|
||||
|
||||
/// The half of [`load_smtp_config`] after the read, so a batched settings pass can parse a
|
||||
/// value it already fetched.
|
||||
pub fn parse_smtp_config(value: Option<serde_json::Value>) -> Option<Smtp> {
|
||||
let smtp = resolve_smtp_config(value);
|
||||
if smtp.is_none() {
|
||||
tracing::warn!("SMTP not configured");
|
||||
}
|
||||
smtp
|
||||
}
|
||||
|
||||
fn resolve_smtp_config(value: Option<serde_json::Value>) -> Option<Smtp> {
|
||||
let config: SmtpConfigOpt = value
|
||||
.map(|x| serde_json::from_value(x).ok())
|
||||
.flatten()
|
||||
@@ -57,7 +72,7 @@ pub fn parse_smtp_config(value: Option<serde_json::Value>) -> Option<Smtp> {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let smtp = config_smtp.or(
|
||||
config_smtp.or(
|
||||
if let (Some(host), username, password) = (
|
||||
std::env::var("SMTP_HOST").ok(),
|
||||
std::env::var("SMTP_USERNAME").ok(),
|
||||
@@ -86,12 +101,7 @@ pub fn parse_smtp_config(value: Option<serde_json::Value>) -> Option<Smtp> {
|
||||
} else {
|
||||
None
|
||||
},
|
||||
);
|
||||
if smtp.is_none() {
|
||||
tracing::warn!("SMTP not configured");
|
||||
}
|
||||
|
||||
smtp
|
||||
)
|
||||
}
|
||||
|
||||
impl Default for SmtpConfigOpt {
|
||||
|
||||
@@ -400,7 +400,61 @@ pub fn legacy_secret_upgrade_pending() -> bool {
|
||||
&& !LEGACY_SECRET_UPGRADE_DONE.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Re-encrypt the secret variables still in the legacy format in v2, workspace by workspace.
|
||||
/// AES-256-GCM ciphertext is 32 bytes (magic, nonce, tag) longer than legacy CBC before base64,
|
||||
/// so the longest CBC value that fits 15000 chars is exactly 15040 chars once re-encrypted.
|
||||
const VARIABLE_VALUE_WIDTH: i32 = 15040;
|
||||
static VARIABLE_VALUE_WIDENED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Whether this process has seen `variable.value` wide enough for every v2 value. Only servers
|
||||
/// look, so it stays false in a worker.
|
||||
pub fn variable_value_widened() -> bool {
|
||||
VARIABLE_VALUE_WIDENED.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Whether `variable.value` may still have to be widened to fit v2 values.
|
||||
pub fn variable_value_widen_pending() -> bool {
|
||||
CipherFormat::current() == CipherFormat::V2 && !VARIABLE_VALUE_WIDENED.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Widen `variable.value` to [`VARIABLE_VALUE_WIDTH`], or `false` when it is too early to.
|
||||
/// Not a migration: changing a column's type, even its length, fails every statement a live
|
||||
/// process prepared on it until its connection closes. It waits for v2 writes, gated on a
|
||||
/// worker version that retires such connections on the first failure, so that no process too
|
||||
/// old to recover is still running.
|
||||
async fn widen_variable_value(db: &DB) -> error::Result<bool> {
|
||||
if VARIABLE_VALUE_WIDENED.load(Ordering::Relaxed) {
|
||||
return Ok(true);
|
||||
}
|
||||
// varchar's typmod is its length + 4, -1 when unbounded.
|
||||
let typmod: i32 = sqlx::query_scalar(
|
||||
"SELECT atttypmod FROM pg_attribute WHERE attrelid = 'variable'::regclass AND attname = 'value'",
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await?;
|
||||
if typmod >= 0 && typmod - 4 < VARIABLE_VALUE_WIDTH {
|
||||
if crate::db::process_uptime() < crate::db::PREVIOUS_RELEASE_ROLLOUT_UPTIME {
|
||||
return Ok(false);
|
||||
}
|
||||
tracing::info!("widening variable.value to VARCHAR({VARIABLE_VALUE_WIDTH})");
|
||||
let mut tx = db.begin().await?;
|
||||
// Queued behind a long transaction, the ALTER would block every variable read meanwhile.
|
||||
sqlx::query("SET LOCAL lock_timeout = '3s'")
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
sqlx::query(&format!(
|
||||
"ALTER TABLE variable ALTER COLUMN value TYPE VARCHAR({VARIABLE_VALUE_WIDTH})"
|
||||
))
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
crate::db::retire_connections_opened_before_now();
|
||||
}
|
||||
VARIABLE_VALUE_WIDENED.store(true, Ordering::Relaxed);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Widen `variable.value` for v2 values, then re-encrypt the secret variables still in the
|
||||
/// legacy format in v2, workspace by workspace.
|
||||
/// Idempotent, and emits no git-sync event: the plaintexts do not change.
|
||||
pub async fn upgrade_legacy_secret_variables(db: &DB) {
|
||||
struct Running;
|
||||
@@ -409,14 +463,26 @@ pub async fn upgrade_legacy_secret_variables(db: &DB) {
|
||||
LEGACY_SECRET_UPGRADE_RUNNING.store(false, Ordering::Release);
|
||||
}
|
||||
}
|
||||
if !legacy_secret_upgrade_pending()
|
||||
if !(variable_value_widen_pending() || legacy_secret_upgrade_pending())
|
||||
|| LEGACY_SECRET_UPGRADE_RUNNING.swap(true, Ordering::AcqRel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Cleared on drop, so a pass that panics does not stop every later one in this process.
|
||||
let _running = Running;
|
||||
upgrade_legacy_secret_variables_pass(db).await;
|
||||
match widen_variable_value(db).await {
|
||||
Ok(true) => {}
|
||||
Ok(false) => return,
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"secret encryption upgrade: widening variable.value failed, retrying later: {e:#}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if legacy_secret_upgrade_pending() {
|
||||
upgrade_legacy_secret_variables_pass(db).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn upgrade_legacy_secret_variables_pass(db: &DB) {
|
||||
|
||||
@@ -30,6 +30,10 @@ pub const V2_MAGIC: [u8; 4] = *b"wmA2";
|
||||
const NONCE_LEN: usize = 12;
|
||||
const TAG_LEN: usize = 16;
|
||||
const BLOCK: usize = 16;
|
||||
const NARROW_VARIABLE_VALUE_WIDTH: usize = 15000;
|
||||
|
||||
/// The most `encrypt_bytes_in` adds to a plaintext, in either format.
|
||||
pub const MAX_BYTES_OVERHEAD: usize = V2_MAGIC.len() + NONCE_LEN + BLOCK + TAG_LEN;
|
||||
|
||||
const AEAD_KEY_INFO: &[u8] = b"windmill workspace secret aes-256-gcm v1";
|
||||
|
||||
@@ -77,8 +81,8 @@ pub struct WorkspaceCrypt {
|
||||
aead: Aes256Gcm,
|
||||
}
|
||||
|
||||
/// Code that drives `magic_crypt` directly on this cipher (app link secrets, AI session
|
||||
/// objects) keeps the legacy format it always had.
|
||||
/// Code that drives `magic_crypt` directly on this cipher (app link secrets) keeps the legacy
|
||||
/// format it always had.
|
||||
impl std::ops::Deref for WorkspaceCrypt {
|
||||
type Target = MagicCrypt256;
|
||||
fn deref(&self) -> &MagicCrypt256 {
|
||||
@@ -102,7 +106,23 @@ impl WorkspaceCrypt {
|
||||
}
|
||||
|
||||
pub fn encrypt_str(&self, plaintext: &str) -> String {
|
||||
self.encrypt_str_in(plaintext, CipherFormat::current())
|
||||
self.encrypt_str_fitting(
|
||||
plaintext,
|
||||
CipherFormat::current(),
|
||||
crate::variables::variable_value_widened(),
|
||||
)
|
||||
}
|
||||
|
||||
/// A v2 value is up to 44 chars longer than CBC: until `variable.value` is known widened
|
||||
/// for it, one past the narrow width is written in CBC, which fits.
|
||||
fn encrypt_str_fitting(&self, plaintext: &str, format: CipherFormat, widened: bool) -> String {
|
||||
if format == CipherFormat::V2 {
|
||||
let v2 = self.encrypt_str_in(plaintext, CipherFormat::V2);
|
||||
if widened || v2.len() <= NARROW_VARIABLE_VALUE_WIDTH {
|
||||
return v2;
|
||||
}
|
||||
}
|
||||
self.encrypt_str_in(plaintext, CipherFormat::LegacyCbc)
|
||||
}
|
||||
|
||||
pub fn encrypt_str_in(&self, plaintext: &str, format: CipherFormat) -> String {
|
||||
@@ -136,7 +156,14 @@ impl WorkspaceCrypt {
|
||||
(self.decrypt_str(&upgraded).ok()? == plaintext).then_some(upgraded)
|
||||
}
|
||||
|
||||
fn decrypt_bytes(&self, bytes: &[u8]) -> Result<Vec<u8>, DecryptFailure> {
|
||||
pub fn encrypt_bytes_in(&self, plaintext: &[u8], format: CipherFormat) -> Vec<u8> {
|
||||
match format {
|
||||
CipherFormat::V2 => self.seal(plaintext),
|
||||
CipherFormat::LegacyCbc => self.legacy.encrypt_bytes_to_bytes(plaintext),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decrypt_bytes(&self, bytes: &[u8]) -> Result<Vec<u8>, DecryptFailure> {
|
||||
match self.open(bytes) {
|
||||
Some(plaintext) => Ok(plaintext),
|
||||
// Also for a value with the magic: a CBC ciphertext starts with it one time in 2^32.
|
||||
@@ -242,6 +269,32 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bytes_round_trip_within_the_overhead_in_both_formats() {
|
||||
let wc = WorkspaceCrypt::new(KEY);
|
||||
for len in 0..64 {
|
||||
let plain = vec![b'x'; len];
|
||||
for format in [CipherFormat::V2, CipherFormat::LegacyCbc] {
|
||||
let sealed = wc.encrypt_bytes_in(&plain, format);
|
||||
assert_eq!(wc.open(&sealed).is_some(), format == CipherFormat::V2);
|
||||
assert!(sealed.len() <= len + MAX_BYTES_OVERHEAD, "len {len}");
|
||||
assert_eq!(wc.decrypt_bytes(&sealed).unwrap(), plain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v2_too_long_for_a_narrow_column_is_written_in_cbc() {
|
||||
let wc = WorkspaceCrypt::new(KEY);
|
||||
// The longest plaintext whose CBC value fits 15000 chars.
|
||||
let long = "x".repeat(11231);
|
||||
let narrow = wc.encrypt_str_fitting(&long, CipherFormat::V2, false);
|
||||
assert!(!wc.is_v2(&narrow) && narrow.len() <= NARROW_VARIABLE_VALUE_WIDTH);
|
||||
assert_eq!(wc.decrypt_str(&narrow).unwrap(), long);
|
||||
assert!(wc.is_v2(&wc.encrypt_str_fitting(&long, CipherFormat::V2, true)));
|
||||
assert!(wc.is_v2(&wc.encrypt_str_fitting("short", CipherFormat::V2, false)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v2_passes_the_block_multiple_ciphertext_shape_check() {
|
||||
let wc = WorkspaceCrypt::new(KEY);
|
||||
|
||||
+1
-1
@@ -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.831.0";
|
||||
export const VERSION = "v1.832.0";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@windmill-labs/chat-sdk",
|
||||
"version": "1.831.0",
|
||||
"version": "1.832.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@windmill-labs/chat-sdk",
|
||||
"version": "1.831.0",
|
||||
"version": "1.832.0",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@ai-sdk/react": "^4.0.102",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@windmill-labs/chat-sdk",
|
||||
"description": "Build chat interfaces on Windmill flows deployed in chat mode, from any frontend or raw app",
|
||||
"version": "1.831.0",
|
||||
"version": "1.832.0",
|
||||
"author": "Ruben Fiszel",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://github.com/windmill-labs/windmill/tree/main/chat-sdk#readme",
|
||||
|
||||
@@ -10,4 +10,4 @@ export const WM_FORK_PREFIX = "wm-fork";
|
||||
// (e.g. utils.ts) can read it without importing main.ts and creating a circular
|
||||
// dependency (main → workspace → utils → main) that triggers a TDZ.
|
||||
// Re-exported from main.ts for backwards compatibility.
|
||||
export const VERSION = "1.831.0";
|
||||
export const VERSION = "1.832.0";
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@windmill-labs/components",
|
||||
"version": "1.831.0",
|
||||
"version": "1.832.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@windmill-labs/components",
|
||||
"version": "1.831.0",
|
||||
"version": "1.832.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@windmill-labs/components",
|
||||
"version": "1.831.0",
|
||||
"version": "1.832.0",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"dev:ui-builder": "mv static/ui_builder static/ui_builder.dev-disabled 2>/dev/null || true ; trap 'mv static/ui_builder.dev-disabled static/ui_builder 2>/dev/null || true' EXIT ; vite dev",
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<script lang="ts" module>
|
||||
import type { ErrorHandler } from '$lib/gen/types.gen'
|
||||
|
||||
export const errorHandlerArgs = [
|
||||
'path',
|
||||
'workspace_id',
|
||||
@@ -19,17 +17,6 @@
|
||||
]
|
||||
|
||||
export const slackErrorHandlerHubPathEnding = '/workspace-or-schedule-error-handler-slack'
|
||||
|
||||
/** The stored `<script|flow>/<path>` form of a handler. `kind` is the Custom tab's Script/Flow
|
||||
* choice and survives switching to a built-in tab, whose handlers are all scripts: stored as
|
||||
* `flow/...` they never run. */
|
||||
export function handlerFullPath(
|
||||
selected: ErrorHandler,
|
||||
kind: 'flow' | 'script',
|
||||
path: string
|
||||
): string {
|
||||
return `${selected === 'custom' ? kind : 'script'}/${path}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -67,34 +54,22 @@
|
||||
RotateCw,
|
||||
XCircle
|
||||
} from 'lucide-svelte'
|
||||
import { hubPaths } from '$lib/hub'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import { untrack } from 'svelte'
|
||||
import { syncSlackBotArg } from './handlerSlackArg'
|
||||
import SmtpConfigurationStatus from './common/smtp/SmtpConfigurationStatus.svelte'
|
||||
import { SettingService } from '$lib/gen'
|
||||
import { isSmtpSettingsValid } from './instanceSettings/SmtpSettings.svelte'
|
||||
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
|
||||
import { builtinHubPath, type HandlerDraft, type HandlerMode } from './handlerDraft.svelte'
|
||||
|
||||
const operatingWorkspace = useOperatingWorkspace()
|
||||
|
||||
const slackRecoveryHandler = hubPaths.slackRecoveryHandler
|
||||
const slackHandlerScriptPath = hubPaths.slackErrorHandler
|
||||
const slackSuccessHandler = hubPaths.slackSuccessHandler
|
||||
const teamsRecoveryHandler = hubPaths.teamsRecoveryHandler
|
||||
const teamsHandlerScriptPath = hubPaths.teamsErrorHandler
|
||||
const teamsSuccessHandler = hubPaths.teamsSuccessHandler
|
||||
|
||||
interface Props {
|
||||
errorOrRecovery: 'error' | 'recovery' | 'success'
|
||||
errorOrRecovery: HandlerMode
|
||||
isEditable: boolean
|
||||
toggleText?: string
|
||||
showScriptHelpText?: boolean
|
||||
handlerSelected: ErrorHandler
|
||||
handlerPath: string | undefined
|
||||
handlerExtraArgs: Record<string, any>
|
||||
/** Owned by the caller, which restores it on Discard; every tab edits its own slot. */
|
||||
draft: HandlerDraft
|
||||
customScriptTemplate: string
|
||||
customHandlerKind?: 'flow' | 'script'
|
||||
customTabTooltip?: import('svelte').Snippet
|
||||
noMargin?: boolean
|
||||
/** Workspace for handler lookup / settings / test jobs. Defaults to the operating
|
||||
@@ -110,11 +85,8 @@
|
||||
isEditable,
|
||||
toggleText = 'Enable',
|
||||
showScriptHelpText = false,
|
||||
handlerSelected = $bindable('custom'),
|
||||
handlerPath = $bindable(),
|
||||
handlerExtraArgs = $bindable(),
|
||||
draft = $bindable(),
|
||||
customScriptTemplate,
|
||||
customHandlerKind = $bindable('script'),
|
||||
customTabTooltip,
|
||||
noMargin = false,
|
||||
workspace = undefined,
|
||||
@@ -142,8 +114,6 @@
|
||||
|
||||
let connectionTestJob: { uuid: string; is_success: boolean; in_progress: boolean } | undefined =
|
||||
$state()
|
||||
const EMAIL_RECIPIENTS_KEY = 'email_recipients'
|
||||
const CHANNEL_KEY = 'channel'
|
||||
|
||||
async function loadSlackResources() {
|
||||
const settings = await WorkspaceService.getPublicSettings({ workspace: effectiveWorkspace! })
|
||||
@@ -171,11 +141,7 @@
|
||||
|
||||
async function loadSmtpConfiguration() {
|
||||
try {
|
||||
const smtpSettings = (await SettingService.getGlobal({ key: 'smtp_settings' })) as Record<
|
||||
string,
|
||||
any
|
||||
> | null
|
||||
hasSmtpConfig = smtpSettings ? isSmtpSettingsValid(smtpSettings) : false
|
||||
hasSmtpConfig = await SettingService.getSmtpConfigured()
|
||||
} catch (error) {
|
||||
hasSmtpConfig = false
|
||||
}
|
||||
@@ -190,7 +156,7 @@
|
||||
let submitted_job = await testJobFunction({
|
||||
workspace: effectiveWorkspace!,
|
||||
requestBody: {
|
||||
hub_script_path: handlerPath,
|
||||
hub_script_path: draft[platform].path,
|
||||
channel: channel,
|
||||
test_msg: `This is a notification to test the connection between ${platform} and Windmill workspace '${effectiveWorkspace!}'`
|
||||
}
|
||||
@@ -236,7 +202,11 @@
|
||||
await sendMessage(channel, 'teams')
|
||||
}
|
||||
|
||||
async function loadHandlerScriptArgs(p: string, defaultArgs: string[] = []) {
|
||||
async function loadHandlerScriptArgs(
|
||||
p: string,
|
||||
kind: 'script' | 'flow',
|
||||
defaultArgs: string[] = []
|
||||
) {
|
||||
try {
|
||||
let schema: Schema | undefined = emptySchema()
|
||||
if (p.startsWith('hub/')) {
|
||||
@@ -251,7 +221,7 @@
|
||||
}
|
||||
} else {
|
||||
let scriptOrFlow: Script | Flow =
|
||||
customHandlerKind === 'script'
|
||||
kind === 'script'
|
||||
? await ScriptService.getScriptByPath({ workspace: effectiveWorkspace!, path: p })
|
||||
: await FlowService.getFlowByPath({ workspace: effectiveWorkspace!, path: p })
|
||||
schema = scriptOrFlow.schema as Schema
|
||||
@@ -269,47 +239,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
function isSlackHandler(scriptPath: string | undefined) {
|
||||
if (!scriptPath) {
|
||||
return false
|
||||
}
|
||||
if (errorOrRecovery == 'error') {
|
||||
return scriptPath.startsWith('hub/') && scriptPath.endsWith(slackErrorHandlerHubPathEnding)
|
||||
} else if (errorOrRecovery == 'recovery') {
|
||||
return (
|
||||
scriptPath.startsWith('hub/') && scriptPath.endsWith('/schedule-recovery-handler-slack')
|
||||
)
|
||||
} else {
|
||||
return scriptPath.startsWith('hub/') && scriptPath.endsWith('/schedule-success-handler-slack')
|
||||
}
|
||||
}
|
||||
|
||||
function isTeamsHandler(scriptPath: string | undefined) {
|
||||
if (!scriptPath) {
|
||||
return false
|
||||
}
|
||||
if (errorOrRecovery == 'error') {
|
||||
return (
|
||||
scriptPath.startsWith('hub/') &&
|
||||
scriptPath.endsWith('/workspace-or-schedule-error-handler-teams')
|
||||
)
|
||||
} else if (errorOrRecovery == 'recovery') {
|
||||
return (
|
||||
scriptPath.startsWith('hub/') && scriptPath.endsWith('/schedule-recovery-handler-teams')
|
||||
)
|
||||
} else {
|
||||
return scriptPath.startsWith('hub/') && scriptPath.endsWith('/schedule-success-handler-teams')
|
||||
}
|
||||
}
|
||||
|
||||
function isEmailHandler(scriptPath: string | undefined) {
|
||||
if (!scriptPath) {
|
||||
return false
|
||||
}
|
||||
|
||||
return scriptPath.startsWith('hub/') && scriptPath.endsWith('/workspace-or-error-handler-email')
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (effectiveWorkspace) {
|
||||
loadSlackResources()
|
||||
@@ -318,82 +247,25 @@
|
||||
loadSmtpConfiguration()
|
||||
})
|
||||
|
||||
let builtinArgs: Record<string, any> | undefined
|
||||
$effect(() => {
|
||||
const isSlackBuiltin = handlerSelected === 'slack' && isSlackHandler(handlerPath)
|
||||
const args = handlerExtraArgs
|
||||
// A replaced args object (Discard, reload) holds the stored config, not what the built-in set.
|
||||
untrack(() => syncSlackBotArg(args, isSlackBuiltin, args === builtinArgs))
|
||||
builtinArgs = isSlackBuiltin ? args : undefined
|
||||
})
|
||||
|
||||
let lastHandlerSelected: ErrorHandler | undefined = $state(undefined)
|
||||
let handlerCache = $state({
|
||||
slack: undefined as string | undefined,
|
||||
teams: undefined as string | undefined,
|
||||
email: undefined as string[] | undefined
|
||||
})
|
||||
let handlerPathCache: Partial<Record<ErrorHandler, string | undefined>> = $state({})
|
||||
$effect(() => {
|
||||
if (lastHandlerSelected !== handlerSelected && lastHandlerSelected !== undefined) {
|
||||
if (lastHandlerSelected != 'custom') {
|
||||
const key = lastHandlerSelected === 'email' ? EMAIL_RECIPIENTS_KEY : CHANNEL_KEY
|
||||
handlerCache[lastHandlerSelected] = handlerExtraArgs[key]
|
||||
}
|
||||
handlerPathCache[lastHandlerSelected] = handlerPath
|
||||
|
||||
if (handlerSelected === 'custom') {
|
||||
delete handlerExtraArgs[CHANNEL_KEY]
|
||||
delete handlerExtraArgs[EMAIL_RECIPIENTS_KEY]
|
||||
handlerPath = handlerPathCache['custom']
|
||||
} else if (handlerSelected === 'email') {
|
||||
handlerExtraArgs[EMAIL_RECIPIENTS_KEY] = handlerCache[handlerSelected] ?? []
|
||||
delete handlerExtraArgs[CHANNEL_KEY]
|
||||
} else {
|
||||
handlerExtraArgs[CHANNEL_KEY] = handlerCache[handlerSelected] ?? ''
|
||||
delete handlerExtraArgs[EMAIL_RECIPIENTS_KEY]
|
||||
handlerPath = handlerPathCache[handlerSelected]
|
||||
}
|
||||
}
|
||||
|
||||
lastHandlerSelected = handlerSelected
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
handlerPath &&
|
||||
!isSlackHandler(handlerPath) &&
|
||||
!isTeamsHandler(handlerPath) &&
|
||||
!isEmailHandler(handlerPath) &&
|
||||
loadHandlerScriptArgs(handlerPath, errorHandlerArgs).then(
|
||||
const { path, kind } = draft.custom
|
||||
path &&
|
||||
loadHandlerScriptArgs(path, kind, errorHandlerArgs).then(
|
||||
(schema) => (customHandlerSchema = schema)
|
||||
)
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
handlerPath &&
|
||||
isSlackHandler(handlerPath) &&
|
||||
loadHandlerScriptArgs(handlerPath, [...errorHandlerArgs, 'slack']).then(
|
||||
const path = draft.slack.path
|
||||
path &&
|
||||
loadHandlerScriptArgs(path, 'script', [...errorHandlerArgs, 'slack']).then(
|
||||
(schema) => (slackHandlerSchema = schema)
|
||||
)
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (handlerSelected === 'email') {
|
||||
handlerPath = hubPaths.emailErrorHandler
|
||||
}
|
||||
})
|
||||
|
||||
// The instance channels are reached by having no workspace handler at all, so the tab
|
||||
// owns an empty path rather than a handler script.
|
||||
$effect(() => {
|
||||
if (handlerSelected === 'instance_alerts') {
|
||||
handlerPath = undefined
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class={classNames('space-y-2', noMargin ? '' : 'mt-2')}>
|
||||
<ToggleButtonGroup bind:selected={handlerSelected} disabled={!isEditable}>
|
||||
<ToggleButtonGroup bind:selected={draft.selected} disabled={!isEditable}>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton label="Slack" value="slack" {item} disabled={!isEditable} />
|
||||
<ToggleButton label="Teams" value="teams" {item} disabled={!isEditable} />
|
||||
@@ -418,21 +290,21 @@
|
||||
</ToggleButtonGroup>
|
||||
|
||||
<div class="flex flex-col gap-6 p-4 rounded-md shadow-sm bg-surface-tertiary">
|
||||
{#if handlerSelected === 'custom'}
|
||||
{#if draft.selected === 'custom'}
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex flex-row">
|
||||
<ScriptPicker
|
||||
disabled={!isEditable || !$enterpriseLicense}
|
||||
kinds={['script', 'failure']}
|
||||
allowFlow={true}
|
||||
bind:scriptPath={handlerPath}
|
||||
bind:itemKind={customHandlerKind}
|
||||
bind:scriptPath={draft.custom.path}
|
||||
bind:itemKind={draft.custom.kind}
|
||||
allowRefresh={isEditable}
|
||||
workspace={effectiveWorkspace}
|
||||
clearable
|
||||
/>
|
||||
|
||||
{#if !handlerPath}
|
||||
{#if !draft.custom.path}
|
||||
<Button
|
||||
btnClasses="ml-4 whitespace-nowrap"
|
||||
variant="default"
|
||||
@@ -456,7 +328,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if handlerPath}
|
||||
{#if draft.custom.path}
|
||||
<div>
|
||||
<p class="font-semibold text-xs mb-1">Extra arguments</p>
|
||||
{#await import('$lib/components/SchemaForm.svelte')}
|
||||
@@ -465,7 +337,7 @@
|
||||
<Module.default
|
||||
disabled={!isEditable}
|
||||
schema={customHandlerSchema}
|
||||
bind:args={handlerExtraArgs}
|
||||
bind:args={draft.custom.args}
|
||||
shouldHideNoInputs
|
||||
className="text-xs"
|
||||
/>
|
||||
@@ -475,7 +347,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{:else if handlerSelected === 'slack'}
|
||||
{:else if draft.selected === 'slack'}
|
||||
<!-- Slack Connection Status -->
|
||||
<SlackConnectionStatus
|
||||
isConnected={workspaceConnectedToSlack}
|
||||
@@ -487,23 +359,15 @@
|
||||
{#if workspaceConnectedToSlack}
|
||||
<Toggle
|
||||
disabled={!$enterpriseLicense || !isEditable}
|
||||
checked={isSlackHandler(handlerPath)}
|
||||
checked={!!draft.slack.path}
|
||||
options={{ right: toggleText }}
|
||||
on:change={async (e) => {
|
||||
if (e.detail && errorOrRecovery === 'error') {
|
||||
handlerPath = slackHandlerScriptPath
|
||||
} else if (e.detail && errorOrRecovery === 'recovery') {
|
||||
handlerPath = slackRecoveryHandler
|
||||
} else if (e.detail && errorOrRecovery === 'success') {
|
||||
handlerPath = slackSuccessHandler
|
||||
} else {
|
||||
handlerPath = undefined
|
||||
}
|
||||
on:change={(e) => {
|
||||
draft.slack.path = e.detail ? builtinHubPath(errorOrRecovery, 'slack') : undefined
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if workspaceConnectedToSlack && isSlackHandler(handlerPath)}
|
||||
{#if workspaceConnectedToSlack && draft.slack.path}
|
||||
<div class="flex flex-col gap-2">
|
||||
{#await import('$lib/components/SchemaForm.svelte')}
|
||||
<Loader2 class="animate-spin" />
|
||||
@@ -515,18 +379,18 @@
|
||||
schemaFieldTooltip={{
|
||||
channel: 'Slack channel name without the "#" - example: "windmill-alerts"'
|
||||
}}
|
||||
bind:args={handlerExtraArgs}
|
||||
bind:args={draft.slack.args}
|
||||
shouldHideNoInputs
|
||||
className="text-xs"
|
||||
/>
|
||||
{/await}
|
||||
|
||||
{#if $enterpriseLicense && isSlackHandler(handlerPath)}
|
||||
{#if $enterpriseLicense}
|
||||
<Button
|
||||
disabled={emptyString(handlerExtraArgs['channel'])}
|
||||
disabled={emptyString(draft.slack.args['channel'])}
|
||||
wrapperClasses="w-fit"
|
||||
variant="default"
|
||||
on:click={() => sendSlackMessage(handlerExtraArgs['channel'])}
|
||||
on:click={() => sendSlackMessage(draft.slack.args['channel'])}
|
||||
unifiedSize="md">Send test message</Button
|
||||
>
|
||||
{#if connectionTestJob !== undefined}
|
||||
@@ -559,7 +423,7 @@
|
||||
{:else if workspaceConnectedToSlack == undefined}
|
||||
<Loader2 class="animate-spin" size={10} />
|
||||
{/if}
|
||||
{:else if handlerSelected === 'teams'}
|
||||
{:else if draft.selected === 'teams'}
|
||||
<!-- Teams Connection Status -->
|
||||
<TeamsConnectionStatus
|
||||
isConnected={workspaceConnectedToTeams}
|
||||
@@ -571,18 +435,10 @@
|
||||
{#if workspaceConnectedToTeams}
|
||||
<Toggle
|
||||
disabled={!$enterpriseLicense || !isEditable}
|
||||
checked={isTeamsHandler(handlerPath)}
|
||||
checked={!!draft.teams.path}
|
||||
options={{ right: toggleText }}
|
||||
on:change={async (e) => {
|
||||
if (e.detail && errorOrRecovery === 'error') {
|
||||
handlerPath = teamsHandlerScriptPath
|
||||
} else if (e.detail && errorOrRecovery === 'recovery') {
|
||||
handlerPath = teamsRecoveryHandler
|
||||
} else if (e.detail && errorOrRecovery === 'success') {
|
||||
handlerPath = teamsSuccessHandler
|
||||
} else {
|
||||
handlerPath = undefined
|
||||
}
|
||||
on:change={(e) => {
|
||||
draft.teams.path = e.detail ? builtinHubPath(errorOrRecovery, 'teams') : undefined
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
@@ -604,26 +460,26 @@
|
||||
placeholder="Search Teams channels"
|
||||
workspace={effectiveWorkspace}
|
||||
teamId={teams_team_guid}
|
||||
selectedChannel={handlerExtraArgs['channel']
|
||||
selectedChannel={draft.teams.args['channel']
|
||||
? {
|
||||
channel_id: handlerExtraArgs['channel'],
|
||||
channel_name: handlerExtraArgs['channel_name']
|
||||
channel_id: draft.teams.args['channel'],
|
||||
channel_name: draft.teams.args['channel_name']
|
||||
}
|
||||
: undefined}
|
||||
onSelectedChannelChange={(channel) => {
|
||||
handlerExtraArgs['channel'] = channel?.channel_id
|
||||
handlerExtraArgs['channel_name'] = channel?.channel_name
|
||||
draft.teams.args['channel'] = channel?.channel_id
|
||||
draft.teams.args['channel_name'] = channel?.channel_name
|
||||
}}
|
||||
onError={(e) => sendUserToast('Failed to load channels: ' + e.message, true)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{#if $enterpriseLicense && isTeamsHandler(handlerPath) && workspaceConnectedToTeams}
|
||||
{#if $enterpriseLicense && draft.teams.path && workspaceConnectedToTeams}
|
||||
<Button
|
||||
disabled={emptyString(handlerExtraArgs['channel'])}
|
||||
disabled={emptyString(draft.teams.args['channel'])}
|
||||
btnClasses="w-32 text-center whitespace-nowrap"
|
||||
variant="default"
|
||||
on:click={() => sendTeamsMessage(handlerExtraArgs['channel'] ?? '')}
|
||||
on:click={() => sendTeamsMessage(draft.teams.args['channel'] ?? '')}
|
||||
size="xs">Send test message</Button
|
||||
>
|
||||
{#if connectionTestJob !== undefined}
|
||||
@@ -649,7 +505,7 @@
|
||||
{:else if workspaceConnectedToTeams == undefined}
|
||||
<Loader2 class="animate-spin" size={10} />
|
||||
{/if}
|
||||
{:else if handlerSelected === 'email'}
|
||||
{:else if draft.selected === 'email'}
|
||||
{#if isCloudHosted()}
|
||||
<Alert type="info" title="Email notifications are not available in Cloud">
|
||||
Email notifications for trigger failures are only available in self-hosted Windmill
|
||||
@@ -662,8 +518,8 @@
|
||||
<MultiSelect
|
||||
items={[] as { label: string; value: string }[]}
|
||||
bind:value={
|
||||
() => handlerExtraArgs[EMAIL_RECIPIENTS_KEY] ?? [],
|
||||
(recipients) => (handlerExtraArgs[EMAIL_RECIPIENTS_KEY] = recipients)
|
||||
() => draft.email.args.email_recipients ?? [],
|
||||
(recipients) => (draft.email.args.email_recipients = recipients)
|
||||
}
|
||||
placeholder="Enter email addresses..."
|
||||
onCreateItem={(email) => {
|
||||
@@ -672,23 +528,24 @@
|
||||
sendUserToast('Invalid email format', true)
|
||||
return
|
||||
}
|
||||
const currentArray = handlerExtraArgs[EMAIL_RECIPIENTS_KEY] ?? []
|
||||
handlerExtraArgs[EMAIL_RECIPIENTS_KEY] = [...currentArray, email]
|
||||
draft.email.args.email_recipients = [
|
||||
...(draft.email.args.email_recipients ?? []),
|
||||
email
|
||||
]
|
||||
}}
|
||||
class="w-full"
|
||||
/>
|
||||
{#if handlerExtraArgs[EMAIL_RECIPIENTS_KEY]?.length > 0}
|
||||
{#if draft.email.args.email_recipients?.length > 0}
|
||||
<span class="text-xs text-secondary">
|
||||
{handlerExtraArgs[EMAIL_RECIPIENTS_KEY]?.length} email{handlerExtraArgs[
|
||||
EMAIL_RECIPIENTS_KEY
|
||||
]?.length === 1
|
||||
{draft.email.args.email_recipients.length} email{draft.email.args.email_recipients
|
||||
.length === 1
|
||||
? ''
|
||||
: 's'} configured
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{:else if handlerSelected === 'instance_alerts'}
|
||||
{:else if draft.selected === 'instance_alerts'}
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-xs text-secondary">
|
||||
Failed jobs are reported to the Slack, Teams and email channels configured at the instance
|
||||
|
||||
@@ -1583,6 +1583,7 @@
|
||||
// Frozen while the rename popover is open so the trail holds still as the user types.
|
||||
kind: 'flow',
|
||||
path: pathSnapshot ?? $pathStore,
|
||||
summary: flowStore.val.summary,
|
||||
summaryContent: flowSummary,
|
||||
// Gated like the summary's own editor: a host that allows neither field keeps the
|
||||
// band's segment, which copies the path.
|
||||
|
||||
@@ -160,6 +160,9 @@ not this component) — so the lead can run the whole line.
|
||||
// The summary is the band's, not the breadcrumb's (see PageHeaderBar), so the path reads as the
|
||||
// last segment only for an item that has no summary to follow it.
|
||||
const itemIsLast = $derived(!!item && !item.summaryContent && !item.summary)
|
||||
/** Only the summary's text names the item: a control offering to add one does not, and the path
|
||||
* then stays at every stage. */
|
||||
const summaryNamesItem = $derived(!!item?.summary?.trim())
|
||||
|
||||
// No hover fill: the chevron beside it is what reacts to the pointer. The accent stays on the
|
||||
// fork's own name inside, so the family's name is not painted as if it were the fork.
|
||||
@@ -388,7 +391,7 @@ not this component) — so the lead can run the whole line.
|
||||
|
||||
<!-- The summary that follows is the item's name for a person, so on a short bar it is the path
|
||||
that goes (see `TrailStage.path`). What the page hangs after the name stays. -->
|
||||
{@const pathShown = f.path || itemIsLast}
|
||||
{@const pathShown = f.path || !summaryNamesItem}
|
||||
{#if !nameOnly && (item ? pathShown : section || routePage)}
|
||||
{@render slash()}
|
||||
{/if}
|
||||
|
||||
@@ -37,6 +37,8 @@ The row's height matches the sidebar's own header row, so the two read as one ba
|
||||
const item = $derived(content?.item)
|
||||
const section = $derived(content?.section)
|
||||
const hasSummary = $derived(!!item && !!(item.summaryContent || item.summary))
|
||||
/** A summary control can be a placeholder offering to add one; the text is what names the item. */
|
||||
const summaryNamesItem = $derived(!!item?.summary?.trim())
|
||||
const pickerAfterSummary = $derived(hasSummary && !navHidden && !!item?.path)
|
||||
|
||||
/** A phone's bar: a tighter gutter around the same row. */
|
||||
@@ -58,8 +60,10 @@ The row's height matches the sidebar's own header row, so the two read as one ba
|
||||
let summaryContentWidth = $state(0)
|
||||
let handleWidth = $state(0)
|
||||
/** The actions box at each fold level it has been seen at, for this page. */
|
||||
let actionWidths = $state<(number | undefined)[]>([])
|
||||
let actionWidthsByVersion = $state<Record<string, (number | undefined)[]>>({})
|
||||
let actionsEpoch = $state(0)
|
||||
const actionsVersion = $derived(content?.actionsVersion ?? '')
|
||||
const actionWidths = $derived(actionWidthsByVersion[actionsVersion] ?? [])
|
||||
|
||||
const handleShown = $derived((navDetached.val || panelled) && !navHidden)
|
||||
// The bar's own padding, in px, as the classes on the row below set it.
|
||||
@@ -81,6 +85,7 @@ The row's height matches the sidebar's own header row, so the two read as one ba
|
||||
actions.length === 0 ? [0] : actionsRigid ? actionWidths : [GAP + FLEXIBLE_ACTIONS_RESERVE],
|
||||
folds: actions.length > 0 && actionsRigid ? (content?.actionFolds ?? 0) : 0,
|
||||
earlyFolds: content?.earlyActionFolds,
|
||||
summaryNamesItem,
|
||||
natural: {
|
||||
family: trailMeasures.family,
|
||||
fork: trailMeasures.fork,
|
||||
@@ -90,13 +95,34 @@ The row's height matches the sidebar's own header row, so the two read as one ba
|
||||
})
|
||||
})
|
||||
const stage = $derived(BAR_STAGES[fit.stage])
|
||||
onDestroy(pageHeader.provideActionFold(() => fit.fold))
|
||||
/** The level alone: `fit` is a new object on every recompute, and the attachment re-measuring
|
||||
* on each one would feed itself. */
|
||||
const fold = $derived(fit.fold)
|
||||
onDestroy(pageHeader.provideActionFold(() => fold))
|
||||
|
||||
// Re-attached whenever the fold level changes, so the first reading at a level is taken even
|
||||
// when the row comes out exactly as wide as it was at the last one.
|
||||
function measureActions(fold: number, _epoch: number): Attachment<HTMLElement> {
|
||||
// Re-attached whenever the fold level, the page or the actions' version changes, so the first
|
||||
// reading there is taken even when the row comes out exactly as wide as it was before.
|
||||
// The page's actions can change on screen — a run's Cancel goes once the job completes — and
|
||||
// the other levels most likely changed with them, so a change here drops their widths: kept, a
|
||||
// stale wider one would hold the row folded for good. The bar measures them again by trying.
|
||||
function recordActions(fold: number, width: number) {
|
||||
const version = actionsVersion
|
||||
const widths = actionWidthsByVersion[version] ?? []
|
||||
const seen = widths[fold]
|
||||
// Unchanged, nothing is written: a write recomputes `fit`, and a reading that always wrote
|
||||
// kept the bar measuring every frame.
|
||||
if (seen === width) return
|
||||
const next = seen !== undefined ? [] : [...widths]
|
||||
next[fold] = width
|
||||
actionWidthsByVersion[version] = next
|
||||
}
|
||||
function measureActions(
|
||||
fold: number,
|
||||
_epoch: number,
|
||||
_version: string
|
||||
): Attachment<HTMLElement> {
|
||||
return (node) => {
|
||||
const ro = new ResizeObserver(() => (actionWidths[fold] = GAP + node.offsetWidth))
|
||||
const ro = new ResizeObserver(() => recordActions(fold, GAP + node.offsetWidth))
|
||||
ro.observe(node)
|
||||
return () => ro.disconnect()
|
||||
}
|
||||
@@ -136,7 +162,7 @@ The row's height matches the sidebar's own header row, so the two read as one ba
|
||||
afterNavigate(({ from, to }) => {
|
||||
scrolledUnder = false
|
||||
if (from?.url.pathname !== to?.url.pathname) {
|
||||
actionWidths = []
|
||||
actionWidthsByVersion = {}
|
||||
actionsEpoch++
|
||||
}
|
||||
})
|
||||
@@ -250,7 +276,7 @@ The row's height matches the sidebar's own header row, so the two read as one ba
|
||||
another level of it. With the path dropped it follows the workspace, and the slash is
|
||||
the trail's own separator. Fixed width, so either fits the width measured below. -->
|
||||
<span class="shrink-0 w-2 text-center text-hint/40 text-xs" aria-hidden="true"
|
||||
>{stage.path ? '·' : '/'}</span
|
||||
>{stage.path || !summaryNamesItem ? '·' : '/'}</span
|
||||
>
|
||||
{#if item.summaryContent}
|
||||
<!-- Read only while uncapped: capped, the box says how wide the bar let it be, not how
|
||||
@@ -297,7 +323,7 @@ The row's height matches the sidebar's own header row, so the two read as one ba
|
||||
bar and run to its right edge, and they stretch its full height because that surface
|
||||
sits on the bottom edge. -->
|
||||
<div
|
||||
{@attach measureActions(fit.fold, actionsEpoch)}
|
||||
{@attach measureActions(fold, actionsEpoch, actionsVersion)}
|
||||
class={twMerge(
|
||||
'flex',
|
||||
// pl-4: the gap between the breadcrumb and the filling surface is bare bar, so it
|
||||
|
||||
@@ -20,6 +20,7 @@ the header, so nesting is decided by the caller rather than guessed from context
|
||||
actionsFill,
|
||||
actionFolds,
|
||||
earlyActionFolds,
|
||||
actionsVersion,
|
||||
belowBar,
|
||||
belowBarFold,
|
||||
separator,
|
||||
@@ -39,6 +40,7 @@ the header, so nesting is decided by the caller rather than guessed from context
|
||||
actionsFill,
|
||||
actionFolds,
|
||||
earlyActionFolds,
|
||||
actionsVersion,
|
||||
belowBar,
|
||||
belowBarFold,
|
||||
separator,
|
||||
|
||||
@@ -2311,6 +2311,7 @@
|
||||
// route binds here and drops the `{#if}` around this editor in one flush, which
|
||||
// invalidates the band before this component is torn down.
|
||||
path: pathSnapshot ?? script?.path,
|
||||
summary: script?.summary,
|
||||
summaryContent: scriptSummary,
|
||||
// Gated like the summary's own editor: a host that allows neither field keeps the
|
||||
// band's segment, which copies the path.
|
||||
|
||||
@@ -1002,6 +1002,7 @@
|
||||
// Frozen while the rename popover is open so the trail holds still as the user types.
|
||||
kind: 'app',
|
||||
path: pathSnapshot ?? (newEditedPath || $appPath || newPath || undefined),
|
||||
summary: $summary,
|
||||
summaryContent: appSummary,
|
||||
pathTrigger: appPathTrigger
|
||||
}}
|
||||
|
||||
@@ -656,22 +656,13 @@
|
||||
wsId={$workspaceStore}
|
||||
itemKind="script"
|
||||
canWrite
|
||||
bind:errorHandlerSelected={config.advanced.errorHandlerSelected}
|
||||
bind:errorHandlerPath={config.advanced.errorHandlerPath}
|
||||
bind:errorHandleritemKind={config.advanced.errorHandleritemKind}
|
||||
bind:errorHandlerExtraArgs={config.advanced.errorHandlerExtraArgs}
|
||||
bind:errorHandler={config.advanced.errorHandler}
|
||||
bind:wsErrorHandlerMuted={config.advanced.wsErrorHandlerMuted}
|
||||
bind:failedTimes={config.advanced.failedTimes}
|
||||
bind:failedExact={config.advanced.failedExact}
|
||||
bind:recoveryHandlerSelected={config.advanced.recoveryHandlerSelected}
|
||||
bind:recoveryHandlerPath={config.advanced.recoveryHandlerPath}
|
||||
bind:recoveryHandlerItemKind={config.advanced.recoveryHandlerItemKind}
|
||||
bind:recoveryHandlerExtraArgs={config.advanced.recoveryHandlerExtraArgs}
|
||||
bind:recoveryHandler={config.advanced.recoveryHandler}
|
||||
bind:recoveredTimes={config.advanced.recoveredTimes}
|
||||
bind:successHandlerSelected={config.advanced.successHandlerSelected}
|
||||
bind:successHandlerPath={config.advanced.successHandlerPath}
|
||||
bind:successHandlerItemKind={config.advanced.successHandlerItemKind}
|
||||
bind:successHandlerExtraArgs={config.advanced.successHandlerExtraArgs}
|
||||
bind:successHandler={config.advanced.successHandler}
|
||||
bind:retry={config.advanced.retry}
|
||||
bind:dynamicSkipPath={config.advanced.dynamicSkipPath}
|
||||
bind:tag={config.advanced.tag}
|
||||
|
||||
@@ -106,10 +106,11 @@
|
||||
|
||||
// The band folds the buttons once its breadcrumb has given all it can (`actionFolds` below):
|
||||
// first the secondary ones go to their `placement`, then every bar button but the last, which
|
||||
// is what the page is for (Edit), then that one too. The host knows when its own box is narrow
|
||||
// while the window is not — a session's preview panel — and says so through `wide`, which only
|
||||
// sends the error handler's toggle to the menu.
|
||||
const fold = $derived(ownsPageHeader ? pageHeader.actionFold : 0)
|
||||
// is what the page is for (Edit), then that one too. Outside the band — a session's preview
|
||||
// panel — no bar measures the row, but the host knows when its own box is narrow while the
|
||||
// window is not and says so through `wide`: the secondary buttons fold there, as at the first
|
||||
// fold, and the error handler's toggle goes to the menu.
|
||||
const fold = $derived(ownsPageHeader ? pageHeader.actionFold : wide ? 0 : 1)
|
||||
const wideRow = $derived(wide && fold <= 1)
|
||||
|
||||
const rowButtons = $derived(mainButtons.filter((b) => !b.placement))
|
||||
@@ -303,6 +304,9 @@
|
||||
item={{
|
||||
kind: errorHandlerKind,
|
||||
path,
|
||||
// Read-only, the summary control falls back to the path when there is no summary, so the
|
||||
// path is what names the item there and the trail's own copy can go.
|
||||
summary: onSaved ? summary : summary || path,
|
||||
summaryContent,
|
||||
// Only where this page can save one: without `onSaved` the editor behind it has nothing
|
||||
// to write, and the band keeps its own segment, which copies the path.
|
||||
|
||||
@@ -318,6 +318,7 @@
|
||||
<PageHeaderContent
|
||||
item={{
|
||||
path: pathSnapshot ?? shownPath,
|
||||
summary: draft?.state?.description,
|
||||
summaryContent: agentSummary,
|
||||
// Only where the rename behind it can be saved: a reader who cannot write this agent
|
||||
// keeps the segment that copies the path, rather than an editor with nothing to edit.
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import type { ErrorHandler } from '$lib/gen'
|
||||
import { WORKSPACE_SLACK_BOT_TOKEN_PATH } from '$lib/consts'
|
||||
import hubPaths from '$lib/hubPaths.json'
|
||||
|
||||
export type HandlerMode = 'error' | 'recovery' | 'success'
|
||||
type BuiltinDestination = 'slack' | 'teams' | 'email'
|
||||
|
||||
export type HandlerSlot = { path?: string; args: Record<string, any> }
|
||||
export type HandlerDraft = {
|
||||
selected: ErrorHandler
|
||||
slack: HandlerSlot
|
||||
teams: HandlerSlot
|
||||
email: HandlerSlot
|
||||
custom: HandlerSlot & { kind: 'script' | 'flow' }
|
||||
}
|
||||
type StoredHandler = { path?: string; kind: 'script' | 'flow'; args: Record<string, any> }
|
||||
|
||||
const SLACK_BOT_ARG = '$res:' + WORKSPACE_SLACK_BOT_TOKEN_PATH
|
||||
|
||||
// Suffixes rather than full paths, so a handler stored under an older hub id is still recognised.
|
||||
// Email uses one script for every mode.
|
||||
const EMAIL_SUFFIX = '/workspace-or-error-handler-email'
|
||||
const BUILTIN_SUFFIXES: Record<HandlerMode, Record<'slack' | 'teams', string>> = {
|
||||
error: {
|
||||
slack: '/workspace-or-schedule-error-handler-slack',
|
||||
teams: '/workspace-or-schedule-error-handler-teams'
|
||||
},
|
||||
recovery: {
|
||||
slack: '/schedule-recovery-handler-slack',
|
||||
teams: '/schedule-recovery-handler-teams'
|
||||
},
|
||||
success: {
|
||||
slack: '/schedule-success-handler-slack',
|
||||
teams: '/schedule-success-handler-teams'
|
||||
}
|
||||
}
|
||||
|
||||
// The only settings each built-in hub script takes besides the ones the backend injects.
|
||||
const BUILTIN_KEYS: Record<BuiltinDestination, string[]> = {
|
||||
slack: ['channel'],
|
||||
teams: ['channel', 'channel_name'],
|
||||
email: ['email_recipients']
|
||||
}
|
||||
|
||||
export function builtinHubPath(mode: HandlerMode, destination: 'slack' | 'teams'): string {
|
||||
const key =
|
||||
`${destination}${mode[0].toUpperCase()}${mode.slice(1)}Handler` as keyof typeof hubPaths
|
||||
return hubPaths[key]
|
||||
}
|
||||
|
||||
export function handlerTypeForPath(mode: HandlerMode, path: string | undefined): ErrorHandler {
|
||||
if (path?.startsWith('hub/')) {
|
||||
if (path.endsWith(EMAIL_SUFFIX)) return 'email'
|
||||
for (const [destination, suffix] of Object.entries(BUILTIN_SUFFIXES[mode])) {
|
||||
if (path.endsWith(suffix)) return destination as BuiltinDestination
|
||||
}
|
||||
}
|
||||
return 'custom'
|
||||
}
|
||||
|
||||
function emptyDraft(selected: ErrorHandler): HandlerDraft {
|
||||
return {
|
||||
selected,
|
||||
slack: { args: {} },
|
||||
teams: { args: {} },
|
||||
email: { path: hubPaths.emailErrorHandler, args: {} },
|
||||
custom: { kind: 'script', args: {} }
|
||||
}
|
||||
}
|
||||
|
||||
function pick(args: Record<string, any>, keys: string[]): Record<string, any> {
|
||||
return Object.fromEntries(keys.filter((k) => args[k] !== undefined).map((k) => [k, args[k]]))
|
||||
}
|
||||
|
||||
/** Puts a stored handler into the slot of its destination; every other slot starts empty. */
|
||||
export function handlerDraftFromStored(
|
||||
mode: HandlerMode,
|
||||
path: string | undefined,
|
||||
kind: 'script' | 'flow' = 'script',
|
||||
args: Record<string, any> | null | undefined = {},
|
||||
opts: { asCustom?: boolean; emptySelected?: ErrorHandler } = {}
|
||||
): HandlerDraft {
|
||||
if (!path) return emptyDraft(opts.emptySelected ?? 'custom')
|
||||
const destination = opts.asCustom ? 'custom' : handlerTypeForPath(mode, path)
|
||||
const draft = emptyDraft(destination)
|
||||
if (destination === 'custom') {
|
||||
draft.custom = { path, kind, args: { ...(args ?? {}) } }
|
||||
} else if (destination !== 'instance_alerts') {
|
||||
draft[destination] = { path, args: pick(args ?? {}, BUILTIN_KEYS[destination]) }
|
||||
}
|
||||
return draft
|
||||
}
|
||||
|
||||
/** What a save stores: the selected destination only. */
|
||||
export function handlerDraftToStored(draft: HandlerDraft): StoredHandler {
|
||||
const none: StoredHandler = { kind: 'script', args: {} }
|
||||
switch (draft.selected) {
|
||||
case 'custom': {
|
||||
const { path, kind, args } = draft.custom
|
||||
return path ? { path, kind, args: $state.snapshot(args) } : none
|
||||
}
|
||||
case 'slack':
|
||||
case 'teams':
|
||||
case 'email': {
|
||||
const { path, args } = draft[draft.selected]
|
||||
if (!path) return none
|
||||
const stored = { path, kind: 'script' as const, args: $state.snapshot(args) }
|
||||
if (draft.selected === 'slack') stored.args.slack = SLACK_BOT_ARG
|
||||
return stored
|
||||
}
|
||||
default:
|
||||
return none
|
||||
}
|
||||
}
|
||||
|
||||
export function cloneHandlerDraft(draft: HandlerDraft): HandlerDraft {
|
||||
return $state.snapshot(draft) as HandlerDraft
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
handlerDraftFromStored,
|
||||
handlerDraftToStored,
|
||||
handlerTypeForPath
|
||||
} from './handlerDraft.svelte'
|
||||
|
||||
const SLACK_BOT = '$res:f/slack_bot/bot_token'
|
||||
|
||||
describe('handler draft', () => {
|
||||
it('round-trips every destination unchanged', () => {
|
||||
const stored = [
|
||||
{
|
||||
path: 'hub/28794/workspace-or-schedule-error-handler-slack',
|
||||
kind: 'script' as const,
|
||||
args: { channel: 'alerts', slack: SLACK_BOT }
|
||||
},
|
||||
{
|
||||
path: 'hub/19742/workspace-or-schedule-error-handler-teams',
|
||||
kind: 'script' as const,
|
||||
args: { channel: 'c1', channel_name: 'General' }
|
||||
},
|
||||
{
|
||||
path: 'hub/19795/workspace-or-error-handler-email',
|
||||
kind: 'script' as const,
|
||||
args: { email_recipients: ['ops@example.com'] }
|
||||
},
|
||||
{
|
||||
path: 'f/handlers/on_error',
|
||||
kind: 'flow' as const,
|
||||
args: { slack: '$res:u/me/my_slack', channel: 'own' }
|
||||
}
|
||||
]
|
||||
for (const s of stored) {
|
||||
const draft = handlerDraftFromStored('error', s.path, s.kind, s.args)
|
||||
expect(handlerDraftToStored(draft)).toEqual(s)
|
||||
}
|
||||
})
|
||||
|
||||
it("saves only the selected destination's settings", () => {
|
||||
const draft = handlerDraftFromStored('error', 'u/me/handler', 'script', {
|
||||
webhook_url: 'https://hooks.example/custom'
|
||||
})
|
||||
draft.selected = 'email'
|
||||
draft.email.args.email_recipients = ['ops@example.com']
|
||||
expect(handlerDraftToStored(draft)).toEqual({
|
||||
path: 'hub/19795/workspace-or-error-handler-email',
|
||||
kind: 'script',
|
||||
args: { email_recipients: ['ops@example.com'] }
|
||||
})
|
||||
})
|
||||
|
||||
it('recognises built-in handlers per mode', () => {
|
||||
const recoverySlack = 'hub/28791/slack/schedule-recovery-handler-slack'
|
||||
expect(handlerTypeForPath('recovery', recoverySlack)).toBe('slack')
|
||||
expect(handlerTypeForPath('error', recoverySlack)).toBe('custom')
|
||||
})
|
||||
|
||||
it('stores no handler when the destination has none', () => {
|
||||
const draft = handlerDraftFromStored('error', undefined)
|
||||
expect(handlerDraftToStored(draft)).toEqual({ kind: 'script', args: {} })
|
||||
draft.selected = 'slack'
|
||||
expect(handlerDraftToStored(draft)).toEqual({ kind: 'script', args: {} })
|
||||
draft.selected = 'instance_alerts'
|
||||
expect(handlerDraftToStored(draft)).toEqual({ kind: 'script', args: {} })
|
||||
})
|
||||
})
|
||||
@@ -1,21 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { syncSlackBotArg } from './handlerSlackArg'
|
||||
|
||||
const AUTO = '$res:f/slack_bot/bot_token'
|
||||
|
||||
describe('syncSlackBotArg', () => {
|
||||
it("leaves a custom handler's slack value alone", () => {
|
||||
const args = { slack: '$res:u/me/my_slack' }
|
||||
syncSlackBotArg(args, false, false)
|
||||
syncSlackBotArg(args, false, true)
|
||||
expect(args).toEqual({ slack: '$res:u/me/my_slack' })
|
||||
})
|
||||
|
||||
it('sets the bot token for the built-in and drops it when leaving', () => {
|
||||
const args: Record<string, any> = {}
|
||||
syncSlackBotArg(args, true, false)
|
||||
expect(args.slack).toBe(AUTO)
|
||||
syncSlackBotArg(args, false, true)
|
||||
expect(args).toStrictEqual({})
|
||||
})
|
||||
})
|
||||
@@ -1,17 +0,0 @@
|
||||
import { WORKSPACE_SLACK_BOT_TOKEN_PATH } from '$lib/consts'
|
||||
|
||||
const AUTO_SLACK_ARG = '$res:' + WORKSPACE_SLACK_BOT_TOKEN_PATH
|
||||
|
||||
// `slack` belongs to the Slack built-in only while it holds the value the picker set; any other
|
||||
// value is a custom handler's own argument and must survive handler switches and reloads.
|
||||
export function syncSlackBotArg(
|
||||
args: Record<string, any>,
|
||||
isSlackBuiltin: boolean,
|
||||
wasSlackBuiltin: boolean
|
||||
) {
|
||||
if (isSlackBuiltin) {
|
||||
args.slack = AUTO_SLACK_ARG
|
||||
} else if (wasSlackBuiltin && args.slack === AUTO_SLACK_ARG) {
|
||||
delete args.slack
|
||||
}
|
||||
}
|
||||
@@ -446,7 +446,7 @@ export const APP_TO_ICON_COMPONENT = {
|
||||
quickbooks: QuickbooksIcon,
|
||||
ms_teams_webhook: MsTeamsIcon,
|
||||
teams: MsTeamsIcon,
|
||||
outlook: OutlookIcon,
|
||||
ms_outlook: OutlookIcon,
|
||||
mailgun: MailgunIcon,
|
||||
ipinfo: IpinfoIcon,
|
||||
gworkspace: GoogleIcon,
|
||||
|
||||
@@ -60,6 +60,10 @@ export type PageHeaderContent = {
|
||||
/** How many of the `actionFolds` levels come before the breadcrumb drops anything: buttons
|
||||
* that are there only while the bar has room for everything. */
|
||||
earlyActionFolds?: number
|
||||
/** Changes whenever the actions may have changed width at a level that is not on screen — a
|
||||
* run's Cancel, folded into a menu, going once the job completes. The bar keeps what it
|
||||
* measured per version, so a new one is measured afresh rather than judged by stale widths. */
|
||||
actionsVersion?: string
|
||||
/** A second row of the band, under the breadcrumb and the actions: where a control too wide
|
||||
* for one row goes at a fold that has no room for it beside them, rather than being squeezed
|
||||
* until it is useless. The band is taller while it is there. */
|
||||
@@ -171,6 +175,7 @@ export const pageHeader = {
|
||||
if (c.actionsFill !== undefined) merged.actionsFill = c.actionsFill
|
||||
if (c.actionFolds !== undefined) merged.actionFolds = c.actionFolds
|
||||
if (c.earlyActionFolds !== undefined) merged.earlyActionFolds = c.earlyActionFolds
|
||||
if (c.actionsVersion !== undefined) merged.actionsVersion = c.actionsVersion
|
||||
if (c.belowBar !== undefined) merged.belowBar = c.belowBar
|
||||
if (c.belowBarFold !== undefined) merged.belowBarFold = c.belowBarFold
|
||||
if (c.separator !== undefined) merged.separator = c.separator
|
||||
|
||||
@@ -63,5 +63,15 @@ describe('fitBar', () => {
|
||||
const fit = fitBar({ ...base, barWidth: 520, natural, actions: [0], folds: 0 })
|
||||
expect(fit.stage).toBe(2)
|
||||
expect(fit.caps.name).toBeUndefined()
|
||||
// A placeholder offering to add a summary names nothing: the path stays a part the bar sizes.
|
||||
const placeholder = fitBar({
|
||||
...base,
|
||||
barWidth: 520,
|
||||
natural,
|
||||
actions: [0],
|
||||
folds: 0,
|
||||
summaryNamesItem: false
|
||||
})
|
||||
expect(placeholder.caps.name).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -95,7 +95,12 @@ export function shownPathLevels(levels: string[], stage: TrailStage): string[] {
|
||||
}
|
||||
|
||||
/** Whether a part is on screen at a stage. A natural width of 0 means the page has no such part. */
|
||||
export function partVisible(part: Part, stage: BarStage, natural: Record<Part, number>): boolean {
|
||||
export function partVisible(
|
||||
part: Part,
|
||||
stage: BarStage,
|
||||
natural: Record<Part, number>,
|
||||
summaryNamesItem = true
|
||||
): boolean {
|
||||
if (natural[part] <= 0) return false
|
||||
switch (part) {
|
||||
case 'summary':
|
||||
@@ -106,7 +111,7 @@ export function partVisible(part: Part, stage: BarStage, natural: Record<Part, n
|
||||
case 'fork':
|
||||
return !stage.wsButton && stage.forkName
|
||||
case 'name':
|
||||
return stage.path || natural.summary <= 0
|
||||
return stage.path || natural.summary <= 0 || !summaryNamesItem
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +131,9 @@ export type BarMeasure = {
|
||||
* only while there is room for everything. */
|
||||
earlyFolds?: number
|
||||
natural: Record<Part, number>
|
||||
/** The summary is the item's own text, not a placeholder offering to add one: only then does it
|
||||
* name the item well enough for the path to go. */
|
||||
summaryNamesItem?: boolean
|
||||
}
|
||||
|
||||
export type BarFit = {
|
||||
@@ -185,7 +193,7 @@ export function fitBar(m: BarMeasure): BarFit {
|
||||
const at = BAR_STAGES[stage]
|
||||
// The summary gives last: it is what a person calls the item, so the path's name goes first.
|
||||
const parts = (['family', 'fork', 'name', 'summary'] as Part[]).filter((p) =>
|
||||
partVisible(p, at, m.natural)
|
||||
partVisible(p, at, m.natural, m.summaryNamesItem ?? true)
|
||||
)
|
||||
const slack = (p: Part) => Math.max(0, m.natural[p] - Math.min(m.natural[p], FLOOR[p]))
|
||||
// The stage was measured with its parts at their floors; this is its width with them whole.
|
||||
|
||||
@@ -951,6 +951,7 @@
|
||||
// rename popover is open so the trail holds still as the user types.
|
||||
kind: 'app',
|
||||
path: pathSnapshot ?? (newEditedPath || appPath || newPath || undefined),
|
||||
summary,
|
||||
summaryContent: appSummary,
|
||||
pathTrigger: rawAppPathTrigger
|
||||
}}
|
||||
|
||||
@@ -765,6 +765,25 @@
|
||||
!isCloudHosted() &&
|
||||
editInForkAllowed(workspaceId, $userWorkspaces)
|
||||
)
|
||||
/** Which actions the row has: they come and go as the job runs and completes, often while the
|
||||
* row is folded into the menu, where the bar cannot see them change (`actionsVersion`). */
|
||||
const actionsVersion = $derived(
|
||||
[
|
||||
canDelete,
|
||||
canCancel,
|
||||
forceCancel,
|
||||
canDebug,
|
||||
canRunNow,
|
||||
canEdit,
|
||||
canEditInFork,
|
||||
dbtResumable,
|
||||
!!job?.schedule_path,
|
||||
persistentScriptDefinition !== undefined,
|
||||
restartStep !== undefined && canRestart(restart)
|
||||
]
|
||||
.map(Number)
|
||||
.join('')
|
||||
)
|
||||
|
||||
async function runAgain() {
|
||||
// The form this lands on rebuilds the whole project. When resuming THIS run is the cheaper
|
||||
@@ -1059,7 +1078,12 @@
|
||||
{@render runActions()}
|
||||
</div>
|
||||
{:else}
|
||||
<PageHeaderContent section={{ label: `run/${runId}` }} actions={runActions} actionFolds={2} />
|
||||
<PageHeaderContent
|
||||
section={{ label: `run/${runId}` }}
|
||||
actions={runActions}
|
||||
actionFolds={2}
|
||||
{actionsVersion}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#snippet runActions()}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import type { ErrorHandler, Retry } from '$lib/gen'
|
||||
import type { Retry } from '$lib/gen'
|
||||
import { enterpriseLicense } from '$lib/stores'
|
||||
import { emptyString } from '$lib/utils'
|
||||
import Alert from '../common/alert/Alert.svelte'
|
||||
import ErrorOrRecoveryHandler from '../ErrorOrRecoveryHandler.svelte'
|
||||
import type { HandlerDraft } from '../handlerDraft.svelte'
|
||||
import FlowRetries from '../flows/content/FlowRetries.svelte'
|
||||
import Tooltip from '../Tooltip.svelte'
|
||||
|
||||
@@ -11,18 +12,14 @@
|
||||
optionTabSelected,
|
||||
itemKind,
|
||||
can_write,
|
||||
errorHandlerSelected = $bindable(),
|
||||
error_handler_path = $bindable(),
|
||||
error_handler_args = $bindable(),
|
||||
errorHandler = $bindable(),
|
||||
retry = $bindable(),
|
||||
workspace = undefined
|
||||
}: {
|
||||
optionTabSelected: 'error_handler' | 'retries' | string
|
||||
itemKind: 'script' | 'flow'
|
||||
can_write: boolean
|
||||
errorHandlerSelected: ErrorHandler
|
||||
error_handler_path: string | undefined
|
||||
error_handler_args: Record<string, any>
|
||||
errorHandler: HandlerDraft
|
||||
retry: Retry | undefined
|
||||
workspace?: string
|
||||
} = $props()
|
||||
@@ -42,12 +39,9 @@
|
||||
errorOrRecovery="error"
|
||||
showScriptHelpText={true}
|
||||
{workspace}
|
||||
bind:handlerSelected={errorHandlerSelected}
|
||||
bind:handlerPath={error_handler_path}
|
||||
bind:draft={errorHandler}
|
||||
toggleText="Alert channel on error"
|
||||
customScriptTemplate="/scripts/add?hub=hub%2F13953%2Fwindmill%2Ftrigger_error_handler_template"
|
||||
customHandlerKind="script"
|
||||
bind:handlerExtraArgs={error_handler_args}
|
||||
>
|
||||
{#snippet customTabTooltip()}
|
||||
<Tooltip>
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
type AmqpExchange,
|
||||
type AmqpOptions,
|
||||
type Retry,
|
||||
type ErrorHandler,
|
||||
type TriggerMode
|
||||
} from '$lib/gen'
|
||||
import AmqpEditorConfigSection from './AmqpEditorConfigSection.svelte'
|
||||
@@ -30,7 +29,8 @@
|
||||
import TriggerEditorToolbar from '../TriggerEditorToolbar.svelte'
|
||||
import PermissionedAsLine from '../PermissionedAsLine.svelte'
|
||||
import { saveAmqpTriggerFromCfg } from './utils'
|
||||
import { getHandlerType, handleConfigChange, type Trigger } from '../utils'
|
||||
import { handleConfigChange, type Trigger } from '../utils'
|
||||
import { handlerDraftFromStored, handlerDraftToStored } from '../../handlerDraft.svelte'
|
||||
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
|
||||
import Tab from '$lib/components/common/tabs/Tab.svelte'
|
||||
import TriggerRetriesAndErrorHandler from '../TriggerRetriesAndErrorHandler.svelte'
|
||||
@@ -127,9 +127,10 @@
|
||||
let permissionedAs = $state<string | undefined>(undefined)
|
||||
let selectedPermissionedAs = $state<string | undefined>(undefined)
|
||||
let preservePermissionedAs = $state(false)
|
||||
let errorHandlerSelected: ErrorHandler = $state('slack')
|
||||
let error_handler_path: string | undefined = $state()
|
||||
let error_handler_args: Record<string, any> = $state({})
|
||||
let errorHandler = $state(handlerDraftFromStored('error', undefined))
|
||||
const storedErrorHandler = $derived(handlerDraftToStored(errorHandler))
|
||||
const error_handler_path = $derived(storedErrorHandler.path)
|
||||
const error_handler_args = $derived(storedErrorHandler.args)
|
||||
let retry: Retry | undefined = $state()
|
||||
let suspendedJobsModal = $state<TriggerSuspendedJobsModal | null>(null)
|
||||
let originalConfig = $state<Record<string, any> | undefined>(undefined)
|
||||
@@ -248,10 +249,13 @@
|
||||
edit = false
|
||||
dirtyPath = false
|
||||
mode = defaultValues?.mode ?? 'enabled'
|
||||
error_handler_path = defaultValues?.error_handler_path ?? undefined
|
||||
error_handler_args = defaultValues?.error_handler_args ?? {}
|
||||
errorHandler = handlerDraftFromStored(
|
||||
'error',
|
||||
defaultValues?.error_handler_path,
|
||||
'script',
|
||||
defaultValues?.error_handler_args
|
||||
)
|
||||
retry = defaultValues?.retry ?? undefined
|
||||
errorHandlerSelected = getHandlerType(error_handler_path ?? '')
|
||||
permissionedAs = undefined
|
||||
selectedPermissionedAs = undefined
|
||||
preservePermissionedAs = false
|
||||
@@ -280,10 +284,13 @@
|
||||
mode = cfg?.mode ?? 'enabled'
|
||||
permsPath = cfg?.path
|
||||
permsForWrite = cfg?.extra_perms
|
||||
error_handler_path = cfg?.error_handler_path
|
||||
error_handler_args = cfg?.error_handler_args ?? {}
|
||||
errorHandler = handlerDraftFromStored(
|
||||
'error',
|
||||
cfg?.error_handler_path,
|
||||
'script',
|
||||
cfg?.error_handler_args
|
||||
)
|
||||
retry = cfg?.retry
|
||||
errorHandlerSelected = getHandlerType(error_handler_path ?? '')
|
||||
permissionedAs = cfg?.permissioned_as
|
||||
selectedPermissionedAs = cfg?.permissioned_as
|
||||
preservePermissionedAs = !!cfg?.permissioned_as
|
||||
@@ -667,9 +674,7 @@
|
||||
{optionTabSelected}
|
||||
{itemKind}
|
||||
{can_write}
|
||||
bind:errorHandlerSelected
|
||||
bind:error_handler_path
|
||||
bind:error_handler_args
|
||||
bind:errorHandler
|
||||
bind:retry
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -14,13 +14,7 @@
|
||||
import { withForkConflictRetry } from '$lib/utils/forkConflict'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import {
|
||||
AzureTriggerService,
|
||||
type AzureMode,
|
||||
type Retry,
|
||||
type ErrorHandler,
|
||||
type TriggerMode
|
||||
} from '$lib/gen'
|
||||
import { AzureTriggerService, type AzureMode, type Retry, type TriggerMode } from '$lib/gen'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
|
||||
import Required from '$lib/components/Required.svelte'
|
||||
@@ -29,7 +23,8 @@
|
||||
import TriggerEditorToolbar from '../TriggerEditorToolbar.svelte'
|
||||
import PermissionedAsLine from '../PermissionedAsLine.svelte'
|
||||
import { saveAzureTriggerFromCfg } from './utils'
|
||||
import { getHandlerType, handleConfigChange, type Trigger } from '../utils'
|
||||
import { handleConfigChange, type Trigger } from '../utils'
|
||||
import { handlerDraftFromStored, handlerDraftToStored } from '../../handlerDraft.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { useTriggerDraftSync } from '../useTriggerDraftSync.svelte'
|
||||
import LocalDraftBanner from '$lib/components/LocalDraftBanner.svelte'
|
||||
@@ -87,9 +82,10 @@
|
||||
let base_endpoint = $derived(apiBaseUrl())
|
||||
|
||||
let optionTabSelected: 'error_handler' | 'retries' = $state('error_handler')
|
||||
let errorHandlerSelected: ErrorHandler = $state('slack')
|
||||
let error_handler_path: string | undefined = $state()
|
||||
let error_handler_args: Record<string, any> = $state({})
|
||||
let errorHandler = $state(handlerDraftFromStored('error', undefined))
|
||||
const storedErrorHandler = $derived(handlerDraftToStored(errorHandler))
|
||||
const error_handler_path = $derived(storedErrorHandler.path)
|
||||
const error_handler_args = $derived(storedErrorHandler.args)
|
||||
let retry: Retry | undefined = $state()
|
||||
let suspendedJobsModal = $state<TriggerSuspendedJobsModal | null>(null)
|
||||
let originalConfig = $state<Record<string, any> | undefined>(undefined)
|
||||
@@ -211,10 +207,13 @@
|
||||
edit = false
|
||||
dirtyPath = false
|
||||
mode = defaultValues?.mode ?? 'enabled'
|
||||
error_handler_path = defaultValues?.error_handler_path ?? undefined
|
||||
error_handler_args = defaultValues?.error_handler_args ?? {}
|
||||
errorHandler = handlerDraftFromStored(
|
||||
'error',
|
||||
defaultValues?.error_handler_path,
|
||||
'script',
|
||||
defaultValues?.error_handler_args
|
||||
)
|
||||
retry = defaultValues?.retry ?? undefined
|
||||
errorHandlerSelected = getHandlerType(error_handler_path ?? '')
|
||||
permissionedAs = undefined
|
||||
selectedPermissionedAs = undefined
|
||||
preservePermissionedAs = false
|
||||
@@ -267,10 +266,13 @@
|
||||
mode = cfg?.mode ?? 'enabled'
|
||||
permsPath = cfg?.path
|
||||
permsForWrite = cfg?.extra_perms
|
||||
error_handler_path = cfg?.error_handler_path
|
||||
error_handler_args = cfg?.error_handler_args ?? {}
|
||||
errorHandler = handlerDraftFromStored(
|
||||
'error',
|
||||
cfg?.error_handler_path,
|
||||
'script',
|
||||
cfg?.error_handler_args
|
||||
)
|
||||
retry = cfg?.retry
|
||||
errorHandlerSelected = getHandlerType(error_handler_path ?? '')
|
||||
permissionedAs = cfg?.permissioned_as
|
||||
selectedPermissionedAs = cfg?.permissioned_as
|
||||
preservePermissionedAs = !!cfg?.permissioned_as
|
||||
@@ -570,9 +572,7 @@
|
||||
{optionTabSelected}
|
||||
{itemKind}
|
||||
{can_write}
|
||||
bind:errorHandlerSelected
|
||||
bind:error_handler_path
|
||||
bind:error_handler_args
|
||||
bind:errorHandler
|
||||
bind:retry
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
import TriggerRunnablePicker from '$lib/components/triggers/TriggerRunnablePicker.svelte'
|
||||
import {
|
||||
EmailTriggerService,
|
||||
type ErrorHandler,
|
||||
type EmailTrigger,
|
||||
type NewEmailTrigger,
|
||||
type Retry,
|
||||
@@ -26,7 +25,8 @@
|
||||
import EmailTriggerEditorConfigSection from './EmailTriggerEditorConfigSection.svelte'
|
||||
import TriggerEditorToolbar from '../TriggerEditorToolbar.svelte'
|
||||
import PermissionedAsLine from '../PermissionedAsLine.svelte'
|
||||
import { getHandlerType, handleConfigChange } from '../utils'
|
||||
import { handleConfigChange } from '../utils'
|
||||
import { handlerDraftFromStored, handlerDraftToStored } from '../../handlerDraft.svelte'
|
||||
import { untrack } from 'svelte'
|
||||
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
|
||||
import Tab from '$lib/components/common/tabs/Tab.svelte'
|
||||
@@ -96,8 +96,10 @@
|
||||
: editRights.canEditItem('trigger', permsPath, permsForWrite)
|
||||
)
|
||||
let extraPerms = $state<Record<string, boolean> | undefined>(undefined)
|
||||
let error_handler_path: string | undefined = $state()
|
||||
let error_handler_args: Record<string, any> = $state({})
|
||||
let errorHandler = $state(handlerDraftFromStored('error', undefined))
|
||||
const storedErrorHandler = $derived(handlerDraftToStored(errorHandler))
|
||||
const error_handler_path = $derived(storedErrorHandler.path)
|
||||
const error_handler_args = $derived(storedErrorHandler.args)
|
||||
let retry: Retry | undefined = $state()
|
||||
let mode = $state<TriggerMode>('enabled')
|
||||
// Component references
|
||||
@@ -108,7 +110,6 @@
|
||||
let selectedPermissionedAs = $state<string | undefined>(undefined)
|
||||
let preservePermissionedAs = $state(false)
|
||||
let optionTabSelected: 'error_handler' | 'retries' = $state('error_handler')
|
||||
let errorHandlerSelected: ErrorHandler = $state('slack')
|
||||
|
||||
let suspendedJobsModal = $state<TriggerSuspendedJobsModal | null>(null)
|
||||
let originalConfig = $state<NewEmailTrigger | undefined>(undefined)
|
||||
@@ -222,10 +223,13 @@
|
||||
dirtyPath = false
|
||||
|
||||
workspaced_local_part = defaultValues?.workspaced_local_part ?? false
|
||||
error_handler_path = defaultValues?.error_handler_path ?? undefined
|
||||
error_handler_args = defaultValues?.error_handler_args ?? {}
|
||||
errorHandler = handlerDraftFromStored(
|
||||
'error',
|
||||
defaultValues?.error_handler_path,
|
||||
'script',
|
||||
defaultValues?.error_handler_args
|
||||
)
|
||||
retry = defaultValues?.retry ?? undefined
|
||||
errorHandlerSelected = getHandlerType(error_handler_path ?? '')
|
||||
mode = defaultValues?.mode ?? 'enabled'
|
||||
permissionedAs = undefined
|
||||
selectedPermissionedAs = undefined
|
||||
@@ -251,10 +255,13 @@
|
||||
extraPerms = cfg?.extra_perms ?? undefined
|
||||
permsPath = path
|
||||
permsForWrite = cfg?.extra_perms ?? {}
|
||||
error_handler_path = cfg?.error_handler_path
|
||||
error_handler_args = cfg?.error_handler_args ?? {}
|
||||
errorHandler = handlerDraftFromStored(
|
||||
'error',
|
||||
cfg?.error_handler_path,
|
||||
'script',
|
||||
cfg?.error_handler_args
|
||||
)
|
||||
retry = cfg?.retry
|
||||
errorHandlerSelected = getHandlerType(error_handler_path ?? '')
|
||||
mode = cfg?.mode ?? 'enabled'
|
||||
permissionedAs = cfg?.permissioned_as
|
||||
selectedPermissionedAs = cfg?.permissioned_as
|
||||
@@ -495,9 +502,7 @@
|
||||
{optionTabSelected}
|
||||
{itemKind}
|
||||
{can_write}
|
||||
bind:errorHandlerSelected
|
||||
bind:error_handler_path
|
||||
bind:error_handler_args
|
||||
bind:errorHandler
|
||||
bind:retry
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
type PushConfig,
|
||||
type SubscriptionMode,
|
||||
type Retry,
|
||||
type ErrorHandler,
|
||||
type TriggerMode
|
||||
} from '$lib/gen'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
@@ -30,7 +29,8 @@
|
||||
import TriggerEditorToolbar from '../TriggerEditorToolbar.svelte'
|
||||
import PermissionedAsLine from '../PermissionedAsLine.svelte'
|
||||
import { saveGcpTriggerFromCfg } from './utils'
|
||||
import { getHandlerType, handleConfigChange, type Trigger } from '../utils'
|
||||
import { handleConfigChange, type Trigger } from '../utils'
|
||||
import { handlerDraftFromStored, handlerDraftToStored } from '../../handlerDraft.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { useTriggerDraftSync } from '../useTriggerDraftSync.svelte'
|
||||
import LocalDraftBanner from '$lib/components/LocalDraftBanner.svelte'
|
||||
@@ -93,9 +93,10 @@
|
||||
let auto_acknowledge_msg = $state(true)
|
||||
let ack_deadline: number | undefined = $state()
|
||||
let optionTabSelected: 'settings' | 'error_handler' | 'retries' = $state('error_handler')
|
||||
let errorHandlerSelected: ErrorHandler = $state('slack')
|
||||
let error_handler_path: string | undefined = $state()
|
||||
let error_handler_args: Record<string, any> = $state({})
|
||||
let errorHandler = $state(handlerDraftFromStored('error', undefined))
|
||||
const storedErrorHandler = $derived(handlerDraftToStored(errorHandler))
|
||||
const error_handler_path = $derived(storedErrorHandler.path)
|
||||
const error_handler_args = $derived(storedErrorHandler.args)
|
||||
let retry: Retry | undefined = $state()
|
||||
let suspendedJobsModal = $state<TriggerSuspendedJobsModal | null>(null)
|
||||
let originalConfig = $state<Record<string, any> | undefined>(undefined)
|
||||
@@ -247,12 +248,15 @@
|
||||
edit = false
|
||||
dirtyPath = false
|
||||
mode = defaultValues?.mode ?? 'enabled'
|
||||
error_handler_path = defaultValues?.error_handler_path ?? undefined
|
||||
error_handler_args = defaultValues?.error_handler_args ?? {}
|
||||
errorHandler = handlerDraftFromStored(
|
||||
'error',
|
||||
defaultValues?.error_handler_path,
|
||||
'script',
|
||||
defaultValues?.error_handler_args
|
||||
)
|
||||
retry = defaultValues?.retry ?? undefined
|
||||
auto_acknowledge_msg = defaultValues?.auto_acknowledge_msg ?? true
|
||||
ack_deadline = defaultValues?.ack_deadline
|
||||
errorHandlerSelected = getHandlerType(error_handler_path ?? '')
|
||||
permissionedAs = undefined
|
||||
selectedPermissionedAs = undefined
|
||||
preservePermissionedAs = false
|
||||
@@ -309,12 +313,15 @@
|
||||
topic_id = cfg?.topic_id ?? ''
|
||||
permsPath = cfg?.path
|
||||
permsForWrite = cfg?.extra_perms
|
||||
error_handler_path = cfg?.error_handler_path
|
||||
error_handler_args = cfg?.error_handler_args ?? {}
|
||||
errorHandler = handlerDraftFromStored(
|
||||
'error',
|
||||
cfg?.error_handler_path,
|
||||
'script',
|
||||
cfg?.error_handler_args
|
||||
)
|
||||
retry = cfg?.retry
|
||||
auto_acknowledge_msg = cfg?.auto_acknowledge_msg ?? true
|
||||
ack_deadline = cfg?.ack_deadline
|
||||
errorHandlerSelected = getHandlerType(error_handler_path ?? '')
|
||||
permissionedAs = cfg?.permissioned_as
|
||||
selectedPermissionedAs = cfg?.permissioned_as
|
||||
preservePermissionedAs = !!cfg?.permissioned_as
|
||||
@@ -701,9 +708,7 @@
|
||||
{optionTabSelected}
|
||||
{itemKind}
|
||||
{can_write}
|
||||
bind:errorHandlerSelected
|
||||
bind:error_handler_path
|
||||
bind:error_handler_args
|
||||
bind:errorHandler
|
||||
bind:retry
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
SettingService,
|
||||
VariableService,
|
||||
type AuthenticationMethod,
|
||||
type ErrorHandler,
|
||||
type HttpTrigger,
|
||||
type NewHttpTrigger,
|
||||
type Retry,
|
||||
@@ -57,7 +56,8 @@
|
||||
import TestingBadge from '../testingBadge.svelte'
|
||||
import TriggerEditorToolbar from '../TriggerEditorToolbar.svelte'
|
||||
import PermissionedAsLine from '../PermissionedAsLine.svelte'
|
||||
import { getHandlerType, handleConfigChange } from '../utils'
|
||||
import { handleConfigChange } from '../utils'
|
||||
import { handlerDraftFromStored, handlerDraftToStored } from '../../handlerDraft.svelte'
|
||||
import autosize from '$lib/autosize'
|
||||
import { untrack } from 'svelte'
|
||||
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
|
||||
@@ -166,8 +166,10 @@
|
||||
let extraPerms = $state<Record<string, boolean> | undefined>(undefined)
|
||||
let summary: string | undefined = $state()
|
||||
let routeDescription: string | undefined = $state()
|
||||
let error_handler_path: string | undefined = $state()
|
||||
let error_handler_args: Record<string, any> = $state({})
|
||||
let errorHandler = $state(handlerDraftFromStored('error', undefined))
|
||||
const storedErrorHandler = $derived(handlerDraftToStored(errorHandler))
|
||||
const error_handler_path = $derived(storedErrorHandler.path)
|
||||
const error_handler_args = $derived(storedErrorHandler.args)
|
||||
let retry: Retry | undefined = $state()
|
||||
// Component references
|
||||
let s3FilePicker = $state<S3FilePicker | undefined>(undefined)
|
||||
@@ -181,7 +183,6 @@
|
||||
let selectedPermissionedAs = $state<string | undefined>(undefined)
|
||||
let preservePermissionedAs = $state(false)
|
||||
let optionTabSelected: 'request_options' | 'error_handler' | 'retries' = $state('request_options')
|
||||
let errorHandlerSelected: ErrorHandler = $state('slack')
|
||||
|
||||
let suspendedJobsModal = $state<TriggerSuspendedJobsModal | null>(null)
|
||||
let originalConfig = $state<NewHttpTrigger | undefined>(undefined)
|
||||
@@ -349,10 +350,13 @@
|
||||
allowed_origins = defaultValues?.allowed_origins ?? undefined
|
||||
summary = defaultValues?.summary ?? ''
|
||||
routeDescription = defaultValues?.description ?? ''
|
||||
error_handler_path = defaultValues?.error_handler_path ?? undefined
|
||||
error_handler_args = defaultValues?.error_handler_args ?? {}
|
||||
errorHandler = handlerDraftFromStored(
|
||||
'error',
|
||||
defaultValues?.error_handler_path,
|
||||
'script',
|
||||
defaultValues?.error_handler_args
|
||||
)
|
||||
retry = defaultValues?.retry ?? undefined
|
||||
errorHandlerSelected = getHandlerType(error_handler_path ?? '')
|
||||
permissionedAs = undefined
|
||||
selectedPermissionedAs = undefined
|
||||
preservePermissionedAs = false
|
||||
@@ -397,10 +401,13 @@
|
||||
extraPerms = cfg?.extra_perms ?? undefined
|
||||
permsPath = path
|
||||
permsForWrite = cfg?.extra_perms ?? {}
|
||||
error_handler_path = cfg?.error_handler_path
|
||||
error_handler_args = cfg?.error_handler_args ?? {}
|
||||
errorHandler = handlerDraftFromStored(
|
||||
'error',
|
||||
cfg?.error_handler_path,
|
||||
'script',
|
||||
cfg?.error_handler_args
|
||||
)
|
||||
retry = cfg?.retry
|
||||
errorHandlerSelected = getHandlerType(error_handler_path ?? '')
|
||||
permissionedAs = cfg?.permissioned_as
|
||||
selectedPermissionedAs = cfg?.permissioned_as
|
||||
preservePermissionedAs = !!cfg?.permissioned_as
|
||||
@@ -1036,9 +1043,7 @@
|
||||
{optionTabSelected}
|
||||
{itemKind}
|
||||
{can_write}
|
||||
bind:errorHandlerSelected
|
||||
bind:error_handler_path
|
||||
bind:error_handler_args
|
||||
bind:errorHandler
|
||||
bind:retry
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
|
||||
import Path from '$lib/components/Path.svelte'
|
||||
import TriggerRunnablePicker from '$lib/components/triggers/TriggerRunnablePicker.svelte'
|
||||
import { KafkaTriggerService, type ErrorHandler, type Retry, type TriggerMode } from '$lib/gen'
|
||||
import { KafkaTriggerService, type Retry, type TriggerMode } from '$lib/gen'
|
||||
import { usedTriggerKinds } from '$lib/stores'
|
||||
import { capitalize, emptyString, sendUserToast } from '$lib/utils'
|
||||
import { useEditRights } from '$lib/operatorWriteRights'
|
||||
@@ -23,7 +23,8 @@
|
||||
import TriggerEditorToolbar from '../TriggerEditorToolbar.svelte'
|
||||
import PermissionedAsLine from '../PermissionedAsLine.svelte'
|
||||
import { saveKafkaTriggerFromCfg } from './utils'
|
||||
import { getHandlerType, handleConfigChange, type Trigger } from '../utils'
|
||||
import { handleConfigChange, type Trigger } from '../utils'
|
||||
import { handlerDraftFromStored, handlerDraftToStored } from '../../handlerDraft.svelte'
|
||||
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
|
||||
import Tab from '$lib/components/common/tabs/Tab.svelte'
|
||||
import TriggerRetriesAndErrorHandler from '../TriggerRetriesAndErrorHandler.svelte'
|
||||
@@ -127,9 +128,10 @@
|
||||
let preservePermissionedAs = $state(false)
|
||||
let resetLoading = $state(false)
|
||||
let optionTabSelected: 'error_handler' | 'retries' = $state('error_handler')
|
||||
let errorHandlerSelected: ErrorHandler = $state('slack')
|
||||
let error_handler_path: string | undefined = $state()
|
||||
let error_handler_args: Record<string, any> = $state({})
|
||||
let errorHandler = $state(handlerDraftFromStored('error', undefined))
|
||||
const storedErrorHandler = $derived(handlerDraftToStored(errorHandler))
|
||||
const error_handler_path = $derived(storedErrorHandler.path)
|
||||
const error_handler_args = $derived(storedErrorHandler.args)
|
||||
let retry: Retry | undefined = $state()
|
||||
let filters: FilterNode[] = $state([])
|
||||
let filterLogic = $state<'and' | 'or'>('and')
|
||||
@@ -254,12 +256,15 @@
|
||||
path = nDefaultValues?.path ?? ''
|
||||
initialPath = ''
|
||||
dirtyPath = false
|
||||
error_handler_path = nDefaultValues?.error_handler_path ?? undefined
|
||||
error_handler_args = nDefaultValues?.error_handler_args ?? {}
|
||||
errorHandler = handlerDraftFromStored(
|
||||
'error',
|
||||
nDefaultValues?.error_handler_path,
|
||||
'script',
|
||||
nDefaultValues?.error_handler_args
|
||||
)
|
||||
retry = nDefaultValues?.retry ?? undefined
|
||||
filters = nDefaultValues?.filters ?? []
|
||||
filterLogic = 'and'
|
||||
errorHandlerSelected = getHandlerType(error_handler_path ?? '')
|
||||
mode = nDefaultValues?.mode ?? 'enabled'
|
||||
permissionedAs = undefined
|
||||
selectedPermissionedAs = undefined
|
||||
@@ -291,12 +296,15 @@
|
||||
extra_perms = cfg?.extra_perms
|
||||
permsPath = path
|
||||
permsForWrite = cfg?.extra_perms
|
||||
error_handler_path = cfg?.error_handler_path
|
||||
error_handler_args = cfg?.error_handler_args ?? {}
|
||||
errorHandler = handlerDraftFromStored(
|
||||
'error',
|
||||
cfg?.error_handler_path,
|
||||
'script',
|
||||
cfg?.error_handler_args
|
||||
)
|
||||
retry = cfg?.retry
|
||||
filters = cfg?.filters ?? []
|
||||
filterLogic = cfg?.filter_logic ?? 'and'
|
||||
errorHandlerSelected = getHandlerType(error_handler_path ?? '')
|
||||
permissionedAs = cfg?.permissioned_as
|
||||
selectedPermissionedAs = cfg?.permissioned_as
|
||||
preservePermissionedAs = !!cfg?.permissioned_as
|
||||
@@ -719,9 +727,7 @@
|
||||
{optionTabSelected}
|
||||
{itemKind}
|
||||
{can_write}
|
||||
bind:errorHandlerSelected
|
||||
bind:error_handler_path
|
||||
bind:error_handler_args
|
||||
bind:errorHandler
|
||||
bind:retry
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
type MqttV5Config,
|
||||
type MqttSubscribeTopic,
|
||||
type Retry,
|
||||
type ErrorHandler,
|
||||
type TriggerMode
|
||||
} from '$lib/gen'
|
||||
import MqttEditorConfigSection from './MqttEditorConfigSection.svelte'
|
||||
@@ -32,7 +31,8 @@
|
||||
import TriggerEditorToolbar from '../TriggerEditorToolbar.svelte'
|
||||
import PermissionedAsLine from '../PermissionedAsLine.svelte'
|
||||
import { saveMqttTriggerFromCfg } from './utils'
|
||||
import { getHandlerType, handleConfigChange, type Trigger } from '../utils'
|
||||
import { handleConfigChange, type Trigger } from '../utils'
|
||||
import { handlerDraftFromStored, handlerDraftToStored } from '../../handlerDraft.svelte'
|
||||
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
|
||||
import Tab from '$lib/components/common/tabs/Tab.svelte'
|
||||
import TriggerRetriesAndErrorHandler from '../TriggerRetriesAndErrorHandler.svelte'
|
||||
@@ -135,9 +135,10 @@
|
||||
let permissionedAs = $state<string | undefined>(undefined)
|
||||
let selectedPermissionedAs = $state<string | undefined>(undefined)
|
||||
let preservePermissionedAs = $state(false)
|
||||
let errorHandlerSelected: ErrorHandler = $state('slack')
|
||||
let error_handler_path: string | undefined = $state()
|
||||
let error_handler_args: Record<string, any> = $state({})
|
||||
let errorHandler = $state(handlerDraftFromStored('error', undefined))
|
||||
const storedErrorHandler = $derived(handlerDraftToStored(errorHandler))
|
||||
const error_handler_path = $derived(storedErrorHandler.path)
|
||||
const error_handler_args = $derived(storedErrorHandler.args)
|
||||
let retry: Retry | undefined = $state()
|
||||
let suspendedJobsModal = $state<TriggerSuspendedJobsModal | null>(null)
|
||||
let originalConfig = $state<Record<string, any> | undefined>(undefined)
|
||||
@@ -247,10 +248,13 @@
|
||||
client_version = defaultValues?.client_version ?? 'v5'
|
||||
client_id = defaultValues?.client_id ?? ''
|
||||
mode = defaultValues?.mode ?? 'enabled'
|
||||
error_handler_path = defaultValues?.error_handler_path ?? undefined
|
||||
error_handler_args = defaultValues?.error_handler_args ?? {}
|
||||
errorHandler = handlerDraftFromStored(
|
||||
'error',
|
||||
defaultValues?.error_handler_path,
|
||||
'script',
|
||||
defaultValues?.error_handler_args
|
||||
)
|
||||
retry = defaultValues?.retry ?? undefined
|
||||
errorHandlerSelected = getHandlerType(error_handler_path ?? '')
|
||||
v3_config = defaultValues?.v3_config ?? DEFAULT_V3_CONFIG
|
||||
v5_config = defaultValues?.v5_config ?? DEFAULT_V5_CONFIG
|
||||
activateV5Options.topic_alias_maximum = Boolean(defaultValues?.v5_config?.topic_alias_maximum)
|
||||
@@ -286,10 +290,13 @@
|
||||
client_id = cfg?.client_id ?? ''
|
||||
permsPath = cfg?.path
|
||||
permsForWrite = cfg?.extra_perms
|
||||
error_handler_path = cfg?.error_handler_path
|
||||
error_handler_args = cfg?.error_handler_args ?? {}
|
||||
errorHandler = handlerDraftFromStored(
|
||||
'error',
|
||||
cfg?.error_handler_path,
|
||||
'script',
|
||||
cfg?.error_handler_args
|
||||
)
|
||||
retry = cfg?.retry
|
||||
errorHandlerSelected = getHandlerType(error_handler_path ?? '')
|
||||
activateV5Options.topic_alias_maximum = Boolean(v5_config.topic_alias_maximum)
|
||||
activateV5Options.session_expiry_interval = Boolean(v5_config.session_expiry_interval)
|
||||
permissionedAs = cfg?.permissioned_as
|
||||
@@ -750,9 +757,7 @@
|
||||
{optionTabSelected}
|
||||
{itemKind}
|
||||
{can_write}
|
||||
bind:errorHandlerSelected
|
||||
bind:error_handler_path
|
||||
bind:error_handler_args
|
||||
bind:errorHandler
|
||||
bind:retry
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
|
||||
import Path from '$lib/components/Path.svelte'
|
||||
import TriggerRunnablePicker from '$lib/components/triggers/TriggerRunnablePicker.svelte'
|
||||
import { NatsTriggerService, type ErrorHandler, type Retry, type TriggerMode } from '$lib/gen'
|
||||
import { NatsTriggerService, type Retry, type TriggerMode } from '$lib/gen'
|
||||
import { usedTriggerKinds } from '$lib/stores'
|
||||
import { capitalize, emptyString, sendUserToast } from '$lib/utils'
|
||||
import { useEditRights } from '$lib/operatorWriteRights'
|
||||
@@ -22,7 +22,8 @@
|
||||
import TriggerEditorToolbar from '../TriggerEditorToolbar.svelte'
|
||||
import PermissionedAsLine from '../PermissionedAsLine.svelte'
|
||||
import { saveNatsTriggerFromCfg } from './utils'
|
||||
import { getHandlerType, handleConfigChange, type Trigger } from '../utils'
|
||||
import { handleConfigChange, type Trigger } from '../utils'
|
||||
import { handlerDraftFromStored, handlerDraftToStored } from '../../handlerDraft.svelte'
|
||||
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
|
||||
import Tab from '$lib/components/common/tabs/Tab.svelte'
|
||||
import TriggerRetriesAndErrorHandler from '../TriggerRetriesAndErrorHandler.svelte'
|
||||
@@ -128,9 +129,10 @@
|
||||
let preservePermissionedAs = $state(false)
|
||||
let isValid = $state(false)
|
||||
let optionTabSelected: 'error_handler' | 'retries' = $state('error_handler')
|
||||
let errorHandlerSelected: ErrorHandler = $state('slack')
|
||||
let error_handler_path: string | undefined = $state()
|
||||
let error_handler_args: Record<string, any> = $state({})
|
||||
let errorHandler = $state(handlerDraftFromStored('error', undefined))
|
||||
const storedErrorHandler = $derived(handlerDraftToStored(errorHandler))
|
||||
const error_handler_path = $derived(storedErrorHandler.path)
|
||||
const error_handler_args = $derived(storedErrorHandler.args)
|
||||
let retry: Retry | undefined = $state()
|
||||
let mode = $state<TriggerMode>('enabled')
|
||||
|
||||
@@ -246,10 +248,13 @@
|
||||
dirtyPath = false
|
||||
defaultValues = nDefaultValues
|
||||
mode = nDefaultValues?.mode ?? 'enabled'
|
||||
error_handler_path = nDefaultValues?.error_handler_path ?? undefined
|
||||
error_handler_args = nDefaultValues?.error_handler_args ?? {}
|
||||
errorHandler = handlerDraftFromStored(
|
||||
'error',
|
||||
nDefaultValues?.error_handler_path,
|
||||
'script',
|
||||
nDefaultValues?.error_handler_args
|
||||
)
|
||||
retry = nDefaultValues?.retry ?? undefined
|
||||
errorHandlerSelected = getHandlerType(error_handler_path ?? '')
|
||||
permissionedAs = undefined
|
||||
selectedPermissionedAs = undefined
|
||||
preservePermissionedAs = false
|
||||
@@ -280,10 +285,13 @@
|
||||
mode = cfg?.mode ?? 'enabled'
|
||||
permsPath = cfg?.path
|
||||
permsForWrite = cfg?.extra_perms
|
||||
error_handler_path = cfg?.error_handler_path
|
||||
error_handler_args = cfg?.error_handler_args ?? {}
|
||||
errorHandler = handlerDraftFromStored(
|
||||
'error',
|
||||
cfg?.error_handler_path,
|
||||
'script',
|
||||
cfg?.error_handler_args
|
||||
)
|
||||
retry = cfg?.retry
|
||||
errorHandlerSelected = getHandlerType(error_handler_path ?? '')
|
||||
permissionedAs = cfg?.permissioned_as
|
||||
selectedPermissionedAs = cfg?.permissioned_as
|
||||
preservePermissionedAs = !!cfg?.permissioned_as
|
||||
@@ -619,9 +627,7 @@
|
||||
{optionTabSelected}
|
||||
{itemKind}
|
||||
{can_write}
|
||||
bind:errorHandlerSelected
|
||||
bind:error_handler_path
|
||||
bind:error_handler_args
|
||||
bind:errorHandler
|
||||
bind:retry
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
import TriggerRunnablePicker from '$lib/components/triggers/TriggerRunnablePicker.svelte'
|
||||
import {
|
||||
PostgresTriggerService,
|
||||
type ErrorHandler,
|
||||
type Language,
|
||||
type Relations,
|
||||
type Retry,
|
||||
@@ -41,7 +40,8 @@
|
||||
import TriggerEditorToolbar from '../TriggerEditorToolbar.svelte'
|
||||
import PermissionedAsLine from '../PermissionedAsLine.svelte'
|
||||
import TestingBadge from '../testingBadge.svelte'
|
||||
import { getHandlerType, handleConfigChange, type Trigger } from '../utils'
|
||||
import { handleConfigChange, type Trigger } from '../utils'
|
||||
import { handlerDraftFromStored, handlerDraftToStored } from '../../handlerDraft.svelte'
|
||||
import TriggerRetriesAndErrorHandler from '../TriggerRetriesAndErrorHandler.svelte'
|
||||
import TriggerAdvancedBadges from '../TriggerAdvancedBadges.svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
@@ -154,9 +154,10 @@
|
||||
let creatingPublication: boolean = $state(false)
|
||||
let pg14: boolean = $derived(postgresVersion.startsWith('14'))
|
||||
let optionTabSelected: 'error_handler' | 'retries' = $state('error_handler')
|
||||
let errorHandlerSelected: ErrorHandler = $state('slack')
|
||||
let error_handler_path: string | undefined = $state()
|
||||
let error_handler_args: Record<string, any> = $state({})
|
||||
let errorHandler = $state(handlerDraftFromStored('error', undefined))
|
||||
const storedErrorHandler = $derived(handlerDraftToStored(errorHandler))
|
||||
const error_handler_path = $derived(storedErrorHandler.path)
|
||||
const error_handler_args = $derived(storedErrorHandler.args)
|
||||
let retry: Retry | undefined = $state()
|
||||
let mode = $state<TriggerMode>('enabled')
|
||||
|
||||
@@ -355,10 +356,13 @@
|
||||
table_to_track: []
|
||||
}
|
||||
]
|
||||
error_handler_path = defaultValues?.error_handler_path ?? undefined
|
||||
error_handler_args = defaultValues?.error_handler_args ?? {}
|
||||
errorHandler = handlerDraftFromStored(
|
||||
'error',
|
||||
defaultValues?.error_handler_path,
|
||||
'script',
|
||||
defaultValues?.error_handler_args
|
||||
)
|
||||
retry = defaultValues?.retry ?? undefined
|
||||
errorHandlerSelected = getHandlerType(error_handler_path ?? '')
|
||||
mode = defaultValues?.mode ?? 'enabled'
|
||||
permissionedAs = undefined
|
||||
selectedPermissionedAs = undefined
|
||||
@@ -411,10 +415,13 @@
|
||||
permsForWrite = cfg?.extra_perms
|
||||
transaction_to_track = [...cfg?.publication?.transaction_to_track]
|
||||
relations = cfg?.publication?.table_to_track ?? []
|
||||
error_handler_path = cfg?.error_handler_path
|
||||
error_handler_args = cfg?.error_handler_args ?? {}
|
||||
errorHandler = handlerDraftFromStored(
|
||||
'error',
|
||||
cfg?.error_handler_path,
|
||||
'script',
|
||||
cfg?.error_handler_args
|
||||
)
|
||||
retry = cfg?.retry
|
||||
errorHandlerSelected = getHandlerType(error_handler_path ?? '')
|
||||
mode = cfg?.mode ?? 'enabled'
|
||||
permissionedAs = cfg?.permissioned_as
|
||||
selectedPermissionedAs = cfg?.permissioned_as
|
||||
@@ -1028,9 +1035,7 @@
|
||||
{optionTabSelected}
|
||||
{itemKind}
|
||||
{can_write}
|
||||
bind:errorHandlerSelected
|
||||
bind:error_handler_path
|
||||
bind:error_handler_args
|
||||
bind:errorHandler
|
||||
bind:retry
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,89 +1,58 @@
|
||||
<script lang="ts" module>
|
||||
import { handlerFullPath } from '$lib/components/ErrorOrRecoveryHandler.svelte'
|
||||
import { SettingService, type ErrorHandler, type Retry } from '$lib/gen'
|
||||
import { SettingService, type Retry } from '$lib/gen'
|
||||
import {
|
||||
handlerDraftFromStored,
|
||||
handlerDraftToStored,
|
||||
type HandlerDraft,
|
||||
type HandlerMode
|
||||
} from '$lib/components/handlerDraft.svelte'
|
||||
|
||||
export type ScheduleAdvancedState = {
|
||||
errorHandlerSelected: ErrorHandler
|
||||
errorHandlerPath: string | undefined
|
||||
errorHandleritemKind: 'flow' | 'script'
|
||||
errorHandlerExtraArgs: Record<string, any>
|
||||
errorHandler: HandlerDraft
|
||||
wsErrorHandlerMuted: boolean
|
||||
failedTimes: number
|
||||
failedExact: boolean
|
||||
recoveryHandlerSelected: ErrorHandler
|
||||
recoveryHandlerPath: string | undefined
|
||||
recoveryHandlerItemKind: 'flow' | 'script'
|
||||
recoveryHandlerExtraArgs: Record<string, any>
|
||||
recoveryHandler: HandlerDraft
|
||||
recoveredTimes: number
|
||||
successHandlerSelected: ErrorHandler
|
||||
successHandlerPath: string | undefined
|
||||
successHandlerItemKind: 'flow' | 'script'
|
||||
successHandlerExtraArgs: Record<string, any>
|
||||
successHandler: HandlerDraft
|
||||
retry: Retry | undefined
|
||||
dynamicSkipPath: string | undefined
|
||||
tag: string | undefined
|
||||
}
|
||||
|
||||
/** A schedule handler stored as `<script|flow>/<path>`, or none (shown on the Slack tab). */
|
||||
export function scheduleHandlerDraft(
|
||||
mode: HandlerMode,
|
||||
fullPath?: string | null,
|
||||
args?: Record<string, any> | null
|
||||
): HandlerDraft {
|
||||
const [kind, ...path] = (fullPath ?? '').split('/')
|
||||
return handlerDraftFromStored(mode, path.join('/'), kind === 'flow' ? 'flow' : 'script', args, {
|
||||
emptySelected: 'slack'
|
||||
})
|
||||
}
|
||||
|
||||
/** The `<script|flow>/<path>` a schedule stores, with its extra args. */
|
||||
export function scheduleHandlerStored(draft: HandlerDraft) {
|
||||
const { path, kind, args } = handlerDraftToStored(draft)
|
||||
return { path: path ? `${kind}/${path}` : undefined, args }
|
||||
}
|
||||
|
||||
export function emptyScheduleAdvanced(): ScheduleAdvancedState {
|
||||
return {
|
||||
errorHandlerSelected: 'slack',
|
||||
errorHandlerPath: undefined,
|
||||
errorHandleritemKind: 'script',
|
||||
errorHandlerExtraArgs: {},
|
||||
errorHandler: scheduleHandlerDraft('error'),
|
||||
wsErrorHandlerMuted: false,
|
||||
failedTimes: 1,
|
||||
failedExact: false,
|
||||
recoveryHandlerSelected: 'slack',
|
||||
recoveryHandlerPath: undefined,
|
||||
recoveryHandlerItemKind: 'script',
|
||||
recoveryHandlerExtraArgs: {},
|
||||
recoveryHandler: scheduleHandlerDraft('recovery'),
|
||||
recoveredTimes: 1,
|
||||
successHandlerSelected: 'slack',
|
||||
successHandlerPath: undefined,
|
||||
successHandlerItemKind: 'script',
|
||||
successHandlerExtraArgs: {},
|
||||
successHandler: scheduleHandlerDraft('success'),
|
||||
retry: undefined,
|
||||
dynamicSkipPath: undefined,
|
||||
tag: undefined
|
||||
}
|
||||
}
|
||||
|
||||
/** Which built-in handler a hub path is, or `custom`. */
|
||||
export function getHandlerType(
|
||||
isHandler: 'error' | 'recovery' | 'success',
|
||||
scriptPath: string
|
||||
): ErrorHandler {
|
||||
const handlerMap = {
|
||||
error: {
|
||||
teams: '/workspace-or-schedule-error-handler-teams',
|
||||
slack: '/workspace-or-schedule-error-handler-slack',
|
||||
email: '/workspace-or-error-handler-email'
|
||||
},
|
||||
recovery: {
|
||||
teams: '/schedule-recovery-handler-teams',
|
||||
slack: '/schedule-recovery-handler-slack',
|
||||
email: '/workspace-or-error-handler-email'
|
||||
},
|
||||
success: {
|
||||
teams: '/schedule-success-handler-teams',
|
||||
slack: '/schedule-success-handler-slack',
|
||||
email: '/workspace-or-error-handler-email'
|
||||
}
|
||||
}
|
||||
|
||||
for (const [type, suffix] of Object.entries(handlerMap[isHandler])) {
|
||||
if (scriptPath.startsWith('hub/') && scriptPath.endsWith(suffix)) {
|
||||
return type as ErrorHandler
|
||||
}
|
||||
}
|
||||
return 'custom'
|
||||
}
|
||||
|
||||
const splitHandler = (path: string) => {
|
||||
const [kind, ...rest] = path.split('/')
|
||||
return { kind: kind as 'flow' | 'script', path: rest.join('/') }
|
||||
}
|
||||
|
||||
/** The Advanced tabs of a new schedule: the workspace's default error, recovery
|
||||
* and success handlers, where it sets them. */
|
||||
export async function loadDefaultScheduleAdvanced(wsId: string): Promise<ScheduleAdvancedState> {
|
||||
@@ -94,55 +63,48 @@
|
||||
)
|
||||
)) as any[]
|
||||
if (err) {
|
||||
const h = splitHandler(err.errorHandlerPath)
|
||||
a.wsErrorHandlerMuted = err.wsErrorHandlerMuted
|
||||
a.errorHandleritemKind = h.kind
|
||||
a.errorHandlerPath = h.path
|
||||
a.errorHandlerExtraArgs = err.errorHandlerExtraArgs
|
||||
a.errorHandlerSelected = getHandlerType('error', h.path)
|
||||
a.errorHandler = scheduleHandlerDraft(
|
||||
'error',
|
||||
err.errorHandlerPath,
|
||||
err.errorHandlerExtraArgs
|
||||
)
|
||||
a.failedTimes = err.failedTimes
|
||||
a.failedExact = err.failedExact
|
||||
}
|
||||
if (rec) {
|
||||
const h = splitHandler(rec.recoveryHandlerPath)
|
||||
a.recoveryHandlerItemKind = h.kind
|
||||
a.recoveryHandlerPath = h.path
|
||||
a.recoveryHandlerExtraArgs = rec.recoveryHandlerExtraArgs
|
||||
a.recoveryHandlerSelected = getHandlerType('recovery', h.path)
|
||||
a.recoveryHandler = scheduleHandlerDraft(
|
||||
'recovery',
|
||||
rec.recoveryHandlerPath,
|
||||
rec.recoveryHandlerExtraArgs
|
||||
)
|
||||
a.recoveredTimes = rec.recoveredTimes
|
||||
}
|
||||
if (suc) {
|
||||
const h = splitHandler(suc.successHandlerPath)
|
||||
a.successHandlerItemKind = h.kind
|
||||
a.successHandlerPath = h.path
|
||||
a.successHandlerExtraArgs = suc.successHandlerExtraArgs
|
||||
a.successHandlerSelected = getHandlerType('success', h.path)
|
||||
a.successHandler = scheduleHandlerDraft(
|
||||
'success',
|
||||
suc.successHandlerPath,
|
||||
suc.successHandlerExtraArgs
|
||||
)
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
/** The schedule fields the Advanced tabs own, as the schedule API takes them. */
|
||||
export function scheduleAdvancedCfg(a: ScheduleAdvancedState) {
|
||||
const error = scheduleHandlerStored(a.errorHandler)
|
||||
const recovery = scheduleHandlerStored(a.recoveryHandler)
|
||||
const success = scheduleHandlerStored(a.successHandler)
|
||||
return {
|
||||
on_failure: a.errorHandlerPath
|
||||
? handlerFullPath(a.errorHandlerSelected, a.errorHandleritemKind, a.errorHandlerPath)
|
||||
: undefined,
|
||||
on_failure: error.path,
|
||||
on_failure_times: a.failedTimes,
|
||||
on_failure_exact: a.failedExact,
|
||||
on_failure_extra_args: a.errorHandlerPath ? a.errorHandlerExtraArgs : undefined,
|
||||
on_recovery: a.recoveryHandlerPath
|
||||
? handlerFullPath(
|
||||
a.recoveryHandlerSelected,
|
||||
a.recoveryHandlerItemKind,
|
||||
a.recoveryHandlerPath
|
||||
)
|
||||
: undefined,
|
||||
on_failure_extra_args: error.path ? error.args : undefined,
|
||||
on_recovery: recovery.path,
|
||||
on_recovery_times: a.recoveredTimes,
|
||||
on_recovery_extra_args: a.recoveryHandlerPath ? a.recoveryHandlerExtraArgs : {},
|
||||
on_success: a.successHandlerPath
|
||||
? handlerFullPath(a.successHandlerSelected, a.successHandlerItemKind, a.successHandlerPath)
|
||||
: undefined,
|
||||
on_success_extra_args: a.successHandlerPath ? a.successHandlerExtraArgs : {},
|
||||
on_recovery_extra_args: recovery.args,
|
||||
on_success: success.path,
|
||||
on_success_extra_args: success.args,
|
||||
ws_error_handler_muted: a.wsErrorHandlerMuted,
|
||||
retry: a.retry,
|
||||
tag: a.tag,
|
||||
@@ -174,22 +136,13 @@
|
||||
itemKind: 'flow' | 'script'
|
||||
canWrite: boolean
|
||||
loading?: boolean
|
||||
errorHandlerSelected: ErrorHandler
|
||||
errorHandlerPath: string | undefined
|
||||
errorHandleritemKind: 'flow' | 'script'
|
||||
errorHandlerExtraArgs: Record<string, any>
|
||||
errorHandler: HandlerDraft
|
||||
wsErrorHandlerMuted: boolean
|
||||
failedTimes: number
|
||||
failedExact: boolean
|
||||
recoveryHandlerSelected: ErrorHandler
|
||||
recoveryHandlerPath: string | undefined
|
||||
recoveryHandlerItemKind: 'flow' | 'script'
|
||||
recoveryHandlerExtraArgs: Record<string, any>
|
||||
recoveryHandler: HandlerDraft
|
||||
recoveredTimes: number
|
||||
successHandlerSelected: ErrorHandler
|
||||
successHandlerPath: string | undefined
|
||||
successHandlerItemKind: 'flow' | 'script'
|
||||
successHandlerExtraArgs: Record<string, any>
|
||||
successHandler: HandlerDraft
|
||||
retry: Retry | undefined
|
||||
dynamicSkipPath: string | undefined
|
||||
tag: string | undefined
|
||||
@@ -200,22 +153,13 @@
|
||||
itemKind,
|
||||
canWrite,
|
||||
loading = false,
|
||||
errorHandlerSelected = $bindable(),
|
||||
errorHandlerPath = $bindable(),
|
||||
errorHandleritemKind = $bindable(),
|
||||
errorHandlerExtraArgs = $bindable(),
|
||||
errorHandler = $bindable(),
|
||||
wsErrorHandlerMuted = $bindable(),
|
||||
failedTimes = $bindable(),
|
||||
failedExact = $bindable(),
|
||||
recoveryHandlerSelected = $bindable(),
|
||||
recoveryHandlerPath = $bindable(),
|
||||
recoveryHandlerItemKind = $bindable(),
|
||||
recoveryHandlerExtraArgs = $bindable(),
|
||||
recoveryHandler = $bindable(),
|
||||
recoveredTimes = $bindable(),
|
||||
successHandlerSelected = $bindable(),
|
||||
successHandlerPath = $bindable(),
|
||||
successHandlerItemKind = $bindable(),
|
||||
successHandlerExtraArgs = $bindable(),
|
||||
successHandler = $bindable(),
|
||||
retry = $bindable(),
|
||||
dynamicSkipPath = $bindable(),
|
||||
tag = $bindable()
|
||||
@@ -233,29 +177,30 @@
|
||||
// in the workspace this schedule lives in.
|
||||
const wsParam = $derived(wsId ? `&workspace=${encodeURIComponent(wsId)}` : '')
|
||||
|
||||
const errorHandlerPath = $derived(scheduleHandlerStored(errorHandler).path)
|
||||
const recoveryHandlerPath = $derived(scheduleHandlerStored(recoveryHandler).path)
|
||||
|
||||
async function saveAsDefaultErrorHandler(overrideExisting: boolean) {
|
||||
if (!$enterpriseLicense) {
|
||||
sendUserToast(`Setting default error handler is an enterprise edition feature`, true)
|
||||
return
|
||||
}
|
||||
if (wsId) {
|
||||
const handler = scheduleHandlerStored(errorHandler)
|
||||
await ScheduleService.setDefaultErrorOrRecoveryHandler({
|
||||
workspace: wsId!,
|
||||
requestBody: {
|
||||
handler_type: 'error',
|
||||
override_existing: overrideExisting,
|
||||
path:
|
||||
errorHandlerPath == undefined
|
||||
? undefined
|
||||
: handlerFullPath(errorHandlerSelected, errorHandleritemKind, errorHandlerPath),
|
||||
extra_args: errorHandlerExtraArgs,
|
||||
path: handler.path,
|
||||
extra_args: handler.args,
|
||||
number_of_occurence: failedTimes,
|
||||
number_of_occurence_exact: failedExact,
|
||||
workspace_handler_muted: wsErrorHandlerMuted
|
||||
}
|
||||
})
|
||||
if (errorHandlerPath !== undefined) {
|
||||
sendUserToast(`Default error handler saved to ${errorHandlerPath}`, false)
|
||||
if (handler.path !== undefined) {
|
||||
sendUserToast(`Default error handler saved to ${handler.path}`, false)
|
||||
} else {
|
||||
sendUserToast(`Default error handler reset`, false)
|
||||
}
|
||||
@@ -268,25 +213,19 @@
|
||||
return
|
||||
}
|
||||
if (wsId) {
|
||||
const handler = scheduleHandlerStored(recoveryHandler)
|
||||
await ScheduleService.setDefaultErrorOrRecoveryHandler({
|
||||
workspace: wsId!,
|
||||
requestBody: {
|
||||
handler_type: 'recovery',
|
||||
override_existing: overrideExisting,
|
||||
path:
|
||||
recoveryHandlerPath === undefined
|
||||
? undefined
|
||||
: handlerFullPath(
|
||||
recoveryHandlerSelected,
|
||||
recoveryHandlerItemKind,
|
||||
recoveryHandlerPath
|
||||
),
|
||||
extra_args: recoveryHandlerExtraArgs,
|
||||
path: handler.path,
|
||||
extra_args: handler.args,
|
||||
number_of_occurence: recoveredTimes
|
||||
}
|
||||
})
|
||||
if (recoveryHandlerPath !== undefined) {
|
||||
sendUserToast(`Default recovery handler saved to ${recoveryHandlerPath}`, false)
|
||||
if (handler.path !== undefined) {
|
||||
sendUserToast(`Default recovery handler saved to ${handler.path}`, false)
|
||||
} else {
|
||||
sendUserToast(`Default recovery handler reset`, false)
|
||||
}
|
||||
@@ -299,21 +238,19 @@
|
||||
return
|
||||
}
|
||||
if (wsId) {
|
||||
const handler = scheduleHandlerStored(successHandler)
|
||||
await ScheduleService.setDefaultErrorOrRecoveryHandler({
|
||||
workspace: wsId!,
|
||||
requestBody: {
|
||||
handler_type: 'success',
|
||||
override_existing: overrideExisting,
|
||||
path:
|
||||
successHandlerPath === undefined
|
||||
? undefined
|
||||
: handlerFullPath(successHandlerSelected, successHandlerItemKind, successHandlerPath),
|
||||
extra_args: successHandlerExtraArgs,
|
||||
path: handler.path,
|
||||
extra_args: handler.args,
|
||||
number_of_occurence: recoveredTimes
|
||||
}
|
||||
})
|
||||
if (successHandlerPath !== undefined) {
|
||||
sendUserToast(`Default success handler saved to ${successHandlerPath}`, false)
|
||||
if (handler.path !== undefined) {
|
||||
sendUserToast(`Default success handler saved to ${handler.path}`, false)
|
||||
} else {
|
||||
sendUserToast(`Default success handler reset`, false)
|
||||
}
|
||||
@@ -377,12 +314,9 @@
|
||||
isEditable={canWrite}
|
||||
errorOrRecovery="error"
|
||||
showScriptHelpText={true}
|
||||
bind:handlerSelected={errorHandlerSelected}
|
||||
bind:handlerPath={errorHandlerPath}
|
||||
bind:draft={errorHandler}
|
||||
toggleText="Alert channel on error"
|
||||
customScriptTemplate="/scripts/add?hub=hub%2F19743%2Fwindmill%2Fschedule_error_handler_template"
|
||||
bind:customHandlerKind={errorHandleritemKind}
|
||||
bind:handlerExtraArgs={errorHandlerExtraArgs}
|
||||
>
|
||||
{#snippet customTabTooltip()}
|
||||
<Tooltip>
|
||||
@@ -471,12 +405,9 @@
|
||||
workspace={wsId}
|
||||
isEditable={!disabled}
|
||||
errorOrRecovery="recovery"
|
||||
bind:handlerSelected={recoveryHandlerSelected}
|
||||
bind:handlerPath={recoveryHandlerPath}
|
||||
bind:draft={recoveryHandler}
|
||||
toggleText="Alert channel when error recovered"
|
||||
customScriptTemplate="/scripts/add?hub=hub%2F9082%2Fwindmill%2Fschedule_recovery_handler_template"
|
||||
bind:customHandlerKind={recoveryHandlerItemKind}
|
||||
bind:handlerExtraArgs={recoveryHandlerExtraArgs}
|
||||
>
|
||||
{#snippet customTabTooltip()}
|
||||
<Tooltip>
|
||||
@@ -560,12 +491,9 @@
|
||||
workspace={wsId}
|
||||
isEditable={!disabled}
|
||||
errorOrRecovery="success"
|
||||
bind:handlerSelected={successHandlerSelected}
|
||||
bind:handlerPath={successHandlerPath}
|
||||
bind:draft={successHandler}
|
||||
toggleText="Alert channel when successful"
|
||||
customScriptTemplate="/scripts/add?hub=hub%2F9071%2Fwindmill%2Fschedule_success_handler_template"
|
||||
bind:customHandlerKind={successHandlerItemKind}
|
||||
bind:handlerExtraArgs={successHandlerExtraArgs}
|
||||
>
|
||||
{#snippet customTabTooltip()}
|
||||
<Tooltip>
|
||||
|
||||
@@ -8,10 +8,12 @@
|
||||
import TriggerAdvancedBadges from '../TriggerAdvancedBadges.svelte'
|
||||
import ScheduleAdvancedOptions, {
|
||||
emptyScheduleAdvanced,
|
||||
getHandlerType,
|
||||
loadDefaultScheduleAdvanced,
|
||||
scheduleAdvancedCfg
|
||||
scheduleAdvancedCfg,
|
||||
scheduleHandlerDraft,
|
||||
scheduleHandlerStored
|
||||
} from './ScheduleAdvancedOptions.svelte'
|
||||
import type { HandlerDraft } from '$lib/components/handlerDraft.svelte'
|
||||
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
|
||||
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
|
||||
import CronInput from '$lib/components/CronInput.svelte'
|
||||
@@ -30,8 +32,7 @@
|
||||
ScriptService,
|
||||
type Flow,
|
||||
type Retry,
|
||||
type Schedule,
|
||||
type ErrorHandler
|
||||
type Schedule
|
||||
} from '$lib/gen'
|
||||
import { emptyString, formatCron, sendUserToast, cronV1toV2 } from '$lib/utils'
|
||||
import { useEditRights } from '$lib/operatorWriteRights'
|
||||
@@ -81,19 +82,11 @@
|
||||
let paused_until: string | undefined = $state(undefined)
|
||||
let itemKind: 'flow' | 'script' = $state('script')
|
||||
let is_flow: boolean = $derived.by(() => itemKind === 'flow')
|
||||
let errorHandleritemKind: 'flow' | 'script' = $state('script')
|
||||
let wsErrorHandlerMuted: boolean = $state(false)
|
||||
let errorHandlerPath: string | undefined = $state(undefined)
|
||||
let errorHandlerSelected: ErrorHandler = $state('slack')
|
||||
let errorHandlerExtraArgs: Record<string, any> = $state({})
|
||||
let recoveryHandlerPath: string | undefined = $state(undefined)
|
||||
let recoveryHandlerSelected: ErrorHandler = $state('slack')
|
||||
let recoveryHandlerItemKind: 'flow' | 'script' = $state('script')
|
||||
let recoveryHandlerExtraArgs: Record<string, any> = $state({})
|
||||
let successHandlerPath: string | undefined = $state(undefined)
|
||||
let successHandlerSelected: ErrorHandler = $state('slack')
|
||||
let successHandlerItemKind: 'flow' | 'script' = $state('script')
|
||||
let successHandlerExtraArgs: Record<string, any> = $state({})
|
||||
let errorHandler: HandlerDraft = $state(scheduleHandlerDraft('error'))
|
||||
let recoveryHandler: HandlerDraft = $state(scheduleHandlerDraft('recovery'))
|
||||
let successHandler: HandlerDraft = $state(scheduleHandlerDraft('success'))
|
||||
const errorHandlerPath = $derived(scheduleHandlerStored(errorHandler).path)
|
||||
let failedTimes = $state(1)
|
||||
let failedExact = $state(false)
|
||||
let recoveredTimes = $state(1)
|
||||
@@ -155,9 +148,9 @@
|
||||
!allowSchedule ||
|
||||
pathError != '' ||
|
||||
emptyString(script_path) ||
|
||||
(errorHandlerSelected == 'slack' &&
|
||||
!emptyString(errorHandlerPath) &&
|
||||
emptyString(errorHandlerExtraArgs['channel'])) ||
|
||||
(errorHandler.selected == 'slack' &&
|
||||
!!errorHandler.slack.path &&
|
||||
emptyString(errorHandler.slack.args['channel'])) ||
|
||||
!can_write
|
||||
)
|
||||
const scheduleCfg = $derived.by(getScheduleCfg)
|
||||
@@ -209,68 +202,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
function setScheduleHandlers(s: Partial<Schedule>) {
|
||||
errorHandler = scheduleHandlerDraft('error', s.on_failure, s.on_failure_extra_args)
|
||||
failedTimes = s.on_failure ? (s.on_failure_times ?? 1) : 1
|
||||
failedExact = s.on_failure ? (s.on_failure_exact ?? false) : false
|
||||
recoveryHandler = scheduleHandlerDraft('recovery', s.on_recovery, s.on_recovery_extra_args)
|
||||
recoveredTimes = s.on_recovery ? (s.on_recovery_times ?? 1) : 1
|
||||
successHandler = scheduleHandlerDraft('success', s.on_success, s.on_success_extra_args)
|
||||
}
|
||||
|
||||
async function setScheduleHandler(s?: Schedule) {
|
||||
if (s) {
|
||||
if (s.on_failure) {
|
||||
let splitted = s.on_failure.split('/')
|
||||
errorHandleritemKind = splitted[0] as 'flow' | 'script'
|
||||
errorHandlerPath = splitted.slice(1)?.join('/')
|
||||
failedTimes = s.on_failure_times ?? 1
|
||||
failedExact = s.on_failure_exact ?? false
|
||||
errorHandlerExtraArgs = s.on_failure_extra_args ?? {}
|
||||
errorHandlerSelected = getHandlerType('error', errorHandlerPath)
|
||||
} else {
|
||||
errorHandlerPath = undefined
|
||||
errorHandleritemKind = 'script'
|
||||
errorHandlerExtraArgs = {}
|
||||
failedExact = false
|
||||
failedTimes = 1
|
||||
errorHandlerSelected = 'slack'
|
||||
}
|
||||
if (s.on_recovery) {
|
||||
let splitted = s.on_recovery.split('/')
|
||||
recoveryHandlerItemKind = splitted[0] as 'flow' | 'script'
|
||||
recoveryHandlerPath = splitted.slice(1)?.join('/')
|
||||
recoveredTimes = s.on_recovery_times ?? 1
|
||||
recoveryHandlerExtraArgs = s.on_recovery_extra_args ?? {}
|
||||
recoveryHandlerSelected = getHandlerType('recovery', recoveryHandlerPath)
|
||||
} else {
|
||||
recoveryHandlerPath = undefined
|
||||
recoveryHandlerItemKind = 'script'
|
||||
recoveredTimes = 1
|
||||
recoveryHandlerSelected = 'slack'
|
||||
recoveryHandlerExtraArgs = {}
|
||||
}
|
||||
if (s.on_success) {
|
||||
let splitted = s.on_success.split('/')
|
||||
successHandlerItemKind = splitted[0] as 'flow' | 'script'
|
||||
successHandlerPath = splitted.slice(1)?.join('/')
|
||||
successHandlerExtraArgs = s.on_success_extra_args ?? {}
|
||||
successHandlerSelected = getHandlerType('success', successHandlerPath)
|
||||
} else {
|
||||
successHandlerPath = undefined
|
||||
successHandlerItemKind = 'script'
|
||||
successHandlerSelected = 'slack'
|
||||
successHandlerExtraArgs = {}
|
||||
}
|
||||
setScheduleHandlers(s)
|
||||
} else {
|
||||
const d = wsId ? await loadDefaultScheduleAdvanced(wsId) : emptyScheduleAdvanced()
|
||||
wsErrorHandlerMuted = d.wsErrorHandlerMuted
|
||||
errorHandlerPath = d.errorHandlerPath
|
||||
errorHandleritemKind = d.errorHandleritemKind
|
||||
errorHandlerExtraArgs = d.errorHandlerExtraArgs
|
||||
errorHandlerSelected = d.errorHandlerSelected
|
||||
errorHandler = d.errorHandler
|
||||
failedTimes = d.failedTimes
|
||||
failedExact = d.failedExact
|
||||
recoveryHandlerPath = d.recoveryHandlerPath
|
||||
recoveryHandlerItemKind = d.recoveryHandlerItemKind
|
||||
recoveryHandlerExtraArgs = d.recoveryHandlerExtraArgs
|
||||
recoveryHandlerSelected = d.recoveryHandlerSelected
|
||||
recoveryHandler = d.recoveryHandler
|
||||
recoveredTimes = d.recoveredTimes
|
||||
successHandlerPath = d.successHandlerPath
|
||||
successHandlerItemKind = d.successHandlerItemKind
|
||||
successHandlerExtraArgs = d.successHandlerExtraArgs
|
||||
successHandlerSelected = d.successHandlerSelected
|
||||
successHandler = d.successHandler
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,48 +394,7 @@
|
||||
no_flow_overlap = cfg.no_flow_overlap ?? false
|
||||
wsErrorHandlerMuted = cfg.ws_error_handler_muted ?? false
|
||||
retry = cfg.retry
|
||||
if (cfg.on_failure) {
|
||||
let splitted = cfg.on_failure.split('/')
|
||||
errorHandleritemKind = splitted[0] as 'flow' | 'script'
|
||||
errorHandlerPath = splitted.slice(1)?.join('/')
|
||||
failedTimes = cfg.on_failure_times ?? 1
|
||||
failedExact = cfg.on_failure_exact ?? false
|
||||
errorHandlerExtraArgs = cfg.on_failure_extra_args ?? {}
|
||||
errorHandlerSelected = getHandlerType('error', errorHandlerPath ?? '')
|
||||
} else {
|
||||
errorHandlerPath = undefined
|
||||
errorHandleritemKind = 'script'
|
||||
errorHandlerExtraArgs = {}
|
||||
failedExact = false
|
||||
failedTimes = 1
|
||||
errorHandlerSelected = 'slack'
|
||||
}
|
||||
if (cfg.on_recovery) {
|
||||
let splitted = cfg.on_recovery.split('/')
|
||||
recoveryHandlerItemKind = splitted[0] as 'flow' | 'script'
|
||||
recoveryHandlerPath = splitted.slice(1)?.join('/')
|
||||
recoveredTimes = cfg.on_recovery_times ?? 1
|
||||
recoveryHandlerExtraArgs = cfg.on_recovery_extra_args ?? {}
|
||||
recoveryHandlerSelected = getHandlerType('recovery', recoveryHandlerPath ?? '')
|
||||
} else {
|
||||
recoveryHandlerPath = undefined
|
||||
recoveryHandlerItemKind = 'script'
|
||||
recoveredTimes = 1
|
||||
recoveryHandlerSelected = 'slack'
|
||||
recoveryHandlerExtraArgs = {}
|
||||
}
|
||||
if (cfg.on_success) {
|
||||
let splitted = cfg.on_success.split('/')
|
||||
successHandlerItemKind = splitted[0] as 'flow' | 'script'
|
||||
successHandlerPath = splitted.slice(1)?.join('/')
|
||||
successHandlerExtraArgs = cfg.on_success_extra_args ?? {}
|
||||
successHandlerSelected = getHandlerType('success', successHandlerPath ?? '')
|
||||
} else {
|
||||
successHandlerPath = undefined
|
||||
successHandlerItemKind = 'script'
|
||||
successHandlerSelected = 'slack'
|
||||
successHandlerExtraArgs = {}
|
||||
}
|
||||
setScheduleHandlers(cfg)
|
||||
dynamicSkipPath = cfg.dynamic_skip
|
||||
args = cfg.args ?? {}
|
||||
extraPerms = cfg.extra_perms ?? {}
|
||||
@@ -549,22 +460,13 @@
|
||||
args: args,
|
||||
enabled: enabled,
|
||||
...scheduleAdvancedCfg({
|
||||
errorHandlerSelected,
|
||||
errorHandlerPath,
|
||||
errorHandleritemKind,
|
||||
errorHandlerExtraArgs,
|
||||
errorHandler,
|
||||
wsErrorHandlerMuted,
|
||||
failedTimes,
|
||||
failedExact,
|
||||
recoveryHandlerSelected,
|
||||
recoveryHandlerPath,
|
||||
recoveryHandlerItemKind,
|
||||
recoveryHandlerExtraArgs,
|
||||
recoveryHandler,
|
||||
recoveredTimes,
|
||||
successHandlerSelected,
|
||||
successHandlerPath,
|
||||
successHandlerItemKind,
|
||||
successHandlerExtraArgs,
|
||||
successHandler,
|
||||
retry,
|
||||
dynamicSkipPath,
|
||||
tag
|
||||
@@ -934,8 +836,8 @@
|
||||
error_handler_path={errorHandlerPath}
|
||||
{retry}
|
||||
extraBadges={[
|
||||
{ name: 'Recovery Handler', active: !!recoveryHandlerPath },
|
||||
{ name: 'Success Handler', active: !!successHandlerPath },
|
||||
{ name: 'Recovery Handler', active: !!scheduleHandlerStored(recoveryHandler).path },
|
||||
{ name: 'Success Handler', active: !!scheduleHandlerStored(successHandler).path },
|
||||
{ name: 'Dynamic Skip', active: !!dynamicSkipPath },
|
||||
{ name: 'Custom Tag', active: !!tag }
|
||||
]}
|
||||
@@ -946,22 +848,13 @@
|
||||
{itemKind}
|
||||
canWrite={can_write}
|
||||
{loading}
|
||||
bind:errorHandlerSelected
|
||||
bind:errorHandlerPath
|
||||
bind:errorHandleritemKind
|
||||
bind:errorHandlerExtraArgs
|
||||
bind:errorHandler
|
||||
bind:wsErrorHandlerMuted
|
||||
bind:failedTimes
|
||||
bind:failedExact
|
||||
bind:recoveryHandlerSelected
|
||||
bind:recoveryHandlerPath
|
||||
bind:recoveryHandlerItemKind
|
||||
bind:recoveryHandlerExtraArgs
|
||||
bind:recoveryHandler
|
||||
bind:recoveredTimes
|
||||
bind:successHandlerSelected
|
||||
bind:successHandlerPath
|
||||
bind:successHandlerItemKind
|
||||
bind:successHandlerExtraArgs
|
||||
bind:successHandler
|
||||
bind:retry
|
||||
bind:dynamicSkipPath
|
||||
bind:tag
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
import {
|
||||
SqsTriggerService,
|
||||
type AwsAuthResourceType,
|
||||
type ErrorHandler,
|
||||
type Retry,
|
||||
type TriggerMode
|
||||
} from '$lib/gen'
|
||||
@@ -28,7 +27,8 @@
|
||||
import TriggerEditorToolbar from '../TriggerEditorToolbar.svelte'
|
||||
import PermissionedAsLine from '../PermissionedAsLine.svelte'
|
||||
import { saveSqsTriggerFromCfg } from './utils'
|
||||
import { getHandlerType, handleConfigChange, type Trigger } from '../utils'
|
||||
import { handleConfigChange, type Trigger } from '../utils'
|
||||
import { handlerDraftFromStored, handlerDraftToStored } from '../../handlerDraft.svelte'
|
||||
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
|
||||
import Tab from '$lib/components/common/tabs/Tab.svelte'
|
||||
import TriggerRetriesAndErrorHandler from '../TriggerRetriesAndErrorHandler.svelte'
|
||||
@@ -126,9 +126,10 @@
|
||||
let selectedPermissionedAs = $state<string | undefined>(undefined)
|
||||
let preservePermissionedAs = $state(false)
|
||||
let optionTabSelected: 'error_handler' | 'retries' = $state('error_handler')
|
||||
let errorHandlerSelected: ErrorHandler = $state('slack')
|
||||
let error_handler_path: string | undefined = $state()
|
||||
let error_handler_args: Record<string, any> = $state({})
|
||||
let errorHandler = $state(handlerDraftFromStored('error', undefined))
|
||||
const storedErrorHandler = $derived(handlerDraftToStored(errorHandler))
|
||||
const error_handler_path = $derived(storedErrorHandler.path)
|
||||
const error_handler_args = $derived(storedErrorHandler.args)
|
||||
let retry: Retry | undefined = $state()
|
||||
let suspendedJobsModal = $state<TriggerSuspendedJobsModal | null>(null)
|
||||
let originalConfig = $state<Record<string, any> | undefined>(undefined)
|
||||
@@ -234,10 +235,13 @@
|
||||
edit = false
|
||||
dirtyPath = false
|
||||
mode = defaultValues?.mode ?? 'enabled'
|
||||
error_handler_path = defaultValues?.error_handler_path ?? undefined
|
||||
error_handler_args = defaultValues?.error_handler_args ?? {}
|
||||
errorHandler = handlerDraftFromStored(
|
||||
'error',
|
||||
defaultValues?.error_handler_path,
|
||||
'script',
|
||||
defaultValues?.error_handler_args
|
||||
)
|
||||
retry = defaultValues?.retry ?? undefined
|
||||
errorHandlerSelected = getHandlerType(error_handler_path ?? '')
|
||||
permissionedAs = undefined
|
||||
selectedPermissionedAs = undefined
|
||||
preservePermissionedAs = false
|
||||
@@ -266,10 +270,13 @@
|
||||
aws_auth_resource_type = cfg?.aws_auth_resource_type
|
||||
permsPath = cfg?.path
|
||||
permsForWrite = cfg?.extra_perms
|
||||
error_handler_path = cfg?.error_handler_path
|
||||
error_handler_args = cfg?.error_handler_args ?? {}
|
||||
errorHandler = handlerDraftFromStored(
|
||||
'error',
|
||||
cfg?.error_handler_path,
|
||||
'script',
|
||||
cfg?.error_handler_args
|
||||
)
|
||||
retry = cfg?.retry
|
||||
errorHandlerSelected = getHandlerType(error_handler_path ?? '')
|
||||
permissionedAs = cfg?.permissioned_as
|
||||
selectedPermissionedAs = cfg?.permissioned_as
|
||||
preservePermissionedAs = !!cfg?.permissioned_as
|
||||
@@ -604,9 +611,7 @@
|
||||
{optionTabSelected}
|
||||
{itemKind}
|
||||
{can_write}
|
||||
bind:errorHandlerSelected
|
||||
bind:error_handler_path
|
||||
bind:error_handler_args
|
||||
bind:errorHandler
|
||||
bind:retry
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -30,7 +30,6 @@ import GoogleMonoIcon from '$lib/components/icons/triggers/GoogleIcon.svelte'
|
||||
import GithubMonoIcon from '$lib/components/icons/triggers/GithubIcon.svelte'
|
||||
import type {
|
||||
CaptureTriggerKind,
|
||||
ErrorHandler,
|
||||
Flow,
|
||||
JobTriggerKind,
|
||||
NewScript,
|
||||
@@ -732,17 +731,3 @@ export function sortTriggers(triggers: Trigger[]): Trigger[] {
|
||||
|
||||
export type FlowWithDraftAndDraftTriggers = Flow
|
||||
export type NewScriptWithDraftAndDraftTriggers = NewScript & { hash?: string }
|
||||
|
||||
export function getHandlerType(scriptPath: string): ErrorHandler {
|
||||
const handlerMap = {
|
||||
teams: '/workspace-or-schedule-error-handler-teams',
|
||||
slack: '/workspace-or-schedule-error-handler-slack',
|
||||
email: '/workspace-or-error-handler-email'
|
||||
}
|
||||
for (const [type, suffix] of Object.entries(handlerMap)) {
|
||||
if (scriptPath.startsWith('hub/') && scriptPath.endsWith(suffix)) {
|
||||
return type as ErrorHandler
|
||||
}
|
||||
}
|
||||
return 'custom'
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
type WebsocketTriggerInitialMessage,
|
||||
type WebsocketHeartbeat,
|
||||
type Retry,
|
||||
type ErrorHandler,
|
||||
type TriggerMode
|
||||
} from '$lib/gen'
|
||||
import { usedTriggerKinds } from '$lib/stores'
|
||||
@@ -43,7 +42,8 @@
|
||||
import TriggerEditorToolbar from '../TriggerEditorToolbar.svelte'
|
||||
import PermissionedAsLine from '../PermissionedAsLine.svelte'
|
||||
import { saveWebsocketTriggerFromCfg } from './utils'
|
||||
import { getHandlerType, handleConfigChange, type Trigger } from '../utils'
|
||||
import { handleConfigChange, type Trigger } from '../utils'
|
||||
import { handlerDraftFromStored, handlerDraftToStored } from '../../handlerDraft.svelte'
|
||||
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
|
||||
import Tab from '$lib/components/common/tabs/Tab.svelte'
|
||||
import TriggerRetriesAndErrorHandler from '../TriggerRetriesAndErrorHandler.svelte'
|
||||
@@ -150,9 +150,10 @@
|
||||
let preservePermissionedAs = $state(false)
|
||||
let isValid = $state(false)
|
||||
let optionTabSelected: 'error_handler' | 'retries' = $state('error_handler')
|
||||
let errorHandlerSelected: ErrorHandler = $state('slack')
|
||||
let error_handler_path: string | undefined = $state()
|
||||
let error_handler_args: Record<string, any> = $state({})
|
||||
let errorHandler = $state(handlerDraftFromStored('error', undefined))
|
||||
const storedErrorHandler = $derived(handlerDraftToStored(errorHandler))
|
||||
const error_handler_path = $derived(storedErrorHandler.path)
|
||||
const error_handler_args = $derived(storedErrorHandler.args)
|
||||
let retry: Retry | undefined = $state()
|
||||
let mode = $state<TriggerMode>('enabled')
|
||||
|
||||
@@ -268,10 +269,13 @@
|
||||
dirtyPath = false
|
||||
can_return_message = false
|
||||
can_return_error_result = false
|
||||
error_handler_path = defaultValues?.error_handler_path ?? undefined
|
||||
error_handler_args = defaultValues?.error_handler_args ?? {}
|
||||
errorHandler = handlerDraftFromStored(
|
||||
'error',
|
||||
defaultValues?.error_handler_path,
|
||||
'script',
|
||||
defaultValues?.error_handler_args
|
||||
)
|
||||
retry = defaultValues?.retry ?? undefined
|
||||
errorHandlerSelected = getHandlerType(error_handler_path ?? '')
|
||||
mode = defaultValues?.mode ?? 'enabled'
|
||||
permissionedAs = undefined
|
||||
selectedPermissionedAs = undefined
|
||||
@@ -305,10 +309,13 @@
|
||||
heartbeat_state_field = hb?.state_field ?? ''
|
||||
permsPath = path
|
||||
permsForWrite = cfg?.extra_perms
|
||||
error_handler_path = cfg?.error_handler_path
|
||||
error_handler_args = cfg?.error_handler_args ?? {}
|
||||
errorHandler = handlerDraftFromStored(
|
||||
'error',
|
||||
cfg?.error_handler_path,
|
||||
'script',
|
||||
cfg?.error_handler_args
|
||||
)
|
||||
retry = cfg?.retry
|
||||
errorHandlerSelected = getHandlerType(error_handler_path ?? '')
|
||||
mode = cfg?.mode ?? 'enabled'
|
||||
permissionedAs = cfg?.permissioned_as
|
||||
selectedPermissionedAs = cfg?.permissioned_as
|
||||
@@ -916,9 +923,7 @@
|
||||
{optionTabSelected}
|
||||
{itemKind}
|
||||
{can_write}
|
||||
bind:errorHandlerSelected
|
||||
bind:error_handler_path
|
||||
bind:error_handler_args
|
||||
bind:errorHandler
|
||||
bind:retry
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -10,9 +10,13 @@
|
||||
import DeployToSetting from '$lib/components/DeployToSetting.svelte'
|
||||
import RemoteDeployTargetSetting from '$lib/components/RemoteDeployTargetSetting.svelte'
|
||||
import DevWorkspaceSetting from '$lib/components/DevWorkspaceSetting.svelte'
|
||||
import ErrorOrRecoveryHandler, {
|
||||
handlerFullPath
|
||||
} from '$lib/components/ErrorOrRecoveryHandler.svelte'
|
||||
import ErrorOrRecoveryHandler from '$lib/components/ErrorOrRecoveryHandler.svelte'
|
||||
import {
|
||||
cloneHandlerDraft,
|
||||
handlerDraftFromStored,
|
||||
handlerDraftToStored,
|
||||
type HandlerDraft
|
||||
} from '$lib/components/handlerDraft.svelte'
|
||||
import PageHeaderContent from '$lib/components/PageHeaderContent.svelte'
|
||||
import ScriptPicker from '$lib/components/ScriptPicker.svelte'
|
||||
|
||||
@@ -26,7 +30,6 @@
|
||||
WorkspaceService,
|
||||
SettingService,
|
||||
type AIConfig,
|
||||
type ErrorHandler,
|
||||
type GetCopilotSettingsStateResponse,
|
||||
type InstanceAISummary,
|
||||
type GetSettingsResponse
|
||||
@@ -67,7 +70,6 @@
|
||||
import GitSyncSection from '$lib/components/git_sync/GitSyncSection.svelte'
|
||||
import Trashbin from '$lib/components/settings/Trashbin.svelte'
|
||||
import { untrack } from 'svelte'
|
||||
import { getHandlerType } from '$lib/components/triggers/utils'
|
||||
import DbtSettings, {
|
||||
convertDbtSettingsFromBackend,
|
||||
type DbtSettingsType
|
||||
@@ -180,10 +182,16 @@
|
||||
let plan: string | undefined = $state(undefined)
|
||||
let customer_id: string | undefined = $state(undefined)
|
||||
let webhook: string | undefined = $state(undefined)
|
||||
let errorHandlerSelected: ErrorHandler = $state('slack')
|
||||
let errorHandlerScriptPath: string | undefined = $state(undefined)
|
||||
let errorHandlerItemKind: 'flow' | 'script' = $state('script')
|
||||
let errorHandlerExtraArgs: Record<string, any> = $state({})
|
||||
const noErrorHandler = handlerDraftFromStored(
|
||||
'error',
|
||||
undefined,
|
||||
'script',
|
||||
{},
|
||||
{
|
||||
emptySelected: 'slack'
|
||||
}
|
||||
)
|
||||
let errorHandlerDraft: HandlerDraft = $state(cloneHandlerDraft(noErrorHandler))
|
||||
let errorHandlerMutedOnCancel: boolean | undefined = $state(undefined)
|
||||
let errorHandlerMutedOnUserPath: boolean | undefined = $state(undefined)
|
||||
let successHandlerScriptPath: string | undefined = $state(undefined)
|
||||
@@ -249,10 +257,7 @@
|
||||
let initialEditedWorkspaceEncryptionKey: string | undefined = $state(undefined)
|
||||
|
||||
// Track initial error handler settings for unsaved changes detection
|
||||
let initialErrorHandlerSelected: ErrorHandler = $state('slack')
|
||||
let initialErrorHandlerScriptPath: string | undefined = $state(undefined)
|
||||
let initialErrorHandlerItemKind: 'flow' | 'script' = $state('script')
|
||||
let initialErrorHandlerExtraArgs: Record<string, any> = $state({})
|
||||
let initialErrorHandlerDraft: HandlerDraft = $state(cloneHandlerDraft(noErrorHandler))
|
||||
let initialErrorHandlerMutedOnCancel: boolean | undefined = $state(undefined)
|
||||
let initialErrorHandlerMutedOnUserPath: boolean | undefined = $state(undefined)
|
||||
|
||||
@@ -300,6 +305,13 @@
|
||||
return hasUnsavedChanges(changes.savedValue, changes.modifiedValue)
|
||||
})
|
||||
|
||||
let errorHandlerChannelMissing = $derived.by(() => {
|
||||
const { selected } = errorHandlerDraft
|
||||
if (selected !== 'slack' && selected !== 'teams') return false
|
||||
const slot = errorHandlerDraft[selected]
|
||||
return !!slot.path && emptyString(slot.args['channel'])
|
||||
})
|
||||
|
||||
// Derived state for checking unsaved changes in success handler
|
||||
let hasSuccessHandlerChanges = $derived.by(() => {
|
||||
if (tab !== 'error_handler') return false
|
||||
@@ -686,10 +698,6 @@
|
||||
| { path?: string; extra_args?: any; muted_on_cancel?: boolean; muted_on_user_path?: boolean }
|
||||
| undefined
|
||||
const errorHandlerPath = errorHandler?.path ?? ''
|
||||
errorHandlerItemKind = errorHandlerPath
|
||||
? (errorHandlerPath.split('/')[0] as 'flow' | 'script')
|
||||
: 'script'
|
||||
errorHandlerScriptPath = errorHandlerPath.split('/').slice(1).join('/')
|
||||
errorHandlerMutedOnCancel = errorHandler?.muted_on_cancel
|
||||
errorHandlerMutedOnUserPath = errorHandler?.muted_on_user_path
|
||||
criticalAlertUIMuted = settings.mute_critical_alerts
|
||||
@@ -706,18 +714,22 @@
|
||||
WorkspaceService.getGuestUsage({ workspace: $workspaceStore! })
|
||||
.then((u) => (guestUsage = u))
|
||||
.catch(() => (guestUsage = undefined))
|
||||
if (emptyString($enterpriseLicense)) {
|
||||
errorHandlerSelected = 'custom'
|
||||
} else if (
|
||||
const [errorHandlerKind, ...errorHandlerScriptPath] = errorHandlerPath.split('/')
|
||||
errorHandlerDraft = handlerDraftFromStored(
|
||||
'error',
|
||||
errorHandlerScriptPath.join('/'),
|
||||
errorHandlerKind === 'flow' ? 'flow' : 'script',
|
||||
errorHandler?.extra_args,
|
||||
{ asCustom: emptyString($enterpriseLicense) }
|
||||
)
|
||||
if (
|
||||
!emptyString($enterpriseLicense) &&
|
||||
canUseInstanceAlerts &&
|
||||
!errorHandlerPath &&
|
||||
settings.error_handler_fallback_to_instance_alerts
|
||||
) {
|
||||
errorHandlerSelected = 'instance_alerts'
|
||||
} else {
|
||||
errorHandlerSelected = getHandlerType(errorHandlerScriptPath)
|
||||
errorHandlerDraft.selected = 'instance_alerts'
|
||||
}
|
||||
errorHandlerExtraArgs = errorHandler?.extra_args ?? {}
|
||||
const successHandler = settings.success_handler as
|
||||
| { path?: string; extra_args?: any }
|
||||
| undefined
|
||||
@@ -764,10 +776,7 @@
|
||||
initialEditedWorkspaceEncryptionKey = editedWorkspaceEncryptionKey
|
||||
|
||||
// Store initial error handler state for unsaved changes detection
|
||||
initialErrorHandlerSelected = errorHandlerSelected
|
||||
initialErrorHandlerScriptPath = errorHandlerScriptPath
|
||||
initialErrorHandlerItemKind = errorHandlerItemKind
|
||||
initialErrorHandlerExtraArgs = clone(errorHandlerExtraArgs)
|
||||
initialErrorHandlerDraft = cloneHandlerDraft(errorHandlerDraft)
|
||||
initialErrorHandlerMutedOnCancel = errorHandlerMutedOnCancel
|
||||
initialErrorHandlerMutedOnUserPath = errorHandlerMutedOnUserPath
|
||||
|
||||
@@ -904,18 +913,19 @@
|
||||
})
|
||||
|
||||
async function editErrorHandler() {
|
||||
if (errorHandlerScriptPath) {
|
||||
const handler = handlerDraftToStored(errorHandlerDraft)
|
||||
if (handler.path) {
|
||||
await WorkspaceService.editErrorHandler({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
path: handlerFullPath(errorHandlerSelected, errorHandlerItemKind, errorHandlerScriptPath),
|
||||
extra_args: errorHandlerExtraArgs,
|
||||
path: `${handler.kind}/${handler.path}`,
|
||||
extra_args: handler.args,
|
||||
muted_on_cancel: errorHandlerMutedOnCancel,
|
||||
muted_on_user_path: errorHandlerMutedOnUserPath,
|
||||
fallback_to_instance_alerts: false
|
||||
}
|
||||
})
|
||||
sendUserToast(`workspace error handler set to ${errorHandlerScriptPath}`)
|
||||
sendUserToast(`workspace error handler set to ${handler.path}`)
|
||||
} else {
|
||||
await WorkspaceService.editErrorHandler({
|
||||
workspace: $workspaceStore!,
|
||||
@@ -924,23 +934,28 @@
|
||||
extra_args: undefined,
|
||||
muted_on_cancel: undefined,
|
||||
muted_on_user_path: undefined,
|
||||
fallback_to_instance_alerts: errorHandlerSelected === 'instance_alerts'
|
||||
fallback_to_instance_alerts: errorHandlerDraft.selected === 'instance_alerts'
|
||||
}
|
||||
})
|
||||
sendUserToast(
|
||||
errorHandlerSelected === 'instance_alerts'
|
||||
errorHandlerDraft.selected === 'instance_alerts'
|
||||
? `failed jobs will be reported to the instance critical alert channels`
|
||||
: initialErrorHandlerScriptPath
|
||||
: handlerDraftToStored(initialErrorHandlerDraft).path
|
||||
? `workspace error handler removed`
|
||||
: `error handler settings saved`
|
||||
)
|
||||
}
|
||||
|
||||
// Update initial values for dirty detection
|
||||
initialErrorHandlerSelected = errorHandlerSelected
|
||||
initialErrorHandlerScriptPath = errorHandlerScriptPath
|
||||
initialErrorHandlerItemKind = errorHandlerItemKind
|
||||
initialErrorHandlerExtraArgs = clone(errorHandlerExtraArgs)
|
||||
// Rebuilt from what was stored, so Discard also drops the other tabs' unsaved edits.
|
||||
initialErrorHandlerDraft = handlerDraftFromStored(
|
||||
'error',
|
||||
handler.path,
|
||||
handler.kind,
|
||||
handler.args,
|
||||
{
|
||||
emptySelected: errorHandlerDraft.selected
|
||||
}
|
||||
)
|
||||
initialErrorHandlerMutedOnCancel = errorHandlerMutedOnCancel
|
||||
initialErrorHandlerMutedOnUserPath = errorHandlerMutedOnUserPath
|
||||
}
|
||||
@@ -1132,50 +1147,40 @@
|
||||
guestJwtKeyType = initialGuestJwtJwksUrl ? 'jwks' : 'pem'
|
||||
}
|
||||
|
||||
// Strip keys from extraArgs that are auto-managed by child components:
|
||||
// - 'slack' while the Slack built-in is selected: set by ErrorOrRecoveryHandler's $effect
|
||||
// - 'channel_name': display metadata stripped by SchemaForm's removeExtraKey()
|
||||
function normalizeHandlerExtraArgs(
|
||||
args: Record<string, any>,
|
||||
handlerSelected: ErrorHandler
|
||||
): Record<string, any> {
|
||||
const { channel_name: _, ...rest } = args
|
||||
if (handlerSelected === 'slack') delete rest.slack
|
||||
return rest
|
||||
// Compares what Save would send, so visiting another tab and coming back is not a change.
|
||||
// The stored handler is nested because `hasUnsavedChanges` drops a top-level `path`.
|
||||
function errorHandlerSettingsValue(
|
||||
draft: HandlerDraft,
|
||||
mutedOnCancel: boolean | undefined,
|
||||
mutedOnUserPath: boolean | undefined
|
||||
) {
|
||||
return {
|
||||
errorHandler: handlerDraftToStored(draft),
|
||||
fallbackToInstanceAlerts: draft.selected === 'instance_alerts',
|
||||
errorHandlerMutedOnCancel: mutedOnCancel,
|
||||
errorHandlerMutedOnUserPath: mutedOnUserPath
|
||||
}
|
||||
}
|
||||
|
||||
// Function to check if there are unsaved changes in error handler settings
|
||||
function getErrorHandlerSettingsInitialAndModifiedValues() {
|
||||
const savedValue = {
|
||||
errorHandlerSelected: initialErrorHandlerSelected,
|
||||
errorHandlerScriptPath: initialErrorHandlerScriptPath,
|
||||
errorHandlerItemKind: initialErrorHandlerItemKind,
|
||||
errorHandlerExtraArgs: normalizeHandlerExtraArgs(
|
||||
initialErrorHandlerExtraArgs,
|
||||
initialErrorHandlerSelected
|
||||
return {
|
||||
savedValue: errorHandlerSettingsValue(
|
||||
initialErrorHandlerDraft,
|
||||
initialErrorHandlerMutedOnCancel,
|
||||
initialErrorHandlerMutedOnUserPath
|
||||
),
|
||||
errorHandlerMutedOnCancel: initialErrorHandlerMutedOnCancel,
|
||||
errorHandlerMutedOnUserPath: initialErrorHandlerMutedOnUserPath
|
||||
modifiedValue: errorHandlerSettingsValue(
|
||||
errorHandlerDraft,
|
||||
errorHandlerMutedOnCancel,
|
||||
errorHandlerMutedOnUserPath
|
||||
)
|
||||
}
|
||||
|
||||
const modifiedValue = {
|
||||
errorHandlerSelected: errorHandlerSelected,
|
||||
errorHandlerScriptPath: errorHandlerScriptPath,
|
||||
errorHandlerItemKind: errorHandlerItemKind,
|
||||
errorHandlerExtraArgs: normalizeHandlerExtraArgs(errorHandlerExtraArgs, errorHandlerSelected),
|
||||
errorHandlerMutedOnCancel: errorHandlerMutedOnCancel,
|
||||
errorHandlerMutedOnUserPath: errorHandlerMutedOnUserPath
|
||||
}
|
||||
|
||||
return { savedValue, modifiedValue }
|
||||
}
|
||||
|
||||
// Function to discard unsaved error handler settings changes
|
||||
function discardErrorHandlerSettingsChanges() {
|
||||
errorHandlerSelected = initialErrorHandlerSelected
|
||||
errorHandlerScriptPath = initialErrorHandlerScriptPath
|
||||
errorHandlerItemKind = initialErrorHandlerItemKind
|
||||
errorHandlerExtraArgs = clone(initialErrorHandlerExtraArgs)
|
||||
errorHandlerDraft = cloneHandlerDraft(initialErrorHandlerDraft)
|
||||
errorHandlerMutedOnCancel = initialErrorHandlerMutedOnCancel
|
||||
errorHandlerMutedOnUserPath = initialErrorHandlerMutedOnUserPath
|
||||
}
|
||||
@@ -1974,11 +1979,8 @@
|
||||
isEditable={true}
|
||||
errorOrRecovery="error"
|
||||
showScriptHelpText={true}
|
||||
bind:handlerSelected={errorHandlerSelected}
|
||||
bind:handlerPath={errorHandlerScriptPath}
|
||||
bind:draft={errorHandlerDraft}
|
||||
customScriptTemplate="/scripts/add?hub=hub%2F9083%2Fwindmill%2Fworkspace_error_handler_template"
|
||||
bind:customHandlerKind={errorHandlerItemKind}
|
||||
bind:handlerExtraArgs={errorHandlerExtraArgs}
|
||||
showInstanceAlerts={canUseInstanceAlerts}
|
||||
>
|
||||
{#snippet customTabTooltip()}
|
||||
@@ -2009,21 +2011,15 @@
|
||||
{/snippet}
|
||||
</ErrorOrRecoveryHandler>
|
||||
|
||||
{#if errorHandlerSelected !== 'instance_alerts'}
|
||||
{#if errorHandlerDraft.selected !== 'instance_alerts'}
|
||||
<SettingCard class="gap-2">
|
||||
<Toggle
|
||||
disabled={!$enterpriseLicense ||
|
||||
((errorHandlerSelected === 'slack' || errorHandlerSelected === 'teams') &&
|
||||
!emptyString(errorHandlerScriptPath) &&
|
||||
emptyString(errorHandlerExtraArgs['channel']))}
|
||||
disabled={!$enterpriseLicense || errorHandlerChannelMissing}
|
||||
bind:checked={errorHandlerMutedOnCancel}
|
||||
options={{ right: 'Do not run error handler for canceled jobs' }}
|
||||
/>
|
||||
<Toggle
|
||||
disabled={!$enterpriseLicense ||
|
||||
((errorHandlerSelected === 'slack' || errorHandlerSelected === 'teams') &&
|
||||
!emptyString(errorHandlerScriptPath) &&
|
||||
emptyString(errorHandlerExtraArgs['channel']))}
|
||||
disabled={!$enterpriseLicense || errorHandlerChannelMissing}
|
||||
bind:checked={errorHandlerMutedOnUserPath}
|
||||
options={{ right: 'Do not run error handler for u/ scripts and flows' }}
|
||||
/>
|
||||
@@ -2038,10 +2034,7 @@
|
||||
onSave={editErrorHandler}
|
||||
onDiscard={discardErrorHandlerSettingsChanges}
|
||||
saveLabel="Save error handler"
|
||||
disabled={!$enterpriseLicense ||
|
||||
((errorHandlerSelected === 'slack' || errorHandlerSelected === 'teams') &&
|
||||
!emptyString(errorHandlerScriptPath) &&
|
||||
emptyString(errorHandlerExtraArgs['channel']))}
|
||||
disabled={!$enterpriseLicense || errorHandlerChannelMissing}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
wmill = ">=1.831.0"
|
||||
wmill = ">=1.832.0"
|
||||
sendgrid = "*"
|
||||
mysql-connector-python = "*"
|
||||
pymongo = "*"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: '3.0.3'
|
||||
|
||||
info:
|
||||
version: 1.831.0
|
||||
version: 1.832.0
|
||||
title: OpenFlow Spec
|
||||
contact:
|
||||
name: Ruben Fiszel
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
RootModule = 'WindmillClient.psm1'
|
||||
|
||||
# Version number of this module.
|
||||
ModuleVersion = '1.831.0'
|
||||
ModuleVersion = '1.832.0'
|
||||
|
||||
# Supported PSEditions
|
||||
# CompatiblePSEditions = @()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "wmill"
|
||||
version = "1.831.0"
|
||||
version = "1.832.0"
|
||||
description = "A client library for accessing Windmill server wrapping the Windmill client API"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://windmill.dev"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@windmill/windmill",
|
||||
"version": "1.831.0",
|
||||
"version": "1.832.0",
|
||||
"exports": "./src/index.ts",
|
||||
"publish": {
|
||||
"exclude": ["!src", "./s3Types.ts", "./sqlUtils.ts", "./client.ts", "./wacError.ts"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "windmill-client",
|
||||
"description": "Windmill SDK client for browsers and Node.js",
|
||||
"version": "1.831.0",
|
||||
"version": "1.832.0",
|
||||
"author": "Ruben Fiszel",
|
||||
"license": "Apache 2.0",
|
||||
"homepage": "https://github.com/windmill-labs/windmill/tree/main/typescript-client#readme",
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.831.0
|
||||
1.832.0
|
||||
|
||||
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "windmill-yaml-validator",
|
||||
"version": "1.831.0",
|
||||
"version": "1.832.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill-yaml-validator",
|
||||
"version": "1.831.0",
|
||||
"version": "1.832.0",
|
||||
"license": "Apache 2.0",
|
||||
"dependencies": {
|
||||
"@stoplight/yaml": "^4.3.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-yaml-validator",
|
||||
"version": "1.831.0",
|
||||
"version": "1.832.0",
|
||||
"description": "YAML validator for Windmill flow, schedule, and trigger files",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
Reference in New Issue
Block a user