Merge remote-tracking branch 'origin/main' into di/data-tables

This commit is contained in:
Diego Imbert
2025-11-14 17:48:46 +01:00
92 changed files with 3439 additions and 2398 deletions
+30
View File
@@ -1,5 +1,35 @@
# Changelog
## [1.575.4](https://github.com/windmill-labs/windmill/compare/v1.575.3...v1.575.4) (2025-11-13)
### Bug Fixes
* fix cancel job selection ([d4340b8](https://github.com/windmill-labs/windmill/commit/d4340b83ac94a9dd9febdcfa11288f35c0a57799))
## [1.575.3](https://github.com/windmill-labs/windmill/compare/v1.575.2...v1.575.3) (2025-11-13)
### Bug Fixes
* fix cancel job selection ([fbab86c](https://github.com/windmill-labs/windmill/commit/fbab86cc43dae17727a3aa99abc43a57c6b59b57))
## [1.575.2](https://github.com/windmill-labs/windmill/compare/v1.575.1...v1.575.2) (2025-11-13)
### Bug Fixes
* fix cancel job selection ([1c0f80d](https://github.com/windmill-labs/windmill/commit/1c0f80dc5594146b0ec18caf3b49c036695ce759))
## [1.575.1](https://github.com/windmill-labs/windmill/compare/v1.575.0...v1.575.1) (2025-11-13)
### Bug Fixes
* Fix flow buttons clipped in Safari ([#7126](https://github.com/windmill-labs/windmill/issues/7126)) ([77316cb](https://github.com/windmill-labs/windmill/commit/77316cbd0e3ee81fccee9680541f9b2346c8a209))
* handle string in input transform strings ([2ee17ae](https://github.com/windmill-labs/windmill/commit/2ee17aea8b29d99c5d2bb609b2becad354835de7))
* send alternatives on timeout toast ([#6920](https://github.com/windmill-labs/windmill/issues/6920)) ([6fac896](https://github.com/windmill-labs/windmill/commit/6fac8963909fb40726d85f5e897398d2a691edb0))
## [1.575.0](https://github.com/windmill-labs/windmill/compare/v1.574.3...v1.575.0) (2025-11-12)
@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT j.id AS \"id!\" FROM v2_job j WHERE j.id = ANY($1) AND j.trigger_kind != 'schedule'::job_trigger_kind AND ($2::text[] IS NULL OR j.tag = ANY($2))",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id!",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"UuidArray",
"TextArray"
]
},
"nullable": [
false
]
},
"hash": "24d302b8215d49a289bedd14a5791e9366d1f6d3d2aa485e0f50c6f2d85693dd"
}
@@ -15,7 +15,7 @@
]
},
"nullable": [
null
true
]
},
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT j.id AS \"id!\" FROM v2_job j LEFT JOIN v2_job_queue q USING (id) WHERE j.id = ANY($1) AND j.trigger_kind IS DISTINCT FROM 'schedule'::job_trigger_kind AND ($2::text[] IS NULL OR j.tag = ANY($2))",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id!",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"UuidArray",
"TextArray"
]
},
"nullable": [
false
]
},
"hash": "78f5684086a8c28a602c583389e974fffd7f74b10618b1467426623973c10131"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT \n path \n FROM \n flow_version \n WHERE \n id = $1 AND \n workspace_id = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Int8",
"Text"
]
},
"nullable": [
false
]
},
"hash": "7cab8475996f0abd7ca7888b460bd3faf3bef6e34200c258e3704981817c241e"
}
@@ -0,0 +1,66 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n flow_version.id AS version,\n flow_version.value->>'early_return' as early_return, \n flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor, \n (flow_version.value->>'chat_input_enabled')::boolean as chat_input_enabled, \n flow.tag, \n flow.dedicated_worker, \n flow.on_behalf_of_email, \n flow.edited_by\n FROM \n flow_version\n INNER JOIN flow\n ON flow.path = flow_version.path AND\n flow.workspace_id = flow_version.workspace_id\n WHERE \n flow_version.workspace_id = $1 AND\n flow_version.path = $2 AND\n flow_version.id = $3\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "version",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "early_return",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "has_preprocessor",
"type_info": "Bool"
},
{
"ordinal": 3,
"name": "chat_input_enabled",
"type_info": "Bool"
},
{
"ordinal": 4,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "dedicated_worker",
"type_info": "Bool"
},
{
"ordinal": 6,
"name": "on_behalf_of_email",
"type_info": "Text"
},
{
"ordinal": 7,
"name": "edited_by",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Int8"
]
},
"nullable": [
false,
null,
null,
null,
true,
true,
true,
false
]
},
"hash": "a7468e9054beed88636786c5495ac3b9d9a6086ae6212ad9237b39a0346d7d26"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT \n path \n FROM \n flow_version \n WHERE \n id = $1 AND \n workspace_id = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Int8",
"Text"
]
},
"nullable": [
false
]
},
"hash": "b82f97f2a858e20b1fb6d9b0b3eab5bab2bafa91689eac5073c1678c338afb27"
}
@@ -1,66 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT tag, dedicated_worker, flow_version.value->>'early_return' as early_return, flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor, (flow_version.value->>'chat_input_enabled')::boolean as chat_input_enabled, on_behalf_of_email, edited_by, flow_version.id AS version\n FROM flow\n INNER JOIN flow_version\n ON flow_version.id = $3\n WHERE flow.path = $1 and flow.workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "dedicated_worker",
"type_info": "Bool"
},
{
"ordinal": 2,
"name": "early_return",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "has_preprocessor",
"type_info": "Bool"
},
{
"ordinal": 4,
"name": "chat_input_enabled",
"type_info": "Bool"
},
{
"ordinal": 5,
"name": "on_behalf_of_email",
"type_info": "Text"
},
{
"ordinal": 6,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "version",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Int8"
]
},
"nullable": [
true,
true,
null,
null,
null,
true,
false,
false
]
},
"hash": "e1409c67b93881cf68f6ac9c8bae0856cf426c7e7860c8b5b799972baa8e6945"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n value->'preprocessor_module'->'value' as \"preprocessor_module: _\",\n schema as \"schema: _\"\n FROM flow_version\n WHERE\n path = $1\n AND workspace_id = $2\n ORDER BY created_at DESC\n LIMIT 1",
"query": "SELECT\n value->'preprocessor_module'->'value' as \"preprocessor_module: _\",\n schema as \"schema: _\"\n FROM flow_version\n WHERE\n id = $1\n AND workspace_id = $2",
"describe": {
"columns": [
{
@@ -16,7 +16,7 @@
],
"parameters": {
"Left": [
"Text",
"Int8",
"Text"
]
},
@@ -25,5 +25,5 @@
true
]
},
"hash": "e2474b7855c8b08f927f2b987421e773e537a8eb0a113477764bbab14e1f3a3d"
"hash": "f0c9670c27d9a3b8e8f316fd28f7d3ede2ecc3225ff2879ff5dfd59c4ef236d5"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT path FROM flow_version WHERE id = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Int8",
"Text"
]
},
"nullable": [
false
]
},
"hash": "fc9753f501974c4b570d710c7621ff1f6787a852bb27acf49593af963a6aacca"
}
+41 -40
View File
@@ -800,9 +800,9 @@ dependencies = [
[[package]]
name = "aws-lc-rs"
version = "1.14.1"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879b6c89592deb404ba4dc0ae6b58ffd1795c78991cbb5b8bc441c48a070440d"
checksum = "5932a7d9d28b0d2ea34c6b3779d35e3dd6f6345317c34e73438c4f1f29144151"
dependencies = [
"aws-lc-sys",
"zeroize",
@@ -810,9 +810,9 @@ dependencies = [
[[package]]
name = "aws-lc-sys"
version = "0.32.3"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "107a4e9d9cab9963e04e84bb8dee0e25f2a987f9a8bad5ed054abd439caa8f8c"
checksum = "1826f2e4cfc2cd19ee53c42fbf68e2f81ec21108e0b7ecf6a71cf062137360fc"
dependencies = [
"bindgen 0.72.1",
"cc",
@@ -6739,9 +6739,9 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.17"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8"
checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56"
dependencies = [
"base64 0.22.1",
"bytes",
@@ -10933,9 +10933,9 @@ dependencies = [
[[package]]
name = "rsa"
version = "0.9.8"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b"
checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88"
dependencies = [
"const-oid",
"digest 0.10.7",
@@ -15148,7 +15148,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill"
version = "1.575.0"
version = "1.575.4"
dependencies = [
"anyhow",
"aws-sdk-config",
@@ -15208,7 +15208,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.575.0"
version = "1.575.4"
dependencies = [
"anyhow",
"argon2",
@@ -15269,6 +15269,7 @@ dependencies = [
"pg_escape",
"pin-project",
"postgres-native-tls 0.5.0",
"postgres-native-tls 0.5.1",
"prometheus",
"quick_cache",
"rand 0.9.0",
@@ -15328,7 +15329,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.575.0"
version = "1.575.4"
dependencies = [
"base64 0.22.1",
"chrono",
@@ -15343,7 +15344,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.575.0"
version = "1.575.4"
dependencies = [
"chrono",
"lazy_static",
@@ -15357,7 +15358,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.575.0"
version = "1.575.4"
dependencies = [
"anyhow",
"axum",
@@ -15376,7 +15377,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.575.0"
version = "1.575.4"
dependencies = [
"anyhow",
"async-recursion",
@@ -15461,7 +15462,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.575.0"
version = "1.575.4"
dependencies = [
"regex",
"serde",
@@ -15476,7 +15477,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.575.0"
version = "1.575.4"
dependencies = [
"anyhow",
"bytes",
@@ -15500,7 +15501,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.575.0"
version = "1.575.4"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -15512,7 +15513,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.575.0"
version = "1.575.4"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -15521,7 +15522,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.575.0"
version = "1.575.4"
dependencies = [
"anyhow",
"lazy_static",
@@ -15533,7 +15534,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.575.0"
version = "1.575.4"
dependencies = [
"anyhow",
"serde_json",
@@ -15545,7 +15546,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.575.0"
version = "1.575.4"
dependencies = [
"anyhow",
"gosyn",
@@ -15557,7 +15558,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.575.0"
version = "1.575.4"
dependencies = [
"anyhow",
"lazy_static",
@@ -15569,7 +15570,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.575.0"
version = "1.575.4"
dependencies = [
"anyhow",
"serde_json",
@@ -15581,7 +15582,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.575.0"
version = "1.575.4"
dependencies = [
"anyhow",
"nu-parser",
@@ -15592,7 +15593,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.575.0"
version = "1.575.4"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -15603,7 +15604,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.575.0"
version = "1.575.4"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -15615,7 +15616,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.575.0"
version = "1.575.4"
dependencies = [
"anyhow",
"async-recursion",
@@ -15638,7 +15639,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.575.0"
version = "1.575.4"
dependencies = [
"anyhow",
"lazy_static",
@@ -15652,7 +15653,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.575.0"
version = "1.575.4"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -15669,7 +15670,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.575.0"
version = "1.575.4"
dependencies = [
"anyhow",
"lazy_static",
@@ -15683,7 +15684,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.575.0"
version = "1.575.4"
dependencies = [
"anyhow",
"lazy_static",
@@ -15701,7 +15702,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.575.0"
version = "1.575.4"
dependencies = [
"anyhow",
"serde",
@@ -15712,7 +15713,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.575.0"
version = "1.575.4"
dependencies = [
"anyhow",
"async-recursion",
@@ -15747,7 +15748,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.575.0"
version = "1.575.4"
dependencies = [
"wasm-bindgen",
"wasm-bindgen-test",
@@ -15757,7 +15758,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.575.0"
version = "1.575.4"
dependencies = [
"anyhow",
"async-once-cell",
@@ -16051,13 +16052,13 @@ dependencies = [
[[package]]
name = "windows-registry"
version = "0.5.3"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
dependencies = [
"windows-link 0.1.3",
"windows-result 0.3.4",
"windows-strings 0.4.2",
"windows-link 0.2.1",
"windows-result 0.4.1",
"windows-strings 0.5.1",
]
[[package]]
+4 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.575.0"
version = "1.575.4"
authors.workspace = true
edition.workspace = true
@@ -33,7 +33,7 @@ members = [
exclude = ["./windmill-duckdb-ffi-internal"]
[workspace.package]
version = "1.575.0"
version = "1.575.4"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
@@ -259,7 +259,7 @@ tokio-tar = "^0"
tempfile = "^3"
tokio-util = { version = "^0", features = ["io"] }
json-pointer = "^0"
itertools = "^0"
itertools = "^0.14.0"
regex = "^1"
semver = "^1"
aws-sigv4 = "^1.3.4"
@@ -300,7 +300,7 @@ swc_ecma_visit = "=0.104.8"
async-recursion = "^1"
base64 = "^0"
base64 = "^0.22.1"
base32 = "^0"
hmac = "0.12.1"
sha2 = "0.10.6"
+1 -1
View File
@@ -1 +1 @@
7cdb62d968feef22046b12a1217b711af457cced
31ee9d3449f05cd0328c0fcf43e2b161dee767b9
+1 -1
View File
@@ -3,7 +3,7 @@
pub use crate::ee::*;
#[cfg(not(feature = "private"))]
pub async fn set_license_key(_license_key: String) -> () {
pub async fn set_license_key(_license_key: String, _db: Option<&windmill_common::db::DB>) -> () {
// Implementation is not open source
}
+47 -49
View File
@@ -79,7 +79,7 @@ use windmill_common::{
OTEL_METRICS_ENABLED, OTEL_TRACING_ENABLED, SERVICE_LOG_RETENTION_SECS,
};
use windmill_common::{client::AuthedClient, global_settings::APP_WORKSPACED_ROUTE_SETTING};
use windmill_queue::{SameWorkerPayload, cancel_job, get_queued_job_v2};
use windmill_queue::{cancel_job, get_queued_job_v2, SameWorkerPayload};
use windmill_worker::{
handle_job_error, JobCompletedSender, SameWorkerSender, BUNFIG_INSTALL_SCOPES,
INSTANCE_PYTHON_VERSION, JOB_DEFAULT_TIMEOUT, KEEP_JOB_DIR, MAVEN_REPOS, NO_DEFAULT_MAVEN,
@@ -1271,7 +1271,7 @@ pub async fn reload_license_key(conn: &Connection) -> anyhow::Result<()> {
tracing::error!("Could not parse LICENSE_KEY found: {:#?}", &q);
}
};
set_license_key(value).await;
set_license_key(value, conn.as_sql()).await;
Ok(())
}
@@ -2219,7 +2219,6 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, node_n
);
}
timeouts
};
@@ -2253,7 +2252,6 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, node_n
}
}
let timeouts = non_restartable_jobs
.into_iter()
.map(|x| (x, ErrorMessage::RestartDisabled))
@@ -2289,59 +2287,59 @@ async fn handle_zombie_jobs(db: &Pool<Postgres>, base_internal_url: &str, node_n
continue;
}
if let Some(job) = job.unwrap() {
let label = if job.permissioned_as != format!("u/{}", job.created_by)
&& job.permissioned_as != job.created_by
{
format!("ephemeral-script-end-user-{}", job.created_by)
} else {
"ephemeral-script".to_string()
};
let token = create_token_for_owner(
&db,
&job.workspace_id,
&job.permissioned_as,
&label,
*SCRIPT_TOKEN_EXPIRY,
&job.permissioned_as_email,
&job.id,
None,
Some(format!("handle_zombie_jobs")),
)
.await
.expect("could not create job token");
let label = if job.permissioned_as != format!("u/{}", job.created_by)
&& job.permissioned_as != job.created_by
{
format!("ephemeral-script-end-user-{}", job.created_by)
} else {
"ephemeral-script".to_string()
};
let token = create_token_for_owner(
&db,
&job.workspace_id,
&job.permissioned_as,
&label,
*SCRIPT_TOKEN_EXPIRY,
&job.permissioned_as_email,
&job.id,
None,
Some(format!("handle_zombie_jobs")),
)
.await
.expect("could not create job token");
let client = AuthedClient::new(
base_internal_url.to_string(),
job.workspace_id.to_string(),
token,
None,
);
let client = AuthedClient::new(
base_internal_url.to_string(),
job.workspace_id.to_string(),
token,
None,
);
let error_message = format!(
let error_message = format!(
"Job timed out after no ping from job since {} (ZOMBIE_JOB_TIMEOUT: {}, reason: {:?}).\nThis likely means that the job died on worker {}, OOM are a common reason for worker crashes.\nCheck the workers around the time of the last ping and the exit code if any.",
job.last_ping.unwrap_or_default(),
*ZOMBIE_JOB_TIMEOUT,
error_kind.to_string(),
job.worker.clone().unwrap_or_default(),
);
let memory_peak = job.memory_peak.unwrap_or(0);
let _ = handle_job_error(
db,
&client,
&windmill_queue::MiniCompletedJob::from(job),
memory_peak,
None,
error::Error::ExecutionErr(error_message),
true,
Some(&same_worker_tx_never_used),
"",
node_name,
send_result_never_used,
#[cfg(feature = "benchmark")]
&mut windmill_common::bench::BenchmarkIter::new(),
)
.await;
}
let memory_peak = job.memory_peak.unwrap_or(0);
let _ = handle_job_error(
db,
&client,
&windmill_queue::MiniCompletedJob::from(job),
memory_peak,
None,
error::Error::ExecutionErr(error_message),
true,
Some(&same_worker_tx_never_used),
"",
node_name,
send_result_never_used,
#[cfg(feature = "benchmark")]
&mut windmill_common::bench::BenchmarkIter::new(),
)
.await;
}
}
}
+1
View File
@@ -208,6 +208,7 @@ impl RunJob {
false,
None,
debounce_job_id_o,
None,
)
.await
.expect("push has to succeed");
+18 -9
View File
@@ -421,14 +421,16 @@ def main():
path: "f/rel/root_flow_renamed".into(),
summary: "".into(),
description: None,
value: to_raw_value(&serde_json::from_str::<serde_json::Value>(
&serde_json::to_string(flow.value())
.unwrap()
.replace("nstep1", "Foxes")
.replace("nstep2_2", "like")
.replace("nstep_4_1", "Emeralds"),
)
.unwrap()),
value: to_raw_value(
&serde_json::from_str::<serde_json::Value>(
&serde_json::to_string(flow.value())
.unwrap()
.replace("nstep1", "Foxes")
.replace("nstep2_2", "like")
.replace("nstep_4_1", "Emeralds"),
)
.unwrap(),
),
schema: None,
draft_only: None,
tag: None,
@@ -437,7 +439,7 @@ def main():
deployment_message: None,
visible_to_runner_only: None,
on_behalf_of_email: None,
ws_error_handler_muted: None
ws_error_handler_muted: None,
})
.send()
.await
@@ -862,6 +864,7 @@ def main():
false,
None,
None,
None,
)
.await
.unwrap();
@@ -1021,6 +1024,7 @@ def main():
false,
None,
debounce_job_id_o,
None,
)
.await
.unwrap();
@@ -1198,6 +1202,7 @@ def main():
false,
None,
debounce_job_id_o,
None,
)
.await
.unwrap();
@@ -1705,6 +1710,7 @@ WHERE
false,
None,
None,
None,
)
.await
.unwrap();
@@ -1844,6 +1850,7 @@ WHERE
false,
None,
None,
None,
)
.await
.unwrap();
@@ -2274,6 +2281,7 @@ WHERE
false,
None,
None,
None,
)
.await
.unwrap();
@@ -2399,6 +2407,7 @@ WHERE
// false,
// None,
// None,
// None,
// )
// .await
// .unwrap();
+1 -1
View File
@@ -2769,7 +2769,7 @@ async fn test_result_format(db: Pool<Postgres>) -> anyhow::Result<()> {
let response = windmill_api::jobs::run_wait_result(
&db,
Uuid::parse_str(ordered_result_job_id).unwrap(),
"test-workspace".to_string(),
"test-workspace",
None,
"test-user",
)
+1
View File
@@ -94,6 +94,7 @@ tempfile.workspace = true
tokio-util.workspace = true
tokio-tar.workspace = true
tokio-postgres.workspace = true
postgres-native-tls.workspace = true
hmac.workspace = true
cookie.workspace = true
sha2.workspace = true
+246 -1
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.575.0
version: 1.575.4
title: Windmill API
contact:
@@ -5956,6 +5956,80 @@ paths:
application/json:
schema: {}
/w/{workspace}/jobs/run_wait_result/fv/{version}:
post:
summary: run flow by version and wait until completion
operationId: runWaitResultFlowByVersion
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: version
description: flow version ID
in: path
required: true
schema:
type: integer
format: int64
- $ref: "#/components/parameters/IncludeHeader"
- $ref: "#/components/parameters/QueueLimit"
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/SkipPreprocessor"
- name: memory_id
description: memory ID for chat-enabled flows
in: query
schema:
type: string
format: uuid
requestBody:
description: script args
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ScriptArgs"
responses:
"200":
description: job result
content:
application/json:
schema: {}
get:
summary: run flow by version with GET and wait until completion
operationId: runWaitResultFlowByVersionGet
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: version
description: flow version ID
in: path
required: true
schema:
type: integer
format: int64
- $ref: "#/components/parameters/IncludeHeader"
- $ref: "#/components/parameters/QueueLimit"
- $ref: "#/components/parameters/Payload"
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/SkipPreprocessor"
- name: memory_id
description: memory ID for chat-enabled flows
in: query
schema:
type: string
format: uuid
responses:
"200":
description: job result
content:
application/json:
schema: {}
/w/{workspace}/jobs/run_and_stream/f/{path}:
post:
summary: run flow by path and stream updates via SSE
@@ -6032,6 +6106,94 @@ paths:
schema:
type: string
/w/{workspace}/jobs/run_and_stream/fv/{version}:
post:
summary: run flow by version and stream updates via SSE
operationId: runAndStreamFlowByVersion
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: version
description: flow version ID
in: path
required: true
schema:
type: integer
format: int64
- $ref: "#/components/parameters/IncludeHeader"
- $ref: "#/components/parameters/QueueLimit"
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/SkipPreprocessor"
- name: memory_id
description: memory ID for chat-enabled flows
in: query
schema:
type: string
format: uuid
- name: poll_delay_ms
description: delay between polling for job updates in milliseconds
in: query
schema:
type: integer
format: int64
requestBody:
description: flow args
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ScriptArgs"
responses:
"200":
description: server-sent events stream of job updates
content:
text/event-stream:
schema:
type: string
get:
summary: run flow by version with GET and stream updates via SSE
operationId: runAndStreamFlowByVersionGet
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: version
description: flow version ID
in: path
required: true
schema:
type: integer
format: int64
- $ref: "#/components/parameters/IncludeHeader"
- $ref: "#/components/parameters/QueueLimit"
- $ref: "#/components/parameters/Payload"
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/SkipPreprocessor"
- name: memory_id
description: memory ID for chat-enabled flows
in: query
schema:
type: string
format: uuid
- name: poll_delay_ms
description: delay between polling for job updates in milliseconds
in: query
schema:
type: integer
format: int64
responses:
"200":
description: server-sent events stream of job updates
content:
text/event-stream:
schema:
type: string
/w/{workspace}/jobs/run_and_stream/p/{path}:
post:
summary: run script by path and stream updates via SSE
@@ -7680,6 +7842,64 @@ paths:
type: string
format: uuid
/w/{workspace}/jobs/run/fv/{version}:
post:
summary: run flow by version
operationId: runFlowByVersion
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: version
description: flow version ID
in: path
required: true
schema:
type: integer
format: int64
- name: scheduled_for
description: when to schedule this job (leave empty for immediate run)
in: query
schema:
type: string
format: date-time
- name: scheduled_in_secs
description: schedule the script to execute in the number of seconds starting now
in: query
schema:
type: integer
- $ref: "#/components/parameters/SkipPreprocessor"
- $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/WorkerTag"
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/IncludeHeader"
- name: invisible_to_owner
description: make the run invisible to the the flow owner (default false)
in: query
schema:
type: boolean
- name: memory_id
description: memory ID for chat-enabled flows
in: query
schema:
type: string
format: uuid
requestBody:
description: flow args
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ScriptArgs"
responses:
"201":
description: job created
content:
text/plain:
schema:
type: string
format: uuid
/w/{workspace}/jobs/run/batch_rerun_jobs:
post:
summary: re-run multiple jobs
@@ -8451,6 +8671,7 @@ paths:
- $ref: "#/components/parameters/ResultFilter"
- $ref: "#/components/parameters/AllowWildcards"
- $ref: "#/components/parameters/PerPage"
- $ref: "#/components/parameters/JobTriggerKind"
- name: is_skipped
description: is the job skipped
in: query
@@ -14424,6 +14645,7 @@ paths:
- $ref: "#/components/parameters/AllowWildcards"
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PerPage"
- $ref: "#/components/parameters/JobTriggerKind"
- name: is_skipped
description: is the job skipped
in: query
@@ -14862,6 +15084,27 @@ components:
in: query
schema:
type: integer
JobTriggerKind:
name: trigger_kind
description: trigger kind (schedule, http, websocket...)
in: query
schema:
type: string
enum:
- webhook
- default_email
- email
- schedule
- http
- websocket
- postgres
- kafka
- nats
- mqtt
- sqs
- gcp
- poll
- cli
OrderDesc:
name: order_desc
description: order by desc order (default true)
@@ -18698,6 +18941,8 @@ components:
properties:
lock_error_logs:
type: string
version_id:
type: number
ExtraPerms:
type: object
+3
View File
@@ -1241,6 +1241,7 @@ async fn create_app_internal<'a>(
false,
None,
None,
None,
)
.await?;
tracing::info!("Pushed app dependency job {}", dependency_job_uuid);
@@ -1630,6 +1631,7 @@ async fn update_app_internal<'a>(
false,
None,
None,
None,
)
.await?;
tracing::info!("Pushed app dependency job {}", dependency_job_uuid);
@@ -1949,6 +1951,7 @@ async fn execute_component(
false,
end_user_email,
None,
None,
)
.await?;
tx.commit().await?;
@@ -222,6 +222,7 @@ async fn get_concurrent_intervals(
all_workspaces: _,
concurrency_key: Some(_),
allow_wildcards: None,
trigger_kind: _,
} => true,
_ => false,
};
+4 -1
View File
@@ -13,7 +13,10 @@ use anyhow::anyhow;
#[cfg(all(feature = "enterprise", not(feature = "private")))]
use {std::sync::Arc, tokio::sync::RwLock};
#[cfg(not(feature = "private"))]
pub async fn validate_license_key(_license_key: String) -> anyhow::Result<(String, bool)> {
pub async fn validate_license_key(
_license_key: String,
_db: Option<&crate::db::DB>,
) -> anyhow::Result<(String, bool)> {
// Implementation is not open source
Err(anyhow!("License can't be validated in Windmill CE"))
}
+5 -1
View File
@@ -562,6 +562,7 @@ async fn create_flow(
false,
None,
None,
None,
)
.await?;
@@ -1025,6 +1026,7 @@ async fn update_flow(
false,
None,
None,
None,
)
.await?;
@@ -1133,6 +1135,7 @@ async fn get_flow_by_path(
flow.timeout,
flow.visible_to_runner_only,
flow.on_behalf_of_email,
flow_version.id AS version_id,
flow_version.schema,
flow_version.value,
flow_version.created_at AS edited_at,
@@ -1172,8 +1175,9 @@ async fn get_flow_by_path(
flow.timeout,
flow.visible_to_runner_only,
flow.on_behalf_of_email,
flow_version.id AS version_id,
flow_version.schema,
flow_version.value,
flow_version.value,
flow_version.created_at AS edited_at,
flow_version.created_by AS edited_by,
NULL AS starred
+402 -146
View File
@@ -34,7 +34,7 @@ use windmill_common::flow_conversations::add_message_to_conversation_tx;
use windmill_common::flow_status::{JobResult, RestartedFrom};
use windmill_common::jobs::{
check_tag_available_for_workspace_internal, format_completed_job_result, format_result,
DynamicInput, ENTRYPOINT_OVERRIDE,
DynamicInput, JobTriggerKind, ENTRYPOINT_OVERRIDE,
};
use windmill_common::s3_helpers::{upload_artifact_to_store, BundleFormat};
use windmill_common::utils::{RunnableKind, WarnAfterExt};
@@ -45,7 +45,7 @@ use windmill_common::{email_oss::send_email_html, server::load_smtp_config};
use windmill_common::variables::get_workspace_key;
use crate::triggers::trigger_helpers::ScriptId;
use crate::triggers::trigger_helpers::{FlowId, ScriptId};
use crate::{
add_webhook_allowed_origin,
args::{self, RawWebhookArgs},
@@ -95,8 +95,9 @@ use windmill_common::{
};
use windmill_common::{
get_latest_deployed_hash_for_path, get_latest_flow_version_info_for_path,
get_script_info_for_hash, utils::empty_as_none, FlowVersionInfo, ScriptHashInfo, BASE_URL,
get_flow_version_info_from_version, get_latest_deployed_hash_for_path,
get_latest_flow_version_info_for_path, get_script_info_for_hash, utils::empty_as_none,
FlowVersionInfo, ScriptHashInfo, BASE_URL,
};
use windmill_queue::{
cancel_job, get_result_and_success_by_id_from_flow, job_is_complete, push, PushArgs,
@@ -124,6 +125,13 @@ pub fn workspaced_service() -> Router {
.layer(cors.clone())
.layer(ce_headers.clone()),
)
.route(
"/run/fv/:version",
post(run_flow_by_version)
.head(|| async { "" })
.layer(cors.clone())
.layer(ce_headers.clone()),
)
.route(
"/run/batch_rerun_jobs",
post(batch_rerun_jobs)
@@ -176,6 +184,14 @@ pub fn workspaced_service() -> Router {
.layer(cors.clone())
.layer(ce_headers.clone()),
)
.route(
"/run_wait_result/fv/:version",
post(run_wait_result_flow_by_version)
.get(run_wait_result_flow_by_version_get)
.head(|| async { "" })
.layer(cors.clone())
.layer(ce_headers.clone()),
)
.route(
"/run_and_stream/f/*script_path",
get(stream_flow_by_path)
@@ -184,6 +200,14 @@ pub fn workspaced_service() -> Router {
.layer(cors.clone())
.layer(ce_headers.clone()),
)
.route(
"/run_and_stream/fv/:version",
get(stream_flow_by_version)
.post(stream_flow_by_version)
.head(|| async { "" })
.layer(cors.clone())
.layer(ce_headers.clone()),
)
.route(
"/run_and_stream/p/*script_path",
get(stream_script_by_path)
@@ -1750,6 +1774,22 @@ impl RunJobQuery {
Ok(None)
}
}
fn payload_as_args(&self) -> error::Result<HashMap<String, Box<RawValue>>> {
let payload_r = self.payload.clone().map(decode_payload).map(|x| {
x.map_err(|e| {
error::Error::internal_err(format!("Impossible to decode query payload: {e:#?}"))
})
});
let payload_as_args = if let Some(payload) = payload_r {
payload?
} else {
HashMap::new()
};
Ok(payload_as_args)
}
}
#[derive(Deserialize, Clone)]
@@ -1781,6 +1821,7 @@ pub struct ListQueueQuery {
pub is_not_schedule: Option<bool>,
pub concurrency_key: Option<String>,
pub allow_wildcards: Option<bool>,
pub trigger_kind: Option<JobTriggerKind>,
}
impl From<ListCompletedQuery> for ListQueueQuery {
@@ -1812,6 +1853,7 @@ impl From<ListCompletedQuery> for ListQueueQuery {
is_not_schedule: lcq.is_not_schedule,
concurrency_key: lcq.concurrency_key,
allow_wildcards: lcq.allow_wildcards,
trigger_kind: lcq.trigger_kind,
}
}
}
@@ -1935,6 +1977,10 @@ pub fn filter_list_queue_query(
sqlb.and_where("trigger_kind IS DISTINCT FROM 'schedule'");
}
if let Some(tk) = &lq.trigger_kind {
sqlb.and_where_eq("trigger_kind", "?".bind(&format!("{}", tk)));
}
sqlb
}
@@ -2142,7 +2188,7 @@ async fn cancel_selection(
let mut tx = user_db.begin(&authed).await?;
let tags = get_scope_tags(&authed).map(|v| v.iter().map(|s| s.to_string()).collect_vec());
let jobs_to_cancel = sqlx::query_scalar!(
"SELECT j.id AS \"id!\" FROM v2_job j WHERE j.id = ANY($1) AND j.trigger_kind != 'schedule'::job_trigger_kind AND ($2::text[] IS NULL OR j.tag = ANY($2))",
"SELECT j.id AS \"id!\" FROM v2_job j LEFT JOIN v2_job_queue q USING (id) WHERE j.id = ANY($1) AND j.trigger_kind IS DISTINCT FROM 'schedule'::job_trigger_kind AND ($2::text[] IS NULL OR j.tag = ANY($2))",
&jobs,
tags.as_ref().map(|v| v.as_slice())
)
@@ -3851,6 +3897,7 @@ async fn batch_rerun_handle_job(
StripPath(job.script_path.clone()),
RunJobQuery { skip_preprocessor: Some(true), ..Default::default() },
PushArgsOwned { extra: None, args },
None,
)
.await;
if let Ok((uuid, _)) = result {
@@ -3867,6 +3914,7 @@ async fn batch_rerun_handle_job(
StripPath(job.script_path.clone()),
RunJobQuery { skip_preprocessor: Some(true), ..Default::default() },
PushArgsOwned { extra: None, args },
None,
)
.await
} else {
@@ -3987,28 +4035,22 @@ pub async fn run_flow_by_path(
.await?;
let (uuid, _) =
run_flow_by_path_inner(authed, db, user_db, w_id, flow_path, run_query, args).await?;
run_flow_by_path_inner(authed, db, user_db, w_id, flow_path, run_query, args, None).await?;
Ok((StatusCode::CREATED, uuid.to_string()))
}
pub async fn run_flow_by_path_inner(
authed: ApiAuthed,
db: DB,
pub async fn run_flow(
authed: &ApiAuthed,
db: &DB,
user_db: UserDB,
w_id: String,
flow_path: StripPath,
w_id: &str,
flow_path: &str,
flow_version_info: FlowVersionInfo,
run_query: RunJobQuery,
args: PushArgsOwned,
trigger_kind: Option<JobTriggerKind>,
) -> error::Result<(Uuid, Option<String>)> {
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
let flow_path = flow_path.to_path();
check_scopes(&authed, || format!("jobs:run:flows:{flow_path}"))?;
let userdb_authed = UserDbWithAuthed { db: user_db.clone(), authed: &authed.to_authed_ref() };
let FlowVersionInfo {
version,
tag,
@@ -4019,8 +4061,7 @@ pub async fn run_flow_by_path_inner(
edited_by,
early_return,
..
} = get_latest_flow_version_info_for_path(Some(userdb_authed), &db, &w_id, &flow_path, true)
.await?;
} = flow_version_info;
let tag = run_query.tag.clone().or(tag);
@@ -4078,6 +4119,7 @@ pub async fn run_flow_by_path_inner(
false,
None,
None,
trigger_kind,
)
.await?;
@@ -4100,9 +4142,146 @@ pub async fn run_flow_by_path_inner(
}
tx.commit().await?;
Ok((uuid, early_return))
}
pub async fn run_flow_and_wait_result(
authed: &ApiAuthed,
db: &DB,
user_db: UserDB,
w_id: &str,
flow_path: &str,
flow_version_info: FlowVersionInfo,
run_query: RunJobQuery,
args: PushArgsOwned,
trigger_kind: Option<JobTriggerKind>,
) -> error::Result<Response> {
let (uuid, early_return) = run_flow(
authed,
db,
user_db,
w_id,
flow_path,
flow_version_info,
run_query,
args,
trigger_kind,
)
.await?;
run_wait_result(&db, uuid, w_id, early_return, &authed.username).await
}
pub async fn run_flow_by_path_inner(
authed: ApiAuthed,
db: DB,
user_db: UserDB,
w_id: String,
flow_path: StripPath,
run_query: RunJobQuery,
args: PushArgsOwned,
trigger_kind: Option<JobTriggerKind>,
) -> error::Result<(Uuid, Option<String>)> {
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
let flow_path = flow_path.to_path();
check_scopes(&authed, || format!("jobs:run:flows:{flow_path}"))?;
let userdb_authed = UserDbWithAuthed { db: user_db.clone(), authed: &authed.to_authed_ref() };
let flow_version_info =
get_latest_flow_version_info_for_path(Some(userdb_authed), &db, &w_id, &flow_path, true)
.await?;
run_flow(
&authed,
&db,
user_db,
&w_id,
flow_path,
flow_version_info,
run_query,
args,
trigger_kind,
)
.await
}
pub async fn run_flow_by_version(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path((w_id, version)): Path<(String, i64)>,
Query(run_query): Query<RunJobQuery>,
args: RawWebhookArgs,
) -> error::Result<(StatusCode, String)> {
let args = args
.to_args_from_runnable(
&authed,
&db,
&w_id,
RunnableId::from_flow_version(version),
run_query.skip_preprocessor,
)
.await?;
let (uuid, _) =
run_flow_by_version_inner(authed, db, user_db, w_id, version, run_query, args, None)
.await?;
Ok((StatusCode::CREATED, uuid.to_string()))
}
pub async fn run_flow_by_version_inner(
authed: ApiAuthed,
db: DB,
user_db: UserDB,
w_id: String,
version: i64,
run_query: RunJobQuery,
args: PushArgsOwned,
trigger_kind: Option<JobTriggerKind>,
) -> error::Result<(Uuid, Option<String>)> {
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
let flow_path = sqlx::query_scalar!(
r#"
SELECT
path
FROM
flow_version
WHERE
id = $1 AND
workspace_id = $2
"#,
version,
&w_id
)
.fetch_one(&db)
.await?;
check_scopes(&authed, || format!("jobs:run:flows:{flow_path}"))?;
let flow_version_info =
get_flow_version_info_from_version(&db, version, &w_id, &flow_path).await?;
run_flow(
&authed,
&db,
user_db,
&w_id,
&flow_path,
flow_version_info,
run_query,
args,
trigger_kind,
)
.await
}
#[cfg(not(feature = "enterprise"))]
pub async fn restart_flow(
_authed: ApiAuthed,
@@ -4195,6 +4374,7 @@ pub async fn restart_flow(
false,
None,
None,
None,
)
.await?;
tx.commit().await?;
@@ -4219,8 +4399,17 @@ pub async fn run_script_by_path(
)
.await?;
let (uuid, _) =
run_script_by_path_inner(authed, db, user_db, w_id, script_path, run_query, args).await?;
let (uuid, _) = run_script_by_path_inner(
authed,
db,
user_db,
w_id,
script_path,
run_query,
args,
None,
)
.await?;
Ok((StatusCode::CREATED, uuid.to_string()))
}
@@ -4233,6 +4422,7 @@ pub async fn run_script_by_path_inner(
script_path: StripPath,
run_query: RunJobQuery,
args: PushArgsOwned,
trigger_kind: Option<JobTriggerKind>,
) -> error::Result<(Uuid, Option<bool>)> {
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
@@ -4304,6 +4494,7 @@ pub async fn run_script_by_path_inner(
false,
None,
None,
trigger_kind,
)
.await?;
tx.commit().await?;
@@ -4461,6 +4652,7 @@ pub async fn run_workflow_as_code(
false,
None,
None,
None,
)
.await?;
@@ -4577,7 +4769,7 @@ pub struct WindmillCompositeResult {
pub async fn run_wait_result_internal(
db: &DB,
uuid: Uuid,
w_id: String,
w_id: &str,
node_id_for_empty_return: Option<String>,
username: &str,
) -> error::Result<(Box<RawValue>, bool)> {
@@ -4593,7 +4785,7 @@ pub async fn run_wait_result_internal(
let mut g = Guard {
done: false,
id: uuid,
w_id: w_id.clone(),
w_id: w_id.to_string(),
db: db.clone(),
username: username.to_string(),
};
@@ -4605,7 +4797,7 @@ pub async fn run_wait_result_internal(
if let Some(node_id_for_empty_return) = node_id_for_empty_return.as_ref() {
let result_and_success = get_result_and_success_by_id_from_flow(
&db,
&w_id,
w_id,
&uuid,
node_id_for_empty_return,
None,
@@ -4762,7 +4954,7 @@ pub fn result_to_response(result: Box<RawValue>, success: bool) -> error::Result
pub async fn run_wait_result(
db: &DB,
uuid: Uuid,
w_id: String,
w_id: &str,
node_id_for_empty_return: Option<String>,
username: &str,
) -> error::Result<Response> {
@@ -4922,18 +5114,11 @@ pub async fn run_wait_result_job_by_path_get(
if method == http::Method::HEAD {
return Ok(Json(serde_json::json!("")).into_response());
}
let payload_r = run_query.payload.map(decode_payload).map(|x| {
x.map_err(|e| Error::internal_err(format!("Impossible to decode query payload: {e:#?}")))
});
let payload_args = if let Some(payload) = payload_r {
payload?
} else {
HashMap::new()
};
let payload_as_args = run_query.payload_as_args()?;
let mut args = args.process_args(&authed, &db, &w_id, None).await?;
args.body = args::Body::HashMap(payload_args);
args.body = args::Body::HashMap(payload_as_args);
let args = args
.to_args_from_runnable(
@@ -5005,11 +5190,12 @@ pub async fn run_wait_result_job_by_path_get(
false,
None,
None,
None,
)
.await?;
tx.commit().await?;
let wait_result = run_wait_result(&db, uuid, w_id, None, &authed.username).await;
let wait_result = run_wait_result(&db, uuid, &w_id, None, &authed.username).await;
if delete_after_use.unwrap_or(false) {
delete_job_metadata_after_use(&db, uuid).await?;
}
@@ -5034,20 +5220,10 @@ pub async fn run_wait_result_flow_by_path_get(
if method == http::Method::HEAD {
return Ok(Json(serde_json::json!("")).into_response());
}
let payload_r = run_query.payload.clone().map(decode_payload).map(|x| {
x.map_err(|e| {
error::Error::internal_err(format!("Impossible to decode query payload: {e:#?}"))
})
});
let payload_args = if let Some(payload) = payload_r {
payload?
} else {
HashMap::new()
};
let payload_as_args = run_query.payload_as_args()?;
let mut args = args.process_args(&authed, &db, &w_id, None).await?;
args.body = args::Body::HashMap(payload_args);
args.body = args::Body::HashMap(payload_as_args);
let args = args
.to_args_from_runnable(
@@ -5159,11 +5335,12 @@ pub async fn run_wait_result_script_by_path_internal(
false,
None,
None,
None,
)
.await?;
tx.commit().await?;
let wait_result = run_wait_result(&db, uuid, w_id, None, &authed.username).await;
let wait_result = run_wait_result(&db, uuid, &w_id, None, &authed.username).await;
if delete_after_use.unwrap_or(false) {
delete_job_metadata_after_use(&db, uuid).await?;
}
@@ -5281,11 +5458,12 @@ pub async fn run_wait_result_script_by_hash(
false,
None,
None,
None,
)
.await?;
tx.commit().await?;
let wait_result = run_wait_result(&db, uuid, w_id, None, &authed.username).await;
let wait_result = run_wait_result(&db, uuid, &w_id, None, &authed.username).await;
if delete_after_use.unwrap_or(false) {
delete_job_metadata_after_use(&db, uuid).await?;
}
@@ -5342,6 +5520,28 @@ pub async fn stream_flow_by_path(
.await
}
pub async fn stream_flow_by_version(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Extension(user_db): Extension<UserDB>,
Path((w_id, version)): Path<(String, i64)>,
Query(run_query): Query<RunJobQuery>,
method: hyper::http::Method,
args: RawWebhookArgs,
) -> error::Result<Response> {
stream_job(
authed,
db,
user_db,
w_id,
RunnableId::from_flow_version(version),
args,
run_query,
method == http::Method::GET,
)
.await
}
pub async fn stream_script_by_path(
authed: ApiAuthed,
Extension(db): Extension<DB>,
@@ -5397,20 +5597,10 @@ pub async fn stream_job(
is_get: bool,
) -> error::Result<Response> {
let args = if is_get {
let payload_r = run_query.payload.clone().map(decode_payload).map(|x| {
x.map_err(|e| {
Error::internal_err(format!("Impossible to decode query payload: {e:#?}"))
})
});
let payload_args = if let Some(payload) = payload_r {
payload?
} else {
HashMap::new()
};
let payload_as_args = run_query.payload_as_args()?;
let mut args = args.process_args(&authed, &db, &w_id, None).await?;
args.body = args::Body::HashMap(payload_args);
args.body = args::Body::HashMap(payload_as_args);
let args = args
.to_args_from_runnable(&db, &w_id, runnable_id.clone(), run_query.skip_preprocessor)
@@ -5439,6 +5629,7 @@ pub async fn stream_job(
StripPath(script_path),
run_query,
args,
None,
)
.await?
.0
@@ -5456,7 +5647,7 @@ pub async fn stream_job(
.await?
.0
}
RunnableId::FlowPath(flow_path) => {
RunnableId::FlowId(FlowId::FlowPath(flow_path)) => {
run_flow_by_path_inner(
authed.clone(),
db.clone(),
@@ -5465,6 +5656,21 @@ pub async fn stream_job(
StripPath(flow_path),
run_query,
args,
None,
)
.await?
.0
}
RunnableId::FlowId(FlowId::FlowVersion(version)) => {
run_flow_by_version_inner(
authed.clone(),
db.clone(),
user_db,
w_id.clone(),
version,
run_query,
args,
None
)
.await?
.0
@@ -5524,100 +5730,137 @@ pub async fn run_wait_result_flow_by_path_internal(
let flow_path = flow_path.to_path();
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let userdb_authed = UserDbWithAuthed { db: user_db.clone(), authed: &authed.to_authed_ref() };
let FlowVersionInfo {
tag,
dedicated_worker,
early_return,
has_preprocessor,
chat_input_enabled,
on_behalf_of_email,
edited_by,
version,
} = get_latest_flow_version_info_for_path(Some(userdb_authed), &db, &w_id, &flow_path, true)
.await?;
let flow_version_info =
get_latest_flow_version_info_for_path(Some(userdb_authed), &db, &w_id, &flow_path, true)
.await?;
let tag = run_query.tag.clone().or(tag);
check_tag_available_for_workspace(&db, &w_id, &tag, &authed).await?;
let (email, permissioned_as, push_authed, tx) =
if let Some(on_behalf_of_email) = on_behalf_of_email.as_ref() {
(
on_behalf_of_email,
username_to_permissioned_as(&edited_by),
None,
PushIsolationLevel::IsolatedRoot(db.clone()),
)
} else {
(
&authed.email,
username_to_permissioned_as(&authed.username),
Some(authed.clone().into()),
PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into()),
)
};
let (uuid, mut tx) = push(
run_flow_and_wait_result(
&authed,
&db,
tx,
user_db,
&w_id,
JobPayload::Flow {
path: flow_path.to_string(),
dedicated_worker,
version,
apply_preprocessor: !run_query.skip_preprocessor.unwrap_or(false)
&& has_preprocessor.unwrap_or(false),
},
PushArgs { args: &args.args, extra: args.extra },
authed.display_username(),
email,
permissioned_as,
authed.token_prefix.as_deref(),
scheduled_for,
None,
run_query.parent_job,
None,
run_query.root_job,
run_query.job_id,
false,
false,
None,
!run_query.invisible_to_owner.unwrap_or(false),
tag,
None,
None,
None,
push_authed.as_ref(),
false,
None,
flow_path,
flow_version_info,
run_query,
args,
None,
)
.await
}
pub async fn run_wait_result_flow_by_version_get(
method: hyper::http::Method,
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, version)): Path<(String, i64)>,
Query(run_query): Query<RunJobQuery>,
args: RawWebhookArgs,
) -> error::Result<Response> {
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
let flow_path = sqlx::query_scalar!(
"SELECT path FROM flow_version WHERE id = $1 AND workspace_id = $2",
version,
&w_id
)
.fetch_one(&db)
.await?;
// Set conversation_id if provided (for agent memory)
if let Some(memory_id) = run_query.memory_id {
set_flow_memory_id(&mut tx, uuid, memory_id).await?;
check_scopes(&authed, || format!("jobs:run:flows:{flow_path}"))?;
if method == http::Method::HEAD {
return Ok(Json(serde_json::json!("")).into_response());
}
// Handle conversation messages for chat-enabled flows
if chat_input_enabled.unwrap_or(false) {
handle_chat_conversation_messages(
&mut tx,
&authed,
let payload_as_args = run_query.payload_as_args()?;
let mut args = args.process_args(&authed, &db, &w_id, None).await?;
args.body = args::Body::HashMap(payload_as_args);
let args = args
.to_args_from_runnable(
&db,
&w_id,
&flow_path.to_string(),
&run_query,
args.args.get("user_message"),
RunnableId::from_flow_version(version),
run_query.skip_preprocessor,
)
.await?;
}
tx.commit().await?;
let flow_version_info =
get_flow_version_info_from_version(&db, version, &w_id, &flow_path).await?;
run_wait_result(&db, uuid, w_id, early_return, &authed.username).await
run_flow_and_wait_result(
&authed,
&db,
user_db,
&w_id,
&flow_path,
flow_version_info,
run_query,
args,
None,
)
.await
}
pub async fn run_wait_result_flow_by_version(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, version)): Path<(String, i64)>,
Query(run_query): Query<RunJobQuery>,
args: RawWebhookArgs,
) -> error::Result<Response> {
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
let flow_path = sqlx::query_scalar!(
r#"
SELECT
path
FROM
flow_version
WHERE
id = $1 AND
workspace_id = $2
"#,
version,
&w_id
)
.fetch_one(&db)
.await?;
check_scopes(&authed, || format!("jobs:run:flows:{flow_path}"))?;
let args = args
.to_args_from_runnable(
&authed,
&db,
&w_id,
RunnableId::from_flow_version(version),
run_query.skip_preprocessor,
)
.await?;
let flow_version_info =
get_flow_version_info_from_version(&db, version, &w_id, &flow_path).await?;
run_flow_and_wait_result(
&authed,
&db,
user_db,
&w_id,
&flow_path,
flow_version_info,
run_query,
args,
None,
)
.await
}
async fn run_preview_script(
@@ -5688,6 +5931,7 @@ async fn run_preview_script(
false,
None,
None,
None,
)
.await?;
tx.commit().await?;
@@ -5715,7 +5959,7 @@ async fn run_wait_result_preview_script(
let uuid = uuid
.parse::<Uuid>()
.map_err(|_| Error::BadRequest("Invalid UUID".to_string()))?;
let result = run_wait_result(&db, uuid, w_id, None, &authed.username).await;
let result = run_wait_result(&db, uuid, &w_id, None, &authed.username).await;
return result;
}
@@ -5808,6 +6052,7 @@ async fn run_bundle_preview_script(
false,
None,
None,
None,
)
.await?;
job_id = Some(uuid);
@@ -5947,11 +6192,12 @@ async fn run_dependencies_job(
false,
None,
None,
None,
)
.await?;
tx.commit().await?;
let wait_result = run_wait_result(&db, uuid, w_id, None, &authed.username).await;
let wait_result = run_wait_result(&db, uuid, &w_id, None, &authed.username).await;
wait_result
}
@@ -6016,11 +6262,12 @@ async fn run_flow_dependencies_job(
false,
None,
None,
None,
)
.await?;
tx.commit().await?;
let wait_result = run_wait_result(&db, uuid, w_id, None, &authed.username).await;
let wait_result = run_wait_result(&db, uuid, &w_id, None, &authed.username).await;
wait_result
}
@@ -6369,6 +6616,7 @@ async fn run_preview_flow_job(
false,
None,
None,
None,
)
.await?;
@@ -6415,7 +6663,7 @@ async fn run_wait_result_preview_flow(
let uuid = uuid
.parse::<Uuid>()
.map_err(|_| Error::BadRequest("Invalid UUID".to_string()))?;
let result = run_wait_result(&db, uuid, w_id, None, &authed.username).await;
let result = run_wait_result(&db, uuid, &w_id, None, &authed.username).await;
return result;
}
@@ -6461,6 +6709,7 @@ async fn run_dynamic_select(
StripPath(path),
run_query.clone(),
push_args.clone(),
None,
)
.await?;
@@ -6565,6 +6814,7 @@ async fn run_dynamic_select(
false,
None,
None,
None,
)
.await?;
tx.commit().await?;
@@ -6698,6 +6948,7 @@ pub async fn run_job_by_hash_inner(
false,
None,
None,
None,
)
.await?;
tx.commit().await?;
@@ -7666,6 +7917,10 @@ pub fn filter_list_completed_query(
sqlb.and_where("trigger_kind IS DISTINCT FROM 'schedule'");
}
if let Some(tk) = &lq.trigger_kind {
sqlb.and_where_eq("trigger_kind", "?".bind(&format!("{}", tk)));
}
sqlb
}
@@ -7742,6 +7997,7 @@ pub struct ListCompletedQuery {
pub concurrency_key: Option<String>,
pub worker: Option<String>,
pub allow_wildcards: Option<bool>,
pub trigger_kind: Option<JobTriggerKind>,
}
async fn list_completed_jobs(
@@ -71,10 +71,12 @@ fn is_public_route_whitelisted(path: &str) -> bool {
"/user/login_callback/*",
"/api/workspaces/users",
"/api/users/whoami",
"/api/apps_u/*",
"/api/oauth/list_connects",
"/api/oauth/list_logins",
"/public/*",
"/a/*",
"/api/oauth/get_connect/*",
"/Inter-Variable.woff2",
];
+1
View File
@@ -1022,6 +1022,7 @@ async fn create_script_internal<'c>(
false,
None,
None,
None,
)
.await?;
Ok((hash, new_tx, None))
+39 -13
View File
@@ -188,7 +188,7 @@ pub async fn test_license_key(
Json(TestKey { license_key }): Json<TestKey>,
) -> error::Result<String> {
require_super_admin(&db, &authed.email).await?;
let (_, expired) = validate_license_key(license_key).await?;
let (_, expired) = validate_license_key(license_key, Some(&db)).await?;
if expired {
Err(error::Error::BadRequest("Expired license key".to_string()))
@@ -701,23 +701,49 @@ async fn setup_ducklake_catalog_db_inner(
sslmode = ssl_mode
);
let (client, connection) = tokio::time::timeout(
std::time::Duration::from_secs(20),
tokio_postgres::connect(&conn_str, tokio_postgres::NoTls),
)
.await
.map_err(|e| error::Error::ExecutionErr(format!("timeout: {}", e.to_string())))?
.map_err(|e| error::Error::ExecutionErr(format!("error: {}", e.to_string())))?;
let join_handle = tokio::spawn(async move { connection.await });
let (client, join_handle) = if ssl_mode == "require" {
use native_tls::TlsConnector;
use postgres_native_tls::MakeTlsConnector;
let mut connector = TlsConnector::builder();
connector.danger_accept_invalid_certs(true);
connector.danger_accept_invalid_hostnames(true);
let (client, connection) = tokio::time::timeout(
std::time::Duration::from_secs(20),
tokio_postgres::connect(
&conn_str,
MakeTlsConnector::new(connector.build().map_err(to_anyhow)?),
),
)
.await
.map_err(|e| error::Error::ExecutionErr(format!("timeout: {}", e.to_string())))?
.map_err(|e| error::Error::ExecutionErr(format!("error: {}", e.to_string())))?;
let join_handle = tokio::spawn(async move { connection.await });
(client, join_handle)
} else {
let (client, connection) = tokio::time::timeout(
std::time::Duration::from_secs(20),
tokio_postgres::connect(&conn_str, tokio_postgres::NoTls),
)
.await
.map_err(|e| error::Error::ExecutionErr(format!("timeout: {}", e.to_string())))?
.map_err(|e| error::Error::ExecutionErr(format!("error: {}", e.to_string())))?;
let join_handle = tokio::spawn(async move { connection.await });
(client, join_handle)
};
logs.db_connect = "OK".to_string();
client
.batch_execute(&format!(
"GRANT CONNECT ON DATABASE \"{dbname}\" TO ducklake_user;
GRANT USAGE ON SCHEMA public TO ducklake_user;
GRANT CREATE ON SCHEMA public TO ducklake_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ducklake_user;"
GRANT USAGE ON SCHEMA public TO ducklake_user;
GRANT CREATE ON SCHEMA public TO ducklake_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ducklake_user;"
))
.await
.map_err(|e| {
@@ -1050,6 +1050,7 @@ async fn route_job(
trigger.error_handler_args.as_ref(),
format!("http_trigger/{}", trigger.path),
None,
Some(windmill_common::jobs::JobTriggerKind::Http),
)
.await
.map_err(|e| e.into_response())?;
@@ -1108,6 +1109,7 @@ async fn route_job(
trigger.error_handler_args.as_ref(),
format!("http_trigger/{}", trigger.path),
None,
Some(windmill_common::jobs::JobTriggerKind::Http),
)
.await
.map_err(|e| e.into_response()),
@@ -1123,6 +1125,7 @@ async fn route_job(
trigger.error_handler_path.as_deref(),
trigger.error_handler_args.as_ref(),
format!("http_trigger/{}", trigger.path),
Some(windmill_common::jobs::JobTriggerKind::Http),
)
.await
.map_err(|e| e.into_response()),
@@ -515,6 +515,7 @@ pub trait Listener: TriggerCrud + TriggerJobArgs {
error_handler_args,
format!("{}_trigger/{}", Self::TRIGGER_KIND, listening_trigger.path),
None,
Some(Self::JOB_TRIGGER_KIND),
)
.await?;
@@ -12,7 +12,10 @@ use windmill_common::{
error::Result,
flows::{FlowModuleValue, Retry},
get_latest_deployed_hash_for_path, get_latest_flow_version_info_for_path,
jobs::{get_has_preprocessor_from_content_and_lang, script_path_to_payload, JobPayload},
jobs::{
get_has_preprocessor_from_content_and_lang, script_path_to_payload, JobPayload,
JobTriggerKind,
},
scripts::{get_full_hub_script_by_path, ScriptHash, ScriptLang},
triggers::{
HubOrWorkspaceId, RunnableFormat, RunnableFormatVersion, TriggerKind,
@@ -21,7 +24,6 @@ use windmill_common::{
users::username_to_permissioned_as,
utils::StripPath,
worker::to_raw_value,
FlowVersionInfo,
};
use windmill_queue::{push, PushArgs, PushArgsOwned, PushIsolationLevel};
@@ -56,7 +58,7 @@ struct PartialSchema {
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum RunnableId {
FlowPath(String),
FlowId(FlowId),
ScriptId(ScriptId),
HubScript(String),
}
@@ -75,7 +77,33 @@ impl RunnableId {
}
pub fn from_flow_path(path: &str) -> Self {
Self::FlowPath(path.to_string())
Self::FlowId(FlowId::FlowPath(path.to_string()))
}
pub fn from_flow_version(version: i64) -> Self {
Self::FlowId(FlowId::FlowVersion(version))
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum FlowId {
FlowPath(String),
FlowVersion(i64),
}
impl FlowId {
async fn get_flow_version_id(self, workspace_id: &str, db: &DB) -> Result<i64> {
let version_id = match self {
FlowId::FlowPath(path) => {
let info =
get_latest_flow_version_info_for_path(None, db, workspace_id, &path, true)
.await?;
info.version
}
FlowId::FlowVersion(version) => version,
};
Ok(version_id)
}
}
@@ -255,9 +283,8 @@ pub async fn get_runnable_format(
},
)
}
RunnableId::FlowPath(path) => {
let FlowVersionInfo { version, .. } =
get_latest_flow_version_info_for_path(None, &db, workspace_id, &path, true).await?;
RunnableId::FlowId(flow_id) => {
let version = flow_id.get_flow_version_id(workspace_id, db).await?;
let key = (
HubOrWorkspaceId::WorkspaceId(workspace_id.to_string()),
@@ -268,7 +295,7 @@ pub async fn get_runnable_format(
let runnable_format = RUNNABLE_FORMAT_VERSION_CACHE.get(&key);
if let Some(runnable_format) = runnable_format {
tracing::debug!("Using cached runnable format for flow {path}");
tracing::debug!("Using cached runnable format for flow version {version}");
return Ok(runnable_format);
}
@@ -279,11 +306,9 @@ pub async fn get_runnable_format(
schema as \"schema: _\"
FROM flow_version
WHERE
path = $1
AND workspace_id = $2
ORDER BY created_at DESC
LIMIT 1",
path,
id = $1
AND workspace_id = $2",
version,
workspace_id,
)
.fetch_one(db)
@@ -494,6 +519,7 @@ pub async fn trigger_runnable_inner(
error_handler_args: Option<&sqlx::types::Json<HashMap<String, serde_json::Value>>>,
trigger_path: String,
job_id: Option<Uuid>,
trigger_kind: Option<JobTriggerKind>,
) -> Result<(Uuid, Option<bool>, Option<String>)> {
let error_handler_args = error_handler_args.map(|args| {
let args = args
@@ -516,6 +542,7 @@ pub async fn trigger_runnable_inner(
path,
run_query,
args,
trigger_kind,
)
.await?;
(uuid, None, early_return)
@@ -532,6 +559,7 @@ pub async fn trigger_runnable_inner(
error_handler_args.as_ref(),
trigger_path,
job_id,
trigger_kind,
)
.await?;
(uuid, delete_after_use, None)
@@ -554,6 +582,7 @@ pub async fn trigger_runnable(
error_handler_args: Option<&sqlx::types::Json<HashMap<String, serde_json::Value>>>,
trigger_path: String,
job_id: Option<Uuid>,
trigger_kind: Option<JobTriggerKind>,
) -> Result<axum::response::Response> {
let (uuid, _, _) = trigger_runnable_inner(
db,
@@ -568,6 +597,7 @@ pub async fn trigger_runnable(
error_handler_args,
trigger_path,
job_id,
trigger_kind,
)
.await?;
Ok((StatusCode::CREATED, uuid.to_string()).into_response())
@@ -586,6 +616,7 @@ pub async fn trigger_runnable_and_wait_for_result(
error_handler_path: Option<&str>,
error_handler_args: Option<&sqlx::types::Json<HashMap<String, serde_json::Value>>>,
trigger_path: String,
trigger_kind: Option<JobTriggerKind>,
) -> Result<axum::response::Response> {
let username = authed.username.clone();
let (uuid, delete_after_use, early_return) = trigger_runnable_inner(
@@ -601,11 +632,11 @@ pub async fn trigger_runnable_and_wait_for_result(
error_handler_args,
trigger_path,
None,
trigger_kind,
)
.await?;
let (result, success) =
run_wait_result_internal(db, uuid, workspace_id.to_string(), early_return, &username)
.await?;
run_wait_result_internal(db, uuid, &workspace_id, early_return, &username).await?;
if delete_after_use.unwrap_or(false) {
delete_job_metadata_after_use(&db, uuid).await?;
@@ -627,6 +658,7 @@ pub async fn trigger_runnable_and_wait_for_raw_result(
error_handler_path: Option<&str>,
error_handler_args: Option<&sqlx::types::Json<HashMap<String, serde_json::Value>>>,
trigger_path: String,
trigger_kind: Option<JobTriggerKind>,
) -> Result<(Box<RawValue>, bool)> {
let username = authed.username.clone();
let (uuid, delete_after_use, early_return) = trigger_runnable_inner(
@@ -642,11 +674,12 @@ pub async fn trigger_runnable_and_wait_for_raw_result(
error_handler_args,
trigger_path,
None,
trigger_kind,
)
.await?;
let (result, success) =
run_wait_result_internal(db, uuid, workspace_id.to_string(), early_return, &username)
run_wait_result_internal(db, uuid, &workspace_id, early_return, &username)
.await
.with_context(|| {
format!(
@@ -675,6 +708,7 @@ pub async fn trigger_runnable_and_wait_for_raw_result_with_error_ctx(
error_handler_path: Option<&str>,
error_handler_args: Option<&sqlx::types::Json<HashMap<String, serde_json::Value>>>,
trigger_path: String,
trigger_kind: Option<JobTriggerKind>,
) -> Result<Box<RawValue>> {
let (result, success) = trigger_runnable_and_wait_for_raw_result(
db,
@@ -688,6 +722,7 @@ pub async fn trigger_runnable_and_wait_for_raw_result_with_error_ctx(
error_handler_path,
error_handler_args,
trigger_path,
trigger_kind,
)
.await?;
@@ -714,6 +749,7 @@ async fn trigger_script_internal(
error_handler_args: Option<&sqlx::types::Json<HashMap<String, Box<RawValue>>>>,
trigger_path: String,
job_id: Option<Uuid>,
trigger_kind: Option<JobTriggerKind>,
) -> Result<(Uuid, Option<bool>)> {
if retry.is_none() && error_handler_path.is_none() {
let run_query = RunJobQuery { job_id, ..Default::default() };
@@ -726,6 +762,7 @@ async fn trigger_script_internal(
path,
run_query,
args,
trigger_kind,
)
.await
} else {
@@ -741,6 +778,7 @@ async fn trigger_script_internal(
error_handler_args,
trigger_path,
job_id,
trigger_kind,
)
.await
}
@@ -758,6 +796,7 @@ async fn trigger_script_with_retry_and_error_handler(
error_handler_args: Option<&sqlx::types::Json<HashMap<String, Box<RawValue>>>>,
trigger_path: String,
job_id: Option<Uuid>,
trigger_kind: Option<JobTriggerKind>,
) -> Result<(Uuid, Option<bool>)> {
#[cfg(feature = "enterprise")]
check_license_key_valid().await?;
@@ -870,6 +909,7 @@ async fn trigger_script_with_retry_and_error_handler(
false,
None,
None,
trigger_kind,
)
.await?;
tx.commit().await?;
@@ -94,6 +94,7 @@ impl ListeningTrigger<WebsocketConfig> {
None,
None,
"".to_string(), // doesn't matter as no retry/error handler
Some(windmill_common::jobs::JobTriggerKind::Websocket),
)
.await
.map(|r| r.get().to_owned())?;
@@ -392,6 +393,7 @@ impl Listener for WebsocketTrigger {
error_handler_path.as_deref(),
error_handler_args.as_ref(),
format!("websocket_trigger/{}", trigger_path),
Some(windmill_common::jobs::JobTriggerKind::Websocket),
) => {
if let Ok((result, success)) = result {
if !success && !can_return_error_result {
@@ -428,6 +430,7 @@ impl Listener for WebsocketTrigger {
error_handler_args,
format!("websocket_trigger/{}", listening_trigger.path),
None,
Some(windmill_common::jobs::JobTriggerKind::Websocket),
)
.await?;
}
@@ -2,7 +2,9 @@ use std::collections::HashMap;
use crate::{
db::ApiAuthed,
triggers::trigger_helpers::{trigger_runnable_and_wait_for_raw_result_with_error_ctx, TriggerJobArgs},
triggers::trigger_helpers::{
trigger_runnable_and_wait_for_raw_result_with_error_ctx, TriggerJobArgs,
},
};
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
@@ -64,9 +66,8 @@ pub fn value_to_args_hashmap(
args: Option<&Box<RawValue>>,
) -> Result<HashMap<String, Box<RawValue>>> {
let args = if let Some(args) = args {
let args_map: Option<HashMap<String, serde_json::Value>> =
serde_json::from_str(args.get())
.map_err(|e| Error::BadRequest(format!("invalid json: {}", e)))?;
let args_map: Option<HashMap<String, serde_json::Value>> = serde_json::from_str(args.get())
.map_err(|e| Error::BadRequest(format!("invalid json: {}", e)))?;
args_map
.unwrap_or_else(HashMap::new)
@@ -112,6 +113,7 @@ pub async fn get_url_from_runnable_value(
None,
None,
"".to_string(), // doesn't matter as no retry/error handler
Some(windmill_common::jobs::JobTriggerKind::Websocket),
)
.await?;
+1
View File
@@ -66,6 +66,7 @@ pub struct FlowWithStarred {
pub starred: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lock_error_logs: Option<String>,
pub version_id: i64,
}
fn is_none_or_false(b: &Option<bool>) -> bool {
+32 -17
View File
@@ -769,7 +769,7 @@ where
}
}
pub fn get_latest_flow_version_info_for_path_from_version<
pub fn get_flow_version_info_from_version<
'a,
'e,
A: sqlx::Acquire<'e, Database = Postgres> + Send + 'a,
@@ -782,7 +782,6 @@ pub fn get_latest_flow_version_info_for_path_from_version<
async move {
// as instructed in the docstring of sqlx::Acquire
let key = (w_id.to_string(), version);
match FLOW_INFO_CACHE.get(&key) {
Some(info) => {
tracing::debug!("Using cached flow version info for {version} ({path})");
@@ -791,21 +790,37 @@ pub fn get_latest_flow_version_info_for_path_from_version<
_ => {
tracing::debug!("Fetching flow version info for {version} ({path})");
let mut conn = db.acquire().await?;
let info = sqlx::query_as!(
FlowVersionInfo,
"SELECT tag, dedicated_worker, flow_version.value->>'early_return' as early_return, flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor, (flow_version.value->>'chat_input_enabled')::boolean as chat_input_enabled, on_behalf_of_email, edited_by, flow_version.id AS version
FROM flow
INNER JOIN flow_version
ON flow_version.id = $3
WHERE flow.path = $1 and flow.workspace_id = $2",
path,
w_id,
version
)
.fetch_optional(&mut *conn)
.await?;
let flow_info =
sqlx::query_as!(
FlowVersionInfo,
r#"
SELECT
flow_version.id AS version,
flow_version.value->>'early_return' as early_return,
flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor,
(flow_version.value->>'chat_input_enabled')::boolean as chat_input_enabled,
flow.tag,
flow.dedicated_worker,
flow.on_behalf_of_email,
flow.edited_by
FROM
flow_version
INNER JOIN flow
ON flow.path = flow_version.path AND
flow.workspace_id = flow_version.workspace_id
WHERE
flow_version.workspace_id = $1 AND
flow_version.path = $2 AND
flow_version.id = $3
"#,
w_id,
path,
version,
)
.fetch_optional(&mut *conn)
.await?;
let info = utils::not_found_if_none(info, "flow", path)?;
let info = utils::not_found_if_none(flow_info, "flow", path)?;
FLOW_INFO_CACHE.insert(key, info.clone());
@@ -825,7 +840,7 @@ pub async fn get_latest_flow_version_info_for_path<'e>(
// as instructed in the docstring of sqlx::Acquire
let version =
get_latest_flow_version_id_for_path(db_authed, &db.clone(), w_id, path, use_cache).await?;
get_latest_flow_version_info_for_path_from_version(db, version, w_id, path).await
get_flow_version_info_from_version(db, version, w_id, path).await
}
async fn get_latest_flow_version_for_path<'e, E: sqlx::PgExecutor<'e>>(
+12 -5
View File
@@ -470,6 +470,7 @@ pub async fn push_init_job<'c>(
false,
None,
None,
None,
)
.await?;
inner_tx.commit().await?;
@@ -528,6 +529,7 @@ pub async fn push_periodic_bash_job<'c>(
false,
None,
None,
None,
)
.await?;
inner_tx.commit().await?;
@@ -1373,6 +1375,7 @@ async fn restart_job_if_perpetual_inner(
false,
None,
None,
None,
)
.await?;
tx.commit().await?;
@@ -1923,6 +1926,7 @@ pub async fn push_error_handler<'a, 'c, T: Serialize + Send + Sync>(
false,
None,
None,
None,
)
.await?;
tx.commit().await?;
@@ -3882,6 +3886,7 @@ pub async fn push<'c, 'd>(
// If we know there is already a debounce job, we can use this for debouncing.
// NOTE: Only works with dependency jobs triggered by relative imports
debounce_job_id_o: Option<Uuid>,
trigger_kind: Option<JobTriggerKind>,
) -> Result<(Uuid, Transaction<'c, Postgres>), Error> {
#[cfg(feature = "cloud")]
if *CLOUD_HOSTED {
@@ -5277,11 +5282,13 @@ pub async fn push<'c, 'd>(
// tracing::error!("Could not insert job_perms for job {job_id}: {err:#}");
// }
let trigger_kind = if schedule_path.is_some() {
Some(JobTriggerKind::Schedule)
} else {
None
};
let trigger_kind = trigger_kind.or_else(|| {
if schedule_path.is_some() {
Some(JobTriggerKind::Schedule)
} else {
None
}
});
let root_job = if root_job.is_some()
&& (root_job == flow_innermost_root_job.or(parent_job).or(Some(job_id)))
+5 -7
View File
@@ -17,8 +17,8 @@ use std::str::FromStr;
use windmill_common::db::Authed;
use windmill_common::ee_oss::LICENSE_KEY_VALID;
use windmill_common::flows::Retry;
use windmill_common::get_flow_version_info_from_version;
use windmill_common::get_latest_flow_version_id_for_path;
use windmill_common::get_latest_flow_version_info_for_path_from_version;
use windmill_common::jobs::check_tag_available_for_workspace_internal;
use windmill_common::jobs::JobPayload;
use windmill_common::schedule::schedule_to_user;
@@ -63,7 +63,7 @@ async fn get_schedule_metadata<'c>(
.await?;
let FlowVersionInfo { tag, on_behalf_of_email, edited_by, .. } =
get_latest_flow_version_info_for_path_from_version(
get_flow_version_info_from_version(
&mut **tx,
version,
&schedule.workspace_id,
@@ -291,16 +291,13 @@ pub async fn push_scheduled_job<'c>(
let FlowVersionInfo {
version, tag, dedicated_worker, on_behalf_of_email, edited_by, ..
} = get_latest_flow_version_info_for_path_from_version(
} = get_flow_version_info_from_version(
&mut *tx,
version,
&schedule.workspace_id,
&schedule.script_path,
)
.warn_after_seconds_with_sql(
1,
"get_latest_flow_version_info_for_path_from_version".to_string(),
)
.warn_after_seconds_with_sql(1, "get_flow_version_info_from_version".to_string())
.await?;
(
@@ -503,6 +500,7 @@ pub async fn push_scheduled_job<'c>(
false,
None,
None,
Some(windmill_common::jobs::JobTriggerKind::Schedule),
)
.warn_after_seconds_with_sql(1, "push in push_scheduled_job".to_string())
.await?;
+1
View File
@@ -471,6 +471,7 @@ async fn execute_windmill_tool(
true,
None,
None,
None,
)
.await?;
+143 -3
View File
@@ -171,6 +171,76 @@ const DOT_PATTERN: &'static str = ".";
const START_BRACKET_PATTERN: &'static str = "[\"";
const END_BRACKET_PATTERN: &'static str = "\"]";
/// Determines if we should prepend "return" to the expression
#[cfg(feature = "deno_core")]
fn should_add_return(expr: &str) -> bool {
// Trim whitespace
let trimmed = expr.trim();
// If it's empty, add return
if trimmed.is_empty() {
return true;
}
// Check if it already starts with 'return' keyword (as a statement)
// Use word boundary to avoid matching "return" in variable names
if trimmed.starts_with("return ") || trimmed.starts_with("return;") || trimmed == "return" {
return false;
}
// Check for common statement patterns that shouldn't have return prepended
let statement_prefixes = [
"const ", "let ", "var ", "if ", "if(", "for ", "for(",
"while ", "while(", "switch ", "switch(", "try ", "try{",
"throw ", "function ", "class ", "async ", "await "
];
for prefix in &statement_prefixes {
if trimmed.starts_with(prefix) {
return false;
}
}
// Check for multiple statements (contains semicolon not in a string)
// This is still not perfect but better than current logic
if contains_semicolon_outside_strings(trimmed) {
return false;
}
// Default: assume it's an expression that needs return
true
}
/// Checks if the expression contains a semicolon outside of strings
#[cfg(feature = "deno_core")]
fn contains_semicolon_outside_strings(expr: &str) -> bool {
let mut in_single_quote = false;
let mut in_double_quote = false;
let mut in_template = false;
let mut prev_char = '\0';
for ch in expr.chars() {
match ch {
'\'' if prev_char != '\\' && !in_double_quote && !in_template => {
in_single_quote = !in_single_quote;
}
'"' if prev_char != '\\' && !in_single_quote && !in_template => {
in_double_quote = !in_double_quote;
}
'`' if prev_char != '\\' && !in_single_quote && !in_double_quote => {
in_template = !in_template;
}
';' if !in_single_quote && !in_double_quote && !in_template => {
return true;
}
_ => {}
}
prev_char = ch;
}
false
}
fn try_exact_property_access(
expr: &str,
flow_input: Option<&mappable_rc::Marc<HashMap<String, Box<RawValue>>>>,
@@ -574,10 +644,10 @@ async function resource(path) {{
(String::new(), String::new())
};
let f = if expr.contains("return ") {
expr.to_string()
} else {
let f = if should_add_return(expr) {
format!("return {expr}")
} else {
expr.to_string()
};
let ctx_str = ctx
@@ -1492,4 +1562,74 @@ multiline template`";
// assert_eq!(res.0.get(), "\"\"");
// Ok(())
// }
#[test]
fn test_should_add_return() {
// Simple expressions should get return added
assert_eq!(should_add_return("5"), true);
assert_eq!(should_add_return("x + y"), true);
assert_eq!(should_add_return("foo()"), true);
assert_eq!(should_add_return("obj.property"), true);
// Object literals should get return added
assert_eq!(should_add_return("{ foo: 'bar' }"), true);
assert_eq!(should_add_return("{ a: 1, b: 2 }"), true);
assert_eq!(should_add_return("{}"), true);
// Already has return
assert_eq!(should_add_return("return 5"), false);
assert_eq!(should_add_return("return x + y"), false);
assert_eq!(should_add_return("return;"), false);
assert_eq!(should_add_return("return"), false);
// Should NOT add return for statements
assert_eq!(should_add_return("const x = 5"), false);
assert_eq!(should_add_return("let y = 10"), false);
assert_eq!(should_add_return("var z = 15"), false);
assert_eq!(should_add_return("if (x > 5) { return x; }"), false);
assert_eq!(should_add_return("for (let i = 0; i < 10; i++) {}"), false);
assert_eq!(should_add_return("while (true) {}"), false);
assert_eq!(should_add_return("function foo() {}"), false);
assert_eq!(should_add_return("throw new Error('test')"), false);
// Multiple statements with semicolons (including block statements)
assert_eq!(should_add_return("let x = 5; x + 1"), false);
assert_eq!(should_add_return("{ const x = 5; return x; }"), false);
// Edge case: "return" in a string should still get return prepended
assert_eq!(should_add_return("\"return this string\""), true);
assert_eq!(should_add_return("'return in single quotes'"), true);
assert_eq!(should_add_return("`return in template literal`"), true);
// Semicolons in strings should not trigger multi-statement detection
assert_eq!(should_add_return("\"hello; world\""), true);
assert_eq!(should_add_return("'test; string'"), true);
assert_eq!(should_add_return("`template; literal`"), true);
}
#[test]
fn test_contains_semicolon_outside_strings() {
// Semicolons outside strings
assert_eq!(contains_semicolon_outside_strings("let x = 5; x + 1"), true);
assert_eq!(contains_semicolon_outside_strings("x; y"), true);
// Semicolons inside strings (should NOT be detected)
assert_eq!(contains_semicolon_outside_strings("\"hello; world\""), false);
assert_eq!(contains_semicolon_outside_strings("'test; string'"), false);
assert_eq!(contains_semicolon_outside_strings("`template; literal`"), false);
// Mixed cases
assert_eq!(
contains_semicolon_outside_strings("let x = 'hello; world'; x"),
true
);
assert_eq!(
contains_semicolon_outside_strings("console.log(\"test; string\")"),
false
);
// No semicolons
assert_eq!(contains_semicolon_outside_strings("x + y"), false);
assert_eq!(contains_semicolon_outside_strings("foo()"), false);
}
}
@@ -3303,6 +3303,7 @@ async fn push_next_flow_job(
false,
None,
None,
None,
)
.warn_after_seconds(2)
.await?;
@@ -670,6 +670,7 @@ pub async fn trigger_dependents_to_recompute_dependencies(
false,
None,
debounce_job_id_o,
None,
)
.await?;
+1 -1
View File
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
export const VERSION = "v1.575.0";
export const VERSION = "v1.575.4";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({
+1 -1
View File
@@ -68,7 +68,7 @@ export {
// }
// });
export const VERSION = "1.575.0";
export const VERSION = "1.575.4";
export const WM_FORK_PREFIX = "wm-fork";
+6 -29
View File
@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.575.0",
"version": "1.575.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.575.0",
"version": "1.575.4",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
@@ -24,7 +24,7 @@
"@redocly/json-to-json-schema": "^0.0.1",
"@scalar/openapi-parser": "^0.15.0",
"@tanstack/svelte-table": "npm:tanstack-table-8-svelte-5@^0.1",
"@tutorlatin/svelte-tiny-virtual-list": "^3.0.2",
"@tutorlatin/svelte-tiny-virtual-list": "^3.0.16",
"@windmill-labs/svelte-dnd-action": "^0.9.44",
"@xterm/addon-fit": "^0.10.0",
"@xyflow/svelte": "^1.0.0",
@@ -3020,9 +3020,9 @@
}
},
"node_modules/@tutorlatin/svelte-tiny-virtual-list": {
"version": "3.0.15",
"resolved": "https://registry.npmjs.org/@tutorlatin/svelte-tiny-virtual-list/-/svelte-tiny-virtual-list-3.0.15.tgz",
"integrity": "sha512-ew61aZNXGf0b5X+UjbOAhiNwzI21vijhB/mtBs8bpNOVYQ50TG6Qx00t+fR5C72eGnmdzguewZ2WP6QPNOTQJg==",
"version": "3.0.16",
"resolved": "https://registry.npmjs.org/@tutorlatin/svelte-tiny-virtual-list/-/svelte-tiny-virtual-list-3.0.16.tgz",
"integrity": "sha512-JQSmhRDAFZbq2rTlzn+kFXJayi5VPLxeGjD01ZVyV2ti7PlQE/ov6rQFR1c8s7Y3B1OiTcv3oEWGi3ib69V8eQ==",
"license": "MIT",
"engines": {
"node": ">=20.17.0"
@@ -13936,29 +13936,6 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xml-utils": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.2.tgz",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.575.0",
"version": "1.575.4",
"scripts": {
"dev": "vite dev",
"build": "vite build",
@@ -89,7 +89,7 @@
"@redocly/json-to-json-schema": "^0.0.1",
"@scalar/openapi-parser": "^0.15.0",
"@tanstack/svelte-table": "npm:tanstack-table-8-svelte-5@^0.1",
"@tutorlatin/svelte-tiny-virtual-list": "^3.0.2",
"@tutorlatin/svelte-tiny-virtual-list": "^3.0.16",
"@windmill-labs/svelte-dnd-action": "^0.9.44",
"@xterm/addon-fit": "^0.10.0",
"@xyflow/svelte": "^1.0.0",
@@ -0,0 +1,84 @@
import { CancelablePromise } from './gen'
export namespace CancelablePromiseUtils {
export function then<T, U>(
promise: CancelablePromise<T>,
f: (value: T) => CancelablePromise<U>
): CancelablePromise<U> {
let promiseToBeCanceled: CancelablePromise<any> = promise
let p = new CancelablePromise<U>((resolve, reject) => {
promise
.then((value1) => {
let promise2 = f(value1)
promiseToBeCanceled = promise2
promise2.then((value2) => resolve(value2)).catch((err) => reject(err))
})
.catch((err) => reject(err))
})
p.cancel = () => promiseToBeCanceled.cancel()
return p
}
export function pure<T>(value: T): CancelablePromise<T> {
return new CancelablePromise((resolve) => resolve(value))
}
export function err<T>(error: any): CancelablePromise<T> {
return new CancelablePromise((_, reject) => reject(error))
}
export function map<T, U>(
promise: CancelablePromise<T>,
f: (value: T) => U
): CancelablePromise<U> {
return then(promise, (value) => pure(f(value)))
}
export function pipe<T>(
promise: CancelablePromise<T>,
f: (value: T) => void
): CancelablePromise<T> {
promise.then((value) => {
f(value)
})
return promise
}
export function catchErr<T, U>(
promise: CancelablePromise<T>,
f: (error: any) => CancelablePromise<U>
): CancelablePromise<T | U> {
let promiseToBeCanceled: CancelablePromise<any> = promise
let p = new CancelablePromise<T | U>((resolve, reject) => {
promise
.then((value) => resolve(value))
.catch((err) => {
let promise2 = f(err)
promiseToBeCanceled = promise2
return promise2.then((value2) => resolve(value2)).catch((err2) => reject(err2))
})
.catch((err) => reject(err))
})
p.cancel = () => promiseToBeCanceled.cancel()
return p
}
export function finallyDo<T>(promise: CancelablePromise<T>, f: () => void): CancelablePromise<T> {
promise = map(promise, (value) => (f(), value))
promise = catchErr(promise, (e) => (f(), err(e)))
return promise
}
// Calls onTimeout if the promise does not settle within timeoutMs milliseconds
export function onTimeout<T>(
promise: CancelablePromise<T>,
timeoutMs: number,
onTimeout: () => void
): CancelablePromise<T> {
let timeoutId: number | undefined = setTimeout(onTimeout, timeoutMs)
promise = finallyDo(promise, () => {
if (timeoutId !== undefined) clearTimeout(timeoutId)
})
return promise
}
}
@@ -33,6 +33,7 @@
import TextInput from './text_input/TextInput.svelte'
import { usePromise } from '$lib/svelte5Utils.svelte'
import { pollJobResult } from './jobs/utils'
import { sameTopDomainOrigin } from '$lib/cookies'
interface Props {
step?: number
@@ -239,7 +240,7 @@
function popupListener(event) {
console.log('Received oauth popup message', event)
let data = event.data
if (event.origin == null || event.origin !== window.location.origin) {
if (!sameTopDomainOrigin(event.origin, window.location.origin)) {
console.log(
'Received oauth popup message from different origin',
event.origin,
@@ -397,6 +398,7 @@
}
window.addEventListener('message', popupListener)
window.addEventListener('storage', handleStorageEvent)
console.log('opening popup', url.toString())
window.open(url.toString(), '_blank', 'popup=true')
step += 1
}
@@ -42,7 +42,7 @@
<Button variant="default" on:click={appConnect?.back ?? (() => {})}>Back</Button>
{/if}
<Button {disabled} on:click={appConnect?.next ?? (() => {})}>
<Button variant="accent" {disabled} on:click={appConnect?.next ?? (() => {})}>
{#if step == 2 && !manual}
Connect
{:else if step == 1}
+7 -3
View File
@@ -19,6 +19,7 @@
import { onDestroy, onMount } from 'svelte'
import Skeleton from './common/skeleton/Skeleton.svelte'
import Button from './common/button/Button.svelte'
import { sameTopDomainOrigin } from '$lib/cookies'
interface Props {
rd?: string | undefined
@@ -212,8 +213,9 @@
function popupListener(event) {
let data = event.data
console.log('popupListener', data, event.origin, window.location.origin)
if (event.origin !== window.location.origin) {
// console.log('popupListener', data, event.origin, window.location.origin)
if (!sameTopDomainOrigin(event.origin, window.location.origin)) {
console.log('popupListener from different origin', event.origin, window.location.origin)
return
}
@@ -261,7 +263,9 @@
console.error('Could not persist redirection to local storage', e)
}
}
let url = base + '/api/oauth/login/' + provider
let url = base + '/api/oauth/login/' + provider + (popup ? '?close=true' : '')
console.log('storeRedirect', popup, url)
if (popup) {
localStorage.setItem('closeUponLogin', 'true')
window.addEventListener('message', popupListener)
+10 -7
View File
@@ -28,6 +28,7 @@
selectedIds?: string[]
canSelect?: boolean
lastFetchWentToEnd?: boolean
totalRowsFetched: number
onPointClicked: (ids: string[]) => void
onLoadExtra: () => void
onZoom: (zoom: { min: Date; max: Date }) => void
@@ -41,6 +42,7 @@
selectedIds = $bindable([]),
canSelect = true,
lastFetchWentToEnd = false,
totalRowsFetched,
onPointClicked,
onLoadExtra,
onZoom
@@ -301,13 +303,14 @@
<!-- {JSON.stringify(jobs?.map((x) => x.started_at))} -->
<div class="relative max-h-40">
{#if !lastFetchWentToEnd}
<div class="absolute top-[-28px] left-[220px]"
><Button size="xs" color="transparent" variant="contained" on:click={() => onLoadExtra()}
>Load more <Tooltip2
>There are more jobs to load but only the first 1000 were fetched</Tooltip2
></Button
></div
>
<div class="absolute top-[-28px] left-[220px]">
<Button size="xs" color="transparent" variant="contained" on:click={() => onLoadExtra()}>
Load more
<Tooltip2>
There are more jobs to load but only the first {totalRowsFetched} were fetched
</Tooltip2>
</Button>
</div>
{/if}
<Scatter {data} options={scatterOptions} />
</div>
File diff suppressed because it is too large Load Diff
@@ -1668,7 +1668,7 @@
args={hasPreprocessor && selectedInputTab !== 'preprocessor' ? {} : args}
isDeployed={savedScript && !savedScript?.draft_only}
schema={script.schema}
hash={script.parent_hash}
runnableVersion={script.parent_hash}
onDeployTrigger={handleDeployTrigger}
/>
@@ -0,0 +1,145 @@
<script lang="ts">
import { User, UserRoundX } from 'lucide-svelte'
import { enterpriseLicense, userStore } from '$lib/stores'
import { base } from '$app/paths'
import { page } from '$app/state'
import Login from '$lib/components/Login.svelte'
import { isCloudHosted } from '$lib/cloud'
import { Alert, Skeleton } from '$lib/components/common'
import { WindmillIcon } from '$lib/components/icons'
import { onMount, setContext } from 'svelte'
import { IS_APP_PUBLIC_CONTEXT_KEY, type EditorBreakpoint } from '../types'
import { UserService, type AppWithLastVersion, type GlobalWhoamiResponse } from '$lib/gen'
import { urlParamsToObject } from '$lib/utils'
import { goto } from '$app/navigation'
import AppPreview from './AppPreview.svelte'
import { twMerge } from 'tailwind-merge'
import { writable } from 'svelte/store'
let {
notExists,
noPermission,
jwtError,
onLoginSuccess,
app,
workspace
}: {
notExists: boolean
noPermission: boolean
jwtError: boolean
onLoginSuccess: () => void
app: (AppWithLastVersion & { value: any }) | undefined
workspace: string | undefined
} = $props()
setContext(IS_APP_PUBLIC_CONTEXT_KEY, true)
const breakpoint = writable<EditorBreakpoint>('lg')
const darkMode =
window.localStorage.getItem('dark-mode') ??
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
if (darkMode === 'dark') {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
let globalUser = $state<GlobalWhoamiResponse | undefined>(undefined)
async function loadGlobalUser() {
try {
globalUser = await UserService.globalWhoami()
} catch (error) {
console.error(error)
}
// const user = await fetch('/api/global/user')
// console.log(user)
}
onMount(() => {
// this is to avoid loading global user if the userStore is set at loading
setTimeout(() => {
if ($userStore) return
loadGlobalUser()
}, 2000)
})
</script>
<div
class="z-50 text-xs fixed bottom-1 right-2 {$enterpriseLicense && !isCloudHosted()
? 'transition-opacity delay-1000 duration-1000 opacity-20 hover:delay-0 hover:opacity-100'
: ''}"
>
<a href="https://windmill.dev" class="whitespace-nowrap text-primary inline-flex items-center"
>Powered by &nbsp;<WindmillIcon />&nbsp;Windmill</a
>
</div>
{#snippet userInfo(child)}
<div class="flex gap-1 items-center"><User size={14} />{child}</div>
{/snippet}
<div class="z-50 text-2xs text-primary absolute top-3 left-2"
>{#if $userStore}
{@render userInfo($userStore.username)}
{:else if globalUser}
{@render userInfo(globalUser.email)}
{:else}<UserRoundX size={14} />{/if}
</div>
{#if notExists}
<div class="px-4 mt-20"
><Alert type="error" title="Not found"
>There was an error loading the app, is the url correct? <a href={base}>Go to Windmill</a>
</Alert></div
>
{:else if noPermission}
<div class="px-4 mt-20 w-full text-center font-bold text-xl"> This app requires read access </div>
<div class="text-center mt-8 text-sm text-primary">
{#if $userStore}You are logged in but have no read access to this app{:else if globalUser && workspace}
You are logged in but are not a member of the workspace <span class="text-xl font-bold"
>{workspace}</span
> this app is part of
{:else}You must be logged in and have read access to this app{/if}</div
>
<div class="px-2 mx-auto mt-20 max-w-xl w-full">
{#if !jwtError}
<Login {onLoginSuccess} popup rd={page.url.toString()} />
{/if}
</div>
{:else if app}
{#key app}
<div
class={twMerge(
'min-h-screen h-full w-full flex',
app?.value?.['css']?.['app']?.['viewer']?.class,
'wm-app-viewer'
)}
style={app?.value?.['css']?.['app']?.['viewer']?.style}
>
<AppPreview
noBackend={false}
context={{
email: $userStore?.email,
name: $userStore?.name,
groups: $userStore?.groups,
username: $userStore?.username,
query: urlParamsToObject(page.url.searchParams),
hash: page.url.hash.substring(1)
}}
{workspace}
summary={app.summary}
app={app.value}
appPath={app.path}
{breakpoint}
policy={app.policy}
isEditor={false}
replaceStateFn={(path) => goto(path)}
gotoFn={(path, opt) => goto(path, opt)}
/>
</div>
{/key}
{:else}
<Skeleton layout={[[4], 0.5, [50]]} />
{/if}
@@ -26,22 +26,23 @@
UserService,
ScriptService,
FlowService,
AppService
AppService,
CancelError
} from '$lib/gen'
import { userStore, workspaceStore } from '$lib/stores'
import { ChevronDown, Loader2, RefreshCcw } from 'lucide-svelte'
import { onDestroy, tick, untrack } from 'svelte'
import { onDestroy, untrack } from 'svelte'
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte'
import Select from '../select/Select.svelte'
import { usePromise } from '$lib/svelte5Utils.svelte'
import { safeSelectItems } from '../select/utils.svelte'
import { CancelablePromiseUtils } from '$lib/cancelable-promise-utils'
import { sendUserToast } from '$lib/toast'
let usernames: string[] | undefined = $state()
let resources = usePromise(() => loadResources($workspaceStore!), { loadInit: false })
let loading: boolean = $state(false)
let page: number | undefined = undefined
interface Props {
logs?: AuditLog[]
@@ -55,6 +56,7 @@
resource?: string | undefined
actionKind?: ActionKind | 'all'
scope?: undefined | 'all_workspaces' | 'instance'
loading?: boolean
}
let {
@@ -68,7 +70,8 @@
operation = $bindable(),
resource = $bindable() as string | undefined,
actionKind = $bindable(undefined),
scope = $bindable(undefined)
scope = $bindable(undefined),
loading = $bindable(false)
}: Props = $props()
$effect.pre(() => {
@@ -86,50 +89,45 @@
}
})
async function loadLogs(
username: string | undefined,
page: number | undefined,
perPage: number | undefined,
before: string | undefined,
after: string | undefined,
operation: string | undefined,
resource: string | undefined,
actionKind: ActionKind | undefined | 'all',
scope: undefined | 'all_workspaces' | 'instance'
): Promise<void> {
function loadLogs() {
loading = true
if (username == 'all') {
username = undefined
}
if (operation == 'all' || operation == '') {
operation = undefined
}
let username_ = username == 'all' ? undefined : username
let operation_ = operation == 'all' || operation == '' ? undefined : operation
let actionKind_ = actionKind == 'all' ? undefined : actionKind
let resource_ = resource == 'all' || resource == '' ? undefined : resource
// @ts-ignore
if (actionKind == 'all' || actionKind == '') {
actionKind = undefined
}
if (resource == 'all' || resource == '') {
resource = undefined
}
logs = await AuditService.listAuditLogs({
let _promise = AuditService.listAuditLogs({
workspace: scope === 'instance' ? 'global' : $workspaceStore!,
page,
page: pageIndex,
perPage,
before,
after,
username,
operation,
resource,
actionKind,
username: username_,
operation: operation_,
resource: resource_,
actionKind: actionKind_,
allWorkspaces: scope === 'all_workspaces'
})
hasMore = logs.length > 0 && logs.length === perPage
loading = false
let promise = CancelablePromiseUtils.map(_promise, (value) => {
logs = value
hasMore = !logs || (logs.length > 0 && logs.length === perPage)
loading = false
})
promise = CancelablePromiseUtils.onTimeout(promise, 4000, () => {
sendUserToast(
'Loading audit logs is taking longer than expected...',
true,
perPage > 25
? [{ label: 'Reduce to 25 items per page', callback: () => (perPage = 25) }]
: []
)
})
promise = CancelablePromiseUtils.catchErr(promise, (e) => {
if (e instanceof CancelError) return CancelablePromiseUtils.pure<void>(undefined)
return CancelablePromiseUtils.err<void>(e)
})
return promise
}
async function loadUsers() {
@@ -139,17 +137,7 @@
: [$userStore?.username ?? '']
}
let initialLoad = true
function refreshLogs() {
loadUsers()
resources.refresh()
loadLogs(username, page, perPage, before, after, operation, resource, actionKind, scope)
tick().then(() => {
initialLoad = false
})
}
function updateLogs() {
function updateQueryParams() {
const queryParams: string[] = []
function addQueryParam(key: string, value: string | number | undefined | null) {
@@ -159,7 +147,7 @@
}
addQueryParam('username', username)
addQueryParam('page', page)
addQueryParam('page', pageIndex)
addQueryParam('perPage', perPage)
addQueryParam('before', before)
addQueryParam('after', after)
@@ -172,25 +160,6 @@
}
const query = '?' + queryParams.join('&')
goto(query, { replaceState: true, keepFocus: true })
loadLogs(username, page, perPage, before, after, operation, resource, actionKind, scope)
}
function updateQueryParams() {
if (initialLoad) {
return
}
page = 1
pageIndex = 1
updateLogs()
}
function updatePageQueryParams(pageIndex?: number | undefined) {
if (initialLoad) {
return
}
page = pageIndex
updateLogs()
}
window.addEventListener('popstate', handlePopState)
@@ -307,18 +276,22 @@
WORKSPACES_DELETE: 'workspaces.delete'
}
let refresh = $state(1)
$effect(() => {
$workspaceStore && refresh && untrack(() => refreshLogs())
})
let refresh = $state(0)
let lastRefresh = $state(-1)
// observe all the variables that should trigger an update
$effect(() => {
;[username, perPage, before, after, operation, resource, actionKind, scope]
updateQueryParams()
})
// observe the pageIndex variable that should trigger an update
$effect(() => {
updatePageQueryParams(pageIndex)
;[refresh, username, perPage, before, after, operation, resource, actionKind, scope, pageIndex]
return untrack(() => {
if (refresh !== lastRefresh) {
loadUsers()
resources.refresh()
lastRefresh = refresh
}
updateQueryParams()
let promise = loadLogs()
return () => promise?.cancel()
})
})
</script>
@@ -390,25 +363,30 @@
<div class="flex gap-1 relative w-full">
<span class="text-xs absolute font-semibold text-emphasis -top-4">Username</span>
<select bind:value={username}>
{#if usernames}
{#if $userStore?.is_admin || $userStore?.is_super_admin}
<option selected>all</option>
{/if}
{#each usernames as e}
{#if e == username || $userStore?.is_admin || $userStore?.is_super_admin}
<option>{e}</option>
{:else}
<option disabled>{e}</option>
{/if}
{/each}
{/if}
</select>
<Select
bind:value={username}
class="w-full"
disablePortal
RightIcon={ChevronDown}
items={usernames
? [
...($userStore?.is_admin || $userStore?.is_super_admin
? [{ value: 'all', label: 'all' }]
: []),
...usernames.map((e) => ({
value: e,
label: e,
disabled: e !== username && !$userStore?.is_admin && !$userStore?.is_super_admin
}))
]
: []}
/>
</div>
<div class="flex gap-1 relative w-full">
<span class="text-xs absolute font-semibold text-emphasis -top-4">Resource</span>
<Select
disablePortal
onCreateItem={(r) => (resources.value?.push(r), (resource = r))}
createText="Press enter to use this value"
bind:value={resource}
@@ -423,6 +401,7 @@
<span class="text-xs absolute font-semibold text-emphasis -top-4">Operation</span>
<Select
disablePortal
bind:value={operation}
items={['all', ...Object.values(operations)].map((r) => ({ value: r, label: r }))}
inputClass="dark:!bg-gray-700"
@@ -434,12 +413,19 @@
<div class="flex gap-1 relative w-full">
<span class="text-xs absolute font-semibold text-emphasis -top-4">Action</span>
<select class="!truncate" bind:value={actionKind}>
<option selected value="all">all</option>
{#each ['Create', 'Update', 'Delete', 'Execute'] as e}
<option value={e.toLocaleLowerCase()}>{e}</option>
{/each}
</select>
<Select
class="w-full"
bind:value={actionKind}
disablePortal
RightIcon={ChevronDown}
items={[
{ value: 'all', label: 'all' },
{ value: 'create', label: 'Create' },
{ value: 'update', label: 'Update' },
{ value: 'delete', label: 'Delete' },
{ value: 'execute', label: 'Execute' }
]}
/>
</div>
<div class="flex flex-row gap-1">
@@ -4,7 +4,7 @@
import { displayDate } from '$lib/utils'
import { onMount, tick } from 'svelte'
import Button from '../common/button/Button.svelte'
import { ChevronLeft, ChevronRight, ListFilterPlus } from 'lucide-svelte'
import { ChevronLeft, ChevronRight, ListFilterPlus, Loader2 } from 'lucide-svelte'
import VirtualList from '@tutorlatin/svelte-tiny-virtual-list'
import { twMerge } from 'tailwind-merge'
@@ -19,6 +19,7 @@
usernameFilter?: string | undefined
resourceFilter?: string | undefined
showWorkspace?: boolean
loading?: boolean
onselect?: (id: number) => void
}
@@ -33,7 +34,8 @@
usernameFilter = $bindable(),
resourceFilter = $bindable(),
showWorkspace = false,
onselect
onselect,
loading
}: Props = $props()
function groupLogsByDay(logs: AuditLog[]): Record<string, AuditLog[]> {
@@ -120,6 +122,7 @@
}
return 'gray'
}
let height = $derived(tableHeight - headerHeight - footerHeight)
</script>
<svelte:window onresize={() => computeHeight()} />
@@ -139,12 +142,22 @@
<div class="w-2/12">Resource</div>
</div>
</div>
{#if logs?.length == 0}
<div class="text-xs text-secondary p-8"> No logs found for the selected filters. </div>
{#if loading}
<div style="height: {height}px;" class="flex justify-center items-center">
<Loader2 class="animate-spin" />
</div>
{:else if !logs?.length}
<div
class="text-xs text-secondary p-8 flex justify-center items-center"
style="height: {height}px;"
>
No logs found for the selected filters.
</div>
{:else}
<VirtualList
width="100%"
height={tableHeight - headerHeight - footerHeight}
{height}
itemCount={flatLogs?.length ?? 0}
itemSize={(index) => {
if (flatLogs?.[index]?.type === 'date') {
@@ -550,7 +550,7 @@
)
</script>
<div class="p-4 bg-surface mb-4 h-full">
<div class="p-4 bg-surface mb-4 h-full text-xs">
{#if logs.length === 0}
<div class="text-center py-8 text-secondary"> No audit logs to display </div>
{:else if !groupedData || groupedData.status === 'loading'}
+163 -137
View File
@@ -6,7 +6,9 @@
type CompletedJob,
type ExtendedJobs,
ConcurrencyGroupsService,
type ObscuredJob
type ObscuredJob,
CancelablePromise,
CancelError
} from '$lib/gen'
import { sendUserToast } from '$lib/toast'
@@ -14,6 +16,8 @@
import { tweened, type Tweened } from 'svelte/motion'
import { subtractDaysFromDateString } from '$lib/utils'
import type { JobTriggerType } from '../triggers/utils'
import { CancelablePromiseUtils } from '$lib/cancelable-promise-utils'
interface Props {
jobs: Job[] | undefined
@@ -27,6 +31,7 @@
showFutureJobs?: boolean
argFilter: string | undefined
resultFilter?: string | undefined
jobTriggerKind?: JobTriggerType | undefined
schedulePath?: string | undefined
jobKindsCat?: string | undefined
minTs?: string | undefined
@@ -49,7 +54,7 @@
allWorkspaces?: boolean
computeMinAndMax: (() => { minTs: string; maxTs: string | undefined } | undefined) | undefined
lookback?: number
perPage?: number | undefined
perPage?: number
allowWildcards?: boolean
}
@@ -66,6 +71,7 @@
showFutureJobs = true,
argFilter,
resultFilter = undefined,
jobTriggerKind = undefined,
schedulePath = undefined,
jobKindsCat = undefined,
minTs = $bindable(undefined),
@@ -87,7 +93,7 @@
allWorkspaces = false,
computeMinAndMax,
lookback = 0,
perPage = undefined,
perPage = $bindable(1000),
allowWildcards = false
}: Props = $props()
let intervalId: number | undefined = $state()
@@ -95,7 +101,23 @@
function onParamChanges() {
resetJobs()
loadJobsIntern(true)
let promise = loadJobsIntern(true)
promise = CancelablePromiseUtils.onTimeout(promise, 4000, () => {
sendUserToast(
'Loading jobs is taking longer than expected...',
true,
perPage > 25
? [{ label: 'Reduce to 25 items per page', callback: () => (perPage = 25) }]
: []
)
})
promise = CancelablePromiseUtils.catchErr(promise, (e) => {
if (e instanceof CancelError) {
return CancelablePromiseUtils.pure<void>(undefined)
}
return CancelablePromiseUtils.err(e)
})
return promise
}
function computeJobKinds(jobKindsCat: string | undefined): string {
@@ -162,123 +184,121 @@
const minCreated = new Date(new Date(ts).getTime() - 1).toISOString()
let olderJobs = await fetchJobs(minCreated, minTs, undefined)
jobs = jobs.concat(olderJobs)
jobs = jobs?.concat(olderJobs)
computeCompletedJobs()
return olderJobs?.length < 1000
return olderJobs?.length < perPage
}
return false
}
async function fetchJobs(
function fetchJobs(
completedBefore: string | undefined,
completedAfter: string | undefined,
createdAfterQueue: string | undefined
): Promise<Job[]> {
): CancelablePromise<Job[]> {
loadingFetch = true
try {
let scriptPathStart = folder === null || folder === '' ? undefined : `f/${folder}/`
let scriptPathExact = path === null || path === '' ? undefined : path
return JobService.listJobs({
workspace: $workspaceStore!,
completedBefore,
completedAfter,
createdAfterQueue,
schedulePath,
scriptPathExact,
createdBy: user === null || user === '' ? undefined : user,
scriptPathStart: scriptPathStart,
jobKinds: jobKindsCat == 'all' || jobKinds == '' ? undefined : jobKinds,
success: success == 'success' ? true : success == 'failure' ? false : undefined,
running:
success == 'running' || success == 'suspended'
? true
: success == 'waiting'
? false
: undefined,
isSkipped: showSkipped ? undefined : false,
// isFlowStep: jobKindsCat != 'all' ? false : undefined,
hasNullParent: jobKindsCat != 'all' ? true : undefined,
label: label === null || label === '' ? undefined : label,
tag: tag === null || tag === '' ? undefined : tag,
worker: worker === null || worker === '' ? undefined : worker,
isNotSchedule: showSchedules == false ? true : undefined,
suspended: success == 'waiting' ? false : success == 'suspended' ? true : undefined,
scheduledForBeforeNow:
showFutureJobs == false || success == 'waiting' || success == 'suspended'
? true
let scriptPathStart = folder === null || folder === '' ? undefined : `f/${folder}/`
let scriptPathExact = path === null || path === '' ? undefined : path
let promise = JobService.listJobs({
workspace: $workspaceStore!,
completedBefore,
completedAfter,
createdAfterQueue,
schedulePath,
scriptPathExact,
createdBy: user === null || user === '' ? undefined : user,
scriptPathStart: scriptPathStart,
jobKinds: jobKindsCat == 'all' || jobKinds == '' ? undefined : jobKinds,
success: success == 'success' ? true : success == 'failure' ? false : undefined,
running:
success == 'running' || success == 'suspended'
? true
: success == 'waiting'
? false
: undefined,
args:
argFilter && argFilter != '{}' && argFilter != '' && argError == ''
? argFilter
: undefined,
result:
resultFilter && resultFilter != '{}' && resultFilter != '' && resultError == ''
? resultFilter
: undefined,
allWorkspaces: allWorkspaces ? true : undefined,
perPage,
allowWildcards: allowWildcards ? true : undefined
})
} catch (e) {
isSkipped: showSkipped ? undefined : false,
// isFlowStep: jobKindsCat != 'all' ? false : undefined,
hasNullParent: jobKindsCat != 'all' ? true : undefined,
label: label === null || label === '' ? undefined : label,
tag: tag === null || tag === '' ? undefined : tag,
worker: worker === null || worker === '' ? undefined : worker,
isNotSchedule: showSchedules == false ? true : undefined,
suspended: success == 'waiting' ? false : success == 'suspended' ? true : undefined,
scheduledForBeforeNow:
showFutureJobs == false || success == 'waiting' || success == 'suspended'
? true
: undefined,
args:
argFilter && argFilter != '{}' && argFilter != '' && argError == '' ? argFilter : undefined,
result:
resultFilter && resultFilter != '{}' && resultFilter != '' && resultError == ''
? resultFilter
: undefined,
triggerKind: jobTriggerKind,
allWorkspaces: allWorkspaces ? true : undefined,
perPage,
allowWildcards: allowWildcards ? true : undefined
})
promise = CancelablePromiseUtils.catchErr(promise, (e) => {
if (e instanceof CancelError) return CancelablePromiseUtils.err(e)
sendUserToast('There was an issue loading jobs, see browser console for more details', true)
console.error(e)
return []
} finally {
return CancelablePromiseUtils.pure([] as Job[])
})
CancelablePromiseUtils.pipe(promise, () => {
loadingFetch = false
}
})
return promise
}
async function fetchExtendedJobs(
function fetchExtendedJobs(
concurrencyKey: string | null,
createdBeforeQueue: string | undefined,
completedAfter: string | undefined
): Promise<ExtendedJobs> {
): CancelablePromise<ExtendedJobs> {
loadingFetch = true
try {
return ConcurrencyGroupsService.listExtendedJobs({
rowLimit: 1000,
concurrencyKey: concurrencyKey == null || concurrencyKey == '' ? undefined : concurrencyKey,
workspace: $workspaceStore!,
completedAfter,
createdBeforeQueue: createdBeforeQueue,
// createdOrStartedBefore: startedBefore,
// createdOrStartedAfter: startedAfter,
// createdOrStartedAfterCompletedJobs: startedAfterCompletedJobs,
schedulePath,
scriptPathExact: path === null || path === '' ? undefined : path,
createdBy: user === null || user === '' ? undefined : user,
scriptPathStart: folder === null || folder === '' ? undefined : `f/${folder}/`,
jobKinds: jobKindsCat == 'all' || jobKinds == '' ? undefined : jobKinds,
success: success == 'success' ? true : success == 'failure' ? false : undefined,
running: success == 'running' ? true : undefined,
isSkipped: showSkipped ? undefined : false,
isFlowStep: jobKindsCat != 'all' ? false : undefined,
label: label === null || label === '' ? undefined : label,
tag: tag === null || tag === '' ? undefined : tag,
isNotSchedule: showSchedules == false ? true : undefined,
scheduledForBeforeNow: showFutureJobs == false ? true : undefined,
args:
argFilter && argFilter != '{}' && argFilter != '' && argError == ''
? argFilter
: undefined,
result:
resultFilter && resultFilter != '{}' && resultFilter != '' && resultError == ''
? resultFilter
: undefined,
allWorkspaces: allWorkspaces ? true : undefined,
perPage,
allowWildcards
})
} catch (e) {
let promise = ConcurrencyGroupsService.listExtendedJobs({
rowLimit: perPage,
concurrencyKey: concurrencyKey == null || concurrencyKey == '' ? undefined : concurrencyKey,
workspace: $workspaceStore!,
completedAfter,
createdBeforeQueue: createdBeforeQueue,
// createdOrStartedBefore: startedBefore,
// createdOrStartedAfter: startedAfter,
// createdOrStartedAfterCompletedJobs: startedAfterCompletedJobs,
schedulePath,
scriptPathExact: path === null || path === '' ? undefined : path,
createdBy: user === null || user === '' ? undefined : user,
scriptPathStart: folder === null || folder === '' ? undefined : `f/${folder}/`,
jobKinds: jobKindsCat == 'all' || jobKinds == '' ? undefined : jobKinds,
success: success == 'success' ? true : success == 'failure' ? false : undefined,
running: success == 'running' ? true : undefined,
isSkipped: showSkipped ? undefined : false,
isFlowStep: jobKindsCat != 'all' ? false : undefined,
label: label === null || label === '' ? undefined : label,
tag: tag === null || tag === '' ? undefined : tag,
isNotSchedule: showSchedules == false ? true : undefined,
scheduledForBeforeNow: showFutureJobs == false ? true : undefined,
args:
argFilter && argFilter != '{}' && argFilter != '' && argError == '' ? argFilter : undefined,
result:
resultFilter && resultFilter != '{}' && resultFilter != '' && resultError == ''
? resultFilter
: undefined,
triggerKind: jobTriggerKind,
allWorkspaces: allWorkspaces ? true : undefined,
perPage,
allowWildcards
})
promise = CancelablePromiseUtils.catchErr(promise, (e) => {
sendUserToast('There was an issue loading jobs, see browser console for more details', true)
console.error(e)
return {
jobs: [],
obscured_jobs: []
}
} finally {
return CancelablePromiseUtils.pure({ jobs: [], obscured_jobs: [] } as ExtendedJobs)
})
promise = CancelablePromiseUtils.pipe(promise, () => {
loadingFetch = false
}
})
return promise
}
export async function loadJobs(
@@ -303,54 +323,58 @@
intervalId && clearInterval(intervalId)
intervalId = setInterval(syncer, refreshRate)
}
async function loadJobsIntern(shouldGetCount?: boolean): Promise<void> {
function loadJobsIntern(shouldGetCount?: boolean): CancelablePromise<void> {
if (shouldGetCount) {
getCount()
}
loading = true
try {
// Extend MinTs to fetch jobs mefore minTs and show a correct concurrency graph
// TODO: when an ended_at column is created on the completed_job table,
// lookback won't be needed anymore (just filter ended_at > minTs instead
const extendedMinTs = subtractDaysFromDateString(minTs, lookback)
if (concurrencyKey == null || concurrencyKey === '') {
let newJobs = await fetchJobs(maxTs, undefined, extendedMinTs)
// Extend MinTs to fetch jobs mefore minTs and show a correct concurrency graph
// TODO: when an ended_at column is created on the completed_job table,
// lookback won't be needed anymore (just filter ended_at > minTs instead
const extendedMinTs = subtractDaysFromDateString(minTs, lookback)
if (concurrencyKey == null || concurrencyKey === '') {
return CancelablePromiseUtils.map(fetchJobs(maxTs, undefined, extendedMinTs), (newJobs) => {
extendedJobs = { jobs: newJobs, obscured_jobs: [] } as ExtendedJobs
// Filter on minTs here and not in the backend
// to get enough data for the concurrency graph
jobs = sortMinDate(minTs, newJobs)
externalJobs = []
} else {
extendedJobs = await fetchExtendedJobs(concurrencyKey, maxTs, extendedMinTs)
const newJobs = extendedJobs.jobs
const newExternalJobs = extendedJobs.obscured_jobs
computeCompletedJobs()
loading = false
})
} else {
return CancelablePromiseUtils.map(
fetchExtendedJobs(concurrencyKey, maxTs, extendedMinTs),
(newExtendedJobs) => {
extendedJobs = newExtendedJobs
const newJobs = newExtendedJobs.jobs
const newExternalJobs = newExtendedJobs.obscured_jobs
// Filter on minTs here and not in the backend
// to get enough data for the concurrency graph
if (minTs != undefined) {
const minDate = new Date(minTs)
jobs = newJobs.filter((x) =>
x.started_at
? new Date(x.started_at) > minDate
: x.created_at
? new Date(x.created_at) > minDate
: false
)
externalJobs = computeExternalJobs(
newExternalJobs.filter((x) => x.started_at && new Date(x.started_at) > minDate)
)
} else {
jobs = newJobs
externalJobs = computeExternalJobs(newExternalJobs)
// Filter on minTs here and not in the backend
// to get enough data for the concurrency graph
if (minTs != undefined) {
const minDate = new Date(minTs)
jobs = newJobs.filter((x) =>
x.started_at
? new Date(x.started_at) > minDate
: x.created_at
? new Date(x.created_at) > minDate
: false
)
externalJobs = computeExternalJobs(
newExternalJobs.filter((x) => x.started_at && new Date(x.started_at) > minDate)
)
} else {
jobs = newJobs
externalJobs = computeExternalJobs(newExternalJobs)
}
computeCompletedJobs()
loading = false
}
}
computeCompletedJobs()
} catch (err) {
sendUserToast(`There was a problem fetching jobs: ${err}`, true)
console.error(JSON.stringify(err))
)
}
loading = false
}
async function getCount() {
@@ -557,10 +581,12 @@
showSchedules,
allWorkspaces,
argFilter,
resultFilter
resultFilter,
jobTriggerKind,
perPage
]
untrack(() => onParamChanges())
let p = untrack(() => onParamChanges())
return () => p.cancel()
})
$effect(() => {
;[autoRefresh, refreshRate]
@@ -4,12 +4,13 @@
import { createEventDispatcher } from 'svelte'
interface Props {
minTs: string | undefined;
maxTs: string | undefined;
loading?: boolean;
selectedManualDate?: number;
loadText?: string | undefined;
serviceLogsChoices?: boolean;
minTs: string | undefined
maxTs: string | undefined
loading?: boolean
selectedManualDate?: number
loadText?: string | undefined
serviceLogsChoices?: boolean
numberOfLastJobsToFetch?: number
}
let {
@@ -18,8 +19,9 @@
loading = false,
selectedManualDate = $bindable(0),
loadText = undefined,
serviceLogsChoices = false
}: Props = $props();
serviceLogsChoices = false,
numberOfLastJobsToFetch = 1000
}: Props = $props()
export function computeMinMax(): { minTs: string; maxTs: string | undefined } | undefined {
return manualDates[selectedManualDate].computeMinMax()
@@ -75,7 +77,7 @@
let manualDates = $derived([
{
label: loadText ?? 'Last 1000 runs',
label: loadText ?? `Last ${numberOfLastJobsToFetch} runs`,
computeMinMax: () => {
return undefined
}
@@ -2,7 +2,6 @@
import { Button } from '../common'
import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte'
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
import Tooltip from '../Tooltip.svelte'
import { CircleAlert, CircleCheck, Hourglass, ListFilterPlus, CirclePlay, X } from 'lucide-svelte'
import JsonEditor from '../JsonEditor.svelte'
import Toggle from '../Toggle.svelte'
@@ -18,6 +17,7 @@
import DropdownSelect from '../DropdownSelect.svelte'
import TooltipV2 from '$lib/components/meltComponents/Tooltip.svelte'
import TextInput from '../text_input/TextInput.svelte'
import { jobTriggerTypes, triggerDisplayNamesMap, type JobTriggerType } from '../triggers/utils'
interface Props {
// Filters
@@ -31,6 +31,7 @@
argFilter: string
argError: string
resultFilter: string
jobTriggerKind: JobTriggerType | undefined
resultError: string
jobKindsCat: string
user?: string | null
@@ -67,6 +68,7 @@
argFilter = $bindable(),
argError = $bindable(),
resultFilter = $bindable(),
jobTriggerKind = $bindable(),
resultError = $bindable(),
jobKindsCat = $bindable(),
user = $bindable(null),
@@ -949,26 +951,40 @@
{/if}
<Label label="Show skipped flows">
<span class="text-2xs text-secondary">
Skipped flows are flows that did an early break
</span>
<div class="flex flex-row gap-1 items-center">
<Toggle size="sm" bind:checked={showSkipped} />
</div>
{#snippet header()}
<Tooltip>Skipped flows are flows that did an early break</Tooltip>
{/snippet}
</Label>
<Label label="Filter by trigger kind">
<span class="text-2xs text-secondary">
{`Filter by what kind of trigger started the run.`}
</span>
<Select
items={jobTriggerTypes.map((value) => ({
label: triggerDisplayNamesMap[value],
value
}))}
bind:value={jobTriggerKind}
clearable
/>
</Label>
<div class="flex flex-col gap-6">
<Label label="Filter by args">
<JsonEditor bind:error={argError} bind:code={copyArgFilter} />
<span class="text-2xs text-secondary">
{`Filter by a json being a subset of the args/result. Try '\{"foo": "bar"\}'`}
</span>
<JsonEditor bind:error={argError} bind:code={copyArgFilter} />
</Label>
<Label label="Filter by result">
<JsonEditor bind:error={resultError} bind:code={copyResultFilter} />
<span class="text-2xs text-secondary">
{`Filter by a json being a subset of the args/result. Try '\{"foo": "bar"\}'`}
</span>
<JsonEditor bind:error={resultError} bind:code={copyResultFilter} />
</Label>
</div>
@@ -22,6 +22,7 @@
activeLabel?: string | null
// const loadMoreQuantity: number = 100
lastFetchWentToEnd?: boolean
perPage?: number
}
let {
@@ -33,7 +34,8 @@
selectedIds = $bindable([]),
selectedWorkspace = $bindable(undefined),
activeLabel = null,
lastFetchWentToEnd = $bindable(false)
lastFetchWentToEnd = $bindable(false),
perPage = 1000
}: Props = $props()
function getTime(job: Job): string | undefined {
@@ -116,7 +118,7 @@
let tableHeight: number = $state(0)
let headerHeight: number = $state(0)
let containerWidth: number = $state(0)
// const MAX_ITEMS = 1000
// const MAX_ITEMS = perPage
/*
function infiniteHandler({ detail: { loaded, error, complete } }) {
@@ -139,7 +141,7 @@
return ''
}
const jc = jobCount
const isTruncated = jc >= 1000 && !lastFetchWentToEnd
const isTruncated = jc >= perPage && !lastFetchWentToEnd
return `${jc}${isTruncated ? '+' : ''} job${jc != 1 ? 's' : ''}`
}
@@ -338,13 +340,15 @@
{/snippet}
{#snippet footer()}
<div
>{#if !lastFetchWentToEnd && jobs && jobs.length >= 1000}
>{#if !lastFetchWentToEnd && jobs && jobs.length >= perPage}
<button
class="text-xs text-blue-600 text-center w-full pb-2"
class="text-xs text-accent text-center w-full pb-2"
onclick={() => {
dispatch('loadExtra')
}}>Load next 1000 jobs</button
}}
>
Load next {perPage} jobs
</button>
{/if}</div
>
{/snippet}
@@ -1,4 +1,7 @@
<script lang="ts" generics="Item extends { label?: string; value: any; subtitle?: string }">
<script
lang="ts"
generics="Item extends { label?: string; value: any; subtitle?: string; disabled?: boolean }"
>
import { clickOutside } from '$lib/utils'
import { twMerge } from 'tailwind-merge'
import CloseButton from '../common/CloseButton.svelte'
@@ -195,11 +195,12 @@
: item.value === value
? 'bg-surface-hover/60'
: 'hover:bg-surface-hover',
itemButtonWrapperClasses
itemButtonWrapperClasses,
item.disabled ? 'cursor-not-allowed text-disabled' : ''
)}
onclick={(e) => {
e.stopImmediatePropagation()
onSelectValue(item)
if (!item.disabled) onSelectValue(item)
}}
>
{@render startSnippet?.({ item, close: () => (open = false) })}
@@ -44,6 +44,7 @@ export type ProcessedItem<T> = {
label: string
value: T
subtitle?: string
disabled?: boolean
}
export function getLabel<T>(item: { label?: string; value: T } | undefined): string | undefined {
@@ -51,7 +52,7 @@ export function getLabel<T>(item: { label?: string; value: T } | undefined): str
if (item.label) return item.label
if (typeof item.value === 'string') return item.value
if (typeof item.value == 'number' || typeof item.value == 'boolean') return item.value.toString()
if (item.value == null) { return undefined }
if (item.value == null) return undefined
return JSON.stringify(item.value)
}
@@ -272,11 +272,7 @@
<div class="flex flex-col gap-1 mb-4 w-full">
<div class="flex justify-center w-full">
<div class="relative h-fit">
<AnimatedButton
animate={captureInfo.active}
wrapperClasses={captureInfo.active ? 'm-[-2px]' : ''}
baseRadius="7px"
>
<AnimatedButton animate={captureInfo.active} baseRadius="6px">
<Button
size="xs"
on:click={() => dispatch('captureToggle', {})}
@@ -287,6 +283,7 @@
? { icon: CircleStop }
: { icon: CaptureIcon, props: { variant: 'redDot' } }}
loading={captureLoading}
btnClasses={captureInfo.active ? '!bg-surface' : ''}
>
{#if captureInfo.active}
<p class="w-24" transition:fade={{ duration: 300 }}>Stop capturing</p>
@@ -41,7 +41,7 @@
newItem?: boolean
currentPath: string
fakeInitialPath?: string
hash?: string | undefined
runnableVersion?: string | undefined
args?: Record<string, any>
initialPath: string
isFlow: boolean
@@ -59,7 +59,7 @@
newItem = false,
currentPath,
fakeInitialPath = '',
hash = undefined,
runnableVersion = undefined,
args = {},
initialPath,
isFlow,
@@ -363,7 +363,7 @@
{initialPath}
{fakeInitialPath}
{currentPath}
{hash}
{runnableVersion}
{isDeployed}
small={useVerticalTriggerBar}
{args}
@@ -416,7 +416,7 @@
{hasPreprocessor}
{canHavePreprocessor}
args={config}
data={{ args, hash, emailDomain }}
data={{ args, hash: !isFlow ? runnableVersion : undefined, emailDomain }}
{isValid}
triggerDeployed={!triggersState.selectedTrigger.isDraft}
on:applyArgs
@@ -24,7 +24,7 @@
initialPath: string
fakeInitialPath: string
currentPath: string
hash?: string
runnableVersion?: string
isDeployed: boolean
small: boolean
args: Record<string, any>
@@ -45,7 +45,7 @@
initialPath,
fakeInitialPath,
currentPath,
hash,
runnableVersion,
small,
args,
newItem,
@@ -68,7 +68,7 @@
<WebhooksPanel
{isFlow}
path={initialPath || fakeInitialPath}
{hash}
{runnableVersion}
token=""
{args}
scopes={isFlow ? [`jobs:run:flows:${currentPath}`] : [`jobs:run:scripts:${currentPath}`]}
@@ -80,7 +80,7 @@
scopes={isFlow ? [`jobs:run:flows:${currentPath}`] : [`jobs:run:scripts:${currentPath}`]}
path={initialPath || fakeInitialPath}
{isFlow}
{hash}
runnableVersion={!isFlow ? runnableVersion : undefined}
{onEmailDomain}
/>
{:else if selectedTrigger.type === 'schedule'}
@@ -14,12 +14,12 @@
import UserSettings from '$lib/components/UserSettings.svelte'
import TextInput from '$lib/components/text_input/TextInput.svelte'
let requestType: 'hash' | 'path' = $state('path')
let requestType: 'runnableVersion' | 'path' = $state('path')
function emailAddress() {
const pathOrHash = requestType === 'hash' ? hash : path.replaceAll('/', '.')
const pathOrHash = requestType === 'runnableVersion' ? runnableVersion : path.replaceAll('/', '.')
const plainPrefix = `${$workspaceStore}+${
(requestType === 'hash' ? 'hash.' : isFlow ? 'flow.' : '') + pathOrHash
(requestType === 'runnableVersion' ? 'hash.' : isFlow ? 'flow.' : '') + pathOrHash
}+${token}`
const encodedPrefix = base32
.stringify(new TextEncoder().encode(plainPrefix), {
@@ -32,7 +32,7 @@
interface Props {
token?: string
isFlow?: boolean
hash?: string | undefined
runnableVersion?: string | undefined
path: string
userSettings: UserSettings
emailDomain?: string | null
@@ -42,7 +42,7 @@
let {
token = $bindable(''),
isFlow = false,
hash = undefined,
runnableVersion = undefined,
path,
userSettings,
emailDomain = null,
@@ -90,7 +90,7 @@
<ToggleButtonGroup class="w-auto" bind:selected={requestType}>
{#snippet children({ item })}
<ToggleButton label="By path" value="path" {item} />
<ToggleButton label="By hash" value="hash" {item} />
<ToggleButton label="By hash" value="runnableVersion" {item} />
{/snippet}
</ToggleButtonGroup>
</div>
@@ -16,7 +16,7 @@
token: string
scopes?: string[]
isFlow?: boolean
hash?: string | undefined
runnableVersion?: string | undefined
path: string
onEmailDomain: (domain: string) => void
}
@@ -25,7 +25,7 @@
token = $bindable(),
scopes = [],
isFlow = false,
hash = undefined,
runnableVersion = undefined,
path,
onEmailDomain
}: Props = $props()
@@ -70,7 +70,7 @@
<Skeleton layout={[[18]]} />
{:else}
{#if emailDomain}
<DefaultEmailConfigSection {hash} {token} {path} {isFlow} {userSettings} {emailDomain} />
<DefaultEmailConfigSection {runnableVersion} {token} {path} {isFlow} {userSettings} {emailDomain} />
{:else}
<div>
<Alert title="Email triggers are disabled" size="xs" type="warning">
@@ -54,6 +54,22 @@ export type TriggerType =
| 'poll'
| 'cli'
export const jobTriggerTypes = [
'webhook',
'http',
'websocket',
'kafka',
'email',
'nats',
'mqtt',
'sqs',
'postgres',
'schedule',
'gcp'
] as const
export type JobTriggerType = (typeof jobTriggerTypes)[number]
export type Trigger = {
type: TriggerType
path?: string
@@ -86,6 +102,23 @@ export const triggerIconMap = {
cli: Terminal
}
export const triggerDisplayNamesMap = {
schedule: 'Schedule',
http: 'HTTP',
websocket: 'WebSocket',
postgres: 'Postgres',
kafka: 'Kafka',
nats: 'NATS',
mqtt: 'MQTT',
sqs: 'SQS',
gcp: 'GCP Pub/Sub',
email: 'Email',
poll: 'Scheduled Poll',
webhook: 'Webhook',
default_email: 'Default Email',
cli: 'CLI'
} as const satisfies Record<TriggerType, string>
/**
* Converts a TriggerType to a CaptureTriggerKind when a mapping exists
* @param triggerType The trigger type to convert
@@ -26,7 +26,7 @@
interface Props {
isFlow?: boolean
path?: string
hash?: string | undefined
runnableVersion?: string | undefined
token?: string
runnableArgs: any
triggerTokens?: TriggerTokens | undefined
@@ -36,46 +36,38 @@
let {
isFlow = false,
path = '',
hash = undefined,
runnableVersion = undefined,
token = $bindable(''),
runnableArgs,
triggerTokens = $bindable(undefined),
scopes = []
}: Props = $props()
let webhooks: {
async: {
get: {}
post: {
hash?: string
path: string
}
const WEBHOOK_BASE_URL = `${location.origin}${base}/api/w/${$workspaceStore}/jobs`
let baseWebhookUrl = $derived.by(() => {
let webhookUrlPath: string
if (isFlow) {
webhookUrlPath = runnableId == 'path' ? `f/${path}` : `fv/${runnableVersion}`
} else {
webhookUrlPath = runnableId == 'path' ? `p/${path}` : `h/${runnableVersion}`
}
sync: {
get: {
path: string
}
post: {
hash?: string
path: string
}
if (requestType == 'async') {
return `${WEBHOOK_BASE_URL}/run/${webhookUrlPath}`
} else if (requestType == 'sync') {
return `${WEBHOOK_BASE_URL}/run_wait_result/${webhookUrlPath}`
} else {
return `${WEBHOOK_BASE_URL}/run_and_stream/${webhookUrlPath}`
}
sync_sse: {
get: {
hash?: string
path: string
}
post: {
hash?: string
path: string
}
}
} = $derived(isFlow ? computeFlowWebhooks(path) : computeScriptWebhooks(hash, path))
})
let selectedTab: string = $state('rest')
let userSettings: UserSettings | undefined = $state()
let requestType = $state(DEFAULT_WEBHOOK_TYPE) as 'async' | 'sync' | 'sync_sse'
let callMethod = $state('post') as 'get' | 'post'
let runnableId = $state('path') as 'hash' | 'path'
let runnableId = $state('path') as 'runnableVersion' | 'path'
let tokenType = $state('headers') as 'query' | 'headers'
$effect(() => {
@@ -91,7 +83,7 @@
: runnableArgs
})
let url: string = $derived(
webhooks[requestType][callMethod][runnableId] +
baseWebhookUrl +
(tokenType === 'query'
? `?token=${token}${
callMethod === 'get' || requestType === 'sync_sse'
@@ -105,70 +97,6 @@
}`)
)
function computeScriptWebhooks(hash: string | undefined, path: string) {
let webhookBase = `${location.origin}${base}/api/w/${$workspaceStore}/jobs`
return {
async: {
get: {},
post: {
hash: `${webhookBase}/run/h/${hash}`,
path: `${webhookBase}/run/p/${path}`
}
},
sync: {
get: {
path: `${webhookBase}/run_wait_result/p/${path}`
},
post: {
hash: `${webhookBase}/run_wait_result/h/${hash}`,
path: `${webhookBase}/run_wait_result/p/${path}`
}
},
sync_sse: {
get: {
path: `${webhookBase}/run_and_stream/p/${path}`,
hash: `${webhookBase}/run_and_stream/h/${hash}`
},
post: {
hash: `${webhookBase}/run_and_stream/h/${hash}`,
path: `${webhookBase}/run_and_stream/p/${path}`
}
}
}
}
function computeFlowWebhooks(path: string) {
let webhooksBase = `${location.origin}${base}/api/w/${$workspaceStore}/jobs`
let urlAsync = `${webhooksBase}/run/f/${path}`
let urlSync = `${webhooksBase}/run_wait_result/f/${path}`
let urlStream = `${webhooksBase}/run_and_stream/f/${path}`
return {
async: {
get: {},
post: {
path: urlAsync
}
},
sync: {
get: {
path: urlSync
},
post: {
path: urlSync
}
},
sync_sse: {
get: {
path: urlStream
},
post: {
path: urlStream
}
}
}
}
function headers() {
const headers = {}
if (callMethod === 'post') {
@@ -433,16 +361,19 @@ done`
{/snippet}
</ToggleButtonGroup>
</Label>
{#if !isFlow}
<Label label="Reference type">
<ToggleButtonGroup bind:selected={runnableId}>
{#snippet children({ item })}
<ToggleButton label="Path" value="path" {item} />
<ToggleButton label="Hash" value="hash" disabled={!hash} {item} />
{/snippet}
</ToggleButtonGroup>
</Label>
{/if}
<Label label="Reference type">
<ToggleButtonGroup bind:selected={runnableId}>
{#snippet children({ item })}
<ToggleButton label="Path" value="path" {item} />
<ToggleButton
label={isFlow ? 'Flow version' : 'Hash'}
value="runnableVersion"
disabled={!runnableVersion}
{item}
/>
{/snippet}
</ToggleButtonGroup>
</Label>
<Label label="Token configuration">
<ToggleButtonGroup bind:selected={tokenType}>
{#snippet children({ item })}
@@ -10,7 +10,7 @@
args?: Record<string, any>
scopes?: string[]
isFlow?: boolean
hash?: string | undefined
runnableVersion?: string | undefined
path: string
newItem?: boolean
}
@@ -20,7 +20,7 @@
args = {},
scopes = [],
isFlow = false,
hash = undefined,
runnableVersion = undefined,
path,
newItem = false
}: Props = $props()
@@ -39,5 +39,5 @@
{isFlow ? 'flow' : 'script'}.
</Alert>
{/if}
<WebhooksConfigSection {isFlow} {path} {hash} {token} runnableArgs={args} {scopes} />
<WebhooksConfigSection {isFlow} {path} {runnableVersion} {token} runnableArgs={args} {scopes} />
</Section>
+29
View File
@@ -0,0 +1,29 @@
/**
* Reads the value of a cookie by name.
* @param {string} name - The name of the cookie to retrieve.
* @returns {string | undefined} The cookie value, or undefined if not found.
*/
export function getCookie(name: string): string | undefined {
const match = document.cookie.match(
new RegExp('(?:^|; )' + name.replace(/([.$?*|{}()\[\]\\\/\+^])/g, '\\$1') + '=([^;]*)')
)
return match ? decodeURIComponent(match[1]) : undefined
}
// this only check the last 2 segments to work for popup on shared top-domain
export function sameTopDomainOrigin(origin: string | null, desktopOrigin: string): boolean {
if (origin == null) {
return false
}
const getLastTwoSegments = (url: string) => {
const parts = url.split('.');
return parts.length >= 2 ? parts.slice(-2).join('.') : url;
};
if (origin.includes('.') && desktopOrigin.includes('.')) {
return getLastTwoSegments(origin) === getLastTwoSegments(desktopOrigin);
} else {
return origin === desktopOrigin;
}
}
@@ -18,9 +18,10 @@
import AuditLogsTimeline from '$lib/components/auditLogs/AuditLogsTimeline.svelte'
let username: string = $state(page.url.searchParams.get('username') ?? 'all')
let pageIndex: number | undefined = $state(Number(page.url.searchParams.get('page')) || 0)
let pageIndex: number | undefined = $state(Number(page.url.searchParams.get('page')) || 1)
let before: string | undefined = $state(page.url.searchParams.get('before') ?? undefined)
let hasMore: boolean = $state(false)
let loading: boolean = $state(false)
let after: string | undefined = $state(page.url.searchParams.get('after') ?? undefined)
let perPage: number | undefined = $state(Number(page.url.searchParams.get('perPage')) || 100)
let operation: string = $state(page.url.searchParams.get('operation') ?? 'all')
@@ -107,6 +108,7 @@
bind:perPage
bind:scope
bind:hasMore
bind:loading
/>
</div>
<div class="2xl:hidden">
@@ -161,6 +163,7 @@
<Pane size={70} minSize={50}>
{#if logs}
<AuditLogsTable
{loading}
{logs}
{selectedId}
bind:pageIndex
@@ -675,6 +675,7 @@
{#if flow}
<TriggersEditor
{args}
runnableVersion={flow.version_id?.toString()}
initialPath={flow.path}
currentPath={flow.path}
noEditor={true}
File diff suppressed because it is too large Load Diff
@@ -791,7 +791,7 @@
{#if script}
<TriggersEditor
{args}
hash={script.hash}
runnableVersion={script.hash}
initialPath={script.path}
currentPath={script.path}
noEditor={true}
+17 -105
View File
@@ -1,33 +1,21 @@
<script lang="ts">
import { BROWSER } from 'esm-env'
import { base } from '$lib/base'
import AppPreview from '$lib/components/apps/editor/AppPreview.svelte'
import { IS_APP_PUBLIC_CONTEXT_KEY, type EditorBreakpoint } from '$lib/components/apps/types'
import { Alert, Skeleton } from '$lib/components/common'
import { WindmillIcon } from '$lib/components/icons'
import { AppService, OpenAPI, type AppWithLastVersion } from '$lib/gen'
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
import { twMerge } from 'tailwind-merge'
import { userStore, workspaceStore } from '$lib/stores'
import { setContext } from 'svelte'
import { writable } from 'svelte/store'
import { setLicense } from '$lib/enterpriseUtils'
import { isCloudHosted } from '$lib/cloud'
import Login from '$lib/components/Login.svelte'
import { getUserExt } from '$lib/user'
import { User, UserRoundX } from 'lucide-svelte'
import { goto } from '$app/navigation'
import { sendUserToast } from '$lib/toast'
import { page } from '$app/state'
import { urlParamsToObject } from '$lib/utils'
import PublicApp from '$lib/components/apps/editor/PublicApp.svelte'
let app: (AppWithLastVersion & { value: any }) | undefined = $state(undefined)
let notExists = $state(false)
let noPermission = $state(false)
let jwtError = $state(false)
setContext(IS_APP_PUBLIC_CONTEXT_KEY, true)
function isJwt(t: string) {
// simply check that the first part is a valid base64 encoded json
@@ -56,9 +44,10 @@
}
}
const parsedCustomPath = parseCustomPath(page.params.path ?? '')
let workspace: string | undefined = $state(undefined)
async function loadApp() {
const parsedCustomPath = parseCustomPath(page.params.path ?? '')
if (parsedCustomPath.jwt) {
const token = 'jwt_ext_' + parsedCustomPath.jwt
OpenAPI.TOKEN = token
@@ -69,6 +58,7 @@
app = await AppService.getPublicAppByCustomPath({
customPath: parsedCustomPath.path
})
workspace = app.workspace_id
workspaceStore.set(app.workspace_id)
noPermission = false
notExists = false
@@ -95,93 +85,15 @@
setLicense()
loadApp()
}
const breakpoint = writable<EditorBreakpoint>('lg')
const darkMode =
window.localStorage.getItem('dark-mode') ??
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
if (darkMode === 'dark') {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
</script>
<div
class="z-50 text-xs fixed bottom-1 right-2 {$enterpriseLicense && !isCloudHosted()
? 'transition-opacity delay-1000 duration-1000 opacity-20 hover:delay-0 hover:opacity-100'
: ''}"
>
<a href="https://windmill.dev" class="whitespace-nowrap text-primary inline-flex items-center"
>Powered by &nbsp;<WindmillIcon />&nbsp;Windmill</a
>
</div>
<div class="z-50 text-2xs text-primary absolute top-3 left-2"
>{#if $userStore}
<div class="flex gap-1 items-center"><User size={14} />{$userStore.username}</div>
{:else}<UserRoundX size={14} />{/if}
</div>
{#if notExists}
<div class="px-4 mt-20"
><Alert type="error" title="Not found"
>There was an error loading the app, is the url correct? <a href={base}>Go to Windmill</a>
</Alert></div
>
{:else if noPermission}
<div class="px-4 mt-20 w-full text-center font-bold text-xl"
>{#if $userStore}You are logged in but have no read access to this app{:else}You must be logged
in and have read access to this app{/if}</div
>
<div class="px-2 mx-auto mt-20 max-w-xl w-full">
{#if !jwtError}
<Login
onLoginSuccess={() => {
// window.location.reload()
loadApp()
app = app
}}
popup
rd={page.url.toString()}
/>
{/if}
</div>
{:else if app}
{#key app}
<div
class={twMerge(
'min-h-screen h-full w-full flex',
app?.value?.['css']?.['app']?.['viewer']?.class,
'wm-app-viewer'
)}
style={app?.value?.['css']?.['app']?.['viewer']?.style}
>
<AppPreview
noBackend={false}
context={{
email: $userStore?.email,
name: $userStore?.name,
groups: $userStore?.groups,
username: $userStore?.username,
query: urlParamsToObject(page.url.searchParams),
hash: page.url.hash.substring(1)
}}
workspace={page.params.workspace}
summary={app.summary}
app={app.value}
appPath={app.path}
{breakpoint}
policy={app.policy}
isEditor={false}
replaceStateFn={(path) => goto(path)}
gotoFn={(path, opt) => goto(path, opt)}
/>
</div>
{/key}
{:else}
<Skeleton layout={[[4], 0.5, [50]]} />
{/if}
<PublicApp
{workspace}
{notExists}
{noPermission}
{jwtError}
{app}
onLoginSuccess={() => {
loadApp()
}}
></PublicApp>
@@ -1,34 +1,22 @@
<script lang="ts">
import { BROWSER } from 'esm-env'
import { base } from '$lib/base'
import AppPreview from '$lib/components/apps/editor/AppPreview.svelte'
import { IS_APP_PUBLIC_CONTEXT_KEY, type EditorBreakpoint } from '$lib/components/apps/types'
import { WindmillIcon } from '$lib/components/icons'
import { AppService, OpenAPI, type AppWithLastVersion } from '$lib/gen'
import { enterpriseLicense, userStore } from '$lib/stores'
import { twMerge } from 'tailwind-merge'
import { userStore } from '$lib/stores'
import { setContext } from 'svelte'
import { writable } from 'svelte/store'
import { setLicense } from '$lib/enterpriseUtils'
import { isCloudHosted } from '$lib/cloud'
import Login from '$lib/components/Login.svelte'
import { getUserExt } from '$lib/user'
import { User, UserRoundX } from 'lucide-svelte'
import { goto } from '$app/navigation'
import { sendUserToast } from '$lib/toast'
import Alert from '$lib/components/common/alert/Alert.svelte'
import Skeleton from '$lib/components/common/skeleton/Skeleton.svelte'
import { page } from '$app/state'
import { urlParamsToObject } from '$lib/utils'
import PublicApp from '$lib/components/apps/editor/PublicApp.svelte'
let app: (AppWithLastVersion & { value: any }) | undefined = $state(undefined)
let notExists = $state(false)
let noPermission = $state(false)
let jwtError = $state(false)
setContext(IS_APP_PUBLIC_CONTEXT_KEY, true)
function parseSecret(secret: string): { secret: string; jwt: string } {
const parts = secret.split('/')
@@ -59,6 +47,11 @@
if (BROWSER) {
setLicense()
loadAll()
}
function loadAll() {
console.log('loadAll')
loadUser().then(() => {
loadApp()
})
@@ -81,95 +74,15 @@
console.warn('Anonymous user')
}
}
const breakpoint = writable<EditorBreakpoint>('lg')
const darkMode =
window.localStorage.getItem('dark-mode') ??
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
if (darkMode === 'dark') {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
</script>
<div
class="z-50 text-xs fixed bottom-1 right-2 {$enterpriseLicense && !isCloudHosted()
? 'transition-opacity delay-1000 duration-1000 opacity-20 hover:delay-0 hover:opacity-100'
: ''}"
>
<a href="https://windmill.dev" class="whitespace-nowrap text-primary inline-flex items-center"
>Powered by &nbsp;<WindmillIcon />&nbsp;Windmill</a
>
</div>
<div class="z-50 text-2xs text-primary absolute top-3 left-2"
>{#if $userStore}
<div class="flex gap-1 items-center"><User size={14} />{$userStore.username}</div>
{:else}<UserRoundX size={14} />{/if}
</div>
{#if notExists}
<div class="px-4 mt-20"
><Alert type="error" title="Not found"
>There was an error loading the app, is the url correct? <a href={base}>Go to Windmill</a>
</Alert></div
>
{:else if noPermission}
<div class="px-4 mt-20 w-full text-center font-bold text-xl"
>{#if $userStore}You are logged in but have no read access to this app{:else}You must be logged
in and have read access to this app{/if}</div
>
<div class="px-2 mx-auto mt-20 max-w-xl w-full">
{#if !jwtError}
<Login
onLoginSuccess={() => {
console.log('login')
// window.location.reload()
loadUser().then(() => {
loadApp()
})
app = app
}}
popup
rd={page.url.toString()}
/>
{/if}
</div>
{:else if app}
{#key app}
<div
class={twMerge(
'min-h-screen h-full w-full flex',
app?.value?.['css']?.['app']?.['viewer']?.class,
'wm-app-viewer'
)}
style={app?.value?.['css']?.['app']?.['viewer']?.style}
>
<AppPreview
noBackend={false}
context={{
email: $userStore?.email,
name: $userStore?.name,
groups: $userStore?.groups,
username: $userStore?.username,
query: urlParamsToObject(page.url.searchParams),
hash: page.url.hash.substring(1)
}}
workspace={page.params.workspace}
summary={app.summary}
app={app.value}
appPath={app.path}
{breakpoint}
policy={app.policy}
isEditor={false}
replaceStateFn={(path) => goto(path)}
gotoFn={(path, opt) => goto(path, opt)}
/>
</div>
{/key}
{:else}
<Skeleton layout={[[4], 0.5, [50]]} />
{/if}
<PublicApp
{app}
workspace={page.params.workspace}
{notExists}
{noPermission}
{jwtError}
onLoginSuccess={() => {
loadAll()
}}
></PublicApp>
@@ -12,6 +12,8 @@
import { parseQueryParams } from '$lib/utils'
import { page } from '$app/state'
import { isCloudHosted } from '$lib/cloud'
import { getCookie } from '$lib/cookies'
// import { getAndDeleteCookie } from '$lib/cookies'
let error = page.url.searchParams.get('error')
let clientName = page.params.client_name ?? ''
@@ -19,11 +21,14 @@
let state = page.url.searchParams.get('state') ?? undefined
onMount(async () => {
// const closeCookie = getAndDeleteCookie('close')
// console.log('closeCookie', closeCookie)
const rd = localStorage.getItem('rd')
if (rd) {
localStorage.removeItem('rd')
}
const closeUponLogin = localStorage.getItem('closeUponLogin') == 'true'
const cookieCloseUponLogin = getCookie('close') == 'true'
const closeUponLogin = cookieCloseUponLogin ?? localStorage.getItem('closeUponLogin') == 'true'
if (error) {
sendUserToast(`Error trying to login with ${clientName} ${error}`, true)
if (closeUponLogin) {
+10 -2
View File
@@ -11,7 +11,15 @@ const version = JSON.parse(json)
const config = {
server: {
https: process.env.HTTPS === 'true',
allowedHosts: ['localhost', '127.0.0.1', '0.0.0.0', 'rubendev.wimill.xyz', 'windmill.xyz'],
allowedHosts: [
'localhost',
'127.0.0.1',
'0.0.0.0',
'rubendev.wimill.xyz',
'windmill.xyz',
'app.windmill.xyz',
'public.windmill.xyz'
],
port: 3000,
proxy: {
'^/api/w/[^/]+/s3_proxy/.*': {
@@ -64,7 +72,7 @@ const config = {
exclude: [
'@codingame/monaco-vscode-standalone-typescript-language-features',
'@codingame/monaco-vscode-standalone-languages'
],
]
},
worker: {
format: 'es'
+2 -2
View File
@@ -4,8 +4,8 @@ verify_ssl = true
name = "pypi"
[packages]
wmill = ">=1.575.0"
wmill_pg = ">=1.575.0"
wmill = ">=1.575.4"
wmill_pg = ">=1.575.4"
sendgrid = "*"
mysql-connector-python = "*"
pymongo = "*"
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.575.0
version: 1.575.4
title: OpenFlow Spec
contact:
name: Ruben Fiszel
@@ -12,7 +12,7 @@
RootModule = 'WindmillClient.psm1'
# Version number of this module.
ModuleVersion = '1.575.0'
ModuleVersion = '1.575.4'
# Supported PSEditions
# CompatiblePSEditions = @()
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill"
version = "1.575.0"
version = "1.575.4"
description = "A client library for accessing Windmill server wrapping the Windmill client API"
license = "Apache-2.0"
homepage = "https://windmill.dev"
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill-pg"
version = "1.575.0"
version = "1.575.4"
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,6 +1,6 @@
{
"name": "@windmill/windmill",
"version": "1.575.0",
"version": "1.575.4",
"exports": "./src/index.ts",
"publish": {
"exclude": ["!src", "./s3Types.ts", "./client.ts"]
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "windmill-client",
"description": "Windmill SDK client for browsers and Node.js",
"version": "1.575.0",
"version": "1.575.4",
"author": "Ruben Fiszel",
"license": "Apache 2.0",
"devDependencies": {
+1 -1
View File
@@ -1 +1 @@
1.575.0
1.575.4