mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-25 16:02:11 +00:00
Merge branch 'main' into di/s3-proxy
This commit is contained in:
@@ -1,5 +1,70 @@
|
||||
# Changelog
|
||||
|
||||
## [1.532.0](https://github.com/windmill-labs/windmill/compare/v1.531.0...v1.532.0) (2025-08-22)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **aichat:** allow adding contexts to flow mode ([#6424](https://github.com/windmill-labs/windmill/issues/6424)) ([73272f1](https://github.com/windmill-labs/windmill/commit/73272f16fddc355703b04f2c3458520753d1e19c))
|
||||
* json schema resource ([#6433](https://github.com/windmill-labs/windmill/issues/6433)) ([7da79a8](https://github.com/windmill-labs/windmill/commit/7da79a8bc525fc6b89748ad0af25c2bac4ca2ef3))
|
||||
|
||||
## [1.531.0](https://github.com/windmill-labs/windmill/compare/v1.530.0...v1.531.0) (2025-08-22)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* ai agent steps ([#6393](https://github.com/windmill-labs/windmill/issues/6393)) ([958e8af](https://github.com/windmill-labs/windmill/commit/958e8af78290cf859f98c45c012ed41e3bada39e))
|
||||
* bump Go version from 1.22.0 to 1.25.0 [#6415](https://github.com/windmill-labs/windmill/issues/6415) ([c92bfe6](https://github.com/windmill-labs/windmill/commit/c92bfe6601fd96f6d74860f52f9307e02961ac21))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **app:** fix ctrl drag for insertion into subgrids ([51ea947](https://github.com/windmill-labs/windmill/commit/51ea9473ef23c6871699e69bbe79772a4d50d3b8))
|
||||
* **frontend:** graph cache of ai agent step tools ([#6431](https://github.com/windmill-labs/windmill/issues/6431)) ([28f1d61](https://github.com/windmill-labs/windmill/commit/28f1d611643459d42531fa217c185408eb97d6d1))
|
||||
* make relevant sidebar menu items a instead of button ([06d078e](https://github.com/windmill-labs/windmill/commit/06d078ebfa8f70b66bc764eae70d33c8c57b4012))
|
||||
* s3 result presigned not working with list ([9df008b](https://github.com/windmill-labs/windmill/commit/9df008b9f8fe58692463e4b9da0538935e458b10))
|
||||
|
||||
## [1.530.0](https://github.com/windmill-labs/windmill/compare/v1.529.0...v1.530.0) (2025-08-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **mcp:** add script preview testing tool ([#6417](https://github.com/windmill-labs/windmill/issues/6417)) ([ae49737](https://github.com/windmill-labs/windmill/commit/ae497376769f5cd49a41c22cf558a9f052d5b56e))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* aggrid newchange to point to correct idx ([#6425](https://github.com/windmill-labs/windmill/issues/6425)) ([511ff5e](https://github.com/windmill-labs/windmill/commit/511ff5e9f794c29c3c8a5fc0480675a1258fb056))
|
||||
* fix preprocessor preview ([47e49b2](https://github.com/windmill-labs/windmill/commit/47e49b243d8cf6d29fa5a59918a2168b87111352))
|
||||
* improve flow editor log streaming for individual tests ([ac066ab](https://github.com/windmill-labs/windmill/commit/ac066abb980501577cc96330a4a5f1309aa35661))
|
||||
|
||||
## [1.529.0](https://github.com/windmill-labs/windmill/compare/v1.528.0...v1.529.0) (2025-08-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add prometheus metric queue_running_count ([#6413](https://github.com/windmill-labs/windmill/issues/6413)) ([49ed757](https://github.com/windmill-labs/windmill/commit/49ed7574245784681f800199b6c7a47df5788e45))
|
||||
* **aichat:** add tool to test specific module in flow mode ([#6381](https://github.com/windmill-labs/windmill/issues/6381)) ([dfb32d2](https://github.com/windmill-labs/windmill/commit/dfb32d2949541ed149722cff88918d7c6e3dc307))
|
||||
* **frontend:** add relative line numbers toggle ([#6416](https://github.com/windmill-labs/windmill/issues/6416)) ([4349a20](https://github.com/windmill-labs/windmill/commit/4349a2024da2aa7406b16254fbfc427526718903))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cli:** pass HEADERS environment variable to fetch calls in generate-locks ([#6422](https://github.com/windmill-labs/windmill/issues/6422)) ([7f11eb9](https://github.com/windmill-labs/windmill/commit/7f11eb98b5682511e05c270d7ba860e3e0db61e9))
|
||||
* improve computeAssetNodes rendering caching and performance ([#6414](https://github.com/windmill-labs/windmill/issues/6414)) ([51568ee](https://github.com/windmill-labs/windmill/commit/51568eee025eab6e0069a8e719a562b721fc8c43))
|
||||
|
||||
## [1.528.0](https://github.com/windmill-labs/windmill/compare/v1.527.1...v1.528.0) (2025-08-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* native k8s autoscaling integration (EE) ([#6405](https://github.com/windmill-labs/windmill/issues/6405)) ([eaf4054](https://github.com/windmill-labs/windmill/commit/eaf4054bd380101856c02a0d07430ff3a0180880))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* flow status reactivity improvement ([#6402](https://github.com/windmill-labs/windmill/issues/6402)) ([5e73c49](https://github.com/windmill-labs/windmill/commit/5e73c49ab670be0f55794f5d0cb182de9efd500a))
|
||||
|
||||
## [1.527.1](https://github.com/windmill-labs/windmill/compare/v1.527.0...v1.527.1) (2025-08-16)
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,17 @@
|
||||
|
||||
Windmill is an open-source developer platform for building internal tools, workflows, API integrations, background jobs, workflows, and user interfaces. See @windmill-overview.mdc for full platform details.
|
||||
|
||||
## New Feature Implementation Guidelines
|
||||
|
||||
When implementing new features in Windmill, follow these best practices:
|
||||
|
||||
- **Clean Code First**: Write clean, readable, and maintainable code. Prioritize clarity over cleverness.
|
||||
- **Avoid Duplication at All Costs**: Before writing new code, thoroughly search for existing implementations that can be reused or extended.
|
||||
- **Adapt Existing Code**: Refactor and generalize existing code when necessary to avoid logic duplication. Extract common patterns into reusable utilities.
|
||||
- **Follow Established Patterns**: Study existing code patterns in the codebase and maintain consistency with established conventions.
|
||||
- **Single Responsibility**: Each function, component, and module should have a single, well-defined responsibility.
|
||||
- **Incremental Implementation**: Break large features into smaller, reviewable chunks that can be implemented and tested incrementally.
|
||||
|
||||
## Language-Specific Guides
|
||||
|
||||
- Backend (Rust): @backend/rust-best-practices.mdc + @backend/summarized_schema.txt
|
||||
|
||||
+1
-1
@@ -90,7 +90,7 @@ ARG POWERSHELL_VERSION=7.5.0
|
||||
ARG POWERSHELL_DEB_VERSION=7.5.0-1
|
||||
ARG KUBECTL_VERSION=1.28.7
|
||||
ARG HELM_VERSION=3.14.3
|
||||
ARG GO_VERSION=1.22.5
|
||||
ARG GO_VERSION=1.25.0
|
||||
ARG APP=/usr/src/app
|
||||
ARG WITH_POWERSHELL=true
|
||||
ARG WITH_KUBECTL=true
|
||||
|
||||
+6
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH inserted_job AS (\n INSERT INTO v2_job (id, workspace_id, raw_code, raw_lock, raw_flow, tag, parent_job,\n created_by, permissioned_as, runnable_id, runnable_path, args, kind, trigger,\n script_lang, same_worker, pre_run_error, permissioned_as_email, visible_to_owner,\n flow_innermost_root_job, root_job, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id,\n cache_ttl, priority, trigger_kind, script_entrypoint_override, preprocessed)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $38, $21, $22, $23, $24, $25, $26, $39::job_trigger_kind,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27)\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id) \n values ($1, $32, $33, $34, $35, $36, $37, $2) \n ON CONFLICT (job_id) DO UPDATE SET email = $32, username = $33, is_admin = $34, is_operator = $35, folders = $36, groups = $37, workspace_id = $2\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 THEN now() END, $30, $31)",
|
||||
"query": "WITH inserted_job AS (\n INSERT INTO v2_job (id, workspace_id, raw_code, raw_lock, raw_flow, tag, parent_job,\n created_by, permissioned_as, runnable_id, runnable_path, args, kind, trigger,\n script_lang, same_worker, pre_run_error, permissioned_as_email, visible_to_owner,\n flow_innermost_root_job, root_job, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id,\n cache_ttl, priority, trigger_kind, script_entrypoint_override, preprocessed)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $38, $21, $22, $23, $24, $25, $26, $39::job_trigger_kind,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27)\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id) \n values ($1, $32, $33, $34, $35, $36, $37, $2) \n ON CONFLICT (job_id) DO UPDATE SET email = $32, username = $33, is_admin = $34, is_operator = $35, folders = $36, groups = $37, workspace_id = $2\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 OR $40 THEN now() END, $30, $31)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -39,7 +39,8 @@
|
||||
"singlescriptflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript"
|
||||
"appscript",
|
||||
"aiagent"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -121,10 +122,11 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "acfe583fe17604ba72ba4800b62a72de0a9de0d58ef8c28dd709adf3be021597"
|
||||
"hash": "193d292c5ed44bf5266ad52c83704c3a36aa284fab3b7e638dbca12ac846b82b"
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT tag AS \"tag!\", count(*) AS \"count!\" FROM v2_job_queue WHERE\n running = true\n GROUP BY tag",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "tag!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "count!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "1b56a720d99a689e80d12ee1efbfeb71d7cd7bb17e936746749c958062cdff9e"
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE v2_job_status SET\n flow_status = jsonb_set(\n flow_status,\n array['modules', $2::TEXT, 'agent_actions_success'],\n COALESCE(\n flow_status->'modules'->$2->'agent_actions_success',\n to_jsonb(ARRAY[]::bool[])\n ) || to_jsonb(ARRAY[$3::bool])\n )\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "3e3afba04a10f16606e17cea6b31d9578cac41b76092722fd2698afb4cf08834"
|
||||
}
|
||||
+2
-1
@@ -28,7 +28,8 @@
|
||||
"singlescriptflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript"
|
||||
"appscript",
|
||||
"aiagent"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -40,7 +40,8 @@
|
||||
"singlescriptflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript"
|
||||
"appscript",
|
||||
"aiagent"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -68,7 +68,8 @@
|
||||
"singlescriptflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript"
|
||||
"appscript",
|
||||
"aiagent"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -98,7 +98,8 @@
|
||||
"singlescriptflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript"
|
||||
"appscript",
|
||||
"aiagent"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -148,7 +149,8 @@
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb"
|
||||
"duckdb",
|
||||
"ruby"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -38,7 +38,8 @@
|
||||
"singlescriptflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript"
|
||||
"appscript",
|
||||
"aiagent"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT runnable_id as \"runnable_id: ScriptHash\", raw_flow as \"raw_flow: _\", kind as \"kind: _\" FROM v2_job WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "runnable_id: ScriptHash",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "raw_flow: _",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "kind: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "job_kind",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"script",
|
||||
"preview",
|
||||
"flow",
|
||||
"dependencies",
|
||||
"flowpreview",
|
||||
"script_hub",
|
||||
"identity",
|
||||
"flowdependencies",
|
||||
"http",
|
||||
"graphql",
|
||||
"postgresql",
|
||||
"noop",
|
||||
"appdependencies",
|
||||
"deploymentcallback",
|
||||
"singlescriptflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript",
|
||||
"aiagent"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "805d633de90fee335f1726284eda0dbc200d45960fb8dea867492c8c7dd096d5"
|
||||
}
|
||||
+2
-1
@@ -28,7 +28,8 @@
|
||||
"singlescriptflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript"
|
||||
"appscript",
|
||||
"aiagent"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE v2_job_status SET\n flow_status = jsonb_set(\n flow_status,\n array['modules', $3::TEXT, 'agent_actions'],\n $2\n )\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Jsonb",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "9a923c85a015e4149328f650e77872a1c39d5992efd6abd2d3b8a558d7b884a1"
|
||||
}
|
||||
+2
-1
@@ -98,7 +98,8 @@
|
||||
"singlescriptflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript"
|
||||
"appscript",
|
||||
"aiagent"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -35,7 +35,8 @@
|
||||
"singlescriptflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript"
|
||||
"appscript",
|
||||
"aiagent"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -33,7 +33,8 @@
|
||||
"singlescriptflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript"
|
||||
"appscript",
|
||||
"aiagent"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -18,8 +18,8 @@
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76"
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT value from resource WHERE path = $1 AND workspace_id = $2 AND resource_type = 'json_schema'",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "value",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "d4c963fa653652b7a3e8529cbf0d0fca091d7c1cb0924f6f9343544abb2666a5"
|
||||
}
|
||||
-248
@@ -1,248 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n cj.id AS \"id!\",\n cj.workspace_id AS \"workspace_id!\",\n cj.parent_job,\n cj.created_by AS \"created_by!\",\n cj.duration_ms AS \"duration_ms!\",\n cj.success AS \"success!\",\n cj.script_hash AS \"script_hash!: Option<ScriptHash>\",\n cj.script_path,\n cj.args AS \"args: sqlx::types::Json<HashMap<String, Box<RawValue>>>\",\n cj.result AS \"result: sqlx::types::Json<Box<RawValue>>\",\n cj.deleted AS \"deleted!\",\n cj.canceled AS \"canceled!\",\n cj.canceled_by,\n cj.canceled_reason,\n cj.job_kind AS \"job_kind!: JobKind\",\n cj.schedule_path,\n cj.permissioned_as AS \"permissioned_as!\",\n cj.is_flow_step AS \"is_flow_step!\",\n cj.language AS \"language: ScriptLang\",\n cj.is_skipped AS \"is_skipped!\",\n cj.email AS \"email!\",\n cj.visible_to_owner AS \"visible_to_owner!\",\n cj.mem_peak,\n cj.tag AS \"tag!\",\n cj.created_at AS \"created_at!\",\n cj.started_at,\n job_logs.logs,\n job_logs.log_offset AS \"log_offset?\",\n job_logs.log_file_index\n\n FROM v2_as_completed_job AS cj\n LEFT JOIN job_logs ON cj.id = job_logs.job_id\n WHERE cj.created_at < $1\n ORDER BY cj.created_at ASC LIMIT $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id!",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "workspace_id!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "parent_job",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "created_by!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "duration_ms!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "success!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "script_hash!: Option<ScriptHash>",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "script_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "args: sqlx::types::Json<HashMap<String, Box<RawValue>>>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "result: sqlx::types::Json<Box<RawValue>>",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "deleted!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "canceled!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "canceled_by",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "canceled_reason",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "job_kind!: JobKind",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "job_kind",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"script",
|
||||
"preview",
|
||||
"flow",
|
||||
"dependencies",
|
||||
"flowpreview",
|
||||
"script_hub",
|
||||
"identity",
|
||||
"flowdependencies",
|
||||
"http",
|
||||
"graphql",
|
||||
"postgresql",
|
||||
"noop",
|
||||
"appdependencies",
|
||||
"deploymentcallback",
|
||||
"singlescriptflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "schedule_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "permissioned_as!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "is_flow_step!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"name": "language: ScriptLang",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "script_lang",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"python3",
|
||||
"deno",
|
||||
"go",
|
||||
"bash",
|
||||
"postgresql",
|
||||
"nativets",
|
||||
"bun",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
"snowflake",
|
||||
"graphql",
|
||||
"powershell",
|
||||
"mssql",
|
||||
"php",
|
||||
"bunnative",
|
||||
"rust",
|
||||
"ansible",
|
||||
"csharp",
|
||||
"oracledb",
|
||||
"nu",
|
||||
"java",
|
||||
"duckdb",
|
||||
"ruby"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "is_skipped!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"name": "email!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 21,
|
||||
"name": "visible_to_owner!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"name": "mem_peak",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 23,
|
||||
"name": "tag!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 24,
|
||||
"name": "created_at!",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 25,
|
||||
"name": "started_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 26,
|
||||
"name": "logs",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 27,
|
||||
"name": "log_offset?",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 28,
|
||||
"name": "log_file_index",
|
||||
"type_info": "TextArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Timestamptz",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "f22964772dc2d67aee437bbbd08b64792c00da1d713d7ca8f9904ccce7bfdae7"
|
||||
}
|
||||
+2
-1
@@ -35,7 +35,8 @@
|
||||
"singlescriptflow",
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript"
|
||||
"appscript",
|
||||
"aiagent"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+593
-214
File diff suppressed because it is too large
Load Diff
+7
-4
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.527.1"
|
||||
version = "1.532.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -33,7 +33,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.527.1"
|
||||
version = "1.532.0"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -49,7 +49,7 @@ incremental = true
|
||||
lto = "thin"
|
||||
|
||||
[features]
|
||||
default = ["ruby"]
|
||||
default = []
|
||||
private = ["windmill-api/private", "windmill-autoscaling/private", "windmill-common/private", "windmill-git-sync/private", "windmill-indexer/private", "windmill-queue/private", "windmill-worker/private"]
|
||||
agent_worker_server = ["windmill-api/agent_worker_server"]
|
||||
enterprise = ["windmill-worker/enterprise", "windmill-queue/enterprise", "windmill-api/enterprise", "dep:windmill-autoscaling", "windmill-autoscaling/enterprise", "windmill-git-sync/enterprise", "windmill-common/prometheus", "windmill-common/enterprise"]
|
||||
@@ -148,6 +148,8 @@ size.workspace = true
|
||||
strum.workspace = true
|
||||
aws-sigv4.workspace = true
|
||||
aws-sdk-config.workspace = true
|
||||
kube.workspace = true
|
||||
k8s-openapi.workspace = true
|
||||
|
||||
[target.'cfg(not(target_env = "msvc"))'.dependencies]
|
||||
tikv-jemallocator = { optional = true, workspace = true }
|
||||
@@ -399,6 +401,8 @@ systemstat = "0.2.4"
|
||||
size = "0.5.0"
|
||||
|
||||
flume = { version = "0.11.1", features = ["async"] }
|
||||
kube = { version = "1.1.0", features = ["runtime", "derive"] }
|
||||
k8s-openapi = { version = "0.25.0", features = ["latest"] }
|
||||
|
||||
# Macro-related
|
||||
proc-macro2 = "1.0"
|
||||
@@ -417,4 +421,3 @@ oracle = { version = "0.6.3", features = ["chrono"] }
|
||||
rumqttc = { version = "0.24.0", features = ["use-native-tls"]}
|
||||
strum = { version = "0.27", features = ["derive"] }
|
||||
strum_macros = "^0"
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
d04959121c6869d764e11e005676003fd2919629
|
||||
15a7592ca66b93b9760d49e58b23c090ead06fe2
|
||||
|
||||
@@ -41,7 +41,7 @@ def load_openapi_spec(file_path: str) -> Dict[str, Any]:
|
||||
print(f"Error loading OpenAPI spec: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Optional[Dict[str, Any]], spec: Dict[str, Any]) -> tuple:
|
||||
def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Optional[Dict[str, Any]], spec: Dict[str, Any], required_fields: Optional[List[str]] = None) -> tuple:
|
||||
"""Extract separate schemas for path parameters, query parameters, and request body."""
|
||||
path_params_schema = {
|
||||
"type": "object",
|
||||
@@ -92,6 +92,20 @@ def extract_separate_schemas(parameters: List[Dict[str, Any]], request_body: Opt
|
||||
# Process request body if present
|
||||
if request_body:
|
||||
body_schema = extract_request_body_schema(request_body, spec)
|
||||
|
||||
# If we have required fields specified and a body schema, update the required array
|
||||
if body_schema and required_fields:
|
||||
if 'required' not in body_schema:
|
||||
body_schema['required'] = []
|
||||
|
||||
# Add each required field if it exists in the schema properties
|
||||
for field in required_fields:
|
||||
if 'properties' in body_schema and field in body_schema['properties']:
|
||||
if field not in body_schema['required']:
|
||||
body_schema['required'].append(field)
|
||||
else:
|
||||
# Log warning when a required field is missing from schema properties
|
||||
print(f"Warning: Required field '{field}' not found in body schema properties", file=sys.stderr)
|
||||
|
||||
# Return None for empty schemas
|
||||
path_params_schema = path_params_schema if path_params_schema['properties'] else None
|
||||
@@ -199,6 +213,7 @@ def find_mcp_tools(spec: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
'method': method.upper(),
|
||||
'parameters': operation.get('parameters', []),
|
||||
'requestBody': operation.get('requestBody'),
|
||||
'required_fields': operation.get('x-mcp-required-fields', []),
|
||||
}
|
||||
tools.append(tool)
|
||||
|
||||
@@ -227,7 +242,7 @@ pub fn all_tools() -> Vec<EndpointTool> {
|
||||
|
||||
# Generate separate schemas
|
||||
path_params_schema, query_params_schema, body_schema = extract_separate_schemas(
|
||||
tool['parameters'], tool['requestBody'], spec
|
||||
tool['parameters'], tool['requestBody'], spec, tool['required_fields']
|
||||
)
|
||||
|
||||
path_params_rust = schema_to_rust_value(path_params_schema)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
-- Add down migration script here
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add up migration script here
|
||||
ALTER TYPE JOB_KIND ADD VALUE IF NOT EXISTS 'aiagent';
|
||||
+45
-3
@@ -114,6 +114,12 @@ lazy_static::lazy_static! {
|
||||
&["tag"]
|
||||
).unwrap();
|
||||
|
||||
static ref QUEUE_RUNNING_COUNT: prometheus::IntGaugeVec = prometheus::register_int_gauge_vec!(
|
||||
"queue_running_count",
|
||||
"Number of running jobs in the queue",
|
||||
&["tag"]
|
||||
).unwrap();
|
||||
|
||||
}
|
||||
lazy_static::lazy_static! {
|
||||
static ref ZOMBIE_JOB_TIMEOUT: String = std::env::var("ZOMBIE_JOB_TIMEOUT")
|
||||
@@ -140,6 +146,7 @@ lazy_static::lazy_static! {
|
||||
pub static ref WORKERS_NAMES: Arc<RwLock<Vec<String>>> = Arc::new(RwLock::new(Vec::new()));
|
||||
|
||||
static ref QUEUE_COUNT_TAGS: Arc<RwLock<Vec<String>>> = Arc::new(RwLock::new(Vec::new()));
|
||||
static ref QUEUE_RUNNING_COUNT_TAGS: Arc<RwLock<Vec<String>>> = Arc::new(RwLock::new(Vec::new()));
|
||||
static ref DISABLE_CONCURRENCY_LIMIT: bool = std::env::var("DISABLE_CONCURRENCY_LIMIT").is_ok_and(|s| s == "true");
|
||||
|
||||
static ref STALE_JOB_TRESHOLD_MINUTES: Option<u64> = std::env::var("STALE_JOB_TRESHOLD_MINUTES")
|
||||
@@ -1324,7 +1331,10 @@ pub async fn reload_url_list_setting(
|
||||
match url::Url::parse(url_str) {
|
||||
Ok(url) => urls.push(url),
|
||||
Err(e) => {
|
||||
return Err(error::Error::BadRequest(format!("Invalid URL in FORCE_{}: '{}': {}", std_env_var, url_str, e)));
|
||||
return Err(error::Error::BadRequest(format!(
|
||||
"Invalid URL in FORCE_{}: '{}': {}",
|
||||
std_env_var, url_str, e
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1347,7 +1357,11 @@ pub async fn reload_url_list_setting(
|
||||
}
|
||||
}
|
||||
}
|
||||
if urls.is_empty() { None } else { Some(urls) }
|
||||
if urls.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(urls)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -1365,7 +1379,11 @@ pub async fn reload_url_list_setting(
|
||||
}
|
||||
}
|
||||
}
|
||||
tracing::info!("Loaded setting {} from db config: {} URLs", setting_name, urls.len());
|
||||
tracing::info!(
|
||||
"Loaded setting {} from db config: {} URLs",
|
||||
setting_name,
|
||||
urls.len()
|
||||
);
|
||||
value = if urls.is_empty() { None } else { Some(urls) };
|
||||
} else {
|
||||
tracing::error!("Could not parse {} found: {:#?}", setting_name, &q);
|
||||
@@ -1671,6 +1689,30 @@ pub async fn expose_queue_metrics(db: &Pool<Postgres>) {
|
||||
let mut w = QUEUE_COUNT_TAGS.write().await;
|
||||
*w = tags_to_watch;
|
||||
}
|
||||
|
||||
#[cfg(feature = "prometheus")]
|
||||
if metrics_enabled {
|
||||
// Handle queue running count metrics
|
||||
let queue_running_counts = windmill_common::queue::get_queue_running_counts(db).await;
|
||||
|
||||
for q in QUEUE_RUNNING_COUNT_TAGS.read().await.iter() {
|
||||
if queue_running_counts.get(q).is_none() {
|
||||
(*QUEUE_RUNNING_COUNT).with_label_values(&[q]).set(0);
|
||||
}
|
||||
}
|
||||
|
||||
let mut running_tags_to_watch = vec![];
|
||||
for q in queue_running_counts {
|
||||
let count = q.1;
|
||||
let tag = q.0;
|
||||
|
||||
let metric = (*QUEUE_RUNNING_COUNT).with_label_values(&[&tag]);
|
||||
metric.set(count as i64);
|
||||
running_tags_to_watch.push(tag.to_string());
|
||||
}
|
||||
let mut w = QUEUE_RUNNING_COUNT_TAGS.write().await;
|
||||
*w = running_tags_to_watch;
|
||||
}
|
||||
}
|
||||
|
||||
// clean queue metrics older than 14 days
|
||||
|
||||
@@ -959,6 +959,7 @@ impl RunJob {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.expect("push has to succeed");
|
||||
|
||||
@@ -50,6 +50,7 @@ windmill-parser-py.workspace = true
|
||||
windmill-parser-py-imports.workspace = true
|
||||
windmill-git-sync.workspace = true
|
||||
windmill-indexer = { workspace = true, optional = true }
|
||||
windmill-autoscaling.workspace = true
|
||||
windmill-worker.workspace = true
|
||||
tokio.workspace = true
|
||||
tokio-stream.workspace = true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.527.1
|
||||
version: 1.532.0
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -7193,6 +7193,36 @@ paths:
|
||||
type: string
|
||||
format: uuid
|
||||
|
||||
/w/{workspace}/jobs/run_wait_result/preview:
|
||||
post:
|
||||
summary: run script preview and wait for result
|
||||
operationId: runScriptPreviewAndWaitResult
|
||||
x-mcp-tool: true
|
||||
x-mcp-instructions: Allows testing a script before deploying it. For typescript code, the language to send is either bun or deno. By default, send bun if no deno specific code is detected.
|
||||
x-mcp-required-fields:
|
||||
- content
|
||||
- language
|
||||
- args
|
||||
tags:
|
||||
- job
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
|
||||
requestBody:
|
||||
description: preview
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Preview"
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: job result
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
|
||||
/w/{workspace}/jobs/workflow_as_code/{job_id}/{entrypoint}:
|
||||
post:
|
||||
summary: run code-workflow task
|
||||
@@ -7302,6 +7332,30 @@ paths:
|
||||
type: string
|
||||
format: uuid
|
||||
|
||||
/w/{workspace}/jobs/run_wait_result/preview_flow:
|
||||
post:
|
||||
summary: run flow preview and wait for result
|
||||
operationId: runFlowPreviewAndWaitResult
|
||||
tags:
|
||||
- job
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
|
||||
requestBody:
|
||||
description: preview
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/FlowPreview"
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: job result
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
|
||||
/w/{workspace}/jobs/queue/list:
|
||||
get:
|
||||
summary: list all queued jobs
|
||||
@@ -11502,6 +11556,22 @@ paths:
|
||||
additionalProperties:
|
||||
type: integer
|
||||
|
||||
/workers/queue_running_counts:
|
||||
get:
|
||||
summary: get counts of currently running jobs per tag
|
||||
operationId: getCountsOfRunningJobsPerTag
|
||||
tags:
|
||||
- worker
|
||||
responses:
|
||||
"200":
|
||||
description: queue running counts
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: integer
|
||||
|
||||
/configs/list_worker_groups:
|
||||
get:
|
||||
summary: list worker groups
|
||||
@@ -11615,6 +11685,22 @@ paths:
|
||||
items:
|
||||
$ref: "#/components/schemas/AutoscalingEvent"
|
||||
|
||||
/configs/native_kubernetes_autoscaling_healthcheck:
|
||||
get:
|
||||
summary: Check Kubernetes autoscaling health for a worker group
|
||||
operationId: nativeKubernetesAutoscalingHealthcheck
|
||||
tags:
|
||||
- config
|
||||
responses:
|
||||
"200":
|
||||
description: Kubernetes autoscaling is healthy
|
||||
"400":
|
||||
description: Error
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/configs/list_available_python_versions:
|
||||
get:
|
||||
summary: Get currently available python versions provided by UV.
|
||||
@@ -13929,6 +14015,8 @@ components:
|
||||
$ref: "../../openflow.openapi.yaml#/components/schemas/BranchOne"
|
||||
BranchAll:
|
||||
$ref: "../../openflow.openapi.yaml#/components/schemas/BranchAll"
|
||||
AiAgent:
|
||||
$ref: "../../openflow.openapi.yaml#/components/schemas/AiAgent"
|
||||
Identity:
|
||||
$ref: "../../openflow.openapi.yaml#/components/schemas/Identity"
|
||||
FlowStatus:
|
||||
@@ -14411,6 +14499,7 @@ components:
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript",
|
||||
"aiagent",
|
||||
]
|
||||
schedule_path:
|
||||
type: string
|
||||
@@ -14520,6 +14609,7 @@ components:
|
||||
"flowscript",
|
||||
"flownode",
|
||||
"appscript",
|
||||
"aiagent",
|
||||
]
|
||||
schedule_path:
|
||||
type: string
|
||||
@@ -15138,10 +15228,13 @@ components:
|
||||
properties:
|
||||
content:
|
||||
type: string
|
||||
description: The code to run
|
||||
path:
|
||||
type: string
|
||||
description: The path to the script
|
||||
script_hash:
|
||||
type: string
|
||||
description: The hash of the script
|
||||
args:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
language:
|
||||
|
||||
@@ -762,20 +762,26 @@ async fn get_public_resource(
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> JsonResult<Option<serde_json::Value>> {
|
||||
let path = path.to_path();
|
||||
if !path.starts_with("f/app_themes/") {
|
||||
return Err(Error::BadRequest(
|
||||
"Only app themes are public resources".to_string(),
|
||||
));
|
||||
}
|
||||
let res = sqlx::query_scalar!(
|
||||
"SELECT value from resource WHERE path = $1 AND workspace_id = $2",
|
||||
path.to_owned(),
|
||||
&w_id
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?
|
||||
.flatten();
|
||||
Ok(Json(res))
|
||||
|
||||
let res = if path.starts_with("f/app_themes/") {
|
||||
sqlx::query_scalar!(
|
||||
"SELECT value from resource WHERE path = $1 AND workspace_id = $2",
|
||||
path.to_owned(),
|
||||
&w_id
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?
|
||||
} else {
|
||||
sqlx::query_scalar!(
|
||||
"SELECT value from resource WHERE path = $1 AND workspace_id = $2 AND resource_type = 'json_schema'",
|
||||
path.to_owned(),
|
||||
&w_id
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?
|
||||
};
|
||||
|
||||
Ok(Json(res.flatten()))
|
||||
}
|
||||
|
||||
async fn get_secret_id(
|
||||
@@ -1092,6 +1098,7 @@ async fn create_app_internal<'a>(
|
||||
None,
|
||||
None,
|
||||
Some(&authed.clone().into()),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tracing::info!("Pushed app dependency job {}", dependency_job_uuid);
|
||||
@@ -1469,6 +1476,7 @@ async fn update_app_internal<'a>(
|
||||
None,
|
||||
None,
|
||||
Some(&authed.clone().into()),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tracing::info!("Pushed app dependency job {}", dependency_job_uuid);
|
||||
@@ -1782,6 +1790,7 @@ async fn execute_component(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
@@ -34,6 +34,10 @@ pub fn global_service() -> Router {
|
||||
"/list_autoscaling_events/:worker_group",
|
||||
get(list_autoscaling_events),
|
||||
)
|
||||
.route(
|
||||
"/native_kubernetes_autoscaling_healthcheck",
|
||||
get(native_kubernetes_autoscaling_healthcheck),
|
||||
)
|
||||
.route(
|
||||
"/list_available_python_versions",
|
||||
get(list_available_python_versions),
|
||||
@@ -246,6 +250,26 @@ async fn list_autoscaling_events(
|
||||
Ok(Json(events))
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "enterprise", feature = "private"))]
|
||||
async fn native_kubernetes_autoscaling_healthcheck(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
) -> Result<(), windmill_autoscaling::kubernetes_integration_ee::KubeError> {
|
||||
require_devops_role(&db, &authed.email).await.map_err(|e| {
|
||||
windmill_autoscaling::kubernetes_integration_ee::KubeError::Other(e.to_string())
|
||||
})?;
|
||||
|
||||
windmill_autoscaling::kubernetes_integration_ee::kubernetes_healthcheck().await
|
||||
}
|
||||
|
||||
#[cfg(not(all(feature = "enterprise", feature = "private")))]
|
||||
async fn native_kubernetes_autoscaling_healthcheck(
|
||||
) -> Result<(), error::Error> {
|
||||
Err(error::Error::BadRequest(
|
||||
"Native Kubernetes autoscaling available only in the enterprise version".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn list_available_python_versions() -> error::JsonResult<Vec<String>> {
|
||||
#[cfg(not(feature = "python"))]
|
||||
return Err(error::Error::BadRequest(
|
||||
|
||||
@@ -519,6 +519,7 @@ async fn create_flow(
|
||||
None,
|
||||
None,
|
||||
Some(&authed.clone().into()),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -985,6 +986,7 @@ async fn update_flow(
|
||||
None,
|
||||
None,
|
||||
Some(&authed.clone().into()),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
sqlx::query!(
|
||||
|
||||
@@ -177,12 +177,14 @@ pub fn workspaced_service() -> Router {
|
||||
.layer(ce_headers.clone()),
|
||||
)
|
||||
.route("/run/preview", post(run_preview_script))
|
||||
.route("/run_wait_result/preview", post(run_wait_result_preview_script))
|
||||
.route(
|
||||
"/run/preview_bundle",
|
||||
post(run_bundle_preview_script).layer(axum::extract::DefaultBodyLimit::disable()),
|
||||
)
|
||||
.route("/add_batch_jobs/:n", post(add_batch_jobs))
|
||||
.route("/run/preview_flow", post(run_preview_flow_job))
|
||||
.route("/run_wait_result/preview_flow", post(run_wait_result_preview_flow))
|
||||
.route("/list", get(list_jobs))
|
||||
.route(
|
||||
"/list_selected_job_groups",
|
||||
@@ -3883,6 +3885,7 @@ pub async fn run_flow_by_path_inner(
|
||||
None,
|
||||
None,
|
||||
push_authed.as_ref(),
|
||||
false
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -3977,6 +3980,7 @@ pub async fn restart_flow(
|
||||
None,
|
||||
completed_job.priority,
|
||||
Some(&authed.clone().into()),
|
||||
false
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -4072,6 +4076,7 @@ pub async fn run_script_by_path_inner(
|
||||
None,
|
||||
None,
|
||||
push_authed.as_ref(),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -4220,6 +4225,7 @@ pub async fn run_workflow_as_code(
|
||||
None,
|
||||
None,
|
||||
push_authed.as_ref(),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -4750,6 +4756,7 @@ pub async fn run_wait_result_job_by_path_get(
|
||||
None,
|
||||
None,
|
||||
push_authed.as_ref(),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -4900,6 +4907,7 @@ pub async fn run_wait_result_script_by_path_internal(
|
||||
None,
|
||||
None,
|
||||
push_authed.as_ref(),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -5014,6 +5022,7 @@ pub async fn run_wait_result_script_by_hash(
|
||||
None,
|
||||
None,
|
||||
push_authed.as_ref(),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -5131,6 +5140,7 @@ pub async fn run_wait_result_flow_by_path_internal(
|
||||
None,
|
||||
None,
|
||||
push_authed.as_ref(),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -5200,6 +5210,7 @@ async fn run_preview_script(
|
||||
None,
|
||||
None,
|
||||
Some(&authed.clone().into()),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -5207,6 +5218,28 @@ async fn run_preview_script(
|
||||
Ok((StatusCode::CREATED, uuid.to_string()))
|
||||
}
|
||||
|
||||
async fn run_wait_result_preview_script(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path(w_id): Path<String>,
|
||||
Query(run_query): Query<RunJobQuery>,
|
||||
Json(preview): Json<Preview>,
|
||||
) -> error::Result<Response> {
|
||||
|
||||
let (_status_code, uuid) = run_preview_script(
|
||||
authed.clone(),
|
||||
Extension(db.clone()),
|
||||
Extension(user_db.clone()),
|
||||
Path(w_id.clone()),
|
||||
Query(run_query.clone()),
|
||||
Json(preview)
|
||||
).await?;
|
||||
let uuid = uuid.parse::<Uuid>().map_err(|_| Error::BadRequest("Invalid UUID".to_string()))?;
|
||||
let result = run_wait_result(&db, uuid, w_id, None, &authed.username).await;
|
||||
return result;
|
||||
}
|
||||
|
||||
async fn run_bundle_preview_script(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
@@ -5288,6 +5321,7 @@ async fn run_bundle_preview_script(
|
||||
None,
|
||||
None,
|
||||
Some(&authed.clone().into()),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
job_id = Some(uuid);
|
||||
@@ -5453,6 +5487,7 @@ async fn run_dependencies_job(
|
||||
None,
|
||||
None,
|
||||
Some(&authed.clone().into()),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -5518,6 +5553,7 @@ async fn run_flow_dependencies_job(
|
||||
None,
|
||||
None,
|
||||
Some(&authed.clone().into()),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -5858,6 +5894,7 @@ async fn run_preview_flow_job(
|
||||
None,
|
||||
None,
|
||||
Some(&authed.clone().into()),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -5865,6 +5902,20 @@ async fn run_preview_flow_job(
|
||||
Ok((StatusCode::CREATED, uuid.to_string()))
|
||||
}
|
||||
|
||||
async fn run_wait_result_preview_flow(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path(w_id): Path<String>,
|
||||
Query(run_query): Query<RunJobQuery>,
|
||||
Json(raw_flow): Json<PreviewFlow>,
|
||||
) -> error::Result<Response> {
|
||||
let (_status_code, uuid) = run_preview_flow_job(authed.clone(), Extension(db.clone()), Extension(user_db.clone()), Path(w_id.clone()), Query(run_query.clone()), Json(raw_flow)).await?;
|
||||
let uuid = uuid.parse::<Uuid>().map_err(|_| Error::BadRequest("Invalid UUID".to_string()))?;
|
||||
let result = run_wait_result(&db, uuid, w_id, None, &authed.username).await;
|
||||
return result;
|
||||
}
|
||||
|
||||
pub async fn run_job_by_hash(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
@@ -5983,6 +6034,7 @@ pub async fn run_job_by_hash_inner(
|
||||
None,
|
||||
None,
|
||||
push_authed.as_ref(),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
@@ -564,6 +564,86 @@ pub fn all_tools() -> Vec<EndpointTool> {
|
||||
})),
|
||||
body_schema: None,
|
||||
},
|
||||
EndpointTool {
|
||||
name: Cow::Borrowed("runScriptPreviewAndWaitResult"),
|
||||
description: Cow::Borrowed("run script preview and wait for result"),
|
||||
instructions: Cow::Borrowed("Allows testing a script before deploying it. For typescript code, the language to send is either bun or deno. By default, send bun if no deno specific code is detected."),
|
||||
path: Cow::Borrowed("/w/{workspace}/jobs/run_wait_result/preview"),
|
||||
method: Cow::Borrowed("POST"),
|
||||
path_params_schema: None,
|
||||
query_params_schema: None,
|
||||
body_schema: Some(serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "The code to run"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "The path to the script"
|
||||
},
|
||||
"script_hash": {
|
||||
"type": "string",
|
||||
"description": "The hash of the script"
|
||||
},
|
||||
"args": {
|
||||
"type": "object",
|
||||
"description": "The arguments to pass to the script or flow",
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"python3",
|
||||
"deno",
|
||||
"go",
|
||||
"bash",
|
||||
"powershell",
|
||||
"postgresql",
|
||||
"mysql",
|
||||
"bigquery",
|
||||
"snowflake",
|
||||
"mssql",
|
||||
"oracledb",
|
||||
"graphql",
|
||||
"nativets",
|
||||
"bun",
|
||||
"php",
|
||||
"rust",
|
||||
"ansible",
|
||||
"csharp",
|
||||
"nu",
|
||||
"java",
|
||||
"ruby",
|
||||
"duckdb"
|
||||
]
|
||||
},
|
||||
"tag": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"code",
|
||||
"identity",
|
||||
"http"
|
||||
]
|
||||
},
|
||||
"dedicated_worker": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"lock": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"args",
|
||||
"content",
|
||||
"language"
|
||||
]
|
||||
})),
|
||||
},
|
||||
EndpointTool {
|
||||
name: Cow::Borrowed("listQueue"),
|
||||
description: Cow::Borrowed("list all queued jobs"),
|
||||
|
||||
@@ -1023,6 +1023,7 @@ async fn create_script_internal<'c>(
|
||||
None,
|
||||
None,
|
||||
Some(&authed.clone().into()),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
Ok((hash, new_tx, None))
|
||||
|
||||
@@ -798,6 +798,7 @@ async fn trigger_script_with_retry_and_error_handler(
|
||||
None,
|
||||
None,
|
||||
push_authed.as_ref(),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
@@ -37,6 +37,7 @@ pub fn global_service() -> Router {
|
||||
.route("/get_default_tags", get(get_default_tags))
|
||||
.route("/queue_metrics", get(get_queue_metrics))
|
||||
.route("/queue_counts", get(get_queue_counts))
|
||||
.route("/queue_running_counts", get(get_queue_running_counts))
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize, Deserialize)]
|
||||
@@ -219,3 +220,12 @@ async fn get_queue_counts(
|
||||
let queue_counts = windmill_common::queue::get_queue_counts(&db).await;
|
||||
Ok(Json(queue_counts))
|
||||
}
|
||||
|
||||
async fn get_queue_running_counts(
|
||||
authed: ApiAuthed,
|
||||
Extension(db): Extension<DB>,
|
||||
) -> JsonResult<std::collections::HashMap<String, u32>> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
let queue_running_counts = windmill_common::queue::get_queue_running_counts(&db).await;
|
||||
Ok(Json(queue_running_counts))
|
||||
}
|
||||
|
||||
@@ -21,4 +21,9 @@ serde_json.workspace = true
|
||||
tracing.workspace = true
|
||||
windmill-common = { workspace = true, default-features = false }
|
||||
windmill-queue.workspace = true
|
||||
anyhow.workspace = true
|
||||
anyhow.workspace = true
|
||||
kube.workspace = true
|
||||
k8s-openapi.workspace = true
|
||||
tokio.workspace = true
|
||||
thiserror.workspace = true
|
||||
axum.workspace = true
|
||||
|
||||
@@ -2,3 +2,7 @@
|
||||
pub mod autoscaling_ee;
|
||||
mod autoscaling_oss;
|
||||
pub use autoscaling_oss::*;
|
||||
#[cfg(feature = "private")]
|
||||
pub mod kubernetes_integration_ee;
|
||||
#[cfg(feature = "private")]
|
||||
pub use kubernetes_integration_ee::{apply_kubernetes_autoscaling, KubernetesIntegration};
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::{
|
||||
error::{Error, Result},
|
||||
jwt,
|
||||
users::{SUPERADMIN_NOTIFICATION_EMAIL, SUPERADMIN_SECRET_EMAIL, SUPERADMIN_SYNC_EMAIL},
|
||||
utils::WarnAfterExt,
|
||||
DB,
|
||||
};
|
||||
|
||||
@@ -259,6 +260,22 @@ pub async fn get_groups_for_user(
|
||||
Ok(groups)
|
||||
}
|
||||
|
||||
pub async fn get_job_perms<'a, E: sqlx::PgExecutor<'a>>(
|
||||
db: E,
|
||||
job_id: &Uuid,
|
||||
w_id: &str,
|
||||
) -> sqlx::Result<Option<JobPerms>> {
|
||||
sqlx::query_as!(
|
||||
JobPerms,
|
||||
"SELECT email, username, is_admin, is_operator, groups, folders FROM job_perms WHERE job_id = $1 AND workspace_id = $2",
|
||||
job_id,
|
||||
w_id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.warn_after_seconds(3)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
pub async fn create_token_for_owner(
|
||||
db: &DB,
|
||||
@@ -274,14 +291,7 @@ pub async fn create_token_for_owner(
|
||||
let job_perms = if perms.is_some() {
|
||||
Ok(perms)
|
||||
} else {
|
||||
sqlx::query_as!(
|
||||
JobPerms,
|
||||
"SELECT email, username, is_admin, is_operator, groups, folders FROM job_perms WHERE job_id = $1 AND workspace_id = $2",
|
||||
job_id,
|
||||
w_id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
get_job_perms(db, job_id, w_id).await
|
||||
};
|
||||
let job_authed = match job_perms {
|
||||
Ok(Some(jp)) => jp.into(),
|
||||
@@ -297,8 +307,16 @@ pub async fn create_token_for_owner(
|
||||
}
|
||||
};
|
||||
|
||||
create_jwt_token(job_authed, w_id, expires_in, Some(*job_id), Some(label.to_string()), audit_span, None)
|
||||
.await
|
||||
create_jwt_token(
|
||||
job_authed,
|
||||
w_id,
|
||||
expires_in,
|
||||
Some(*job_id),
|
||||
Some(label.to_string()),
|
||||
audit_span,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_jwt_token(
|
||||
@@ -319,8 +337,8 @@ pub async fn create_jwt_token(
|
||||
folders: authed.folders.clone(),
|
||||
label,
|
||||
workspace_id: workspace_id.to_string(),
|
||||
exp: (chrono::Utc::now() + chrono::Duration::seconds(expires_in_seconds as i64))
|
||||
.timestamp() as usize,
|
||||
exp: (chrono::Utc::now() + chrono::Duration::seconds(expires_in_seconds as i64)).timestamp()
|
||||
as usize,
|
||||
job_id: job_id.map(|id| id.to_string()),
|
||||
scopes,
|
||||
audit_span,
|
||||
|
||||
@@ -129,6 +129,15 @@ struct UntaggedFlowStatusModule {
|
||||
approvers: Option<Vec<Approval>>,
|
||||
failed_retries: Option<Vec<Uuid>>,
|
||||
skipped: Option<bool>,
|
||||
agent_actions: Option<Vec<AgentAction>>,
|
||||
agent_actions_success: Option<Vec<bool>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum AgentAction {
|
||||
ToolCall { job_id: uuid::Uuid, function_name: String, module_id: String },
|
||||
Message {},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
@@ -165,6 +174,10 @@ pub enum FlowStatusModule {
|
||||
parallel: bool,
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
while_loop: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
agent_actions: Option<Vec<AgentAction>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
agent_actions_success: Option<Vec<bool>>,
|
||||
},
|
||||
Success {
|
||||
id: String,
|
||||
@@ -181,6 +194,10 @@ pub enum FlowStatusModule {
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
failed_retries: Vec<Uuid>,
|
||||
skipped: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
agent_actions: Option<Vec<AgentAction>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
agent_actions_success: Option<Vec<bool>>,
|
||||
},
|
||||
Failure {
|
||||
id: String,
|
||||
@@ -193,6 +210,10 @@ pub enum FlowStatusModule {
|
||||
branch_chosen: Option<BranchChosen>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
failed_retries: Vec<Uuid>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
agent_actions: Option<Vec<AgentAction>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
agent_actions_success: Option<Vec<bool>>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -244,6 +265,8 @@ impl<'de> Deserialize<'de> for FlowStatusModule {
|
||||
parallel: untagged.parallel.unwrap_or(false),
|
||||
while_loop: untagged.while_loop.unwrap_or(false),
|
||||
progress: untagged.progress,
|
||||
agent_actions: untagged.agent_actions,
|
||||
agent_actions_success: untagged.agent_actions_success,
|
||||
}),
|
||||
"Success" => Ok(FlowStatusModule::Success {
|
||||
id: untagged
|
||||
@@ -258,6 +281,8 @@ impl<'de> Deserialize<'de> for FlowStatusModule {
|
||||
approvers: untagged.approvers.unwrap_or_default(),
|
||||
failed_retries: untagged.failed_retries.unwrap_or_default(),
|
||||
skipped: untagged.skipped.unwrap_or(false),
|
||||
agent_actions: untagged.agent_actions,
|
||||
agent_actions_success: untagged.agent_actions_success,
|
||||
}),
|
||||
"Failure" => Ok(FlowStatusModule::Failure {
|
||||
id: untagged
|
||||
@@ -270,6 +295,8 @@ impl<'de> Deserialize<'de> for FlowStatusModule {
|
||||
flow_jobs_success: untagged.flow_jobs_success,
|
||||
branch_chosen: untagged.branch_chosen,
|
||||
failed_retries: untagged.failed_retries.unwrap_or_default(),
|
||||
agent_actions: untagged.agent_actions,
|
||||
agent_actions_success: untagged.agent_actions_success,
|
||||
}),
|
||||
other => Err(serde::de::Error::unknown_variant(
|
||||
other,
|
||||
@@ -354,6 +381,30 @@ impl FlowStatusModule {
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn agent_actions(&self) -> Option<Vec<AgentAction>> {
|
||||
match self {
|
||||
FlowStatusModule::InProgress { agent_actions, .. } => agent_actions.clone(),
|
||||
FlowStatusModule::Success { agent_actions, .. } => agent_actions.clone(),
|
||||
FlowStatusModule::Failure { agent_actions, .. } => agent_actions.clone(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn agent_actions_success(&self) -> Option<Vec<bool>> {
|
||||
match self {
|
||||
FlowStatusModule::InProgress { agent_actions_success, .. } => {
|
||||
agent_actions_success.clone()
|
||||
}
|
||||
FlowStatusModule::Success { agent_actions_success, .. } => {
|
||||
agent_actions_success.clone()
|
||||
}
|
||||
FlowStatusModule::Failure { agent_actions_success, .. } => {
|
||||
agent_actions_success.clone()
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FlowStatus {
|
||||
|
||||
@@ -529,6 +529,10 @@ pub enum FlowModuleValue {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
assets: Option<Vec<AssetWithAltAccessType>>,
|
||||
},
|
||||
AIAgent {
|
||||
input_transforms: HashMap<String, InputTransform>,
|
||||
tools: Vec<FlowModule>,
|
||||
},
|
||||
}
|
||||
|
||||
fn is_none_or_empty(expr: &Option<String>) -> bool {
|
||||
@@ -563,6 +567,7 @@ struct UntaggedFlowModuleValue {
|
||||
default_node: Option<FlowNodeId>,
|
||||
modules_node: Option<FlowNodeId>,
|
||||
assets: Option<Vec<AssetWithAltAccessType>>,
|
||||
tools: Option<Vec<FlowModule>>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for FlowModuleValue {
|
||||
@@ -655,6 +660,12 @@ impl<'de> Deserialize<'de> for FlowModuleValue {
|
||||
assets: untagged.assets,
|
||||
}),
|
||||
"identity" => Ok(FlowModuleValue::Identity),
|
||||
"aiagent" => Ok(FlowModuleValue::AIAgent {
|
||||
input_transforms: untagged.input_transforms.unwrap_or_default(),
|
||||
tools: untagged
|
||||
.tools
|
||||
.ok_or_else(|| serde::de::Error::missing_field("tools"))?,
|
||||
}),
|
||||
other => Err(serde::de::Error::unknown_variant(
|
||||
other,
|
||||
&[
|
||||
@@ -666,6 +677,7 @@ impl<'de> Deserialize<'de> for FlowModuleValue {
|
||||
"branchall",
|
||||
"rawscript",
|
||||
"identity",
|
||||
"aiagent",
|
||||
],
|
||||
)),
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ pub enum JobKind {
|
||||
FlowScript,
|
||||
FlowNode,
|
||||
AppScript,
|
||||
AIAgent,
|
||||
}
|
||||
|
||||
impl JobKind {
|
||||
@@ -375,6 +376,9 @@ pub enum JobPayload {
|
||||
},
|
||||
Identity,
|
||||
Noop,
|
||||
AIAgent {
|
||||
path: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
|
||||
|
||||
@@ -627,8 +627,8 @@ pub fn get_latest_flow_version_info_for_path<
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_latest_hash_for_path<'c>(
|
||||
db: &mut sqlx::Transaction<'c, sqlx::Postgres>,
|
||||
pub async fn get_latest_hash_for_path<'c, E: sqlx::PgExecutor<'c>>(
|
||||
db: E,
|
||||
w_id: &str,
|
||||
script_path: &str,
|
||||
) -> error::Result<(
|
||||
@@ -652,7 +652,7 @@ pub async fn get_latest_hash_for_path<'c>(
|
||||
script_path,
|
||||
w_id
|
||||
)
|
||||
.fetch_optional(&mut **db)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
|
||||
let script = utils::not_found_if_none(r_o, "script", script_path)?;
|
||||
|
||||
@@ -14,3 +14,16 @@ pub async fn get_queue_counts(db: &Pool<Postgres>) -> HashMap<String, u32> {
|
||||
.map(|v| v.into_iter().map(|x| (x.tag, x.count as u32)).collect())
|
||||
.unwrap_or_else(|| HashMap::new())
|
||||
}
|
||||
|
||||
pub async fn get_queue_running_counts(db: &Pool<Postgres>) -> HashMap<String, u32> {
|
||||
sqlx::query!(
|
||||
"SELECT tag AS \"tag!\", count(*) AS \"count!\" FROM v2_job_queue WHERE
|
||||
running = true
|
||||
GROUP BY tag",
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.ok()
|
||||
.map(|v| v.into_iter().map(|x| (x.tag, x.count as u32)).collect())
|
||||
.unwrap_or_else(|| HashMap::new())
|
||||
}
|
||||
|
||||
@@ -642,6 +642,12 @@ pub struct PythonAnnotations {
|
||||
pub py313: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[annotations("//")]
|
||||
pub struct GoAnnotations {
|
||||
pub go1_22_compat: bool,
|
||||
}
|
||||
|
||||
#[annotations("//")]
|
||||
pub struct TypeScriptAnnotations {
|
||||
pub npm: bool,
|
||||
|
||||
@@ -117,7 +117,7 @@ pub async fn update_workflow_as_code_status(
|
||||
|
||||
// TODO: merge as a CTE
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
async fn get_step_of_flow_status(db: &DB, id: Uuid) -> error::Result<Step> {
|
||||
pub async fn get_step_of_flow_status(db: &DB, id: Uuid) -> error::Result<Step> {
|
||||
let r = sqlx::query!(
|
||||
"SELECT (flow_status->'step')::integer as step, jsonb_array_length(flow_status->'modules') as len
|
||||
FROM v2_job_status WHERE id = $1",
|
||||
|
||||
@@ -448,6 +448,7 @@ pub async fn push_init_job<'c>(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
inner_tx.commit().await?;
|
||||
@@ -500,6 +501,7 @@ pub async fn push_periodic_bash_job<'c>(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
inner_tx.commit().await?;
|
||||
@@ -1274,6 +1276,7 @@ async fn restart_job_if_perpetual_inner(
|
||||
None,
|
||||
queued_job.priority,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -2036,6 +2039,7 @@ pub async fn push_error_handler<'a, 'c, T: Serialize + Send + Sync>(
|
||||
None,
|
||||
priority,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
@@ -2144,6 +2148,7 @@ async fn handle_recovered_schedule<'a, 'c, T: Serialize + Send + Sync>(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tracing::info!(
|
||||
@@ -2233,6 +2238,7 @@ async fn handle_successful_schedule<'a, 'c, T: Serialize + Send + Sync>(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tracing::info!(
|
||||
@@ -3618,7 +3624,7 @@ pub async fn push<'c, 'd>(
|
||||
root_job: Option<Uuid>,
|
||||
job_id: Option<Uuid>,
|
||||
_is_flow_step: bool,
|
||||
mut same_worker: bool,
|
||||
mut same_worker: bool, // whether the job will be executed on the same worker: if true, the job will be set to running but started_at will not be set.
|
||||
pre_run_error: Option<&windmill_common::error::Error>,
|
||||
visible_to_owner: bool,
|
||||
mut tag: Option<String>,
|
||||
@@ -3626,6 +3632,7 @@ pub async fn push<'c, 'd>(
|
||||
flow_step_id: Option<String>,
|
||||
_priority_override: Option<i16>,
|
||||
authed: Option<&Authed>,
|
||||
running: bool, // whether the job is already running: only set this to true if you don't want the job to be picked up by a worker from the queue. It will also set started_at to now.
|
||||
) -> Result<(Uuid, Transaction<'c, Postgres>), Error> {
|
||||
#[cfg(feature = "cloud")]
|
||||
if *CLOUD_HOSTED {
|
||||
@@ -4435,6 +4442,21 @@ pub async fn push<'c, 'd>(
|
||||
None,
|
||||
None,
|
||||
),
|
||||
JobPayload::AIAgent { path } => (
|
||||
None,
|
||||
Some(path),
|
||||
None,
|
||||
JobKind::AIAgent,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
};
|
||||
|
||||
let final_priority: Option<i16>;
|
||||
@@ -4462,7 +4484,7 @@ pub async fn push<'c, 'd>(
|
||||
final_priority
|
||||
};
|
||||
|
||||
let is_running = same_worker;
|
||||
let is_running = same_worker || running;
|
||||
if let Some(flow) = raw_flow.as_ref() {
|
||||
same_worker = same_worker || flow.same_worker;
|
||||
|
||||
@@ -4508,7 +4530,10 @@ pub async fn push<'c, 'd>(
|
||||
let per_workspace = per_workspace_tag(&workspace_id).await;
|
||||
|
||||
let default = || {
|
||||
let ntag = if job_kind.is_flow() || job_kind == JobKind::Identity {
|
||||
let ntag = if job_kind.is_flow()
|
||||
|| job_kind == JobKind::Identity
|
||||
|| job_kind == JobKind::AIAgent
|
||||
{
|
||||
"flow".to_string()
|
||||
} else if job_kind == JobKind::Dependencies
|
||||
|| job_kind == JobKind::FlowDependencies
|
||||
@@ -4667,7 +4692,7 @@ pub async fn push<'c, 'd>(
|
||||
)
|
||||
INSERT INTO v2_job_queue
|
||||
(workspace_id, id, running, scheduled_for, started_at, tag, priority)
|
||||
VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 THEN now() END, $30, $31)",
|
||||
VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 OR $40 THEN now() END, $30, $31)",
|
||||
job_id,
|
||||
workspace_id,
|
||||
raw_code,
|
||||
@@ -4711,6 +4736,7 @@ pub async fn push<'c, 'd>(
|
||||
job_authed.groups.as_slice(),
|
||||
root_job.or(parent_job),
|
||||
trigger_kind as Option<JobTriggerKind>,
|
||||
running,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.warn_after_seconds(1)
|
||||
@@ -4782,6 +4808,7 @@ pub async fn push<'c, 'd>(
|
||||
JobKind::FlowScript => "jobs.run.flow_script",
|
||||
JobKind::FlowNode => "jobs.run.flow_node",
|
||||
JobKind::AppScript => "jobs.run.app_script",
|
||||
JobKind::AIAgent => "jobs.run.ai_agent",
|
||||
};
|
||||
|
||||
let audit_author = if format!("u/{user}") != permissioned_as && user != permissioned_as {
|
||||
@@ -4971,6 +4998,8 @@ async fn restarted_flows_resolution(
|
||||
parallel,
|
||||
while_loop: false,
|
||||
progress: None,
|
||||
agent_actions: None,
|
||||
agent_actions_success: None,
|
||||
});
|
||||
}
|
||||
Ok(FlowModuleValue::ForloopFlow { parallel, .. }) => {
|
||||
@@ -5009,6 +5038,8 @@ async fn restarted_flows_resolution(
|
||||
parallel,
|
||||
while_loop: false,
|
||||
progress: None,
|
||||
agent_actions: None,
|
||||
agent_actions_success: None,
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
|
||||
@@ -153,7 +153,7 @@ pub async fn push_scheduled_job<'c>(
|
||||
on_behalf_of_email,
|
||||
created_by,
|
||||
) = windmill_common::get_latest_hash_for_path(
|
||||
&mut tx,
|
||||
&mut *tx,
|
||||
&schedule.workspace_id,
|
||||
&schedule.script_path,
|
||||
)
|
||||
@@ -302,6 +302,7 @@ pub async fn push_scheduled_job<'c>(
|
||||
None,
|
||||
None,
|
||||
push_authed,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ windmill-git-sync.workspace = true
|
||||
flume.workspace = true
|
||||
sqlx.workspace = true
|
||||
uuid.workspace = true
|
||||
ulid.workspace = true
|
||||
tracing.workspace = true
|
||||
tokio.workspace = true
|
||||
tokio-stream.workspace = true
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -758,7 +758,7 @@ pub async fn prebundle_bun_script(
|
||||
pub const BUN_BUNDLE_OBJECT_STORE_PREFIX: &str = "bun_bundle/";
|
||||
|
||||
async fn get_script_import_updated_at(db: &DB, w_id: &str, script_path: &str) -> Result<String> {
|
||||
let script_hash = get_latest_hash_for_path(&mut db.begin().await?, w_id, script_path).await?;
|
||||
let script_hash = get_latest_hash_for_path(db, w_id, script_path).await?;
|
||||
let last_updated_at = sqlx::query_scalar!(
|
||||
"SELECT created_at FROM script WHERE workspace_id = $1 AND hash = $2",
|
||||
w_id,
|
||||
|
||||
@@ -549,13 +549,14 @@ pub async fn update_worker_ping_for_failed_init_script(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn error_to_value(err: Error) -> serde_json::Value {
|
||||
pub fn error_to_value(err: &Error) -> serde_json::Value {
|
||||
match err {
|
||||
Error::JsonErr(err) => err,
|
||||
Error::JsonErr(err) => err.clone(),
|
||||
_ => json!({"message": err.to_string(), "name": err.name()}),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OccupancyMetrics {
|
||||
pub running_job_started_at: Option<Instant>,
|
||||
pub total_duration_of_running_jobs: f32,
|
||||
|
||||
@@ -433,6 +433,7 @@ async fn spawn_dedicated_workers_for_flow(
|
||||
}
|
||||
FlowModuleValue::Flow { .. } => (),
|
||||
FlowModuleValue::Identity => (),
|
||||
FlowModuleValue::AIAgent { .. } => (),
|
||||
}
|
||||
} else {
|
||||
tracing::error!("failed to get value for module: {:?}", module);
|
||||
|
||||
@@ -12,7 +12,7 @@ use uuid::Uuid;
|
||||
use windmill_common::{
|
||||
error::{self, Error},
|
||||
utils::calculate_hash,
|
||||
worker::{save_cache, write_file, Connection},
|
||||
worker::{save_cache, write_file, Connection, GoAnnotations},
|
||||
};
|
||||
use windmill_parser_go::{parse_go_imports, REQUIRE_PARSE};
|
||||
use windmill_queue::{append_logs, CanceledBy, MiniPulledJob};
|
||||
@@ -464,6 +464,7 @@ pub async fn install_go_dependencies(
|
||||
w_id: &str,
|
||||
occupation_metrics: &mut OccupancyMetrics,
|
||||
) -> error::Result<String> {
|
||||
let anns = GoAnnotations::parse(code);
|
||||
if raw_deps {
|
||||
let go_mod =
|
||||
if let Some(module) = code.lines().find(|l| l.trim_start().starts_with("module ")) {
|
||||
@@ -529,7 +530,11 @@ pub async fn install_go_dependencies(
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
let hash = format!("go-{}", hash);
|
||||
let hash = format!(
|
||||
"go{}-{}",
|
||||
if anns.go1_22_compat { "1.22" } else { "" },
|
||||
hash
|
||||
);
|
||||
|
||||
let mut skip_tidy = has_sum;
|
||||
|
||||
@@ -579,6 +584,11 @@ pub async fn install_go_dependencies(
|
||||
.args(vec!["mod", mod_command])
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
// If annotation used we want to call tidy with special flag to pin go to 1.22
|
||||
// The reason for this that at some point we had to jump from go 1.22 to 1.25 and this addds backward compatibility.
|
||||
if anns.go1_22_compat && mod_command == "tidy" {
|
||||
child_cmd.args(vec!["-go", "1.22"]);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
set_windows_env_vars(&mut child_cmd);
|
||||
|
||||
@@ -16,6 +16,7 @@ mod java_executor;
|
||||
#[cfg(feature = "ruby")]
|
||||
mod ruby_executor;
|
||||
|
||||
mod ai_executor;
|
||||
mod bun_executor;
|
||||
pub mod common;
|
||||
mod config;
|
||||
|
||||
@@ -852,6 +852,7 @@ mount {{
|
||||
{
|
||||
python_cmd.env("SystemRoot", SYSTEM_ROOT.as_str());
|
||||
python_cmd.env("USERPROFILE", crate::USERPROFILE_ENV.as_str());
|
||||
python_cmd.env("windir", SYSTEM_ROOT.as_str());
|
||||
python_cmd.env(
|
||||
"LOCALAPPDATA",
|
||||
std::env::var("LOCALAPPDATA")
|
||||
|
||||
@@ -647,7 +647,7 @@ pub async fn process_completed_job(
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
async fn handle_non_flow_job_error(
|
||||
pub async fn handle_non_flow_job_error(
|
||||
db: &DB,
|
||||
job: &MiniPulledJob,
|
||||
mem_peak: i32,
|
||||
@@ -692,7 +692,7 @@ pub async fn handle_job_error(
|
||||
#[cfg(feature = "benchmark")] bench: &mut BenchmarkIter,
|
||||
) {
|
||||
let err_string = format!("{}: {}", err.name(), err.to_string());
|
||||
let err_json = error_to_value(err);
|
||||
let err_json = error_to_value(&err);
|
||||
|
||||
let update_job_future = || async {
|
||||
handle_non_flow_job_error(
|
||||
|
||||
@@ -20,13 +20,13 @@ use windmill_queue::{CanceledBy, MiniPulledJob, HTTP_CLIENT};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::common::build_args_values;
|
||||
use crate::common::{
|
||||
build_http_client, resolve_job_timeout, s3_mode_args_to_worker_data, OccupancyMetrics,
|
||||
S3ModeWorkerData,
|
||||
};
|
||||
use crate::handle_child::run_future_with_polling_update_job_poller;
|
||||
use crate::sanitized_sql_params::sanitize_and_interpolate_unsafe_sql_args;
|
||||
use crate::common::build_args_values;
|
||||
use windmill_common::client::AuthedClient;
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
||||
@@ -14,6 +14,7 @@ use windmill_common::{error, worker::Connection};
|
||||
|
||||
use crate::{common::start_child_process, DISABLE_NSJAIL};
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RequiredDependency<T: Clone + Send + Sync> {
|
||||
/// Expected directory of dependency in cache
|
||||
|
||||
@@ -99,6 +99,7 @@ use tokio::{
|
||||
|
||||
use rand::Rng;
|
||||
|
||||
use crate::ai_executor::handle_ai_agent_job;
|
||||
use crate::{
|
||||
agent_workers::{queue_init_job, queue_periodic_job},
|
||||
bash_executor::{handle_bash_job, handle_powershell_job},
|
||||
@@ -753,7 +754,7 @@ pub async fn handle_all_job_kind_error(
|
||||
preprocessed_args: None,
|
||||
job: job.clone(),
|
||||
result: Arc::new(windmill_common::worker::to_raw_value(&error_to_value(
|
||||
err,
|
||||
&err,
|
||||
))),
|
||||
result_columns: None,
|
||||
mem_peak: 0,
|
||||
@@ -899,7 +900,7 @@ pub fn start_interactive_worker_shell(
|
||||
})
|
||||
}
|
||||
|
||||
async fn create_job_dir(worker_directory: &str, job_id: impl Display) -> String {
|
||||
pub async fn create_job_dir(worker_directory: &str, job_id: impl Display) -> String {
|
||||
let job_dir_path = format!("{}/{}", worker_directory, job_id);
|
||||
|
||||
create_directory_async(&job_dir_path).await;
|
||||
@@ -2190,11 +2191,13 @@ pub struct SendResult {
|
||||
pub time: Instant,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum SendResultPayload {
|
||||
JobCompleted(JobCompleted),
|
||||
UpdateFlow(UpdateFlow),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct UpdateFlow {
|
||||
pub flow: Uuid,
|
||||
pub w_id: String,
|
||||
@@ -2341,9 +2344,10 @@ pub async fn handle_queued_job(
|
||||
| JobKind::FlowDependencies,
|
||||
x,
|
||||
) => match x.map(|x| x.0) {
|
||||
None | Some(PREVIEW_IS_CODEBASE_HASH) | Some(PREVIEW_IS_TAR_CODEBASE_HASH) => {
|
||||
Some(cache::job::fetch_preview(conn, &job.id, raw_lock, raw_code, raw_flow).await?)
|
||||
}
|
||||
None | Some(PREVIEW_IS_CODEBASE_HASH) | Some(PREVIEW_IS_TAR_CODEBASE_HASH) => Some(
|
||||
cache::job::fetch_preview(conn, &job.id, raw_lock, raw_code, raw_flow.clone())
|
||||
.await?,
|
||||
),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
@@ -2548,6 +2552,31 @@ pub async fn handle_queued_job(
|
||||
.flatten()
|
||||
.map(|x| x.to_owned())
|
||||
.unwrap_or_else(|| serde_json::from_str("{}").unwrap())),
|
||||
JobKind::AIAgent => match conn {
|
||||
Connection::Sql(db) => {
|
||||
handle_ai_agent_job(
|
||||
conn,
|
||||
db,
|
||||
job.as_ref(),
|
||||
&client,
|
||||
&mut canceled_by,
|
||||
&mut mem_peak,
|
||||
&mut *occupancy_metrics,
|
||||
&job_completed_tx,
|
||||
worker_dir,
|
||||
base_internal_url,
|
||||
worker_name,
|
||||
hostname,
|
||||
killpill_rx,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Connection::Http(_) => {
|
||||
return Err(Error::internal_err(
|
||||
"Agent worker does not support ai agent jobs".to_string(),
|
||||
));
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
let metric_timer = Instant::now();
|
||||
let preview_data = preview_data.and_then(|data| match data {
|
||||
@@ -2732,6 +2761,7 @@ async fn try_validate_schema(
|
||||
JobKind::AppDependencies => 12,
|
||||
JobKind::Noop => 13,
|
||||
JobKind::FlowNode => 14,
|
||||
JobKind::AIAgent => 15,
|
||||
};
|
||||
|
||||
let sv = match job.runnable_id {
|
||||
@@ -3488,7 +3518,7 @@ mount {{
|
||||
result
|
||||
}
|
||||
|
||||
fn parse_sig_of_lang(
|
||||
pub fn parse_sig_of_lang(
|
||||
code: &str,
|
||||
language: Option<&ScriptLang>,
|
||||
main_override: Option<String>,
|
||||
|
||||
@@ -29,7 +29,7 @@ use sqlx::types::Json;
|
||||
use sqlx::{FromRow, Postgres, Transaction};
|
||||
use tracing::instrument;
|
||||
use uuid::Uuid;
|
||||
use windmill_common::auth::JobPerms;
|
||||
use windmill_common::auth::get_job_perms;
|
||||
#[cfg(feature = "benchmark")]
|
||||
use windmill_common::bench::BenchmarkIter;
|
||||
use windmill_common::cache::{self, RawData};
|
||||
@@ -626,6 +626,8 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
approvers: vec![],
|
||||
failed_retries: vec![],
|
||||
skipped: false,
|
||||
agent_actions: None,
|
||||
agent_actions_success: None,
|
||||
}
|
||||
} else {
|
||||
success = false;
|
||||
@@ -636,6 +638,8 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
flow_jobs_success: flow_jobs_success.clone(),
|
||||
branch_chosen: None,
|
||||
failed_retries: vec![],
|
||||
agent_actions: None,
|
||||
agent_actions_success: None,
|
||||
}
|
||||
};
|
||||
let r = sqlx::query_scalar!(
|
||||
@@ -806,6 +810,8 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
approvers: vec![],
|
||||
failed_retries: old_status.retry.failed_jobs.clone(),
|
||||
skipped: is_skipped,
|
||||
agent_actions: module_status.agent_actions(),
|
||||
agent_actions_success: module_status.agent_actions_success(),
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
@@ -829,6 +835,8 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
flow_jobs_success,
|
||||
branch_chosen,
|
||||
failed_retries: old_status.retry.failed_jobs.clone(),
|
||||
agent_actions: module_status.agent_actions(),
|
||||
agent_actions_success: module_status.agent_actions_success(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -2517,7 +2525,8 @@ async fn push_next_flow_job(
|
||||
FlowModuleValue::Script { input_transforms, .. }
|
||||
| FlowModuleValue::RawScript { input_transforms, .. }
|
||||
| FlowModuleValue::FlowScript { input_transforms, .. }
|
||||
| FlowModuleValue::Flow { input_transforms, .. },
|
||||
| FlowModuleValue::Flow { input_transforms, .. }
|
||||
| FlowModuleValue::AIAgent { input_transforms, .. },
|
||||
) => {
|
||||
let ctx = get_transform_context(&flow_job, &previous_id, &status)
|
||||
.warn_after_seconds(3)
|
||||
@@ -2605,6 +2614,8 @@ async fn push_next_flow_job(
|
||||
approvers: vec![],
|
||||
failed_retries: vec![],
|
||||
skipped: false,
|
||||
agent_actions: None,
|
||||
agent_actions_success: None,
|
||||
}),
|
||||
flow_job.id
|
||||
)
|
||||
@@ -2825,16 +2836,9 @@ async fn push_next_flow_job(
|
||||
.flow_innermost_root_job
|
||||
.or_else(|| Some(flow_job.id))
|
||||
{
|
||||
sqlx::query_as!(
|
||||
JobPerms,
|
||||
"SELECT email, username, is_admin, is_operator, groups, folders FROM job_perms WHERE job_id = $1 AND workspace_id = $2",
|
||||
root_job,
|
||||
flow_job.workspace_id,
|
||||
)
|
||||
.fetch_optional(&mut *tx)
|
||||
.warn_after_seconds(3)
|
||||
.await?
|
||||
.map(|x| x.into())
|
||||
get_job_perms(&mut *tx, root_job, &flow_job.workspace_id)
|
||||
.await?
|
||||
.map(|x| x.into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -2886,6 +2890,7 @@ async fn push_next_flow_job(
|
||||
Some(module.id.clone()),
|
||||
new_job_priority_override,
|
||||
job_perms.as_ref(),
|
||||
false,
|
||||
)
|
||||
.warn_after_seconds(2)
|
||||
.await?;
|
||||
@@ -3001,6 +3006,8 @@ async fn push_next_flow_job(
|
||||
parallel: false,
|
||||
while_loop,
|
||||
progress: None,
|
||||
agent_actions: None,
|
||||
agent_actions_success: None,
|
||||
}
|
||||
}
|
||||
NextStatus::AllFlowJobs { iterator, branchall, .. } => FlowStatusModule::InProgress {
|
||||
@@ -3014,6 +3021,8 @@ async fn push_next_flow_job(
|
||||
parallel: true,
|
||||
while_loop: false,
|
||||
progress: None,
|
||||
agent_actions: None,
|
||||
agent_actions_success: None,
|
||||
},
|
||||
NextStatus::NextBranchStep(NextBranch {
|
||||
mut flow_jobs,
|
||||
@@ -3037,6 +3046,8 @@ async fn push_next_flow_job(
|
||||
parallel: false,
|
||||
while_loop: false,
|
||||
progress: None,
|
||||
agent_actions: None,
|
||||
agent_actions_success: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3051,6 +3062,8 @@ async fn push_next_flow_job(
|
||||
parallel: false,
|
||||
while_loop: false,
|
||||
progress: None,
|
||||
agent_actions: None,
|
||||
agent_actions_success: None,
|
||||
},
|
||||
NextStatus::NextStep => {
|
||||
FlowStatusModule::WaitingForExecutor { id: status_module.id(), job: one_uuid? }
|
||||
@@ -3253,12 +3266,12 @@ enum NextStatus {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct JobPayloadWithTag {
|
||||
payload: JobPayload,
|
||||
tag: Option<String>,
|
||||
delete_after_use: bool,
|
||||
timeout: Option<i32>,
|
||||
on_behalf_of: Option<OnBehalfOf>,
|
||||
pub struct JobPayloadWithTag {
|
||||
pub payload: JobPayload,
|
||||
pub tag: Option<String>,
|
||||
pub delete_after_use: bool,
|
||||
pub timeout: Option<i32>,
|
||||
pub on_behalf_of: Option<OnBehalfOf>,
|
||||
}
|
||||
enum ContinuePayload {
|
||||
SingleJob(JobPayloadWithTag),
|
||||
@@ -3320,7 +3333,7 @@ fn payload_from_modules<'a>(
|
||||
})
|
||||
}
|
||||
|
||||
fn get_path(flow_job: &MiniPulledJob, status: &FlowStatus, module: &FlowModule) -> String {
|
||||
pub fn get_path(flow_job: &MiniPulledJob, status: &FlowStatus, module: &FlowModule) -> String {
|
||||
if status
|
||||
.preprocessor_module
|
||||
.as_ref()
|
||||
@@ -3390,6 +3403,20 @@ async fn compute_next_flow_transform(
|
||||
NextStatus::NextStep,
|
||||
))
|
||||
}
|
||||
FlowModuleValue::AIAgent { .. } => {
|
||||
let path = get_path(flow_job, status, module);
|
||||
let payload = JobPayload::AIAgent { path };
|
||||
Ok(NextFlowTransform::Continue(
|
||||
ContinuePayload::SingleJob(JobPayloadWithTag {
|
||||
payload,
|
||||
tag: None,
|
||||
delete_after_use,
|
||||
timeout: None,
|
||||
on_behalf_of: None,
|
||||
}),
|
||||
NextStatus::NextStep,
|
||||
))
|
||||
}
|
||||
FlowModuleValue::Script { path: script_path, hash: script_hash, tag_override, .. } => {
|
||||
let payload = script_to_payload(
|
||||
script_hash,
|
||||
@@ -4087,7 +4114,7 @@ async fn payload_from_simple_module(
|
||||
})
|
||||
}
|
||||
|
||||
fn raw_script_to_payload(
|
||||
pub fn raw_script_to_payload(
|
||||
path: String,
|
||||
content: String,
|
||||
language: windmill_common::scripts::ScriptLang,
|
||||
@@ -4137,7 +4164,7 @@ async fn flow_to_payload(
|
||||
Ok(JobPayloadWithTag { payload, tag, delete_after_use, timeout: None, on_behalf_of })
|
||||
}
|
||||
|
||||
async fn script_to_payload(
|
||||
pub async fn script_to_payload(
|
||||
script_hash: Option<windmill_common::scripts::ScriptHash>,
|
||||
script_path: String,
|
||||
db: &sqlx::Pool<sqlx::Postgres>,
|
||||
|
||||
@@ -627,6 +627,7 @@ async fn trigger_dependents_to_recompute_dependencies(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
tracing::info!(
|
||||
|
||||
+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.527.1";
|
||||
export const VERSION = "v1.532.0";
|
||||
|
||||
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.527.1";
|
||||
export const VERSION = "1.532.0";
|
||||
|
||||
const command = new Command()
|
||||
.name("wmill")
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
import { inferContentTypeFromFilePath } from "./script_common.ts";
|
||||
import { GlobalDeps, exts, findGlobalDeps } from "../commands/script/script.ts";
|
||||
import { FSFSElement, findCodebase, yamlOptions } from "../commands/sync/sync.ts";
|
||||
import { generateHash, readInlinePathSync } from "./utils.ts";
|
||||
import { generateHash, readInlinePathSync, getHeaders } from "./utils.ts";
|
||||
import { SyncCodebase } from "./codebase.ts";
|
||||
import { FlowFile } from "../commands/flow/flow.ts";
|
||||
import { replaceInlineScripts } from "../../windmill-utils-internal/src/inline-scripts/replacer.ts";
|
||||
@@ -384,6 +384,7 @@ async function updateScriptLock(
|
||||
}
|
||||
// generate the script lock running a dependency job in Windmill and update it inplace
|
||||
// TODO: update this once the client is released
|
||||
const extraHeaders = getHeaders();
|
||||
const rawResponse = await fetch(
|
||||
`${workspace.remote}api/w/${workspace.workspaceId}/jobs/run/dependencies`,
|
||||
{
|
||||
@@ -391,6 +392,7 @@ async function updateScriptLock(
|
||||
headers: {
|
||||
Cookie: `token=${workspace.token}`,
|
||||
"Content-Type": "application/json",
|
||||
...extraHeaders,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
raw_scripts: [
|
||||
@@ -455,6 +457,7 @@ export async function updateFlow(
|
||||
log.info(colors.blue("Using raw requirements for flow dependencies"));
|
||||
|
||||
// generate the script lock running a dependency job in Windmill and update it inplace
|
||||
const extraHeaders = getHeaders();
|
||||
rawResponse = await fetch(
|
||||
`${workspace.remote}api/w/${workspace.workspaceId}/jobs/run/flow_dependencies`,
|
||||
{
|
||||
@@ -462,6 +465,7 @@ export async function updateFlow(
|
||||
headers: {
|
||||
Cookie: `token=${workspace.token}`,
|
||||
"Content-Type": "application/json",
|
||||
...extraHeaders,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
flow_value,
|
||||
@@ -473,6 +477,7 @@ export async function updateFlow(
|
||||
);
|
||||
} else {
|
||||
// Standard dependency resolution on the server
|
||||
const extraHeaders = getHeaders();
|
||||
rawResponse = await fetch(
|
||||
`${workspace.remote}api/w/${workspace.workspaceId}/jobs/run/flow_dependencies`,
|
||||
{
|
||||
@@ -480,6 +485,7 @@ export async function updateFlow(
|
||||
headers: {
|
||||
Cookie: `token=${workspace.token}`,
|
||||
"Content-Type": "application/json",
|
||||
...extraHeaders,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
flow_value,
|
||||
|
||||
@@ -19,8 +19,6 @@ services:
|
||||
- db_data:/var/lib/postgresql/data
|
||||
expose:
|
||||
- 5432
|
||||
ports:
|
||||
- 5432:5432
|
||||
environment:
|
||||
POSTGRES_PASSWORD: changeme
|
||||
POSTGRES_DB: windmill
|
||||
|
||||
@@ -148,6 +148,13 @@
|
||||
# LSP/Local dev
|
||||
svelte-language-server
|
||||
taplo
|
||||
|
||||
# Orchestration/Kubernetes
|
||||
minikube
|
||||
kubectl
|
||||
kubernetes-helm
|
||||
conntrack-tools # To run minikube without driver (--driver=none)
|
||||
cri-tools
|
||||
]);
|
||||
packages = [
|
||||
(pkgs.writeScriptBin "wm-caddy" ''
|
||||
@@ -245,6 +252,7 @@
|
||||
ANSIBLE_PLAYBOOK_PATH = "${pkgs.ansible}/bin/ansible-playbook";
|
||||
ANSIBLE_GALAXY_PATH = "${pkgs.ansible}/bin/ansible-galaxy";
|
||||
# RUST_LOG = "debug";
|
||||
# RUST_LOG = "kube=debug";
|
||||
SQLX_OFFLINE = "true";
|
||||
|
||||
# See this issue: https://github.com/NixOS/nixpkgs/issues/370494
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.527.1",
|
||||
"version": "1.532.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill-components",
|
||||
"version": "1.527.1",
|
||||
"version": "1.532.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.527.1",
|
||||
"version": "1.532.0",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
<script lang="ts">
|
||||
import type { GraphModuleState } from './graph'
|
||||
import {
|
||||
JobService,
|
||||
type CompletedJob,
|
||||
type FlowModule,
|
||||
type FlowStatusModule,
|
||||
type Job
|
||||
} from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import FlowLogViewerWrapper from './FlowLogViewerWrapper.svelte'
|
||||
import { z } from 'zod'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
type AgentActionWithContent = NonNullable<FlowStatusModule['agent_actions']>[number] & {
|
||||
content: string
|
||||
}
|
||||
|
||||
const resultSchema = z.object({
|
||||
messages: z.array(
|
||||
z.object({
|
||||
role: z.string(),
|
||||
content: z.string().optional(),
|
||||
agent_action: z
|
||||
.union([
|
||||
z.object({
|
||||
type: z.literal('tool_call'),
|
||||
job_id: z.string(),
|
||||
module_id: z.string(),
|
||||
function_name: z.string()
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('message')
|
||||
})
|
||||
])
|
||||
.optional()
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
interface Props {
|
||||
tools: FlowModule[]
|
||||
agentJob: Partial<CompletedJob> & Pick<CompletedJob, 'id'> & { type: 'CompletedJob' }
|
||||
workspaceId?: string | undefined
|
||||
storedToolCallJobs?: Record<number, Job>
|
||||
onToolJobLoaded?: (job: Job, idx: number) => void
|
||||
}
|
||||
|
||||
let { tools, agentJob, workspaceId, onToolJobLoaded, storedToolCallJobs }: Props = $props()
|
||||
|
||||
const fakeModuleStates: Record<string, GraphModuleState> = $state({})
|
||||
|
||||
async function loadMissingJobs(agentActions: AgentActionWithContent[]) {
|
||||
const promises = agentActions.map(async (toolCall, idx) => {
|
||||
if (toolCall.type === 'tool_call') {
|
||||
let job: Job | undefined = storedToolCallJobs?.[idx]
|
||||
|
||||
if (!job || job.type !== 'CompletedJob') {
|
||||
job = await JobService.getJob({
|
||||
id: toolCall.job_id,
|
||||
workspace: workspaceId ?? $workspaceStore!
|
||||
})
|
||||
}
|
||||
fakeModuleStates[idx.toString()] = {
|
||||
args: job.args,
|
||||
type: job['success'] ? 'Success' : 'Failure',
|
||||
logs: job.logs,
|
||||
result: job['result'],
|
||||
job_id: toolCall.job_id
|
||||
}
|
||||
onToolJobLoaded?.(job, idx)
|
||||
} else {
|
||||
fakeModuleStates[idx.toString()] = {
|
||||
type: 'Success',
|
||||
args: {},
|
||||
logs: '',
|
||||
result: toolCall.content
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
let job: Partial<Job> | undefined = $state(undefined)
|
||||
async function loadToolCalls() {
|
||||
let parsedResult = resultSchema.safeParse(agentJob.result)
|
||||
if (!parsedResult.success) {
|
||||
console.error('Invalid result', parsedResult.error)
|
||||
return
|
||||
}
|
||||
let agentActions = parsedResult.data.messages
|
||||
.map(
|
||||
(m) =>
|
||||
(m.agent_action?.type === 'message'
|
||||
? {
|
||||
type: 'message',
|
||||
content: m.content
|
||||
}
|
||||
: m.agent_action?.type === 'tool_call'
|
||||
? {
|
||||
type: 'tool_call',
|
||||
job_id: m.agent_action.job_id,
|
||||
module_id: m.agent_action.module_id,
|
||||
function_name: m.agent_action.function_name
|
||||
}
|
||||
: undefined) as AgentActionWithContent | undefined
|
||||
)
|
||||
.filter((m) => m !== undefined)
|
||||
|
||||
await loadMissingJobs(agentActions)
|
||||
|
||||
job = {
|
||||
...agentJob,
|
||||
raw_flow: {
|
||||
modules: agentActions
|
||||
.map((toolCall, idx) => {
|
||||
if (toolCall.type === 'message') {
|
||||
return {
|
||||
id: idx.toString(),
|
||||
value: {
|
||||
type: 'identity' as const
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const module = tools.find((m) => m.summary === toolCall.function_name)
|
||||
return module
|
||||
? {
|
||||
...module,
|
||||
id: idx.toString()
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
})
|
||||
.filter((m) => m !== undefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadToolCalls()
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if job}
|
||||
<div class="p-2">
|
||||
<FlowLogViewerWrapper
|
||||
{job}
|
||||
localModuleStates={fakeModuleStates}
|
||||
{workspaceId}
|
||||
render={true}
|
||||
onSelectedIteration={async () => {}}
|
||||
mode="aiagent"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -6,6 +6,7 @@
|
||||
import {
|
||||
setInputCat as computeInputCat,
|
||||
debounce,
|
||||
emptySchema,
|
||||
emptyString,
|
||||
getSchemaFromProperties,
|
||||
type DynamicSelect
|
||||
@@ -42,6 +43,8 @@
|
||||
import { safeSelectItems } from './select/utils.svelte'
|
||||
import S3ArgInput from './common/fileUpload/S3ArgInput.svelte'
|
||||
import { base } from '$lib/base'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { getJsonSchemaFromResource } from './schema/jsonSchemaResource.svelte'
|
||||
|
||||
interface Props {
|
||||
label?: string
|
||||
@@ -658,6 +661,47 @@
|
||||
{appPath}
|
||||
{computeS3ForceViewerPolicies}
|
||||
/>
|
||||
{:else if inputCat == 'object' && format == 'json-schema'}
|
||||
{#await import('$lib/components/EditableSchemaForm.svelte')}
|
||||
<Loader2 class="animate-spin" />
|
||||
{:then Module}
|
||||
<Module.default
|
||||
bind:schema={
|
||||
() =>
|
||||
value && typeof value === 'object' && !Array.isArray(value) ? value : emptySchema(),
|
||||
(v) => {
|
||||
value = v
|
||||
}
|
||||
}
|
||||
isFlowInput
|
||||
editTab="inputEditor"
|
||||
noPreview
|
||||
addPropertyInEditorTab
|
||||
/>
|
||||
{/await}
|
||||
{:else if inputCat == 'object' && format?.startsWith('jsonschema-')}
|
||||
{#await getJsonSchemaFromResource(format.substring('jsonschema-'.length), workspace ?? $workspaceStore ?? '')}
|
||||
<Loader2 class="animate-spin" />
|
||||
{:then schema}
|
||||
{#if !schema || !schema.properties}
|
||||
{#await import('$lib/components/JsonEditor.svelte')}
|
||||
<Loader2 class="animate-spin" />
|
||||
{:then Module}
|
||||
<Module.default code={JSON.stringify(value, null, 2)} bind:value />
|
||||
{/await}
|
||||
{:else}
|
||||
<div class="py-4 pr-2 pl-6 border rounded-md w-full">
|
||||
<SchemaForm
|
||||
{onlyMaskPassword}
|
||||
{disablePortal}
|
||||
{disabled}
|
||||
{prettifyHeader}
|
||||
{schema}
|
||||
bind:args={value}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
{:else if inputCat == 'list' && !isListJson}
|
||||
<div class="w-full flex gap-4">
|
||||
<div class="w-full">
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import Label from './Label.svelte'
|
||||
import MultiSelect from './select/MultiSelect.svelte'
|
||||
import { safeSelectItems } from './select/utils.svelte'
|
||||
import { ConfigService } from '$lib/gen'
|
||||
|
||||
interface Props {
|
||||
config: AutoscalingConfig | undefined
|
||||
@@ -21,6 +22,27 @@
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let test_input: number = $state(3)
|
||||
let healthCheckLoading: boolean = $state(false)
|
||||
let healthCheckResult: { success: boolean; error?: string } | null = $state(null)
|
||||
|
||||
async function checkKubernetesHealth() {
|
||||
if (!config?.integration || config.integration.type !== 'kubernetes') return
|
||||
|
||||
healthCheckLoading = true
|
||||
healthCheckResult = null
|
||||
|
||||
try {
|
||||
await ConfigService.nativeKubernetesAutoscalingHealthcheck()
|
||||
healthCheckResult = { success: true }
|
||||
} catch (error: any) {
|
||||
healthCheckResult = {
|
||||
success: false,
|
||||
error: error.body || error.message || 'Unknown error'
|
||||
}
|
||||
} finally {
|
||||
healthCheckLoading = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row gap-16 pt-2">
|
||||
@@ -262,7 +284,7 @@
|
||||
/>
|
||||
<ToggleButton disabled value="ecs" label="ECS (soon)" {item} />
|
||||
<ToggleButton disabled value="nomad" label="Nomad (soon)" {item} />
|
||||
<ToggleButton disabled value="kubernetes" label="Kubernetes (soon)" {item} />
|
||||
<ToggleButton value="kubernetes" label="Kubernetes" {item} />
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
|
||||
@@ -324,6 +346,58 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if config.integration.type === 'kubernetes'}
|
||||
<div class="text-sm text-secondary mb-3">
|
||||
Kubernetes configuration is automatically inferred from the cluster environment.
|
||||
The worker group name and namespace will be detected automatically.
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3 mt-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
color="blue"
|
||||
size="xs"
|
||||
variant="contained"
|
||||
startIcon={{ icon: ExternalLink }}
|
||||
href="https://windmill.dev/docs/core_concepts/autoscaling#kubernetes"
|
||||
target="_blank"
|
||||
>
|
||||
Setup Guide (Roles & Bindings)
|
||||
</Button>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="contained"
|
||||
onclick={checkKubernetesHealth}
|
||||
disabled={healthCheckLoading}
|
||||
>
|
||||
{healthCheckLoading ? 'Checking...' : 'Check Health'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if healthCheckResult !== null}
|
||||
<div class="p-2 rounded-md text-sm {healthCheckResult.success ? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300' : 'bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400'}">
|
||||
{#if healthCheckResult.success}
|
||||
Kubernetes autoscaling is healthy
|
||||
{:else}
|
||||
{healthCheckResult.error}
|
||||
{#if healthCheckResult.error?.includes('permissions') || healthCheckResult.error?.includes('role')}
|
||||
<br><small>Please follow the setup guide above to configure proper RBAC permissions.</small>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-row gap-2">
|
||||
<Button color="light" size="xs" variant="contained">Test scaling</Button>
|
||||
<div class="flex text-xs flex-row gap-2 items-center">
|
||||
<input class="!w-16" type="number" bind:value={test_input} />
|
||||
workers
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<ToggleButtonGroup selected={'script'} disabled class="mb-4 mt-2">
|
||||
{#snippet children({ item })}
|
||||
@@ -331,7 +405,7 @@
|
||||
<ToggleButton value="script" label="Custom script" {item} />
|
||||
<ToggleButton value="ecs" label="ECS (soon)" {item} />
|
||||
<ToggleButton value="nomad" label="Nomad (soon)" {item} />
|
||||
<ToggleButton value="kubernetes" label="Kubernetes (soon)" {item} />
|
||||
<ToggleButton value="kubernetes" label="Kubernetes" {item} />
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
} from '$lib/gen'
|
||||
import { inferArgs } from '$lib/infer'
|
||||
import { setCopilotInfo, userStore, workspaceStore } from '$lib/stores'
|
||||
import { emptySchema, readFieldsRecursively, sendUserToast } from '$lib/utils'
|
||||
import { emptySchema, readFieldsRecursively, sendUserToast, type StateStore } from '$lib/utils'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import { onDestroy, onMount, setContext, untrack } from 'svelte'
|
||||
import DarkModeToggle from '$lib/components/sidebar/DarkModeToggle.svelte'
|
||||
@@ -27,7 +27,7 @@
|
||||
import FlowModuleSchemaMap from './flows/map/FlowModuleSchemaMap.svelte'
|
||||
import FlowEditorPanel from './flows/content/FlowEditorPanel.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import { writable } from 'svelte/store'
|
||||
import type { FlowState } from './flows/flowState'
|
||||
import { initHistory } from '$lib/history.svelte'
|
||||
import type { FlowEditorContext, FlowInput, FlowInputEditorState } from './flows/types'
|
||||
@@ -51,7 +51,6 @@
|
||||
import { TestSteps } from './flows/testSteps.svelte'
|
||||
import { ModulesTestStates } from './modulesTest.svelte'
|
||||
import type { GraphModuleState } from './graph'
|
||||
import { updateDerivedModuleStatesFromTestJobs } from './flows/utils'
|
||||
|
||||
let flowCopilotContext: FlowCopilotContext = {
|
||||
shouldUpdatePropertyType: writable<{
|
||||
@@ -116,7 +115,6 @@
|
||||
const flowPreviewContent = $derived(flowPreviewButtons?.getFlowPreviewContent())
|
||||
const job: Job | undefined = $derived(flowPreviewContent?.getJob())
|
||||
let showJobStatus = $state(false)
|
||||
let testModuleId: string | undefined = $state(undefined)
|
||||
|
||||
type LastEditScript = {
|
||||
content: string
|
||||
@@ -453,7 +451,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
const flowStateStore = writable({} as FlowState)
|
||||
const flowStateStore = $state({ val: {} }) as StateStore<FlowState>
|
||||
|
||||
const previewArgsStore = $state({ val: {} })
|
||||
const scriptEditorDrawer = writable(undefined)
|
||||
@@ -464,8 +462,6 @@
|
||||
const triggersCount = writable<TriggersCount | undefined>(undefined)
|
||||
const modulesTestStates = new ModulesTestStates((moduleId) => {
|
||||
// Update the derived store with test job states
|
||||
delete $derivedModuleStates[moduleId]
|
||||
testModuleId = moduleId
|
||||
showJobStatus = false
|
||||
})
|
||||
const outputPickerOpenFns: Record<string, () => void> = $state({})
|
||||
@@ -538,11 +534,11 @@
|
||||
}
|
||||
|
||||
mod.value.input_transforms = input_transforms
|
||||
if (!deepEqual(schema, $flowStateStore[mod.id]?.schema)) {
|
||||
if (!$flowStateStore[mod.id]) {
|
||||
$flowStateStore[mod.id] = { schema }
|
||||
if (!deepEqual(schema, flowStateStore.val[mod.id]?.schema)) {
|
||||
if (!flowStateStore.val[mod.id]) {
|
||||
flowStateStore.val[mod.id] = { schema }
|
||||
} else {
|
||||
$flowStateStore[mod.id].schema = schema
|
||||
flowStateStore.val[mod.id].schema = schema
|
||||
}
|
||||
reload++
|
||||
}
|
||||
@@ -586,25 +582,12 @@
|
||||
$selectedIdStore && untrack(() => inferModuleArgs($selectedIdStore))
|
||||
})
|
||||
|
||||
const localModuleStates: Writable<Record<string, GraphModuleState>> = $derived(
|
||||
flowPreviewContent?.getLocalModuleStates() ?? writable({})
|
||||
)
|
||||
let localModuleStates: Record<string, GraphModuleState> = $state({})
|
||||
|
||||
const suspendStatus: Writable<Record<string, { job: Job; nb: number }>> = $derived(
|
||||
flowPreviewContent?.getSuspendStatus() ?? writable({})
|
||||
)
|
||||
let suspendStatus: StateStore<Record<string, { job: Job; nb: number }>> = $state({ val: {} })
|
||||
|
||||
// Create a derived store that only shows the module states when showModuleStatus is true
|
||||
// this store can also be updated
|
||||
let derivedModuleStates = writable<Record<string, GraphModuleState>>({})
|
||||
$effect(() => {
|
||||
derivedModuleStates.update((currentStates) => {
|
||||
return showJobStatus ? $localModuleStates : currentStates
|
||||
})
|
||||
})
|
||||
$effect(() => {
|
||||
updateDerivedModuleStatesFromTestJobs(testModuleId, modulesTestStates, derivedModuleStates)
|
||||
})
|
||||
|
||||
let flowModuleSchemaMap: FlowModuleSchemaMap | undefined = $state()
|
||||
function onJobDone() {
|
||||
@@ -639,14 +622,9 @@
|
||||
}
|
||||
|
||||
function resetModulesStates() {
|
||||
derivedModuleStates.set({})
|
||||
showJobStatus = false
|
||||
}
|
||||
|
||||
const individualStepTests = $derived(
|
||||
!(showJobStatus && job) && Object.keys($derivedModuleStates).length > 0
|
||||
)
|
||||
|
||||
const flowHasChanged = $derived(flowPreviewContent?.flowHasChanged())
|
||||
</script>
|
||||
|
||||
@@ -785,7 +763,7 @@
|
||||
bind:this={flowPreviewButtons}
|
||||
{onJobDone}
|
||||
onRunPreview={() => {
|
||||
localModuleStates.set({})
|
||||
localModuleStates = {}
|
||||
showJobStatus = true
|
||||
}}
|
||||
/>
|
||||
@@ -800,19 +778,20 @@
|
||||
disableTutorials
|
||||
smallErrorHandler={true}
|
||||
disableStaticInputs
|
||||
localModuleStates={derivedModuleStates}
|
||||
{localModuleStates}
|
||||
onTestUpTo={flowPreviewButtons?.testUpTo}
|
||||
testModuleStates={modulesTestStates}
|
||||
isOwner={flowPreviewContent?.getIsOwner?.()}
|
||||
onTestFlow={flowPreviewButtons?.runPreview}
|
||||
isRunning={flowPreviewContent?.getIsRunning?.()}
|
||||
onCancelTestFlow={flowPreviewContent?.cancelTest}
|
||||
onOpenPreview={flowPreviewButtons?.openPreview}
|
||||
onHideJobStatus={resetModulesStates}
|
||||
{individualStepTests}
|
||||
flowJob={job}
|
||||
{showJobStatus}
|
||||
onDelete={(id) => {
|
||||
delete $derivedModuleStates[id]
|
||||
delete localModuleStates[id]
|
||||
delete modulesTestStates.states[id]
|
||||
}}
|
||||
{flowHasChanged}
|
||||
/>
|
||||
|
||||
@@ -928,7 +928,7 @@
|
||||
>
|
||||
</button>
|
||||
{:else if !s3object?.disable_download}
|
||||
<FileDownload {s3object} />
|
||||
<FileDownload {workspaceId} {s3object} {appPath} />
|
||||
{:else}
|
||||
<div class="flex text-secondary pt-2">{s3object?.s3} (download disabled)</div>
|
||||
{/if}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
import type { EditableSchemaFormUi } from '$lib/components/custom_ui'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import Editor from './Editor.svelte'
|
||||
import AddPropertyV2 from './schema/AddPropertyV2.svelte'
|
||||
|
||||
// export let openEditTab: () => void = () => {}
|
||||
const dispatch = createEventDispatcher()
|
||||
@@ -69,6 +70,7 @@
|
||||
dynSelectCode?: string | undefined
|
||||
dynSelectLang?: ScriptLang | undefined
|
||||
showDynSelectOpt?: boolean
|
||||
addPropertyInEditorTab?: boolean
|
||||
openEditTab?: import('svelte').Snippet
|
||||
addProperty?: import('svelte').Snippet
|
||||
runButton?: import('svelte').Snippet
|
||||
@@ -104,6 +106,7 @@
|
||||
dynSelectCode = $bindable(),
|
||||
dynSelectLang = $bindable(),
|
||||
showDynSelectOpt = false,
|
||||
addPropertyInEditorTab = false,
|
||||
openEditTab,
|
||||
addProperty,
|
||||
runButton,
|
||||
@@ -509,22 +512,31 @@
|
||||
{:else}
|
||||
<!-- WIP -->
|
||||
{#if jsonEnabled && customUi?.jsonOnly != true}
|
||||
<div class="w-full p-3 flex justify-end">
|
||||
<Toggle
|
||||
bind:checked={jsonView}
|
||||
label="JSON View"
|
||||
size="xs"
|
||||
options={{
|
||||
right: 'JSON editor',
|
||||
rightTooltip:
|
||||
'Arguments can be edited either using the wizard, or by editing their JSON Schema.'
|
||||
}}
|
||||
lightMode
|
||||
on:change={() => {
|
||||
schemaString = JSON.stringify(schema, null, '\t')
|
||||
editor?.setCode(schemaString)
|
||||
}}
|
||||
/>
|
||||
<div class="w-full p-3 flex gap-4 justify-end items-center">
|
||||
{#if addPropertyInEditorTab}
|
||||
<AddPropertyV2 bind:schema on:change>
|
||||
{#snippet trigger()}
|
||||
<Button color="light" size="xs" iconOnly startIcon={{ icon: Plus }} />
|
||||
{/snippet}
|
||||
</AddPropertyV2>
|
||||
{/if}
|
||||
<div class="shrink-0">
|
||||
<Toggle
|
||||
bind:checked={jsonView}
|
||||
label="JSON View"
|
||||
size="xs"
|
||||
options={{
|
||||
right: 'JSON editor',
|
||||
rightTooltip:
|
||||
'Arguments can be edited either using the wizard, or by editing their JSON Schema.'
|
||||
}}
|
||||
lightMode
|
||||
on:change={() => {
|
||||
schemaString = JSON.stringify(schema, null, '\t')
|
||||
editor?.setCode(schemaString)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -655,7 +667,6 @@
|
||||
const isS3 = v == 'S3'
|
||||
const isOneOf = v == 'oneOf'
|
||||
const isDynSelect = v == 'dynselect'
|
||||
|
||||
const emptyProperty = {
|
||||
contentEncoding: undefined,
|
||||
enum_: undefined,
|
||||
|
||||
@@ -100,7 +100,8 @@
|
||||
codeCompletionSessionEnabled,
|
||||
lspTokenStore,
|
||||
formatOnSave,
|
||||
vimMode
|
||||
vimMode,
|
||||
relativeLineNumbers
|
||||
} from '$lib/stores'
|
||||
|
||||
import { editorConfig, updateOptions } from '$lib/editorUtils'
|
||||
@@ -183,6 +184,7 @@
|
||||
loadAsync?: boolean
|
||||
key?: string | undefined
|
||||
class?: string | undefined
|
||||
moduleId?: string
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -208,7 +210,8 @@
|
||||
changeTimeout = 500,
|
||||
loadAsync = false,
|
||||
key = undefined,
|
||||
class: clazz = undefined
|
||||
class: clazz = undefined,
|
||||
moduleId = undefined
|
||||
}: Props = $props()
|
||||
|
||||
$effect.pre(() => {
|
||||
@@ -1234,7 +1237,7 @@
|
||||
|
||||
try {
|
||||
editor = meditor.create(divEl as HTMLDivElement, {
|
||||
...editorConfig(code ?? '', lang, automaticLayout, fixedOverflowWidgets),
|
||||
...editorConfig(code ?? '', lang, automaticLayout, fixedOverflowWidgets, $relativeLineNumbers),
|
||||
model,
|
||||
fontSize: !small ? 14 : 12,
|
||||
lineNumbersMinChars,
|
||||
@@ -1327,7 +1330,8 @@
|
||||
aiChatManager.addSelectedLinesToContext(
|
||||
selectedLines,
|
||||
selection.startLineNumber,
|
||||
selection.endLineNumber
|
||||
selection.endLineNumber,
|
||||
moduleId
|
||||
)
|
||||
} else {
|
||||
aiChatManager.toggleOpen()
|
||||
@@ -1652,6 +1656,11 @@
|
||||
$effect(() => {
|
||||
files && model && untrack(() => onFileChanges())
|
||||
})
|
||||
$effect(() => {
|
||||
editor?.updateOptions({
|
||||
lineNumbers: $relativeLineNumbers ? 'relative' : 'on'
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={onKeyDown} />
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
showHistoryDrawer?: boolean
|
||||
right?: import('svelte').Snippet
|
||||
openAiChat?: boolean
|
||||
moduleId?: string
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -105,7 +106,8 @@
|
||||
diffMode = false,
|
||||
showHistoryDrawer = $bindable(false),
|
||||
right,
|
||||
openAiChat = false
|
||||
openAiChat = false,
|
||||
moduleId = undefined
|
||||
}: Props = $props()
|
||||
|
||||
let contextualVariablePicker: ItemPicker | undefined = $state()
|
||||
@@ -964,7 +966,7 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
|
||||
|
||||
{#if customUi?.aiGen != false}
|
||||
{#if openAiChat}
|
||||
<FlowInlineScriptAiButton />
|
||||
<FlowInlineScriptAiButton {moduleId} />
|
||||
{:else}
|
||||
<ScriptGen {editor} {diffEditor} {lang} {iconOnly} {args} />
|
||||
{/if}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { Settings } from 'lucide-svelte'
|
||||
import FormatOnSave from './FormatOnSave.svelte'
|
||||
import VimMode from './VimMode.svelte'
|
||||
import RelativeLineNumbers from './RelativeLineNumbers.svelte'
|
||||
import { Button } from './common'
|
||||
import CodeCompletionStatus from './copilot/CodeCompletionStatus.svelte'
|
||||
import type { EditorBarUi } from './custom_ui'
|
||||
@@ -39,6 +40,11 @@
|
||||
<VimMode />
|
||||
</div>
|
||||
{/if}
|
||||
{#if customUi?.relativeLineNumbers != false}
|
||||
<div>
|
||||
<RelativeLineNumbers />
|
||||
</div>
|
||||
{/if}
|
||||
{#if customUi?.aiCompletion != false}
|
||||
<div>
|
||||
<CodeCompletionStatus />
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
{/if}
|
||||
|
||||
{#if displayType}
|
||||
{#if format && !format.startsWith('resource')}
|
||||
{#if format && !format.startsWith('resource') && !format.startsWith('jsonschema-')}
|
||||
<span class="text-xs italic ml-2 text-tertiary dark:text-indigo-400">
|
||||
{format}
|
||||
</span>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
let mod: any | undefined = $state(undefined)
|
||||
async function loadSchema() {
|
||||
try {
|
||||
const res = await getFirstStepSchema($flowStateStore, flowStore.val)
|
||||
const res = await getFirstStepSchema(flowStateStore.val, flowStore.val)
|
||||
schema = res.schema
|
||||
mod = res.mod
|
||||
dispatch('connectFirstNode', { connectFirstNode: res.connectFirstNode })
|
||||
@@ -28,7 +28,7 @@
|
||||
}
|
||||
}
|
||||
$effect(() => {
|
||||
flowStore.val && $flowStateStore && untrack(() => loadSchema())
|
||||
flowStore.val && flowStateStore.val && untrack(() => loadSchema())
|
||||
})
|
||||
|
||||
function handleClick() {
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
orderedJsonStringify,
|
||||
readFieldsRecursively,
|
||||
replaceFalseWithUndefined,
|
||||
type StateStore,
|
||||
type Value
|
||||
} from '$lib/utils'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
@@ -33,7 +34,7 @@
|
||||
import AIChangesWarningModal from '$lib/components/copilot/chat/flow/AIChangesWarningModal.svelte'
|
||||
|
||||
import { onMount, setContext, untrack, type ComponentType } from 'svelte'
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import { writable } from 'svelte/store'
|
||||
import CenteredPage from './CenteredPage.svelte'
|
||||
import { Badge, Button, UndoRedo } from './common'
|
||||
import FlowEditor from './flows/FlowEditor.svelte'
|
||||
@@ -42,7 +43,7 @@
|
||||
import FlowImportExportMenu from './flows/header/FlowImportExportMenu.svelte'
|
||||
import FlowPreviewButtons from './flows/header/FlowPreviewButtons.svelte'
|
||||
import type { FlowEditorContext, FlowInput, FlowInputEditorState } from './flows/types'
|
||||
import { cleanInputs, updateDerivedModuleStatesFromTestJobs } from './flows/utils'
|
||||
import { cleanInputs } from './flows/utils'
|
||||
import {
|
||||
Calendar,
|
||||
Pen,
|
||||
@@ -577,13 +578,7 @@
|
||||
}
|
||||
|
||||
let insertButtonOpen = writable<boolean>(false)
|
||||
let testModuleId: string | undefined = $state(undefined)
|
||||
let modulesTestStates = new ModulesTestStates((moduleId) => {
|
||||
// Update the derived store with test job states
|
||||
delete $derivedModuleStates[moduleId]
|
||||
testModuleId = moduleId
|
||||
showJobStatus = false
|
||||
})
|
||||
let modulesTestStates = new ModulesTestStates()
|
||||
let outputPickerOpenFns: Record<string, () => void> = $state({})
|
||||
let flowEditor: FlowEditor | undefined = $state(undefined)
|
||||
|
||||
@@ -933,33 +928,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
const localModuleStates: Writable<Record<string, GraphModuleState>> = $derived(
|
||||
flowPreviewContent?.getLocalModuleStates() ?? writable({})
|
||||
)
|
||||
const suspendStatus: Writable<Record<string, { job: Job; nb: number }>> = $derived(
|
||||
flowPreviewContent?.getSuspendStatus() ?? writable({})
|
||||
)
|
||||
|
||||
// Create a derived store that only shows the module states when showModuleStatus is true
|
||||
// this store can also be updated
|
||||
let derivedModuleStates = writable<Record<string, GraphModuleState>>({})
|
||||
$effect(() => {
|
||||
derivedModuleStates.update((currentStates) => {
|
||||
return showJobStatus ? $localModuleStates : currentStates
|
||||
})
|
||||
})
|
||||
$effect(() => {
|
||||
updateDerivedModuleStatesFromTestJobs(testModuleId, modulesTestStates, derivedModuleStates)
|
||||
})
|
||||
|
||||
function resetModulesStates() {
|
||||
derivedModuleStates.set({})
|
||||
showJobStatus = false
|
||||
}
|
||||
|
||||
const individualStepTests = $derived(
|
||||
!(showJobStatus && job) && Object.keys($derivedModuleStates).length > 0
|
||||
)
|
||||
let localModuleStates: Record<string, GraphModuleState> = $state({})
|
||||
let suspendStatus: StateStore<Record<string, { job: Job; nb: number }>> = $state({ val: {} })
|
||||
|
||||
const flowHasChanged = $derived(flowPreviewContent?.flowHasChanged())
|
||||
</script>
|
||||
@@ -1025,7 +995,7 @@
|
||||
for (const mod of restoredModules) {
|
||||
if (mod) {
|
||||
try {
|
||||
loadFlowModuleState(mod).then((state) => ($flowStateStore[mod.id] = state))
|
||||
loadFlowModuleState(mod).then((state) => (flowStateStore.val[mod.id] = state))
|
||||
} catch (e) {
|
||||
console.error('Error loading state for restored node', e)
|
||||
}
|
||||
@@ -1155,10 +1125,12 @@
|
||||
showCaptureHint.set(true)
|
||||
}}
|
||||
{onJobDone}
|
||||
bind:localModuleStates
|
||||
bind:this={flowPreviewButtons}
|
||||
{loading}
|
||||
onRunPreview={() => {
|
||||
localModuleStates.set({})
|
||||
modulesTestStates.hideJobsInGraph()
|
||||
localModuleStates = {}
|
||||
showJobStatus = true
|
||||
}}
|
||||
/>
|
||||
@@ -1185,7 +1157,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- metadata -->
|
||||
{#if $flowStateStore}
|
||||
{#if flowStateStore.val}
|
||||
<FlowEditor
|
||||
bind:this={flowEditor}
|
||||
{disabledFlowInputs}
|
||||
@@ -1228,18 +1200,22 @@
|
||||
showFlowAiButton={!disableAi && customUi?.topBar?.aiBuilder != false}
|
||||
toggleAiChat={() => aiChatManager.toggleOpen()}
|
||||
onOpenPreview={flowPreviewButtons?.openPreview}
|
||||
localModuleStates={derivedModuleStates}
|
||||
localModuleStates={showJobStatus ? localModuleStates : {}}
|
||||
{showJobStatus}
|
||||
testModuleStates={modulesTestStates}
|
||||
isOwner={flowPreviewContent?.getIsOwner()}
|
||||
onTestFlow={flowPreviewButtons?.runPreview}
|
||||
isRunning={flowPreviewContent?.getIsRunning()}
|
||||
onCancelTestFlow={flowPreviewContent?.cancelTest}
|
||||
onHideJobStatus={resetModulesStates}
|
||||
{individualStepTests}
|
||||
onHideJobStatus={() => {
|
||||
modulesTestStates.hideJobsInGraph()
|
||||
showJobStatus = false
|
||||
}}
|
||||
{job}
|
||||
{suspendStatus}
|
||||
{showJobStatus}
|
||||
onDelete={(id) => {
|
||||
delete $derivedModuleStates[id]
|
||||
delete localModuleStates[id]
|
||||
delete modulesTestStates.states[id]
|
||||
}}
|
||||
{flowHasChanged}
|
||||
/>
|
||||
|
||||
@@ -137,6 +137,8 @@
|
||||
Inline {stepDetail.value.language} script
|
||||
{:else if stepDetail.value.type == 'script'}
|
||||
Workspace script
|
||||
{:else if stepDetail.value.type == 'aiagent'}
|
||||
AI Agent
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
@@ -217,6 +219,11 @@
|
||||
{:else}
|
||||
<FlowModuleScript path={stepDetail.value.path} />
|
||||
{/if}
|
||||
{:else if stepDetail.value.type == 'aiagent'}
|
||||
<div class="text-2xs">
|
||||
<h3 class="mb-2 font-semibold mt-2">Step Inputs</h3>
|
||||
<InputTransformsViewer inputTransforms={stepDetail?.value?.input_transforms ?? {}} />
|
||||
</div>
|
||||
{:else if stepDetail.value.type == 'forloopflow'}
|
||||
<div>
|
||||
<p class="font-medium text-secondary pb-2"> Iterator expression: </p>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import DisplayResult from './DisplayResult.svelte'
|
||||
import LogViewer from './LogViewer.svelte'
|
||||
import type { CompletedJob, FlowModule, Job } from '$lib/gen'
|
||||
import AiAgentLogViewer from './AIAgentLogViewer.svelte'
|
||||
|
||||
interface Props {
|
||||
waitingForExecutor?: boolean
|
||||
@@ -18,6 +20,12 @@
|
||||
refreshLog?: boolean
|
||||
downloadLogs?: boolean
|
||||
tagLabel?: string | undefined
|
||||
aiAgentStatus?: {
|
||||
tools: FlowModule[]
|
||||
agentJob: Partial<CompletedJob> & Pick<CompletedJob, 'id'> & { type: 'CompletedJob' }
|
||||
storedToolCallJobs?: Record<number, Job>
|
||||
onToolJobLoaded?: (job: Job, idx: number) => void
|
||||
}
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -33,7 +41,8 @@
|
||||
tag = undefined,
|
||||
workspaceId = undefined,
|
||||
downloadLogs = true,
|
||||
tagLabel = undefined
|
||||
tagLabel = undefined,
|
||||
aiAgentStatus = undefined
|
||||
}: Props = $props()
|
||||
</script>
|
||||
|
||||
@@ -54,13 +63,17 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="overflow-auto {col ? '' : 'max-h-80'} relative">
|
||||
<LogViewer
|
||||
{tagLabel}
|
||||
download={downloadLogs}
|
||||
content={logs ?? ''}
|
||||
{jobId}
|
||||
isLoading={waitingForExecutor}
|
||||
{tag}
|
||||
/>
|
||||
{#if aiAgentStatus}
|
||||
<AiAgentLogViewer {...aiAgentStatus} {workspaceId} />
|
||||
{:else}
|
||||
<LogViewer
|
||||
{tagLabel}
|
||||
download={downloadLogs}
|
||||
content={logs ?? ''}
|
||||
{jobId}
|
||||
isLoading={waitingForExecutor}
|
||||
{tag}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,13 +21,12 @@
|
||||
import FlowJobsMenu from './flows/map/FlowJobsMenu.svelte'
|
||||
import BarsStaggered from './icons/BarsStaggered.svelte'
|
||||
import type { GraphModuleState } from './graph/model'
|
||||
import type { Writable } from 'svelte/store'
|
||||
|
||||
type RootJobData = Partial<Job>
|
||||
|
||||
interface Props {
|
||||
modules: FlowModule[]
|
||||
localModuleStates: Writable<Record<string, GraphModuleState>>
|
||||
localModuleStates: Record<string, GraphModuleState>
|
||||
rootJob: RootJobData
|
||||
flowStatus: FlowStatusModule['type'] | undefined
|
||||
expandedRows: Record<string, boolean>
|
||||
@@ -46,6 +45,7 @@
|
||||
) => Promise<void>
|
||||
getSelectedIteration: (stepId: string) => number
|
||||
flowSummary?: string
|
||||
mode?: 'flow' | 'aiagent'
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -64,7 +64,8 @@
|
||||
flowId = 'root',
|
||||
onSelectedIteration,
|
||||
getSelectedIteration,
|
||||
flowSummary
|
||||
flowSummary,
|
||||
mode = 'flow'
|
||||
}: Props = $props()
|
||||
|
||||
function getJobLink(jobId: string | undefined): string {
|
||||
@@ -97,16 +98,18 @@
|
||||
function getStepProgress(job: RootJobData, totalSteps: number): string {
|
||||
if (totalSteps === 0) return ''
|
||||
|
||||
const stepWord = mode === 'aiagent' ? 'action' : 'step'
|
||||
|
||||
// If flow is completed, show total steps
|
||||
if (job.type === 'CompletedJob') {
|
||||
return ` (${totalSteps} step${totalSteps === 1 ? '' : 's'})`
|
||||
return ` (${totalSteps} ${stepWord}${totalSteps === 1 ? '' : 's'})`
|
||||
}
|
||||
|
||||
// If flow is running, use flow_status.step if available (like JobStatus.svelte)
|
||||
if (job.type === 'QueuedJob') {
|
||||
if (job.flow_status?.step !== undefined) {
|
||||
const currentStep = (job.flow_status.step ?? 0) + 1
|
||||
return ` (step ${currentStep} of ${totalSteps})`
|
||||
return ` (${stepWord} ${currentStep} of ${totalSteps})`
|
||||
}
|
||||
|
||||
return ''
|
||||
@@ -122,7 +125,7 @@
|
||||
}
|
||||
|
||||
function hasEmptySubflow(stepId: string, stepType: FlowModuleValue['type'] | undefined): boolean {
|
||||
const state = $localModuleStates[stepId]
|
||||
const state = localModuleStates[stepId]
|
||||
|
||||
if (!state || !stepType) return false
|
||||
return (
|
||||
@@ -172,7 +175,7 @@
|
||||
}
|
||||
|
||||
// Check if this entry itself has an error (but don't flag it - only its parents)
|
||||
const stepStatus = $localModuleStates[module.id]?.type
|
||||
const stepStatus = localModuleStates[module.id]?.type
|
||||
if (stepStatus === 'Failure') {
|
||||
currentEntryHasError = true
|
||||
// Don't add the entry itself to parentsWithErrors
|
||||
@@ -318,7 +321,7 @@
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs font-mono">
|
||||
{level == 0 ? 'Flow' : 'Subflow'}
|
||||
{mode === 'aiagent' ? 'AI Agent' : level == 0 ? 'Flow' : 'Subflow'}
|
||||
{#if flowInfo.label}
|
||||
: {flowInfo.label}
|
||||
{/if}
|
||||
@@ -402,7 +405,7 @@
|
||||
{#if modules.length > 0}
|
||||
{#each modules as module (module.id)}
|
||||
{@const isLeafStep = !hasSubflows(module)}
|
||||
{@const status = $localModuleStates[module.id]?.type}
|
||||
{@const status = localModuleStates[module.id]?.type}
|
||||
{@const isRunning = status === 'InProgress' || status === 'WaitingForExecutor'}
|
||||
{@const hasEmptySubflowValue = hasEmptySubflow(module.id, module.value.type)}
|
||||
{@const isCollapsible = !hasEmptySubflowValue}
|
||||
@@ -451,20 +454,26 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs font-mono">
|
||||
<b>
|
||||
{module.id}
|
||||
{mode === 'aiagent'
|
||||
? module.summary
|
||||
? 'Tool call'
|
||||
: 'Message'
|
||||
: module.id}
|
||||
</b>
|
||||
{#if module.value.type === 'forloopflow'}
|
||||
For loop
|
||||
{:else if module.value.type === 'whileloopflow'}
|
||||
While loop
|
||||
{:else if module.value.type === 'branchall'}
|
||||
Branch to all
|
||||
{:else if module.value.type === 'branchone'}
|
||||
Branch to one
|
||||
{:else if module.value.type === 'flow'}
|
||||
Subflow
|
||||
{:else}
|
||||
Step
|
||||
{#if mode === 'flow'}
|
||||
{#if module.value.type === 'forloopflow'}
|
||||
For loop
|
||||
{:else if module.value.type === 'whileloopflow'}
|
||||
While loop
|
||||
{:else if module.value.type === 'branchall'}
|
||||
Branch to all
|
||||
{:else if module.value.type === 'branchone'}
|
||||
Branch to one
|
||||
{:else if module.value.type === 'flow'}
|
||||
Subflow
|
||||
{:else}
|
||||
Step
|
||||
{/if}
|
||||
{/if}
|
||||
{#if module.summary}
|
||||
: {module.summary}
|
||||
@@ -479,7 +488,7 @@
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
{#if !hasEmptySubflowValue && $localModuleStates[module.id]?.flow_jobs && (module.value.type === 'forloopflow' || module.value.type === 'whileloopflow')}
|
||||
{#if !hasEmptySubflowValue && localModuleStates[module.id]?.flow_jobs && (module.value.type === 'forloopflow' || module.value.type === 'whileloopflow')}
|
||||
<span
|
||||
class="text-xs font-mono font-medium inline-flex items-center grow min-w-0 -my-2"
|
||||
>
|
||||
@@ -488,18 +497,18 @@
|
||||
moduleId={module.id}
|
||||
id={module.id}
|
||||
{onSelectedIteration}
|
||||
flowJobsSuccess={$localModuleStates[module.id]
|
||||
flowJobsSuccess={localModuleStates[module.id]
|
||||
?.flow_jobs_success}
|
||||
flowJobs={$localModuleStates[module.id]?.flow_jobs}
|
||||
selected={$localModuleStates[module.id]?.selectedForloopIndex ??
|
||||
flowJobs={localModuleStates[module.id]?.flow_jobs}
|
||||
selected={localModuleStates[module.id]?.selectedForloopIndex ??
|
||||
0}
|
||||
selectedManually={$localModuleStates[module.id]
|
||||
selectedManually={localModuleStates[module.id]
|
||||
?.selectedForLoopSetManually ?? false}
|
||||
showIcon={false}
|
||||
/>
|
||||
</span>
|
||||
{#if module.value.type === 'forloopflow'}
|
||||
{`/${$localModuleStates[module.id]?.iteration_total ?? 0}`}
|
||||
{`/${localModuleStates[module.id]?.iteration_total ?? 0}`}
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
@@ -507,37 +516,39 @@
|
||||
</div>
|
||||
|
||||
{#if isLeafStep}
|
||||
{@const jobId = $localModuleStates[module.id]?.job_id}
|
||||
<a
|
||||
href={getJobLink(jobId ?? '')}
|
||||
class="text-xs text-primary hover:underline font-mono"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{truncateRev(jobId ?? '', 6)}
|
||||
</a>
|
||||
{@const jobId = localModuleStates[module.id]?.job_id}
|
||||
{#if jobId}
|
||||
<a
|
||||
href={getJobLink(jobId ?? '')}
|
||||
class="text-xs text-primary hover:underline font-mono"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{truncateRev(jobId ?? '', 6)}
|
||||
</a>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if isCollapsible && isExpanded(module.id, isRunning)}
|
||||
{@const args = $localModuleStates[module.id]?.args}
|
||||
{@const logs = $localModuleStates[module.id]?.logs}
|
||||
{@const result = $localModuleStates[module.id]?.result}
|
||||
{@const jobId = $localModuleStates[module.id]?.job_id}
|
||||
{@const args = localModuleStates[module.id]?.args}
|
||||
{@const logs = localModuleStates[module.id]?.logs}
|
||||
{@const result = localModuleStates[module.id]?.result}
|
||||
{@const jobId = localModuleStates[module.id]?.job_id}
|
||||
<div class="my-1 transition-all duration-200 ease-in-out">
|
||||
<!-- Show child steps if they exist -->
|
||||
{#each getSubflows(module) as subflow}
|
||||
{@const subflowJob = {
|
||||
id: jobId,
|
||||
type:
|
||||
$localModuleStates[module.id]?.type === 'Failure' ||
|
||||
$localModuleStates[module.id]?.type === 'Success'
|
||||
localModuleStates[module.id]?.type === 'Failure' ||
|
||||
localModuleStates[module.id]?.type === 'Success'
|
||||
? 'CompletedJob'
|
||||
: ('QueuedJob' as Job['type']),
|
||||
logs,
|
||||
result,
|
||||
args,
|
||||
success: $localModuleStates[module.id]?.type === 'Success'
|
||||
success: localModuleStates[module.id]?.type === 'Success'
|
||||
}}
|
||||
<div class="border-l mb-2">
|
||||
<!-- Recursively render child steps using FlowLogViewer -->
|
||||
@@ -545,7 +556,7 @@
|
||||
modules={subflow.modules}
|
||||
{localModuleStates}
|
||||
rootJob={subflowJob}
|
||||
flowStatus={$localModuleStates[module.id]?.type}
|
||||
flowStatus={localModuleStates[module.id]?.type}
|
||||
{expandedRows}
|
||||
{allExpanded}
|
||||
{showResultsInputs}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { Job } from '$lib/gen'
|
||||
import { type Writable } from 'svelte/store'
|
||||
import type { GraphModuleState } from './graph'
|
||||
import FlowLogViewer from './FlowLogViewer.svelte'
|
||||
import { untrack } from 'svelte'
|
||||
@@ -8,8 +7,8 @@
|
||||
import { readFieldsRecursively } from '$lib/utils'
|
||||
|
||||
interface Props {
|
||||
job: Job
|
||||
localModuleStates: Writable<Record<string, GraphModuleState>>
|
||||
job: Partial<Job>
|
||||
localModuleStates: Record<string, GraphModuleState>
|
||||
workspaceId: string | undefined
|
||||
render: boolean
|
||||
onSelectedIteration: (
|
||||
@@ -17,9 +16,17 @@
|
||||
| { id: string; index: number; manuallySet: true; moduleId: string }
|
||||
| { manuallySet: false; moduleId: string }
|
||||
) => Promise<void>
|
||||
mode?: 'flow' | 'aiagent'
|
||||
}
|
||||
|
||||
let { job, localModuleStates, workspaceId, render, onSelectedIteration }: Props = $props()
|
||||
let {
|
||||
job,
|
||||
localModuleStates,
|
||||
workspaceId,
|
||||
render,
|
||||
onSelectedIteration,
|
||||
mode = 'flow'
|
||||
}: Props = $props()
|
||||
|
||||
// State for tracking expanded rows - using Record to allow explicit control
|
||||
let expandedRows: Record<string, boolean> = $state({})
|
||||
@@ -44,7 +51,7 @@
|
||||
}
|
||||
|
||||
function getSelectedIteration(stepId: string): number {
|
||||
return $localModuleStates[stepId]?.selectedForloopIndex ?? 0
|
||||
return localModuleStates[stepId]?.selectedForloopIndex ?? 0
|
||||
}
|
||||
|
||||
function toggleExpandAll() {
|
||||
@@ -69,5 +76,6 @@
|
||||
{getSelectedIteration}
|
||||
flowId="root"
|
||||
flowStatus={undefined}
|
||||
{mode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -176,10 +176,10 @@
|
||||
<div class="pt-4 grow">
|
||||
{#if jobId}
|
||||
<FlowStatusViewer
|
||||
{flowStateStore}
|
||||
bind:flowStateStore={flowStateStore.val}
|
||||
{jobId}
|
||||
on:jobsLoaded={({ detail }) => {
|
||||
job = detail
|
||||
onJobsLoaded={({ job: newJob }) => {
|
||||
job = newJob
|
||||
}}
|
||||
bind:selectedJobStep
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { stopPropagation } from 'svelte/legacy'
|
||||
|
||||
import { type Job, JobService, type RestartedFrom, type OpenFlow, type ScriptLang } from '$lib/gen'
|
||||
import {
|
||||
type Job,
|
||||
JobService,
|
||||
type RestartedFrom,
|
||||
type OpenFlow,
|
||||
type ScriptLang
|
||||
} from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { Badge, Button } from './common'
|
||||
import Popover from '$lib/components/meltComponents/Popover.svelte'
|
||||
@@ -13,14 +19,13 @@
|
||||
import FlowStatusViewer from '../components/FlowStatusViewer.svelte'
|
||||
import FlowProgressBar from './flows/FlowProgressBar.svelte'
|
||||
import { AlertTriangle, ArrowRight, CornerDownLeft, Play, RefreshCw, X } from 'lucide-svelte'
|
||||
import { emptyString, sendUserToast } from '$lib/utils'
|
||||
import { emptyString, sendUserToast, type StateStore } from '$lib/utils'
|
||||
import { dfs } from './flows/dfs'
|
||||
import { sliceModules } from './flows/flowStateUtils.svelte'
|
||||
import InputSelectedBadge from './schema/InputSelectedBadge.svelte'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import JsonInputs from './JsonInputs.svelte'
|
||||
import FlowHistoryJobPicker from './FlowHistoryJobPicker.svelte'
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import type { DurationStatus, GraphModuleState } from './graph'
|
||||
import { getStepHistoryLoaderContext } from './stepHistoryLoader.svelte'
|
||||
import { aiChatManager } from './copilot/chat/AIChatManager.svelte'
|
||||
@@ -39,8 +44,8 @@
|
||||
rightColumnSelect?: 'timeline' | 'node_status' | 'node_definition' | 'user_states'
|
||||
branchOrIterationN?: number
|
||||
scrollTop?: number
|
||||
localModuleStates?: Writable<Record<string, GraphModuleState>>
|
||||
localDurationStatuses?: Writable<Record<string, DurationStatus>>
|
||||
localModuleStates?: Record<string, GraphModuleState>
|
||||
localDurationStatuses?: Record<string, DurationStatus>
|
||||
onRunPreview?: () => void
|
||||
render?: boolean
|
||||
onJobDone?: () => void
|
||||
@@ -63,8 +68,8 @@
|
||||
rightColumnSelect = $bindable('timeline'),
|
||||
branchOrIterationN = $bindable(0),
|
||||
scrollTop = $bindable(0),
|
||||
localModuleStates = $bindable(writable({})),
|
||||
localDurationStatuses = $bindable(writable({})),
|
||||
localModuleStates = $bindable({}),
|
||||
localDurationStatuses = $bindable({}),
|
||||
onRunPreview,
|
||||
render = false,
|
||||
onJobDone,
|
||||
@@ -77,7 +82,7 @@
|
||||
let jsonEditor: JsonInputs | undefined = $state(undefined)
|
||||
let schemaHeight = $state(0)
|
||||
let isValid: boolean = $state(true)
|
||||
let suspendStatus: Writable<Record<string, { job: Job; nb: number }>> = $state(writable({}))
|
||||
let suspendStatus: StateStore<Record<string, { job: Job; nb: number }>> = $state({ val: {} })
|
||||
let isOwner: boolean = $state(false)
|
||||
|
||||
export function test() {
|
||||
@@ -568,9 +573,9 @@
|
||||
bind:suspendStatus
|
||||
hideDownloadInGraph={customUi?.downloadLogs === false}
|
||||
wideResults
|
||||
{flowStateStore}
|
||||
bind:flowStateStore={flowStateStore.val}
|
||||
{jobId}
|
||||
on:done={(x) => {
|
||||
onDone={() => {
|
||||
isRunning = false
|
||||
$executionCount = $executionCount + 1
|
||||
onJobDone?.()
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
import FlowPreviewStatus from './preview/FlowPreviewStatus.svelte'
|
||||
import FlowStatusWaitingForEvents from './FlowStatusWaitingForEvents.svelte'
|
||||
import type { FlowStatusModule, Job } from '$lib/gen'
|
||||
import { emptyString } from '$lib/utils'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import { emptyString, type StateStore } from '$lib/utils'
|
||||
import Badge from './common/badge/Badge.svelte'
|
||||
|
||||
interface Props {
|
||||
@@ -14,8 +13,8 @@
|
||||
isOwner: boolean
|
||||
hideFlowResult: boolean
|
||||
hideDownloadLogs: boolean
|
||||
innerModules: FlowStatusModule[]
|
||||
suspendStatus: Writable<Record<string, { job: Job; nb: number }>>
|
||||
innerModules: FlowStatusModule[] | undefined
|
||||
suspendStatus: StateStore<Record<string, { job: Job; nb: number }>>
|
||||
hideJobId?: boolean
|
||||
extra?: import('svelte').Snippet
|
||||
result_streams?: Record<string, string | undefined>
|
||||
@@ -57,9 +56,9 @@
|
||||
{/if}
|
||||
{:else if job.flow_status?.modules?.[job?.flow_status?.step]?.type === 'WaitingForEvents'}
|
||||
<FlowStatusWaitingForEvents {workspaceId} {job} {isOwner} />
|
||||
{:else if $suspendStatus && Object.keys($suspendStatus).length > 0}
|
||||
{:else if suspendStatus.val && Object.keys(suspendStatus.val).length > 0}
|
||||
<div class="flex gap-2 flex-col">
|
||||
{#each Object.values($suspendStatus) as suspendCount (suspendCount.job.id)}
|
||||
{#each Object.values(suspendStatus.val) as suspendCount (suspendCount.job.id)}
|
||||
<div>
|
||||
<div class="text-sm">
|
||||
Flow suspended, waiting for {suspendCount.nb} events
|
||||
@@ -74,7 +73,7 @@
|
||||
>
|
||||
<pre class="w-full">{job.logs}</pre>
|
||||
</div>
|
||||
{:else if innerModules?.length > 0}
|
||||
{:else if innerModules && innerModules?.length > 0}
|
||||
<div class="flex flex-col gap-1">
|
||||
{#each innerModules as mod, i (mod.id)}
|
||||
{#if mod.type == 'InProgress'}
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import FlowStatusViewerInner from './FlowStatusViewerInner.svelte'
|
||||
import type { FlowState } from './flows/flowState'
|
||||
import { createEventDispatcher, setContext, untrack } from 'svelte'
|
||||
import { setContext, untrack } from 'svelte'
|
||||
import type { DurationStatus, FlowStatusViewerContext, GraphModuleState } from './graph'
|
||||
import { isOwner as loadIsOwner } from '$lib/utils'
|
||||
import { isOwner as loadIsOwner, type StateStore } from '$lib/utils'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import type { Job } from '$lib/gen'
|
||||
import type { CompletedJob, Job } from '$lib/gen'
|
||||
|
||||
interface Props {
|
||||
jobId: string
|
||||
initialJob?: Job | undefined
|
||||
workspaceId?: string | undefined
|
||||
flowStateStore?: Writable<FlowState>
|
||||
flowStateStore?: FlowState
|
||||
selectedJobStep?: string | undefined
|
||||
hideFlowResult?: boolean
|
||||
hideTimeline?: boolean
|
||||
@@ -23,21 +22,24 @@
|
||||
rightColumnSelect?: 'timeline' | 'node_status' | 'node_definition' | 'user_states'
|
||||
isOwner?: boolean
|
||||
wideResults?: boolean
|
||||
localModuleStates?: Writable<Record<string, GraphModuleState>>
|
||||
localDurationStatuses?: Writable<Record<string, DurationStatus>>
|
||||
localModuleStates?: Record<string, GraphModuleState>
|
||||
localDurationStatuses?: Record<string, DurationStatus>
|
||||
job?: Job | undefined
|
||||
render?: boolean
|
||||
suspendStatus?: any
|
||||
suspendStatus?: StateStore<Record<string, { job: Job; nb: number }>>
|
||||
customUi?: {
|
||||
tagLabel?: string | undefined
|
||||
}
|
||||
onStart?: () => void
|
||||
onJobsLoaded?: ({ job, force }: { job: Job; force: boolean }) => void
|
||||
onDone?: ({ job }: { job: CompletedJob }) => void
|
||||
}
|
||||
|
||||
let {
|
||||
jobId,
|
||||
initialJob = undefined,
|
||||
workspaceId = undefined,
|
||||
flowStateStore = writable({}),
|
||||
flowStateStore = $bindable({}),
|
||||
selectedJobStep = $bindable(undefined),
|
||||
hideFlowResult = false,
|
||||
hideTimeline = false,
|
||||
@@ -48,17 +50,24 @@
|
||||
rightColumnSelect = $bindable('timeline'),
|
||||
isOwner = $bindable(false),
|
||||
wideResults = false,
|
||||
localModuleStates = $bindable(writable({})),
|
||||
localDurationStatuses = $bindable(writable({})),
|
||||
localModuleStates = $bindable({}),
|
||||
localDurationStatuses = $bindable({}),
|
||||
job = $bindable(undefined),
|
||||
render = true,
|
||||
suspendStatus = $bindable(writable({})),
|
||||
customUi
|
||||
suspendStatus = $bindable({ val: {} }),
|
||||
customUi,
|
||||
onStart,
|
||||
onJobsLoaded,
|
||||
onDone
|
||||
}: Props = $props()
|
||||
|
||||
let lastJobId: string = jobId
|
||||
|
||||
let retryStatus = writable({})
|
||||
let retryStatus = $state({ val: {} })
|
||||
let globalRefreshes: Record<string, ((clear, root) => Promise<void>)[]> = $state({})
|
||||
|
||||
let globalIterationBounds = $state({})
|
||||
|
||||
setContext<FlowStatusViewerContext>('FlowStatusViewer', {
|
||||
flowStateStore,
|
||||
suspendStatus,
|
||||
@@ -77,13 +86,13 @@
|
||||
async function updateJobId() {
|
||||
if (jobId !== lastJobId) {
|
||||
lastJobId = jobId
|
||||
$retryStatus = {}
|
||||
$suspendStatus = {}
|
||||
retryStatus.val = {}
|
||||
suspendStatus.val = {}
|
||||
globalRefreshes = {}
|
||||
globalIterationBounds = {}
|
||||
}
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let lastScriptPath: string | undefined = $state(undefined)
|
||||
|
||||
$effect.pre(() => {
|
||||
@@ -92,25 +101,33 @@
|
||||
jobId && updateJobId()
|
||||
})
|
||||
})
|
||||
|
||||
let refreshGlobal = async (moduleId: string, clear: boolean, root: string) => {
|
||||
let allFns = globalRefreshes?.[moduleId]?.map((x) => x(clear, root)) ?? []
|
||||
await Promise.all(allFns)
|
||||
}
|
||||
|
||||
let updateGlobalRefresh = (moduleId: string, updateFn: (clear, root) => Promise<void>) => {
|
||||
globalRefreshes[moduleId] = [...(globalRefreshes[moduleId] ?? []), updateFn]
|
||||
}
|
||||
</script>
|
||||
|
||||
<FlowStatusViewerInner
|
||||
{hideFlowResult}
|
||||
on:jobsLoaded={({ detail }) => {
|
||||
let { job } = detail
|
||||
onJobsLoaded={({ job, force }) => {
|
||||
if (job.script_path != lastScriptPath && job.script_path) {
|
||||
lastScriptPath = job.script_path
|
||||
loadOwner(lastScriptPath ?? '')
|
||||
}
|
||||
dispatch('jobsLoaded', job)
|
||||
onJobsLoaded?.({ job, force })
|
||||
}}
|
||||
globalModuleStates={[]}
|
||||
globalDurationStatuses={[]}
|
||||
{localModuleStates}
|
||||
{localDurationStatuses}
|
||||
{globalIterationBounds}
|
||||
bind:localModuleStates
|
||||
bind:selectedNode={selectedJobStep}
|
||||
on:start
|
||||
on:done
|
||||
bind:localDurationStatuses
|
||||
{onStart}
|
||||
{onDone}
|
||||
bind:job
|
||||
{initialJob}
|
||||
{jobId}
|
||||
@@ -122,4 +139,6 @@
|
||||
{customUi}
|
||||
graphTabOpen={true}
|
||||
isNodeSelected={true}
|
||||
{refreshGlobal}
|
||||
{updateGlobalRefresh}
|
||||
/>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,27 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { debounce, displayDate, msToSec } from '$lib/utils'
|
||||
import { debounce, displayDate, msToSec, readFieldsRecursively } from '$lib/utils'
|
||||
import { onDestroy, untrack } from 'svelte'
|
||||
import { getDbClockNow } from '$lib/forLater'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import TimelineBar from './TimelineBar.svelte'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import WaitTimeWarning from './common/waitTimeWarning/WaitTimeWarning.svelte'
|
||||
import type { GlobalIterationBounds } from './graph'
|
||||
|
||||
interface Props {
|
||||
selfWaitTime?: number | undefined
|
||||
aggregateWaitTime?: number | undefined
|
||||
flowModules: string[]
|
||||
durationStatuses: Writable<
|
||||
Record<
|
||||
string,
|
||||
{
|
||||
byJob: Record<string, { created_at?: number; started_at?: number; duration_ms?: number }>
|
||||
iteration_from?: number
|
||||
iteration_total?: number
|
||||
}
|
||||
>
|
||||
durationStatuses: Record<
|
||||
string,
|
||||
{
|
||||
byJob: Record<string, { created_at?: number; started_at?: number; duration_ms?: number }>
|
||||
}
|
||||
>
|
||||
flowDone?: boolean
|
||||
decreaseIterationFrom?: (key: string, amount: number) => void
|
||||
buildSubflowKey: (key: string) => string
|
||||
globalIterationBounds: Record<string, GlobalIterationBounds>
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -29,7 +28,10 @@
|
||||
aggregateWaitTime = undefined,
|
||||
flowModules,
|
||||
durationStatuses,
|
||||
flowDone = false
|
||||
flowDone = false,
|
||||
decreaseIterationFrom,
|
||||
buildSubflowKey,
|
||||
globalIterationBounds
|
||||
}: Props = $props()
|
||||
|
||||
let min: undefined | number = $state(undefined)
|
||||
@@ -43,15 +45,16 @@
|
||||
>
|
||||
| undefined = $state(undefined)
|
||||
|
||||
let { debounced, clearDebounce } = debounce(() => computeItems($durationStatuses), 30)
|
||||
let { debounced, clearDebounce } = debounce(() => computeItems(durationStatuses), 30)
|
||||
$effect(() => {
|
||||
flowDone != undefined && $durationStatuses && untrack(() => debounced())
|
||||
readFieldsRecursively(durationStatuses)
|
||||
flowDone != undefined && durationStatuses && untrack(() => debounced())
|
||||
})
|
||||
|
||||
export function reset() {
|
||||
min = undefined
|
||||
max = undefined
|
||||
items = computeItems($durationStatuses)
|
||||
items = computeItems(durationStatuses)
|
||||
}
|
||||
|
||||
function computeItems(
|
||||
@@ -172,20 +175,17 @@
|
||||
</div>
|
||||
{/if}
|
||||
{#each Object.values(flowModules) as k (k)}
|
||||
{@const iterationFrom = globalIterationBounds[buildSubflowKey(k)]?.iteration_from ?? 0}
|
||||
<div class="overflow-auto max-h-60 shadow-inner dark:shadow-gray-700 relative">
|
||||
{#if ($durationStatuses?.[k]?.iteration_from ?? 0) > 0}
|
||||
{#if iterationFrom > 0}
|
||||
<div class="w-full flex flex-row-reverse sticky top-0">
|
||||
<button
|
||||
class="!text-secondary underline mr-2 text-2xs text-right whitespace-nowrap"
|
||||
onclick={() => {
|
||||
let r = $durationStatuses[k]
|
||||
if (r.iteration_from) {
|
||||
r.iteration_from -= 20
|
||||
$durationStatuses = $durationStatuses
|
||||
}
|
||||
decreaseIterationFrom?.(k, 20)
|
||||
}}
|
||||
>Viewing iterations {$durationStatuses[k].iteration_from} to {$durationStatuses[k]
|
||||
.iteration_total}. Load more
|
||||
>Viewing iterations {iterationFrom} to {globalIterationBounds[buildSubflowKey(k)]
|
||||
?.iteration_total}. Load more
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script context="module" lang="ts">
|
||||
<script module lang="ts">
|
||||
import pLimit from 'p-limit'
|
||||
|
||||
const plimit = pLimit(5)
|
||||
@@ -11,7 +11,8 @@
|
||||
type FlowStatus,
|
||||
type Preview,
|
||||
type GetJobUpdatesResponse,
|
||||
type WorkflowStatus
|
||||
type WorkflowStatus,
|
||||
type OpenFlow
|
||||
} from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { onDestroy, tick, untrack } from 'svelte'
|
||||
@@ -44,6 +45,7 @@
|
||||
jobUpdateLastFetch?: Date | undefined
|
||||
toastError?: boolean
|
||||
onlyResult?: boolean
|
||||
loadPlaceholderJobOnStart?: Job
|
||||
// If you want to find out progress of subjobs of a flow, check job.flow_status.progress
|
||||
scriptProgress?: number | undefined
|
||||
|
||||
@@ -60,6 +62,7 @@
|
||||
jobUpdateLastFetch = $bindable(undefined),
|
||||
toastError = false,
|
||||
onlyResult = false,
|
||||
loadPlaceholderJobOnStart = undefined,
|
||||
scriptProgress = $bindable(undefined),
|
||||
noLogs = false,
|
||||
children
|
||||
@@ -226,6 +229,25 @@
|
||||
)
|
||||
}
|
||||
|
||||
export async function runFlowPreview(
|
||||
args: Record<string, any>,
|
||||
flow: OpenFlow & { tag?: string },
|
||||
callbacks?: Callbacks
|
||||
): Promise<string> {
|
||||
return abstractRun(
|
||||
() =>
|
||||
JobService.runFlowPreview({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
args,
|
||||
value: flow.value,
|
||||
tag: flow.tag
|
||||
}
|
||||
}),
|
||||
callbacks
|
||||
)
|
||||
}
|
||||
|
||||
function refreshLogOffset() {
|
||||
if (logOffset == 0) {
|
||||
logOffset = job?.logs?.length ? job.logs?.length + 1 : 0
|
||||
@@ -326,7 +348,11 @@
|
||||
syncIteration = 0
|
||||
errorIteration = 0
|
||||
currentId = testId
|
||||
job = undefined
|
||||
if (loadPlaceholderJobOnStart) {
|
||||
job = structuredClone(loadPlaceholderJobOnStart)
|
||||
} else {
|
||||
job = undefined
|
||||
}
|
||||
startedWatchingJob = Date.now()
|
||||
|
||||
// Clean up any existing SSE connection
|
||||
@@ -564,13 +590,15 @@
|
||||
if (isCurrentJob(id)) {
|
||||
try {
|
||||
// First load the job to get initial state
|
||||
if (!job && !onlyResult) {
|
||||
if ((!job || job.id == '') && !onlyResult) {
|
||||
job = await JobService.getJob({
|
||||
workspace: workspace!,
|
||||
id,
|
||||
noLogs: noLogs,
|
||||
noCode
|
||||
})
|
||||
|
||||
callbacks?.change?.(job)
|
||||
}
|
||||
|
||||
if (!onlyResult) {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { FoldVertical, UnfoldVertical } from 'lucide-svelte'
|
||||
|
||||
interface Props {
|
||||
showResultsInputs: boolean | undefined
|
||||
toggleExpandAll: () => void
|
||||
allExpanded: boolean | undefined
|
||||
}
|
||||
|
||||
let { showResultsInputs = $bindable(), toggleExpandAll, allExpanded }: Props = $props()
|
||||
</script>
|
||||
|
||||
<div class="flex justify-end gap-4 items-center p-2 bg-surface-secondary border-b">
|
||||
<div class="flex items-center gap-2 whitespace-nowrap">
|
||||
<label
|
||||
for="showResultsInputs"
|
||||
class="text-xs text-tertiary hover:text-primary transition-colors">Show inputs/results</label
|
||||
>
|
||||
<div class="flex-shrink-0">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="showResultsInputs"
|
||||
id="showResultsInputs"
|
||||
bind:checked={showResultsInputs}
|
||||
class="w-3 h-4 accent-primary -my-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onclick={toggleExpandAll}
|
||||
class="text-xs text-tertiary hover:text-primary transition-colors flex items-center gap-2 min-w-24 justify-end"
|
||||
>
|
||||
{allExpanded ? 'Collapse All' : 'Expand All'}
|
||||
{#if allExpanded}
|
||||
<FoldVertical size={16} />
|
||||
{:else}
|
||||
<UnfoldVertical size={16} />
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { Schema } from '$lib/common'
|
||||
|
||||
import { allTrue } from '$lib/utils'
|
||||
import { allTrue, sendUserToast } from '$lib/utils'
|
||||
import { RefreshCw } from 'lucide-svelte'
|
||||
import ArgInput from './ArgInput.svelte'
|
||||
import { Button } from './common'
|
||||
@@ -98,11 +98,11 @@
|
||||
|
||||
loadResourceTypes()
|
||||
|
||||
let args = $state(<Record<string, any>>{})
|
||||
|
||||
onMount(() => {
|
||||
testSteps?.updateStepArgs(mod.id, $flowStateStore, flowStore?.val, previewArgs?.val)
|
||||
args = testSteps?.getStepArgs(mod.id) ?? { value: {} }
|
||||
if (!testSteps) {
|
||||
sendUserToast('testSteps module not initialized. Preview will not work.', true)
|
||||
}
|
||||
testSteps?.updateStepArgs(mod.id, flowStateStore.val, flowStore?.val, previewArgs?.val)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -117,14 +117,17 @@
|
||||
)}
|
||||
data-arg={argName}
|
||||
>
|
||||
{#if typeof args.value == 'object' && schema?.properties?.[argName]}
|
||||
{#if schema?.properties?.[argName]}
|
||||
<ArgInput
|
||||
{resourceTypes}
|
||||
minW={false}
|
||||
autofocus={autofocus && !focusArg && i == 0}
|
||||
label={argName}
|
||||
description={schema.properties[argName].description}
|
||||
bind:value={args.value[argName]}
|
||||
bind:value={
|
||||
() => testSteps?.getStepInputArgs(mod.id, argName),
|
||||
(v) => testSteps?.setStepInputArgs(mod.id, argName, v)
|
||||
}
|
||||
type={schema.properties[argName].type}
|
||||
oneOf={schema.properties[argName].oneOf}
|
||||
required={schema?.required?.includes(argName)}
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
import ScriptFix from './copilot/ScriptFix.svelte'
|
||||
import type DiffEditor from './DiffEditor.svelte'
|
||||
import type Editor from './Editor.svelte'
|
||||
import type { Script, Job, FlowModule } from '$lib/gen'
|
||||
import { type Script, type Job, type FlowModule } from '$lib/gen'
|
||||
import OutputPickerInner from '$lib/components/flows/propPicker/OutputPickerInner.svelte'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import type { FlowEditorContext } from './flows/types'
|
||||
import { getContext } from 'svelte'
|
||||
import { getStringError } from './copilot/chat/utils'
|
||||
import AiAgentLogViewer from './AIAgentLogViewer.svelte'
|
||||
|
||||
interface Props {
|
||||
lang: Script['language']
|
||||
@@ -51,6 +52,12 @@
|
||||
let preview: 'mock' | 'job' | undefined = $state(undefined)
|
||||
let jobProgressReset: () => void = $state(() => {})
|
||||
|
||||
$effect(() => {
|
||||
if (preview != undefined && testJob) {
|
||||
preview = undefined
|
||||
}
|
||||
})
|
||||
|
||||
let forceJson = $state(false)
|
||||
|
||||
const logJob = $derived(testJob ?? selectedJob)
|
||||
@@ -102,6 +109,15 @@
|
||||
customEmptyMessage="Using pinned data"
|
||||
{tagLabel}
|
||||
/>
|
||||
{:else if mod.value.type === 'aiagent' && logJob?.type === 'CompletedJob'}
|
||||
<AiAgentLogViewer
|
||||
tools={mod.value.tools}
|
||||
agentJob={{
|
||||
...logJob,
|
||||
type: 'CompletedJob'
|
||||
}}
|
||||
workspaceId={logJob.workspace_id}
|
||||
/>
|
||||
{:else}
|
||||
<LogViewer
|
||||
small
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { ScriptService, type FlowModule, type Job } from '$lib/gen'
|
||||
import { ScriptService, type FlowModule, type JavascriptTransform, type Job } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { getScriptByPath } from '$lib/scripts'
|
||||
import { getContext } from 'svelte'
|
||||
import type { FlowEditorContext } from './flows/types'
|
||||
import JobLoader, { type Callbacks } from './JobLoader.svelte'
|
||||
import { getStepHistoryLoaderContext } from './stepHistoryLoader.svelte'
|
||||
import { loadSchemaFromModule } from './flows/flowInfers'
|
||||
|
||||
interface Props {
|
||||
mod: FlowModule
|
||||
@@ -31,18 +32,17 @@
|
||||
let stepHistoryLoader = getStepHistoryLoaderContext()
|
||||
|
||||
export function runTestWithStepArgs() {
|
||||
runTest(testSteps.getStepArgs(mod.id)?.value)
|
||||
runTest(testSteps.getStepArgs(mod.id))
|
||||
}
|
||||
|
||||
export function loadArgsAndRunTest() {
|
||||
testSteps?.updateStepArgs(mod.id, $flowStateStore, flowStore?.val, previewArgs?.val)
|
||||
runTest(testSteps.getStepArgs(mod.id)?.value)
|
||||
testSteps?.updateStepArgs(mod.id, flowStateStore.val, flowStore?.val, previewArgs?.val)
|
||||
runTest(testSteps.getStepArgs(mod.id))
|
||||
}
|
||||
|
||||
export async function runTest(args: any) {
|
||||
// Not defined if JobProgressBar not loaded
|
||||
if (jobProgressReset) jobProgressReset()
|
||||
|
||||
if (modulesTestStates.states[mod.id]) {
|
||||
modulesTestStates.states[mod.id].cancel = async () => {
|
||||
await jobLoader?.cancelJob()
|
||||
@@ -85,6 +85,39 @@
|
||||
)
|
||||
} else if (val.type == 'flow') {
|
||||
await jobLoader?.runFlowByPath(val.path, args, callbacks)
|
||||
} else if (val.type == 'aiagent') {
|
||||
const { schema } = await loadSchemaFromModule(mod)
|
||||
|
||||
const inputTransforms: { [key: string]: JavascriptTransform } = Object.fromEntries(
|
||||
Object.keys(args).map((key) => [
|
||||
key,
|
||||
{
|
||||
expr: `flow_input.${key}`,
|
||||
type: 'javascript'
|
||||
}
|
||||
])
|
||||
)
|
||||
|
||||
await jobLoader?.runFlowPreview(
|
||||
args,
|
||||
{
|
||||
value: {
|
||||
modules: [
|
||||
{
|
||||
...mod,
|
||||
value: {
|
||||
type: 'aiagent',
|
||||
tools: mod.value.type == 'aiagent' ? mod.value.tools : [],
|
||||
input_transforms: inputTransforms
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
summary: '',
|
||||
schema
|
||||
},
|
||||
callbacks
|
||||
)
|
||||
} else {
|
||||
throw Error('Not supported module type')
|
||||
}
|
||||
@@ -92,16 +125,19 @@
|
||||
|
||||
function jobDone(testJob: Job & { result?: any }) {
|
||||
if (testJob && !testJob.canceled && testJob.type == 'CompletedJob') {
|
||||
if ($flowStateStore[mod.id]) {
|
||||
$flowStateStore[mod.id].previewResult = testJob.result
|
||||
$flowStateStore[mod.id].previewSuccess = testJob.success
|
||||
$flowStateStore[mod.id].previewJobId = testJob.id
|
||||
$flowStateStore[mod.id].previewWorkspaceId = testJob.workspace_id
|
||||
$flowStateStore = $flowStateStore
|
||||
if (flowStateStore.val[mod.id]) {
|
||||
flowStateStore.val[mod.id] = {
|
||||
...flowStateStore.val[mod.id],
|
||||
previewResult: testJob.result,
|
||||
previewSuccess: testJob.success,
|
||||
previewJobId: testJob.id
|
||||
}
|
||||
}
|
||||
stepHistoryLoader?.resetInitial(mod.id)
|
||||
}
|
||||
modulesTestStates.states[mod.id].testJob = undefined
|
||||
if (modulesTestStates.states[mod.id]) {
|
||||
modulesTestStates.states[mod.id].testJob = testJob
|
||||
}
|
||||
}
|
||||
|
||||
export function cancelJob() {
|
||||
@@ -110,16 +146,16 @@
|
||||
|
||||
$effect(() => {
|
||||
// Update testIsLoading to read the state from parent components
|
||||
testIsLoading = modulesTestStates.states[mod.id]?.loading ?? false
|
||||
testIsLoading = modulesTestStates.states?.[mod.id]?.loading ?? false
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
// Update testJob to read the state from parent components
|
||||
testJob = modulesTestStates.states[mod.id]?.testJob
|
||||
testJob = modulesTestStates.states?.[mod.id]?.testJob
|
||||
})
|
||||
|
||||
modulesTestStates.states[mod.id] = {
|
||||
...(modulesTestStates.states[mod.id] ?? { loading: false }),
|
||||
...(modulesTestStates.states?.[mod.id] ?? { loading: false }),
|
||||
loading: testIsLoading,
|
||||
testJob: testJob
|
||||
}
|
||||
@@ -134,13 +170,29 @@
|
||||
() => modulesTestStates.states[mod.id]?.loading ?? false,
|
||||
(v) => {
|
||||
let newLoading = v ?? false
|
||||
if (modulesTestStates.states[mod.id]?.loading !== newLoading) {
|
||||
if (modulesTestStates.states && modulesTestStates.states?.[mod.id]?.loading !== newLoading) {
|
||||
modulesTestStates.states[mod.id] = {
|
||||
...(modulesTestStates.states[mod.id] ?? {}),
|
||||
loading: newLoading
|
||||
...(modulesTestStates.states?.[mod.id] ?? {}),
|
||||
loading: newLoading,
|
||||
hiddenInGraph: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
bind:job={modulesTestStates.states[mod.id].testJob}
|
||||
bind:job={
|
||||
() => modulesTestStates.states[mod.id]?.testJob,
|
||||
(v) => modulesTestStates.states[mod.id] && (modulesTestStates.states[mod.id].testJob = v)
|
||||
}
|
||||
loadPlaceholderJobOnStart={{
|
||||
type: 'QueuedJob',
|
||||
id: '',
|
||||
running: false,
|
||||
canceled: false,
|
||||
job_kind: 'preview',
|
||||
permissioned_as: '',
|
||||
is_flow_step: false,
|
||||
email: '',
|
||||
visible_to_owner: true,
|
||||
tag: ''
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { RELATIVE_LINE_NUMBERS_SETTING_NAME, relativeLineNumbers } from '$lib/stores'
|
||||
import { storeLocalSetting } from '$lib/utils'
|
||||
import Toggle from './Toggle.svelte'
|
||||
|
||||
function storeSetting() {
|
||||
storeLocalSetting(RELATIVE_LINE_NUMBERS_SETTING_NAME, $relativeLineNumbers.toString())
|
||||
}
|
||||
</script>
|
||||
|
||||
<Toggle
|
||||
size="xs"
|
||||
bind:checked={$relativeLineNumbers}
|
||||
on:change={() => {
|
||||
storeSetting()
|
||||
}}
|
||||
options={{ right: 'relative line numbers' }}
|
||||
/>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user