Compare commits

...
9 Commits
Author SHA1 Message Date
Ruben Fiszelandrubenfiszel 0789bef120 chore(main): release 1.75.0 (#1278)
* chore(main): release 1.75.0

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <rubenfiszel@users.noreply.github.com>
2023-03-11 20:54:54 +01:00
Ruben Fiszel 1a7dc0a3bd more cli fixes 2023-03-11 20:53:34 +01:00
Ruben Fiszel ce323709a9 fix(cli): many small fixes 2023-03-11 20:16:56 +01:00
Ruben Fiszel 61a5e1f1ac feat(frontend): make runs filters synced with query args 2023-03-11 17:13:49 +01:00
Ruben Fiszel 3b44f9a72c feat: add filter jobs by args or result 2023-03-11 15:30:46 +01:00
Ruben Fiszel b349308ff7 handle better linked variables renaming 2023-03-11 11:25:47 +01:00
Ruben Fiszel f87b722a21 apps improvements 2023-03-10 23:27:55 +01:00
Ruben Fiszel 0e9be7f300 fix for app viewer 2023-03-10 20:26:49 +01:00
Ruben Fiszel 8681e83b57 feat(apps): add resource picker 2023-03-10 20:01:00 +01:00
61 changed files with 727 additions and 350 deletions
+17
View File
@@ -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)
+42 -42
View File
@@ -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",
+2 -2
View File
@@ -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 <ruben@windmill.dev>"]
edition = "2021"
+36 -14
View File
@@ -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": [],
+30 -11
View File
@@ -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
+17 -5
View File
@@ -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::<Vec<_>>()
.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("*");
+42 -21
View File
@@ -327,13 +327,15 @@ pub struct ListQueueQuery {
pub script_path_exact: Option<String>,
pub script_hash: Option<String>,
pub created_by: Option<String>,
pub created_before: Option<chrono::DateTime<chrono::Utc>>,
pub created_after: Option<chrono::DateTime<chrono::Utc>>,
pub started_before: Option<chrono::DateTime<chrono::Utc>>,
pub started_after: Option<chrono::DateTime<chrono::Utc>>,
pub running: Option<bool>,
pub parent_job: Option<String>,
pub order_desc: Option<bool>,
pub job_kinds: Option<String>,
pub suspended: Option<bool>,
// filter by matching a subset of the args using base64 encoded json subset
pub args: Option<String>,
}
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<String>,
}
fn decode_payload<D: DeserializeOwned, T: AsRef<[u8]>>(t: T) -> anyhow::Result<D> {
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<D: DeserializeOwned, T: AsRef<[u8]>>(t: T) -> anyhow::Result<D> {
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<UserDB>,
@@ -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<String>,
pub script_hash: Option<String>,
pub created_by: Option<String>,
pub created_before: Option<chrono::DateTime<chrono::Utc>>,
pub created_after: Option<chrono::DateTime<chrono::Utc>>,
pub started_before: Option<chrono::DateTime<chrono::Utc>>,
pub started_after: Option<chrono::DateTime<chrono::Utc>>,
pub success: Option<bool>,
pub parent_job: Option<String>,
pub order_desc: Option<bool>,
@@ -1723,6 +1740,10 @@ pub struct ListCompletedQuery {
pub is_skipped: Option<bool>,
pub is_flow_step: Option<bool>,
pub suspended: Option<bool>,
// filter by matching a subset of the args using base64 encoded json subset
pub args: Option<String>,
// filter by matching a subset of the result using base64 encoded json subset
pub result: Option<String>,
}
async fn list_completed_jobs(
+36 -1
View File
@@ -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,
+2
View File
@@ -1073,6 +1073,7 @@ struct ScriptMetadata {
schema: Option<Schema>,
is_template: bool,
lock: Vec<String>,
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();
+13 -1
View File
@@ -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 {
+16 -14
View File
@@ -28,14 +28,18 @@ export class AppFile implements Resource, PushDiffs {
remotePath: string,
diffs: Difference[],
): Promise<void> {
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<void> {
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 }),
);
}
}
+2 -10
View File
@@ -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<void> {
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 }),
);
}
}
+10 -13
View File
@@ -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<string> | undefined;
@property(map(() => String, () => Boolean, { shape: MapShape.Object }))
extra_perms: Map<string, boolean> | undefined;
@property(() => String)
display_name: string| undefined;
async push(workspace: string, remotePath: string): Promise<void> {
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));
+1 -1
View File
@@ -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")
+4 -14
View File
@@ -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<void> {
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 = {};
+5 -17
View File
@@ -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<void> {
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 }),
);
}
}
+15 -4
View File
@@ -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
}
+14 -15
View File
@@ -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<Change[]> {
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);
+5 -14
View File
@@ -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<void> {
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 }),
);
}
}
+2 -2
View File
@@ -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",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "windmill",
"version": "1.74.2",
"version": "1.75.0",
"scripts": {
"dev": "vite dev",
"build": "vite build",
@@ -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}
<Path
initialPath=""
namePlaceholder="{resource_type}"
namePlaceholder={resource_type}
bind:error={pathError}
bind:path
kind="resource"
@@ -234,6 +234,14 @@
name,
requestBody: { username: member_name }
})
await GranularAclService.removeGranularAcls({
workspace: $workspaceStore ?? '',
path: name,
kind: 'group_',
requestBody: {
owner: 'u/' + member_name
}
})
loadGroup()
}}>remove</button
>
@@ -88,7 +88,7 @@
}}
items={collection}
class="text-clip grow min-w-0"
placeholder="{resourceType} resource"
placeholder="{resourceType ?? 'any'} resource"
inputStyles={SELECT_INPUT_DEFAULT_STYLE.inputStyles}
containerStyles={SELECT_INPUT_DEFAULT_STYLE.containerStyles}
/>
+28 -5
View File
@@ -45,7 +45,7 @@
label: 'Failed',
data:
failed?.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
@@ -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)
}
</script>
<Scatter
@@ -115,7 +137,8 @@
display: false
},
type: 'time',
min: jobs?.[jobs?.length - 1]?.created_at ?? new Date().toString()
min: minTime,
max: maxTime
},
y: {
grid: {
@@ -38,7 +38,7 @@
let summaryC: HTMLInputElement | undefined = undefined
let pathC: Path | undefined = undefined
$: setQueryWithoutLoad($page.url, 'state', encodeState(script))
$: setQueryWithoutLoad($page.url, [{ key: 'state', value: encodeState(script) }])
$: step = Number($page.url.searchParams.get('step')) || 1
if (script.content == '') {
@@ -2,13 +2,18 @@
import { ChevronDown, ChevronUp } from 'lucide-svelte'
import { slide } from 'svelte/transition'
import Button from './common/button/Button.svelte'
import Tooltip from './Tooltip.svelte'
export let text: string
export let tooltip: string | undefined = undefined
export let view = false
</script>
<Button color="light" on:click={() => (view = !view)} variant="border"
>{text}
{#if tooltip}
<Tooltip wrapperClass="mx-1">{tooltip}</Tooltip>
{/if}
{#if !view}<ChevronDown />{:else}<ChevronUp />{/if}</Button
>
{#if view}
@@ -43,6 +43,8 @@
: []
if (defaultValue) {
value = JSON.stringify(defaultValue)
} else if (listItems.length > 0) {
value = listItems[0]?.value
}
}
@@ -220,18 +220,23 @@
return
}
loading.save = true
await computeTriggerables()
await AppService.updateApp({
workspace: $workspaceStore!,
path: $page.params.path,
requestBody: {
value: $app!,
summary: $summary,
policy
}
})
loading.save = false
sendUserToast('App saved')
try {
await computeTriggerables()
await AppService.updateApp({
workspace: $workspaceStore!,
path: $page.params.path,
requestBody: {
value: $app!,
summary: $summary,
policy
}
})
sendUserToast('App saved')
loading.save = false
} catch (e) {
loading.save = false
throw e
}
}
let selectedJobId: string | undefined = undefined
@@ -471,8 +476,8 @@
<input type="text" placeholder="App summary" class="text-sm w-full" bind:value={$summary} />
</div>
<UndoRedo
undoProps={{ disabled: $history.index === 0 }}
redoProps={{ disabled: $history.index === $history.history.length - 1 }}
undoProps={{ disabled: $history?.index === 0 }}
redoProps={{ disabled: $history && $history?.index === $history.history.length - 1 }}
on:undo={() => {
$app = undo(history, $app)
}}
@@ -4,6 +4,7 @@
import { buildWorld, type World } from '../rx'
import type {
App,
AppEditorContext,
AppViewerContext,
ConnectingInput,
EditorBreakpoint,
@@ -64,6 +65,12 @@
parentWidth: writable(0)
})
setContext<AppEditorContext>('AppEditorContext', {
history: undefined,
componentControl: writable({}),
pickVariableCallback: writable(undefined)
})
let mounted = false
onMount(() => {
@@ -56,15 +56,15 @@
{/if}
</button>
<button
title="Position locking"
title="Expand"
class={classNames(
'text-gray-800 px-1 text-2xs py-0.5 font-bold w-fit shadow border border-gray-300 absolute -top-1 right-[0.5rem] z-50 cursor-pointer',
'text-gray-800 px-1 text-2xs py-0.5 font-bold w-fit shadow border border-gray-300 absolute -top-1 right-[4.5rem] z-50 cursor-pointer',
' hover:bg-gray-300',
selected ? 'bg-gray-200/80' : 'bg-gray-200/80'
)}
on:click={() => dispatch('expand')}
>
<Expand aria-label="Lock position" size={14} />
<Expand aria-label="Expand position" size={14} />
</button>
{/if}
@@ -73,7 +73,7 @@
title="Move"
on:mousedown|stopPropagation|capture
class={classNames(
'text-gray-600 px-1 text-2xs py-0.5 font-bold rounded-t-sm w-fit absolute border border-gray-300 -top-1 shadow right-[4.5rem] z-50 cursor-move',
'text-gray-600 px-1 text-2xs py-0.5 font-bold rounded-t-sm w-fit absolute border border-gray-300 -top-1 shadow right-[0.5rem] z-50 cursor-move',
'bg-gray-200/80'
)}
>
@@ -1,5 +1,5 @@
<script lang="ts">
import { getContext, afterUpdate } from 'svelte'
import { getContext, afterUpdate, setContext } from 'svelte'
import type { App, AppEditorContext, AppViewerContext, GridItem } from '../types'
import Grid from '@windmill-labs/svelte-grid'
import { classNames } from '$lib/utils'
@@ -11,7 +11,7 @@
import HiddenComponent from '../components/helpers/HiddenComponent.svelte'
import Component from './component/Component.svelte'
import { deepEqual } from 'fast-equals'
import { push } from '$lib/history'
import { push, type History } from '$lib/history'
import { expandGriditem, findGridItem } from './appUtils'
export let policy: Policy
@@ -108,13 +108,17 @@
let pointerdown = false
let lastapp: App | undefined = undefined
const onpointerdown = () => {
lastapp = JSON.parse(JSON.stringify($app))
pointerdown = true
if (history) {
lastapp = JSON.parse(JSON.stringify($app))
pointerdown = true
}
}
const onpointerup = () => {
pointerdown = false
if (!deepEqual(lastapp, $app)) {
push(history, lastapp, false, true)
if (history) {
pointerdown = false
if (!deepEqual(lastapp, $app)) {
push(history, lastapp, false, true)
}
}
}
@@ -222,7 +222,7 @@
recomputeIds={component.recomputeIds}
{render}
/>
{:else if component.type === 'selectcomponent'}
{:else if component.type === 'selectcomponent' || component.type === 'resourceselectcomponent'}
<AppSelect
id={component.id}
verticalAlignment={component.verticalAlignment}
@@ -74,7 +74,8 @@ export type AggridComponent = BaseComponent<'aggridcomponent'>
export type DisplayComponent = BaseComponent<'displaycomponent'>
export type ImageComponent = BaseComponent<'imagecomponent'>
export type InputComponent = BaseComponent<'inputcomponent'>
export type SelectComponent = BaseComponent<'selectcomponent'>
export type SelectComponent = BaseComponent<'resourceselectcomponent'>
export type ResourceSelectComponent = BaseComponent<'selectcomponent'>
export type MultiSelectComponent = BaseComponent<'multiselectcomponent'>
export type CheckboxComponent = BaseComponent<'checkboxcomponent'>
export type RadioComponent = BaseComponent<'radiocomponent'>
@@ -116,6 +117,7 @@ export type AppComponent = BaseAppComponent &
| PieChartComponent
| ScatterChartComponent
| SelectComponent
| ResourceSelectComponent
| MultiSelectComponent
| CheckboxComponent
| FormComponent
@@ -911,7 +913,7 @@ Hello \${ctx.username}
}
},
multiselectcomponent: {
name: 'MultiSelect',
name: 'Multi Select',
icon: List,
dims: '2:1-3:1',
data: {
@@ -940,6 +942,36 @@ Hello \${ctx.username}
softWrap: true
}
},
resourceselectcomponent: {
name: 'Resource Select',
icon: List,
dims: '2:1-3:1',
data: {
verticalAlignment: 'center',
id: '',
type: 'resourceselectcomponent',
componentInput: undefined,
configuration: {
items: {
type: 'static',
fieldType: 'array',
subFieldType: 'labeledresource',
value: []
},
placeholder: {
type: 'static',
fieldType: 'text',
value: 'Select an item',
onlyStatic: true
}
},
customCss: {
input: { style: '' }
} as const,
card: false,
softWrap: true
}
},
numberinputcomponent: {
name: 'Number',
icon: Binary,
@@ -32,6 +32,7 @@ const inputs: ComponentSet = {
'fileinputcomponent',
'checkboxcomponent',
'selectcomponent',
'resourceselectcomponent',
'multiselectcomponent',
]
} as const
@@ -23,7 +23,13 @@
<label class="block pb-2">
<div class="text-xs font-medium pb-0.5"> Style </div>
<div class="relative">
<input type="text" class="!pr-7" bind:value={value.style} on:focus />
<input
on:keydown|stopPropagation
type="text"
class="!pr-7"
bind:value={value.style}
on:focus
/>
{#if value?.style}
<button
transition:fade|local={{ duration: 100 }}
@@ -41,7 +47,13 @@
<label class="block">
<div class="text-xs font-medium pb-0.5"> Tailwind classes </div>
<div class="relative">
<input type="text" class="!pr-7" bind:value={value.class} on:focus />
<input
on:keydown|stopPropagation
type="text"
class="!pr-7"
bind:value={value.class}
on:focus
/>
{#if value?.class}
<button
transition:fade|local={{ duration: 100 }}
@@ -82,7 +82,12 @@
<div class="h-full flex flex-col gap-1">
<div class="flex justify-between w-full gap-2 px-2 pt-1 flex-row items-center">
{#if name !== undefined}
<input bind:value={name} placeholder="Inline script name" class="!text-xs !rounded-xs" />
<input
on:keydown|stopPropagation
bind:value={name}
placeholder="Inline script name"
class="!text-xs !rounded-xs"
/>
{/if}
<div class="flex w-full flex-row gap-2 items-center justify-end">
{#if validCode}
@@ -22,6 +22,8 @@
componentInput.value.push(0)
} else if (componentInput.subFieldType === 'object') {
componentInput.value.push({})
} else if (componentInput.subFieldType === 'labeledresource') {
componentInput.value.push({ value: '', label: '' })
} else if (
componentInput.subFieldType === 'text' ||
componentInput.subFieldType === 'textarea' ||
@@ -56,7 +56,7 @@
<div class="w-full flex gap-2 flex-col mt-2">
{#each panes as value, index (index)}
<div class="w-full flex flex-row gap-2 items-center relative">
<input type="number" bind:value />
<input on:keydown|stopPropagation type="number" bind:value />
<div class="absolute top-1 right-1">
<Button
@@ -54,7 +54,7 @@
<div class="w-full flex gap-2 flex-col mt-2">
{#each tabs as value, index (index)}
<div class="w-full flex flex-row gap-2 items-center relative">
<input type="text" bind:value />
<input on:keydown|stopPropagation type="text" bind:value />
<div class="absolute top-1 right-1">
<Button
@@ -73,7 +73,13 @@
<Loader2 class="animate-spin" size={18} />
</div>
{:else if filteredItems}
<input bind:value={search} type="text" placeholder="Search" class="col-span-4 mb-2" />
<input
on:keydown|stopPropagation
bind:value={search}
type="text"
placeholder="Search"
class="col-span-4 mb-2"
/>
<div class="grid gap-1 grid-cols-4 max-h-[300px] overflow-auto">
{#each filteredItems as { label, icon }}
{@const formatedLabel = formatName(label)}
@@ -1,7 +1,8 @@
<script lang="ts">
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
export let code: string
export let value: any
export let value: any = undefined
export let error = ''
function parseJson() {
try {
@@ -11,7 +12,6 @@
error = e.message
}
}
let error = ''
$: code && parseJson()
</script>
@@ -39,6 +39,29 @@
</select>
{:else if componentInput.fieldType === 'icon-select'}
<IconSelectInput bind:componentInput />
{:else if componentInput.fieldType === 'labeledresource'}
{#if componentInput?.value && typeof componentInput?.value == 'object' && 'label' in componentInput?.value}
<div class="flex flex-col gap-1 w-full">
<input
on:keydown|stopPropagation
placeholder="Label"
type="text"
bind:value={componentInput.value['label']}
/>
<ResourcePicker
initialValue={componentInput.value?.['value']?.split('$res:')[1] || ''}
on:change={(e) => {
let path = e.detail
if (componentInput && path) {
componentInput.value['value'] = `$res:${path}`
}
}}
/>
</div>
{:else}
Inconsistent labeled resource object
{/if}
{:else if componentInput.fieldType === 'color'}
<ColorInput bind:componentInput />
{:else if componentInput.fieldType === 'object'}
@@ -19,6 +19,7 @@
<div class="w-full flex mt-1 items-center gap-2">
<slot />
<input
on:keydown|stopPropagation
type="text"
placeholder="Search inline scripts"
bind:value={filter}
+11 -7
View File
@@ -17,6 +17,7 @@ export type InputType =
| 'object'
| 'array'
| 'any'
| 'labeledresource'
// Connection to an output of another component
// defined by the id of the component and the path of the output
@@ -139,11 +140,11 @@ export type AppInput =
| AppInputSpec<'object', Record<string | number, any>>
| AppInputSpec<'object', string>
| (AppInputSpec<'select', string> & {
/**
* One of the keys of `staticValues` from `lib/components/apps/editor/componentsPanel/componentStaticValues`
*/
optionValuesKey: keyof typeof staticValues
})
/**
* One of the keys of `staticValues` from `lib/components/apps/editor/componentsPanel/componentStaticValues`
*/
optionValuesKey: keyof typeof staticValues
})
| (AppInputSpec<'select', string> & StaticOptions)
| AppInputSpec<'icon-select', string>
| AppInputSpec<'color', string>
@@ -156,9 +157,12 @@ export type AppInput =
| AppInputSpec<'array', string[], 'datetime'>
| AppInputSpec<'array', object[], 'object'>
| (AppInputSpec<'array', string[], 'select'> & {
optionValuesKey: keyof typeof staticValues
})
optionValuesKey: keyof typeof staticValues
})
| (AppInputSpec<'array', string[], 'select'> & StaticOptions)
| AppInputSpec<'array', object[], 'labeledresource'>
| AppInputSpec<'labeledresource', object>
export type RowAppInput = Extract<AppInput, { type: 'row' }>
export type StaticAppInput = Extract<AppInput, { type: 'static' }>
+1 -1
View File
@@ -137,7 +137,7 @@ export type AppViewerContext = {
}
export type AppEditorContext = {
history: History<App>
history: History<App> | undefined
componentControl: Writable<Record<string, { left?: () => boolean; right?: () => boolean }>>
pickVariableCallback: Writable<((path: string) => void) | undefined>
}
@@ -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',
+31 -22
View File
@@ -7,34 +7,43 @@ export function initHistory<T>(initial: T): History<T> {
return writable({ history: [initial], index: 0 })
}
export function undo<T>(history: History<T>, now: T): T {
let chistory = get(history)
if (chistory.index == 0) return now
if (!deepEqual(chistory.history[chistory.index], now)) {
push(history, now, true)
export function undo<T>(history: History<T> | undefined, now: T): T {
if (history) {
let chistory = get(history)
if (chistory.index == 0) return now
if (!deepEqual(chistory.history[chistory.index], now)) {
push(history, now, true)
} else {
history.update((history) => {
history.index--
return history
})
}
let nhistory = get(history)
return nhistory.history[nhistory.index]
} else {
history.update((history) => {
history.index--
return now
}
}
export function redo<T>(history: History<T> | undefined): T {
if (history) {
history?.update((history) => {
if (history.index < history.history.length - 1) {
history.index++
}
return history
})
let nhistory = get(history)
return nhistory.history[nhistory.index]
} else {
return undefined as T
}
let nhistory = get(history)
return nhistory.history[nhistory.index]
}
export function redo<T>(history: History<T>): T {
history.update((history) => {
if (history.index < history.history.length - 1) {
history.index++
}
return history
})
let nhistory = get(history)
return nhistory.history[nhistory.index]
}
export function push<T>(history: History<T>, value: T, noSetIndex: boolean = false, skipCopy: boolean = false) {
history.update((history) => {
export function push<T>(history: History<T> | undefined, value: T, noSetIndex: boolean = false, skipCopy: boolean = false) {
history?.update((history) => {
history.history = JSON.parse(JSON.stringify(history.history.slice(0, history.index + 1)))
const toPush = skipCopy ? value : JSON.parse(JSON.stringify(value))
history.history.push(toPush)
+12 -4
View File
@@ -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<vo
}
let debounced: NodeJS.Timeout | undefined = undefined
export function setQueryWithoutLoad(url: URL, key: string, value: string): void {
export function setQueryWithoutLoad(url: URL, args: { key: string, value: string | null | undefined }[], bounceTime?: number): void {
debounced && clearTimeout(debounced)
debounced = setTimeout(() => {
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<T>(
@@ -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}
<div class="absolute bottom-4 z-20 right-4">
<Button size="sm" startIcon={{ icon: faPen }} variant="border" href="/apps/edit/{app.path}"
>Edit</Button
>
</div>
{/if}
</div>
{:else}
<Skeleton layout={[10]} />
@@ -171,10 +171,9 @@
<CenteredPage>
{#if flow}
<div class="prose-sm mx-auto mt-6">
<div class="flex flex-row w-full justify-between item-center">
<h1 class="mb-1 truncate">
{defaultIfEmptyString(flow.summary, flow.path)}
</h1>
<div
class="flex flex-row-reverse w-full flex-wrap md:flex-nowrap justify-between gap-x-2 gap-y-4"
>
<div class="flex flex-row-reverse gap-2 h-full">
<Button
href="/flows/run/{path}"
@@ -198,7 +197,7 @@
</Button>
<Button
href="/flows/add?template={flow.path}"
variant="contained"
variant="border"
color="blue"
size="md"
startIcon={{ icon: faCodeFork }}
@@ -206,10 +205,19 @@
Fork
</Button>
{/if}
<Button href="/runs/{flow.path}" color="blue" size="md" startIcon={{ icon: faList }}>
<Button
href="/runs/{flow.path}"
variant="border"
color="blue"
size="md"
startIcon={{ icon: faList }}
>
View runs
</Button>
</div>
<h1 class="mb-1 truncate grow">
{defaultIfEmptyString(flow.summary, flow.path)}
</h1>
</div>
{#if !emptyString(flow.summary)}
<span class="text-lg font-semibold">{flow.path}</span>
@@ -218,7 +226,7 @@
{/if}
<ShareModal bind:this={shareModal} />
<div class="grid grid-cols-1 gap-6 max-w-7xl pb-6 mt-2">
<div class="grid grid-cols-1 gap-6 max-w-7xl pb-6">
<Skeleton
loading={!flow}
layout={[[{ h: 1.5, w: 40 }], 1, [4], 2.25, [{ h: 1.5, w: 30 }], 1, [10]]}
@@ -13,7 +13,7 @@
import CenteredPage from '$lib/components/CenteredPage.svelte'
import RunForm from '$lib/components/RunForm.svelte'
import { Button, Kbd, Skeleton } from '$lib/components/common'
import { faEye, faPlay, faScroll } from '@fortawesome/free-solid-svg-icons'
import { faEye, faPen, faPlay } from '@fortawesome/free-solid-svg-icons'
import SharedBadge from '$lib/components/SharedBadge.svelte'
const path = $page.params.path
@@ -87,9 +87,21 @@
<div
class="flex flex-row-reverse w-full flex-wrap md:flex-nowrap justify-between gap-x-1"
>
<div class="flex flex-row">
<div>
<div class="flex flex-row gap-4">
{#if !$userStore?.operator && can_write}
<div>
<Button
size="sm"
startIcon={{ icon: faPen }}
disabled={flow == undefined}
variant="border"
href="/flows/edit/{flow?.path}">Edit</Button
>
</div>
{/if}
<div class="md:pr-4">
<Button
size="sm"
startIcon={{ icon: faEye }}
disabled={flow == undefined}
btnClasses="mr-4"
@@ -281,7 +281,7 @@
><Badge color="gray">{truncateHash(job.script_hash)}</Badge></a
>
{/if}
{#if job && 'job_kind' in job}<Badge color="blue">{job.job_kind}</Badge>
{#if job && 'job_kind' in job}<Badge baseClass="ml-2" color="blue">{job.job_kind}</Badge>
{/if}
{#if !job.visible_to_owner}<Badge color="red"
>only visible to you <Tooltip
@@ -1,7 +1,7 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte'
import { JobService, Job, CompletedJob, ScriptService, FlowService } from '$lib/gen'
import { setQuery } from '$lib/utils'
import { setQuery, setQueryWithoutLoad } from '$lib/utils'
import { page } from '$app/stores'
import { sendUserToast } from '$lib/utils'
@@ -18,11 +18,12 @@
import Icon from 'svelte-awesome'
import AutoComplete from 'simple-svelte-autocomplete'
import NoItemFound from '$lib/components/home/NoItemFound.svelte'
import Slider from '$lib/components/Slider.svelte'
import JsonEditor from '$lib/components/apps/editor/settingsPanel/inputEditor/JsonEditor.svelte'
let jobs: Job[] | undefined
let error: Error | undefined
let intervalId: NodeJS.Timer | undefined
let createdBefore: string | undefined = $page.url.searchParams.get('createdBefore') ?? undefined
let success: boolean | undefined =
$page.url.searchParams.get('success') != undefined
@@ -33,6 +34,11 @@
? $page.url.searchParams.get('is_skipped') == 'true'
: false
let argFilter: any = $page.url.searchParams.get('arg') ?? undefined
let resultFilter: any = $page.url.searchParams.get('result') ?? undefined
let minTs = $page.url.searchParams.get('min_ts') ?? undefined
let maxTs = $page.url.searchParams.get('max_ts') ?? undefined
let nbOfJobs = 30
$: path = $page.params.path
@@ -53,27 +59,44 @@
}
}
$: ($workspaceStore && loadJobs(createdBefore)) || (path && success && isSkipped && jobKinds)
$: ($workspaceStore && loadJobs()) || (path && success && isSkipped && jobKinds)
let filterTimeout: NodeJS.Timeout | undefined = undefined
function debounceSyncer() {
filterTimeout && clearTimeout(filterTimeout)
filterTimeout = setTimeout(() => {
loadJobs()
}, 1000)
}
$: (true || argFilter || resultFilter) && debounceSyncer()
async function fetchJobs(
createdBefore: string | undefined,
createdAfter: string | undefined
startedBefore: string | undefined,
startedAfter: string | undefined
): Promise<Job[]> {
return JobService.listJobs({
workspace: $workspaceStore!,
createdBefore,
createdAfter,
startedBefore,
startedAfter,
scriptPathExact: path === '' ? undefined : path,
jobKinds,
success,
isSkipped,
isFlowStep: jobKindsCat != 'all' ? false : undefined
isFlowStep: jobKindsCat != 'all' ? false : undefined,
args:
argFilter && argFilter != '{}' && argFilter != '' && argError == '' ? argFilter : undefined,
result:
resultFilter && resultFilter != '{}' && resultFilter != '' && resultError == ''
? resultFilter
: undefined
})
}
async function loadJobs(createdBefore: string | undefined): Promise<void> {
async function loadJobs(): Promise<void> {
jobs = undefined
try {
const newJobs = await fetchJobs(createdBefore, undefined)
const newJobs = await fetchJobs(maxTs, minTs)
jobs = newJobs
} catch (err) {
sendUserToast(`There was a problem fetching jobs: ${err}`, true)
@@ -89,7 +112,7 @@
}
async function syncer() {
if (jobs && createdBefore === undefined) {
if (sync && jobs && maxTs == undefined) {
const reversedJobs = [...jobs].reverse()
const lastIndex = reversedJobs.findIndex((x) => x.type == Job.type.QUEUED_JOB) - 1
let ts = lastIndex >= 0 ? reversedJobs[lastIndex].created_at : undefined
@@ -110,9 +133,17 @@
}
}
let sync = true
onMount(() => {
loadPaths()
intervalId = setInterval(syncer, 5000)
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
sync = false
} else {
sync = true
}
})
})
let paths: string[] = []
@@ -122,11 +153,23 @@
const npaths_flows = await FlowService.listFlowPaths({ workspace: $workspaceStore ?? '' })
paths = npaths_scripts.concat(npaths_flows).sort()
}
async function syncCatWithURL() {
await setQuery($page.url, 'job_kinds', jobKindsCat)
function syncWithUrl(arg: string, value: string) {
setQueryWithoutLoad($page.url, [{ key: arg, value }])
}
$: jobKindsCat && syncCatWithURL()
async function syncTsWithURL(minTs?: string, maxTs?: string) {
console.log(minTs, maxTs)
setQueryWithoutLoad($page.url, [
{ key: 'min_ts', value: minTs },
{ key: 'max_ts', value: maxTs }
])
}
$: syncWithUrl('job_kinds', jobKindsCat)
$: syncWithUrl('arg', argFilter)
$: syncWithUrl('result', resultFilter)
$: syncTsWithURL(minTs, maxTs)
$: completedJobs =
jobs?.filter((x) => x.type == 'CompletedJob').map((x) => x as CompletedJob) ?? []
@@ -138,14 +181,15 @@
})
let searchPath = ''
$: searchPath = path
let minTs = undefined
let maxTs = undefined
$: searchPath && onSearchPathChange()
function onSearchPathChange() {
goto(`/runs/${searchPath}?${$page.url.searchParams.toString()}`)
}
let argError = ''
let resultError = ''
</script>
<CenteredPage>
@@ -210,28 +254,49 @@
<input type="text" value={maxTs ?? 'zoom x axis to set max'} disabled />
</div>
</div>
<div class="flex flex-row gap-x-2 mb-2 w-full">
{#key path}
<AutoComplete
items={paths}
value={path}
bind:selectedItem={searchPath}
placeholder="Search by path of script/flow"
/>
{/key}
<Button
variant="border"
on:click={() => {
minTs = undefined
maxTs = undefined
goto('/runs?' + $page.url.searchParams.toString())
fetchJobs(createdBefore, undefined)
}}
size="xs"
<div class="grid grid-cols-1 md:grid-cols-3 gap-2 mb-2 w-full flex-wrap">
<div>
<div class="flex flex-row gap-x-2">
{#key path}
<AutoComplete
items={paths}
value={path}
bind:selectedItem={searchPath}
placeholder="Search by path"
/>
{/key}
<Button
title="Clear path and time filters"
variant="border"
on:click={async () => {
minTs = undefined
maxTs = undefined
jobs = undefined
await goto('/runs?' + $page.url.searchParams.toString())
loadJobs()
}}
size="xs"
>
<Icon data={faSearchMinus} />
</Button>
</div>
</div>
<div
><Slider
text="Filter by args {argFilter ? '(set)' : ''}"
tooltip={'Filter by a json being a subset of the args. Try \'{"foo": "bar"}\''}
><JsonEditor bind:error={argError} bind:code={argFilter} /></Slider
></div
>
<div
><Slider
text="Filter by result {resultFilter ? '(set)' : ''}"
tooltip={'Filter by a json being a subset of the result. Try \'{"foo": "bar"}\''}
><JsonEditor bind:error={resultError} bind:code={resultFilter} /></Slider
></div
>
<Icon data={faSearchMinus} />
</Button>
</div>
<Skeleton loading={!jobs} layout={[[6], 1, [6], 1, [6], 1, [6], 1, [6]]} />
{#if jobs}
@@ -200,9 +200,9 @@
<Skeleton {loading} layout={[[{ h: 1.5, w: 40 }], 1, [{ h: 1, w: 30 }]]} />
<div class="prose-sm mx-auto mt-6">
<div class="flex flex-row w-full justify-between item-center">
<h1 class="mb-1 truncate">{defaultIfEmptyString(script.summary, script.path)}</h1>
<div
class="flex flex-row-reverse w-full flex-wrap md:flex-nowrap justify-between gap-x-2 gap-y-4"
>
<div class="flex flex-row-reverse gap-2 h-full">
<Button
href={`/scripts/run/${script.hash}`}
@@ -243,13 +243,16 @@
View Runs
</Button>
</div>
<div class="grow truncate">
<h1 class="mb-1 truncate">{defaultIfEmptyString(script.summary, script.path)}</h1>
</div>
</div>
{#if !emptyString(script.summary)}
<span class="text-lg font-semibold">{script.path}</span>
{/if}
<div class="flex flex-row gap-x-2 flex-wrap items-center mt-4">
<div class="flex flex-row gap-x-2 flex-wrap items-center mt-2">
<span class="text-sm text-gray-600">
Edited {displayDaysAgo(script.created_at || '')} by {script.created_by || 'unknown'}
</span>
@@ -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 @@
<div
class="flex flex-row-reverse w-full flex-wrap md:flex-nowrap justify-between gap-x-1 gap-y-2"
>
<div class="flex flex-row">
<div>
<div class="flex flex-row gap-4">
{#if !$userStore?.operator && can_write}
<div>
<Button
size="sm"
startIcon={{ icon: faPen }}
disabled={script == undefined}
variant="border"
href="/scripts/edit/{script?.hash}?step=2">Edit</Button
>
</div>
{/if}
<div class="md:pr-4">
<Button
startIcon={{ icon: faScroll }}
size="sm"
startIcon={{ icon: faEye }}
disabled={script == undefined}
btnClasses="mr-4"
variant="border"
href="/scripts/get/{script?.hash}?workspace_id={$workspaceStore}"
>View script</Button
href="/scripts/get/{script?.hash}?workspace_id={$workspaceStore}">View</Button
>
</div>
<div>
+2 -2
View File
@@ -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 = "*"
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.74.2
version: 1.75.0
title: OpenFlow Spec
contact:
name: Ruben Fiszel
+2 -2
View File
@@ -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"]
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -1 +1 @@
1.74.2
1.75.0