diff --git a/CHANGELOG.md b/CHANGELOG.md index e9e165ce7f..051d981f30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,23 @@ # Changelog +## [1.75.0](https://github.com/windmill-labs/windmill/compare/v1.74.2...v1.75.0) (2023-03-11) + + +### Features + +* add filter jobs by args or result ([3b44f9a](https://github.com/windmill-labs/windmill/commit/3b44f9a72ca0466a44963a4b9657a0ee59b44753)) +* **apps:** add resource picker ([8681e83](https://github.com/windmill-labs/windmill/commit/8681e83b574141acbf7e5a389a9e8a4f340336d1)) +* **bash:** add default argument handling for bash ([1d5c194](https://github.com/windmill-labs/windmill/commit/1d5c194f09ffba963d52e418c5954843d84ae337)) +* **frontend-apps:** add variable picker for static string input on apps ([bc440f8](https://github.com/windmill-labs/windmill/commit/bc440f8d4154ce464c0e027d93b7a0a3b76d782e)) +* **frontend:** make runs filters synced with query args ([61a5e1f](https://github.com/windmill-labs/windmill/commit/61a5e1f1accc988628b785b3b9be04c4ea719874)) + + +### Bug Fixes + +* **backend:** add killpill for lines reading ([7c825c2](https://github.com/windmill-labs/windmill/commit/7c825c212dd0f1e8be427eabd9a9756303241d1b)) +* **cli:** many small fixes ([ce32370](https://github.com/windmill-labs/windmill/commit/ce323709a94d27fb24214719180ea1aafc66d646)) + ## [1.74.2](https://github.com/windmill-labs/windmill/compare/v1.74.1...v1.74.2) (2023-03-09) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 5179ccf80a..56e53f66a6 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1123,9 +1123,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84" +checksum = "531ac96c6ff5fd7c62263c5e3c67a603af4fcaee2e1a0ae5565ba3a11e69e549" dependencies = [ "futures-channel", "futures-core", @@ -1138,9 +1138,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5" +checksum = "164713a5a0dcc3e7b4b1ed7d3b433cabc18025386f9339346e8daf15963cf7ac" dependencies = [ "futures-core", "futures-sink", @@ -1148,15 +1148,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608" +checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd" [[package]] name = "futures-executor" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e" +checksum = "1997dd9df74cdac935c76252744c1ed5794fac083242ea4fe77ef3ed60ba0f83" dependencies = [ "futures-core", "futures-task", @@ -1176,9 +1176,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531" +checksum = "89d422fa3cbe3b40dca574ab087abb5bc98258ea57eea3fd6f1fa7162c778b91" [[package]] name = "futures-lite" @@ -1197,9 +1197,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70" +checksum = "3eb14ed937631bd8b8b8977f2c198443447a8355b6e3ca599f38c975e5a963b6" dependencies = [ "proc-macro2", "quote", @@ -1208,21 +1208,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364" +checksum = "ec93083a4aecafb2a80a885c9de1f0ccae9dbd32c2bb54b0c3a65690e0b8d2f2" [[package]] name = "futures-task" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366" +checksum = "fd65540d33b37b16542a0438c12e6aeead10d4ac5d05bd3f805b8f35ab592879" [[package]] name = "futures-util" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1" +checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab" dependencies = [ "futures-channel", "futures-core", @@ -1503,9 +1503,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "hyper" -version = "0.14.24" +version = "0.14.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e011372fa0b68db8350aa7a248930ecc7839bf46d8485577d69f117a75f164c" +checksum = "cc5e554ff619822309ffd57d8734d77cd5ce6238bc956f037ea06c58238c9899" dependencies = [ "bytes", "futures-channel", @@ -2953,7 +2953,7 @@ dependencies = [ [[package]] name = "rustpython-ast" version = "0.2.0" -source = "git+https://github.com/RustPython/RustPython#87728c44527272ece739cddbb4942ad7e176dd79" +source = "git+https://github.com/RustPython/RustPython#415cdb1ef9b832fbca858a05f6894087e133dd31" dependencies = [ "num-bigint", "rustpython-compiler-core", @@ -2962,7 +2962,7 @@ dependencies = [ [[package]] name = "rustpython-compiler-core" version = "0.2.0" -source = "git+https://github.com/RustPython/RustPython#87728c44527272ece739cddbb4942ad7e176dd79" +source = "git+https://github.com/RustPython/RustPython#415cdb1ef9b832fbca858a05f6894087e133dd31" dependencies = [ "bitflags", "bstr", @@ -2975,7 +2975,7 @@ dependencies = [ [[package]] name = "rustpython-parser" version = "0.2.0" -source = "git+https://github.com/RustPython/RustPython#87728c44527272ece739cddbb4942ad7e176dd79" +source = "git+https://github.com/RustPython/RustPython#415cdb1ef9b832fbca858a05f6894087e133dd31" dependencies = [ "ahash", "anyhow", @@ -3667,9 +3667,9 @@ dependencies = [ [[package]] name = "swc_common" -version = "0.29.35" +version = "0.29.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d515be281f603cb97afaa89896aca5e5c748fde11c7f926e35cdaa8ff8da705" +checksum = "5005cd73617e18592faa31298225b26f1c407b84a681d67efb735c3d3458e101" dependencies = [ "ahash", "ast_node", @@ -4052,9 +4052,9 @@ checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" [[package]] name = "toml_edit" -version = "0.19.4" +version = "0.19.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a1eb0622d28f4b9c90adc4ea4b2b46b47663fde9ac5fafcb14a1369d5508825" +checksum = "7082a95d48029677a28f181e5f6422d0c8339ad8396a39d3f33d62a90c1f6c30" dependencies = [ "indexmap", "toml_datetime", @@ -4274,7 +4274,7 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "typify" version = "0.0.11-dev" -source = "git+https://github.com/oxidecomputer/typify#18e7faf1957812626e3bf459e20fbe16785de2d4" +source = "git+https://github.com/oxidecomputer/typify#d1cae0f7d5115782c9557a46396dae786ad5f592" dependencies = [ "typify-impl", "typify-macro", @@ -4283,7 +4283,7 @@ dependencies = [ [[package]] name = "typify-impl" version = "0.0.11-dev" -source = "git+https://github.com/oxidecomputer/typify#18e7faf1957812626e3bf459e20fbe16785de2d4" +source = "git+https://github.com/oxidecomputer/typify#d1cae0f7d5115782c9557a46396dae786ad5f592" dependencies = [ "heck", "log", @@ -4301,7 +4301,7 @@ dependencies = [ [[package]] name = "typify-macro" version = "0.0.11-dev" -source = "git+https://github.com/oxidecomputer/typify#18e7faf1957812626e3bf459e20fbe16785de2d4" +source = "git+https://github.com/oxidecomputer/typify#d1cae0f7d5115782c9557a46396dae786ad5f592" dependencies = [ "proc-macro2", "quote", @@ -4742,7 +4742,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windmill" -version = "1.74.2" +version = "1.75.0" dependencies = [ "anyhow", "axum", @@ -4769,7 +4769,7 @@ dependencies = [ [[package]] name = "windmill-api" -version = "1.74.2" +version = "1.75.0" dependencies = [ "anyhow", "argon2", @@ -4824,7 +4824,7 @@ dependencies = [ [[package]] name = "windmill-api-client" -version = "1.74.2" +version = "1.75.0" dependencies = [ "base64 0.21.0", "chrono", @@ -4839,7 +4839,7 @@ dependencies = [ [[package]] name = "windmill-audit" -version = "1.74.2" +version = "1.75.0" dependencies = [ "chrono", "serde", @@ -4852,7 +4852,7 @@ dependencies = [ [[package]] name = "windmill-common" -version = "1.74.2" +version = "1.75.0" dependencies = [ "anyhow", "axum", @@ -4877,7 +4877,7 @@ dependencies = [ [[package]] name = "windmill-parser" -version = "1.74.2" +version = "1.75.0" dependencies = [ "serde", "serde_json", @@ -4885,7 +4885,7 @@ dependencies = [ [[package]] name = "windmill-parser-bash" -version = "1.74.2" +version = "1.75.0" dependencies = [ "anyhow", "itertools", @@ -4900,7 +4900,7 @@ dependencies = [ [[package]] name = "windmill-parser-go" -version = "1.74.2" +version = "1.75.0" dependencies = [ "anyhow", "itertools", @@ -4912,7 +4912,7 @@ dependencies = [ [[package]] name = "windmill-parser-py" -version = "1.74.2" +version = "1.75.0" dependencies = [ "anyhow", "itertools", @@ -4927,7 +4927,7 @@ dependencies = [ [[package]] name = "windmill-parser-ts" -version = "1.74.2" +version = "1.75.0" dependencies = [ "anyhow", "deno_core", @@ -4941,7 +4941,7 @@ dependencies = [ [[package]] name = "windmill-queue" -version = "1.74.2" +version = "1.75.0" dependencies = [ "anyhow", "chrono", @@ -4964,7 +4964,7 @@ dependencies = [ [[package]] name = "windmill-worker" -version = "1.74.2" +version = "1.75.0" dependencies = [ "anyhow", "async-recursion", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index f34f03447b..4a085f16bf 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "windmill" -version = "1.74.2" +version = "1.75.0" authors.workspace = true edition.workspace = true @@ -19,7 +19,7 @@ members = [ ] [workspace.package] -version = "1.74.2" +version = "1.75.0" authors = ["Ruben Fiszel "] edition = "2021" diff --git a/backend/sqlx-data.json b/backend/sqlx-data.json index 8a839ffc02..a3dd13a9a0 100644 --- a/backend/sqlx-data.json +++ b/backend/sqlx-data.json @@ -2984,20 +2984,6 @@ }, "query": "UPDATE group_ SET summary = $1 WHERE name = $2 AND workspace_id = $3" }, - "83fc9bf32b9b40987feff61fea82e20613e74efb7bf8ba52485566667ccf2284": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Varchar", - "Text", - "Text" - ] - } - }, - "query": "UPDATE resource SET path = $1 WHERE path = $2 AND workspace_id = $3" - }, "8543f029d9784234e4c6a6dcd7b03e62d544b98be261334ee210594e0bb839f2": { "describe": { "columns": [ @@ -3168,6 +3154,21 @@ }, "query": "INSERT INTO usage (id, is_workspace, month_, usage) \n VALUES ($1, false, EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date), 0) \n ON CONFLICT (id, is_workspace, month_) DO UPDATE SET usage = usage.usage + 1 \n RETURNING usage.usage" }, + "8c0131a9cc61f2daa258d49767242bcaab6bb34a977ff7fb0c18aa9202d11f47": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Varchar", + "Jsonb", + "Text", + "Text" + ] + } + }, + "query": "UPDATE resource SET path = $1, value = $2 WHERE path = $3 AND workspace_id = $4" + }, "8c11511a74a41a65f448249a00ebe6964a61d00c2f7b4875a55e64741bf1f0ca": { "describe": { "columns": [ @@ -4697,6 +4698,27 @@ }, "query": "SELECT null FROM queue WHERE id = $1 FOR UPDATE" }, + "be1de9116b18a40681cb8c5ec7578fd7c5f7ce77f7af63f766c2899d85fe0bef": { + "describe": { + "columns": [ + { + "name": "value", + "ordinal": 0, + "type_info": "Jsonb" + } + ], + "nullable": [ + true + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + } + }, + "query": "SELECT value FROM resource WHERE path = $1 AND workspace_id = $2" + }, "be7a99a5bb6858323ca61dd51077010f51ba58ae76b9a413339255024dcb524d": { "describe": { "columns": [], diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 6f44eed791..9dee91477a 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.3" info: - version: 1.74.2 + version: 1.75.0 title: Windmill API contact: @@ -3168,12 +3168,14 @@ paths: - $ref: "#/components/parameters/ScriptExactPath" - $ref: "#/components/parameters/ScriptStartPath" - $ref: "#/components/parameters/ScriptExactHash" - - $ref: "#/components/parameters/CreatedBefore" - - $ref: "#/components/parameters/CreatedAfter" + - $ref: "#/components/parameters/StartedBefore" + - $ref: "#/components/parameters/StartedAfter" - $ref: "#/components/parameters/Success" - $ref: "#/components/parameters/JobKinds" - $ref: "#/components/parameters/Suspended" - $ref: "#/components/parameters/Running" + - $ref: "#/components/parameters/ArgsFilter" + - $ref: "#/components/parameters/ResultFilter" responses: "200": description: All available queued jobs @@ -3198,10 +3200,12 @@ paths: - $ref: "#/components/parameters/ScriptExactPath" - $ref: "#/components/parameters/ScriptStartPath" - $ref: "#/components/parameters/ScriptExactHash" - - $ref: "#/components/parameters/CreatedBefore" - - $ref: "#/components/parameters/CreatedAfter" + - $ref: "#/components/parameters/StartedBefore" + - $ref: "#/components/parameters/StartedAfter" - $ref: "#/components/parameters/Success" - $ref: "#/components/parameters/JobKinds" + - $ref: "#/components/parameters/ArgsFilter" + - $ref: "#/components/parameters/ResultFilter" - name: is_skipped description: is the job skipped in: query @@ -3235,9 +3239,11 @@ paths: - $ref: "#/components/parameters/ScriptExactPath" - $ref: "#/components/parameters/ScriptStartPath" - $ref: "#/components/parameters/ScriptExactHash" - - $ref: "#/components/parameters/CreatedBefore" - - $ref: "#/components/parameters/CreatedAfter" + - $ref: "#/components/parameters/StartedBefore" + - $ref: "#/components/parameters/StartedAfter" - $ref: "#/components/parameters/JobKinds" + - $ref: "#/components/parameters/ArgsFilter" + - $ref: "#/components/parameters/ResultFilter" - name: is_skipped description: is the job skipped in: query @@ -4614,15 +4620,15 @@ components: in: query schema: type: string - CreatedBefore: - name: created_before + StartedBefore: + name: started_before description: filter on created before (inclusive) timestamp in: query schema: type: string format: date-time - CreatedAfter: - name: created_after + StartedAfter: + name: started_after description: filter on created after (exclusive) timestamp in: query schema: @@ -4646,6 +4652,19 @@ components: in: query schema: type: boolean + ArgsFilter: + name: args + description: filter on jobs containing those args as a json subset (@> in postgres) + in: query + schema: + type: string + + ResultFilter: + name: result + description: filter on jobs containing those result as a json subset (@> in postgres) + in: query + schema: + type: string After: name: after description: filter on created after (exclusive) timestamp diff --git a/backend/windmill-api/src/folders.rs b/backend/windmill-api/src/folders.rs index cb10f45b8d..5115670e5a 100644 --- a/backend/windmill-api/src/folders.rs +++ b/backend/windmill-api/src/folders.rs @@ -16,10 +16,9 @@ use axum::{ routing::{delete, get, post}, Json, Router, }; -use itertools::Itertools; use windmill_audit::{audit_log, ActionKind}; use windmill_common::{ - error::{self, Error, JsonResult, Result}, + error::{self, to_anyhow, Error, JsonResult, Result}, users::username_to_permissioned_as, utils::{not_found_if_none, paginate, Pagination}, }; @@ -262,13 +261,26 @@ async fn update_folder( sqlb.and_where_eq("workspace_id", "?".bind(&w_id)); if let Some(display_name) = ng.display_name { - sqlb.set("display_name", display_name); + sqlb.set("display_name", "?".bind(&display_name)); } if let Some(owners) = ng.owners { - sqlb.set_str("owners", format!("{{{}}}", owners.into_iter().join(","))); + sqlb.set( + "owners", + "?".bind(&format!( + "{{{}}}", + owners + .iter() + .map(|x| format!("\"{x}\"")) + .collect::>() + .join(","), + )), + ); } if let Some(extra_perms) = ng.extra_perms { - sqlb.set_str("extra_perms", extra_perms.to_string()); + sqlb.set( + "extra_perms", + "?".bind(&serde_json::to_string(&extra_perms).map_err(to_anyhow)?), + ); } sqlb.returning("*"); diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index a34297fcf1..692a5696eb 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -327,13 +327,15 @@ pub struct ListQueueQuery { pub script_path_exact: Option, pub script_hash: Option, pub created_by: Option, - pub created_before: Option>, - pub created_after: Option>, + pub started_before: Option>, + pub started_after: Option>, pub running: Option, pub parent_job: Option, pub order_desc: Option, pub job_kinds: Option, pub suspended: Option, + // filter by matching a subset of the args using base64 encoded json subset + pub args: Option, } fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> SqlBuilder { @@ -362,11 +364,11 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq if let Some(pj) = &lq.parent_job { sqlb.and_where_eq("parent_job", "?".bind(pj)); } - if let Some(dt) = &lq.created_before { - sqlb.and_where_lt("created_at", format!("to_timestamp({})", dt.timestamp())); + if let Some(dt) = &lq.started_before { + sqlb.and_where_le("started_at", format!("to_timestamp({})", dt.timestamp())); } - if let Some(dt) = &lq.created_after { - sqlb.and_where_gt("created_at", format!("to_timestamp({})", dt.timestamp())); + if let Some(dt) = &lq.started_after { + sqlb.and_where_ge("started_at", format!("to_timestamp({})", dt.timestamp())); } if let Some(s) = &lq.suspended { @@ -376,6 +378,7 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq sqlb.and_where_eq("suspend", 0); } } + if let Some(jk) = &lq.job_kinds { sqlb.and_where_in( "job_kind", @@ -383,6 +386,10 @@ fn list_queue_jobs_query(w_id: &str, lq: &ListQueueQuery, fields: &[&str]) -> Sq ); } + if let Some(args) = &lq.args { + sqlb.and_where("args @> ?".bind(&args.replace("'", "''"))); + } + sqlb } @@ -455,13 +462,14 @@ async fn list_jobs( script_path_exact: lq.script_path_exact, script_hash: lq.script_hash, created_by: lq.created_by, - created_before: lq.created_before, - created_after: lq.created_after, + started_before: lq.started_before, + started_after: lq.started_after, running: None, parent_job: lq.parent_job, order_desc: Some(true), job_kinds: lq.job_kinds, suspended: lq.suspended, + args: lq.args, }, &[ "'QueuedJob' as typ", @@ -1136,15 +1144,15 @@ where struct InPayload { payload: Option, } - - fn decode_payload>(t: T) -> anyhow::Result { - let vec = base64::engine::general_purpose::URL_SAFE - .decode(t) - .context("invalid base64")?; - serde_json::from_slice(vec.as_slice()).context("invalid json") - } } } + +fn decode_payload>(t: T) -> anyhow::Result { + let vec = base64::engine::general_purpose::URL_SAFE + .decode(t) + .context("invalid base64")?; + serde_json::from_slice(vec.as_slice()).context("invalid json") +} pub async fn run_flow_by_path( authed: Authed, Extension(user_db): Extension, @@ -1687,11 +1695,11 @@ fn list_completed_jobs_query( if let Some(pj) = &lq.parent_job { sqlb.and_where_eq("parent_job", "?".bind(pj)); } - if let Some(dt) = &lq.created_before { - sqlb.and_where_lt("created_at", format!("to_timestamp({})", dt.timestamp())); + if let Some(dt) = &lq.started_before { + sqlb.and_where_le("started_at", format!("to_timestamp({})", dt.timestamp())); } - if let Some(dt) = &lq.created_after { - sqlb.and_where_gt("created_at", format!("to_timestamp({})", dt.timestamp())); + if let Some(dt) = &lq.started_after { + sqlb.and_where_ge("started_at", format!("to_timestamp({})", dt.timestamp())); } if let Some(sk) = &lq.is_skipped { sqlb.and_where_eq("is_skipped", sk); @@ -1706,6 +1714,15 @@ fn list_completed_jobs_query( ); } + if let Some(args) = &lq.args { + sqlb.and_where("args @> ?".bind(&args.replace("'", "''"))); + } + + if let Some(result) = &lq.result { + sqlb.and_where("result @> ?".bind(&result.replace("'", "''"))); + } + + tracing::info!("{:?}", sqlb.sql()); sqlb } #[derive(Deserialize, Clone)] @@ -1714,8 +1731,8 @@ pub struct ListCompletedQuery { pub script_path_exact: Option, pub script_hash: Option, pub created_by: Option, - pub created_before: Option>, - pub created_after: Option>, + pub started_before: Option>, + pub started_after: Option>, pub success: Option, pub parent_job: Option, pub order_desc: Option, @@ -1723,6 +1740,10 @@ pub struct ListCompletedQuery { pub is_skipped: Option, pub is_flow_step: Option, pub suspended: Option, + // filter by matching a subset of the args using base64 encoded json subset + pub args: Option, + // filter by matching a subset of the result using base64 encoded json subset + pub result: Option, } async fn list_completed_jobs( diff --git a/backend/windmill-api/src/variables.rs b/backend/windmill-api/src/variables.rs index 65655c8c39..599c7ccfba 100644 --- a/backend/windmill-api/src/variables.rs +++ b/backend/windmill-api/src/variables.rs @@ -26,6 +26,7 @@ use axum::{ Json, Router, }; use hyper::StatusCode; +use serde_json::Value; use windmill_audit::{audit_log, ActionKind}; use windmill_common::{ error::{Error, JsonResult, Result}, @@ -378,9 +379,26 @@ async fn update_variable( if !authed.is_admin { require_owner_of_path(&w_id, &authed.username, &authed.groups, &path, &db).await?; } + let mut v = sqlx::query_scalar!( + "SELECT value FROM resource WHERE path = $1 AND workspace_id = $2", + path, + w_id + ) + .fetch_one(&mut tx) + .await?; + + if let Some(old_v) = v { + v = Some(replace_path( + old_v, + &format!("$var:{path}"), + &format!("$var:{npath}"), + )) + } + sqlx::query!( - "UPDATE resource SET path = $1 WHERE path = $2 AND workspace_id = $3", + "UPDATE resource SET path = $1, value = $2 WHERE path = $3 AND workspace_id = $4", npath, + v, path, w_id ) @@ -419,6 +437,23 @@ async fn update_variable( Ok(format!("variable {} updated (npath: {:?})", path, npath)) } +fn replace_path(v: serde_json::Value, path: &str, npath: &str) -> Value { + match v { + Value::Object(v) => Value::Object( + v.into_iter() + .map(|(k, v)| (k, replace_path(v, path, npath))) + .collect(), + ), + Value::Array(arr) => Value::Array( + arr.into_iter() + .map(|v| replace_path(v, path, npath)) + .collect(), + ), + Value::String(s) if s == path => Value::String(npath.to_owned()), + _ => v, + } +} + pub async fn build_crypt<'c>( db: &mut Transaction<'c, Postgres>, w_id: &str, diff --git a/backend/windmill-api/src/workspaces.rs b/backend/windmill-api/src/workspaces.rs index dd2ee22f34..049ac59e63 100644 --- a/backend/windmill-api/src/workspaces.rs +++ b/backend/windmill-api/src/workspaces.rs @@ -1073,6 +1073,7 @@ struct ScriptMetadata { schema: Option, is_template: bool, lock: Vec, + kind: String, } enum ArchiveImpl { @@ -1235,6 +1236,7 @@ async fn tarball_workspace( description: script.description, schema: script.schema, is_template: script.is_template, + kind: script.kind.to_string(), lock, }; let metadata_str = serde_json::to_string_pretty(&metadata).unwrap(); diff --git a/backend/windmill-common/src/scripts.rs b/backend/windmill-common/src/scripts.rs index 5a4a556cd3..36b1466919 100644 --- a/backend/windmill-common/src/scripts.rs +++ b/backend/windmill-common/src/scripts.rs @@ -7,7 +7,7 @@ */ use std::{ - fmt::Display, + fmt::{self, Display}, hash::{Hash, Hasher}, }; @@ -103,6 +103,18 @@ pub enum ScriptKind { Approval, } +impl Display for ScriptKind { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + fmt.write_str(match self { + ScriptKind::Trigger => "trigger", + ScriptKind::Failure => "failure", + ScriptKind::Script => "script", + ScriptKind::Approval => "approval", + })?; + Ok(()) + } +} + #[derive(Serialize)] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] pub struct Script { diff --git a/cli/apps.ts b/cli/apps.ts index 46d0dd4456..1bbd1bafd5 100644 --- a/cli/apps.ts +++ b/cli/apps.ts @@ -28,14 +28,18 @@ export class AppFile implements Resource, PushDiffs { remotePath: string, diffs: Difference[], ): Promise { - if (await AppService.existsApp({ workspace, path: remotePath })) { + let app: AppWithLastVersion | undefined = undefined; + try { + app = await AppService.getAppByPath({ workspace, path: remotePath }); + } catch (e) {} + + if (app) { console.log( colors.bold.yellow( - `Applying ${diffs.length} diffs to existing app...`, + `Applying ${diffs.length} diffs to existing app... ${remotePath}`, ), ); const changeset: { - path?: string | undefined; summary?: string | undefined; value?: any; policy?: Policy | undefined; @@ -46,7 +50,7 @@ export class AppFile implements Resource, PushDiffs { ( diff.path[0] !== "value" && diff.path[0] !== "policy" && ( diff.path.length !== 1 || - !["path", "summary"].includes( + !["summary"].includes( diff.path[0] as string, ) ) @@ -61,6 +65,13 @@ export class AppFile implements Resource, PushDiffs { } } + if ((!changeset?.policy || JSON.stringify(changeset?.policy) == JSON.stringify(app.policy)) + && (!changeset?.value || JSON.stringify(changeset?.value) == JSON.stringify(app.value)) + && (!changeset?.summary || changeset.summary == app.summary)) { + console.log(colors.yellow(`No changes to push for app ${remotePath}, skipping`)) + return; + } + const hasChanges = Object.values(changeset).some((v) => v !== null && typeof v !== "undefined" ); @@ -87,19 +98,10 @@ export class AppFile implements Resource, PushDiffs { } } async push(workspace: string, remotePath: string): Promise { - let existing: AppWithLastVersion | undefined; - try { - existing = await AppService.getAppByPath({ - workspace: workspace, - path: remotePath, - }); - } catch { - existing = undefined; - } await this.pushDiffs( workspace, remotePath, - microdiff(existing ?? {}, this, { cyclesFix: false }), + microdiff({}, this, { cyclesFix: false }), ); } } diff --git a/cli/flow.ts b/cli/flow.ts index ca4a2661ec..eb455c0dda 100644 --- a/cli/flow.ts +++ b/cli/flow.ts @@ -94,6 +94,7 @@ export class FlowFile implements Resource, PushDiffs { ...changeset, ...base_changeset, } + await FlowService.updateFlow({ workspace: workspace, path: remotePath, @@ -114,20 +115,11 @@ export class FlowFile implements Resource, PushDiffs { } } async push(workspace: string, remotePath: string): Promise { - let remote: Flow | undefined; - try { - remote = await FlowService.getFlowByPath({ - workspace, - path: remotePath, - }); - } catch { - remote = undefined; - } await this.pushDiffs( workspace, remotePath, - microdiff(remote ?? {}, this, { cyclesFix: false }), + microdiff({}, this, { cyclesFix: false }), ); } } diff --git a/cli/folder.ts b/cli/folder.ts index ccaf6e2a01..548f950906 100644 --- a/cli/folder.ts +++ b/cli/folder.ts @@ -1,4 +1,4 @@ -import { colors, Command, Folder, FolderService, microdiff } from "./deps.ts"; +import { colors, Command, FolderService, microdiff } from "./deps.ts"; import { requireLogin, resolveWorkspace, validatePath } from "./context.ts"; import { Difference, @@ -22,7 +22,9 @@ export class FolderFile implements Resource, PushDiffs { owners: Array | undefined; @property(map(() => String, () => Boolean, { shape: MapShape.Object })) extra_perms: Map | undefined; - + @property(() => String) + display_name: string| undefined; + async push(workspace: string, remotePath: string): Promise { if (remotePath.startsWith("/")) { remotePath = remotePath.substring(1); @@ -31,16 +33,10 @@ export class FolderFile implements Resource, PushDiffs { remotePath = remotePath.substring(2); } - let existing: Folder | undefined; - try { - existing = await FolderService.getFolder({ workspace, name: remotePath }); - } catch { - existing = undefined; - } await this.pushDiffs( workspace, remotePath, - microdiff(existing ?? {}, this, { cyclesFix: false }), + microdiff({}, this, { cyclesFix: false }), ); } @@ -66,22 +62,24 @@ export class FolderFile implements Resource, PushDiffs { if (exists) { console.log( colors.bold.yellow( - `Applying ${diffs.length} diffs to existing folder...`, + `Applying ${diffs.length} diffs to existing folder... ${remotePath}`, ), ); const changeset: { owners?: string[] | undefined; extra_perms?: any; + display_name?: string | undefined; } = {}; for (const diff of diffs) { if ( diff.type !== "REMOVE" && ( diff.path.length !== 1 || - !["owners", "extra_perms"].includes(diff.path[0] as string) + !["owners", "extra_perms", "display_name"].includes(diff.path[0] as string) ) ) { + console.log(diff.path) throw new Error("Invalid folder diff with path " + diff.path); } if (diff.type === "CREATE" || diff.type === "CHANGE") { @@ -97,11 +95,10 @@ export class FolderFile implements Resource, PushDiffs { if (!hasChanges) { return; } - await FolderService.updateFolder({ workspace: workspace, name: remotePath, - requestBody: changeset, + requestBody: {...changeset, extra_perms: changeset.extra_perms ? Object.fromEntries(this.extra_perms?.entries() ?? []) : undefined} }); } else { console.log(colors.bold.yellow("Creating new folder: " + remotePath)); diff --git a/cli/main.ts b/cli/main.ts index cd92062ce1..024940b81c 100644 --- a/cli/main.ts +++ b/cli/main.ts @@ -13,7 +13,7 @@ import sync from "./sync.ts"; import { tryResolveVersion } from "./context.ts"; import { GlobalOptions } from "./types.ts"; -const VERSION = "v1.74.2"; +const VERSION = "v1.75.0"; let command: any = new Command() .name("wmill") diff --git a/cli/resource-type.ts b/cli/resource-type.ts index c0040c3bbc..031328cbd3 100644 --- a/cli/resource-type.ts +++ b/cli/resource-type.ts @@ -13,7 +13,6 @@ import { EditResourceType, microdiff, ResourceService, - ResourceType, Table, } from "./deps.ts"; import { Any, decoverto, model, property } from "./decoverto.ts"; @@ -26,19 +25,10 @@ export class ResourceTypeFile implements ResourceI, PushDiffs { description?: string; async push(workspace: string, remotePath: string): Promise { - let existing: ResourceType | undefined; - try { - existing = await ResourceService.getResourceType({ - workspace, - path: remotePath, - }); - } catch { - existing = undefined; - } - this.pushDiffs( + await this.pushDiffs( workspace, remotePath, - microdiff(existing ?? {}, this, { cyclesFix: false }), + microdiff({}, this, { cyclesFix: false }), ); } @@ -65,8 +55,8 @@ export class ResourceTypeFile implements ResourceI, PushDiffs { return; } console.log( - colors.yellow( - `Applying ${diffs.length} diffs to existing resource type...`, + colors.yellow.bold( + `Applying ${diffs.length} diffs to existing resource type... ${remotePath}`, ), ); const changeset: EditResourceType = {}; diff --git a/cli/resource.ts b/cli/resource.ts index c417a00ea2..c1165b034e 100644 --- a/cli/resource.ts +++ b/cli/resource.ts @@ -43,7 +43,7 @@ export class ResourceFile implements Resource2, PushDiffs { }) ) { console.log( - colors.yellow(`Applying ${diffs.length} diffs to existing resource...`), + colors.yellow.bold(`Applying ${diffs.length} diffs to existing resource... ${remotePath}`), ); const changeset: EditResource = { @@ -51,11 +51,7 @@ export class ResourceFile implements Resource2, PushDiffs { }; for (const diff of diffs) { if (diff.path[0] === "is_oauth") { - console.log( - colors.yellow( - "! is_oauth has been removed in newer versions. Ignoring.", - ), - ); + //is_oauth is not updatable continue; } if ( @@ -64,9 +60,10 @@ export class ResourceFile implements Resource2, PushDiffs { diff.path[0] !== "value" && ( diff.path.length !== 1 || diff.path[0] !== "description" - ) + ) && diff.path[0] !== "resource_type" ) ) { + console.log(colors.red("Invalid variable diff with path " + diff.path)); throw new Error("Invalid folder diff with path " + diff.path); } if (diff.type === "CREATE" || diff.type === "CHANGE") { @@ -110,19 +107,10 @@ export class ResourceFile implements Resource2, PushDiffs { } } async push(workspace: string, remotePath: string): Promise { - let existing: Resource | undefined; - try { - existing = await ResourceService.getResource({ - workspace, - path: remotePath, - }); - } catch { - existing = undefined; - } await this.pushDiffs( workspace, remotePath, - microdiff(existing ?? {}, this, { cyclesFix: false }), + microdiff({}, this, { cyclesFix: false }), ); } } diff --git a/cli/script.ts b/cli/script.ts index fc0bf22ac0..52da84b7b9 100644 --- a/cli/script.ts +++ b/cli/script.ts @@ -42,6 +42,7 @@ export class ScriptFile { }, toPlain: (data) => data, }) + @property(() => String) kind?: "script" | "failure" | "trigger" | "command" | "approval"; constructor(summary: string, description: string) { @@ -105,11 +106,21 @@ export async function handleFile(path: string, content: string, workspace: strin } catch { } const language = inferContentTypeFromFilePath(path); + let remote = undefined try { - const remote = await ScriptService.getScriptByPath({ + remote = await ScriptService.getScriptByPath({ workspace, path: remotePath, }); + } catch { } + + if (remote) { + if (typed.description === remote.description && content === remote.content && typed.summary === remote.summary && typed.is_template === remote.is_template && typed.kind == remote.kind && remote?.lock == typed.lock?.join('\n') && JSON.stringify(typed.schema) == JSON.stringify(remote.schema)) { + console.log(colors.yellow(`No change to push for script ${remotePath}, skipping`)) + return true + } + + await ScriptService.createScript({ workspace, requestBody: { @@ -123,10 +134,11 @@ export async function handleFile(path: string, content: string, workspace: strin lock: typed.lock, parent_hash: remote.hash, schema: typed.schema, - }, + } }); + console.log(colors.yellow.bold(`Creating script with a parent ${remotePath}`)) - } catch { + } else { // no parent hash await ScriptService.createScript({ workspace: workspace, @@ -144,7 +156,6 @@ export async function handleFile(path: string, content: string, workspace: strin }, }); console.log(colors.yellow.bold(`Creating script without parent ${remotePath}`)) - } return true } diff --git a/cli/sync.ts b/cli/sync.ts index b0c7335787..7cadb0ab52 100644 --- a/cli/sync.ts +++ b/cli/sync.ts @@ -33,8 +33,8 @@ import { ResourceFile } from "./resource.ts"; import { FlowFile } from "./flow.ts"; import { VariableFile } from "./variable.ts"; import { handleFile } from "./script.ts"; -import { equal } from "https://deno.land/x/equal/mod.ts"; -import { diffCharacters } from "https://deno.land/x/diff/mod.ts"; +import { equal } from "https://deno.land/x/equal@v1.5.0/mod.ts"; +import { diffCharacters } from "https://deno.land/x/diff@v0.3.5/mod.ts"; type DynFSElement = { isDirectory: boolean; path: string; @@ -179,13 +179,13 @@ async function elementsToMap(els: DynFSElement, ignore: (path: string, isDirecto return map; } async function compareDynFSElement( - els1: DynFSElement, els2: DynFSElement, + els1: DynFSElement, els2: DynFSElement | undefined, ignore: (path: string, isDirectory: boolean) => boolean, - raw: boolean ): Promise { - const [m1, m2] = raw ? [await elementsToMap(els1, ignore), {}] : - await Promise.all([elementsToMap(els1, ignore), elementsToMap(els2, ignore)]); + const [m1, m2] = els2 + ? await Promise.all([elementsToMap(els1, ignore), elementsToMap(els2, ignore)]) + : [await elementsToMap(els1, ignore), {}]; const changes: Change[] = []; @@ -266,8 +266,8 @@ async function pull( console.log(colors.gray("Computing the files to update locally to match remote (taking .wmillignore into account)")); const remote = ZipFSElement((await downloadZip(workspace))!) - const local = await FSFSElement(path.join(Deno.cwd(), opts.raw ? "" : ".wmill")) - const changes = await compareDynFSElement(remote, local, await ignoreF(), opts.raw) + const local = opts.raw ? undefined : await FSFSElement(path.join(Deno.cwd(), opts.raw ? "" : ".wmill")) + const changes = await compareDynFSElement(remote, local, await ignoreF()) console.log(`remote -> local: ${changes.length} changes to apply`); @@ -289,7 +289,7 @@ async function pull( try { const currentLocal = await Deno.readTextFile(target) - if (currentLocal !== change.before) { + if (currentLocal !== change.before && currentLocal !== change.after) { console.log(colors.red(`Conflict detected on ${change.path}\nBoth local and remote have been modified.`)) if (opts.failConflicts) { conflicts.push({ local: currentLocal, change, path: change.path }) @@ -459,7 +459,6 @@ function removeSuffix(str: string, suffix: string) { async function push(opts: GlobalOptions & { raw: boolean, yes: boolean, skipPull: boolean, failConflicts: boolean }) { - if (!opts.raw) { if (!opts.skipPull) { console.log(colors.gray("You need to be up-to-date before pushing, pulling first.")) @@ -474,9 +473,9 @@ async function push(opts: GlobalOptions & { raw: boolean, yes: boolean, skipPull console.log(colors.gray("Computing the files to update on the remote to match local (taking .wmillignore into account)")); - const remote = ZipFSElement((await downloadZip(workspace))!) + const remote = opts.raw ? undefined : ZipFSElement((await downloadZip(workspace))!) const local = await FSFSElement(path.join(Deno.cwd(), "")) - const changes = await compareDynFSElement(local, remote, await ignoreF(), opts.raw) + const changes = await compareDynFSElement(local, remote, await ignoreF()) console.log(`remote <- local: ${changes.length} changes to apply`); if (changes.length > 0) { @@ -575,7 +574,7 @@ async function push(opts: GlobalOptions & { raw: boolean, yes: boolean, skipPull break; } try { - Deno.remove(stateTarget) + await Deno.remove(stateTarget) } catch { } } } @@ -616,7 +615,7 @@ async function push(opts: GlobalOptions & { raw: boolean, yes: boolean, skipPull await file.pushDiffs(workspace, remotePath, diffs); } catch (e) { console.error("Failing to apply diffs to " + remotePath) - console.error(e.body) + console.error(JSON.stringify(e)) } } } @@ -635,7 +634,7 @@ const command = new Command() "Push any local changes and apply them remotely. Use --raw for usage without local state tracking.", ) .option("--fail-conflicts", "Error on conflicts (both remote and local have changes on the same item)") - .option("--skip-pull", "Push without pulling first") + .option("--skip-pull", "Push without pulling first (you have pulled prior)") .option("--yes", "Push without needing confirmation") .option("--raw", "Push without using state, just overwrite.") .action(push as any); diff --git a/cli/variable.ts b/cli/variable.ts index 6c6e001a44..3c1b4527ce 100644 --- a/cli/variable.ts +++ b/cli/variable.ts @@ -11,7 +11,6 @@ import { colors, Command, EditVariable, - ListableVariable, microdiff, Table, VariableService, @@ -67,7 +66,7 @@ export class VariableFile implements Resource, PushDiffs { if (await VariableService.existsVariable({ workspace, path: remotePath })) { console.log( colors.bold.yellow( - `Applying ${diffs.length} diffs to existing variable...`, + `Applying ${diffs.length} diffs to existing variable... ${remotePath}`, ), ); const changeset: EditVariable = {}; @@ -76,11 +75,12 @@ export class VariableFile implements Resource, PushDiffs { diff.type !== "REMOVE" && ( diff.path.length !== 1 || - !["path", "value", "is_secret", "description"].includes( + !["path", "value", "is_secret", "description", "account", "is_oauth"].includes( diff.path[0] as string, ) ) ) { + console.log(colors.red("Invalid variable diff with path " + diff.path)); throw new Error("Invalid variable diff with path " + diff.path); } if (diff.type === "CREATE" || diff.type === "CHANGE") { @@ -96,6 +96,7 @@ export class VariableFile implements Resource, PushDiffs { if (!hasChanges) { return; } + await VariableService.updateVariable({ workspace, path: remotePath, @@ -103,7 +104,6 @@ export class VariableFile implements Resource, PushDiffs { requestBody: changeset, }); - console.log(changeset); } else { console.log(colors.yellow.bold("Creating new variable...")); await VariableService.createVariable({ @@ -121,19 +121,10 @@ export class VariableFile implements Resource, PushDiffs { } } async push(workspace: string, remotePath: string): Promise { - let existing: ListableVariable | undefined; - try { - existing = await VariableService.getVariable({ - workspace: workspace, - path: remotePath, - }); - } catch { - existing = undefined; - } await this.pushDiffs( workspace, remotePath, - microdiff(existing ?? {}, this, { cyclesFix: false }), + microdiff({}, this, { cyclesFix: false }), ); } } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ebb0e97fd2..e444873ab4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "windmill", - "version": "1.74.2", + "version": "1.75.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "windmill", - "version": "1.74.2", + "version": "1.75.0", "dependencies": { "@fortawesome/free-brands-svg-icons": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1", diff --git a/frontend/package.json b/frontend/package.json index ac7544c7ef..9785c52395 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "windmill", - "version": "1.74.2", + "version": "1.75.0", "scripts": { "dev": "vite dev", "build": "vite build", diff --git a/frontend/src/lib/components/AppConnect.svelte b/frontend/src/lib/components/AppConnect.svelte index 77dd9c0414..28ac13d52f 100644 --- a/frontend/src/lib/components/AppConnect.svelte +++ b/frontend/src/lib/components/AppConnect.svelte @@ -81,7 +81,7 @@ $: key = apiTokenApps[resource_type]?.key ?? (args != undefined - ? Object.keys(args).filter((x) => ['token', 'password', 'api_key'].includes(x))[0] + ? Object.keys(args).filter((x) => ['token', 'password', 'api_key', 'key'].includes(x))[0] : undefined) let scopes: string[] = [] @@ -259,6 +259,7 @@ args['token'] == '' && args['password'] == '' && args['api_key'] == '' && + args['key'] == '' && key != undefined) || (step == 3 && pathError != '') || !isValid @@ -396,7 +397,7 @@ bind:error={pathError} bind:path initialPath="" - namePlaceholder="{resource_type}" + namePlaceholder={resource_type} kind="resource" /> @@ -433,7 +434,7 @@ {:else} ({ - x: job.created_at as any, + x: job.started_at as any, y: job.duration_ms, id: job.id, path: job.script_path @@ -57,7 +57,7 @@ label: 'Successful', data: success?.map((job) => ({ - x: job.created_at as any, + x: job.started_at as any, y: job.duration_ms, id: job.id, path: job.script_path @@ -71,7 +71,10 @@ enabled: true, modifierKey: 'ctrl' as 'ctrl', onPanComplete: ({ chart }) => { - dispatch('zoom', { min: new Date(chart.scales.x.min), max: new Date(chart.scales.x.max) }) + dispatch('zoom', { + min: addSeconds(new Date(chart.scales.x.min), -1), + max: addSeconds(new Date(chart.scales.x.max), 1) + }) } }, zoom: { @@ -80,7 +83,10 @@ }, mode: 'x' as 'x', onZoom: ({ chart }) => { - dispatch('zoom', { min: new Date(chart.scales.x.min), max: new Date(chart.scales.x.max) }) + dispatch('zoom', { + min: addSeconds(new Date(chart.scales.x.min), -1), + max: addSeconds(new Date(chart.scales.x.max), 1) + }) } } } @@ -88,6 +94,22 @@ function getPath(x: any): string { return x.path } + + $: minTime = getMinTime(jobs) + + function addSeconds(date: Date, seconds: number): Date { + date.setTime(date.getTime() + seconds * 1000) + return date + } + function getMinTime(jobs: CompletedJob[] | undefined): Date { + return addSeconds(new Date(jobs?.[jobs?.length - 1]?.started_at ?? new Date().toString()), -15) + } + + $: maxTime = getMaxTime(jobs) + + function getMaxTime(jobs: CompletedJob[] | undefined): Date { + return addSeconds(new Date(jobs?.[0]?.started_at ?? new Date().toString()), 15) + } {#if view} diff --git a/frontend/src/lib/components/apps/components/layout/AppTabs.svelte b/frontend/src/lib/components/apps/components/layout/AppTabs.svelte index e3ca52d390..7986c7bb54 100644 --- a/frontend/src/lib/components/apps/components/layout/AppTabs.svelte +++ b/frontend/src/lib/components/apps/components/layout/AppTabs.svelte @@ -1,6 +1,7 @@ @@ -486,12 +497,12 @@
- +
- +
@@ -508,36 +519,40 @@ {#if $focusedGrid !== undefined} - -
-
{`Sub grid: ${$focusedGrid.parentComponentId} (${$focusedGrid.subGridIndex})`}
- -
-
+ {/if}
@@ -587,7 +602,7 @@ variant="border" startIcon={{ icon: faBug }} > - + @@ -598,7 +613,7 @@ variant="border" startIcon={{ icon: faExternalLink }} > - + {#if appPath == ''} diff --git a/frontend/src/lib/components/apps/editor/GridEditor.svelte b/frontend/src/lib/components/apps/editor/GridEditor.svelte index f4380d70bd..c1e15567ce 100644 --- a/frontend/src/lib/components/apps/editor/GridEditor.svelte +++ b/frontend/src/lib/components/apps/editor/GridEditor.svelte @@ -218,8 +218,9 @@ }} on:expand={() => { push(history, $app) + $selectedComponent = gridComponent.data.id expandGriditem($app.grid, gridComponent, $breakpoint) - $app = { ...$app } + $app = $app }} />
diff --git a/frontend/src/lib/components/apps/editor/SubGridEditor.svelte b/frontend/src/lib/components/apps/editor/SubGridEditor.svelte index d4cfe0f5c2..8e38521068 100644 --- a/frontend/src/lib/components/apps/editor/SubGridEditor.svelte +++ b/frontend/src/lib/components/apps/editor/SubGridEditor.svelte @@ -136,11 +136,11 @@ if (!parentGridItem) { return } - + $selectedComponent = gridComponent.data.id push(history, $app) expandGriditem(subGrid, gridComponent, $breakpoint, parentGridItem) - $app = { ...$app } + $app = $app }} /> diff --git a/frontend/src/lib/components/apps/editor/appUtils.ts b/frontend/src/lib/components/apps/editor/appUtils.ts index 0dee608d98..1692a0553a 100644 --- a/frontend/src/lib/components/apps/editor/appUtils.ts +++ b/frontend/src/lib/components/apps/editor/appUtils.ts @@ -4,6 +4,7 @@ import { getRecommendedDimensionsByComponent, type AppComponent } from './compon import gridHelp from '@windmill-labs/svelte-grid/src/utils/helper' import { gridColumns } from '../gridUtils' import { allItems } from '../utils' +import type { Output, World } from '../rx' function findGridItemById( root: GridItem[], @@ -18,7 +19,7 @@ function findGridItemById( return undefined } -export function findGridItemParentId(app: App, id: string): string | undefined { +export function findGridItemParentGrid(app: App, id: string): string | undefined { const gridItem = app.grid.find((x) => x.id === id) if (gridItem) { return undefined @@ -27,7 +28,7 @@ export function findGridItemParentId(app: App, id: string): string | undefined { const subGrid = app.subgrids[key] const gridItem = subGrid.find((x) => x.id === id) if (gridItem) { - return key.split('-')[0] + return key } } } @@ -57,9 +58,6 @@ export function getNextGridItemId(app: App): string { } export function createNewGridItem(grid: GridItem[], id: string, data: AppComponent): GridItem { - const appComponent = data - - appComponent.id = id const newComponent = { resizable: true, @@ -68,7 +66,8 @@ export function createNewGridItem(grid: GridItem[], id: string, data: AppCompone y: 0 } - let newData: AppComponent = JSON.parse(JSON.stringify(appComponent)) + let newData: AppComponent = JSON.parse(JSON.stringify(data)) + newData.id = id const newItem: GridItem = { data: newData, @@ -76,7 +75,7 @@ export function createNewGridItem(grid: GridItem[], id: string, data: AppCompone } gridColumns.forEach((column) => { - const rec = getRecommendedDimensionsByComponent(appComponent.type, column) + const rec = getRecommendedDimensionsByComponent(newData.type, column) newItem[column] = { ...newComponent, @@ -95,6 +94,16 @@ export function createNewGridItem(grid: GridItem[], id: string, data: AppCompone return newItem } +export function getGridItems(app: App, focusedGrid: FocusedGrid | undefined): GridItem[] { + if (!focusedGrid) { + return app.grid + } else { + const { parentComponentId, subGridIndex } = focusedGrid + const key = `${parentComponentId}-${subGridIndex ?? 0}` + return app?.subgrids?.[key] ?? [] + } +} + export function insertNewGridItem( app: App, data: AppComponent, @@ -107,18 +116,7 @@ export function insertNewGridItem( app.subgrids = {} } - if (!focusedGrid) { - const newItem = createNewGridItem(app.grid, id, data) - app.grid.push(newItem) - } else { - const { parentComponentId, subGridIndex } = focusedGrid - const key = `${parentComponentId}-${subGridIndex ?? 0}` - - const subGrid = app.subgrids[key] ?? [] - subGrid.push(createNewGridItem(subGrid, id, data)) - app.subgrids[key] = subGrid - } // We only want to set subgrids when we are not moving if (!keepId) { for (let i = 0; i < (data.numberOfSubgrids ?? 0); i++) { @@ -126,6 +124,16 @@ export function insertNewGridItem( } } + + const key = focusedGrid ? `${focusedGrid?.parentComponentId}-${focusedGrid?.subGridIndex ?? 0}` : undefined + let grid = focusedGrid ? app.subgrids[key!] : app.grid + + const newItem = createNewGridItem(grid, id, data) + grid.push(newItem) + + if (focusedGrid) { + app.subgrids[key!] = grid + } return id } @@ -180,27 +188,7 @@ export function deleteGridItem( return components } -export function duplicateGridItem( - app: App, - parent: string | undefined, - id: string -): string | undefined { - const gridItem = findGridItem(app, id) - if (gridItem) { - const newId = getNextGridItemId(app) - const newItem = JSON.parse(JSON.stringify(gridItem)) - newItem.id = newId - newItem.data.id = newId - - let focusedGrid = parent - ? { parentComponentId: parent.split('-')[0], subGridIndex: Number(parent.split('-')[1]) } - : undefined - - return insertNewGridItem(app, newItem.data, focusedGrid) - } - return undefined -} type AvailableSpace = { left: number @@ -217,7 +205,7 @@ export function findAvailableSpace( ): AvailableSpace | undefined { if (gridItem) { const breakpoint = editorBreakpoint === 'sm' ? 3 : 12 - const maxHeight = parentGridItem ? parentGridItem[breakpoint].h - 1 : 12 + const maxHeight = parentGridItem ? parentGridItem[breakpoint].h - 1 : 16 const maxWidth = 12 const availableSpace = { @@ -308,6 +296,22 @@ function isOverlapping(item1: any, item2: any) { ) } +type Outputtable = { + -readonly [Property in keyof Type]: Output; +}; + + +export function initOutput>(world: World, id: string, init: I): Outputtable { + const output = world.outputsById[id] as Outputtable + if (init) { + for (const key in init) { + if (output && output[key] && output[key].peak() == undefined) { + output[key].set(init[key] as any) + } + } + } + return output +} export function expandGriditem( grid: GridItem[], gridComponent: GridItem, @@ -322,10 +326,10 @@ export function expandGriditem( const { left, right, top, bottom } = availableSpace const width = $breakpoint === 'sm' ? 3 : 12 - const previousGridItem = JSON.parse(JSON.stringify(gridComponent[width])) + const item = gridComponent[width] - gridComponent[width].x = previousGridItem.x - left - gridComponent[width].y = previousGridItem.y - top - gridComponent[width].w = previousGridItem.w + left + right - gridComponent[width].h = previousGridItem.h + top + bottom + item.x = item.x - left + item.y = item.y - top + item.w = item.w + left + right + item.h = item.h + top + bottom } diff --git a/frontend/src/lib/components/apps/editor/component/ComponentNavigation.svelte b/frontend/src/lib/components/apps/editor/component/ComponentNavigation.svelte index 641e2cc7d0..694e0c7c89 100644 --- a/frontend/src/lib/components/apps/editor/component/ComponentNavigation.svelte +++ b/frontend/src/lib/components/apps/editor/component/ComponentNavigation.svelte @@ -1,23 +1,35 @@ - -{#if component && !options.every((option) => option.disabled)} - - -{:else} -

No grids available

-{/if} diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/IconSelectInput.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/IconSelectInput.svelte index d8325508c1..017560c282 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/IconSelectInput.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/IconSelectInput.svelte @@ -73,7 +73,13 @@ {:else if filteredItems} - +
{#each filteredItems as { label, icon }} {@const formatedLabel = formatName(label)} diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/JsonEditor.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/JsonEditor.svelte index 74a24a86a0..081b5ff1cd 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/JsonEditor.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/JsonEditor.svelte @@ -1,7 +1,8 @@ diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/StaticInputEditor.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/StaticInputEditor.svelte index 73065d1970..711c5baebd 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/StaticInputEditor.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/StaticInputEditor.svelte @@ -42,7 +42,12 @@ {:else if componentInput.fieldType === 'labeledresource'} {#if componentInput?.value && typeof componentInput?.value == 'object' && 'label' in componentInput?.value}
- + { diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/mainInput/InlineScriptList.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/mainInput/InlineScriptList.svelte index 560dd6cee2..d928e1b014 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/mainInput/InlineScriptList.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/mainInput/InlineScriptList.svelte @@ -19,6 +19,7 @@
+ import { faClose } from '@fortawesome/free-solid-svg-icons' + import { createEventDispatcher } from 'svelte' + + import { Icon } from 'svelte-awesome' + + export let noBg = false + const dispatch = createEventDispatcher() + + + diff --git a/frontend/src/lib/components/common/drawer/DrawerContent.svelte b/frontend/src/lib/components/common/drawer/DrawerContent.svelte index cc5d907273..9728b26c94 100644 --- a/frontend/src/lib/components/common/drawer/DrawerContent.svelte +++ b/frontend/src/lib/components/common/drawer/DrawerContent.svelte @@ -3,6 +3,7 @@ import { faClose } from '@fortawesome/free-solid-svg-icons' import { createEventDispatcher } from 'svelte' import { Icon } from 'svelte-awesome' + import CloseButton from '../CloseButton.svelte' export let title: string | undefined = undefined export let overflow_y = true @@ -15,12 +16,7 @@
- + {title}
diff --git a/frontend/src/lib/components/common/table/AppRow.svelte b/frontend/src/lib/components/common/table/AppRow.svelte index 575ebdce0c..1cbf6de8c8 100644 --- a/frontend/src/lib/components/common/table/AppRow.svelte +++ b/frontend/src/lib/components/common/table/AppRow.svelte @@ -96,7 +96,8 @@ { displayName: 'Edit', icon: faPen, - href: `/apps/edit/${path}?nodraft=true` + href: `/apps/edit/${path}?nodraft=true`, + disabled: !canWrite }, { displayName: 'Use as template', diff --git a/frontend/src/lib/components/common/tabs/Tab.svelte b/frontend/src/lib/components/common/tabs/Tab.svelte index 39f95f9c30..24c4f62f39 100644 --- a/frontend/src/lib/components/common/tabs/Tab.svelte +++ b/frontend/src/lib/components/common/tabs/Tab.svelte @@ -35,6 +35,7 @@ )} style={`${style} ${$selected?.startsWith(value) ? selectedStyle : ''}`} on:click={() => update(value)} + on:pointerdown|stopPropagation >
diff --git a/frontend/src/lib/components/graph/FlowGraph.svelte b/frontend/src/lib/components/graph/FlowGraph.svelte index 7086d6349e..97042e3ae4 100644 --- a/frontend/src/lib/components/graph/FlowGraph.svelte +++ b/frontend/src/lib/components/graph/FlowGraph.svelte @@ -341,7 +341,7 @@ undefined, 1000, loopDepth, - 0, + modules.findIndex((m) => m.id == module.id) + 1, true, undefined, module.id diff --git a/frontend/src/lib/history.ts b/frontend/src/lib/history.ts index 983228d32c..703f8b5ef4 100644 --- a/frontend/src/lib/history.ts +++ b/frontend/src/lib/history.ts @@ -21,7 +21,13 @@ export function undo(history: History | undefined, now: T): T { }) } let nhistory = get(history) - return nhistory.history[nhistory.index] + let r = nhistory.history[nhistory.index] + if (!r) { + console.log('undo failed', nhistory, now) + return now + } else { + return r + } } else { return now } diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 28cf542b2f..4bbf5c87f0 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -3,7 +3,6 @@ import { goto } from '$app/navigation' import { AppService, FlowService, - FolderService, Script, ScriptService, UserService, @@ -234,6 +233,7 @@ export function canWrite( if (user.folders.findIndex((x) => path.startsWith('f/' + x)) != -1) { return true } + return false } @@ -297,17 +297,25 @@ export async function setQuery(url: URL, key: string, value: string): Promise { const nurl = new URL(url.toString()) - nurl.searchParams.set(key, value) + console.log(url.toString()) + for (const { key, value } of args) { + if (value) { + nurl.searchParams.set(key, value) + } else { + nurl.searchParams.delete(key) + } + } + try { history.replaceState(history.state, '', nurl.toString()) } catch (e) { console.error(e) } - }, 200) + }, bounceTime ?? 200) } export function groupBy( diff --git a/frontend/src/routes/(root)/(logged)/apps/get/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/apps/get/[...path]/+page.svelte index c5c2732054..6a5e51f4c8 100644 --- a/frontend/src/routes/(root)/(logged)/apps/get/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/apps/get/[...path]/+page.svelte @@ -3,17 +3,20 @@ import AppPreview from '$lib/components/apps/editor/AppPreview.svelte' import type { EditorBreakpoint } from '$lib/components/apps/types' - import { Skeleton } from '$lib/components/common' + import { Button, Skeleton } from '$lib/components/common' import { AppService, AppWithLastVersion } from '$lib/gen' import { userStore, workspaceStore } from '$lib/stores' - import { classNames } from '$lib/utils' + import { canWrite } from '$lib/utils' + import { faPen } from '@fortawesome/free-solid-svg-icons' import { writable } from 'svelte/store' import { twMerge } from 'tailwind-merge' let app: AppWithLastVersion | undefined = undefined + let can_write = false async function loadApp() { app = await AppService.getAppByPath({ workspace: $workspaceStore!, path: $page.params.path }) + can_write = canWrite(app?.path, app?.extra_perms!, $userStore) } let queryId = $page.url.searchParams.get('workspace_id') @@ -49,6 +52,13 @@ isEditor={false} noBackend={false} /> + {#if can_write} +
+ +
+ {/if}
{:else} diff --git a/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte index 18c21977d2..390d0d0711 100644 --- a/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/flows/get/[...path]/+page.svelte @@ -171,10 +171,9 @@ {#if flow}
-
-

- {defaultIfEmptyString(flow.summary, flow.path)} -

+
+

+ {defaultIfEmptyString(flow.summary, flow.path)} +

{#if !emptyString(flow.summary)} {flow.path} @@ -218,7 +226,7 @@ {/if} -
+
-
-
+
+ {#if !$userStore?.operator && can_write} +
+ +
+ {/if} +
-
- {#key path} - - {/key} - +
+
+
+
- -
+ {#if jobs} diff --git a/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte b/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte index 8e0a1995aa..d243258f40 100644 --- a/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/scripts/get/[...hash]/+page.svelte @@ -200,9 +200,9 @@
-
-

{defaultIfEmptyString(script.summary, script.path)}

- +
+
+

{defaultIfEmptyString(script.summary, script.path)}

+
{#if !emptyString(script.summary)} {script.path} {/if} -
+
Edited {displayDaysAgo(script.created_at || '')} by {script.created_by || 'unknown'} diff --git a/frontend/src/routes/(root)/(logged)/scripts/run/[...hash]/+page.svelte b/frontend/src/routes/(root)/(logged)/scripts/run/[...hash]/+page.svelte index 895382a859..f0188a12f5 100644 --- a/frontend/src/routes/(root)/(logged)/scripts/run/[...hash]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/scripts/run/[...hash]/+page.svelte @@ -17,7 +17,7 @@ import RunForm from '$lib/components/RunForm.svelte' import { Alert, Badge, Button, Kbd, Skeleton } from '$lib/components/common' import SharedBadge from '$lib/components/SharedBadge.svelte' - import { faPlay, faScroll } from '@fortawesome/free-solid-svg-icons' + import { faEye, faPen, faPlay, faScroll } from '@fortawesome/free-solid-svg-icons' $: hash = $page.params.hash let script: Script | undefined @@ -112,15 +112,25 @@
-
-
+
+ {#if !$userStore?.operator && can_write} +
+ +
+ {/if} +
View
diff --git a/lsp/Pipfile b/lsp/Pipfile index 479270ca97..6375fa9e71 100644 --- a/lsp/Pipfile +++ b/lsp/Pipfile @@ -4,8 +4,8 @@ verify_ssl = true name = "pypi" [packages] -wmill = ">=1.74.2" -wmill_pg = ">=1.74.2" +wmill = ">=1.75.0" +wmill_pg = ">=1.75.0" sendgrid = "*" mysql-connector-python = "*" pymongo = "*" diff --git a/openflow.openapi.yaml b/openflow.openapi.yaml index 5e6e97224e..b8b0b91dc7 100644 --- a/openflow.openapi.yaml +++ b/openflow.openapi.yaml @@ -1,7 +1,7 @@ openapi: "3.0.3" info: - version: 1.74.2 + version: 1.75.0 title: OpenFlow Spec contact: name: Ruben Fiszel diff --git a/python-client/wmill/pyproject.toml b/python-client/wmill/pyproject.toml index 5b76eeb0f8..aa0ef773b2 100644 --- a/python-client/wmill/pyproject.toml +++ b/python-client/wmill/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wmill" -version = "1.74.2" +version = "1.75.0" description = "A client library for accessing Windmill server wrapping the Windmill client API" license = "Apache-2.0" homepage = "https://windmill.dev" @@ -16,7 +16,7 @@ include = ["wmill/py.typed"] [tool.poetry.dependencies] python = "^3.7" -windmill-api = "^1.74.2" +windmill-api = "^1.75.0" [build-system] requires = ["poetry>=1.0.2", "poetry-dynamic-versioning"] diff --git a/python-client/wmill_pg/pyproject.toml b/python-client/wmill_pg/pyproject.toml index 2e42f34624..9ee15893fe 100644 --- a/python-client/wmill_pg/pyproject.toml +++ b/python-client/wmill_pg/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wmill-pg" -version = "1.74.2" +version = "1.75.0" description = "An extension client for the wmill client library focused on pg" license = "Apache-2.0" homepage = "https://windmill.dev" diff --git a/version.txt b/version.txt index a6e970de6f..7c7053aa23 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.74.2 +1.75.0