mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-09 00:04:10 +00:00
Merge branch 'main' into dieri/triggers-queue-mode
This commit is contained in:
@@ -40,11 +40,12 @@ jobs:
|
||||
with:
|
||||
github-token: ${{ steps.app.outputs.token }}
|
||||
script: |
|
||||
const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: 'Starting sqlx update...'
|
||||
body: `Starting sqlx update...\n\n[View workflow run](${runUrl})`
|
||||
})
|
||||
|
||||
- name: Checkout repository
|
||||
|
||||
@@ -49,14 +49,14 @@ jobs:
|
||||
- **Bug Fixes**: PRs with titles starting with "fix:", "bug:", or containing "fix", "resolve", "patch"
|
||||
- **Other**: All remaining PRs (improvements, refactors, docs, chores, etc.)
|
||||
|
||||
4. **Gather Details**: For each merged PR, include:
|
||||
4. **Gather Details**: For each feature and bug fix merged PR, include:
|
||||
- Full PR title (NO truncation, NO links)
|
||||
- Author (extract login from author.login in JSON)
|
||||
- Brief summary: Use `gh pr view <number> --json body` to get PR description, then extract first paragraph or key points (1-2 sentences max)
|
||||
|
||||
5. **Character Limit Enforcement**:
|
||||
- The final summary MUST be under 6000 characters
|
||||
- If the summary exceeds 6000 characters, truncate PR descriptions (NOT titles) and add at the end: "_and X more PRs_" where X is the count of omitted PRs
|
||||
- The final summary MUST be under 5000 characters
|
||||
- If the summary exceeds 5000 characters, truncate PR descriptions (NOT titles) and add at the end: "_and X more PRs_" where X is the count of omitted PRs
|
||||
|
||||
6. **Save Summary to Markdown File**: Write the summary to a file for webhook delivery:
|
||||
- Save the complete formatted markdown to: `summary.md`
|
||||
@@ -65,23 +65,19 @@ jobs:
|
||||
## Output Format:
|
||||
|
||||
```markdown
|
||||
#### 📈 Weekly overview
|
||||
### 📈 Weekly overview
|
||||
- **Total merged**: X
|
||||
- **Features**: Y
|
||||
- **Bug Fixes**: Z
|
||||
- **Other**: W
|
||||
|
||||
#### ✨ Features (Y)
|
||||
• **[Full PR Title]** by @username - [brief impact description]
|
||||
• **[Full PR Title]** by @username - [brief impact description]
|
||||
### ✨ Features (Y)
|
||||
- **[Full PR Title]** by @username - [brief impact description]
|
||||
- **[Full PR Title]** by @username - [brief impact description]
|
||||
|
||||
#### 🐛 Bug Fixes (Z)
|
||||
• **[Full PR Title]** by @username - [brief impact description]
|
||||
• **[Full PR Title]** by @username - [brief impact description]
|
||||
|
||||
#### 🔧 Other (W)
|
||||
• **[Full PR Title]** by @username - [brief impact description]
|
||||
• **[Full PR Title]** by @username - [brief impact description]
|
||||
### 🐛 Bug Fixes (Z)
|
||||
- **[Full PR Title]** by @username - [brief impact description]
|
||||
- **[Full PR Title]** by @username - [brief impact description]
|
||||
|
||||
_and X more PRs_
|
||||
```
|
||||
@@ -89,9 +85,9 @@ jobs:
|
||||
## Important Notes:
|
||||
- **CRITICAL**: ONLY include PRs with state "merged" from the last 7 days
|
||||
- **CRITICAL**: EXCLUDE all PRs with titles starting with "chore: release" or "chore(release)"
|
||||
- **CRITICAL**: Total character count MUST be under 6000 characters
|
||||
- Only use #### markdown headers for major sections and emoji indicators
|
||||
- Use bullet points (•) for individual PR entries - more compact than paragraphs
|
||||
- **CRITICAL**: Total character count MUST be under 5000 characters
|
||||
- Count the number of "Other" PRs but do not include a section for them in the output
|
||||
- Only use ### markdown headers for major sections and emoji indicators
|
||||
- NO links to PRs
|
||||
- NO merged date in output
|
||||
- NEVER truncate PR titles - show full titles
|
||||
@@ -100,7 +96,7 @@ jobs:
|
||||
- If a PR has no description, write "(No description provided)"
|
||||
- Extract meaningful summary from PR body - look for the first paragraph or key bullet points
|
||||
- Parse JSON responses carefully using `jq` or similar tools
|
||||
- If summary exceeds 6000 chars, shorten PR descriptions and add "_and X more PRs_" at the end
|
||||
- If summary exceeds 5000 chars, shorten PR descriptions and add "_and X more PRs_" at the end
|
||||
- Count PRs in each category and display in both overview and section headers
|
||||
|
||||
## Saving the Markdown Output:
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# Changelog
|
||||
|
||||
## [1.573.5](https://github.com/windmill-labs/windmill/compare/v1.573.4...v1.573.5) (2025-11-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* dep jobs improvements ([#7081](https://github.com/windmill-labs/windmill/issues/7081)) ([ad43680](https://github.com/windmill-labs/windmill/commit/ad43680bb979f80db3bdb182ccb5bc61ca385ceb))
|
||||
* modal action in App AgGrid ([#7085](https://github.com/windmill-labs/windmill/issues/7085)) ([b07c35f](https://github.com/windmill-labs/windmill/commit/b07c35fe1c9dbc4955ce47766ef92bd5cfdac4c8))
|
||||
|
||||
## [1.573.4](https://github.com/windmill-labs/windmill/compare/v1.573.3...v1.573.4) (2025-11-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bun:** support relative imports without .ts but starting with /u/ or /f/ ([9e3ea76](https://github.com/windmill-labs/windmill/commit/9e3ea7657568d6561abcdfd23cc8a210432e4050))
|
||||
* create git branch right before creating the workspace fork to catch errors and have a coherent fork point ([#7073](https://github.com/windmill-labs/windmill/issues/7073)) ([192fecc](https://github.com/windmill-labs/windmill/commit/192fecc86f54cf2a69ec7dddc466139b40933ac9))
|
||||
* improve priority for child jobs ran from api ([5f7e0b7](https://github.com/windmill-labs/windmill/commit/5f7e0b7244e27533d9a8d1551e8affeb17eb94c0))
|
||||
* make it impossible to have non linear script history even in edge condition ([3bda6da](https://github.com/windmill-labs/windmill/commit/3bda6da1683589180cad0780dd87b391a04851f8))
|
||||
|
||||
## [1.573.3](https://github.com/windmill-labs/windmill/compare/v1.573.2...v1.573.3) (2025-11-06)
|
||||
|
||||
|
||||
|
||||
+5
-5
@@ -46,11 +46,11 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT \n CASE \n WHEN flow_version.id IS NOT NULL THEN\n (flow_version.value -> 'flow_env' -> $3) #> $4\n ELSE\n (root_job.raw_flow -> 'flow_env' -> $3) #> $4\n END AS \"flow_env: sqlx::types::Json<Box<RawValue>>\"\n FROM \n v2_job current_job\n JOIN \n v2_job root_job ON root_job.id = COALESCE(current_job.root_job, current_job.flow_innermost_root_job, current_job.parent_job, current_job.id)\n AND root_job.workspace_id = current_job.workspace_id\n LEFT JOIN\n flow_version ON flow_version.id = root_job.runnable_id\n AND flow_version.path = root_job.runnable_path\n AND flow_version.workspace_id = root_job.workspace_id\n WHERE \n current_job.id = $1 AND \n current_job.workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "flow_env: sqlx::types::Json<Box<RawValue>>",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text",
|
||||
"Text",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "0c0f3909b80c35210fc64c685905308621f9135c2c45a2fa0531ea750387da1f"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT 1 FROM script WHERE hash = $1 AND workspace_id = $2 FOR UPDATE",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "1fc43faf3a7ea15be82b1666273836901cc6ba17c7d5e194b50b9204f264de27"
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO ai_agent_memory (workspace_id, conversation_id, step_id, messages, created_at, updated_at)\n VALUES ($1, $2, $3, $4, NOW(), NOW())\n ON CONFLICT (workspace_id, conversation_id, step_id)\n DO UPDATE SET\n messages = EXCLUDED.messages,\n updated_at = NOW()",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Uuid",
|
||||
"Varchar",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "38baa6cf7d1c2532d38486a01e6d27ac1a58540a51e97dec5613e9bc0791e890"
|
||||
}
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT messages FROM ai_agent_memory\n WHERE workspace_id = $1 AND conversation_id = $2 AND step_id = $3",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "messages",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "6a1005b8ae5326c5d5955534e9228b6bd524253971ae09cbe100c0d6d42e6cdc"
|
||||
}
|
||||
+2
-2
@@ -18,8 +18,8 @@
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76"
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM ai_agent_memory\n WHERE workspace_id = $1 AND conversation_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d1d9cb0a7043760564171f481dd73f04eaa2c7d5c1ddc0501bc70c3fb34a07be"
|
||||
}
|
||||
Generated
+33
-33
@@ -788,9 +788,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-credential-types"
|
||||
version = "1.2.8"
|
||||
version = "1.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "faf26925f4a5b59eb76722b63c2892b1d70d06fa053c72e4a100ec308c1d47bc"
|
||||
checksum = "86590e57ea40121d47d3f2e131bfd873dea15d78dc2f4604f4734537ad9e56c4"
|
||||
dependencies = [
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-runtime-api",
|
||||
@@ -1945,9 +1945,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.44"
|
||||
version = "1.2.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3"
|
||||
checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
@@ -10296,9 +10296,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.41"
|
||||
version = "1.0.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
|
||||
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -15137,7 +15137,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windmill"
|
||||
version = "1.573.3"
|
||||
version = "1.573.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"aws-sdk-config",
|
||||
@@ -15197,7 +15197,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.573.3"
|
||||
version = "1.573.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -15317,7 +15317,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.573.3"
|
||||
version = "1.573.5"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
@@ -15332,7 +15332,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.573.3"
|
||||
version = "1.573.5"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"lazy_static",
|
||||
@@ -15346,7 +15346,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-autoscaling"
|
||||
version = "1.573.3"
|
||||
version = "1.573.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -15365,7 +15365,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.573.3"
|
||||
version = "1.573.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15450,7 +15450,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-git-sync"
|
||||
version = "1.573.3"
|
||||
version = "1.573.5"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -15465,7 +15465,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-indexer"
|
||||
version = "1.573.3"
|
||||
version = "1.573.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -15489,7 +15489,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.573.3"
|
||||
version = "1.573.5"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"lazy_static",
|
||||
@@ -15501,7 +15501,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.573.3"
|
||||
version = "1.573.5"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"serde",
|
||||
@@ -15510,7 +15510,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.573.3"
|
||||
version = "1.573.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15522,7 +15522,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.573.3"
|
||||
version = "1.573.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -15534,7 +15534,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.573.3"
|
||||
version = "1.573.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -15546,7 +15546,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.573.3"
|
||||
version = "1.573.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15558,7 +15558,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-java"
|
||||
version = "1.573.3"
|
||||
version = "1.573.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -15570,7 +15570,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-nu"
|
||||
version = "1.573.3"
|
||||
version = "1.573.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nu-parser",
|
||||
@@ -15581,7 +15581,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.573.3"
|
||||
version = "1.573.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -15592,7 +15592,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.573.3"
|
||||
version = "1.573.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -15604,7 +15604,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.573.3"
|
||||
version = "1.573.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15627,7 +15627,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ruby"
|
||||
version = "1.573.3"
|
||||
version = "1.573.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15641,7 +15641,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.573.3"
|
||||
version = "1.573.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case 0.6.0",
|
||||
@@ -15658,7 +15658,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.573.3"
|
||||
version = "1.573.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15672,7 +15672,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.573.3"
|
||||
version = "1.573.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -15690,7 +15690,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.573.3"
|
||||
version = "1.573.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -15701,7 +15701,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.573.3"
|
||||
version = "1.573.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -15735,7 +15735,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-sql-datatype-parser-wasm"
|
||||
version = "1.573.3"
|
||||
version = "1.573.5"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
@@ -15745,7 +15745,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.573.3"
|
||||
version = "1.573.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-once-cell",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.573.3"
|
||||
version = "1.573.5"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -33,7 +33,7 @@ members = [
|
||||
exclude = ["./windmill-duckdb-ffi-internal"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.573.3"
|
||||
version = "1.573.5"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
ab6116235f7869cef306996059f8663f7e831b8a
|
||||
5b7afe50da442441747e7a8f6ef461c96faa9dc2
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Add down migration script here
|
||||
|
||||
-- Drop table
|
||||
DROP TABLE IF EXISTS ai_agent_memory;
|
||||
@@ -0,0 +1,16 @@
|
||||
-- Add up migration script here
|
||||
|
||||
-- Create ai_agent_memory table for storing AI agent step memory when S3 is unavailable
|
||||
CREATE TABLE ai_agent_memory (
|
||||
workspace_id VARCHAR(50) NOT NULL,
|
||||
conversation_id UUID NOT NULL,
|
||||
step_id VARCHAR(255) NOT NULL,
|
||||
messages JSONB NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (workspace_id, conversation_id, step_id)
|
||||
);
|
||||
|
||||
-- Grant permissions
|
||||
GRANT ALL ON ai_agent_memory TO windmill_admin;
|
||||
GRANT ALL ON ai_agent_memory TO windmill_user;
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.573.3
|
||||
version: 1.573.5
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
|
||||
@@ -11,7 +11,7 @@ use uuid::Uuid;
|
||||
|
||||
use crate::db::ApiAuthed;
|
||||
use windmill_common::{
|
||||
db::UserDB,
|
||||
db::{UserDB, DB},
|
||||
error::{JsonResult, Result},
|
||||
flow_conversations::MessageType,
|
||||
utils::{not_found_if_none, paginate, Pagination},
|
||||
@@ -154,6 +154,7 @@ pub async fn get_or_create_conversation_with_id(
|
||||
async fn delete_conversation(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, conversation_id)): Path<(String, Uuid)>,
|
||||
) -> Result<String> {
|
||||
let mut tx = user_db.clone().begin(&authed).await?;
|
||||
@@ -185,10 +186,14 @@ async fn delete_conversation(
|
||||
|
||||
// Delete associated memory in background (non-blocking cleanup)
|
||||
let w_id_clone = w_id.clone();
|
||||
let db_clone = db.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) =
|
||||
windmill_worker::memory_oss::delete_conversation_memory(&w_id_clone, conversation_id)
|
||||
.await
|
||||
if let Err(e) = windmill_worker::memory_oss::delete_conversation_memory(
|
||||
&db_clone,
|
||||
&w_id_clone,
|
||||
conversation_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to delete memory for conversation {} in workspace {}: {:?}",
|
||||
|
||||
@@ -1634,6 +1634,7 @@ mod tests {
|
||||
early_return: None,
|
||||
concurrency_key: None,
|
||||
chat_input_enabled: None,
|
||||
flow_env: None,
|
||||
debounce_key: None,
|
||||
debounce_delay_s: None,
|
||||
};
|
||||
|
||||
@@ -281,6 +281,10 @@ pub fn workspaced_service() -> Router {
|
||||
"/result_by_id/:job_id/:node_id",
|
||||
get(get_result_by_id).layer(cors.clone()),
|
||||
)
|
||||
.route(
|
||||
"/flow_env_by_flow_job_id/:flow_job_id/:var_name",
|
||||
get(get_flow_env_by_flow_job_id).layer(cors.clone()),
|
||||
)
|
||||
.route("/run/dependencies", post(run_dependencies_job))
|
||||
.route("/run/flow_dependencies", post(run_flow_dependencies_job))
|
||||
.route(
|
||||
@@ -376,6 +380,59 @@ async fn get_root_job(
|
||||
Ok(Json(res))
|
||||
}
|
||||
|
||||
async fn get_flow_env_by_flow_job_id(
|
||||
authed: ApiAuthed,
|
||||
tokened: Tokened,
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, flow_job_id, var_name)): Path<(String, Uuid, String)>,
|
||||
Query(JsonPath { json_path, .. }): Query<JsonPath>,
|
||||
) -> windmill_common::error::JsonResult<Box<JsonRawValue>> {
|
||||
let flow_env = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT
|
||||
CASE
|
||||
WHEN flow_version.id IS NOT NULL THEN
|
||||
(flow_version.value -> 'flow_env' -> $3) #> $4
|
||||
ELSE
|
||||
(root_job.raw_flow -> 'flow_env' -> $3) #> $4
|
||||
END AS "flow_env: sqlx::types::Json<Box<RawValue>>"
|
||||
FROM
|
||||
v2_job current_job
|
||||
JOIN
|
||||
v2_job root_job ON root_job.id = COALESCE(current_job.root_job, current_job.flow_innermost_root_job, current_job.parent_job, current_job.id)
|
||||
AND root_job.workspace_id = current_job.workspace_id
|
||||
LEFT JOIN
|
||||
flow_version ON flow_version.id = root_job.runnable_id
|
||||
AND flow_version.path = root_job.runnable_path
|
||||
AND flow_version.workspace_id = root_job.workspace_id
|
||||
WHERE
|
||||
current_job.id = $1 AND
|
||||
current_job.workspace_id = $2"#,
|
||||
flow_job_id,
|
||||
w_id,
|
||||
var_name,
|
||||
json_path
|
||||
.as_ref()
|
||||
.map(|x| x.split(".").collect::<Vec<_>>())
|
||||
.unwrap_or_default() as Vec<&str>,
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?
|
||||
.map(|r| r.map(|x| x.0))
|
||||
.flatten()
|
||||
.unwrap_or_else(|| to_raw_value(&serde_json::Value::Null));
|
||||
|
||||
log_job_view(
|
||||
&db,
|
||||
Some(&authed),
|
||||
Some(&tokened.token),
|
||||
&w_id,
|
||||
&flow_job_id,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(flow_env))
|
||||
}
|
||||
|
||||
async fn compute_root_job_for_flow(db: &DB, w_id: &str, job_id: Uuid) -> error::Result<String> {
|
||||
let root_job = sqlx::query_scalar!(
|
||||
r#"SELECT COALESCE(root_job, flow_innermost_root_job, parent_job, id) as "root_job!" FROM v2_job WHERE id = $1 AND workspace_id = $2"#,
|
||||
@@ -3769,6 +3826,7 @@ async fn batch_rerun_handle_job(
|
||||
batch_rerun_compute_js_expression(expr.clone(), job.clone()).await?,
|
||||
);
|
||||
}
|
||||
InputTransform::Ai => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4226,7 +4284,8 @@ pub async fn run_script_by_path_inner(
|
||||
tag,
|
||||
timeout,
|
||||
None,
|
||||
None,
|
||||
// If the job has a parent job, set priority to 2 as it may be ran synchronously and block a current worker until being executed. Flow steps have a priority of 1 so this is higher.
|
||||
if run_query.parent_job.is_some() || run_query.root_job.is_some() { Some(2) } else { None },
|
||||
push_authed.as_ref(),
|
||||
false,
|
||||
None,
|
||||
|
||||
@@ -607,8 +607,10 @@ async fn create_script_internal<'c>(
|
||||
Ok(None)
|
||||
}
|
||||
(Some(p_hash), o) => {
|
||||
// Lock the parent row to prevent concurrent updates with the same parent_hash
|
||||
// This ensures linear lineage - only one script can have a given parent at a time
|
||||
if sqlx::query_scalar!(
|
||||
"SELECT 1 FROM script WHERE hash = $1 AND workspace_id = $2",
|
||||
"SELECT 1 FROM script WHERE hash = $1 AND workspace_id = $2 FOR UPDATE",
|
||||
p_hash.0,
|
||||
&w_id
|
||||
)
|
||||
|
||||
@@ -49,14 +49,7 @@ impl AuthedClient {
|
||||
"{}/api/w/{}/oidc/token/{}",
|
||||
self.base_internal_url, self.workspace, audience
|
||||
);
|
||||
let response = self.get(&url, vec![]).await?;
|
||||
match response.status().as_u16() {
|
||||
200u16 => Ok(response
|
||||
.json::<String>()
|
||||
.await
|
||||
.context("decoding oidc token as json string")?),
|
||||
_ => Err(anyhow::anyhow!(response.text().await.unwrap_or_default())),
|
||||
}
|
||||
make_basic_get_request(self, &url, None, Some("decoding oidc token as json string")).await
|
||||
}
|
||||
|
||||
pub async fn get_resource_value<T: DeserializeOwned>(&self, path: &str) -> anyhow::Result<T> {
|
||||
@@ -64,14 +57,7 @@ impl AuthedClient {
|
||||
"{}/api/w/{}/resources/get_value/{}",
|
||||
self.base_internal_url, self.workspace, path
|
||||
);
|
||||
let response = self.get(&url, vec![]).await?;
|
||||
match response.status().as_u16() {
|
||||
200u16 => Ok(response
|
||||
.json::<T>()
|
||||
.await
|
||||
.context("decoding resource value as json")?),
|
||||
_ => Err(anyhow::anyhow!(response.text().await.unwrap_or_default())),
|
||||
}
|
||||
make_basic_get_request(self, &url, None, Some("decoding resource value as json")).await
|
||||
}
|
||||
|
||||
pub async fn get_variable_value(&self, path: &str) -> anyhow::Result<String> {
|
||||
@@ -79,14 +65,7 @@ impl AuthedClient {
|
||||
"{}/api/w/{}/variables/get_value/{}",
|
||||
self.base_internal_url, self.workspace, path
|
||||
);
|
||||
let response = self.get(&url, vec![]).await?;
|
||||
match response.status().as_u16() {
|
||||
200u16 => Ok(response
|
||||
.json::<String>()
|
||||
.await
|
||||
.context("decoding variable value as json")?),
|
||||
_ => Err(anyhow::anyhow!(response.text().await.unwrap_or_default())),
|
||||
}
|
||||
make_basic_get_request(self, &url, None, Some("decoding variable value as json")).await
|
||||
}
|
||||
|
||||
pub async fn get_resource_value_interpolated<T: DeserializeOwned>(
|
||||
@@ -121,19 +100,34 @@ impl AuthedClient {
|
||||
"{}/api/w/{}/jobs_u/completed/get_result/{}",
|
||||
self.base_internal_url, self.workspace, path
|
||||
);
|
||||
let query = if let Some(json_path) = json_path {
|
||||
vec![("json_path", json_path)]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
let response = self.get(&url, query).await?;
|
||||
match response.status().as_u16() {
|
||||
200u16 => Ok(response
|
||||
.json::<T>()
|
||||
.await
|
||||
.context("decoding completed job result as json")?),
|
||||
_ => Err(anyhow::anyhow!(response.text().await.unwrap_or_default())),
|
||||
}
|
||||
let query = query_from_json_path(json_path);
|
||||
make_basic_get_request(
|
||||
self,
|
||||
&url,
|
||||
Some(query),
|
||||
Some("decoding completed job result as json"),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_flow_env_by_flow_job_id<T: DeserializeOwned>(
|
||||
&self,
|
||||
root_job_id: &str,
|
||||
var_name: &str,
|
||||
json_path: Option<String>,
|
||||
) -> anyhow::Result<T> {
|
||||
let url = format!(
|
||||
"{}/api/w/{}/jobs/flow_env_by_flow_job_id/{}/{}",
|
||||
self.base_internal_url, self.workspace, root_job_id, var_name
|
||||
);
|
||||
let query = query_from_json_path(json_path);
|
||||
make_basic_get_request(
|
||||
self,
|
||||
&url,
|
||||
Some(query),
|
||||
Some("decoding flow env variable as json"),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_result_by_id<T: DeserializeOwned>(
|
||||
@@ -146,19 +140,14 @@ impl AuthedClient {
|
||||
"{}/api/w/{}/jobs/result_by_id/{}/{}",
|
||||
self.base_internal_url, self.workspace, flow_job_id, node_id
|
||||
);
|
||||
let query = if let Some(json_path) = json_path {
|
||||
vec![("json_path", json_path)]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
let response = self.get(&url, query).await?;
|
||||
match response.status().as_u16() {
|
||||
200u16 => Ok(response
|
||||
.json::<T>()
|
||||
.await
|
||||
.context("decoding result by id as json")?),
|
||||
_ => Err(anyhow::anyhow!(response.text().await.unwrap_or_default())),
|
||||
}
|
||||
let query = query_from_json_path(json_path);
|
||||
make_basic_get_request(
|
||||
self,
|
||||
&url,
|
||||
Some(query),
|
||||
Some("decoding result by id as json"),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn upload_s3_file<S>(
|
||||
@@ -245,3 +234,33 @@ impl AuthedClient {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn query_from_json_path(json_path: Option<String>) -> Vec<(&'static str, String)> {
|
||||
json_path
|
||||
.map(|json_path| vec![("json_path", json_path)])
|
||||
.unwrap_or_else(|| Vec::new())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
async fn make_basic_get_request<T: DeserializeOwned>(
|
||||
client: &AuthedClient,
|
||||
url: &str,
|
||||
query: Option<Vec<(&'static str, String)>>,
|
||||
context: Option<&'static str>,
|
||||
) -> anyhow::Result<T> {
|
||||
let response = client
|
||||
.get(&url, query.unwrap_or_else(|| Vec::new()))
|
||||
.await?;
|
||||
|
||||
match response.status().as_u16() {
|
||||
200u16 => {
|
||||
let json_body = response
|
||||
.json::<T>()
|
||||
.await
|
||||
.context(context.unwrap_or("error decoding body as json"))?;
|
||||
Ok(json_body)
|
||||
}
|
||||
_ => Err(anyhow::anyhow!(response.text().await.unwrap_or_default())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,6 +192,8 @@ pub struct FlowValue {
|
||||
pub priority: Option<i16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub chat_input_enabled: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub flow_env: Option<HashMap<String, Box<RawValue>>>
|
||||
}
|
||||
|
||||
impl FlowValue {
|
||||
@@ -643,6 +645,7 @@ pub enum InputTransform {
|
||||
#[serde(default = "default_empty_string")]
|
||||
expr: String,
|
||||
},
|
||||
Ai,
|
||||
}
|
||||
|
||||
impl InputTransform {
|
||||
@@ -661,6 +664,7 @@ impl TryFrom<UntaggedInputTransform> for InputTransform {
|
||||
let input_transform = match value.type_.as_str() {
|
||||
"static" => InputTransform::new_static_value(value.value.unwrap_or_else(default_null)),
|
||||
"javascript" => InputTransform::new_javascript_expr(&value.expr.unwrap_or_default()),
|
||||
"ai" => InputTransform::Ai,
|
||||
other => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"got value: {other} for field `type`, expected value: `static` or `javascript`"
|
||||
@@ -816,8 +820,7 @@ pub struct McpToolValue {
|
||||
pub exclude_tools: Vec<String>,
|
||||
}
|
||||
|
||||
fn is_none_or_empty_vec<T>(expr: &Option<Vec<T>>) -> bool
|
||||
{
|
||||
fn is_none_or_empty_vec<T>(expr: &Option<Vec<T>>) -> bool {
|
||||
expr.is_none() || expr.as_ref().unwrap().is_empty()
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ pub mod job_metrics;
|
||||
pub mod job_s3_helpers_ee;
|
||||
#[cfg(feature = "parquet")]
|
||||
pub mod job_s3_helpers_oss;
|
||||
pub mod lockfiles;
|
||||
|
||||
#[cfg(feature = "private")]
|
||||
pub mod git_sync_ee;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
use crate::scripts::ScriptLang;
|
||||
|
||||
pub const LOCKFILE_GENERATED_FROM_REQUIREMENTS_TXT: &str = "# from requirements.txt";
|
||||
|
||||
pub fn is_generated_from_raw_requirements(
|
||||
lang: &Option<ScriptLang>,
|
||||
lock: &Option<String>,
|
||||
) -> bool {
|
||||
(lang.is_some_and(|v| v == ScriptLang::Bun)
|
||||
&& lock
|
||||
.as_ref()
|
||||
.is_some_and(|v| v.contains("generatedFromPackageJson")))
|
||||
|| (lang.is_some_and(|v| v == ScriptLang::Python3)
|
||||
&& lock
|
||||
.as_ref()
|
||||
.is_some_and(|v| v.starts_with(LOCKFILE_GENERATED_FROM_REQUIREMENTS_TXT)))
|
||||
}
|
||||
@@ -732,18 +732,32 @@ pub fn hash_script(ns: &NewScript) -> i64 {
|
||||
dh.finish() as i64
|
||||
}
|
||||
|
||||
pub struct ClonedScript {
|
||||
pub old_script: NewScript,
|
||||
pub new_hash: i64,
|
||||
}
|
||||
pub async fn clone_script<'c>(
|
||||
base_hash: ScriptHash,
|
||||
w_id: &str,
|
||||
deployment_message: Option<String>,
|
||||
tx: &mut sqlx::Transaction<'c, sqlx::Postgres>,
|
||||
) -> crate::error::Result<i64> {
|
||||
let s =
|
||||
sqlx::query_as::<_, Script>("SELECT * FROM script WHERE hash = $1 AND workspace_id = $2")
|
||||
.bind(base_hash.0)
|
||||
.bind(w_id)
|
||||
.fetch_one(&mut **tx)
|
||||
.await?;
|
||||
) -> crate::error::Result<ClonedScript> {
|
||||
let s = sqlx::query_as::<_, Script>(
|
||||
"SELECT * FROM script WHERE hash = $1 AND workspace_id = $2 AND archived = false FOR UPDATE",
|
||||
)
|
||||
.bind(base_hash.0)
|
||||
.bind(w_id)
|
||||
.fetch_optional(&mut **tx)
|
||||
.await?;
|
||||
|
||||
let s = if let Some(s) = s {
|
||||
s
|
||||
} else {
|
||||
return Err(crate::error::Error::NotFound(format!(
|
||||
"Non-archived script with hash {} not found",
|
||||
base_hash.0
|
||||
)));
|
||||
};
|
||||
|
||||
let ns = NewScript {
|
||||
path: s.path.clone(),
|
||||
@@ -819,5 +833,5 @@ pub async fn clone_script<'c>(
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
Ok(new_hash)
|
||||
Ok(ClonedScript { old_script: ns, new_hash })
|
||||
}
|
||||
|
||||
@@ -401,8 +401,7 @@ pub async fn get_reserved_variables(
|
||||
value,
|
||||
description: "Custom workspace environment variable".to_string(),
|
||||
is_custom: true,
|
||||
})
|
||||
).collect()
|
||||
})).collect()
|
||||
}
|
||||
|
||||
async fn get_cached_workspace_envs(conn: &Connection, w_id: &str) -> Vec<(String, String)> {
|
||||
@@ -439,7 +438,11 @@ async fn get_cached_workspace_envs(conn: &Connection, w_id: &str) -> Vec<(String
|
||||
custom_envs
|
||||
}
|
||||
|
||||
pub async fn get_variable_or_self(path: String, db: &DB, w_id: &str) -> crate::error::Result<String> {
|
||||
pub async fn get_variable_or_self(
|
||||
path: String,
|
||||
db: &DB,
|
||||
w_id: &str,
|
||||
) -> crate::error::Result<String> {
|
||||
if !path.starts_with("$var:") {
|
||||
return Ok(path);
|
||||
}
|
||||
|
||||
@@ -498,7 +498,6 @@ pub async fn store_pull_query(wc: &WorkerConfig) {
|
||||
|
||||
pub const TMP_DIR: &str = "/tmp/windmill";
|
||||
pub const TMP_LOGS_DIR: &str = concatcp!(TMP_DIR, "/logs");
|
||||
pub const TMP_MEMORY_DIR: &str = concatcp!(TMP_DIR, "/memory");
|
||||
|
||||
pub const HUB_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "hub");
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ use windmill_common::add_time;
|
||||
use windmill_common::auth::JobPerms;
|
||||
#[cfg(feature = "benchmark")]
|
||||
use windmill_common::bench::BenchmarkIter;
|
||||
use windmill_common::lockfiles::is_generated_from_raw_requirements;
|
||||
use windmill_common::jobs::{JobTriggerKind, EMAIL_ERROR_HANDLER_USER_EMAIL};
|
||||
use windmill_common::utils::{configure_client, now_from_db};
|
||||
use windmill_common::worker::{Connection, MIN_VERSION_SUPPORTS_DEBOUNCING, SCRIPT_TOKEN_EXPIRY};
|
||||
@@ -4328,6 +4329,7 @@ pub async fn push<'c, 'd>(
|
||||
skip_expr: None,
|
||||
preprocessor_module: None,
|
||||
chat_input_enabled: None,
|
||||
flow_env: None,
|
||||
};
|
||||
// this is a new flow being pushed, flow_status is set to flow_value:
|
||||
let flow_status: FlowStatus = FlowStatus::new(&flow_value);
|
||||
@@ -5660,15 +5662,20 @@ pub async fn preprocess_dependency_job(job: &mut PulledJob, db: &DB) -> error::R
|
||||
args.insert("base_hash".to_owned(), to_raw_value(&*base_hash))
|
||||
});
|
||||
|
||||
let new_hash = windmill_common::scripts::clone_script(
|
||||
let cloned_script = windmill_common::scripts::clone_script(
|
||||
base_hash,
|
||||
&job.workspace_id,
|
||||
deployment_message,
|
||||
&mut tx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
new_hash
|
||||
if is_generated_from_raw_requirements(&Some(cloned_script.old_script.language), &cloned_script.old_script.lock.map(|v| v.to_string())) {
|
||||
return Err(Error::BadRequest(format!(
|
||||
"Script at path {} is generated from raw requirements, not overriding",
|
||||
job.runnable_path()
|
||||
)));
|
||||
}
|
||||
cloned_script.new_hash
|
||||
}
|
||||
JobKind::FlowDependencies => {
|
||||
sqlx::query_scalar!(
|
||||
|
||||
@@ -32,7 +32,12 @@ const p = {
|
||||
const imports = transpiler.scanImports(code);
|
||||
for (const imp of imports) {
|
||||
if (imp.kind == "import-statement") {
|
||||
if (imp.path.startsWith(".") && !imp.path.endsWith(".ts")) {
|
||||
if (
|
||||
(imp.path.startsWith(".") ||
|
||||
imp.path.startsWith("/u/") ||
|
||||
imp.path.startsWith("/f/")) &&
|
||||
!imp.path.endsWith(".ts")
|
||||
) {
|
||||
code = code.replaceAll(imp.path, imp.path + ".ts");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,16 +322,23 @@ async fn execute_windmill_tool(
|
||||
// Evaluate each input transform and merge with AI-provided args
|
||||
for (key, transform) in input_transforms.iter() {
|
||||
// We skip static empty / null values, those are the one the AI will fill in
|
||||
if let InputTransform::Static { value } = transform {
|
||||
let val = value.get().trim();
|
||||
if val.is_empty() || val == "null" {
|
||||
match transform {
|
||||
InputTransform::Static { value } => {
|
||||
let val = value.get().trim();
|
||||
if val.is_empty() || val == "null" {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
InputTransform::Ai => {
|
||||
continue;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
let result = evaluate_input_transform::<Box<RawValue>>(
|
||||
transform,
|
||||
last_result.clone(),
|
||||
flow_inputs.clone(),
|
||||
None,
|
||||
Some(ctx.client),
|
||||
ctx.id_context.as_ref(),
|
||||
)
|
||||
|
||||
@@ -78,6 +78,7 @@ pub fn filter_schema_by_input_transforms(
|
||||
!val.is_empty() && val != "null"
|
||||
}
|
||||
InputTransform::Javascript { expr } => !expr.trim().is_empty(),
|
||||
InputTransform::Ai => false,
|
||||
};
|
||||
if is_completed {
|
||||
Some(key.clone())
|
||||
|
||||
@@ -405,7 +405,7 @@ pub async fn run_agent(
|
||||
.and_then(|fs| fs.memory_id)
|
||||
{
|
||||
// Read messages from memory
|
||||
match read_from_memory(&job.workspace_id, memory_id, step_id).await {
|
||||
match read_from_memory(db, &job.workspace_id, memory_id, step_id).await {
|
||||
Ok(Some(loaded_messages)) => {
|
||||
// Take the last n messages
|
||||
let start_idx = loaded_messages.len().saturating_sub(context_length);
|
||||
@@ -856,6 +856,7 @@ pub async fn run_agent(
|
||||
|
||||
if let Some(memory_id) = flow_context.flow_status.and_then(|fs| fs.memory_id) {
|
||||
if let Err(e) = write_to_memory(
|
||||
db,
|
||||
&job.workspace_id,
|
||||
memory_id,
|
||||
step_id,
|
||||
|
||||
@@ -1068,7 +1068,6 @@ pub fn build_http_client(timeout_duration: std::time::Duration) -> error::Result
|
||||
}
|
||||
|
||||
pub fn get_root_job_id(job: &MiniPulledJob) -> uuid::Uuid {
|
||||
// fallback to flow_innermost_root_job and parent_job as root_job is not set if equal to innermost root job or parent job
|
||||
job.root_job
|
||||
.or(job.flow_innermost_root_job)
|
||||
.or(job.parent_job)
|
||||
|
||||
@@ -165,10 +165,98 @@ impl NetPermissions for PermissionsContainer {
|
||||
#[cfg(feature = "deno_core")]
|
||||
pub struct OptAuthedClient(Option<AuthedClient>);
|
||||
|
||||
const FLOW_INPUT_PREFIX: &'static str = "flow_input";
|
||||
const ENV_KEY_PREFIX: &'static str = "flow_env";
|
||||
const DOT_PATTERN: &'static str = ".";
|
||||
const START_BRACKET_PATTERN: &'static str = "[\"";
|
||||
const END_BRACKET_PATTERN: &'static str = "\"]";
|
||||
|
||||
fn try_exact_property_access(
|
||||
expr: &str,
|
||||
flow_input: Option<&mappable_rc::Marc<HashMap<String, Box<RawValue>>>>,
|
||||
flow_env: Option<&HashMap<String, Box<RawValue>>>,
|
||||
) -> Option<Box<RawValue>> {
|
||||
let obj = if expr.starts_with(FLOW_INPUT_PREFIX) {
|
||||
Some((
|
||||
FLOW_INPUT_PREFIX,
|
||||
flow_input.as_ref().map(|obj| obj.as_ref()),
|
||||
))
|
||||
} else if expr.starts_with(ENV_KEY_PREFIX) {
|
||||
Some((ENV_KEY_PREFIX, flow_env))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some((prefix, obj)) = obj {
|
||||
let access_pattern_pos = prefix.len();
|
||||
let suffix = &expr[access_pattern_pos..];
|
||||
let maybe_key_name = if suffix.starts_with(DOT_PATTERN) {
|
||||
let key_name_pos = DOT_PATTERN.len();
|
||||
Some(&expr[key_name_pos..])
|
||||
} else if suffix.starts_with(START_BRACKET_PATTERN) {
|
||||
let key_name_pos = START_BRACKET_PATTERN.len();
|
||||
let suffix = &suffix[key_name_pos..];
|
||||
|
||||
let flow_arg_name = suffix
|
||||
.ends_with(END_BRACKET_PATTERN)
|
||||
.then(|| {
|
||||
let start_key_name_pos = access_pattern_pos + key_name_pos;
|
||||
let end_key_name_pos = expr.len() - END_BRACKET_PATTERN.len();
|
||||
&expr[start_key_name_pos..end_key_name_pos]
|
||||
})
|
||||
.filter(|s| s.len() > 0);
|
||||
flow_arg_name
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(key_name) = maybe_key_name {
|
||||
if let Some(key_value) = obj.and_then(|obj| obj.get(key_name)) {
|
||||
return Some(key_value.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
async fn handle_full_regex(
|
||||
captures: regex::Captures<'_>,
|
||||
authed_client: &AuthedClient,
|
||||
by_id: &IdContext,
|
||||
) -> anyhow::Result<Box<RawValue>> {
|
||||
let obj_name = captures.get(1).unwrap().as_str();
|
||||
let obj_key = captures.get(2).unwrap().as_str();
|
||||
let idx_o = captures.get(3).map(|y| y.as_str());
|
||||
let rest = captures.get(4).map(|y| y.as_str());
|
||||
let query = if let Some(idx) = idx_o {
|
||||
match rest {
|
||||
Some(rest) => Some(format!("{}{}", idx, rest)),
|
||||
None => Some(idx.to_string()),
|
||||
}
|
||||
} else {
|
||||
rest.map(|x| x.trim_start_matches('.').to_string())
|
||||
};
|
||||
|
||||
let result = if obj_name == "results" {
|
||||
authed_client
|
||||
.get_result_by_id(&by_id.flow_job.to_string(), obj_key, query)
|
||||
.await
|
||||
} else if obj_name == "flow_env" {
|
||||
authed_client
|
||||
.get_flow_env_by_flow_job_id(&by_id.flow_job.to_string(), obj_key, query)
|
||||
.await
|
||||
} else {
|
||||
unreachable!();
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
pub async fn eval_timeout(
|
||||
expr: String,
|
||||
transform_context: HashMap<String, Arc<Box<RawValue>>>,
|
||||
flow_input: Option<mappable_rc::Marc<HashMap<String, Box<RawValue>>>>,
|
||||
flow_env: Option<&HashMap<String, Box<RawValue>>>,
|
||||
authed_client: Option<&AuthedClient>,
|
||||
by_id: Option<&IdContext>,
|
||||
#[allow(unused_variables)] ctx: Option<Vec<(String, String)>>,
|
||||
@@ -180,21 +268,13 @@ pub async fn eval_timeout(
|
||||
expr,
|
||||
transform_context
|
||||
);
|
||||
for (k, v) in transform_context.iter() {
|
||||
if k == &expr {
|
||||
return Ok(v.as_ref().clone());
|
||||
}
|
||||
|
||||
if let Some(value) = transform_context.get(&expr) {
|
||||
return Ok(value.as_ref().to_owned());
|
||||
}
|
||||
|
||||
if expr.starts_with("flow_input.") || expr.starts_with("flow_input[") {
|
||||
if let Some(ref flow_input) = flow_input {
|
||||
for (k, v) in flow_input.iter() {
|
||||
if &format!("flow_input.{k}") == &expr || &format!("flow_input[\"{k}\"]") == &expr {
|
||||
// tracing::error!("FLOW_INPUT");
|
||||
return Ok(v.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(value) = try_exact_property_access(&expr, flow_input.as_ref(), flow_env) {
|
||||
return Ok(value);
|
||||
}
|
||||
|
||||
let p_ids = by_id.map(|x| {
|
||||
@@ -219,24 +299,8 @@ pub async fn eval_timeout(
|
||||
}
|
||||
|
||||
if let (Some(by_id), Some(authed_client)) = (by_id, authed_client) {
|
||||
if let Some((id, idx_o, rest)) = RE_FULL.captures(&expr).map(|x| {
|
||||
(
|
||||
x.get(1).unwrap().as_str(),
|
||||
x.get(2).map(|y| y.as_str()),
|
||||
x.get(3).map(|y| y.as_str()),
|
||||
)
|
||||
}) {
|
||||
let query = if let Some(idx) = idx_o {
|
||||
match rest {
|
||||
Some(rest) => Some(format!("{}{}", idx, rest)),
|
||||
None => Some(idx.to_string()),
|
||||
}
|
||||
} else {
|
||||
rest.map(|x| x.trim_start_matches('.').to_string())
|
||||
};
|
||||
return authed_client
|
||||
.get_result_by_id(&by_id.flow_job.to_string(), id, query)
|
||||
.await;
|
||||
if let Some(captures) = RE_FULL.captures(&expr) {
|
||||
return handle_full_regex(captures, authed_client, by_id).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,8 +335,8 @@ pub async fn eval_timeout(
|
||||
if by_id.is_some() && authed_client.is_some() {
|
||||
ops.push(op_get_result());
|
||||
ops.push(op_get_id());
|
||||
ops.push(op_get_flow_env());
|
||||
}
|
||||
|
||||
let ext = Extension { name: "js_eval", ops: ops.into(), ..Default::default() };
|
||||
let exts = vec![ext];
|
||||
// Use our snapshot to provision our new runtime
|
||||
@@ -307,13 +371,15 @@ pub async fn eval_timeout(
|
||||
let mut client = authed_client.clone();
|
||||
if let Some(client) = client.as_mut() {
|
||||
client.force_client = Some(
|
||||
configure_client(reqwest::ClientBuilder::new()
|
||||
.user_agent("windmill/beta")
|
||||
.danger_accept_invalid_certs(
|
||||
std::env::var("ACCEPT_INVALID_CERTS").is_ok(),
|
||||
))
|
||||
.build()
|
||||
.unwrap(),
|
||||
configure_client(
|
||||
reqwest::ClientBuilder::new()
|
||||
.user_agent("windmill/beta")
|
||||
.danger_accept_invalid_certs(
|
||||
std::env::var("ACCEPT_INVALID_CERTS").is_ok(),
|
||||
),
|
||||
)
|
||||
.build()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
op_state.put(OptAuthedClient(client));
|
||||
@@ -323,7 +389,7 @@ pub async fn eval_timeout(
|
||||
.into_iter()
|
||||
.filter(|(a, _)| context_keys.contains(a))
|
||||
.collect(),
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
sender
|
||||
@@ -379,10 +445,12 @@ fn replace_with_await(expr: String, fn_name: &str) -> String {
|
||||
}
|
||||
lazy_static! {
|
||||
static ref RE: Regex =
|
||||
Regex::new(r#"(?m)(?P<r>results(?:\?)?(?:(?:\.[a-zA-Z_0-9]+)|(?:\[\".*?\"\])))"#).unwrap();
|
||||
static ref RE_FULL: Regex =
|
||||
Regex::new(r"(?m)^results(?:\?)?\.([a-zA-Z_0-9]+)(?:\[(\d+)\])?((?:\.[a-zA-Z_0-9]+)+)?$")
|
||||
Regex::new(r#"(?m)(?P<r>(?:results|flow_env)(?:\?)?(?:(?:\.[a-zA-Z_0-9]+)|(?:\[\".*?\"\])))"#)
|
||||
.unwrap();
|
||||
static ref RE_FULL: Regex = Regex::new(
|
||||
r"(?m)^(results|flow_env)(?:\?)?\.([a-zA-Z_0-9]+)(?:\[(\d+)\])?((?:\.[a-zA-Z_0-9]+)+)?$"
|
||||
)
|
||||
.unwrap();
|
||||
static ref RE_PROXY: Regex =
|
||||
Regex::new(r"^(https?)://(([^:@\s]+):([^:@\s]+)@)?([^:@\s]+)(:(\d+))?$").unwrap();
|
||||
}
|
||||
@@ -453,6 +521,17 @@ const results = new Proxy({{}}, {{
|
||||
}}
|
||||
}});
|
||||
|
||||
async function flow_env_by_var_name(var_name) {{
|
||||
let root_job_id = "{}";
|
||||
return JSON.parse(await Deno.core.ops.op_get_flow_env(root_job_id, var_name, null));
|
||||
}}
|
||||
|
||||
const flow_env = new Proxy({{}}, {{
|
||||
get: function(target, name, receiver) {{
|
||||
return flow_env_by_var_name(name);
|
||||
}}
|
||||
}});
|
||||
|
||||
"#,
|
||||
by_id
|
||||
.steps_results
|
||||
@@ -469,6 +548,7 @@ const results = new Proxy({{}}, {{
|
||||
.join(","),
|
||||
by_id.previous_id,
|
||||
by_id.flow_job,
|
||||
by_id.flow_job
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
@@ -641,6 +721,33 @@ async fn op_resource(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
#[op2(async)]
|
||||
#[string]
|
||||
async fn op_get_flow_env(
|
||||
op_state: Rc<RefCell<OpState>>,
|
||||
#[string] root_job_id: String,
|
||||
#[string] var_name: String,
|
||||
#[string] json_path: Option<String>,
|
||||
) -> Result<Option<String>, deno_error::JsErrorBox> {
|
||||
let client = op_state.borrow().borrow::<OptAuthedClient>().0.clone();
|
||||
if let Some(client) = client {
|
||||
client
|
||||
.get_flow_env_by_flow_job_id::<Option<Box<RawValue>>>(
|
||||
&root_job_id,
|
||||
&var_name,
|
||||
json_path,
|
||||
)
|
||||
.await
|
||||
.map(|value| value.map(|val| val.get().to_string()))
|
||||
.map_err(|e| deno_error::JsErrorBox::generic(e.to_string()))
|
||||
} else {
|
||||
Err(deno_error::JsErrorBox::generic(
|
||||
"No client found in op state",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
pub struct TransformContext {
|
||||
pub envs: HashMap<String, Arc<Box<RawValue>>>,
|
||||
@@ -1318,7 +1425,7 @@ multiline template`";
|
||||
op_state.put(TransformContext { flow_input: None, envs: env.clone() })
|
||||
}
|
||||
|
||||
let res = eval_timeout(code.to_string(), env, None, None, None, None).await?;
|
||||
let res = eval_timeout(code.to_string(), env, None, None, None, None, None).await?;
|
||||
assert_eq!(res.get(), "2");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,72 +1,123 @@
|
||||
use crate::ai::types::OpenAIMessage;
|
||||
use std::path::PathBuf;
|
||||
use tokio::{fs, io::AsyncWriteExt};
|
||||
use uuid::Uuid;
|
||||
use windmill_common::worker::TMP_MEMORY_DIR;
|
||||
use windmill_common::{db::DB, error::Error};
|
||||
|
||||
/// Get the file path for storing memory for a specific AI agent step
|
||||
pub fn path_for(workspace_id: &str, conversation_id: Uuid, step_id: &str) -> PathBuf {
|
||||
PathBuf::from(TMP_MEMORY_DIR)
|
||||
.join(workspace_id)
|
||||
.join(conversation_id.to_string())
|
||||
.join(format!("{step_id}.json"))
|
||||
}
|
||||
pub const MAX_MEMORY_SIZE_BYTES: usize = 100_000; // 100KB per memory entry in database
|
||||
|
||||
/// Read messages from disk storage
|
||||
pub async fn read_from_disk(
|
||||
/// Read AI agent memory from database
|
||||
pub async fn read_from_db(
|
||||
db: &DB,
|
||||
workspace_id: &str,
|
||||
conversation_id: Uuid,
|
||||
step_id: &str,
|
||||
) -> anyhow::Result<Option<Vec<OpenAIMessage>>> {
|
||||
let path = path_for(workspace_id, conversation_id, step_id);
|
||||
if !fs::try_exists(&path).await? {
|
||||
return Ok(None);
|
||||
}
|
||||
) -> Result<Option<Vec<OpenAIMessage>>, Error> {
|
||||
let result = sqlx::query!(
|
||||
"SELECT messages FROM ai_agent_memory
|
||||
WHERE workspace_id = $1 AND conversation_id = $2 AND step_id = $3",
|
||||
workspace_id,
|
||||
conversation_id,
|
||||
step_id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
|
||||
let bytes = fs::read(&path).await?;
|
||||
let messages: Vec<OpenAIMessage> = serde_json::from_slice(&bytes)?;
|
||||
Ok(Some(messages))
|
||||
match result {
|
||||
Some(row) => {
|
||||
let messages: Vec<OpenAIMessage> = serde_json::from_value(row.messages)?;
|
||||
Ok(Some(messages))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Write messages to disk storage
|
||||
pub async fn write_to_disk(
|
||||
/// Write AI agent memory to database with size checking and truncation
|
||||
pub async fn write_to_db(
|
||||
db: &DB,
|
||||
workspace_id: &str,
|
||||
conversation_id: Uuid,
|
||||
step_id: &str,
|
||||
messages: &[OpenAIMessage],
|
||||
) -> anyhow::Result<()> {
|
||||
let path = path_for(workspace_id, conversation_id, step_id);
|
||||
|
||||
// Ensure parent directories exist
|
||||
if let Some(dir) = path.parent() {
|
||||
fs::create_dir_all(dir).await?;
|
||||
) -> Result<(), Error> {
|
||||
if messages.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Write atomically using a temporary file
|
||||
let tmp = path.with_extension("json.tmp");
|
||||
let mut f = fs::File::create(&tmp).await?;
|
||||
f.write_all(&serde_json::to_vec(messages)?).await?;
|
||||
f.flush().await?;
|
||||
drop(f);
|
||||
// Serialize messages and check size
|
||||
let mut messages_to_store = messages.to_vec();
|
||||
let mut json_value = serde_json::to_value(&messages_to_store)?;
|
||||
let size_bytes = json_value.to_string().len();
|
||||
|
||||
// Atomic rename
|
||||
fs::rename(tmp, &path).await?;
|
||||
// Truncate if necessary
|
||||
if size_bytes > MAX_MEMORY_SIZE_BYTES {
|
||||
tracing::warn!(
|
||||
"Memory size ({} bytes) exceeds limit ({} bytes) for workspace={} conversation={} step={}. Truncating messages. Use S3 storage in workspace settings to store full conversation history.",
|
||||
size_bytes,
|
||||
MAX_MEMORY_SIZE_BYTES,
|
||||
workspace_id,
|
||||
conversation_id,
|
||||
step_id
|
||||
);
|
||||
|
||||
messages_to_store = truncate_messages(messages, MAX_MEMORY_SIZE_BYTES)?;
|
||||
json_value = serde_json::to_value(&messages_to_store)?;
|
||||
}
|
||||
|
||||
// Insert or update using UPSERT
|
||||
sqlx::query!(
|
||||
"INSERT INTO ai_agent_memory (workspace_id, conversation_id, step_id, messages, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, NOW(), NOW())
|
||||
ON CONFLICT (workspace_id, conversation_id, step_id)
|
||||
DO UPDATE SET
|
||||
messages = EXCLUDED.messages,
|
||||
updated_at = NOW()",
|
||||
workspace_id,
|
||||
conversation_id,
|
||||
step_id,
|
||||
json_value
|
||||
)
|
||||
.execute(db)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete all memory for a conversation from disk storage
|
||||
pub async fn delete_conversation_from_disk(
|
||||
/// Delete all memory for a conversation from database
|
||||
pub async fn delete_conversation_from_db(
|
||||
db: &DB,
|
||||
workspace_id: &str,
|
||||
conversation_id: Uuid,
|
||||
) -> anyhow::Result<()> {
|
||||
let conversation_path = PathBuf::from(TMP_MEMORY_DIR)
|
||||
.join(workspace_id)
|
||||
.join(conversation_id.to_string());
|
||||
|
||||
if fs::try_exists(&conversation_path).await? {
|
||||
fs::remove_dir_all(&conversation_path).await?;
|
||||
}
|
||||
) -> Result<(), Error> {
|
||||
sqlx::query!(
|
||||
"DELETE FROM ai_agent_memory
|
||||
WHERE workspace_id = $1 AND conversation_id = $2",
|
||||
workspace_id,
|
||||
conversation_id
|
||||
)
|
||||
.execute(db)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Truncate messages to fit within the size limit
|
||||
fn truncate_messages(
|
||||
messages: &[OpenAIMessage],
|
||||
max_size_bytes: usize,
|
||||
) -> Result<Vec<OpenAIMessage>, Error> {
|
||||
let mut result = messages.to_vec();
|
||||
|
||||
// Keep removing oldest messages until we're under the threshold
|
||||
while !result.is_empty() {
|
||||
let test_json = serde_json::to_value(&result)?;
|
||||
let test_size = test_json.to_string().len();
|
||||
|
||||
if test_size <= max_size_bytes {
|
||||
break;
|
||||
}
|
||||
|
||||
// Remove the first (oldest) message
|
||||
result.remove(0);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
@@ -3,23 +3,27 @@
|
||||
pub use crate::memory_ee::*;
|
||||
|
||||
#[cfg(not(all(feature = "private", feature = "enterprise")))]
|
||||
use {crate::ai::types::OpenAIMessage, crate::memory_common, uuid::Uuid};
|
||||
use {crate::ai::types::OpenAIMessage, crate::memory_common, uuid::Uuid, windmill_common::db::DB};
|
||||
|
||||
/// Read AI agent memory from storage
|
||||
/// In OSS: always reads from disk
|
||||
/// In OSS: always reads from database
|
||||
#[cfg(not(all(feature = "private", feature = "enterprise")))]
|
||||
pub async fn read_from_memory(
|
||||
db: &DB,
|
||||
workspace_id: &str,
|
||||
conversation_id: Uuid,
|
||||
step_id: &str,
|
||||
) -> anyhow::Result<Option<Vec<OpenAIMessage>>> {
|
||||
memory_common::read_from_disk(workspace_id, conversation_id, step_id).await
|
||||
memory_common::read_from_db(db, workspace_id, conversation_id, step_id)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Database read failed: {e:?}"))
|
||||
}
|
||||
|
||||
/// Write AI agent memory to storage
|
||||
/// In OSS: always writes to disk
|
||||
/// In OSS: always writes to database
|
||||
#[cfg(not(all(feature = "private", feature = "enterprise")))]
|
||||
pub async fn write_to_memory(
|
||||
db: &DB,
|
||||
workspace_id: &str,
|
||||
conversation_id: Uuid,
|
||||
step_id: &str,
|
||||
@@ -29,15 +33,20 @@ pub async fn write_to_memory(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
memory_common::write_to_disk(workspace_id, conversation_id, step_id, messages).await
|
||||
memory_common::write_to_db(db, workspace_id, conversation_id, step_id, messages)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Database write failed: {e:?}"))
|
||||
}
|
||||
|
||||
/// Delete all memory for a conversation from storage
|
||||
/// In OSS: always deletes from disk
|
||||
/// In OSS: always deletes from database
|
||||
#[cfg(not(all(feature = "private", feature = "enterprise")))]
|
||||
pub async fn delete_conversation_memory(
|
||||
db: &DB,
|
||||
workspace_id: &str,
|
||||
conversation_id: Uuid,
|
||||
) -> anyhow::Result<()> {
|
||||
memory_common::delete_conversation_from_disk(workspace_id, conversation_id).await
|
||||
memory_common::delete_conversation_from_db(db, workspace_id, conversation_id)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Database delete failed: {e:?}"))
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use tokio::{fs::DirBuilder, process::Command, sync::RwLock};
|
||||
use uuid::Uuid;
|
||||
use windmill_common::{
|
||||
error::{self, Error},
|
||||
lockfiles::LOCKFILE_GENERATED_FROM_REQUIREMENTS_TXT,
|
||||
worker::Connection,
|
||||
};
|
||||
|
||||
@@ -22,7 +23,6 @@ use crate::{
|
||||
common::{start_child_process, OccupancyMetrics},
|
||||
handle_child::handle_child,
|
||||
python_executor::{PYTHON_PATH, UV_PATH},
|
||||
worker_lockfiles::LOCKFILE_GENERATED_FROM_REQUIREMENTS_TXT,
|
||||
HOME_ENV, INSTANCE_PYTHON_VERSION, PATH_ENV, PROXY_ENVS, PY_INSTALL_DIR, WIN_ENVS,
|
||||
};
|
||||
|
||||
|
||||
@@ -5,15 +5,12 @@ use windmill_common::{
|
||||
cache,
|
||||
error::{Error, Result},
|
||||
flows::{FlowModuleValue, FlowValue},
|
||||
scripts::ScriptLang,
|
||||
};
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::worker_lockfiles::{
|
||||
extract_relative_imports, is_generated_from_raw_requirements,
|
||||
LOCKFILE_GENERATED_FROM_REQUIREMENTS_TXT,
|
||||
};
|
||||
use crate::worker_lockfiles::extract_relative_imports;
|
||||
use windmill_common::lockfiles::is_generated_from_raw_requirements;
|
||||
|
||||
// TODO: To be removed in future versions
|
||||
lazy_static::lazy_static! {
|
||||
@@ -295,16 +292,7 @@ SELECT importer_node_id, imported_path
|
||||
let mut dmap = ScopedDependencyMap::fetch(w_id, &r.path, "script", db).await?;
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
if (smd.language.is_some_and(|v| v == ScriptLang::Bun)
|
||||
&& sd
|
||||
.lock
|
||||
.as_ref()
|
||||
.is_some_and(|v| v.contains("generatedFromPackageJson")))
|
||||
|| (smd.language.is_some_and(|v| v == ScriptLang::Python3)
|
||||
&& sd.lock.as_ref().is_some_and(|v| {
|
||||
v.starts_with(LOCKFILE_GENERATED_FROM_REQUIREMENTS_TXT)
|
||||
}))
|
||||
{
|
||||
if is_generated_from_raw_requirements(&smd.language, &sd.lock) {
|
||||
// if the lock file is generated from a package.json/requirements.txt, we need to clear the dependency map
|
||||
// because we do not want to have dependencies be recomputed automatically. Empty relative imports passed
|
||||
// to update_script_dependency_map will clear the dependency map.
|
||||
@@ -350,7 +338,7 @@ SELECT importer_node_id, imported_path
|
||||
match fmv {
|
||||
// Since we fetched from flow_version it is safe to assume all inline scripts are in form of RawScript.
|
||||
FlowModuleValue::RawScript { content, language, lock ,.. } => {
|
||||
if !is_generated_from_raw_requirements(Some(*language), lock) {
|
||||
if !is_generated_from_raw_requirements(&Some(*language), lock) {
|
||||
to_process.push((
|
||||
extract_relative_imports(
|
||||
content,
|
||||
|
||||
@@ -877,7 +877,7 @@ pub fn start_interactive_worker_shell(
|
||||
token,
|
||||
precomputed_agent_info: precomputed_bundle,
|
||||
} = extract_job_and_perms(job, &conn).await;
|
||||
|
||||
|
||||
let authed_client = AuthedClient::new(
|
||||
base_internal_url.to_owned(),
|
||||
job.workspace_id.clone(),
|
||||
@@ -886,7 +886,7 @@ pub fn start_interactive_worker_shell(
|
||||
);
|
||||
|
||||
let arc_job = Arc::new(job);
|
||||
|
||||
|
||||
let _ = handle_queued_job(
|
||||
arc_job.clone(),
|
||||
raw_code,
|
||||
|
||||
@@ -245,6 +245,7 @@ async fn evaluate_stop_after_all_iters_if(
|
||||
let stop_early_after_all_iters = compute_bool_from_expr(
|
||||
&stop_after_all_iters_if.expr,
|
||||
Marc::new(args),
|
||||
None,
|
||||
iters_result.clone(),
|
||||
None,
|
||||
None,
|
||||
@@ -427,17 +428,20 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await;
|
||||
let args =
|
||||
args.map(|flow_args| flow_args.map(|flow_args| flow_args.0).unwrap_or_default());
|
||||
|
||||
args
|
||||
}));
|
||||
|
||||
let from_result_to_args =
|
||||
|args: &Result<Option<Json<HashMap<String, Box<RawValue>>>>, sqlx::Error>| {
|
||||
let args = args.as_ref().map_err(|e| {
|
||||
Error::internal_err(format!("retrieval of args from state: {e:#}"))
|
||||
})?;
|
||||
|
||||
Ok::<_, Error>(args.clone().unwrap_or_default().0)
|
||||
};
|
||||
let from_result_to_args = |args: &Result<HashMap<String, Box<RawValue>>, sqlx::Error>| {
|
||||
let args = args
|
||||
.as_ref()
|
||||
.map_err(|e| Error::internal_err(format!("retrieval of args from state: {e:#}")))?;
|
||||
|
||||
Ok::<_, Error>(args.clone())
|
||||
};
|
||||
|
||||
let (mut stop_early, mut stop_early_err_msg, mut skip_if_stop_early, continue_on_error) =
|
||||
if stop_early_override.is_some()
|
||||
@@ -469,9 +473,11 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
_ => None,
|
||||
};
|
||||
let args = from_result_to_args(args.as_ref().await.get_ref())?;
|
||||
|
||||
compute_bool_from_expr(
|
||||
&expr,
|
||||
Marc::new(args),
|
||||
None,
|
||||
result.clone(),
|
||||
all_iters,
|
||||
None,
|
||||
@@ -736,6 +742,7 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
&mut stop_early_err_msg,
|
||||
&mut nresult,
|
||||
args,
|
||||
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -924,6 +931,7 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
.and_then(|x| x.stop_after_all_iters_if.as_ref())
|
||||
{
|
||||
let args = from_result_to_args(args.as_ref().await.get_ref())?;
|
||||
|
||||
evaluate_stop_after_all_iters_if(
|
||||
db,
|
||||
stop_after_all_iters_if,
|
||||
@@ -1016,6 +1024,7 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
&old_status.retry,
|
||||
result.clone(),
|
||||
Marc::new(args),
|
||||
None,
|
||||
Some(client),
|
||||
)
|
||||
.await?
|
||||
@@ -1334,6 +1343,7 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
&old_status.retry,
|
||||
result.clone(),
|
||||
Marc::new(args),
|
||||
None,
|
||||
Some(client),
|
||||
)
|
||||
.await?
|
||||
@@ -1824,6 +1834,7 @@ async fn evaluate_retry(
|
||||
status: &RetryStatus,
|
||||
result: Arc<Box<RawValue>>,
|
||||
flow_args: Marc<HashMap<String, Box<RawValue>>>,
|
||||
flow_env: Option<&HashMap<String, Box<RawValue>>>,
|
||||
client: Option<&AuthedClient>,
|
||||
) -> anyhow::Result<Option<(u32, Duration)>> {
|
||||
if status.fail_count > MAX_RETRY_ATTEMPTS {
|
||||
@@ -1834,6 +1845,7 @@ async fn evaluate_retry(
|
||||
let should_retry = compute_bool_from_expr(
|
||||
&retry_if.expr,
|
||||
flow_args,
|
||||
flow_env,
|
||||
result,
|
||||
None,
|
||||
None,
|
||||
@@ -1857,6 +1869,7 @@ async fn evaluate_retry(
|
||||
async fn compute_bool_from_expr(
|
||||
expr: &str,
|
||||
flow_args: Marc<HashMap<String, Box<RawValue>>>,
|
||||
flow_env: Option<&HashMap<String, Box<RawValue>>>,
|
||||
result: Arc<Box<RawValue>>,
|
||||
all_iters: Option<Arc<Box<RawValue>>>,
|
||||
by_id: Option<&IdContext>,
|
||||
@@ -1881,6 +1894,7 @@ async fn compute_bool_from_expr(
|
||||
format!("Boolean({expr})"),
|
||||
context,
|
||||
Some(flow_args),
|
||||
flow_env,
|
||||
client,
|
||||
by_id,
|
||||
ctx,
|
||||
@@ -1906,15 +1920,13 @@ pub async fn evaluate_input_transform<T>(
|
||||
transform: &InputTransform,
|
||||
last_result: Arc<Box<RawValue>>,
|
||||
flow_args: Option<Marc<HashMap<String, Box<RawValue>>>>,
|
||||
flow_env: Option<&HashMap<String, Box<RawValue>>>,
|
||||
authed_client: Option<&AuthedClient>,
|
||||
by_id: Option<&IdContext>,
|
||||
) -> error::Result<T>
|
||||
where
|
||||
T: for<'de> serde::Deserialize<'de> + Send,
|
||||
T: for<'de> serde::Deserialize<'de> + Send + Default,
|
||||
{
|
||||
let mut context = HashMap::with_capacity(2);
|
||||
context.insert("result".to_string(), last_result.clone());
|
||||
context.insert("previous_result".to_string(), last_result.clone());
|
||||
match transform {
|
||||
InputTransform::Static { value } => serde_json::from_str(value.get()).map_err(|e| {
|
||||
Error::ExecutionErr(format!(
|
||||
@@ -1923,10 +1935,14 @@ where
|
||||
))
|
||||
}),
|
||||
InputTransform::Javascript { expr } => {
|
||||
let mut context = HashMap::with_capacity(2);
|
||||
context.insert("result".to_string(), last_result.clone());
|
||||
context.insert("previous_result".to_string(), last_result.clone());
|
||||
let result = eval_timeout(
|
||||
expr.to_string(),
|
||||
context,
|
||||
flow_args,
|
||||
flow_env,
|
||||
authed_client,
|
||||
by_id,
|
||||
None,
|
||||
@@ -1947,6 +1963,7 @@ where
|
||||
))
|
||||
})
|
||||
}
|
||||
InputTransform::Ai => Ok(T::default()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1954,6 +1971,7 @@ where
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
async fn transform_input(
|
||||
flow_args: Marc<HashMap<String, Box<RawValue>>>,
|
||||
flow_env: Option<&HashMap<String, Box<RawValue>>>,
|
||||
last_result: Arc<Box<RawValue>>,
|
||||
input_transforms: &HashMap<String, InputTransform>,
|
||||
resumes: Arc<Box<RawValue>>,
|
||||
@@ -1995,6 +2013,7 @@ async fn transform_input(
|
||||
expr.to_string(),
|
||||
env.clone(),
|
||||
Some(flow_args.clone()),
|
||||
flow_env,
|
||||
Some(client),
|
||||
Some(by_id),
|
||||
None,
|
||||
@@ -2007,6 +2026,7 @@ async fn transform_input(
|
||||
})?;
|
||||
mapped.insert(key.to_string(), v);
|
||||
}
|
||||
InputTransform::Ai => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2067,6 +2087,7 @@ pub async fn handle_flow(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut rec = PushNextFlowJobRec { flow_job: flow_job, status: status };
|
||||
loop {
|
||||
let PushNextFlowJobRec { flow_job, status } = rec;
|
||||
@@ -2202,13 +2223,14 @@ async fn push_next_flow_job(
|
||||
// tracing::error!("status_module: {status_module:#?}");
|
||||
|
||||
let fj: mappable_rc::Marc<MiniPulledJob> = flow_job.clone().into();
|
||||
let arc_flow_job_args: Marc<HashMap<String, Box<RawValue>>> = Marc::map(fj, |x| {
|
||||
if let Some(args) = &x.args {
|
||||
&args.0
|
||||
} else {
|
||||
&EHM
|
||||
}
|
||||
});
|
||||
let arc_flow_job_args: Marc<HashMap<String, Box<RawValue>>> =
|
||||
Marc::map(fj, |x: &MiniPulledJob| {
|
||||
if let Some(args) = &x.args {
|
||||
&args.0
|
||||
} else {
|
||||
&EHM
|
||||
}
|
||||
});
|
||||
|
||||
// if this is an empty module without preprocessor of if the module has already been completed, successfully, update the parent flow
|
||||
if (flow.modules.is_empty() && !step.is_preprocessor_step())
|
||||
@@ -2298,6 +2320,7 @@ async fn push_next_flow_job(
|
||||
let skip = compute_bool_from_expr(
|
||||
&skip_expr,
|
||||
arc_flow_job_args.clone(),
|
||||
flow.flow_env.as_ref(),
|
||||
Arc::new(to_raw_value(&json!("{}"))),
|
||||
None,
|
||||
None,
|
||||
@@ -2419,6 +2442,7 @@ async fn push_next_flow_job(
|
||||
expr.to_string(),
|
||||
context,
|
||||
Some(arc_flow_job_args.clone()),
|
||||
flow.flow_env.as_ref(),
|
||||
None,
|
||||
None,
|
||||
None
|
||||
@@ -2441,6 +2465,9 @@ async fn push_next_flow_job(
|
||||
)));
|
||||
}
|
||||
}
|
||||
InputTransform::Ai => {
|
||||
user_groups_required = Vec::new();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
user_groups_required = Vec::new();
|
||||
@@ -2676,6 +2703,7 @@ async fn push_next_flow_job(
|
||||
&input_transform,
|
||||
arc_last_job_result.clone(),
|
||||
Some(arc_flow_job_args.clone()),
|
||||
flow.flow_env.as_ref(),
|
||||
Some(client),
|
||||
None,
|
||||
)
|
||||
@@ -2713,6 +2741,7 @@ async fn push_next_flow_job(
|
||||
&status.retry,
|
||||
arc_last_job_result.clone(),
|
||||
arc_flow_job_args.clone(),
|
||||
flow.flow_env.as_ref(),
|
||||
Some(client),
|
||||
)
|
||||
.await?
|
||||
@@ -2802,6 +2831,7 @@ async fn push_next_flow_job(
|
||||
compute_bool_from_expr(
|
||||
&skip_if.expr,
|
||||
arc_flow_job_args.clone(),
|
||||
flow.flow_env.as_ref(),
|
||||
arc_last_job_result.clone(),
|
||||
None,
|
||||
Some(&idcontext),
|
||||
@@ -2893,6 +2923,7 @@ async fn push_next_flow_job(
|
||||
};
|
||||
transform_input(
|
||||
arc_flow_job_args.clone(),
|
||||
flow.flow_env.as_ref(),
|
||||
arc_last_job_result.clone(),
|
||||
input_transforms,
|
||||
resumes.clone(),
|
||||
@@ -2919,6 +2950,7 @@ async fn push_next_flow_job(
|
||||
let next_flow_transform = compute_next_flow_transform(
|
||||
arc_flow_job_args.clone(),
|
||||
arc_last_job_result.clone(),
|
||||
flow.flow_env.as_ref(),
|
||||
&flow_job,
|
||||
&flow,
|
||||
transform_context,
|
||||
@@ -3067,6 +3099,7 @@ async fn push_next_flow_job(
|
||||
.await?;
|
||||
let ti = transform_input(
|
||||
Marc::new(args),
|
||||
flow.flow_env.as_ref(),
|
||||
arc_last_job_result.clone(),
|
||||
input_transforms,
|
||||
resumes.clone(),
|
||||
@@ -3117,6 +3150,7 @@ async fn push_next_flow_job(
|
||||
.await?;
|
||||
let ti = transform_input(
|
||||
Marc::new(hm),
|
||||
flow.flow_env.as_ref(),
|
||||
arc_last_job_result.clone(),
|
||||
input_transforms,
|
||||
resumes.clone(),
|
||||
@@ -3220,6 +3254,7 @@ async fn push_next_flow_job(
|
||||
timeout_transform,
|
||||
arc_last_job_result.clone(),
|
||||
Some(arc_flow_job_args.clone()),
|
||||
flow.flow_env.as_ref(),
|
||||
Some(client),
|
||||
Some(&ctx),
|
||||
)
|
||||
@@ -3299,6 +3334,7 @@ async fn push_next_flow_job(
|
||||
parallelism_transform,
|
||||
arc_last_job_result.clone(),
|
||||
Some(arc_flow_job_args.clone()),
|
||||
flow.flow_env.as_ref(),
|
||||
Some(client),
|
||||
Some(&ctx),
|
||||
)
|
||||
@@ -3756,6 +3792,7 @@ pub fn get_path(flow_job: &MiniPulledJob, status: &FlowStatus, module: &FlowModu
|
||||
async fn compute_next_flow_transform(
|
||||
arc_flow_job_args: Marc<HashMap<String, Box<RawValue>>>,
|
||||
arc_last_job_result: Arc<Box<RawValue>>,
|
||||
flow_env: Option<&HashMap<String, Box<RawValue>>>,
|
||||
flow_job: &MiniPulledJob,
|
||||
flow: &FlowValue,
|
||||
by_id: Option<IdContext>,
|
||||
@@ -3974,6 +4011,7 @@ async fn compute_next_flow_transform(
|
||||
resume,
|
||||
approvers,
|
||||
arc_flow_job_args,
|
||||
flow_env,
|
||||
client,
|
||||
¶llel,
|
||||
)
|
||||
@@ -4069,6 +4107,7 @@ async fn compute_next_flow_transform(
|
||||
let pred = compute_bool_from_expr(
|
||||
&b.expr,
|
||||
arc_flow_job_args.clone(),
|
||||
flow.flow_env.as_ref(),
|
||||
arc_last_job_result.clone(),
|
||||
None,
|
||||
Some(&idcontext),
|
||||
@@ -4333,6 +4372,7 @@ async fn next_forloop_status(
|
||||
resume: Arc<Box<RawValue>>,
|
||||
approvers: Arc<Box<RawValue>>,
|
||||
arc_flow_job_args: Marc<HashMap<String, Box<RawValue>>>,
|
||||
flow_env: Option<&HashMap<String, Box<RawValue>>>,
|
||||
client: &AuthedClient,
|
||||
parallel: &bool,
|
||||
) -> Result<ForLoopStatus, Error> {
|
||||
@@ -4348,6 +4388,11 @@ async fn next_forloop_status(
|
||||
/* Iterator is an InputTransform, evaluate it into an array. */
|
||||
let itered_raw = match iterator {
|
||||
InputTransform::Static { value } => to_raw_value(value),
|
||||
InputTransform::Ai => {
|
||||
return Err(Error::ExecutionErr(format!(
|
||||
"AI input transform not supported for iterator"
|
||||
)))?
|
||||
}
|
||||
InputTransform::Javascript { expr } => {
|
||||
let mut context = HashMap::with_capacity(5);
|
||||
context.insert("result".to_string(), arc_last_job_result.clone());
|
||||
@@ -4360,6 +4405,7 @@ async fn next_forloop_status(
|
||||
expr.to_string(),
|
||||
context,
|
||||
Some(arc_flow_job_args),
|
||||
flow_env,
|
||||
Some(client),
|
||||
Some(&by_id),
|
||||
None,
|
||||
@@ -4423,12 +4469,18 @@ async fn next_forloop_status(
|
||||
expr.to_string(),
|
||||
context,
|
||||
Some(arc_flow_job_args),
|
||||
flow_env,
|
||||
Some(client),
|
||||
Some(&by_id),
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
InputTransform::Ai => {
|
||||
return Err(Error::ExecutionErr(format!(
|
||||
"AI input transform not supported for iterator"
|
||||
)))?
|
||||
}
|
||||
};
|
||||
serde_json::from_str::<Vec<Box<RawValue>>>(itered_raw.get()).map_err(
|
||||
|not_array| {
|
||||
|
||||
@@ -20,6 +20,7 @@ use windmill_common::error::Error;
|
||||
use windmill_common::error::Result;
|
||||
use windmill_common::flows::{FlowModule, FlowModuleValue, FlowNodeId};
|
||||
use windmill_common::jobs::JobPayload;
|
||||
use windmill_common::lockfiles::is_generated_from_raw_requirements;
|
||||
use windmill_common::scripts::ScriptHash;
|
||||
use windmill_common::utils::WarnAfterExt;
|
||||
#[cfg(feature = "python")]
|
||||
@@ -373,15 +374,7 @@ pub async fn process_relative_imports(
|
||||
db,
|
||||
)
|
||||
.await?;
|
||||
if (script_lang.is_some_and(|v| v == ScriptLang::Bun)
|
||||
&& lock
|
||||
.as_ref()
|
||||
.is_some_and(|v| v.contains("generatedFromPackageJson")))
|
||||
|| (script_lang.is_some_and(|v| v == ScriptLang::Python3)
|
||||
&& lock
|
||||
.as_ref()
|
||||
.is_some_and(|v| v.starts_with(LOCKFILE_GENERATED_FROM_REQUIREMENTS_TXT)))
|
||||
{
|
||||
if is_generated_from_raw_requirements(script_lang, &lock) {
|
||||
// if the lock file is generated from a package.json/requirements.txt, we need to clear the dependency map
|
||||
// because we do not want to have dependencies be recomputed automatically. Empty relative imports passed
|
||||
// to update_script_dependency_map will clear the dependency map.
|
||||
@@ -445,17 +438,6 @@ pub async fn process_relative_imports(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_generated_from_raw_requirements(lang: Option<ScriptLang>, lock: &Option<String>) -> bool {
|
||||
(lang.is_some_and(|v| v == ScriptLang::Bun)
|
||||
&& lock
|
||||
.as_ref()
|
||||
.is_some_and(|v| v.contains("generatedFromPackageJson")))
|
||||
|| (lang.is_some_and(|v| v == ScriptLang::Python3)
|
||||
&& lock
|
||||
.as_ref()
|
||||
.is_some_and(|v| v.starts_with(LOCKFILE_GENERATED_FROM_REQUIREMENTS_TXT)))
|
||||
}
|
||||
|
||||
pub async fn trigger_dependents_to_recompute_dependencies(
|
||||
w_id: &str,
|
||||
script_path: &str,
|
||||
@@ -1450,7 +1432,7 @@ async fn lock_modules<'c>(
|
||||
|
||||
if let Some(locks_to_reload) = locks_to_reload {
|
||||
if !locks_to_reload.contains(&e.id) {
|
||||
if !is_generated_from_raw_requirements(Some(language), &lock) {
|
||||
if !is_generated_from_raw_requirements(&Some(language), &lock) {
|
||||
let relative_imports = get_imports();
|
||||
tx = dependency_map
|
||||
.patch(relative_imports.clone(), e.id.clone(), tx)
|
||||
@@ -1463,7 +1445,7 @@ async fn lock_modules<'c>(
|
||||
if lock.as_ref().is_some_and(|x| !x.trim().is_empty()) {
|
||||
let skip_creating_new_lock = skip_creating_new_lock(&language, &content);
|
||||
if skip_creating_new_lock {
|
||||
if !is_generated_from_raw_requirements(Some(language), &lock) {
|
||||
if !is_generated_from_raw_requirements(&Some(language), &lock) {
|
||||
let relative_imports = get_imports();
|
||||
tx = dependency_map
|
||||
.patch(relative_imports.clone(), e.id.clone(), tx)
|
||||
@@ -2582,8 +2564,6 @@ async fn ansible_dep(
|
||||
serde_json::to_string(&ansible_lockfile).map_err(|e| e.into())
|
||||
}
|
||||
|
||||
pub const LOCKFILE_GENERATED_FROM_REQUIREMENTS_TXT: &str = "# from requirements.txt";
|
||||
|
||||
async fn capture_dependency_job(
|
||||
job_id: &Uuid,
|
||||
job_language: &ScriptLang,
|
||||
@@ -2672,7 +2652,11 @@ async fn capture_dependency_job(
|
||||
.await
|
||||
.map(|res| {
|
||||
if raw_deps {
|
||||
format!("{}\n{}", LOCKFILE_GENERATED_FROM_REQUIREMENTS_TXT, res)
|
||||
format!(
|
||||
"{}\n{}",
|
||||
windmill_common::lockfiles::LOCKFILE_GENERATED_FROM_REQUIREMENTS_TXT,
|
||||
res
|
||||
)
|
||||
} else {
|
||||
res
|
||||
}
|
||||
|
||||
+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.573.3";
|
||||
export const VERSION = "v1.573.5";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ export {
|
||||
// }
|
||||
// });
|
||||
|
||||
export const VERSION = "1.573.3";
|
||||
export const VERSION = "1.573.5";
|
||||
|
||||
export const WM_FORK_PREFIX = "wm-fork";
|
||||
|
||||
|
||||
@@ -73,8 +73,6 @@ services:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- worker_dependency_cache:/tmp/windmill/cache
|
||||
- worker_logs:/tmp/windmill/logs
|
||||
# for AI agent memory
|
||||
- worker_memory:/tmp/windmill/memory
|
||||
|
||||
logging: *default-logging
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.573.3",
|
||||
"version": "1.573.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill-components",
|
||||
"version": "1.573.3",
|
||||
"version": "1.573.5",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.573.3",
|
||||
"version": "1.573.5",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
|
||||
@@ -50,6 +50,7 @@ export interface SchemaProperty {
|
||||
oneOf?: SchemaProperty[]
|
||||
originalType?: string
|
||||
disabled?: boolean
|
||||
'x-no-s3-storage-workspace-warning'?: string
|
||||
}
|
||||
|
||||
export interface ModalSchemaProperty {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
import { DollarSign, Plus, X, Check, Loader2, ExternalLink } from 'lucide-svelte'
|
||||
import { createEventDispatcher, onDestroy, onMount, tick, untrack } from 'svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { Button, SecondsInput } from './common'
|
||||
import { Alert, Button, SecondsInput } from './common'
|
||||
import FieldHeader from './FieldHeader.svelte'
|
||||
import type ItemPicker from './ItemPicker.svelte'
|
||||
import ObjectResourceInput from './ObjectResourceInput.svelte'
|
||||
@@ -126,6 +126,7 @@
|
||||
| undefined)
|
||||
| undefined
|
||||
workspace?: string | undefined
|
||||
s3StorageConfigured?: boolean
|
||||
actions?: import('svelte').Snippet
|
||||
innerBottomSnippet?: import('svelte').Snippet
|
||||
fieldHeaderActions?: import('svelte').Snippet
|
||||
@@ -185,6 +186,7 @@
|
||||
appPath = undefined,
|
||||
computeS3ForceViewerPolicies = undefined,
|
||||
workspace = undefined,
|
||||
s3StorageConfigured = true,
|
||||
actions,
|
||||
innerBottomSnippet,
|
||||
fieldHeaderActions,
|
||||
@@ -1455,6 +1457,15 @@
|
||||
{:else if !noMargin}
|
||||
<div class="mb-2"></div>
|
||||
{/if}
|
||||
|
||||
{#if !s3StorageConfigured && extra['x-no-s3-storage-workspace-warning']}
|
||||
<Alert
|
||||
type="warning"
|
||||
title={extra['x-no-s3-storage-workspace-warning']}
|
||||
size="xs"
|
||||
titleClass="text-2xs"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
channels?: ChannelItem[]
|
||||
teamId?: string
|
||||
onError?: (error: Error) => void
|
||||
onselectedchannelchange?: (channel: ChannelItem | undefined) => void
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -28,34 +29,56 @@
|
||||
minWidth = '160px',
|
||||
channels = undefined,
|
||||
teamId,
|
||||
onError
|
||||
onError,
|
||||
onselectedchannelchange
|
||||
}: Props = $props()
|
||||
|
||||
let isFetching = $state(false)
|
||||
let searchResults = $state<ChannelItem[]>([])
|
||||
|
||||
// Only enable search mode if no channels are provided AND teamId is provided
|
||||
let selectedChannelId = $state<string | undefined>(selectedChannel?.channel_id)
|
||||
|
||||
const searchMode = !channels && !!teamId
|
||||
|
||||
// Determine which channels to show: provided channels or search results
|
||||
// In search mode, include the selected channel if it exists
|
||||
let displayChannels = $derived(() => {
|
||||
const baseChannels = channels || searchResults;
|
||||
if (searchMode && selectedChannel && !baseChannels.find(c => c.channel_id === selectedChannel?.channel_id)) {
|
||||
return [selectedChannel, ...baseChannels];
|
||||
let displayChannels = $derived.by(() => {
|
||||
const baseChannels = channels || searchResults
|
||||
if (selectedChannel && !baseChannels.find(c => c.channel_id === selectedChannel?.channel_id)) {
|
||||
return [selectedChannel, ...baseChannels]
|
||||
}
|
||||
return baseChannels
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
const newChannel = selectedChannelId
|
||||
? displayChannels.find(c => c.channel_id === selectedChannelId)
|
||||
: undefined
|
||||
|
||||
if (newChannel?.channel_id !== selectedChannel?.channel_id) {
|
||||
selectedChannel = newChannel
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (selectedChannel?.channel_id !== selectedChannelId) {
|
||||
selectedChannelId = selectedChannel?.channel_id
|
||||
}
|
||||
})
|
||||
|
||||
let previousChannelId = $state<string | undefined>(undefined)
|
||||
|
||||
$effect(() => {
|
||||
if (selectedChannel?.channel_id !== previousChannelId) {
|
||||
previousChannelId = selectedChannel?.channel_id
|
||||
onselectedchannelchange?.(selectedChannel)
|
||||
}
|
||||
return baseChannels;
|
||||
})
|
||||
|
||||
// Create separate filter text for search mode
|
||||
let searchFilterText = $state('')
|
||||
|
||||
// Debounced search function
|
||||
const debouncedSearch = debounce(async (query: string) => {
|
||||
await searchChannels(query)
|
||||
}, 500)
|
||||
|
||||
// Watch for search filter text changes (only in search mode)
|
||||
$effect(() => {
|
||||
if (searchMode) {
|
||||
if (searchFilterText.length >= 1) {
|
||||
@@ -97,37 +120,27 @@
|
||||
{#if searchMode}
|
||||
<Select
|
||||
containerStyle={'min-width: ' + minWidth}
|
||||
items={searchFilterText.length >= 1 || (searchFilterText.length === 0 && selectedChannel) ? displayChannels().filter(channel => channel.channel_id && channel.channel_name).map((channel) => ({
|
||||
items={displayChannels.filter(channel => channel.channel_id && channel.channel_name).map((channel) => ({
|
||||
label: channel.channel_name ?? 'Unknown Channel',
|
||||
value: channel.channel_id ?? ''
|
||||
})) : []}
|
||||
}))}
|
||||
placeholder={isFetching ? "Searching..." : (teamId ? "Search channels..." : "Select a team first")}
|
||||
clearable
|
||||
disabled={disabled || isFetching || !teamId}
|
||||
bind:filterText={searchFilterText}
|
||||
bind:value={
|
||||
() => selectedChannel?.channel_id,
|
||||
(value) => {
|
||||
selectedChannel = value ? displayChannels().find((channel) => channel.channel_id === value) : undefined
|
||||
}
|
||||
}
|
||||
bind:value={selectedChannelId}
|
||||
/>
|
||||
{:else}
|
||||
<Select
|
||||
containerStyle={'min-width: ' + minWidth}
|
||||
items={displayChannels().filter(channel => channel.channel_id && channel.channel_name).map((channel) => ({
|
||||
items={displayChannels.filter(channel => channel.channel_id && channel.channel_name).map((channel) => ({
|
||||
label: channel.channel_name ?? 'Unknown Channel',
|
||||
value: channel.channel_id ?? ''
|
||||
}))}
|
||||
{placeholder}
|
||||
clearable
|
||||
disabled={disabled || displayChannels().length === 0}
|
||||
bind:value={
|
||||
() => selectedChannel?.channel_id,
|
||||
(value) => {
|
||||
selectedChannel = value ? displayChannels().find((channel) => channel.channel_id === value) : undefined
|
||||
}
|
||||
}
|
||||
disabled={disabled || displayChannels.length === 0}
|
||||
bind:value={selectedChannelId}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
hideConnectButton?: boolean
|
||||
} = $props()
|
||||
|
||||
let selectedTeam: TeamItem | undefined = undefined
|
||||
let selectedTeam: TeamItem | undefined = $state(undefined)
|
||||
|
||||
async function connectTeams() {
|
||||
if (!selectedTeam) return
|
||||
|
||||
@@ -496,19 +496,16 @@
|
||||
minWidth="200px"
|
||||
placeholder="Search Teams channels"
|
||||
teamId={teams_team_id}
|
||||
bind:selectedChannel={
|
||||
() =>
|
||||
handlerExtraArgs['channel']
|
||||
? {
|
||||
channel_id: handlerExtraArgs['channel'],
|
||||
channel_name: handlerExtraArgs['channel_name']
|
||||
}
|
||||
: undefined,
|
||||
(channel) => {
|
||||
handlerExtraArgs['channel'] = channel?.channel_id
|
||||
handlerExtraArgs['channel_name'] = channel?.channel_name
|
||||
}
|
||||
}
|
||||
selectedChannel={handlerExtraArgs['channel']
|
||||
? {
|
||||
channel_id: handlerExtraArgs['channel'],
|
||||
channel_name: handlerExtraArgs['channel_name']
|
||||
}
|
||||
: undefined}
|
||||
onselectedchannelchange={(channel) => {
|
||||
handlerExtraArgs['channel'] = channel?.channel_id
|
||||
handlerExtraArgs['channel_name'] = channel?.channel_name
|
||||
}}
|
||||
onError={(e) => sendUserToast('Failed to load channels: ' + e.message, true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
'flow_input',
|
||||
'results',
|
||||
'resource',
|
||||
'variable'
|
||||
'variable',
|
||||
'flow_env'
|
||||
])
|
||||
</script>
|
||||
|
||||
@@ -67,6 +68,7 @@
|
||||
otherArgs?: Record<string, InputTransform>
|
||||
helperScript?: DynamicInputTypes.HelperScript | undefined
|
||||
isAgentTool?: boolean
|
||||
s3StorageConfigured?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -91,7 +93,8 @@
|
||||
editor = $bindable(undefined),
|
||||
otherArgs = {},
|
||||
helperScript = undefined,
|
||||
isAgentTool = false
|
||||
isAgentTool = false,
|
||||
s3StorageConfigured = true
|
||||
}: Props = $props()
|
||||
|
||||
let monaco: SimpleEditor | undefined = $state(undefined)
|
||||
@@ -159,7 +162,13 @@
|
||||
|
||||
function getPropertyType(arg: InputTransform | any): PropertyType {
|
||||
// For agent tools, if static with undefined/empty value, treat as 'ai', meaning the field will be filled by the AI agent dynamically.
|
||||
if (isAgentTool && arg?.type === 'static' && arg?.value === undefined) {
|
||||
if (
|
||||
isAgentTool &&
|
||||
((arg?.type === 'static' && arg?.value === undefined) || arg?.type === 'ai')
|
||||
) {
|
||||
if (arg?.type === 'static') {
|
||||
arg.type = 'ai'
|
||||
}
|
||||
return 'ai'
|
||||
}
|
||||
|
||||
@@ -563,7 +572,7 @@
|
||||
if (e.detail === 'ai') {
|
||||
// Switch to AI mode: static with no value
|
||||
if (arg) {
|
||||
arg.type = 'static'
|
||||
arg.type = 'ai'
|
||||
arg.value = undefined
|
||||
arg.expr = undefined
|
||||
}
|
||||
@@ -792,6 +801,7 @@
|
||||
bind:title={schema.properties[argName].title}
|
||||
bind:placeholder={schema.properties[argName].placeholder}
|
||||
{helperScript}
|
||||
{s3StorageConfigured}
|
||||
otherArgs={Object.fromEntries(
|
||||
Object.entries(otherArgs).map(([key, transform]) => [
|
||||
key,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { Schema } from '$lib/common'
|
||||
import { VariableService, type InputTransform } from '$lib/gen'
|
||||
import { VariableService, WorkspaceService, type InputTransform } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { allTrue, type DynamicInput as DynamicInputTypes } from '$lib/utils'
|
||||
import { untrack } from 'svelte'
|
||||
@@ -73,6 +73,22 @@
|
||||
let itemPicker: ItemPicker | undefined = $state(undefined)
|
||||
let variableEditor: VariableEditor | undefined = $state(undefined)
|
||||
|
||||
let s3StorageConfigured = $state(true)
|
||||
|
||||
async function checkS3Storage() {
|
||||
try {
|
||||
if ($workspaceStore) {
|
||||
const settings = await WorkspaceService.getSettings({ workspace: $workspaceStore })
|
||||
s3StorageConfigured = settings.large_file_storage?.s3_resource_path !== undefined
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch workspace settings:', error)
|
||||
s3StorageConfigured = true
|
||||
}
|
||||
}
|
||||
|
||||
checkS3Storage()
|
||||
|
||||
let keys: string[] = $state([])
|
||||
$effect(() => {
|
||||
let lkeys = Object.keys(schema?.properties ?? {})
|
||||
@@ -110,6 +126,7 @@
|
||||
bind:arg={args[argName]}
|
||||
bind:schema
|
||||
bind:argName={keys[index]}
|
||||
argExtra={schema.properties?.[argName] ?? {}}
|
||||
bind:inputCheck={
|
||||
() => inputCheck[argName] ?? false, (value) => (inputCheck[argName] = value)
|
||||
}
|
||||
@@ -122,6 +139,7 @@
|
||||
{enableAi}
|
||||
{helperScript}
|
||||
{isAgentTool}
|
||||
{s3StorageConfigured}
|
||||
otherArgs={Object.fromEntries(
|
||||
Object.entries(args ?? {}).filter(([key]) => key !== argName)
|
||||
)}
|
||||
|
||||
@@ -163,13 +163,17 @@
|
||||
teamItem: { team_id: string; team_name: string } | undefined,
|
||||
i: number
|
||||
) {
|
||||
const currentTeamChannel = $values['critical_error_channels'][i]?.teams_channel
|
||||
const teamIdChanged = currentTeamChannel?.team_id !== teamItem?.team_id
|
||||
|
||||
$values['critical_error_channels'][i] = {
|
||||
teams_channel: teamItem
|
||||
? {
|
||||
team_id: teamItem.team_id,
|
||||
team_name: teamItem.team_name,
|
||||
channel_id: undefined, // Will be set when channel is selected
|
||||
channel_name: undefined
|
||||
// Preserve existing channel if team didn't actually change
|
||||
channel_id: teamIdChanged ? undefined : currentTeamChannel?.channel_id,
|
||||
channel_name: teamIdChanged ? undefined : currentTeamChannel?.channel_name
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
@@ -191,6 +195,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<!-- {JSON.stringify($values, null, 2)} -->
|
||||
@@ -368,6 +373,11 @@
|
||||
onKeyDown={() => {
|
||||
licenseKeyChanged = true
|
||||
}}
|
||||
onBlur={() => {
|
||||
if ($values[setting.key] && typeof $values[setting.key] === 'string') {
|
||||
$values[setting.key] = $values[setting.key].trim()
|
||||
}
|
||||
}}
|
||||
bind:password={$values[setting.key]}
|
||||
/>
|
||||
<Button
|
||||
@@ -558,24 +568,25 @@
|
||||
value={v?.slack_channel ?? ''}
|
||||
/>
|
||||
{:else if v && 'teams_channel' in v}
|
||||
{@const currentTeam = $values['critical_error_channels'][i]?.teams_channel
|
||||
? {
|
||||
team_id: $values['critical_error_channels'][i]?.teams_channel?.team_id,
|
||||
team_name: $values['critical_error_channels'][i]?.teams_channel?.team_name
|
||||
}
|
||||
: undefined}
|
||||
{@const currentChannel = $values['critical_error_channels'][i]?.teams_channel?.channel_id
|
||||
? {
|
||||
channel_id: $values['critical_error_channels'][i]?.teams_channel?.channel_id,
|
||||
channel_name: $values['critical_error_channels'][i]?.teams_channel?.channel_name
|
||||
}
|
||||
: undefined}
|
||||
<div class="flex flex-row gap-2 w-full">
|
||||
<TeamSelector
|
||||
containerClass="w-44"
|
||||
minWidth="140px"
|
||||
showRefreshButton={false}
|
||||
bind:selectedTeam={
|
||||
() =>
|
||||
$values['critical_error_channels'][i]?.teams_channel
|
||||
? {
|
||||
team_id:
|
||||
$values['critical_error_channels'][i]?.teams_channel?.team_id,
|
||||
team_name:
|
||||
$values['critical_error_channels'][i]?.teams_channel
|
||||
?.team_name
|
||||
}
|
||||
: undefined,
|
||||
(team) => handleTeamChange(team, i)
|
||||
}
|
||||
selectedTeam={currentTeam}
|
||||
onselectedteamchange={(team) => handleTeamChange(team, i)}
|
||||
/>
|
||||
|
||||
{#if $values['critical_error_channels'][i]?.teams_channel?.team_id}
|
||||
@@ -583,20 +594,8 @@
|
||||
containerClass=""
|
||||
placeholder="Search channels"
|
||||
teamId={$values['critical_error_channels'][i]?.teams_channel?.team_id}
|
||||
bind:selectedChannel={
|
||||
() =>
|
||||
$values['critical_error_channels'][i]?.teams_channel?.channel_id
|
||||
? {
|
||||
channel_id:
|
||||
$values['critical_error_channels'][i]?.teams_channel
|
||||
?.channel_id,
|
||||
channel_name:
|
||||
$values['critical_error_channels'][i]?.teams_channel
|
||||
?.channel_name
|
||||
}
|
||||
: undefined,
|
||||
(channel) => handleChannelChange(channel, i)
|
||||
}
|
||||
selectedChannel={currentChannel}
|
||||
onselectedchannelchange={(channel) => handleChannelChange(channel, i)}
|
||||
onError={(e) =>
|
||||
sendUserToast('Failed to load channels: ' + e.message, true)}
|
||||
/>
|
||||
|
||||
@@ -136,6 +136,11 @@
|
||||
|
||||
let shouldReloadPage = false
|
||||
if ($values) {
|
||||
// Trim license key before saving
|
||||
if ($values['license_key'] && typeof $values['license_key'] === 'string') {
|
||||
$values['license_key'] = $values['license_key'].trim()
|
||||
}
|
||||
|
||||
const allSettings = [...Object.values(settings), scimSamlSetting].flatMap((x) =>
|
||||
Object.entries(x)
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
required?: boolean
|
||||
small?: boolean
|
||||
onKeyDown?: (event: KeyboardEvent) => void
|
||||
onBlur?: (event: FocusEvent) => void
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -17,7 +18,8 @@
|
||||
disabled = false,
|
||||
required = false,
|
||||
small = false,
|
||||
onKeyDown
|
||||
onKeyDown,
|
||||
onBlur
|
||||
}: Props = $props()
|
||||
|
||||
let red = $derived(required && (password == '' || password == undefined))
|
||||
@@ -43,6 +45,7 @@
|
||||
type="password"
|
||||
bind:value={password}
|
||||
onkeydown={onKeyDown}
|
||||
onblur={onBlur}
|
||||
autocomplete="new-password"
|
||||
{placeholder}
|
||||
{disabled}
|
||||
@@ -55,6 +58,7 @@
|
||||
type="text"
|
||||
bind:value={password}
|
||||
onkeydown={bubble('keydown')}
|
||||
onblur={onBlur}
|
||||
autocomplete="new-password"
|
||||
{placeholder}
|
||||
{disabled}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
teams?: TeamItem[] | undefined
|
||||
minWidth?: string
|
||||
onError?: (error: Error) => void
|
||||
onselectedteamchange?: (team: TeamItem | undefined) => void
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -27,34 +28,56 @@
|
||||
showRefreshButton = true,
|
||||
teams = undefined,
|
||||
minWidth = '160px',
|
||||
onError
|
||||
onError,
|
||||
onselectedteamchange
|
||||
}: Props = $props()
|
||||
|
||||
let isFetching = $state(false)
|
||||
let searchResults = $state<TeamItem[]>([])
|
||||
|
||||
// Only enable search mode if no teams are provided
|
||||
let selectedTeamId = $state<string | undefined>(selectedTeam?.team_id)
|
||||
|
||||
const searchMode = !teams
|
||||
|
||||
// Determine which teams to show: provided teams or search results
|
||||
// In search mode, include the selected team if it exists
|
||||
let displayTeams = $derived(() => {
|
||||
const baseTeams = teams || searchResults;
|
||||
if (searchMode && selectedTeam && !baseTeams.find(t => t.team_id === selectedTeam?.team_id)) {
|
||||
return [selectedTeam, ...baseTeams];
|
||||
let displayTeams = $derived.by(() => {
|
||||
const baseTeams = teams || searchResults
|
||||
if (selectedTeam && !baseTeams.find(t => t.team_id === selectedTeam?.team_id)) {
|
||||
return [selectedTeam, ...baseTeams]
|
||||
}
|
||||
return baseTeams
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
const newTeam = selectedTeamId
|
||||
? displayTeams.find(t => t.team_id === selectedTeamId)
|
||||
: undefined
|
||||
|
||||
if (newTeam?.team_id !== selectedTeam?.team_id) {
|
||||
selectedTeam = newTeam
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (selectedTeam?.team_id !== selectedTeamId) {
|
||||
selectedTeamId = selectedTeam?.team_id
|
||||
}
|
||||
})
|
||||
|
||||
let previousTeamId = $state<string | undefined>(undefined)
|
||||
|
||||
$effect(() => {
|
||||
if (selectedTeam?.team_id !== previousTeamId) {
|
||||
previousTeamId = selectedTeam?.team_id
|
||||
onselectedteamchange?.(selectedTeam)
|
||||
}
|
||||
return baseTeams;
|
||||
})
|
||||
|
||||
// Create separate filter text for search mode
|
||||
let searchFilterText = $state('')
|
||||
|
||||
// Debounced search function
|
||||
const debouncedSearch = debounce(async (query: string) => {
|
||||
await searchTeams(query)
|
||||
}, 500)
|
||||
|
||||
// Watch for search filter text changes (only in search mode)
|
||||
$effect(() => {
|
||||
if (searchMode) {
|
||||
if (searchFilterText.length >= 1) {
|
||||
@@ -100,37 +123,27 @@
|
||||
{#if searchMode}
|
||||
<Select
|
||||
containerStyle={'min-width: ' + minWidth}
|
||||
items={searchFilterText.length >= 1 || (searchFilterText.length === 0 && selectedTeam) ? displayTeams().map((team) => ({
|
||||
items={displayTeams.map((team) => ({
|
||||
label: team.team_name,
|
||||
value: team.team_id
|
||||
})) : []}
|
||||
}))}
|
||||
placeholder={isFetching ? "Searching..." : "Search teams..."}
|
||||
clearable
|
||||
disabled={disabled || isFetching}
|
||||
bind:filterText={searchFilterText}
|
||||
bind:value={
|
||||
() => selectedTeam?.team_id,
|
||||
(value) => {
|
||||
selectedTeam = value ? displayTeams().find((team) => team.team_id === value) : undefined
|
||||
}
|
||||
}
|
||||
bind:value={selectedTeamId}
|
||||
/>
|
||||
{:else}
|
||||
<Select
|
||||
containerStyle={'min-width: ' + minWidth}
|
||||
items={displayTeams().map((team) => ({
|
||||
items={displayTeams.map((team) => ({
|
||||
label: team.team_name,
|
||||
value: team.team_id
|
||||
}))}
|
||||
placeholder="Select a team"
|
||||
clearable
|
||||
disabled={disabled || isFetching}
|
||||
bind:value={
|
||||
() => selectedTeam?.team_id,
|
||||
(value) => {
|
||||
selectedTeam = value ? displayTeams().find((team) => team.team_id === value) : undefined
|
||||
}
|
||||
}
|
||||
bind:value={selectedTeamId}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
+38
-1
@@ -19,6 +19,7 @@
|
||||
import RowWrapper from '../../layout/RowWrapper.svelte'
|
||||
import type { ICellRendererParams } from 'ag-grid-community'
|
||||
import Popover from '$lib/components/meltComponents/Popover.svelte'
|
||||
import AppModal from '../../layout/AppModal.svelte'
|
||||
|
||||
interface Props {
|
||||
p: ICellRendererParams<any>
|
||||
@@ -47,8 +48,18 @@
|
||||
}: Props = $props()
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const { selectedComponent, hoverStore, mode, connectingInput } =
|
||||
const { selectedComponent, hoverStore, mode, connectingInput, componentControl, app } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
$componentControl[id] = {
|
||||
...$componentControl[id],
|
||||
onDelete: () => {
|
||||
// Remove associated subgrid
|
||||
actions.forEach((action) => {
|
||||
if (action?.type === 'modalcomponent') delete $app.subgrids?.[`${action.id}-0`]
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<RowWrapper
|
||||
@@ -198,6 +209,19 @@
|
||||
replaceCallback={true}
|
||||
{controls}
|
||||
/>
|
||||
{:else if action.type == 'modalcomponent'}
|
||||
<AppModal
|
||||
{render}
|
||||
noWFull
|
||||
id={action.id}
|
||||
customCss={action.customCss}
|
||||
configuration={action.configuration}
|
||||
verticalAlignment="center"
|
||||
preclickAction={async () => {
|
||||
dispatch('toggleRow')
|
||||
selectRow(p)
|
||||
}}
|
||||
/>
|
||||
{:else if action.type == 'checkboxcomponent'}
|
||||
<AppCheckbox
|
||||
noInitialize
|
||||
@@ -256,6 +280,19 @@
|
||||
replaceCallback={true}
|
||||
componentInput={action.componentInput}
|
||||
/>
|
||||
{:else if action.type == 'modalcomponent'}
|
||||
<AppModal
|
||||
{render}
|
||||
noWFull
|
||||
id={action.id}
|
||||
customCss={action.customCss}
|
||||
configuration={action.configuration}
|
||||
verticalAlignment="center"
|
||||
preclickAction={async () => {
|
||||
dispatch('toggleRow')
|
||||
selectRow(p)
|
||||
}}
|
||||
/>
|
||||
{:else if action.type == 'checkboxcomponent'}
|
||||
<AppCheckbox
|
||||
noInitialize
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
components,
|
||||
type ButtonComponent,
|
||||
type CheckboxComponent,
|
||||
type ModalComponent,
|
||||
type SelectComponent
|
||||
} from '../../../editor/component'
|
||||
import { initCss } from '../../../utils'
|
||||
@@ -62,7 +63,8 @@
|
||||
id: string
|
||||
componentInput: AppInput | undefined
|
||||
configuration: RichConfigurations
|
||||
actionButtons: (BaseAppComponent & (ButtonComponent | CheckboxComponent | SelectComponent))[]
|
||||
actionButtons: (BaseAppComponent &
|
||||
(ButtonComponent | CheckboxComponent | SelectComponent | ModalComponent))[]
|
||||
initializing?: boolean | undefined
|
||||
customCss?: ComponentCustomCSS<'tablecomponent'> | undefined
|
||||
render: boolean
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
render: boolean
|
||||
onOpenRecomputeIds?: string[] | undefined
|
||||
onCloseRecomputeIds?: string[] | undefined
|
||||
preclickAction?: (() => Promise<void>) | undefined
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -40,7 +41,8 @@
|
||||
noWFull = false,
|
||||
render,
|
||||
onOpenRecomputeIds = undefined,
|
||||
onCloseRecomputeIds = undefined
|
||||
onCloseRecomputeIds = undefined,
|
||||
preclickAction
|
||||
}: Props = $props()
|
||||
|
||||
const {
|
||||
@@ -156,6 +158,7 @@
|
||||
e?.stopPropagation()
|
||||
}}
|
||||
on:click={async (e) => {
|
||||
await preclickAction?.()
|
||||
$focusedGrid = {
|
||||
parentComponentId: id,
|
||||
subGridIndex: 0
|
||||
|
||||
@@ -176,7 +176,7 @@ export type AgChartsComponentEe = BaseComponent<'agchartscomponentee'> & {
|
||||
export type ScatterChartComponent = BaseComponent<'scatterchartcomponent'>
|
||||
|
||||
export type TableAction = BaseAppComponent &
|
||||
(ButtonComponent | CheckboxComponent | SelectComponent) &
|
||||
(ButtonComponent | CheckboxComponent | SelectComponent | ModalComponent) &
|
||||
GridItem
|
||||
|
||||
export type TableComponent = BaseComponent<'tablecomponent'> & {
|
||||
|
||||
@@ -9,7 +9,12 @@
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import type { AppViewerContext, BaseAppComponent, RichConfiguration } from '../../types'
|
||||
import { appComponentFromType } from '../appUtils'
|
||||
import type { ButtonComponent, CheckboxComponent, SelectComponent } from '../component'
|
||||
import type {
|
||||
ButtonComponent,
|
||||
CheckboxComponent,
|
||||
ModalComponent,
|
||||
SelectComponent
|
||||
} from '../component'
|
||||
import PanelSection from './common/PanelSection.svelte'
|
||||
import { GripVertical, Inspect, List, ToggleRightIcon, ListOrdered } from 'lucide-svelte'
|
||||
import { dragHandle, dragHandleZone } from '@windmill-labs/svelte-dnd-action'
|
||||
@@ -17,10 +22,12 @@
|
||||
import { flip } from 'svelte/animate'
|
||||
import TableActionsWizard from '$lib/components/wizards/TableActionsWizard.svelte'
|
||||
import Alert from '$lib/components/common/alert/Alert.svelte'
|
||||
import DropdownV2 from '$lib/components/DropdownV2.svelte'
|
||||
|
||||
interface Props {
|
||||
components:
|
||||
| (BaseAppComponent & (ButtonComponent | CheckboxComponent | SelectComponent))[]
|
||||
| (BaseAppComponent &
|
||||
(ButtonComponent | CheckboxComponent | SelectComponent | ModalComponent))[]
|
||||
| undefined
|
||||
actionsOrder?: RichConfiguration | undefined
|
||||
id: string
|
||||
@@ -48,35 +55,40 @@
|
||||
const { selectedComponent, app, errorByComponent, hoverStore } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
function addComponent(typ: 'buttoncomponent' | 'checkboxcomponent' | 'selectcomponent') {
|
||||
function addComponent(
|
||||
typ: 'buttoncomponent' | 'checkboxcomponent' | 'selectcomponent' | 'modalcomponent'
|
||||
) {
|
||||
if (!components) {
|
||||
return
|
||||
}
|
||||
|
||||
const actionId = getNextId(components.map((x) => x.id.split('_')[1]))
|
||||
|
||||
const newComponent = {
|
||||
...appComponentFromType(typ)(`${id}_${actionId}`),
|
||||
recomputeIds: []
|
||||
}
|
||||
const newComponent = appComponentFromType(typ)(`${id}_${actionId}`)
|
||||
|
||||
if (typ == 'buttoncomponent') {
|
||||
if (newComponent?.type == 'buttoncomponent') {
|
||||
if (newComponent?.configuration?.size) {
|
||||
// @ts-ignore
|
||||
newComponent.configuration.size = { type: 'static', value: 'xs2' }
|
||||
}
|
||||
}
|
||||
|
||||
items = [
|
||||
...items,
|
||||
{
|
||||
value: newComponent,
|
||||
id: generateRandomString(),
|
||||
originalIndex: items.length
|
||||
if (newComponent?.type == 'modalcomponent') {
|
||||
if (newComponent?.configuration?.buttonSize) {
|
||||
// @ts-ignore
|
||||
newComponent.configuration.buttonSize = { type: 'static', value: 'xs2' }
|
||||
}
|
||||
]
|
||||
// Create associated subgrid
|
||||
if ($app.subgrids) $app.subgrids[`${newComponent.id}-0`] = []
|
||||
newComponent.id
|
||||
}
|
||||
|
||||
components = [...components, newComponent]
|
||||
const newItem = {
|
||||
value: newComponent,
|
||||
id: generateRandomString(),
|
||||
originalIndex: items.length
|
||||
}
|
||||
items = [...items, newItem as any]
|
||||
components = [...components, newComponent as any]
|
||||
$app = $app
|
||||
}
|
||||
|
||||
@@ -84,6 +96,10 @@
|
||||
if (!components) {
|
||||
return
|
||||
}
|
||||
if (components.find((x) => x.id === cid)?.type === 'modalcomponent') {
|
||||
// Remove associated subgrid
|
||||
delete $app.subgrids?.[`${cid}-0`]
|
||||
}
|
||||
components = components.filter((x) => x.id !== cid)
|
||||
delete $errorByComponent[cid]
|
||||
|
||||
@@ -171,6 +187,8 @@
|
||||
Select
|
||||
{:else if component.type == 'checkboxcomponent'}
|
||||
Toggle
|
||||
{:else if component.type == 'modalcomponent'}
|
||||
Modal
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -192,8 +210,8 @@
|
||||
</div>
|
||||
<div class="w-full flex gap-2">
|
||||
<Button
|
||||
btnClasses="gap-1 flex items-center text-sm text-primary"
|
||||
wrapperClasses="w-full"
|
||||
btnClasses="gap-1 flex items-center text-xs text-primary"
|
||||
wrapperClasses="flex-1"
|
||||
variant="default"
|
||||
on:click={() => addComponent('buttoncomponent')}
|
||||
title="Add Button"
|
||||
@@ -201,8 +219,8 @@
|
||||
+ <Inspect size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
btnClasses="gap-1 flex items-center text-sm text-primary"
|
||||
wrapperClasses="w-full"
|
||||
btnClasses="gap-1 flex items-center text-xs text-primary"
|
||||
wrapperClasses="flex-1"
|
||||
variant="default"
|
||||
on:click={() => addComponent('checkboxcomponent')}
|
||||
title="Add Toggle"
|
||||
@@ -210,14 +228,24 @@
|
||||
+ <ToggleRightIcon size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
btnClasses="gap-1 flex items-center text-sm text-primary"
|
||||
wrapperClasses="w-full"
|
||||
btnClasses="gap-1 flex items-center text-xs text-primary"
|
||||
wrapperClasses="flex-1"
|
||||
variant="default"
|
||||
on:click={() => addComponent('selectcomponent')}
|
||||
title="Add Select"
|
||||
>
|
||||
+ <List size={14} />
|
||||
</Button>
|
||||
<DropdownV2
|
||||
items={[
|
||||
{
|
||||
displayName: 'Modal button',
|
||||
icon: Inspect,
|
||||
action: () => addComponent('modalcomponent')
|
||||
}
|
||||
]}
|
||||
fixedHeight={false}
|
||||
></DropdownV2>
|
||||
</div>
|
||||
<div class="w-full flex flex-col">
|
||||
{#if actionsOrder}
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { dfs } from '$lib/components/flows/dfs'
|
||||
import FlowCard from '../common/FlowCard.svelte'
|
||||
import { Alert, Badge } from '$lib/components/common'
|
||||
import type { FlowModule, FlowModuleValue, InputTransform, PathScript, RawScript } from '$lib/gen'
|
||||
import { getContext, setContext } from 'svelte'
|
||||
import type { PropPickerWrapperContext } from '../propPicker/PropPickerWrapper.svelte'
|
||||
import { writable } from 'svelte/store'
|
||||
import Toggle from '../../Toggle.svelte'
|
||||
import InputTransformSchemaForm from '$lib/components/InputTransformSchemaForm.svelte'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
|
||||
interface Props {
|
||||
noEditor: boolean
|
||||
}
|
||||
|
||||
let { noEditor }: Props = $props()
|
||||
|
||||
let hideOptional = $state(false)
|
||||
const { flowStateStore, flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
let scriptModules = $derived(
|
||||
dfs(flowStore.val.value.modules, (x) => x)
|
||||
.map((x) => [x.value, x] as [FlowModuleValue, FlowModule])
|
||||
.filter((x) => x[0].type == 'script' || x[0].type == 'rawscript' || x[0].type == 'flow') as [
|
||||
PathScript | RawScript,
|
||||
FlowModule
|
||||
][]
|
||||
)
|
||||
|
||||
let resources = $derived(
|
||||
Object.fromEntries(
|
||||
scriptModules
|
||||
.map(([v, m]) => [
|
||||
m.id,
|
||||
Object.entries(v.input_transforms)
|
||||
.map((x) => {
|
||||
let schema = flowStateStore.val[m.id]?.schema
|
||||
let val: { argName: string; type: string } | undefined = undefined
|
||||
|
||||
const [k, inputTransform] = x
|
||||
const v = schema?.properties[k]
|
||||
|
||||
if (
|
||||
v?.format?.includes('resource') &&
|
||||
inputTransform.type === 'static' &&
|
||||
(inputTransform.value === '' ||
|
||||
inputTransform.value === undefined ||
|
||||
inputTransform.value === null)
|
||||
) {
|
||||
val = {
|
||||
argName: k,
|
||||
type: v.format.split('-')[1]
|
||||
}
|
||||
}
|
||||
return val
|
||||
})
|
||||
.filter(Boolean)
|
||||
])
|
||||
.filter((x) => x[1].length > 0)
|
||||
) as {
|
||||
[k: string]: {
|
||||
argName: string
|
||||
type: string
|
||||
}[]
|
||||
}
|
||||
)
|
||||
|
||||
let steps = $derived(
|
||||
scriptModules
|
||||
.map(
|
||||
([v, m]) =>
|
||||
[
|
||||
v.input_transforms,
|
||||
Object.entries(v.input_transforms)
|
||||
.filter((x) => {
|
||||
const shouldDisplay = hideOptional
|
||||
? flowStateStore.val[m.id]?.schema?.required?.includes(x[0])
|
||||
: true
|
||||
return x[1].type == 'static' && shouldDisplay
|
||||
})
|
||||
.map((x) => x[0]),
|
||||
m
|
||||
] as [Record<string, InputTransform>, string[], FlowModule]
|
||||
)
|
||||
.filter(([i, f, m]) => f.length > 0)
|
||||
)
|
||||
|
||||
setContext<PropPickerWrapperContext>('PropPickerWrapper', {
|
||||
inputMatches: writable(undefined),
|
||||
focusProp: () => {},
|
||||
propPickerConfig: writable(undefined),
|
||||
clearFocus: () => {}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="min-h-full">
|
||||
<FlowCard {noEditor} title="All Static Inputs">
|
||||
{#snippet header()}
|
||||
<Toggle bind:checked={hideOptional} options={{ left: 'Hide optional inputs' }} />
|
||||
{/snippet}
|
||||
<div class="min-h-full flex-1">
|
||||
<Alert type="info" title="Static Inputs" class="m-4"
|
||||
>This page centralizes the static inputs of every steps. It is aking to a file containing
|
||||
all constants. Modifying a value here modifies it in the step input directly. It is
|
||||
especially useful when forking a flow to get an overview of all the variables to parametrize
|
||||
that are not exposed directly as flow inputs.</Alert
|
||||
>
|
||||
{#if Object.keys(resources).length > 0}
|
||||
<Alert type="warning" title="Missing resources" class="m-4">
|
||||
The following resources are missing and the flow will not be fully runnable until they are
|
||||
set. Add your own resources:
|
||||
{#each Object.entries(resources) as [id, r]}
|
||||
{#each r as resource}
|
||||
<div class="mt-2">
|
||||
<Badge color="red">{id}</Badge> is missing a resource of type{' '}
|
||||
<Badge color="red">{resource?.type}</Badge> for the input{' '}
|
||||
<Badge color="red">{resource?.argName}</Badge>
|
||||
</div>
|
||||
{/each}
|
||||
{/each}
|
||||
</Alert>
|
||||
{/if}
|
||||
{#if steps.length == 0}
|
||||
<div class="mt-2"></div>
|
||||
{#if flowStore.val.value.modules.length == 0}
|
||||
<Alert type="warning" title="No steps" class="m-4">
|
||||
This flow has no steps. Add a step to see its static inputs.
|
||||
</Alert>
|
||||
{:else}
|
||||
<Alert type="warning" title="No static inputs" class="m-4">
|
||||
This flow has no steps with static inputs. Add a step with static inputs to see them
|
||||
here.
|
||||
</Alert>
|
||||
{/if}
|
||||
{/if}
|
||||
{#each steps as [_args, filter, m], index (m.id + index)}
|
||||
{#if filter.length > 0}
|
||||
<div class="relative h-full border-t p-4">
|
||||
<p class="text-sm font-bold sticky w-full top-0 z-10 inline-flex items-center">
|
||||
<span class="mr-4">{m.summary || m.value['path'] || 'Inline script'}</span>
|
||||
<Badge large color="indigo">{m.id}</Badge>
|
||||
</p>
|
||||
|
||||
<InputTransformSchemaForm
|
||||
noDynamicToggle
|
||||
{filter}
|
||||
class="mt-2"
|
||||
schema={flowStateStore.val[m.id]?.schema ?? {}}
|
||||
bind:args={steps[index][0]}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</FlowCard>
|
||||
</div>
|
||||
@@ -5,7 +5,7 @@
|
||||
import FlowSettings from './FlowSettings.svelte'
|
||||
import FlowInput from './FlowInput.svelte'
|
||||
import FlowFailureModule from './FlowFailureModule.svelte'
|
||||
import FlowConstants from './FlowConstants.svelte'
|
||||
import FlowEnvironmentVariables from './FlowEnvironmentVariables.svelte'
|
||||
import type { FlowModule, Flow, Job } from '$lib/gen'
|
||||
import FlowPreprocessorModule from './FlowPreprocessorModule.svelte'
|
||||
import type { TriggerContext } from '$lib/components/triggers'
|
||||
@@ -102,7 +102,7 @@
|
||||
{:else if $selectedId === 'Result'}
|
||||
<FlowResult {noEditor} {job} {isOwner} {suspendStatus} {onOpenDetails} />
|
||||
{:else if $selectedId === 'constants'}
|
||||
<FlowConstants {noEditor} />
|
||||
<FlowEnvironmentVariables {noEditor} />
|
||||
{:else if $selectedId === 'failure'}
|
||||
<FlowFailureModule {noEditor} savedModule={savedFlow?.value.failure_module} />
|
||||
{:else if $selectedId === 'preprocessor'}
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
<script lang="ts">
|
||||
import { Alert } from '$lib/components/common'
|
||||
import { getContext, setContext } from 'svelte'
|
||||
import type { PropPickerWrapperContext } from '../propPicker/PropPickerWrapper.svelte'
|
||||
import { writable } from 'svelte/store'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
import { Button } from '$lib/components/common'
|
||||
import { Plus, Trash2 } from 'lucide-svelte'
|
||||
import FlowCard from '../common/FlowCard.svelte'
|
||||
import JsonEditor from '$lib/components/JsonEditor.svelte'
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import Select from '$lib/components/select/Select.svelte'
|
||||
|
||||
interface Props {
|
||||
noEditor: boolean
|
||||
}
|
||||
|
||||
type EnvVarType = 'string' | 'json'
|
||||
|
||||
interface EnvVarEntry {
|
||||
id: string
|
||||
key: string
|
||||
value: any
|
||||
type: EnvVarType
|
||||
displayValue: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
let { noEditor }: Props = $props()
|
||||
|
||||
const { flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
if (!flowStore.val.value.flow_env) {
|
||||
flowStore.val.value.flow_env = {}
|
||||
}
|
||||
let flowEnvVarsMap = $derived(new Map(Object.entries(flowStore.val.value.flow_env || {})))
|
||||
|
||||
function determineValueType(value: any): EnvVarType {
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
JSON.parse(value)
|
||||
return value.trim().startsWith('{') ||
|
||||
value.trim().startsWith('[') ||
|
||||
value.trim().startsWith('"')
|
||||
? 'json'
|
||||
: 'string'
|
||||
} catch {
|
||||
return 'string'
|
||||
}
|
||||
}
|
||||
return 'json'
|
||||
}
|
||||
|
||||
let flowEnvTypes = $state<Record<string, EnvVarType>>({})
|
||||
|
||||
const typeOptions = [
|
||||
{ label: 'String', value: 'string' as EnvVarType },
|
||||
{ label: 'JSON', value: 'json' as EnvVarType }
|
||||
]
|
||||
|
||||
$effect(() => {
|
||||
for (const [key, value] of flowEnvVarsMap.entries()) {
|
||||
if (!flowEnvTypes[key]) {
|
||||
flowEnvTypes[key] = determineValueType(value)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
for (const [key, type] of Object.entries(flowEnvTypes)) {
|
||||
if (flowStore.val.value.flow_env && key in flowStore.val.value.flow_env) {
|
||||
const currentType = determineValueType(flowStore.val.value.flow_env[key])
|
||||
if (currentType !== type) {
|
||||
updateEnvType(key, type)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let flowEnvEntries = $derived(
|
||||
Array.from(flowEnvVarsMap.entries()).map(([key, value]): EnvVarEntry => {
|
||||
const stringValue = typeof value === 'string' ? value : JSON.stringify(value, null, 2)
|
||||
const type = flowEnvTypes[key] || determineValueType(value)
|
||||
return {
|
||||
id: key,
|
||||
key,
|
||||
value,
|
||||
type,
|
||||
displayValue: stringValue,
|
||||
error: undefined
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
function addEnvVar() {
|
||||
const existingKeys = Array.from(flowEnvVarsMap.keys())
|
||||
let counter = 1
|
||||
let newKey = `VAR_${counter}`
|
||||
while (existingKeys.includes(newKey)) {
|
||||
counter++
|
||||
newKey = `VAR_${counter}`
|
||||
}
|
||||
|
||||
if (!flowStore.val.value.flow_env) {
|
||||
flowStore.val.value.flow_env = {}
|
||||
}
|
||||
flowStore.val.value.flow_env[newKey] = ''
|
||||
flowEnvTypes[newKey] = 'string'
|
||||
flowStore.val = flowStore.val
|
||||
}
|
||||
|
||||
function removeEnvVar(key: string) {
|
||||
if (flowStore.val.value.flow_env && key in flowStore.val.value.flow_env) {
|
||||
delete flowStore.val.value.flow_env[key]
|
||||
delete flowEnvTypes[key]
|
||||
flowStore.val = flowStore.val
|
||||
}
|
||||
}
|
||||
|
||||
function updateEnvValue(key: string, value: string, type: EnvVarType) {
|
||||
if (flowStore.val.value.flow_env) {
|
||||
if (type === 'json') {
|
||||
try {
|
||||
const parsed = JSON.parse(value)
|
||||
flowStore.val.value.flow_env[key] = parsed
|
||||
} catch (e) {
|
||||
flowStore.val.value.flow_env[key] = value
|
||||
}
|
||||
} else {
|
||||
flowStore.val.value.flow_env[key] = value
|
||||
}
|
||||
flowStore.val = flowStore.val
|
||||
}
|
||||
}
|
||||
|
||||
function updateEnvKey(oldKey: string, newKey: string) {
|
||||
if (flowStore.val.value.flow_env && oldKey !== newKey && newKey.trim() !== '') {
|
||||
const value = flowStore.val.value.flow_env[oldKey]
|
||||
const type = flowEnvTypes[oldKey] || 'string'
|
||||
|
||||
const newEnvVars: Record<string, any> = {}
|
||||
for (const [k, v] of flowEnvVarsMap.entries()) {
|
||||
if (k === oldKey) {
|
||||
newEnvVars[newKey] = value
|
||||
} else {
|
||||
newEnvVars[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
flowStore.val.value.flow_env = newEnvVars
|
||||
delete flowEnvTypes[oldKey]
|
||||
flowEnvTypes[newKey] = type
|
||||
flowStore.val = flowStore.val
|
||||
}
|
||||
}
|
||||
|
||||
function updateEnvType(key: string, newType: EnvVarType) {
|
||||
if (flowStore.val.value.flow_env && key in flowStore.val.value.flow_env) {
|
||||
const currentValue = flowStore.val.value.flow_env[key]
|
||||
const stringValue =
|
||||
typeof currentValue === 'string' ? currentValue : JSON.stringify(currentValue, null, 2)
|
||||
|
||||
flowEnvTypes[key] = newType
|
||||
|
||||
if (newType === 'json') {
|
||||
try {
|
||||
const parsed = JSON.parse(stringValue)
|
||||
flowStore.val.value.flow_env[key] = parsed
|
||||
} catch {
|
||||
flowStore.val.value.flow_env[key] = stringValue
|
||||
}
|
||||
} else {
|
||||
flowStore.val.value.flow_env[key] = stringValue
|
||||
}
|
||||
flowStore.val = flowStore.val
|
||||
}
|
||||
}
|
||||
|
||||
setContext<PropPickerWrapperContext>('PropPickerWrapper', {
|
||||
inputMatches: writable(undefined),
|
||||
focusProp: () => {},
|
||||
propPickerConfig: writable(undefined),
|
||||
clearFocus: () => {}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="min-h-full">
|
||||
<FlowCard {noEditor} title="Flow Env Variables">
|
||||
<div class="min-h-full flex-1">
|
||||
<Alert type="info" title="Flow Env Variables" class="m-4">
|
||||
Flow envs can be referenced in any flow step input using the syntax{' '}
|
||||
<code>flow_env.VARIABLE_NAME</code> or <code>flow_env["VARIABLE_NAME"]</code>. These
|
||||
variables are available in the property picker and can be used in JavaScript expressions and
|
||||
input bindings. You can choose between String or JSON types for each variable - JSON types
|
||||
allow complex data structures.
|
||||
</Alert>
|
||||
|
||||
{#if flowEnvEntries.length === 0}
|
||||
<Alert type="warning" title="No flow env variables" class="m-4">
|
||||
This flow has no flow env variables defined. Click "Add Variable" to create your first
|
||||
flow env variable.
|
||||
</Alert>
|
||||
{:else}
|
||||
<div class="space-y-4 p-4">
|
||||
{#each flowEnvEntries as entry (entry.id)}
|
||||
<div class="flex flex-col gap-4 p-4 border rounded-lg bg-surface-secondary">
|
||||
<div class="flex items-end gap-3">
|
||||
<div class="flex-1 min-w-0 max-w-xs">
|
||||
<Label label="Variable Name">
|
||||
<input
|
||||
type="text"
|
||||
value={entry.key}
|
||||
onblur={(e) => {
|
||||
const newKey = e.currentTarget.value.trim()
|
||||
if (newKey !== entry.key && newKey !== '') {
|
||||
updateEnvKey(entry.key, newKey)
|
||||
}
|
||||
}}
|
||||
disabled={noEditor}
|
||||
class="input w-full"
|
||||
placeholder="VARIABLE_NAME"
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<Label label="Type">
|
||||
<Select
|
||||
bind:value={flowEnvTypes[entry.key]}
|
||||
items={typeOptions}
|
||||
disabled={noEditor}
|
||||
size="sm"
|
||||
class="text-sm"
|
||||
/>
|
||||
</Label>
|
||||
|
||||
{#if !noEditor}
|
||||
<Button
|
||||
size="sm"
|
||||
color="red"
|
||||
startIcon={{ icon: Trash2 }}
|
||||
onClick={() => removeEnvVar(entry.key)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<!-- svelte-ignore a11y_label_has_associated_control -->
|
||||
<label class="text-sm font-medium">Value</label>
|
||||
{#if entry.type === 'json'}
|
||||
<div class="w-full">
|
||||
<JsonEditor
|
||||
bind:code={entry.displayValue}
|
||||
disabled={noEditor}
|
||||
class="min-h-[60px] max-h-[200px]"
|
||||
on:change={() => {
|
||||
updateEnvValue(entry.key, entry.displayValue, 'json')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<input
|
||||
type="text"
|
||||
value={entry.displayValue}
|
||||
oninput={(e) => updateEnvValue(entry.key, e.currentTarget.value, 'string')}
|
||||
disabled={noEditor}
|
||||
class="input w-full"
|
||||
placeholder="Variable value"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class=" p-4">
|
||||
{#if !noEditor}
|
||||
<Button size="sm" startIcon={{ icon: Plus }} onClick={addEnvVar} color="light">
|
||||
Add Variable
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</FlowCard>
|
||||
</div>
|
||||
@@ -173,7 +173,6 @@
|
||||
bind:value={tool.value.include_tools}
|
||||
items={toolOptions}
|
||||
placeholder="Choose tools to include..."
|
||||
disablePortal
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
@@ -183,7 +182,6 @@
|
||||
bind:value={tool.value.exclude_tools}
|
||||
items={toolOptions}
|
||||
placeholder="Choose tools to exclude..."
|
||||
disablePortal
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
@@ -37,6 +37,8 @@ export const AI_AGENT_SCHEMA = {
|
||||
type: 'number',
|
||||
description:
|
||||
'Maximum number of conversation messages to store and retrieve from memory. If not set or 0, memory is disabled.',
|
||||
'x-no-s3-storage-workspace-warning':
|
||||
'When no S3 storage is configured in your workspace settings, memory will be stored in database, which implies a limit of 100KB per memory entry. If you need to store more messages, you should use S3 storage in your workspace settings.',
|
||||
showExpr: "fields.output_type === 'text'"
|
||||
},
|
||||
output_schema: {
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
onClick={() => ($selectedId = 'constants')}
|
||||
/>
|
||||
{#snippet text()}
|
||||
Static inputs
|
||||
Environment Variables
|
||||
{/snippet}
|
||||
</Popover>
|
||||
{/if}
|
||||
|
||||
@@ -9,6 +9,7 @@ export type PickableProperties = {
|
||||
priorIds: Record<string, any>
|
||||
previousId: string | undefined
|
||||
hasResume: boolean
|
||||
flow_env?: Record<string, any>
|
||||
}
|
||||
|
||||
type StepPropPicker = {
|
||||
@@ -156,7 +157,8 @@ export function getFailureStepPropPicker(flowState: FlowState, flow: OpenFlow, a
|
||||
flow_input: schemaToObject(flow.schema as any, args),
|
||||
priorIds: priorIds,
|
||||
previousId: undefined,
|
||||
hasResume: false
|
||||
hasResume: false,
|
||||
flow_env: flow.value.flow_env
|
||||
},
|
||||
extraLib: `
|
||||
/**
|
||||
@@ -178,6 +180,17 @@ declare const results = ${JSON.stringify(priorIds)}
|
||||
* flow input as an object
|
||||
*/
|
||||
declare const flow_input = ${JSON.stringify(flowInput)};
|
||||
|
||||
${
|
||||
flow.value.flow_env
|
||||
? `
|
||||
/**
|
||||
* flow environment variables
|
||||
*/
|
||||
declare const flow_env = ${JSON.stringify(flow.value.flow_env)};
|
||||
`
|
||||
: ''
|
||||
}
|
||||
`
|
||||
}
|
||||
}
|
||||
@@ -218,7 +231,8 @@ export function getStepPropPicker(
|
||||
flow_input: flowInput,
|
||||
priorIds: priorIds,
|
||||
previousId: previousIds[0],
|
||||
hasResume: previousModule?.suspend != undefined
|
||||
hasResume: previousModule?.suspend != undefined,
|
||||
flow_env: flow.value.flow_env
|
||||
}
|
||||
|
||||
if (pickableProperties.hasResume) {
|
||||
@@ -230,7 +244,8 @@ export function getStepPropPicker(
|
||||
flowInput,
|
||||
priorIds,
|
||||
previousModule?.suspend != undefined,
|
||||
previousModule?.id
|
||||
previousModule?.id,
|
||||
flow.value.flow_env
|
||||
),
|
||||
pickableProperties
|
||||
}
|
||||
@@ -240,7 +255,8 @@ export function buildExtraLib(
|
||||
flowInput: Record<string, any>,
|
||||
results: Record<string, any>,
|
||||
resume: boolean,
|
||||
previousId: string | undefined
|
||||
previousId: string | undefined,
|
||||
flowEnv?: Record<string, any>
|
||||
): string {
|
||||
return `
|
||||
/**
|
||||
@@ -275,6 +291,17 @@ declare const results = ${JSON.stringify(results)};
|
||||
*/
|
||||
declare const previous_result: ${previousId ? JSON.stringify(results[previousId]) : 'any'};
|
||||
|
||||
${
|
||||
flowEnv
|
||||
? `
|
||||
/**
|
||||
* flow environment variables
|
||||
*/
|
||||
declare const flow_env = ${JSON.stringify(flowEnv)};
|
||||
`
|
||||
: ''
|
||||
}
|
||||
|
||||
${
|
||||
resume
|
||||
? `
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
import type { PickableProperties } from '../previousResults'
|
||||
import AnimatedButton from '$lib/components/common/button/AnimatedButton.svelte'
|
||||
import type { PropPickerContext } from '$lib/components/prop_picker'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
|
||||
interface Props {
|
||||
pickableProperties: PickableProperties | undefined
|
||||
@@ -69,6 +70,10 @@
|
||||
|
||||
const { flowPropPickerConfig } = getContext<PropPickerContext>('PropPickerContext')
|
||||
flowPropPickerConfig.set(undefined)
|
||||
|
||||
const { flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
let flow_env = $derived(pickableProperties?.flow_env || flowStore.val.value.flow_env)
|
||||
setContext<PropPickerWrapperContext>('PropPickerWrapper', {
|
||||
propPickerConfig,
|
||||
inputMatches,
|
||||
@@ -156,6 +161,7 @@
|
||||
<PropPicker
|
||||
{displayContext}
|
||||
{error}
|
||||
{flow_env}
|
||||
previousId={pickableProperties?.previousId}
|
||||
{pickableProperties}
|
||||
allowCopy={!notSelectable && !$propPickerConfig}
|
||||
|
||||
@@ -18,11 +18,13 @@
|
||||
export let error: boolean = false
|
||||
export let allowCopy = false
|
||||
export let previousId: string | undefined = undefined
|
||||
export let flow_env: Record<string, string> | undefined = undefined
|
||||
|
||||
let variables: Record<string, string> = {}
|
||||
let resources: Record<string, any> = {}
|
||||
let displayVariable = false
|
||||
let displayResources = false
|
||||
let displayFlowEnv = false
|
||||
|
||||
let allResultsCollapsed = true
|
||||
let collapsableInitialState:
|
||||
@@ -30,6 +32,7 @@
|
||||
allResultsCollapsed: boolean
|
||||
displayVariable: boolean
|
||||
displayResources: boolean
|
||||
displayFlowEnv: boolean
|
||||
}
|
||||
| undefined
|
||||
|
||||
@@ -46,6 +49,7 @@
|
||||
|
||||
let flowInputsFiltered: any = pickableProperties.flow_input
|
||||
let resultByIdFiltered: any = pickableProperties.priorIds
|
||||
let flowEnvFiltered: any = pickableProperties.flow_env
|
||||
|
||||
let timeout: number | undefined
|
||||
function onSearch(search: string) {
|
||||
@@ -63,6 +67,9 @@
|
||||
search === EMPTY_STRING
|
||||
? pickableProperties.priorIds
|
||||
: keepByKey(pickableProperties.priorIds, search)
|
||||
|
||||
flowEnvFiltered =
|
||||
search === EMPTY_STRING ? pickableProperties.flow_env : keepByKey(pickableProperties.flow_env, search)
|
||||
}, 50)
|
||||
}
|
||||
|
||||
@@ -98,6 +105,7 @@
|
||||
if (search === EMPTY_STRING) {
|
||||
flowInputsFiltered = pickableProperties.flow_input
|
||||
resultByIdFiltered = pickableProperties.priorIds
|
||||
flowEnvFiltered = pickableProperties.flow_env
|
||||
}
|
||||
filteringFlowInputsOrResult = ''
|
||||
return
|
||||
@@ -109,6 +117,9 @@
|
||||
if (!$inputMatches?.some((match) => match.word === 'results')) {
|
||||
resultByIdFiltered = {}
|
||||
}
|
||||
if (!$inputMatches?.some((match) => match.word === 'flow_env')) {
|
||||
flowEnvFiltered = {}
|
||||
}
|
||||
if ($inputMatches?.length == 1) {
|
||||
filteringFlowInputsOrResult = $inputMatches[0].value
|
||||
if ($inputMatches[0].word === 'flow_input') {
|
||||
@@ -125,6 +136,13 @@
|
||||
if (Object.keys(filtered).length > 0) {
|
||||
resultByIdFiltered = filtered
|
||||
}
|
||||
} else if ($inputMatches[0].word === 'flow_env') {
|
||||
flowEnvFiltered = pickableProperties.flow_env
|
||||
let [, ...nestedKeys] = $inputMatches[0].value.split('.')
|
||||
let filtered = filterNestedObject(flowEnvFiltered, nestedKeys)
|
||||
if (Object.keys(filtered).length > 0) {
|
||||
flowEnvFiltered = filtered
|
||||
}
|
||||
}
|
||||
} else {
|
||||
filteringFlowInputsOrResult = ''
|
||||
@@ -143,7 +161,12 @@
|
||||
}
|
||||
|
||||
if (!collapsableInitialState) {
|
||||
collapsableInitialState = { allResultsCollapsed, displayVariable, displayResources }
|
||||
collapsableInitialState = {
|
||||
allResultsCollapsed,
|
||||
displayVariable,
|
||||
displayResources,
|
||||
displayFlowEnv
|
||||
}
|
||||
}
|
||||
|
||||
if ($inputMatches[0].word === 'variable') {
|
||||
@@ -156,6 +179,10 @@
|
||||
displayResources = true
|
||||
return
|
||||
}
|
||||
if ($inputMatches[0].word === 'flow_env') {
|
||||
displayFlowEnv = true
|
||||
return
|
||||
}
|
||||
if ($inputMatches[0].word === 'results') {
|
||||
allResultsCollapsed = false
|
||||
return
|
||||
@@ -166,7 +193,8 @@
|
||||
if (!collapsableInitialState) {
|
||||
return
|
||||
}
|
||||
;({ allResultsCollapsed, displayVariable, displayResources } = collapsableInitialState)
|
||||
;({ allResultsCollapsed, displayVariable, displayResources, displayFlowEnv } =
|
||||
collapsableInitialState)
|
||||
collapsableInitialState = undefined
|
||||
}
|
||||
|
||||
@@ -183,6 +211,7 @@
|
||||
if (prev && !filterActive) {
|
||||
flowInputsFiltered = pickableProperties.flow_input
|
||||
resultByIdFiltered = pickableProperties.priorIds
|
||||
flowEnvFiltered = pickableProperties.flow_env
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +221,7 @@
|
||||
await updateCollapsable()
|
||||
}
|
||||
|
||||
$: (search, $inputMatches, $propPickerConfig, pickableProperties, updateState())
|
||||
$: search, $inputMatches, $propPickerConfig, pickableProperties, updateState()
|
||||
|
||||
onDestroy(() => {
|
||||
clearTimeout(timeout)
|
||||
@@ -400,6 +429,45 @@
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if flow_env && Object.keys(flow_env).length > 0 && (!filterActive || $inputMatches?.some((match) => match.word === 'flow_env'))}
|
||||
<div class="overflow-y-auto pb-2">
|
||||
<span class="font-normal text-xs text-secondary">Flow Env Variables:</span>
|
||||
|
||||
{#if displayFlowEnv}
|
||||
<Button
|
||||
color="light"
|
||||
size="xs2"
|
||||
variant="border"
|
||||
on:click={() => {
|
||||
displayFlowEnv = false
|
||||
}}
|
||||
wrapperClasses="inline-flex whitespace-nowrap w-fit"
|
||||
btnClasses="font-mono h-4 text-2xs font-thin px-1 rounded-[0.275rem]">-</Button
|
||||
>
|
||||
<ObjectViewer
|
||||
{allowCopy}
|
||||
pureViewer={!$propPickerConfig}
|
||||
rawKey={false}
|
||||
json={flowEnvFiltered}
|
||||
prefix="flow_env"
|
||||
on:select
|
||||
/>
|
||||
{:else}
|
||||
<Button
|
||||
color="light"
|
||||
size="xs2"
|
||||
variant="border"
|
||||
on:click={() => {
|
||||
displayFlowEnv = true
|
||||
}}
|
||||
wrapperClasses="inline-flex whitespace-nowrap w-fit"
|
||||
btnClasses="font-normal text-2xs rounded-[0.275rem] h-4 px-1"
|
||||
>
|
||||
{'{...}'}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
<!-- </div> -->
|
||||
</Scrollable>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
export let result: any
|
||||
export let extraResults: any = undefined
|
||||
export let flow_input: any = undefined
|
||||
export let flow_env: any = undefined
|
||||
</script>
|
||||
|
||||
<div class="w-full px-2">
|
||||
@@ -18,4 +19,10 @@
|
||||
<ObjectViewer {allowCopy} json={flow_input} prefix="flow_input" on:select />
|
||||
</div>
|
||||
{/if}
|
||||
{#if flow_env}
|
||||
<span class="font-normal text-sm text-secondary">Flow Environment Variables</span>
|
||||
<div class="overflow-y-auto w-full">
|
||||
<ObjectViewer {allowCopy} json={flow_env} prefix="flow_env" on:select />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
<div
|
||||
bind:this={wrapperEl}
|
||||
class={twMerge(
|
||||
'flex items-center flex-wrap',
|
||||
'flex items-center flex-wrap relative',
|
||||
inputBaseClass,
|
||||
inputSizeClasses[size],
|
||||
inputBorderClass({ forceFocus: open && !disabled }),
|
||||
|
||||
@@ -169,10 +169,7 @@
|
||||
)}
|
||||
style="height: {uiState.visible ? dropdownPos.height : 0}px;"
|
||||
>
|
||||
<div
|
||||
bind:this={listEl}
|
||||
class="flex flex-col max-h-64 rounded-md overflow-clip bg-surface-tertiary"
|
||||
>
|
||||
<div bind:this={listEl} class="flex flex-col max-h-64 rounded-md bg-surface-tertiary">
|
||||
{@render header?.()}
|
||||
{#if processedItems?.length === 0}
|
||||
<div class="py-8 px-4 text-center text-primary text-xs">{noItemsMsg}</div>
|
||||
|
||||
@@ -10,13 +10,15 @@
|
||||
import type {
|
||||
ButtonComponent,
|
||||
CheckboxComponent,
|
||||
ModalComponent,
|
||||
SelectComponent
|
||||
} from '../apps/editor/component'
|
||||
interface Props {
|
||||
actionsOrder?: RichConfiguration | undefined
|
||||
selectedId?: string | undefined
|
||||
components:
|
||||
| (BaseAppComponent & (ButtonComponent | CheckboxComponent | SelectComponent))[]
|
||||
| (BaseAppComponent &
|
||||
(ButtonComponent | CheckboxComponent | SelectComponent | ModalComponent))[]
|
||||
| undefined
|
||||
trigger?: import('svelte').Snippet
|
||||
}
|
||||
|
||||
+2
-2
@@ -4,8 +4,8 @@ verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
wmill = ">=1.573.3"
|
||||
wmill_pg = ">=1.573.3"
|
||||
wmill = ">=1.573.5"
|
||||
wmill_pg = ">=1.573.5"
|
||||
sendgrid = "*"
|
||||
mysql-connector-python = "*"
|
||||
pymongo = "*"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.573.3
|
||||
version: 1.573.5
|
||||
title: OpenFlow Spec
|
||||
contact:
|
||||
name: Ruben Fiszel
|
||||
@@ -62,6 +62,10 @@ components:
|
||||
type: string
|
||||
cache_ttl:
|
||||
type: number
|
||||
flow_env:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
priority:
|
||||
type: number
|
||||
early_return:
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
RootModule = 'WindmillClient.psm1'
|
||||
|
||||
# Version number of this module.
|
||||
ModuleVersion = '1.573.3'
|
||||
ModuleVersion = '1.573.5'
|
||||
|
||||
# Supported PSEditions
|
||||
# CompatiblePSEditions = @()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "wmill"
|
||||
version = "1.573.3"
|
||||
version = "1.573.5"
|
||||
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 @@
|
||||
[tool.poetry]
|
||||
name = "wmill-pg"
|
||||
version = "1.573.3"
|
||||
version = "1.573.5"
|
||||
description = "An extension client for the wmill client library focused on pg"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://windmill.dev"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@windmill/windmill",
|
||||
"version": "1.573.3",
|
||||
"version": "1.573.5",
|
||||
"exports": "./src/index.ts",
|
||||
"publish": {
|
||||
"exclude": ["!src", "./s3Types.ts", "./client.ts"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "windmill-client",
|
||||
"description": "Windmill SDK client for browsers and Node.js",
|
||||
"version": "1.573.3",
|
||||
"version": "1.573.5",
|
||||
"author": "Ruben Fiszel",
|
||||
"license": "Apache 2.0",
|
||||
"devDependencies": {
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.573.3
|
||||
1.573.5
|
||||
|
||||
Reference in New Issue
Block a user