mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-04 16:03:06 +00:00
Merge remote-tracking branch 'upstream/main' into appeditor-multiselect-input-type
This commit is contained in:
@@ -1,5 +1,25 @@
|
||||
# Changelog
|
||||
|
||||
## [1.418.0](https://github.com/windmill-labs/windmill/compare/v1.417.3...v1.418.0) (2024-11-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **frontend:** improve and simplify scheduled poll flows ([#4560](https://github.com/windmill-labs/windmill/issues/4560)) ([22ab51e](https://github.com/windmill-labs/windmill/commit/22ab51e9914376b47827f0be66708deef3a37767))
|
||||
* **oauth:** add snowflake oauth support ([#4622](https://github.com/windmill-labs/windmill/issues/4622)) ([693b7a4](https://github.com/windmill-labs/windmill/commit/693b7a4fd4da0e2b14cb6c8ea19537aa600225be))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **prometheus:** improve queue_count when tags have no more jobs ([09156b6](https://github.com/windmill-labs/windmill/commit/09156b65c2a0f0ac59fce1dede2b68b790323c49))
|
||||
|
||||
## [1.417.3](https://github.com/windmill-labs/windmill/compare/v1.417.2...v1.417.3) (2024-11-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cli:** improve pulling instance with folderPerInstance ([1e7909e](https://github.com/windmill-labs/windmill/commit/1e7909e95931e1e121cb3767bad093239254935d))
|
||||
|
||||
## [1.417.2](https://github.com/windmill-labs/windmill/compare/v1.417.1...v1.417.2) (2024-11-04)
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "bool",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n hostname,\n mode::text,\n worker_group,\n log_ts,\n file_path,\n ok_lines,\n err_lines,\n json_fmt\n FROM log_file\n WHERE log_ts > $1\n ORDER BY log_ts ASC LIMIT $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "hostname",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "mode",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "worker_group",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "log_ts",
|
||||
"type_info": "Timestamp"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "file_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "ok_lines",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "err_lines",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "json_fmt",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Timestamp",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
null,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "b5c839baab25c4dcdd503d380cf7a886242277cd50555f20b2e22e13942d2a3a"
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n hostname,\n mode::text,\n worker_group,\n log_ts,\n file_path,\n ok_lines,\n err_lines,\n json_fmt\n FROM log_file\n ORDER BY log_ts ASC LIMIT $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "hostname",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "mode",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "worker_group",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "log_ts",
|
||||
"type_info": "Timestamp"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "file_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "ok_lines",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "err_lines",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "json_fmt",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
null,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "dd967c5983fa0ff05e2b320ad0e0b5a152784826cb8fb4381c1ffe228cb7feb6"
|
||||
}
|
||||
Generated
+34
-34
@@ -117,9 +117,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.17"
|
||||
version = "0.6.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338"
|
||||
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
@@ -3987,9 +3987,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.0"
|
||||
version = "0.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
|
||||
checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
@@ -4535,7 +4535,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.15.0",
|
||||
"hashbrown 0.15.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -4942,7 +4942,7 @@ version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.0",
|
||||
"hashbrown 0.15.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9124,18 +9124,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.67"
|
||||
version = "1.0.68"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b3c6efbfc763e64eb85c11c25320f0737cb7364c4b6336db90aa9ebe27a0bbd"
|
||||
checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.67"
|
||||
version = "1.0.68"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b607164372e89797d78b8e23a6d67d5d1038c1c65efd52e1389ef8b77caba2a6"
|
||||
checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -10563,7 +10563,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windmill"
|
||||
version = "1.417.2"
|
||||
version = "1.418.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -10605,7 +10605,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.417.2"
|
||||
version = "1.418.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -10690,7 +10690,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.417.2"
|
||||
version = "1.418.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
@@ -10708,7 +10708,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.417.2"
|
||||
version = "1.418.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"serde",
|
||||
@@ -10721,7 +10721,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-autoscaling"
|
||||
version = "1.417.2"
|
||||
version = "1.418.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rsmq_async",
|
||||
@@ -10736,7 +10736,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.417.2"
|
||||
version = "1.418.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@@ -10782,7 +10782,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-git-sync"
|
||||
version = "1.417.2"
|
||||
version = "1.418.0"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"rsmq_async",
|
||||
@@ -10797,7 +10797,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-indexer"
|
||||
version = "1.417.2"
|
||||
version = "1.418.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -10819,7 +10819,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.417.2"
|
||||
version = "1.418.0"
|
||||
dependencies = [
|
||||
"itertools 0.13.0",
|
||||
"lazy_static",
|
||||
@@ -10831,7 +10831,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.417.2"
|
||||
version = "1.418.0"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"serde",
|
||||
@@ -10840,7 +10840,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.417.2"
|
||||
version = "1.418.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -10852,7 +10852,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.417.2"
|
||||
version = "1.418.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -10864,7 +10864,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.417.2"
|
||||
version = "1.418.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -10876,7 +10876,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.417.2"
|
||||
version = "1.418.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.13.0",
|
||||
@@ -10887,7 +10887,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.417.2"
|
||||
version = "1.418.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.13.0",
|
||||
@@ -10898,7 +10898,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.417.2"
|
||||
version = "1.418.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -10916,7 +10916,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.417.2"
|
||||
version = "1.418.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case 0.6.0",
|
||||
@@ -10933,7 +10933,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.417.2"
|
||||
version = "1.418.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -10945,7 +10945,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.417.2"
|
||||
version = "1.418.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -10963,7 +10963,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wasm"
|
||||
version = "1.417.2"
|
||||
version = "1.418.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"getrandom 0.2.15",
|
||||
@@ -10984,7 +10984,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.417.2"
|
||||
version = "1.418.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -10994,7 +10994,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.417.2"
|
||||
version = "1.418.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -11027,7 +11027,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-sql-datatype-parser-wasm"
|
||||
version = "1.417.2"
|
||||
version = "1.418.0"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
@@ -11037,7 +11037,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.417.2"
|
||||
version = "1.418.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.417.2"
|
||||
version = "1.418.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -29,7 +29,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.417.2"
|
||||
version = "1.418.0"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
f136a2f499e0fe7c10c54c79488851980d796eb2
|
||||
01f384348341c87c5a0b32c4be7c699975ee7d6d
|
||||
@@ -171,5 +171,6 @@
|
||||
"user-library-modify",
|
||||
"user-library-read"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"snowflake_oauth": {}
|
||||
}
|
||||
|
||||
+42
-6
@@ -531,7 +531,7 @@ Windmill Community Edition {GIT_VERSION}
|
||||
|
||||
#[cfg(feature = "tantivy")]
|
||||
let (index_reader, index_writer) = if should_index_jobs {
|
||||
let (r, w) = windmill_indexer::indexer_ee::init_index(&db).await?;
|
||||
let (r, w) = windmill_indexer::completed_runs_ee::init_index(&db).await?;
|
||||
(Some(r), Some(w))
|
||||
} else {
|
||||
(None, None)
|
||||
@@ -543,26 +543,61 @@ Windmill Community Edition {GIT_VERSION}
|
||||
let index_writer2 = index_writer.clone();
|
||||
async {
|
||||
if let Some(index_writer) = index_writer2 {
|
||||
windmill_indexer::indexer_ee::run_indexer(db.clone(), index_writer, indexer_rx)
|
||||
.await;
|
||||
windmill_indexer::completed_runs_ee::run_indexer(
|
||||
db.clone(),
|
||||
index_writer,
|
||||
indexer_rx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(all(feature = "tantivy", feature = "parquet"))]
|
||||
let (log_index_reader, log_index_writer) = if should_index_jobs {
|
||||
let (r, w) = windmill_indexer::service_logs_ee::init_index(&db).await?;
|
||||
(Some(r), Some(w))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
#[cfg(all(feature = "tantivy", feature = "parquet"))]
|
||||
let log_indexer_f = {
|
||||
let log_indexer_rx = killpill_rx.resubscribe();
|
||||
let log_index_writer2 = log_index_writer.clone();
|
||||
async {
|
||||
if let Some(log_index_writer) = log_index_writer2 {
|
||||
windmill_indexer::service_logs_ee::run_indexer(
|
||||
db.clone(),
|
||||
log_index_writer,
|
||||
log_indexer_rx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "tantivy"))]
|
||||
let (index_reader, index_writer) = (None, None);
|
||||
let index_reader = None;
|
||||
|
||||
#[cfg(not(feature = "tantivy"))]
|
||||
let indexer_f = async { Ok(()) as anyhow::Result<()> };
|
||||
|
||||
#[cfg(not(all(feature = "tantivy", feature = "parquet")))]
|
||||
let log_index_reader = None;
|
||||
|
||||
#[cfg(not(all(feature = "tantivy", feature = "parquet")))]
|
||||
let log_indexer_f = async { Ok(()) as anyhow::Result<()> };
|
||||
|
||||
let server_f = async {
|
||||
if !is_agent {
|
||||
windmill_api::run_server(
|
||||
db.clone(),
|
||||
rsmq2,
|
||||
index_reader,
|
||||
index_writer,
|
||||
log_index_reader,
|
||||
addr,
|
||||
server_killpill_rx,
|
||||
base_internal_tx,
|
||||
@@ -842,7 +877,8 @@ Windmill Community Edition {GIT_VERSION}
|
||||
monitor_f,
|
||||
server_f,
|
||||
metrics_f,
|
||||
indexer_f
|
||||
indexer_f,
|
||||
log_indexer_f
|
||||
)?;
|
||||
} else {
|
||||
tracing::info!("Nothing to do, exiting.");
|
||||
|
||||
@@ -111,6 +111,9 @@ lazy_static::lazy_static! {
|
||||
"Number of jobs in the queue",
|
||||
&["tag"]
|
||||
).unwrap();
|
||||
|
||||
static ref QUEUE_COUNT_TAGS: Arc<RwLock<Vec<String>>> = Arc::new(RwLock::new(Vec::new()));
|
||||
|
||||
}
|
||||
|
||||
pub async fn initial_load(
|
||||
@@ -1089,12 +1092,23 @@ pub async fn expose_queue_metrics(db: &Pool<Postgres>) {
|
||||
if metrics_enabled || save_metrics {
|
||||
let queue_counts = windmill_common::queue::get_queue_counts(db).await;
|
||||
|
||||
if metrics_enabled {
|
||||
for q in QUEUE_COUNT_TAGS.read().await.iter() {
|
||||
if queue_counts.get(q).is_none() {
|
||||
(*QUEUE_COUNT).with_label_values(&[q]).set(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut tags_to_watch = vec![];
|
||||
for q in queue_counts {
|
||||
let count = q.1;
|
||||
let tag = q.0;
|
||||
|
||||
if metrics_enabled {
|
||||
let metric = (*QUEUE_COUNT).with_label_values(&[&tag]);
|
||||
metric.set(count as i64);
|
||||
tags_to_watch.push(tag.to_string());
|
||||
}
|
||||
|
||||
// save queue_count and delay metrics per tag
|
||||
@@ -1119,6 +1133,10 @@ pub async fn expose_queue_metrics(db: &Pool<Postgres>) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if metrics_enabled {
|
||||
let mut w = QUEUE_COUNT_TAGS.write().await;
|
||||
*w = tags_to_watch;
|
||||
}
|
||||
}
|
||||
|
||||
// clean queue metrics older than 14 days
|
||||
|
||||
@@ -1112,6 +1112,7 @@ async fn test_deno_flow(db: Pool<Postgres>) {
|
||||
custom_concurrency_key: None,
|
||||
concurrent_limit: None,
|
||||
concurrency_time_window_s: None,
|
||||
is_trigger: None,
|
||||
}
|
||||
.into(),
|
||||
stop_after_if: Default::default(),
|
||||
@@ -1153,6 +1154,7 @@ async fn test_deno_flow(db: Pool<Postgres>) {
|
||||
custom_concurrency_key: None,
|
||||
concurrent_limit: None,
|
||||
concurrency_time_window_s: None,
|
||||
is_trigger: None,
|
||||
}
|
||||
.into(),
|
||||
stop_after_if: Default::default(),
|
||||
@@ -1276,6 +1278,8 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
|
||||
custom_concurrency_key: None,
|
||||
concurrent_limit: None,
|
||||
concurrency_time_window_s: None,
|
||||
is_trigger: None,
|
||||
|
||||
}.into(),
|
||||
stop_after_if: Default::default(),
|
||||
stop_after_all_iters_if: Default::default(),
|
||||
@@ -1327,6 +1331,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
|
||||
custom_concurrency_key: None,
|
||||
concurrent_limit: None,
|
||||
concurrency_time_window_s: None,
|
||||
is_trigger: None,
|
||||
}.into(),
|
||||
stop_after_if: Default::default(),
|
||||
stop_after_all_iters_if: Default::default(),
|
||||
@@ -1364,6 +1369,8 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
|
||||
custom_concurrency_key: None,
|
||||
concurrent_limit: None,
|
||||
concurrency_time_window_s: None,
|
||||
is_trigger: None,
|
||||
|
||||
}.into(),
|
||||
stop_after_if: Default::default(),
|
||||
stop_after_all_iters_if: Default::default(),
|
||||
@@ -1424,6 +1431,7 @@ async fn test_deno_flow_same_worker(db: Pool<Postgres>) {
|
||||
custom_concurrency_key: None,
|
||||
concurrent_limit: None,
|
||||
concurrency_time_window_s: None,
|
||||
is_trigger: None,
|
||||
}.into(),
|
||||
stop_after_if: Default::default(),
|
||||
stop_after_all_iters_if: Default::default(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.417.2
|
||||
version: 1.418.0
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
@@ -9546,6 +9546,109 @@ paths:
|
||||
items:
|
||||
$ref: "#/components/schemas/JobSearchHit"
|
||||
|
||||
/srch/index/search/service_logs:
|
||||
get:
|
||||
summary: Search through service logs with a string query
|
||||
operationId: searchLogsIndex
|
||||
tags:
|
||||
- indexSearch
|
||||
parameters:
|
||||
- name: search_query
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: mode
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: worker_group
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: hostname
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: min_ts
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- name: max_ts
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
responses:
|
||||
"200":
|
||||
description: search results
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
query_parse_errors:
|
||||
description: a list of the terms that couldn't be parsed (and thus ignored)
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
hits:
|
||||
description: log files that matched the query
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/LogSearchHit"
|
||||
|
||||
/srch/index/search/count_service_logs:
|
||||
get:
|
||||
summary: Search and count the log line hits on every provided host
|
||||
operationId: countSearchLogsIndex
|
||||
tags:
|
||||
- indexSearch
|
||||
parameters:
|
||||
- name: search_query
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: hosts
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: min_ts
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- name: max_ts
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
responses:
|
||||
"200":
|
||||
description: search results
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
query_parse_errors:
|
||||
description: a list of the terms that couldn't be parsed (and thus ignored)
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
count_per_host:
|
||||
description: count of log lines that matched the query per hostname
|
||||
type: object
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
@@ -12500,6 +12603,12 @@ components:
|
||||
dancer:
|
||||
type: string
|
||||
|
||||
LogSearchHit:
|
||||
type: object
|
||||
properties:
|
||||
dancer:
|
||||
type: string
|
||||
|
||||
AutoscalingEvent:
|
||||
type: object
|
||||
properties:
|
||||
@@ -12517,3 +12626,4 @@ components:
|
||||
applied_at:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
|
||||
@@ -1209,6 +1209,7 @@ mod tests {
|
||||
.into(),
|
||||
hash: None,
|
||||
tag_override: None,
|
||||
is_trigger: None,
|
||||
}),
|
||||
stop_after_if: None,
|
||||
stop_after_all_iters_if: None,
|
||||
@@ -1236,6 +1237,7 @@ mod tests {
|
||||
custom_concurrency_key: None,
|
||||
concurrent_limit: None,
|
||||
concurrency_time_window_s: None,
|
||||
is_trigger: None,
|
||||
}),
|
||||
stop_after_if: Some(StopAfterIf {
|
||||
expr: "foo = 'bar'".to_string(),
|
||||
@@ -1290,6 +1292,7 @@ mod tests {
|
||||
input_transforms: HashMap::new(),
|
||||
hash: None,
|
||||
tag_override: None,
|
||||
is_trigger: None,
|
||||
}
|
||||
.into(),
|
||||
stop_after_if: Some(StopAfterIf {
|
||||
|
||||
@@ -3,3 +3,7 @@ use axum::Router;
|
||||
pub fn workspaced_service() -> Router {
|
||||
Router::new()
|
||||
}
|
||||
|
||||
pub fn global_service() -> Router {
|
||||
Router::new()
|
||||
}
|
||||
|
||||
@@ -154,18 +154,18 @@ pub async fn add_webhook_allowed_origin(
|
||||
type IndexReader = ();
|
||||
|
||||
#[cfg(not(feature = "tantivy"))]
|
||||
type IndexWriter = ();
|
||||
type ServiceLogIndexReader = ();
|
||||
|
||||
#[cfg(feature = "tantivy")]
|
||||
type IndexReader = windmill_indexer::indexer_ee::IndexReader;
|
||||
type IndexReader = windmill_indexer::completed_runs_ee::IndexReader;
|
||||
#[cfg(feature = "tantivy")]
|
||||
type IndexWriter = windmill_indexer::indexer_ee::IndexWriter;
|
||||
type ServiceLogIndexReader = windmill_indexer::service_logs_ee::ServiceLogIndexReader;
|
||||
|
||||
pub async fn run_server(
|
||||
db: DB,
|
||||
rsmq: Option<rsmq_async::MultiplexedRsmq>,
|
||||
index_reader: Option<IndexReader>,
|
||||
index_writer: Option<IndexWriter>,
|
||||
job_index_reader: Option<IndexReader>,
|
||||
log_index_reader: Option<ServiceLogIndexReader>,
|
||||
addr: SocketAddr,
|
||||
mut rx: tokio::sync::broadcast::Receiver<()>,
|
||||
port_tx: tokio::sync::oneshot::Sender<String>,
|
||||
@@ -205,8 +205,9 @@ pub async fn run_server(
|
||||
.layer(Extension(rsmq.clone()))
|
||||
.layer(Extension(user_db.clone()))
|
||||
.layer(Extension(auth_cache.clone()))
|
||||
.layer(Extension(index_reader))
|
||||
.layer(Extension(index_writer))
|
||||
.layer(Extension(job_index_reader))
|
||||
.layer(Extension(log_index_reader))
|
||||
// .layer(Extension(index_writer))
|
||||
.layer(CookieManagerLayer::new())
|
||||
.layer(Extension(WebhookShared::new(rx.resubscribe(), db.clone())))
|
||||
.layer(DefaultBodyLimit::max(
|
||||
@@ -322,6 +323,10 @@ pub async fn run_server(
|
||||
"/srch/w/:workspace_id/index",
|
||||
indexer_ee::workspaced_service(),
|
||||
)
|
||||
.nest(
|
||||
"/srch/index",
|
||||
indexer_ee::global_service(),
|
||||
)
|
||||
.nest("/oidc", oidc_ee::global_service())
|
||||
.nest(
|
||||
"/saml",
|
||||
|
||||
@@ -426,6 +426,8 @@ pub enum FlowModuleValue {
|
||||
hash: Option<ScriptHash>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
tag_override: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
is_trigger: Option<bool>,
|
||||
},
|
||||
Flow {
|
||||
#[serde(default)]
|
||||
@@ -474,6 +476,8 @@ pub enum FlowModuleValue {
|
||||
concurrent_limit: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
concurrency_time_window_s: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
is_trigger: Option<bool>,
|
||||
},
|
||||
Identity,
|
||||
}
|
||||
@@ -505,6 +509,7 @@ struct UntaggedFlowModuleValue {
|
||||
custom_concurrency_key: Option<String>,
|
||||
concurrent_limit: Option<i32>,
|
||||
concurrency_time_window_s: Option<i32>,
|
||||
is_trigger: Option<bool>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for FlowModuleValue {
|
||||
@@ -522,6 +527,7 @@ impl<'de> Deserialize<'de> for FlowModuleValue {
|
||||
.ok_or_else(|| serde::de::Error::missing_field("path"))?,
|
||||
hash: untagged.hash,
|
||||
tag_override: untagged.tag_override,
|
||||
is_trigger: untagged.is_trigger,
|
||||
}),
|
||||
"flow" => Ok(FlowModuleValue::Flow {
|
||||
input_transforms: untagged.input_transforms.unwrap_or_default(),
|
||||
@@ -574,6 +580,7 @@ impl<'de> Deserialize<'de> for FlowModuleValue {
|
||||
custom_concurrency_key: untagged.custom_concurrency_key,
|
||||
concurrent_limit: untagged.concurrent_limit,
|
||||
concurrency_time_window_s: untagged.concurrency_time_window_s,
|
||||
is_trigger: untagged.is_trigger,
|
||||
}),
|
||||
"identity" => Ok(FlowModuleValue::Identity),
|
||||
other => Err(serde::de::Error::unknown_variant(
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
use sqlx::{Pool, Postgres};
|
||||
use windmill_common::error::Error;
|
||||
use anyhow::anyhow;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IndexReader;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IndexWriter;
|
||||
|
||||
pub async fn init_index() -> Result<(IndexReader, IndexWriter), Error> {
|
||||
Err(anyhow!("Cannot initialize index: not in EE").into())
|
||||
}
|
||||
|
||||
pub async fn run_indexer(
|
||||
_db: Pool<Postgres>,
|
||||
mut _index_writer: IndexWriter,
|
||||
mut _killpill_rx: tokio::sync::broadcast::Receiver<()>,
|
||||
) {
|
||||
tracing::error!("Cannot run indexer: not in EE");
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
use anyhow::anyhow;
|
||||
use sqlx::{Pool, Postgres};
|
||||
use windmill_common::error::Error;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IndexReader;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IndexWriter;
|
||||
|
||||
pub async fn init_index() -> Result<(IndexReader, IndexWriter), Error> {
|
||||
Err(anyhow!("Cannot initialize index: not in EE").into())
|
||||
}
|
||||
|
||||
pub async fn run_indexer(
|
||||
_db: Pool<Postgres>,
|
||||
mut _index_writer: IndexWriter,
|
||||
mut _killpill_rx: tokio::sync::broadcast::Receiver<()>,
|
||||
) {
|
||||
tracing::error!("Cannot run indexer: not in EE");
|
||||
}
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
pub mod completed_runs_ee;
|
||||
pub mod service_logs_ee;
|
||||
pub mod indexer_ee;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
use sqlx::{Pool, Postgres};
|
||||
use windmill_common::error::Error;
|
||||
use anyhow::anyhow;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IndexReader;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IndexWriter;
|
||||
|
||||
pub async fn init_index() -> Result<(IndexReader, IndexWriter), Error> {
|
||||
Err(anyhow!("Cannot initialize index: not in EE").into())
|
||||
}
|
||||
|
||||
pub async fn run_indexer(
|
||||
_db: Pool<Postgres>,
|
||||
mut _index_writer: IndexWriter,
|
||||
mut _killpill_rx: tokio::sync::broadcast::Receiver<()>,
|
||||
) {
|
||||
tracing::error!("Cannot run indexer: not in EE");
|
||||
}
|
||||
@@ -3522,6 +3522,7 @@ pub async fn push<'c, 'd, R: rsmq_async::RsmqConnection + Send + 'c>(
|
||||
path: path.clone(),
|
||||
hash: Some(hash),
|
||||
tag_override: tag_override,
|
||||
is_trigger: None,
|
||||
},
|
||||
),
|
||||
stop_after_if: None,
|
||||
|
||||
@@ -32,9 +32,9 @@ struct Claims {
|
||||
#[derive(Deserialize)]
|
||||
struct SnowflakeDatabase {
|
||||
account_identifier: String,
|
||||
public_key: String,
|
||||
private_key: String,
|
||||
username: String,
|
||||
public_key: Option<String>,
|
||||
private_key: Option<String>,
|
||||
username: Option<String>,
|
||||
database: Option<String>,
|
||||
schema: Option<String>,
|
||||
warehouse: Option<String>,
|
||||
@@ -119,6 +119,7 @@ fn do_snowflake_inner<'a>(
|
||||
mut body: serde_json::Map<String, Value>,
|
||||
account_identifier: &'a str,
|
||||
token: &'a str,
|
||||
token_is_keypair: bool,
|
||||
column_order: Option<&'a mut Option<Vec<String>>>,
|
||||
skip_collect: bool,
|
||||
) -> windmill_common::error::Result<BoxFuture<'a, windmill_common::error::Result<Box<RawValue>>>> {
|
||||
@@ -144,16 +145,19 @@ fn do_snowflake_inner<'a>(
|
||||
}
|
||||
|
||||
let result_f = async move {
|
||||
let result = HTTP_CLIENT
|
||||
let mut request = HTTP_CLIENT
|
||||
.post(format!(
|
||||
"https://{}.snowflakecomputing.com/api/v2/statements/",
|
||||
account_identifier.to_uppercase()
|
||||
))
|
||||
.bearer_auth(token)
|
||||
.header("X-Snowflake-Authorization-Token-Type", "KEYPAIR_JWT")
|
||||
.json(&body)
|
||||
.send()
|
||||
.await;
|
||||
.json(&body);
|
||||
|
||||
if token_is_keypair {
|
||||
request = request.header("X-Snowflake-Authorization-Token-Type", "KEYPAIR_JWT");
|
||||
}
|
||||
|
||||
let result = request.send().await;
|
||||
|
||||
if skip_collect {
|
||||
handle_snowflake_result(result).await?;
|
||||
@@ -189,11 +193,17 @@ fn do_snowflake_inner<'a>(
|
||||
account_identifier.to_uppercase(),
|
||||
response.statementHandle
|
||||
);
|
||||
let response = HTTP_CLIENT
|
||||
let mut request = HTTP_CLIENT
|
||||
.get(url)
|
||||
.bearer_auth(token)
|
||||
.header("X-Snowflake-Authorization-Token-Type", "KEYPAIR_JWT")
|
||||
.query(&[("partition", idx.to_string())])
|
||||
.query(&[("partition", idx.to_string())]);
|
||||
|
||||
if token_is_keypair {
|
||||
request =
|
||||
request.header("X-Snowflake-Authorization-Token-Type", "KEYPAIR_JWT");
|
||||
}
|
||||
|
||||
let response = request
|
||||
.send()
|
||||
.await
|
||||
.parse_snowflake_response::<SnowflakeDataOnlyResponse>()
|
||||
@@ -258,7 +268,7 @@ pub async fn do_snowflake(
|
||||
snowflake_args.get("database").cloned()
|
||||
};
|
||||
|
||||
let database = if let Some(db) = db_arg {
|
||||
let database = if let Some(ref db) = db_arg {
|
||||
serde_json::from_value::<SnowflakeDatabase>(db.clone())
|
||||
.map_err(|e| Error::ExecutionErr(e.to_string()))?
|
||||
} else {
|
||||
@@ -267,37 +277,59 @@ pub async fn do_snowflake(
|
||||
|
||||
let annotations = windmill_common::worker::SqlAnnotations::parse(query);
|
||||
|
||||
let qualified_username = format!(
|
||||
"{}.{}",
|
||||
database.account_identifier.split('.').next().unwrap_or(""), // get first part of account identifier
|
||||
database.username
|
||||
)
|
||||
.to_uppercase();
|
||||
// Check if the token is present in db_arg and use it if available
|
||||
let (token, token_is_keypair) = if let Some(token) = db_arg
|
||||
.as_ref()
|
||||
.and_then(|db| db.get("token"))
|
||||
.and_then(|t| t.as_str())
|
||||
.filter(|t| !t.is_empty())
|
||||
{
|
||||
tracing::debug!("Using oauth token from db_arg");
|
||||
(token.to_string(), false)
|
||||
} else {
|
||||
tracing::debug!("Generating new oauth token");
|
||||
|
||||
let public_key = pem::parse(database.public_key.as_bytes()).map_err(|e| {
|
||||
Error::ExecutionErr(format!("Failed to parse public key: {}", e.to_string()))
|
||||
})?;
|
||||
let mut public_key_hash = Sha256::new();
|
||||
public_key_hash.update(public_key.contents());
|
||||
let qualified_username = format!(
|
||||
"{}.{}",
|
||||
database.account_identifier.split('.').next().unwrap_or(""),
|
||||
database.username.as_deref().unwrap_or("")
|
||||
)
|
||||
.to_uppercase();
|
||||
|
||||
let public_key_fp = engine::general_purpose::STANDARD.encode(public_key_hash.finalize());
|
||||
let public_key = match database.public_key.as_deref() {
|
||||
Some(key) => pem::parse(key.as_bytes()).map_err(|e| {
|
||||
Error::ExecutionErr(format!("Failed to parse public key: {}", e.to_string()))
|
||||
})?,
|
||||
None => return Err(Error::ExecutionErr("Public key is missing".to_string())),
|
||||
};
|
||||
let mut public_key_hash = Sha256::new();
|
||||
public_key_hash.update(public_key.contents());
|
||||
|
||||
let iss = format!("{}.SHA256:{}", qualified_username, public_key_fp);
|
||||
let public_key_fp = engine::general_purpose::STANDARD.encode(public_key_hash.finalize());
|
||||
|
||||
let claims = Claims {
|
||||
iss: iss,
|
||||
sub: qualified_username,
|
||||
iat: chrono::Utc::now().timestamp(),
|
||||
exp: (chrono::Utc::now() + chrono::Duration::try_hours(1).unwrap()).timestamp(),
|
||||
let iss = format!("{}.SHA256:{}", qualified_username, public_key_fp);
|
||||
|
||||
let claims = Claims {
|
||||
iss: iss,
|
||||
sub: qualified_username,
|
||||
iat: chrono::Utc::now().timestamp(),
|
||||
exp: (chrono::Utc::now() + chrono::Duration::try_hours(1).unwrap()).timestamp(),
|
||||
};
|
||||
|
||||
let private_key = match database.private_key.as_deref() {
|
||||
Some(key) => EncodingKey::from_rsa_pem(key.as_bytes()).map_err(|e| {
|
||||
Error::ExecutionErr(format!("Failed to parse private key: {}", e.to_string()))
|
||||
})?,
|
||||
None => return Err(Error::ExecutionErr("Private key is missing".to_string())),
|
||||
};
|
||||
|
||||
(
|
||||
encode(&Header::new(Algorithm::RS256), &claims, &private_key)
|
||||
.map_err(|e| Error::ExecutionErr(e.to_string()))?,
|
||||
true,
|
||||
)
|
||||
};
|
||||
|
||||
let private_key = EncodingKey::from_rsa_pem(database.private_key.as_bytes()).map_err(|e| {
|
||||
Error::ExecutionErr(format!("Failed to parse private key: {}", e.to_string()))
|
||||
})?;
|
||||
|
||||
let token = encode(&Header::new(Algorithm::RS256), &claims, &private_key)
|
||||
.map_err(|e| Error::ExecutionErr(e.to_string()))?;
|
||||
|
||||
tracing::debug!("Snowflake token: {}", token);
|
||||
|
||||
let mut body = serde_json::Map::new();
|
||||
@@ -344,6 +376,7 @@ pub async fn do_snowflake(
|
||||
body.clone(),
|
||||
&database.account_identifier,
|
||||
&token,
|
||||
token_is_keypair,
|
||||
None,
|
||||
annotations.return_last_result && i < queries.len() - 1,
|
||||
)
|
||||
@@ -371,6 +404,7 @@ pub async fn do_snowflake(
|
||||
body.clone(),
|
||||
&database.account_identifier,
|
||||
&token,
|
||||
token_is_keypair,
|
||||
Some(column_order),
|
||||
false,
|
||||
)?
|
||||
|
||||
@@ -735,6 +735,7 @@ async fn lock_modules<'c>(
|
||||
custom_concurrency_key,
|
||||
concurrent_limit,
|
||||
concurrency_time_window_s,
|
||||
is_trigger,
|
||||
} = e.get_value()?
|
||||
else {
|
||||
match e.get_value()? {
|
||||
@@ -970,6 +971,7 @@ async fn lock_modules<'c>(
|
||||
custom_concurrency_key,
|
||||
concurrent_limit,
|
||||
concurrency_time_window_s,
|
||||
is_trigger,
|
||||
});
|
||||
new_flow_modules.push(e);
|
||||
continue;
|
||||
@@ -992,6 +994,7 @@ async fn lock_modules<'c>(
|
||||
custom_concurrency_key,
|
||||
concurrent_limit,
|
||||
concurrency_time_window_s,
|
||||
is_trigger,
|
||||
});
|
||||
new_flow_modules.push(e);
|
||||
continue;
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
|
||||
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
|
||||
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
|
||||
|
||||
export const VERSION = "v1.417.2";
|
||||
export const VERSION = "v1.418.0";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
+10
-6
@@ -575,13 +575,14 @@ async function getLocalWorkspaces(
|
||||
}
|
||||
if (folderPerInstance) {
|
||||
for await (const dir of Deno.readDir(rootDir + "/" + localPrefix)) {
|
||||
const dirName = dir.name;
|
||||
localWorkspaces.push({
|
||||
dir: localPrefix + "/" + dirName,
|
||||
id: dirName,
|
||||
});
|
||||
if (dir.isDirectory) {
|
||||
const dirName = dir.name;
|
||||
localWorkspaces.push({
|
||||
dir: localPrefix + "/" + dirName,
|
||||
id: dirName,
|
||||
});
|
||||
}
|
||||
}
|
||||
log.info(localWorkspaces);
|
||||
} else {
|
||||
for await (const dir of Deno.readDir(rootDir)) {
|
||||
const dirName = dir.name;
|
||||
@@ -593,6 +594,9 @@ async function getLocalWorkspaces(
|
||||
}
|
||||
}
|
||||
}
|
||||
log.info(
|
||||
"Local workspaces found: " + localWorkspaces.map((x) => x.id).join(", ")
|
||||
);
|
||||
return localWorkspaces;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ export {
|
||||
// }
|
||||
// });
|
||||
|
||||
export const VERSION = "1.417.2";
|
||||
export const VERSION = "1.418.0";
|
||||
|
||||
const command = new Command()
|
||||
.name("wmill")
|
||||
|
||||
+2
-2
@@ -285,8 +285,8 @@ export async function removeWorkspace(
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (silent) {
|
||||
log.info(colors.yellow(`Replacing existing workspace ${name}`));
|
||||
if (!silent) {
|
||||
log.info(colors.yellow(`Removing existing workspace ${name}`));
|
||||
}
|
||||
|
||||
await Deno.writeTextFile(
|
||||
|
||||
Generated
+7
-7
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.417.2",
|
||||
"version": "1.418.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill-components",
|
||||
"version": "1.417.2",
|
||||
"version": "1.418.0",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@aws-crypto/sha256-js": "^4.0.0",
|
||||
@@ -43,7 +43,7 @@
|
||||
"graphql": "^16.7.1",
|
||||
"hash-sum": "^2.0.0",
|
||||
"highlight.js": "^11.8.0",
|
||||
"lucide-svelte": "^0.293.0",
|
||||
"lucide-svelte": "^0.399.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@~8.0.2",
|
||||
"monaco-editor-wrapper": "^5.5.2",
|
||||
@@ -8607,11 +8607,11 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/lucide-svelte": {
|
||||
"version": "0.293.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-0.293.0.tgz",
|
||||
"integrity": "sha512-nQ6QO6anjjiVoipw3dNptzI6NWuiArlhysVtRpoAWICSQVMR3ybwVf6piUUqbjGe+RfGufLzCl93C/yhsS+p4w==",
|
||||
"version": "0.399.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-0.399.0.tgz",
|
||||
"integrity": "sha512-NQ8AxNMKbIJsx7HV//gnAsIY1wJfb3rbXSK2S/ZDjIldvAEdzGngpUT8T8Q8zHYUuii0bavAmVARN8giR4vvpA==",
|
||||
"peerDependencies": {
|
||||
"svelte": ">=3 <5"
|
||||
"svelte": "^3 || ^4 || ^5.0.0-next.42"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.417.2",
|
||||
"version": "1.418.0",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
@@ -116,7 +116,7 @@
|
||||
"graphql": "^16.7.1",
|
||||
"hash-sum": "^2.0.0",
|
||||
"highlight.js": "^11.8.0",
|
||||
"lucide-svelte": "^0.293.0",
|
||||
"lucide-svelte": "^0.399.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@~8.0.2",
|
||||
"monaco-editor-wrapper": "^5.5.2",
|
||||
@@ -182,6 +182,11 @@
|
||||
"svelte": "./package/components/icons/WindmillIcon2.svelte",
|
||||
"default": "./package/components/icons/WindmillIcon2.svelte"
|
||||
},
|
||||
"./components/icons/SchedulePollIcon.svelte": {
|
||||
"types": "./package/components/icons/SchedulePollIcon.d.ts",
|
||||
"svelte": "./package/components/icons/SchedulePollIcon.svelte",
|
||||
"default": "./package/components/icons/SchedulePollIcon.svelte"
|
||||
},
|
||||
"./components/IconedResourceType.svelte": {
|
||||
"types": "./package/components/IconedResourceType.svelte.d.ts",
|
||||
"svelte": "./package/components/IconedResourceType.svelte",
|
||||
|
||||
@@ -250,6 +250,14 @@
|
||||
throw Error(`Resource at path ${path} already exists. Delete it or pick another path`)
|
||||
}
|
||||
|
||||
|
||||
if (resourceType == 'snowflake_oauth') {
|
||||
const account_identifier = extra_params.find(([key, _]) => key == 'account_identifier')
|
||||
if (account_identifier) {
|
||||
args['account_identifier'] = account_identifier[1]
|
||||
}
|
||||
}
|
||||
|
||||
let account: number | undefined = undefined
|
||||
if (valueToken?.expires_in != undefined) {
|
||||
account = Number(
|
||||
|
||||
@@ -476,7 +476,7 @@
|
||||
const testStepStore = writable<Record<string, any>>({})
|
||||
const selectedIdStore = writable('settings-metadata')
|
||||
const selectedTriggerStore = writable<
|
||||
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets'
|
||||
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets' | 'scheduledPoll'
|
||||
>('webhooks')
|
||||
|
||||
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(undefined)
|
||||
@@ -484,7 +484,8 @@
|
||||
setContext<TriggerContext>('TriggerContext', {
|
||||
primarySchedule: primaryScheduleStore,
|
||||
selectedTrigger: selectedTriggerStore,
|
||||
triggersCount: triggersCount
|
||||
triggersCount: triggersCount,
|
||||
simplifiedPoll: writable(false)
|
||||
})
|
||||
setContext<FlowEditorContext>('FlowEditorContext', {
|
||||
selectedId: selectedIdStore,
|
||||
|
||||
@@ -144,7 +144,7 @@
|
||||
? { schedule_count: 1, primary_schedule: { schedule: savedPrimarySchedule.cron } }
|
||||
: undefined
|
||||
)
|
||||
|
||||
const simplifiedPoll = writable(false)
|
||||
export function setPrimarySchedule(schedule: ScheduleTrigger | undefined | false) {
|
||||
primaryScheduleStore.set(schedule)
|
||||
loadTriggers()
|
||||
@@ -468,7 +468,7 @@
|
||||
|
||||
const selectedIdStore = writable<string>(selectedId ?? 'settings-metadata')
|
||||
const selectedTriggerStore = writable<
|
||||
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets'
|
||||
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets' | 'scheduledPoll'
|
||||
>('webhooks')
|
||||
|
||||
export function getSelectedId() {
|
||||
@@ -490,7 +490,14 @@
|
||||
}
|
||||
|
||||
function selectTrigger(
|
||||
selectedTrigger: 'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets'
|
||||
selectedTrigger:
|
||||
| 'webhooks'
|
||||
| 'emails'
|
||||
| 'schedules'
|
||||
| 'cli'
|
||||
| 'routes'
|
||||
| 'websockets'
|
||||
| 'scheduledPoll'
|
||||
) {
|
||||
selectedTriggerStore.set(selectedTrigger)
|
||||
}
|
||||
@@ -517,7 +524,8 @@
|
||||
setContext<TriggerContext>('TriggerContext', {
|
||||
selectedTrigger: selectedTriggerStore,
|
||||
primarySchedule: primaryScheduleStore,
|
||||
triggersCount
|
||||
triggersCount,
|
||||
simplifiedPoll
|
||||
})
|
||||
|
||||
async function loadTriggers() {
|
||||
@@ -876,7 +884,6 @@
|
||||
await tick()
|
||||
select(module.id)
|
||||
await tick()
|
||||
await tick()
|
||||
focusCopilot()
|
||||
|
||||
let isFirstInLoop = false
|
||||
|
||||
@@ -1008,6 +1008,7 @@
|
||||
modules={job.raw_flow?.modules ?? []}
|
||||
failureModule={job.raw_flow?.failure_module}
|
||||
preprocessorModule={job.raw_flow?.preprocessor_module}
|
||||
allowSimplifiedPoll={false}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -108,9 +108,24 @@
|
||||
loading = false
|
||||
|
||||
latestKeyRenewalAttempt = await SettingService.getLatestKeyRenewalAttempt()
|
||||
|
||||
// populate snowflake account identifier from db
|
||||
const account_identifier =
|
||||
oauths?.snowflake_oauth?.connect_config?.extra_params?.account_identifier
|
||||
if (account_identifier) {
|
||||
snowflakeAccountIdentifier = account_identifier
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveSettings() {
|
||||
if (
|
||||
oauths?.snowflake_oauth &&
|
||||
oauths?.snowflake_oauth?.connect_config?.extra_params?.account_identifier !==
|
||||
snowflakeAccountIdentifier
|
||||
) {
|
||||
setupSnowflakeUrls()
|
||||
}
|
||||
|
||||
let shouldReloadPage = false
|
||||
if (values) {
|
||||
const allSettings = Object.values(settings).flatMap((x) => Object.entries(x))
|
||||
@@ -217,7 +232,8 @@
|
||||
'linkedin',
|
||||
'quickbooks',
|
||||
'visma',
|
||||
'spotify'
|
||||
'spotify',
|
||||
'snowflake_oauth'
|
||||
]
|
||||
|
||||
let oauth_name = undefined
|
||||
@@ -269,6 +285,23 @@
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
let snowflakeAccountIdentifier = ''
|
||||
|
||||
function setupSnowflakeUrls() {
|
||||
// strip all whitespaces from account identifier
|
||||
snowflakeAccountIdentifier = snowflakeAccountIdentifier.replace(/\s/g, '')
|
||||
|
||||
const connect_config = {
|
||||
scopes: [],
|
||||
auth_url: `https://${snowflakeAccountIdentifier}.snowflakecomputing.com/oauth/authorize`,
|
||||
token_url: `https://${snowflakeAccountIdentifier}.snowflakecomputing.com/oauth/token-request`,
|
||||
req_body_auth: false,
|
||||
extra_params: { account_identifier: snowflakeAccountIdentifier },
|
||||
extra_params_callback: {}
|
||||
}
|
||||
oauths['snowflake_oauth'].connect_config = connect_config
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="pb-8">
|
||||
@@ -513,6 +546,22 @@
|
||||
{#if !windmillBuiltins.includes(k) && k != 'slack'}
|
||||
<CustomOauth bind:connect_config={oauths[k]['connect_config']} />
|
||||
{/if}
|
||||
{#if k == 'snowflake_oauth'}
|
||||
<label class="block pb-2">
|
||||
<span class="text-primary font-semibold text-sm"
|
||||
><a
|
||||
href="https://docs.snowflake.com/en/user-guide/admin-account-identifier#using-an-account-name-as-an-identifier"
|
||||
target="_blank">Snowflake Account Identifier</a
|
||||
></span
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="<orgname>-<account_name>"
|
||||
required={true}
|
||||
bind:value={snowflakeAccountIdentifier}
|
||||
/>
|
||||
</label>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import AnsiUp from 'ansi_up'
|
||||
|
||||
export let content: string
|
||||
export let highlighted: any[]
|
||||
|
||||
const ansi_up = new AnsiUp()
|
||||
ansi_up.use_classes = true
|
||||
|
||||
function highlightSnippet(snippet: string) {
|
||||
const opener = '!-!-!_H_START_WMILL_!-!-!'
|
||||
const closer = '!-!-!_H_ENDER_WMILL_!-!-!'
|
||||
|
||||
let offset = 0
|
||||
let ret = snippet
|
||||
for (const range of highlighted) {
|
||||
ret = ret.slice(0, range.start + offset) + opener + ret.slice(range.start + offset)
|
||||
offset += opener.length
|
||||
ret = ret.slice(0, range.end + offset) + closer + ret.slice(range.end + offset)
|
||||
offset += closer.length
|
||||
}
|
||||
|
||||
let html = ansi_up.ansi_to_html(ret)
|
||||
let html2 = html
|
||||
.replaceAll(opener, '<span class="bg-amber-400 text-black">')
|
||||
.replaceAll(closer, '</span>')
|
||||
return html2
|
||||
}
|
||||
|
||||
let html = highlightSnippet(content)
|
||||
</script>
|
||||
|
||||
<button on:click class="font-light !m-0 !p-0">
|
||||
<pre
|
||||
class="bg-surface-secondary hover:bg-surface px-2 py-1 text-secondary text-xs w-[100%] whitespace-pre border min-w-full text-start" >
|
||||
{@html html}
|
||||
</pre>
|
||||
</button>
|
||||
@@ -245,7 +245,7 @@
|
||||
: 'top-2'} left-36">mem peak: {(mem / 1024).toPrecision(4)}MB</span
|
||||
>
|
||||
{/if}
|
||||
<pre class="whitespace-pre-wrap break-words {small ? '!text-2xs' : '!text-xs'} w-full p-2"
|
||||
<pre class="whitespace-pre break-words {small ? '!text-2xs' : '!text-xs'} w-full p-2"
|
||||
>{#if content}{@const len =
|
||||
(content?.length ?? 0) +
|
||||
(loadedFromObjectStore?.length ?? 0)}{#if downloadStartUrl}<button on:click={getStoreLogs}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { createPopperActions, type PopperOptions } from 'svelte-popperjs'
|
||||
import type { PopoverPlacement } from './Popover.model'
|
||||
import Portal from '$lib/components/Portal.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
import { ExternalLink } from 'lucide-svelte'
|
||||
|
||||
@@ -13,6 +14,7 @@
|
||||
export let appearTimeout = 300
|
||||
export let documentationLink: string | undefined = undefined
|
||||
export let style: string | undefined = undefined
|
||||
export let forceOpen = false
|
||||
|
||||
const [popperRef, popperContent] = createPopperActions({ placement })
|
||||
|
||||
@@ -47,6 +49,8 @@
|
||||
inTimeout = undefined
|
||||
timeout = setTimeout(() => (showTooltip = false), disappearTimeout)
|
||||
}
|
||||
|
||||
$: forceOpen ? open() : close()
|
||||
</script>
|
||||
|
||||
{#if notClickable}
|
||||
@@ -73,7 +77,10 @@
|
||||
use:popperContent={popperOptions}
|
||||
on:mouseenter={open}
|
||||
on:mouseleave={close}
|
||||
class="z-[5001] py-2 px-3 rounded-md text-sm font-normal !text-gray-300 bg-gray-800 whitespace-normal text-left {popupClass}"
|
||||
class={twMerge(
|
||||
'z-[5001] py-2 px-3 rounded-md text-sm font-normal !text-gray-300 bg-gray-800 whitespace-normal text-left',
|
||||
popupClass
|
||||
)}
|
||||
>
|
||||
<div class="max-w-sm break-words">
|
||||
<slot name="text" />
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
import SchemaForm from './SchemaForm.svelte'
|
||||
import { emptyString, sendUserToast } from '$lib/utils'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import { loadSchedules, saveSchedule } from './flows/scheduleUtils'
|
||||
import { type Writable, writable } from 'svelte/store'
|
||||
|
||||
export let schema: any
|
||||
export let isFlow: boolean
|
||||
@@ -22,103 +24,33 @@
|
||||
const { primarySchedule, triggersCount } = getContext<TriggerContext>('TriggerContext')
|
||||
|
||||
let scheduleEditor: ScheduleEditor
|
||||
let schedules: Writable<Schedule[] | undefined> = writable(undefined)
|
||||
let initialPrimarySchedule: Writable<ScheduleTrigger | false | undefined> = writable(undefined)
|
||||
|
||||
$: loadSchedules(false) || path
|
||||
|
||||
let schedules: Schedule[] | undefined = undefined
|
||||
let initialPrimarySchedule: ScheduleTrigger | false | undefined = undefined
|
||||
async function loadSchedules(forceRefresh: boolean) {
|
||||
if (!path || path == '') {
|
||||
schedules = []
|
||||
if ($primarySchedule == undefined) {
|
||||
$primarySchedule = false
|
||||
}
|
||||
initialPrimarySchedule = structuredClone($primarySchedule)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const allSchedules = await ScheduleService.listSchedules({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: path,
|
||||
isFlow
|
||||
})
|
||||
const primary = allSchedules.find((s) => s.path == path)
|
||||
let remotePrimarySchedule = primary
|
||||
? {
|
||||
summary: primary.summary,
|
||||
args: primary.args ?? {},
|
||||
cron: primary.schedule,
|
||||
timezone: primary.timezone,
|
||||
enabled: primary.enabled
|
||||
}
|
||||
: false
|
||||
if ($primarySchedule == undefined || forceRefresh) {
|
||||
$primarySchedule = remotePrimarySchedule
|
||||
}
|
||||
initialPrimarySchedule = structuredClone(remotePrimarySchedule)
|
||||
|
||||
$triggersCount = {
|
||||
...($triggersCount ?? {}),
|
||||
schedule_count: allSchedules.length,
|
||||
primary_schedule: $primarySchedule ? { schedule: $primarySchedule.cron } : undefined
|
||||
}
|
||||
schedules = allSchedules.filter((s) => s.path != path)
|
||||
} catch (e) {
|
||||
console.error('impossible to load schedules', e)
|
||||
}
|
||||
async function updateSchedules(forceRefresh: boolean) {
|
||||
loadSchedules(
|
||||
forceRefresh,
|
||||
path,
|
||||
isFlow,
|
||||
schedules,
|
||||
primarySchedule,
|
||||
initialPrimarySchedule,
|
||||
$workspaceStore ?? '',
|
||||
triggersCount
|
||||
)
|
||||
}
|
||||
|
||||
$: updateSchedules(false) || path
|
||||
|
||||
async function save() {
|
||||
const scheduleExists =
|
||||
path != '' &&
|
||||
!newItem &&
|
||||
(await ScheduleService.existsSchedule({
|
||||
workspace: $workspaceStore!,
|
||||
path
|
||||
}))
|
||||
if (scheduleExists) {
|
||||
console.log('primary schedule exists')
|
||||
if ($primarySchedule) {
|
||||
await ScheduleService.updateSchedule({
|
||||
workspace: $workspaceStore!,
|
||||
path,
|
||||
requestBody: {
|
||||
summary: $primarySchedule.summary,
|
||||
args: $primarySchedule.args,
|
||||
schedule: $primarySchedule.cron,
|
||||
timezone: $primarySchedule.timezone
|
||||
}
|
||||
})
|
||||
sendUserToast(`Primary schedule updated`)
|
||||
} else {
|
||||
await ScheduleService.deleteSchedule({ workspace: $workspaceStore!, path })
|
||||
sendUserToast(`Primary schedule deleted`)
|
||||
}
|
||||
} else {
|
||||
if ($primarySchedule) {
|
||||
await ScheduleService.createSchedule({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
path,
|
||||
script_path: path,
|
||||
is_flow: isFlow,
|
||||
summary: $primarySchedule.summary,
|
||||
args: $primarySchedule.args,
|
||||
schedule: $primarySchedule.cron,
|
||||
timezone: $primarySchedule.timezone,
|
||||
enabled: $primarySchedule.enabled
|
||||
}
|
||||
})
|
||||
sendUserToast(`Primary schedule created`)
|
||||
}
|
||||
}
|
||||
loadSchedules(true)
|
||||
await saveSchedule(path, newItem, $workspaceStore ?? '', primarySchedule, isFlow)
|
||||
updateSchedules(true)
|
||||
}
|
||||
</script>
|
||||
|
||||
<ScheduleEditor
|
||||
on:update={() => {
|
||||
loadSchedules(true)
|
||||
updateSchedules(true)
|
||||
}}
|
||||
bind:this={scheduleEditor}
|
||||
/>
|
||||
@@ -204,7 +136,7 @@
|
||||
<p class="text-xs text-tertiary mt-10">Define a schedule frequency first</p>
|
||||
{/if}
|
||||
|
||||
{#if initialPrimarySchedule != false}
|
||||
{#if $initialPrimarySchedule != false}
|
||||
<div class="flex">
|
||||
<Button size="xs" color="light" on:click={() => scheduleEditor?.openEdit(path, isFlow)}
|
||||
>Advanced</Button
|
||||
@@ -213,29 +145,31 @@
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex flex-row gap-4 mt-2">
|
||||
<Button
|
||||
on:click={() => {
|
||||
$primarySchedule = {
|
||||
summary: '',
|
||||
args: {},
|
||||
cron: '0 0 */1 * * *',
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
enabled: true
|
||||
}
|
||||
$triggersCount = {
|
||||
...($triggersCount ?? {}),
|
||||
schedule_count: ($triggersCount?.schedule_count ?? 0) + 1,
|
||||
primary_schedule: { schedule: $primarySchedule.cron }
|
||||
}
|
||||
}}
|
||||
variant="contained"
|
||||
color="dark"
|
||||
size="md"
|
||||
startIcon={{ icon: Calendar }}
|
||||
>
|
||||
Set Primary Schedule
|
||||
</Button>
|
||||
{#if initialPrimarySchedule != undefined && initialPrimarySchedule != false && !newItem}
|
||||
<div class="flex items-center">
|
||||
<Button
|
||||
on:click={() => {
|
||||
$primarySchedule = {
|
||||
summary: '',
|
||||
args: {},
|
||||
cron: '0 0 */1 * * *',
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
enabled: true
|
||||
}
|
||||
$triggersCount = {
|
||||
...($triggersCount ?? {}),
|
||||
schedule_count: ($triggersCount?.schedule_count ?? 0) + 1,
|
||||
primary_schedule: { schedule: $primarySchedule.cron }
|
||||
}
|
||||
}}
|
||||
variant="contained"
|
||||
color="dark"
|
||||
size="md"
|
||||
startIcon={{ icon: Calendar }}
|
||||
>
|
||||
Set Primary Schedule
|
||||
</Button>
|
||||
</div>
|
||||
{#if $initialPrimarySchedule != undefined && $initialPrimarySchedule != false && !newItem}
|
||||
<Button on:click={save} color="dark" size="md" startIcon={{ icon: Save }}>
|
||||
Apply changes now
|
||||
</Button>
|
||||
@@ -274,12 +208,12 @@
|
||||
{/if}
|
||||
|
||||
<Label label="Other schedules">
|
||||
{#if schedules}
|
||||
{#if schedules?.length == 0 || schedules == undefined}
|
||||
{#if $schedules}
|
||||
{#if $schedules.length == 0 || $schedules == undefined}
|
||||
<div class="text-xs text-tertiary"> No other schedules </div>
|
||||
{:else}
|
||||
<div class="flex flex-col divide-y">
|
||||
{#each schedules as schedule (schedule.path)}
|
||||
{#each $schedules as schedule (schedule.path)}
|
||||
<div class="grid grid-cols-6 text-xs items-center py-2">
|
||||
<div class="col-span-3 truncate">{schedule.path}</div>
|
||||
<div class="col-span-2 flex flex-row gap-4 flex-nowrap">
|
||||
|
||||
@@ -106,8 +106,9 @@
|
||||
? { schedule_count: 1, primary_schedule: { schedule: savedPrimarySchedule.cron } }
|
||||
: undefined
|
||||
)
|
||||
const simplifiedPoll = writable(false)
|
||||
const selectedTriggerStore = writable<
|
||||
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets'
|
||||
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets' | 'scheduledPoll'
|
||||
>('webhooks')
|
||||
|
||||
export function setPrimarySchedule(schedule: ScheduleTrigger | undefined | false) {
|
||||
@@ -138,7 +139,8 @@
|
||||
setContext<TriggerContext>('TriggerContext', {
|
||||
selectedTrigger: selectedTriggerStore,
|
||||
primarySchedule: primaryScheduleStore,
|
||||
triggersCount
|
||||
triggersCount,
|
||||
simplifiedPoll
|
||||
})
|
||||
|
||||
const enterpriseLangs = ['bigquery', 'snowflake', 'mssql']
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { ServiceLogsService } from '$lib/gen'
|
||||
import { IndexSearchService, ServiceLogsService } from '$lib/gen'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
|
||||
import ManuelDatePicker from './runs/ManuelDatePicker.svelte'
|
||||
@@ -7,9 +7,18 @@
|
||||
import LogViewer from './LogViewer.svelte'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { onDestroy } from 'svelte'
|
||||
import { onDestroy, tick } from 'svelte'
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import { truncateRev } from '$lib/utils'
|
||||
import { copyToClipboard, truncateRev } from '$lib/utils'
|
||||
import LogSnippetViewer from './LogSnippetViewer.svelte'
|
||||
import { Button, Drawer, DrawerContent } from './common'
|
||||
import ClipboardCopy from 'lucide-svelte/icons/clipboard-copy'
|
||||
import AnsiUp from 'ansi_up'
|
||||
import { scroll_into_view_if_needed_polyfill } from './multiselect/utils'
|
||||
import SplitPanesWrapper from './splitPanes/SplitPanesWrapper.svelte'
|
||||
|
||||
export let searchTerm: string
|
||||
export let queryParseErrors: string[] = []
|
||||
|
||||
let minTs: undefined | string = undefined
|
||||
let maxTs: undefined | string = undefined
|
||||
@@ -132,8 +141,9 @@
|
||||
if (upToIsLatest && selected) {
|
||||
upTo = getLatestUpTo(selected)
|
||||
}
|
||||
if (autoRefresh && !maxTsManual) {
|
||||
if (autoRefresh && searchTerm === '' && !maxTsManual) {
|
||||
timeout = setTimeout(() => {
|
||||
if (searchTerm !== '') return
|
||||
let minMax = manualPicker?.computeMinMax()
|
||||
if (minMax) {
|
||||
maxTsManual = minMax?.maxTs
|
||||
@@ -267,11 +277,129 @@
|
||||
return log
|
||||
}
|
||||
}
|
||||
|
||||
let logs: any
|
||||
|
||||
let debounceTimeout: NodeJS.Timeout | undefined = undefined
|
||||
const debouncePeriod: number = 400
|
||||
let loadingLogs = false
|
||||
let loadingLogCounts = false
|
||||
|
||||
let countsPerHost: any
|
||||
|
||||
async function searchLogs(
|
||||
searchTerm: string,
|
||||
selected: [string, string, string] | undefined,
|
||||
minTs: string | undefined,
|
||||
maxTs: string | undefined,
|
||||
allLogs: ByMode | undefined
|
||||
) {
|
||||
if (searchTerm.trim() === '') {
|
||||
debounceTimeout && clearTimeout(debounceTimeout)
|
||||
logs = undefined
|
||||
countsPerHost = undefined
|
||||
loadingLogs = false
|
||||
loadingLogCounts = false
|
||||
return
|
||||
}
|
||||
timeout && clearTimeout(timeout)
|
||||
|
||||
loadingLogCounts = true
|
||||
loadingLogs = true
|
||||
debounceTimeout && clearTimeout(debounceTimeout)
|
||||
debounceTimeout = setTimeout(async () => {
|
||||
if (allLogs) {
|
||||
const hostnames: string[] = []
|
||||
Object.entries(allLogs).forEach(([mode, wgs]) => {
|
||||
Object.entries(wgs).forEach(([wg, keys]) => {
|
||||
Object.entries(keys).forEach(([key, _log_files]) => {
|
||||
hostnames.push(`${mode},${wg},${key}`)
|
||||
})
|
||||
})
|
||||
})
|
||||
const countLogsResponse = await IndexSearchService.countSearchLogsIndex({
|
||||
searchQuery: searchTerm,
|
||||
hosts: hostnames.join(','),
|
||||
minTs,
|
||||
maxTs
|
||||
})
|
||||
countsPerHost = countLogsResponse.count_per_host
|
||||
queryParseErrors = countLogsResponse.query_parse_errors ?? []
|
||||
loadingLogCounts = false
|
||||
}
|
||||
|
||||
if (selected) {
|
||||
logs = await IndexSearchService.searchLogsIndex({
|
||||
searchQuery: searchTerm,
|
||||
mode: selected[0],
|
||||
workerGroup: selected[1] != '' ? selected[1] : undefined,
|
||||
hostname: selected[2],
|
||||
minTs,
|
||||
maxTs
|
||||
})
|
||||
}
|
||||
|
||||
loadingLogs = false
|
||||
}, debouncePeriod)
|
||||
}
|
||||
|
||||
const ansi_up = new AnsiUp()
|
||||
ansi_up.use_classes = true
|
||||
|
||||
let logDrawer: Drawer
|
||||
let logDrawerOpen: boolean
|
||||
let content: string = ''
|
||||
let hitLineNumber: number | undefined = undefined
|
||||
|
||||
async function seeLogContext(
|
||||
lineNumber: number,
|
||||
path: string,
|
||||
hostname: string,
|
||||
jsonFmt: boolean
|
||||
) {
|
||||
const res = await ServiceLogsService.getLogFile({ path: `${hostname}/${path}` })
|
||||
|
||||
content = processLogWithJsonFmt(ansi_up.ansi_to_html(res), jsonFmt)
|
||||
hitLineNumber = lineNumber
|
||||
logDrawerOpen = true
|
||||
|
||||
await tick()
|
||||
let el = document.getElementById(`log-line-${lineNumber}`)
|
||||
if (el) scroll_into_view_if_needed_polyfill(el, false)
|
||||
}
|
||||
|
||||
$: searchLogs(searchTerm, selected, minTsManual, maxTsManual, allLogs)
|
||||
</script>
|
||||
|
||||
<div class="w-full h-[70vh]" on:scroll|preventDefault>
|
||||
<Drawer bind:this={logDrawer} bind:open={logDrawerOpen} size="1400px">
|
||||
<DrawerContent title="See context" on:close={logDrawer.closeDrawer}>
|
||||
<svelte:fragment slot="actions">
|
||||
<Button
|
||||
on:click={() => copyToClipboard(content)}
|
||||
color="light"
|
||||
size="xs"
|
||||
startIcon={{
|
||||
icon: ClipboardCopy
|
||||
}}
|
||||
>
|
||||
Copy to clipboard
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
<div class="w-fit">
|
||||
<pre
|
||||
class="bg-surface-secondary text-secondary text-xs w-full p-2 whitespace-pre border rounded-md"
|
||||
>...<br />{#each content.split('\n') as line, index}<div
|
||||
id={`log-line-${index}`}
|
||||
class={index === hitLineNumber ? 'bg-yellow-200 bg-opacity-20' : ''}>{@html line}</div
|
||||
>{/each}...</pre
|
||||
>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<SplitPanesWrapper class="hidden md:block">
|
||||
<Splitpanes>
|
||||
<Pane size={40} minSize={20}>
|
||||
<Pane size={30} minSize={25}>
|
||||
<div class="p-1">
|
||||
<div
|
||||
class="flex flex-col lg:flex-row gap-y-1 justify-between w-full relative pb-4 gap-x-0.5"
|
||||
@@ -290,7 +418,17 @@
|
||||
: 'min datetime'}
|
||||
disabled
|
||||
/>
|
||||
<CalendarPicker label="min datetime" date={minTs} />
|
||||
<CalendarPicker
|
||||
label="min datetime"
|
||||
date={minTsManual}
|
||||
on:change={({ detail }) => {
|
||||
minTs = undefined
|
||||
maxTs = undefined
|
||||
allLogs = undefined
|
||||
minTsManual = detail
|
||||
getAllLogs(minTsManual, maxTsManual)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<ManuelDatePicker
|
||||
bind:minTs={minTsManual}
|
||||
@@ -304,7 +442,7 @@
|
||||
getAllLogs(minTsManual, maxTsManual)
|
||||
}}
|
||||
serviceLogsChoices
|
||||
loadText="Last 1000 logfiles"
|
||||
loadText={searchTerm === '' ? 'Last 1000 logfiles' : 'All time'}
|
||||
/>
|
||||
<div class="flex relative">
|
||||
<input
|
||||
@@ -319,7 +457,17 @@
|
||||
: 'max datetime'}
|
||||
disabled
|
||||
/>
|
||||
<CalendarPicker label="max datetime" date={maxTs} />
|
||||
<CalendarPicker
|
||||
label="max datetime"
|
||||
date={maxTsManual}
|
||||
on:change={({ detail }) => {
|
||||
minTs = undefined
|
||||
maxTs = undefined
|
||||
allLogs = undefined
|
||||
maxTsManual = detail
|
||||
getAllLogs(minTsManual, maxTsManual)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full flex-row-reverse pb-4 -mt-2 gap-2"
|
||||
@@ -335,6 +483,7 @@
|
||||
<Toggle
|
||||
size="xs"
|
||||
bind:checked={autoRefresh}
|
||||
disabled={searchTerm != ''}
|
||||
on:change={(e) => {
|
||||
if (e.detail) {
|
||||
getAllLogs(maxTs, undefined)
|
||||
@@ -353,27 +502,29 @@
|
||||
{@const minTsN = new Date(minTs).getTime()}
|
||||
{@const maxTsN = new Date(maxTs).getTime()}
|
||||
{@const diff = maxTsN - minTsN}
|
||||
<div class="flex w-full text-2xs text-tertiary pb-6">
|
||||
<div style="width: 60px;" />
|
||||
{#if searchTerm === ''}
|
||||
<div class="flex w-full text-2xs text-tertiary pb-6">
|
||||
<div style="width: 60px;" />
|
||||
|
||||
<div class="flex justify-between w-full"
|
||||
><div
|
||||
>{new Date(minTs).toLocaleTimeString([], {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}</div
|
||||
><div
|
||||
>{new Date(maxTs).toLocaleTimeString([], {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}</div
|
||||
></div
|
||||
>
|
||||
</div>
|
||||
<div class="flex justify-between w-full"
|
||||
><div
|
||||
>{new Date(minTs).toLocaleTimeString([], {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}</div
|
||||
><div
|
||||
>{new Date(maxTs).toLocaleTimeString([], {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}</div
|
||||
></div
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{#each Object.entries(allLogs) as [mode, o1]}
|
||||
<div class="w-full pb-8">
|
||||
<h2 class="pb-2 text-2xl">{mode}s</h2>
|
||||
@@ -383,11 +534,21 @@
|
||||
<h4 class="pt-4">{wg}</h4>
|
||||
{/if}
|
||||
<div class="divide-y flex flex-col">
|
||||
{#each Object.entries(o2) as [hn, files]}
|
||||
{#each Object.entries(o2).filter(([hn, files]) => {
|
||||
if (selected && selected[0] === mode && selected[1] === wg && selected[2] === hn) {
|
||||
return true
|
||||
}
|
||||
const hostKey = `${mode},${wg},${hn}`
|
||||
if (countsPerHost && countsPerHost[hostKey] && countsPerHost[hostKey].count === 0) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}) as [hn, files]}
|
||||
{@const hostKey = `${mode},${wg},${hn}`}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="w-full flex items-baseline rounded px-1 hover:bg-surface-hover cursor-pointer {selected &&
|
||||
class="w-full flex items-baseline justify-between rounded px-1 hover:bg-surface-hover cursor-pointer {selected &&
|
||||
selected[0] == mode &&
|
||||
selected[1] == wg &&
|
||||
selected[2] == hn
|
||||
@@ -405,22 +566,31 @@
|
||||
title={hn}
|
||||
style="width: 90px;">{truncateRev(hn, 8)}</div
|
||||
>
|
||||
<div class="relative grow h-8 mr-2">
|
||||
{#each files as file}
|
||||
{@const okHeight = 100.0 * ((file.ok_lines * 1.0) / (max_lines ?? 1))}
|
||||
{@const errHeight = 100.0 * ((file.err_lines * 1.0) / (max_lines ?? 1))}
|
||||
<div
|
||||
class=" w-2 bg-red-400 absolute"
|
||||
style="left: {((file.ts - minTsN) / diff) *
|
||||
100}%; height: {errHeight}%; bottom: {okHeight}%;"
|
||||
/>
|
||||
<div
|
||||
class="w-2 bg-surface-secondary-inverse absolute bottom-0"
|
||||
style="left: {((file.ts - minTsN) / diff) *
|
||||
100}%; height: {okHeight}%"
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{#if loadingLogCounts}
|
||||
<Loader2 size={15} class="animate-spin" />
|
||||
{:else if countsPerHost}
|
||||
<div class="text-tertiary text-xs">
|
||||
{countsPerHost[hostKey]?.count} matches
|
||||
</div>
|
||||
{:else}
|
||||
<div class="relative grow h-8 mr-2">
|
||||
{#each files as file}
|
||||
{@const okHeight = 100.0 * ((file.ok_lines * 1.0) / (max_lines ?? 1))}
|
||||
{@const errHeight =
|
||||
100.0 * ((file.err_lines * 1.0) / (max_lines ?? 1))}
|
||||
<div
|
||||
class=" w-2 bg-red-400 absolute"
|
||||
style="left: {((file.ts - minTsN) / diff) *
|
||||
100}%; height: {errHeight}%; bottom: {okHeight}%;"
|
||||
/>
|
||||
<div
|
||||
class="w-2 bg-surface-secondary-inverse absolute bottom-0"
|
||||
style="left: {((file.ts - minTsN) / diff) *
|
||||
100}%; height: {okHeight}%"
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -431,126 +601,162 @@
|
||||
{/if}
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane size={60} minSize={20}
|
||||
<Pane size={70} minSize={25}
|
||||
><div class="relative h-full flex flex-col gap-1"
|
||||
><div class="w-full bg-surface-primary-inverse text-tertiary text-xs text-center"
|
||||
>1 min delay: logs are compacted before being available</div
|
||||
>
|
||||
{#if selected}
|
||||
<div class="grow overflow-auto" id="logviewer">
|
||||
{#each getLogs(selected, upTo) as file}
|
||||
<div
|
||||
style="min-height: {logsContent[file.file_path]
|
||||
? 10
|
||||
: (file.ok_lines + file.err_lines) / 20}px;"
|
||||
>
|
||||
<div class="bg-surface-primary-inverse text-sm font-semibold px-1"
|
||||
>{new Date(file.ts).toLocaleTimeString([], {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}</div
|
||||
>
|
||||
{#if logsContent[file.file_path] == undefined}
|
||||
<div
|
||||
class="animate-skeleton dark:bg-frost-900/50 [animation-delay:1000ms] h-full w-full"
|
||||
{#if loadingLogs}
|
||||
<div class="flex w-full justify-center items-center h-48">
|
||||
<div class="text-tertiary text-center">
|
||||
<Loader2 size={34} class="animate-spin" />
|
||||
</div>
|
||||
</div>
|
||||
{:else if logs != undefined}
|
||||
<div class="flex flex-col min-w-full w-fit">
|
||||
{#each logs.hits as { snippet_fragment, snippet_highlighted, document }}
|
||||
<LogSnippetViewer
|
||||
content={snippet_fragment || document.logs[0]}
|
||||
highlighted={snippet_highlighted}
|
||||
on:click={() => {
|
||||
let logLineNumber = document.line_number[0]
|
||||
let logFile = document.file_name[0]
|
||||
let host = document.host[0]
|
||||
let jsonFmt = document.json_fmt[0]
|
||||
seeLogContext(logLineNumber, logFile, host, jsonFmt)
|
||||
}}
|
||||
/>
|
||||
{:else if logsContent[file.file_path]}
|
||||
{#if logsContent[file.file_path].error}
|
||||
{#if logsContent[file.file_path].error?.startsWith('Not Found')}
|
||||
<div class="text-xs pb-4 pt-2 text-secondary"
|
||||
>Log file is missing. Log files require a shared log volume to be mounted
|
||||
across servers and workers or to use the EE S3/object storage integration
|
||||
for logs. To avoid mounting a shared volume, set the EE object store logs in
|
||||
the instance settings</div
|
||||
{/each}
|
||||
{#if logs.hits.length === 0}
|
||||
<div class="text-center py-20 text-bold text-xl text-tertiary"> No logs </div>
|
||||
{/if}
|
||||
{#if logs.hits.length === 30}
|
||||
<div class="pl-6 py-6 text-sm text-secondary">
|
||||
Older matches were truncated from this search, try refining your filters to get
|
||||
more precise results.
|
||||
</div>
|
||||
{/if}
|
||||
<div class="py-20" />
|
||||
</div>
|
||||
{:else}
|
||||
{#each getLogs(selected, upTo) as file}
|
||||
<div
|
||||
style="min-height: {logsContent[file.file_path]
|
||||
? 10
|
||||
: (file.ok_lines + file.err_lines) / 20}px;"
|
||||
>
|
||||
<div class="bg-surface-primary-inverse text-sm font-semibold px-1"
|
||||
>{new Date(file.ts).toLocaleTimeString([], {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}</div
|
||||
>
|
||||
{#if logsContent[file.file_path] == undefined}
|
||||
<div
|
||||
class="animate-skeleton dark:bg-frost-900/50 [animation-delay:1000ms] h-full w-full"
|
||||
/>
|
||||
{:else if logsContent[file.file_path]}
|
||||
{#if logsContent[file.file_path].error}
|
||||
{#if logsContent[file.file_path].error?.startsWith('Not Found')}
|
||||
<div class="text-xs pb-4 pt-2 text-secondary"
|
||||
>Log file is missing. Log files require a shared log volume to be mounted
|
||||
across servers and workers or to use the EE S3/object storage integration
|
||||
for logs. To avoid mounting a shared volume, set the EE object store logs
|
||||
in the instance settings</div
|
||||
>
|
||||
{:else}
|
||||
<div class="text-xs text-red-400 pb-4"
|
||||
>{logsContent[file.file_path].error}</div
|
||||
>
|
||||
{/if}
|
||||
{:else if logsContent[file.file_path].content}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div on:click|preventDefault class="pr-2"
|
||||
><LogViewer
|
||||
noAutoScroll
|
||||
noMaxH
|
||||
isLoading={false}
|
||||
tag={undefined}
|
||||
content={processLogWithJsonFmt(
|
||||
logsContent[file.file_path].content,
|
||||
file.json_fmt
|
||||
)}
|
||||
/></div
|
||||
>
|
||||
{:else}
|
||||
<div class="text-xs text-red-400 pb-4"
|
||||
>{logsContent[file.file_path].error}</div
|
||||
>
|
||||
<div>No logs</div>
|
||||
{/if}
|
||||
{:else if logsContent[file.file_path].content}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div on:click|preventDefault
|
||||
><LogViewer
|
||||
noAutoScroll
|
||||
noMaxH
|
||||
isLoading={false}
|
||||
tag={undefined}
|
||||
content={processLogWithJsonFmt(
|
||||
logsContent[file.file_path].content,
|
||||
file.json_fmt
|
||||
)}
|
||||
/></div
|
||||
>
|
||||
{:else}
|
||||
<div>No logs</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{#if searchTerm == ''}
|
||||
<div class="flex w-full items-center gap-4">
|
||||
<div class="text-tertiary px-1 text-2xs">Last 5 log files up to:</div>
|
||||
<div class="flex grow text-xs justify-center px-2 items-center gap-2">
|
||||
{#if upTo}
|
||||
<button
|
||||
on:click={() => {
|
||||
if (upTo) {
|
||||
upToIsLatest = false
|
||||
upTo = new Date(new Date(upTo).getTime() - 5 * 60 * 1000).toISOString()
|
||||
}
|
||||
}}>{'<'} 5m</button
|
||||
>
|
||||
{:else}
|
||||
<div />
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-1 relative items-center"
|
||||
><div class="flex gap-1 relative">
|
||||
<input
|
||||
type="text"
|
||||
value={upTo
|
||||
? new Date(upTo).toLocaleTimeString([], {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
: ''}
|
||||
disabled
|
||||
/><CalendarPicker bind:date={upTo} label="Logs up to" /></div
|
||||
></div
|
||||
>
|
||||
{#if upTo}
|
||||
<button
|
||||
on:click={() => {
|
||||
if (upTo) {
|
||||
upToIsLatest = false
|
||||
upTo = new Date(new Date(upTo).getTime() + 5 * 60 * 1000).toISOString()
|
||||
}
|
||||
}}>5m {'>'}</button
|
||||
>
|
||||
{:else}
|
||||
<div />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="flex w-full items-center gap-4">
|
||||
<div class="text-tertiary px-1 text-2xs">Last 5 log files up to:</div>
|
||||
<div class="flex grow text-xs justify-center px-2 items-center gap-2">
|
||||
{#if upTo}
|
||||
<div>
|
||||
<button
|
||||
class="text-xs"
|
||||
on:click={() => {
|
||||
if (upTo) {
|
||||
upToIsLatest = false
|
||||
upTo = new Date(new Date(upTo).getTime() - 5 * 60 * 1000).toISOString()
|
||||
}
|
||||
}}>{'<'} 5m</button
|
||||
upTo = new Date().toISOString()
|
||||
upToIsLatest = true
|
||||
}}>now</button
|
||||
>
|
||||
{:else}
|
||||
<div />
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-1 relative items-center"
|
||||
><div class="flex gap-1 relative">
|
||||
<input
|
||||
type="text"
|
||||
value={upTo
|
||||
? new Date(upTo).toLocaleTimeString([], {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
: ''}
|
||||
disabled
|
||||
/><CalendarPicker bind:date={upTo} label="Logs up to" /></div
|
||||
></div
|
||||
>
|
||||
{#if upTo}
|
||||
<button
|
||||
on:click={() => {
|
||||
if (upTo) {
|
||||
upToIsLatest = false
|
||||
upTo = new Date(new Date(upTo).getTime() + 5 * 60 * 1000).toISOString()
|
||||
}
|
||||
}}>5m {'>'}</button
|
||||
>
|
||||
{:else}
|
||||
<div />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="text-xs"
|
||||
on:click={() => {
|
||||
upTo = new Date().toISOString()
|
||||
upToIsLatest = true
|
||||
}}>now</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex justify-center items-center pt-8">Select a host to see its logs</div>
|
||||
{/if}</div
|
||||
></Pane
|
||||
>
|
||||
</Splitpanes>
|
||||
</div>
|
||||
</SplitPanesWrapper>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
export let lightMode: boolean = false
|
||||
export let eeOnly: boolean = false
|
||||
|
||||
export let size: 'sm' | 'xs' = 'sm'
|
||||
export let size: 'sm' | 'xs' | '2xs' = 'sm'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const bothOptions = Boolean(options.left) && Boolean(options.right)
|
||||
@@ -39,7 +39,7 @@
|
||||
class={twMerge(
|
||||
'mr-2 font-medium duration-50 select-none',
|
||||
bothOptions || textDisabled ? (checked ? 'text-disabled' : 'text-primary') : 'text-primary',
|
||||
size === 'xs' ? 'text-xs' : 'text-sm',
|
||||
size === 'xs' ? 'text-xs' : size === '2xs' ? 'text-[0.5rem]' : 'text-sm',
|
||||
textClass
|
||||
)}
|
||||
style={textStyle}
|
||||
@@ -76,6 +76,8 @@
|
||||
: 'peer-checked:bg-nord-950 dark:peer-checked:bg-nord-400',
|
||||
size === 'sm'
|
||||
? 'w-11 h-6 after:top-0.5 after:left-[2px] after:h-5 after:w-5'
|
||||
: size === '2xs'
|
||||
? 'w-5 h-3 after:top-0.5 after:left-[2px] after:h-2 after:w-2'
|
||||
: 'w-7 h-4 after:top-0.5 after:left-[2px] after:h-3 after:w-3'
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -265,7 +265,7 @@ Generate a description for the flow below:
|
||||
</div>
|
||||
{#if elementType === 'textarea'}
|
||||
<div>
|
||||
<div class="flex flex-row-reverse text-2xs text-tertiary -mt-4">GH Markdown</div>
|
||||
<div class="flex flex-row-reverse !text-3xs text-tertiary -mt-4">GH Markdown</div>
|
||||
<textarea
|
||||
bind:this={el}
|
||||
bind:value={content}
|
||||
|
||||
@@ -11,8 +11,10 @@
|
||||
| 'schedules'
|
||||
| 'cli'
|
||||
| 'routes'
|
||||
| 'websockets' = 'webhooks'
|
||||
| 'websockets'
|
||||
| 'scheduledPoll' = 'webhooks'
|
||||
export let flow_json: any | undefined = undefined
|
||||
export let simplfiedPoll: boolean = false
|
||||
|
||||
export let isOperator: boolean = false
|
||||
|
||||
@@ -45,7 +47,7 @@
|
||||
<slot name="script" />
|
||||
</TabContent>
|
||||
<TabContent value="triggers" class="h-full pt-2">
|
||||
<DetailPageTriggerPanel bind:triggerSelected>
|
||||
<DetailPageTriggerPanel {simplfiedPoll} bind:triggerSelected>
|
||||
<slot slot="webhooks" name="webhooks" />
|
||||
<slot slot="routes" name="routes" />
|
||||
<slot slot="websockets" name="websockets" />
|
||||
|
||||
@@ -20,13 +20,15 @@
|
||||
|
||||
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(undefined)
|
||||
const selectedTriggerStore = writable<
|
||||
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets'
|
||||
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets' | 'scheduledPoll'
|
||||
>('webhooks')
|
||||
|
||||
const simplifiedPoll = writable(false)
|
||||
setContext<TriggerContext>('TriggerContext', {
|
||||
selectedTrigger: selectedTriggerStore,
|
||||
primarySchedule: primaryScheduleStore,
|
||||
triggersCount
|
||||
triggersCount,
|
||||
simplifiedPoll
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -41,6 +43,7 @@
|
||||
</Pane>
|
||||
<Pane size={35} minSize={15}>
|
||||
<DetailPageDetailPanel
|
||||
simplfiedPoll={$simplifiedPoll}
|
||||
bind:triggerSelected={$selectedTriggerStore}
|
||||
bind:selected
|
||||
{isOperator}
|
||||
@@ -83,7 +86,10 @@
|
||||
<slot name="save_inputs" />
|
||||
</TabContent>
|
||||
<TabContent value="triggers" class="flex flex-col flex-1 h-full">
|
||||
<DetailPageTriggerPanel bind:triggerSelected={$selectedTriggerStore}>
|
||||
<DetailPageTriggerPanel
|
||||
simplfiedPoll={$simplifiedPoll}
|
||||
bind:triggerSelected={$selectedTriggerStore}
|
||||
>
|
||||
<slot slot="webhooks" name="webhooks" />
|
||||
<slot slot="routes" name="routes" />
|
||||
<slot slot="script" name="script" />
|
||||
|
||||
@@ -10,64 +10,70 @@
|
||||
| 'schedules'
|
||||
| 'cli'
|
||||
| 'routes'
|
||||
| 'websockets' = 'webhooks'
|
||||
| 'websockets'
|
||||
| 'scheduledPoll' = 'webhooks'
|
||||
export let simplfiedPoll: boolean = false
|
||||
</script>
|
||||
|
||||
<HighlightTheme />
|
||||
|
||||
<Tabs bind:selected={triggerSelected}>
|
||||
<Tab value="webhooks">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<Webhook size={12} />
|
||||
Webhooks
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="schedules">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<CalendarCheck2 size={12} />
|
||||
Schedules
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="routes">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<Route size={12} />
|
||||
HTTP
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="websockets">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<Unplug size={12} />
|
||||
Websockets
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="emails">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<MailIcon size={12} />
|
||||
Email
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="cli">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<Terminal size={12} />
|
||||
CLI
|
||||
</span>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
{#if !simplfiedPoll}
|
||||
<Tabs bind:selected={triggerSelected}>
|
||||
<Tab value="webhooks">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<Webhook size={12} />
|
||||
Webhooks
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="schedules">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<CalendarCheck2 size={12} />
|
||||
Schedules
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="routes">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<Route size={12} />
|
||||
HTTP
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="websockets">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<Unplug size={12} />
|
||||
Websockets
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="emails">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<MailIcon size={12} />
|
||||
Email
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="cli">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<Terminal size={12} />
|
||||
CLI
|
||||
</span>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<div class="h-[calc(100%-32px)]">
|
||||
<div class="h-full overflow-auto">
|
||||
{#if triggerSelected === 'webhooks'}
|
||||
<slot name="webhooks" />
|
||||
{:else if triggerSelected === 'routes'}
|
||||
<slot name="routes" />
|
||||
{:else if triggerSelected === 'emails'}
|
||||
<slot name="emails" />
|
||||
{:else if triggerSelected === 'schedules'}
|
||||
<slot name="schedules" />
|
||||
{:else if triggerSelected === 'websockets'}
|
||||
<slot name="websockets" />
|
||||
{:else if triggerSelected === 'cli'}
|
||||
<slot name="cli" />
|
||||
{/if}
|
||||
<div class="h-[calc(100%-32px)]">
|
||||
<div class="h-full overflow-auto">
|
||||
{#if triggerSelected === 'webhooks'}
|
||||
<slot name="webhooks" />
|
||||
{:else if triggerSelected === 'routes'}
|
||||
<slot name="routes" />
|
||||
{:else if triggerSelected === 'emails'}
|
||||
<slot name="emails" />
|
||||
{:else if triggerSelected === 'schedules'}
|
||||
<slot name="schedules" />
|
||||
{:else if triggerSelected === 'websockets'}
|
||||
<slot name="websockets" />
|
||||
{:else if triggerSelected === 'cli'}
|
||||
<slot name="cli" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<slot name="schedules" />
|
||||
{/if}
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
export let kind: 'trigger' | 'script' | 'preprocessor' | 'failure' | 'approval'
|
||||
export let selectedKind: 'script' | 'flow' | 'approval' | 'trigger' | 'preprocessor' | 'failure' =
|
||||
kind
|
||||
export let displayPath = false
|
||||
|
||||
let lang: FlowCopilotModule['lang'] = undefined
|
||||
let selectedCompletion: FlowCopilotModule['selectedCompletion'] = undefined
|
||||
@@ -424,6 +425,7 @@
|
||||
selected={selectedByKeyboard - inlineScripts?.length - aiLength - topLevelNodes.length}
|
||||
on:pickScript
|
||||
on:pickFlow
|
||||
{displayPath}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -446,6 +448,7 @@
|
||||
topLevelNodes.length}
|
||||
on:pickScript
|
||||
bind:loading
|
||||
{displayPath}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -12,15 +12,16 @@
|
||||
|
||||
let flow: Flow | undefined = undefined
|
||||
|
||||
const selectedTriggerStore = writable<'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes'>(
|
||||
'webhooks'
|
||||
)
|
||||
const selectedTriggerStore = writable<
|
||||
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'scheduledPoll'
|
||||
>('webhooks')
|
||||
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(undefined)
|
||||
const triggersCount = writable<TriggersCount | undefined>(undefined)
|
||||
setContext<TriggerContext>('TriggerContext', {
|
||||
primarySchedule: primaryScheduleStore,
|
||||
selectedTrigger: selectedTriggerStore,
|
||||
triggersCount: triggersCount
|
||||
triggersCount: triggersCount,
|
||||
simplifiedPoll: writable(false)
|
||||
})
|
||||
|
||||
async function loadFlow(path: string) {
|
||||
|
||||
@@ -45,11 +45,12 @@ export async function pickScript(
|
||||
path: string,
|
||||
summary: string,
|
||||
id: string,
|
||||
hash?: string
|
||||
hash?: string,
|
||||
kind?: string
|
||||
): Promise<[FlowModule & { value: PathScript }, FlowModuleState]> {
|
||||
const flowModule: FlowModule & { value: PathScript } = {
|
||||
id,
|
||||
value: { type: 'script', path, hash, input_transforms: {} },
|
||||
value: { type: 'script', path, hash, input_transforms: {}, is_trigger: kind === 'trigger' },
|
||||
summary
|
||||
}
|
||||
|
||||
@@ -82,7 +83,13 @@ export async function createInlineScriptModule(
|
||||
const flowModule: FlowModule = {
|
||||
id,
|
||||
summary,
|
||||
value: { type: 'rawscript', content: code, language, input_transforms: {} }
|
||||
value: {
|
||||
type: 'rawscript',
|
||||
content: code,
|
||||
language,
|
||||
input_transforms: {},
|
||||
is_trigger: kind === 'trigger'
|
||||
}
|
||||
}
|
||||
|
||||
return [flowModule, await loadFlowModuleState(flowModule)]
|
||||
|
||||
@@ -41,11 +41,13 @@
|
||||
export let bold: boolean = false
|
||||
export let id: string | undefined = undefined
|
||||
export let label: string
|
||||
export let path: string = ''
|
||||
export let modType: string | undefined = undefined
|
||||
export let bgColor: string = ''
|
||||
export let concurrency: boolean = false
|
||||
export let retries: number | undefined = undefined
|
||||
export let warningMessage: string | undefined = undefined
|
||||
export let isTrigger: boolean = false
|
||||
|
||||
const { flowInputsStore } = getContext<{ flowInputsStore: Writable<FlowInput | undefined> }>(
|
||||
'FlowGraphContext'
|
||||
@@ -131,7 +133,7 @@
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class={classNames(
|
||||
'w-full module flex rounded-sm cursor-pointer',
|
||||
'w-full module flex rounded-sm cursor-pointer max-w-full',
|
||||
selected ? 'outline outline-offset-0 outline-2 outline-slate-500 dark:outline-gray-400' : '',
|
||||
'flex relative',
|
||||
$copilotCurrentStepStore === id ? 'z-[901]' : ''
|
||||
@@ -139,7 +141,7 @@
|
||||
style="width: 275px; height: 34px; background-color: {bgColor};"
|
||||
on:mouseenter={() => (hover = true)}
|
||||
on:mouseleave={() => (hover = false)}
|
||||
on:click
|
||||
on:click|preventDefault|stopPropagation
|
||||
>
|
||||
<div class="absolute text-sm right-12 -bottom-3 flex flex-row gap-1 z-10">
|
||||
{#if retry}
|
||||
@@ -185,7 +187,11 @@
|
||||
>
|
||||
<Square size={12} />
|
||||
</div>
|
||||
<svelte:fragment slot="text">Early stop/break</svelte:fragment>
|
||||
<svelte:fragment slot="text"
|
||||
>{isTrigger
|
||||
? 'Stop early if there are no new events'
|
||||
: 'Early stop/break'}</svelte:fragment
|
||||
>
|
||||
</Popover>
|
||||
{/if}
|
||||
{#if skip}
|
||||
@@ -243,11 +249,22 @@
|
||||
<slot name="icon" />
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
class="absolute left-1/2 transform -translate-x-1/2 text-center truncate"
|
||||
class:font-bold={bold}
|
||||
style="max-width: calc(100% - {marginLeft}px)">{label}</div
|
||||
|
||||
<Popover
|
||||
class="absolute left-1/2 transform -translate-x-1/2 center-center"
|
||||
style="max-width: calc(100% - {marginLeft}px)"
|
||||
>
|
||||
<div class="text-center truncate {bold ? '!font-bold' : 'font-normal'}">
|
||||
{label}
|
||||
</div>
|
||||
<svelte:fragment slot="text">
|
||||
<div>
|
||||
<div>{label}</div>
|
||||
{#if path != ''}<div>{path}</div>{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
|
||||
<div class="flex items-center space-x-2 relative max-w-[25%]" bind:clientWidth={idBadgeWidth}>
|
||||
{#if id && id !== 'preprocessor' && !id.startsWith('failure')}
|
||||
<Badge color="indigo" wrapperClass="max-w-full" baseClass="max-w-full truncate" title={id}>
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
import { tutorialInProgress } from '$lib/tutorialUtils'
|
||||
import FlowGraphV2 from '$lib/components/graph/FlowGraphV2.svelte'
|
||||
import { replaceId } from '../flowStore'
|
||||
import { setScheduledPollSchedule, type TriggerContext } from '$lib/components/triggers'
|
||||
|
||||
export let modules: FlowModule[] | undefined
|
||||
export let sidebarSize: number | undefined = undefined
|
||||
@@ -47,7 +48,7 @@
|
||||
|
||||
const { selectedId, moving, history, flowStateStore, flowStore, flowInputsStore, pathStore } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
const { primarySchedule, triggersCount } = getContext<TriggerContext>('TriggerContext')
|
||||
async function insertNewModuleAtIndex(
|
||||
modules: FlowModule[],
|
||||
index: number,
|
||||
@@ -74,10 +75,17 @@
|
||||
push(history, $flowStore)
|
||||
var module = emptyModule($flowStateStore, $flowStore, kind == 'flow')
|
||||
var state = emptyFlowModuleState()
|
||||
$flowStateStore[module.id] = state
|
||||
if (wsFlow) {
|
||||
;[module, state] = await pickFlow(wsFlow.path, wsFlow.summary, module.id)
|
||||
} else if (wsScript) {
|
||||
;[module, state] = await pickScript(wsScript.path, wsScript.summary, module.id, wsScript.hash)
|
||||
;[module, state] = await pickScript(
|
||||
wsScript.path,
|
||||
wsScript.summary,
|
||||
module.id,
|
||||
wsScript.hash,
|
||||
kind
|
||||
)
|
||||
} else if (kind == 'forloop') {
|
||||
;[module, state] = await createLoop(
|
||||
module.id,
|
||||
@@ -89,10 +97,7 @@
|
||||
;[module, state] = await createBranches(module.id)
|
||||
} else if (kind == 'branchall') {
|
||||
;[module, state] = await createBranchAll(module.id)
|
||||
}
|
||||
$flowStateStore[module.id] = state
|
||||
|
||||
if (inlineScript) {
|
||||
} else if (inlineScript) {
|
||||
const { language, kind, subkind } = inlineScript
|
||||
;[module, state] = await createInlineScriptModule(
|
||||
language,
|
||||
@@ -108,6 +113,7 @@
|
||||
module.summary = 'Approval'
|
||||
}
|
||||
}
|
||||
$flowStateStore[module.id] = state
|
||||
|
||||
if (kind == 'approval') {
|
||||
module.suspend = { required_events: 1, timeout: 1800 }
|
||||
@@ -116,6 +122,9 @@
|
||||
expr: '!result || (Array.isArray(result) && result.length == 0)',
|
||||
skip_if_stopped: true
|
||||
}
|
||||
} else if (kind == 'end') {
|
||||
module.summary = 'Terminate flow'
|
||||
module.stop_after_if = { skip_if_stopped: false, expr: 'true' }
|
||||
}
|
||||
|
||||
if (!modules) return [module]
|
||||
@@ -403,6 +412,7 @@
|
||||
undefined
|
||||
)
|
||||
setExpr(detail.modules[index + 1], `results.${id}`)
|
||||
setScheduledPollSchedule(primarySchedule, triggersCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import { Cross, Zap } from 'lucide-svelte'
|
||||
import { Cross } from 'lucide-svelte'
|
||||
import StepGenQuick from '$lib/components/copilot/StepGenQuick.svelte'
|
||||
import FlowInputsQuick from '../content/FlowInputsQuick.svelte'
|
||||
import type { FlowModule } from '$lib/gen'
|
||||
@@ -11,6 +11,7 @@
|
||||
import TopLevelNode from '../pickers/TopLevelNode.svelte'
|
||||
import PopupV2 from '$lib/components/common/popup/PopupV2.svelte'
|
||||
import { flip, offset } from 'svelte-floating-ui/dom'
|
||||
import { SchedulePollIcon } from '$lib/components/icons'
|
||||
|
||||
// import type { Writable } from 'svelte/store'
|
||||
|
||||
@@ -22,6 +23,7 @@
|
||||
export let disableAi = false
|
||||
export let kind: 'script' | 'trigger' | 'preprocessor' | 'failure' = 'script'
|
||||
export let allowTrigger = true
|
||||
export let iconSize = 12
|
||||
|
||||
type Alignment = 'start' | 'end' | 'center'
|
||||
type Side = 'top' | 'bottom'
|
||||
@@ -44,6 +46,11 @@
|
||||
let small = false
|
||||
let open = false
|
||||
|
||||
let width = 0
|
||||
let height = 0
|
||||
|
||||
$: displayPath = width > 650 || height > 400
|
||||
|
||||
$: small = kind === 'preprocessor' || kind === 'failure'
|
||||
</script>
|
||||
|
||||
@@ -70,29 +77,35 @@ shouldUsePortal={true} -->
|
||||
id={`flow-editor-add-step-${index}`}
|
||||
type="button"
|
||||
class={twMerge(
|
||||
'w-5 h-5 flex items-center justify-center',
|
||||
'outline-[1px] outline dark:outline-gray-500 outline-gray-300',
|
||||
'text-secondary',
|
||||
'bg-surface focus:outline-none hover:bg-surface-hover rounded'
|
||||
'w-[17.5px] h-[17.5px] flex items-center justify-center outline-[1px] outline dark:outline-gray-500 outline-gray-300 text-secondary bg-surface focus:outline-none hover:bg-surface-hover rounded',
|
||||
$$props.class
|
||||
)}
|
||||
on:pointerdown|preventDefault|stopPropagation={pointerdown}
|
||||
on:pointerdown|preventDefault|stopPropagation={() => {
|
||||
dispatch('open')
|
||||
pointerdown()
|
||||
}}
|
||||
on:pointerup={pointerup}
|
||||
>
|
||||
{#if kind === 'trigger'}
|
||||
<Zap size={12} />
|
||||
<SchedulePollIcon size={14} />
|
||||
{:else}
|
||||
<Cross size={12} />
|
||||
<Cross size={iconSize} />
|
||||
{/if}
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
<!-- FOO -->
|
||||
<div
|
||||
id="flow-editor-insert-module"
|
||||
class="flex flex-col h-[400px] {small ? 'w-[450px]' : 'w-[650px]'} pt-1 pr-1 pl-1 gap-1.5"
|
||||
class="flex flex-col h-[400px] {small
|
||||
? 'w-[450px]'
|
||||
: 'w-[650px]'} pt-1 pr-1 pl-1 gap-1.5 resize overflow-auto {small
|
||||
? 'min-w-[450px]'
|
||||
: 'min-w-[650px]'} min-h-[400px]"
|
||||
on:wheel={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
role="none"
|
||||
bind:clientWidth={width}
|
||||
bind:clientHeight={height}
|
||||
>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<StepGenQuick
|
||||
@@ -109,7 +122,7 @@ shouldUsePortal={true} -->
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row grow min-h-0">
|
||||
{#if kind === 'script' || kind == 'trigger'}
|
||||
{#if kind === 'script'}
|
||||
<div class="flex-none flex flex-col text-xs text-primary">
|
||||
<TopLevelNode
|
||||
label="Action"
|
||||
@@ -201,6 +214,7 @@ shouldUsePortal={true} -->
|
||||
on:pickFlow
|
||||
{preFilter}
|
||||
{small}
|
||||
{displayPath}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import { msToSec } from '$lib/utils'
|
||||
import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte'
|
||||
import FlowJobsMenu from './FlowJobsMenu.svelte'
|
||||
import { isTriggerStep } from '$lib/components/graph/graphBuilder'
|
||||
|
||||
export let mod: FlowModule
|
||||
export let insertable: boolean
|
||||
@@ -19,6 +20,7 @@
|
||||
export let modules: FlowModule[]
|
||||
export let moving: string | undefined = undefined
|
||||
export let duration_ms: number | undefined = undefined
|
||||
export let isTrigger: boolean = false
|
||||
|
||||
export let retries: number | undefined = undefined
|
||||
export let flowJobs:
|
||||
@@ -173,6 +175,8 @@
|
||||
(mod.value.type === 'rawscript'
|
||||
? `Inline ${prettyLanguage(mod.value.language)}`
|
||||
: 'To be defined')}
|
||||
path={`path` in mod.value && mod.summary ? mod.value.path : ''}
|
||||
isTrigger={isTriggerStep(mod)}
|
||||
>
|
||||
<div slot="icon">
|
||||
{#if mod.value.type === 'rawscript'}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { Badge } from '$lib/components/common'
|
||||
import type { FlowModule } from '$lib/gen'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
|
||||
import { getContext } from 'svelte'
|
||||
import type { FlowCopilotContext } from '$lib/components/copilot/flow'
|
||||
import VirtualItemWrapper from './VirtualItemWrapper.svelte'
|
||||
|
||||
export let label: string | undefined = undefined
|
||||
export let bgColor: string = ''
|
||||
@@ -15,45 +15,22 @@
|
||||
export let hideId: boolean = false
|
||||
export let preLabel: string | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
insert: {
|
||||
script?: { path: string; summary: string; hash: string | undefined }
|
||||
detail: 'script' | 'forloop' | 'branchone' | 'branchall' | 'trigger' | 'move'
|
||||
modules: FlowModule[]
|
||||
index: number
|
||||
}
|
||||
select: string
|
||||
}>()
|
||||
|
||||
const { currentStepStore: copilotCurrentStepStore } =
|
||||
getContext<FlowCopilotContext | undefined>('FlowCopilotContext') || {}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class={classNames(
|
||||
'w-full flex relative overflow-hidden rounded-sm',
|
||||
selectable ? 'cursor-pointer' : '',
|
||||
selected ? 'outline outline-offset-1 outline-2 outline-gray-600' : '',
|
||||
label === 'Input' && $copilotCurrentStepStore === 'Input' ? 'z-[901]' : ''
|
||||
)}
|
||||
style="width: 275px; max-height: 34px; background-color: {bgColor} !important;"
|
||||
on:click={() => {
|
||||
if (selectable) {
|
||||
if (id) {
|
||||
dispatch('select', id)
|
||||
} else {
|
||||
dispatch('select', label || label || '')
|
||||
}
|
||||
}
|
||||
}}
|
||||
title={(label ? label + ' ' : '') + (label ?? '')}
|
||||
id={`flow-editor-virtual-${encodeURIComponent(label || label || '')}`}
|
||||
<VirtualItemWrapper
|
||||
{label}
|
||||
{bgColor}
|
||||
{selected}
|
||||
{selectable}
|
||||
{id}
|
||||
onTop={label === 'Input' && $copilotCurrentStepStore === 'Input'}
|
||||
on:select
|
||||
>
|
||||
<div
|
||||
style={borderColor ? `border-color: ${borderColor};` : 'border: 0'}
|
||||
class="flex gap-1 justify-between {center
|
||||
class="flex flex-row gap-1 justify-between {center
|
||||
? 'items-center'
|
||||
: 'items-baseline'} w-full overflow-hidden rounded-sm border p-2 text-2xs module text-primary border-gray-400 dark:border-gray-600"
|
||||
>
|
||||
@@ -61,8 +38,7 @@
|
||||
<slot name="icon" />
|
||||
<span class="mr-2" />
|
||||
{/if}
|
||||
<div />
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex flex-col flex-grow shrink-0 max-w-full min-w-0">
|
||||
{#if label}
|
||||
<div class="truncate text-center">{label}</div>
|
||||
{/if}
|
||||
@@ -70,10 +46,12 @@
|
||||
<div class="truncate text-2xs text-center"><pre>{preLabel}</pre></div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
{#if id && !hideId}
|
||||
<Badge color="indigo">{id}</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
{#if id && !hideId}
|
||||
<div class="flex items-center shrink min-w-0">
|
||||
<Badge color="indigo" wrapperClass="w-full" baseClass="max-w-full" title={id}>
|
||||
<span class="max-w-full text-2xs truncate">{id}</span>
|
||||
</Badge>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</VirtualItemWrapper>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import type { FlowModule } from '$lib/gen'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let label: string | undefined
|
||||
export let selectable: boolean
|
||||
export let selected: boolean
|
||||
export let id: string | undefined
|
||||
export let onTop: boolean = false
|
||||
export let bgColor: string
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
insert: {
|
||||
script?: { path: string; summary: string; hash: string | undefined }
|
||||
detail: 'script' | 'forloop' | 'branchone' | 'branchall' | 'trigger' | 'move'
|
||||
modules: FlowModule[]
|
||||
index: number
|
||||
}
|
||||
select: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class={classNames(
|
||||
'w-full flex relative overflow-hidden rounded-sm',
|
||||
selectable ? 'cursor-pointer' : '',
|
||||
selected ? 'outline outline-offset-1 outline-2 outline-gray-600 dark:outline-gray-400' : '',
|
||||
onTop ? 'z-[901]' : ''
|
||||
)}
|
||||
style="width: 275px; max-height: 34px; background-color: {bgColor} !important;"
|
||||
on:click={() => {
|
||||
if (selectable) {
|
||||
if (id) {
|
||||
dispatch('select', id)
|
||||
} else {
|
||||
dispatch('select', label || label || '')
|
||||
}
|
||||
}
|
||||
}}
|
||||
title={label ? label + ' ' : ''}
|
||||
id={`flow-editor-virtual-${encodeURIComponent(label || label || '')}`}><slot /></div
|
||||
>
|
||||
@@ -5,6 +5,7 @@
|
||||
import { APP_TO_ICON_COMPONENT } from '$lib/components/icons'
|
||||
import { IntegrationService, ScriptService, type HubScriptKind } from '$lib/gen'
|
||||
import { Circle } from 'lucide-svelte'
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
|
||||
export let kind: HubScriptKind & string = 'script'
|
||||
export let filter = ''
|
||||
@@ -25,6 +26,7 @@
|
||||
app: string
|
||||
kind: HubScriptKind
|
||||
}[] = []
|
||||
export let displayPath = false
|
||||
|
||||
export let apps: string[] = []
|
||||
let allApps: string[] = []
|
||||
@@ -138,32 +140,56 @@
|
||||
<ul>
|
||||
{#each items as item, index (item.path)}
|
||||
<li class="w-full">
|
||||
<button
|
||||
class="px-3 py-2 gap-2 flex flex-row w-full hover:bg-surface-hover transition-all items-center rounded-md {index ===
|
||||
selected
|
||||
? 'bg-surface-hover'
|
||||
: ''}"
|
||||
on:click={() => dispatch('pickScript', item)}
|
||||
>
|
||||
<div class={classNames('flex justify-center items-center')}>
|
||||
{#if item['app'] in APP_TO_ICON_COMPONENT}
|
||||
<svelte:component this={APP_TO_ICON_COMPONENT[item['app']]} height={14} width={14} />
|
||||
{:else}
|
||||
<div
|
||||
class="w-[14px] h-[14px] text-gray-400 flex flex-row items-center justify-center"
|
||||
<Popover class="w-full" placement="right" forceOpen={index === selected}>
|
||||
<svelte:fragment slot="text">
|
||||
<div class="flex flex-col">
|
||||
<div class="text-left text-xs font-normal leading-tight py-0"
|
||||
>{item.summary ?? ''}</div
|
||||
>
|
||||
<Circle size="12" />
|
||||
<div class="text-left text-2xs font-normal">
|
||||
{item.path ?? ''}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<button
|
||||
class="px-3 py-2 gap-2 flex flex-row w-full hover:bg-surface-hover transition-all items-center rounded-md {index ===
|
||||
selected
|
||||
? 'bg-surface-hover'
|
||||
: ''}"
|
||||
on:click={() => dispatch('pickScript', item)}
|
||||
>
|
||||
<div class={classNames('flex justify-center items-center')}>
|
||||
{#if item['app'] in APP_TO_ICON_COMPONENT}
|
||||
<svelte:component
|
||||
this={APP_TO_ICON_COMPONENT[item['app']]}
|
||||
height={14}
|
||||
width={14}
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class="w-[14px] h-[14px] text-gray-400 flex flex-row items-center justify-center"
|
||||
>
|
||||
<Circle size="12" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<span class="grow truncate text-left text-2xs text-primary font-normal">
|
||||
{item.summary ?? ''}
|
||||
</span>
|
||||
{#if index === selected}
|
||||
<kbd class="!text-xs">↵</kbd>
|
||||
{/if}
|
||||
</button>
|
||||
<div class="flex flex-col grow min-w-0">
|
||||
<div
|
||||
class="grow truncate text-left text-2xs text-primary font-normal leading-tight py-0.5"
|
||||
>{item.summary ?? ''}</div
|
||||
>
|
||||
{#if displayPath && item.path}
|
||||
<div class="grow truncate text-left text-2xs text-secondary font-[220]">
|
||||
{item.path}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if index === selected}
|
||||
<kbd class="!text-xs">↵</kbd>
|
||||
{/if}
|
||||
</button>
|
||||
</Popover>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
import { emptyString } from '$lib/utils'
|
||||
import { Code2 } from 'lucide-svelte'
|
||||
import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte'
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
|
||||
export let kind: 'script' | 'trigger' | 'approval' | 'failure' | 'flow' | 'preprocessor' =
|
||||
'script'
|
||||
export let isTemplate: boolean | undefined = undefined
|
||||
export let selected: number | undefined = undefined
|
||||
export let displayPath = false
|
||||
|
||||
type Item = {
|
||||
path: string
|
||||
@@ -105,35 +107,52 @@
|
||||
<ul>
|
||||
{#each filteredWithOwner ?? [] as { path, hash, summary, marked }, index}
|
||||
<li class="w-full">
|
||||
<button
|
||||
class="px-3 py-2 gap-2 flex flex-row w-full hover:bg-surface-hover transition-all items-center rounded-md {index ===
|
||||
selected
|
||||
? 'bg-surface-hover'
|
||||
: ''}"
|
||||
on:click={() => {
|
||||
if (kind == 'flow') {
|
||||
dispatch('pickFlow', { path: path })
|
||||
} else {
|
||||
dispatch('pickScript', { path: path, hash: lockHash ? hash : undefined, kind })
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if kind == 'flow'}
|
||||
<BarsStaggered size={14} class="shrink-0" />
|
||||
{:else}
|
||||
<Code2 size={14} />
|
||||
{/if}
|
||||
<span class="grow min-w-0 truncate text-left text-2xs text-primary font-normal">
|
||||
{#if marked}
|
||||
{@html marked}
|
||||
{:else}
|
||||
{!summary || summary.length == 0 ? path : summary}
|
||||
{/if}</span
|
||||
<Popover class="w-full " placement="right" forceOpen={index === selected}>
|
||||
<svelte:fragment slot="text">
|
||||
<div class="flex flex-col">
|
||||
<div class="text-left text-xs font-normal leading-tight py-0">{summary ?? ''}</div>
|
||||
<div class="text-left text-2xs font-normal">
|
||||
{path ?? ''}
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<button
|
||||
class="px-3 py-2 gap-2 flex flex-row w-full hover:bg-surface-hover transition-all items-center rounded-md {index ===
|
||||
selected
|
||||
? 'bg-surface-hover'
|
||||
: ''}"
|
||||
on:click={() => {
|
||||
if (kind == 'flow') {
|
||||
dispatch('pickFlow', { path: path })
|
||||
} else {
|
||||
dispatch('pickScript', { path: path, hash: lockHash ? hash : undefined, kind })
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if index === selected}
|
||||
<kbd class="!text-xs">↵</kbd>
|
||||
{/if}
|
||||
</button>
|
||||
{#if kind == 'flow'}
|
||||
<BarsStaggered size={14} class="shrink-0" />
|
||||
{:else}
|
||||
<Code2 size={14} />
|
||||
{/if}
|
||||
<div class="flex flex-col grow min-w-0">
|
||||
<div class="grow min-w-0 truncate text-left text-2xs text-primary font-normal">
|
||||
{#if marked}
|
||||
{@html marked}
|
||||
{:else}
|
||||
{!summary || summary.length == 0 ? path : summary}
|
||||
{/if}
|
||||
</div>
|
||||
{#if displayPath && path}
|
||||
<div class="grow min-w-0 truncate text-left text-2xs text-secondary font-[220]">
|
||||
{path}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if index === selected}
|
||||
<kbd class="!text-xs">↵</kbd>
|
||||
{/if}
|
||||
</button>
|
||||
</Popover>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { ScheduleService } from '$lib/gen'
|
||||
import { ScheduleService, type Schedule, type TriggersCount } from '$lib/gen'
|
||||
import type { ScheduleTrigger } from '../triggers'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import { get } from 'svelte/store'
|
||||
import { sendUserToast } from '$lib/utils'
|
||||
|
||||
// Load the schedule of a flow given its path and the workspace
|
||||
export async function loadFlowSchedule(path: string, workspace: string): Promise<ScheduleTrigger> {
|
||||
@@ -25,3 +28,108 @@ export async function loadFlowSchedule(path: string, workspace: string): Promise
|
||||
args: schedule.args ?? {}
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadSchedules(
|
||||
forceRefresh: boolean,
|
||||
path: string,
|
||||
isFlow: boolean,
|
||||
schedules: Writable<Schedule[] | undefined>,
|
||||
primarySchedule: Writable<ScheduleTrigger | false | undefined>,
|
||||
initialPrimarySchedule: Writable<ScheduleTrigger | false | undefined>,
|
||||
workspace: string,
|
||||
triggersCount: Writable<TriggersCount | undefined>
|
||||
) {
|
||||
console.log('loading schedules for path', path)
|
||||
if (!path || path == '') {
|
||||
schedules.set([])
|
||||
primarySchedule.update((ps) => (ps === undefined ? false : ps))
|
||||
initialPrimarySchedule.set(structuredClone(get(primarySchedule)))
|
||||
return
|
||||
}
|
||||
console.log('loading schedules for path', path)
|
||||
try {
|
||||
const allSchedules = await ScheduleService.listSchedules({
|
||||
workspace,
|
||||
path: path,
|
||||
isFlow
|
||||
})
|
||||
const primary = allSchedules.find((s) => s.path == path)
|
||||
let remotePrimarySchedule: ScheduleTrigger | false | undefined = primary
|
||||
? {
|
||||
summary: primary.summary,
|
||||
args: primary.args ?? {},
|
||||
cron: primary.schedule,
|
||||
timezone: primary.timezone,
|
||||
enabled: primary.enabled
|
||||
}
|
||||
: false
|
||||
primarySchedule.update((ps) => (ps === undefined || forceRefresh ? remotePrimarySchedule : ps))
|
||||
initialPrimarySchedule.set(structuredClone(remotePrimarySchedule))
|
||||
|
||||
triggersCount.update((tc) => {
|
||||
const primary = get(primarySchedule)
|
||||
return {
|
||||
...(tc ?? {}),
|
||||
schedule_count: allSchedules.length,
|
||||
primary_schedule: primary ? { schedule: primary.cron } : undefined
|
||||
}
|
||||
})
|
||||
schedules.set(allSchedules.filter((s) => s.path != path))
|
||||
} catch (e) {
|
||||
console.error('impossible to load schedules', e)
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveSchedule(
|
||||
path: string,
|
||||
newItem: boolean,
|
||||
workspace: string,
|
||||
primarySchedule: Writable<ScheduleTrigger | false | undefined>,
|
||||
isFlow: boolean
|
||||
) {
|
||||
const scheduleExists =
|
||||
path != '' &&
|
||||
!newItem &&
|
||||
(await ScheduleService.existsSchedule({
|
||||
workspace,
|
||||
path
|
||||
}))
|
||||
if (scheduleExists) {
|
||||
console.log('primary schedule exists')
|
||||
const primary = get(primarySchedule)
|
||||
if (primary) {
|
||||
await ScheduleService.updateSchedule({
|
||||
workspace,
|
||||
path,
|
||||
requestBody: {
|
||||
summary: primary.summary,
|
||||
args: primary.args,
|
||||
schedule: primary.cron,
|
||||
timezone: primary.timezone
|
||||
}
|
||||
})
|
||||
sendUserToast(`Primary schedule updated`)
|
||||
} else {
|
||||
await ScheduleService.deleteSchedule({ workspace, path })
|
||||
sendUserToast(`Primary schedule deleted`)
|
||||
}
|
||||
} else {
|
||||
const primary = get(primarySchedule)
|
||||
if (primary) {
|
||||
await ScheduleService.createSchedule({
|
||||
workspace,
|
||||
requestBody: {
|
||||
path,
|
||||
script_path: path,
|
||||
is_flow: isFlow,
|
||||
summary: primary.summary,
|
||||
args: primary.args,
|
||||
schedule: primary.cron,
|
||||
timezone: primary.timezone,
|
||||
enabled: primary.enabled
|
||||
}
|
||||
})
|
||||
sendUserToast(`Primary schedule created`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { type FlowModule } from '../../gen'
|
||||
import { NODE, type GraphModuleState } from '.'
|
||||
import { createEventDispatcher, onMount, setContext } from 'svelte'
|
||||
import { createEventDispatcher, getContext, onDestroy, onMount, setContext } from 'svelte'
|
||||
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import { get, writable, type Writable } from 'svelte/store'
|
||||
import '@xyflow/svelte/dist/style.css'
|
||||
import type { FlowInput } from '../flows/types'
|
||||
import {
|
||||
@@ -15,7 +15,7 @@
|
||||
ControlButton,
|
||||
type Viewport
|
||||
} from '@xyflow/svelte'
|
||||
import graphBuilder from './graphBuilder'
|
||||
import { graphBuilder, isTriggerStep, type SimplifiableFlow } from './graphBuilder'
|
||||
import ModuleNode from './renderers/nodes/ModuleNode.svelte'
|
||||
import InputNode from './renderers/nodes/InputNode.svelte'
|
||||
import BranchAllStart from './renderers/nodes/BranchAllStart.svelte'
|
||||
@@ -38,6 +38,8 @@
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import FlowYamlEditor from '../flows/header/FlowYamlEditor.svelte'
|
||||
import BranchOneEndNode from './renderers/nodes/branchOneEndNode.svelte'
|
||||
import type { TriggerContext } from '../triggers'
|
||||
|
||||
export let success: boolean | undefined = undefined
|
||||
export let modules: FlowModule[] | undefined = []
|
||||
export let failureModule: FlowModule | undefined = undefined
|
||||
@@ -72,11 +74,46 @@
|
||||
useDataflow: Writable<boolean | undefined>
|
||||
}>('FlowGraphContext', { selectedId, flowInputsStore, useDataflow })
|
||||
|
||||
const triggerContext = getContext<TriggerContext>('TriggerContext')
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let fullWidth = 0
|
||||
let width = 0
|
||||
|
||||
export let allowSimplifiedPoll: boolean = true
|
||||
|
||||
let simplifiableFlow: SimplifiableFlow | undefined = undefined
|
||||
|
||||
if (triggerContext && allowSimplifiedPoll) {
|
||||
if (isSimplifiable(modules)) {
|
||||
triggerContext?.simplifiedPoll?.set(true)
|
||||
}
|
||||
triggerContext?.simplifiedPoll.subscribe((value) => {
|
||||
computeSimplifiableFlow(modules ?? [], value ?? false)
|
||||
})
|
||||
}
|
||||
|
||||
function computeSimplifiableFlow(modules: FlowModule[], simplifiedFlow: boolean) {
|
||||
const isSimplif = isSimplifiable(modules)
|
||||
simplifiableFlow = isSimplif ? { simplifiedFlow } : undefined
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
if (isSimplifiable(modules)) {
|
||||
triggerContext?.simplifiedPoll?.set(undefined)
|
||||
}
|
||||
})
|
||||
|
||||
function onModulesChange(modules: FlowModule[]) {
|
||||
computeSimplifiableFlow(
|
||||
modules,
|
||||
triggerContext?.simplifiedPoll ? get(triggerContext.simplifiedPoll) ?? false : false
|
||||
)
|
||||
}
|
||||
|
||||
$: allowSimplifiedPoll && onModulesChange(modules ?? [])
|
||||
|
||||
function layoutNodes(nodes: Node[]): Node[] {
|
||||
let seenId: string[] = []
|
||||
for (const n of nodes) {
|
||||
@@ -129,6 +166,44 @@
|
||||
return newNodes
|
||||
}
|
||||
|
||||
let eventHandler = {
|
||||
deleteBranch: (detail, label) => {
|
||||
$selectedId = label
|
||||
dispatch('deleteBranch', detail)
|
||||
},
|
||||
insert: (detail) => {
|
||||
dispatch('insert', detail)
|
||||
},
|
||||
select: (modId) => {
|
||||
if (!notSelectable) {
|
||||
if ($selectedId != modId) {
|
||||
$selectedId = modId
|
||||
}
|
||||
dispatch('select', modId)
|
||||
}
|
||||
},
|
||||
changeId: (detail) => {
|
||||
dispatch('changeId', detail)
|
||||
},
|
||||
delete: (detail, label) => {
|
||||
$selectedId = label
|
||||
|
||||
dispatch('delete', detail)
|
||||
},
|
||||
newBranch: (module) => {
|
||||
dispatch('newBranch', { module })
|
||||
},
|
||||
move: (module, modules) => {
|
||||
dispatch('move', { module, modules })
|
||||
},
|
||||
selectedIteration: (detail, moduleId) => {
|
||||
dispatch('selectedIteration', { ...detail, moduleId: moduleId })
|
||||
},
|
||||
simplifyFlow: (detail) => {
|
||||
triggerContext?.simplifiedPoll.set(detail)
|
||||
}
|
||||
}
|
||||
|
||||
$: graph = graphBuilder(
|
||||
modules,
|
||||
{
|
||||
@@ -141,50 +216,13 @@
|
||||
},
|
||||
failureModule,
|
||||
preprocessorModule,
|
||||
{
|
||||
deleteBranch: (detail, label) => {
|
||||
$selectedId = label
|
||||
|
||||
dispatch('deleteBranch', detail)
|
||||
},
|
||||
insert: (detail) => {
|
||||
dispatch('insert', detail)
|
||||
},
|
||||
select: (modId) => {
|
||||
if (!notSelectable) {
|
||||
if ($selectedId != modId) {
|
||||
$selectedId = modId
|
||||
}
|
||||
dispatch('select', modId)
|
||||
}
|
||||
},
|
||||
changeId: (detail) => {
|
||||
dispatch('changeId', detail)
|
||||
},
|
||||
delete: (detail, label) => {
|
||||
$selectedId = label
|
||||
|
||||
dispatch('delete', detail)
|
||||
},
|
||||
newBranch: (module) => {
|
||||
dispatch('newBranch', { module })
|
||||
},
|
||||
move: (module, modules) => {
|
||||
dispatch('move', { module, modules })
|
||||
},
|
||||
selectedIteration: (detail, moduleId) => {
|
||||
dispatch('selectedIteration', { ...detail, moduleId: moduleId })
|
||||
}
|
||||
},
|
||||
eventHandler,
|
||||
success,
|
||||
$useDataflow,
|
||||
$selectedId,
|
||||
moving,
|
||||
triggerNode
|
||||
? {
|
||||
path
|
||||
}
|
||||
: undefined
|
||||
simplifiableFlow,
|
||||
triggerNode ? path : undefined
|
||||
)
|
||||
|
||||
const nodes = writable<Node[]>([])
|
||||
@@ -192,18 +230,93 @@
|
||||
|
||||
let height = 0
|
||||
|
||||
// function removeInputNode(nodes, edges, id) {
|
||||
// const inputNode = nodes.find((node) => node.id === id)
|
||||
// if (!inputNode) return { nodes, edges }
|
||||
|
||||
// // Find edges connected to the input node
|
||||
// const connectedEdges = edges.filter((edge) => edge.source === id || edge.target === id)
|
||||
|
||||
// // Remove the input node
|
||||
// let updatedNodes = nodes.filter((node) => node.id !== id)
|
||||
|
||||
// // Remove edges connected to the input node
|
||||
// let updatedEdges = edges.filter((edge) => edge.source !== id && edge.target !== id)
|
||||
|
||||
// // Create new edges from the input node's parent to its children
|
||||
// const inputEdges = connectedEdges.filter((edge) => edge.target === id)
|
||||
// const outputEdges = connectedEdges.filter((edge) => edge.source === id)
|
||||
|
||||
// inputEdges.forEach((inputEdge) => {
|
||||
// outputEdges.forEach((outputEdge) => {
|
||||
// const newEdge = {
|
||||
// id: `edge:${inputEdge.source}->${outputEdge.target}`,
|
||||
// source: inputEdge.source,
|
||||
// target: outputEdge.target,
|
||||
// type: 'empty',
|
||||
// data: {
|
||||
// ...outputEdge.data,
|
||||
// sourceId: inputEdge.source,
|
||||
// targetId: outputEdge.target
|
||||
// }
|
||||
// }
|
||||
// updatedEdges.push(newEdge)
|
||||
// })
|
||||
// })
|
||||
|
||||
// // Update parent ids of the nodes
|
||||
// updatedNodes = updatedNodes.map((node) => {
|
||||
// if (node.data && node.data.parentIds && node.data.parentIds.includes(id)) {
|
||||
// const updatedParentIds = node.data.parentIds.filter((parentId) => parentId !== id)
|
||||
// if (inputNode.data && inputNode.data.parentIds) {
|
||||
// updatedParentIds.push(...inputNode.data.parentIds)
|
||||
// }
|
||||
// return {
|
||||
// ...node,
|
||||
// data: {
|
||||
// ...node.data,
|
||||
// parentIds: [...new Set(updatedParentIds)] // Remove duplicates
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return node
|
||||
// })
|
||||
|
||||
// return { nodes: updatedNodes, edges: updatedEdges }
|
||||
// }
|
||||
|
||||
// function processGraph(graph, simplifiable) {
|
||||
// let newGraph = { nodes: graph.nodes, edges: graph.edges }
|
||||
// newGraph = removeInputNode(newGraph.nodes, newGraph.edges, 'Input')
|
||||
// newGraph = removeInputNode(newGraph.nodes, newGraph.edges, simplifiable.forLoopNode.id)
|
||||
// newGraph = removeInputNode(newGraph.nodes, newGraph.edges, simplifiable.triggerNode.id)
|
||||
// return newGraph
|
||||
// }
|
||||
|
||||
function isSimplifiable(modules: FlowModule[] | undefined): boolean {
|
||||
if (!modules || modules?.length !== 2) {
|
||||
return false
|
||||
}
|
||||
if (isTriggerStep(modules?.[0])) {
|
||||
let secondValue = modules?.[1].value
|
||||
return secondValue.type == 'forloopflow'
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function updateStores() {
|
||||
if (graph.error) {
|
||||
return
|
||||
}
|
||||
let newGraph = graph
|
||||
|
||||
$nodes = layoutNodes(graph?.nodes)
|
||||
$edges = graph.edges
|
||||
|
||||
$nodes = layoutNodes(newGraph.nodes)
|
||||
$edges = newGraph.edges
|
||||
height = Math.max(...$nodes.map((n) => n.position.y + NODE.height + 40), minHeight)
|
||||
}
|
||||
|
||||
$: graph && updateStores()
|
||||
$: (graph || allowSimplifiedPoll) && updateStores()
|
||||
|
||||
const nodeTypes = {
|
||||
input2: InputNode,
|
||||
|
||||
@@ -14,9 +14,19 @@ export type GraphEventHandlers = {
|
||||
move: (module: FlowModule, modules: FlowModule[]) => void
|
||||
selectedIteration: (detail, moduleId: string) => void
|
||||
changeId: (newId: string) => void
|
||||
simplifyFlow: (detail: boolean) => void
|
||||
}
|
||||
|
||||
export default function graphBuilder(
|
||||
export type SimplifiableFlow = { simplifiedFlow: boolean }
|
||||
|
||||
export function isTriggerStep(module: FlowModule | undefined): boolean {
|
||||
return (
|
||||
module?.value != undefined &&
|
||||
(module.value.type === 'script' || module.value.type === 'rawscript') &&
|
||||
module.value.is_trigger === true
|
||||
)
|
||||
}
|
||||
export function graphBuilder(
|
||||
modules: FlowModule[] | undefined,
|
||||
extra: Record<string, any>,
|
||||
failureModule: FlowModule | undefined,
|
||||
@@ -26,9 +36,12 @@ export default function graphBuilder(
|
||||
useDataflow: boolean | undefined,
|
||||
selectedId: string | undefined,
|
||||
moving: string | undefined,
|
||||
triggerProps?: {
|
||||
path?: string
|
||||
}
|
||||
simplifiableFlow: SimplifiableFlow | undefined,
|
||||
flowPathForTriggerNode: string | undefined
|
||||
// triggerProps?: {
|
||||
// path?: string
|
||||
// flowIsSimplifiable?: boolean
|
||||
// }
|
||||
): {
|
||||
nodes: Node[]
|
||||
edges: Edge[]
|
||||
@@ -102,6 +115,7 @@ export default function graphBuilder(
|
||||
sourceId: string,
|
||||
targetId: string,
|
||||
options?: {
|
||||
disableInsert?: boolean
|
||||
customId?: string
|
||||
type?: string
|
||||
subModules?: FlowModule[]
|
||||
@@ -129,17 +143,18 @@ export default function graphBuilder(
|
||||
target: targetId,
|
||||
type: options?.type ?? 'edge',
|
||||
data: {
|
||||
insertable: extra.insertable,
|
||||
modules: options?.subModules ?? modules,
|
||||
sourceId,
|
||||
targetId,
|
||||
moving,
|
||||
eventHandlers,
|
||||
simplifiedTriggerView: simplifiableFlow?.simplifiedFlow,
|
||||
disableMoveIds: options?.disableMoveIds,
|
||||
enableTrigger: sourceId === 'Input',
|
||||
// If the index is -1, it means that the target module is not in the modules array, so we set it to the length of the array
|
||||
index: index >= 0 ? index : mods?.length ?? 0,
|
||||
...extra
|
||||
...extra,
|
||||
insertable: extra.insertable && !options?.disableInsert
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -156,15 +171,19 @@ export default function graphBuilder(
|
||||
}
|
||||
}
|
||||
|
||||
if (extra.path && triggerProps != undefined) {
|
||||
const triggerNode: Node = {
|
||||
let triggerNode: Node | undefined = undefined
|
||||
if (flowPathForTriggerNode) {
|
||||
triggerNode = {
|
||||
id: 'Trigger',
|
||||
position: { x: -1, y: -1 },
|
||||
type: 'trigger',
|
||||
data: {
|
||||
path: triggerProps?.path,
|
||||
simplifiableFlow: simplifiableFlow,
|
||||
path: flowPathForTriggerNode,
|
||||
newFlow: extra.newFlow,
|
||||
eventHandlers: eventHandlers
|
||||
eventHandlers: eventHandlers,
|
||||
modules: modules,
|
||||
isEditor: extra.insertable
|
||||
}
|
||||
}
|
||||
nodes.push(triggerNode)
|
||||
@@ -191,13 +210,16 @@ export default function graphBuilder(
|
||||
type: 'result'
|
||||
}
|
||||
|
||||
nodes.push(inputNode)
|
||||
nodes.push(resultNode)
|
||||
if (simplifiableFlow?.simplifiedFlow !== true) {
|
||||
nodes.push(inputNode)
|
||||
nodes.push(resultNode)
|
||||
}
|
||||
|
||||
function processModules(
|
||||
modules: FlowModule[],
|
||||
beforeNode: Node,
|
||||
nextNode: Node,
|
||||
nextNode: Node | undefined,
|
||||
simplifiedTriggerView: boolean,
|
||||
currentOffset = 0,
|
||||
disableMoveIds: string[] = [],
|
||||
parentIndex?: string,
|
||||
@@ -206,10 +228,12 @@ export default function graphBuilder(
|
||||
let previousId: string | undefined = undefined
|
||||
|
||||
if (modules.length === 0) {
|
||||
addEdge(beforeNode.id, nextNode.id, {
|
||||
subModules: modules,
|
||||
disableMoveIds
|
||||
})
|
||||
if (nextNode) {
|
||||
addEdge(beforeNode.id, nextNode.id, {
|
||||
subModules: modules,
|
||||
disableMoveIds
|
||||
})
|
||||
}
|
||||
} else {
|
||||
modules.forEach((module, index) => {
|
||||
const localDisableMoveIds = [...disableMoveIds, module.id]
|
||||
@@ -295,6 +319,7 @@ export default function graphBuilder(
|
||||
branch.modules,
|
||||
startNode,
|
||||
endNode,
|
||||
false,
|
||||
currentOffset,
|
||||
localDisableMoveIds,
|
||||
parentIndex ? `${parentIndex}-${index}-${branchIndex}` : `${index}-${branchIndex}`
|
||||
@@ -304,7 +329,9 @@ export default function graphBuilder(
|
||||
|
||||
previousId = endNode.id
|
||||
} else if (module.value.type === 'forloopflow') {
|
||||
addNode(module, currentOffset, 'module', modules)
|
||||
if (!simplifiedTriggerView) {
|
||||
addNode(module, currentOffset, 'module', modules)
|
||||
}
|
||||
|
||||
const startNode = {
|
||||
id: `${module.id}-start`,
|
||||
@@ -313,6 +340,7 @@ export default function graphBuilder(
|
||||
id: module.id,
|
||||
module: module,
|
||||
modules: modules,
|
||||
simplifiedTriggerView,
|
||||
eventHandlers: eventHandlers,
|
||||
...extra
|
||||
},
|
||||
@@ -320,9 +348,15 @@ export default function graphBuilder(
|
||||
type: 'forLoopStart'
|
||||
}
|
||||
|
||||
addEdge(module.id, startNode.id, {
|
||||
type: 'empty'
|
||||
})
|
||||
if (!simplifiedTriggerView) {
|
||||
addEdge(module.id, startNode.id, {
|
||||
type: 'empty'
|
||||
})
|
||||
} else if (previousId) {
|
||||
addEdge(previousId, startNode.id, {
|
||||
type: 'empty'
|
||||
})
|
||||
}
|
||||
|
||||
const endNode = {
|
||||
id: `${module.id}-end`,
|
||||
@@ -332,6 +366,7 @@ export default function graphBuilder(
|
||||
module: module,
|
||||
modules: modules,
|
||||
eventHandlers: eventHandlers,
|
||||
simplifiedTriggerView,
|
||||
...extra
|
||||
},
|
||||
position: { x: -1, y: -1 },
|
||||
@@ -347,6 +382,7 @@ export default function graphBuilder(
|
||||
module.value.modules,
|
||||
startNode,
|
||||
endNode,
|
||||
false,
|
||||
currentOffset + 25,
|
||||
localDisableMoveIds,
|
||||
parentIndex
|
||||
@@ -390,6 +426,7 @@ export default function graphBuilder(
|
||||
module.value.modules,
|
||||
startNode,
|
||||
endNode,
|
||||
false,
|
||||
currentOffset + 25,
|
||||
localDisableMoveIds,
|
||||
parentIndex
|
||||
@@ -435,6 +472,7 @@ export default function graphBuilder(
|
||||
module.value.default,
|
||||
defaultBranch,
|
||||
endNode,
|
||||
false,
|
||||
currentOffset,
|
||||
localDisableMoveIds,
|
||||
parentIndex ? `${parentIndex}-${index}` : index.toString(),
|
||||
@@ -468,6 +506,7 @@ export default function graphBuilder(
|
||||
branch.modules,
|
||||
startNode,
|
||||
endNode,
|
||||
false,
|
||||
currentOffset,
|
||||
localDisableMoveIds,
|
||||
parentIndex ? `${parentIndex}-${index}` : index.toString(),
|
||||
@@ -480,30 +519,31 @@ export default function graphBuilder(
|
||||
addNode(module, currentOffset, 'module', modules)
|
||||
|
||||
previousId = module.id
|
||||
|
||||
|
||||
}
|
||||
|
||||
if (index === 0) {
|
||||
addEdge(beforeNode.id, module.id, {
|
||||
subModules: modules,
|
||||
disableMoveIds
|
||||
disableMoveIds,
|
||||
disableInsert: simplifiedTriggerView
|
||||
})
|
||||
}
|
||||
|
||||
if (index === modules.length - 1 && previousId) {
|
||||
if (index === modules.length - 1 && previousId && nextNode) {
|
||||
addEdge(previousId, nextNode.id, {
|
||||
subModules: modules,
|
||||
disableMoveIds
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
processModules(modules, inputNode, resultNode)
|
||||
if (simplifiableFlow?.simplifiedFlow === true && triggerNode) {
|
||||
processModules(modules, triggerNode, undefined, true)
|
||||
} else {
|
||||
processModules(modules, inputNode, resultNode, false)
|
||||
}
|
||||
|
||||
if (failureModule) {
|
||||
let toAdd: Record<string, string> = {}
|
||||
|
||||
@@ -86,45 +86,6 @@
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{#if data.enableTrigger}
|
||||
<div
|
||||
class="edgeButtonContainer nodrag nopan"
|
||||
style:transform="translate(100%, 50%) translate({sourceX}px,{sourceY + 2}px)"
|
||||
>
|
||||
<InsertModuleButton
|
||||
disableAi={data.disableAi}
|
||||
on:new={(e) => {
|
||||
// console.log('new', e)
|
||||
data?.eventHandlers.insert({
|
||||
modules: data.modules,
|
||||
index: data.index,
|
||||
kind: e.detail.kind,
|
||||
inlineScript: e.detail.inlineScript
|
||||
})
|
||||
}}
|
||||
on:pickScript={(e) => {
|
||||
// console.log('pickScript', e)
|
||||
data?.eventHandlers.insert({
|
||||
modules: data.modules,
|
||||
index: data.index,
|
||||
script: e.detail,
|
||||
kind: e.detail.kind
|
||||
})
|
||||
}}
|
||||
on:pickFlow={(e) => {
|
||||
// console.log('pickFlow', e)
|
||||
data?.eventHandlers.insert({
|
||||
modules: data.modules,
|
||||
index: data.index,
|
||||
flow: e.detail
|
||||
})
|
||||
}}
|
||||
kind="trigger"
|
||||
index={data?.index ?? 0}
|
||||
modules={data?.modules ?? []}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if data?.moving}
|
||||
|
||||
@@ -11,19 +11,35 @@
|
||||
insertable: boolean
|
||||
flowModuleStates: Record<string, GraphModuleState> | undefined
|
||||
eventHandlers: GraphEventHandlers
|
||||
simplifiedTriggerView: boolean
|
||||
}
|
||||
</script>
|
||||
|
||||
<NodeWrapper offset={data.offset} let:darkMode>
|
||||
<VirtualItem
|
||||
label={'Collect result of each iteration'}
|
||||
selectable={true}
|
||||
selected={false}
|
||||
id={data.id}
|
||||
bgColor={getStateColor(undefined, darkMode)}
|
||||
borderColor={getStateColor(data.flowModuleStates?.[data.id]?.type, darkMode)}
|
||||
on:select={(e) => {
|
||||
data?.eventHandlers?.select(e.detail)
|
||||
}}
|
||||
/>
|
||||
{#if data.simplifiedTriggerView}
|
||||
<VirtualItem
|
||||
label={'Each event is processed'}
|
||||
selectable={false}
|
||||
selected={false}
|
||||
id={data.id}
|
||||
hideId
|
||||
bgColor={getStateColor(undefined, darkMode)}
|
||||
borderColor={getStateColor(data.flowModuleStates?.[data.id]?.type, darkMode)}
|
||||
on:select={(e) => {
|
||||
data?.eventHandlers?.select(e.detail)
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<VirtualItem
|
||||
label={'Collect result of each iteration'}
|
||||
selectable={true}
|
||||
selected={false}
|
||||
id={data.id}
|
||||
bgColor={getStateColor(undefined, darkMode)}
|
||||
borderColor={getStateColor(data.flowModuleStates?.[data.id]?.type, darkMode)}
|
||||
on:select={(e) => {
|
||||
data?.eventHandlers?.select(e.detail)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</NodeWrapper>
|
||||
|
||||
@@ -12,12 +12,13 @@
|
||||
modules: FlowModule[]
|
||||
flowModuleStates: Record<string, GraphModuleState> | undefined
|
||||
eventHandlers: GraphEventHandlers
|
||||
simplifiedTriggerView: boolean
|
||||
}
|
||||
</script>
|
||||
|
||||
<NodeWrapper let:darkMode offset={data.offset}>
|
||||
<VirtualItem
|
||||
label={'Do one iteration'}
|
||||
label={data.simplifiedTriggerView ? 'For each new event' : 'Do one iteration'}
|
||||
selectable={false}
|
||||
selected={false}
|
||||
id={data.id}
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
moving: string | undefined
|
||||
eventHandlers: GraphEventHandlers
|
||||
index: number
|
||||
enableTrigger: boolean
|
||||
disableAi: boolean
|
||||
disableMoveIds: string[]
|
||||
}
|
||||
@@ -50,6 +49,8 @@
|
||||
detail: 'preprocessor'
|
||||
})
|
||||
}}
|
||||
class="w-[14px] h-[14px]"
|
||||
iconSize={10}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
<script lang="ts">
|
||||
import NodeWrapper from './NodeWrapper.svelte'
|
||||
import TriggersWrapper from '../triggers/TriggersWrapper.svelte'
|
||||
import { type GraphEventHandlers } from '../../graphBuilder'
|
||||
import { type GraphEventHandlers, type SimplifiableFlow } from '../../graphBuilder'
|
||||
import type { FlowModule } from '$lib/gen'
|
||||
import { getContext } from 'svelte'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import { Maximize2, Minimize2, Calendar } from 'lucide-svelte'
|
||||
import { getStateColor } from '../../util'
|
||||
import { setScheduledPollSchedule, type TriggerContext } from '$lib/components/triggers'
|
||||
import VirtualItemWrapper from '$lib/components/flows/map/VirtualItemWrapper.svelte'
|
||||
|
||||
export let data: {
|
||||
path: string
|
||||
@@ -11,21 +16,107 @@
|
||||
newFlow: boolean
|
||||
extra_perms: Record<string, any>
|
||||
eventHandlers: GraphEventHandlers
|
||||
modules: FlowModule[]
|
||||
index: number
|
||||
disableAi: boolean
|
||||
simplifiableFlow: SimplifiableFlow
|
||||
}
|
||||
|
||||
const { selectedId } = getContext<{
|
||||
selectedId: Writable<string | undefined>
|
||||
}>('FlowGraphContext')
|
||||
|
||||
const { primarySchedule, triggersCount, selectedTrigger } =
|
||||
getContext<TriggerContext>('TriggerContext')
|
||||
</script>
|
||||
|
||||
<NodeWrapper wrapperClass="shadow-none">
|
||||
<TriggersWrapper
|
||||
path={data.path}
|
||||
on:select={() => {
|
||||
data?.eventHandlers?.select('triggers')
|
||||
}}
|
||||
isFlow={true}
|
||||
selected={$selectedId == 'triggers'}
|
||||
newItem={data.newFlow}
|
||||
/>
|
||||
<NodeWrapper wrapperClass="shadow-md" let:darkMode>
|
||||
{#if data.simplifiableFlow?.simplifiedFlow != true}
|
||||
<TriggersWrapper
|
||||
disableAi={data.disableAi}
|
||||
isEditor={data.isEditor}
|
||||
path={data.path}
|
||||
bgColor={getStateColor(undefined, darkMode)}
|
||||
on:new={(e) => {
|
||||
data?.eventHandlers.insert({
|
||||
modules: data.modules,
|
||||
index: 0,
|
||||
kind: 'trigger',
|
||||
inlineScript: e.detail.inlineScript
|
||||
})
|
||||
data?.eventHandlers?.simplifyFlow(true)
|
||||
}}
|
||||
on:pickScript={(e) => {
|
||||
data?.eventHandlers.insert({
|
||||
modules: data.modules,
|
||||
index: 0,
|
||||
kind: 'trigger',
|
||||
script: e.detail
|
||||
})
|
||||
data?.eventHandlers?.simplifyFlow(true)
|
||||
}}
|
||||
on:openScheduledPoll={(e) => {
|
||||
$selectedTrigger = 'scheduledPoll'
|
||||
}}
|
||||
on:select={(e) => {
|
||||
data?.eventHandlers?.select('triggers')
|
||||
}}
|
||||
on:delete={(e) => {
|
||||
data.eventHandlers.delete(e, '')
|
||||
}}
|
||||
selected={$selectedId == 'triggers'}
|
||||
newItem={data.newFlow}
|
||||
modules={data.modules}
|
||||
/>
|
||||
{:else}
|
||||
<VirtualItemWrapper
|
||||
label="Check for new events"
|
||||
selectable={true}
|
||||
selected={$selectedId == 'triggers'}
|
||||
id={'triggers'}
|
||||
bgColor={getStateColor(undefined, darkMode)}
|
||||
on:select={(e) => {
|
||||
data?.eventHandlers?.select(e.detail)
|
||||
}}
|
||||
>
|
||||
{#if $primarySchedule || ($primarySchedule == undefined && $triggersCount?.primary_schedule?.schedule)}
|
||||
<div class="text-2xs text-primary p-2 flex gap-2 items-center">
|
||||
<Calendar size={12} />
|
||||
<div>
|
||||
Schedule every {$primarySchedule?.cron ?? $triggersCount?.primary_schedule?.schedule}
|
||||
{$primarySchedule?.enabled ||
|
||||
($primarySchedule == undefined && $triggersCount?.primary_schedule?.schedule)
|
||||
? ''
|
||||
: ' (disabled)'}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class="px-2 py-1 hover:bg-surface-inverse w-full hover:text-primary-inverse"
|
||||
on:click={() => {
|
||||
setScheduledPollSchedule(primarySchedule, triggersCount)
|
||||
}}
|
||||
>
|
||||
Set primary schedule
|
||||
</button>
|
||||
{/if}
|
||||
</VirtualItemWrapper>
|
||||
{/if}
|
||||
{#if data.simplifiableFlow != undefined}
|
||||
<button
|
||||
class="absolute -top-[10px] -right-[10px] rounded-full h-[20px] w-[20px] trash center-center text-secondary
|
||||
outline-[1px] outline dark:outline-gray-500 outline-gray-300 bg-surface duration-150 hover:bg-nord-950 hover:text-white"
|
||||
on:click|preventDefault|stopPropagation={() =>
|
||||
data?.eventHandlers?.simplifyFlow(!data.simplifiableFlow?.simplifiedFlow)}
|
||||
title={data.simplifiableFlow?.simplifiedFlow
|
||||
? 'Expand to full flow view'
|
||||
: 'Simplify flow view for scheduled poll'}
|
||||
>
|
||||
{#if data.simplifiableFlow?.simplifiedFlow}
|
||||
<Maximize2 size={12} strokeWidth={2} />
|
||||
{:else}
|
||||
<Minimize2 size={12} strokeWidth={2} />
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</NodeWrapper>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<button
|
||||
on:click
|
||||
class={twMerge(
|
||||
'hover:bg-surface-hover rounded-md border text-xs w-6 h-6 relative center-center cursor-pointer bg-surface',
|
||||
'hover:bg-surface-hover rounded-md border text-xs w-[23px] h-[23px] relative center-center cursor-pointer bg-surface',
|
||||
selected ? 'outline-1 outline-tertiary outline' : 'outline-0'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
export let isFlow: boolean
|
||||
export let selected: boolean
|
||||
export let showOnlyWithCount: boolean
|
||||
export let triggersToDisplay: ('webhooks' | 'schedules' | 'routes' | 'websockets' | 'emails')[] =
|
||||
['webhooks', 'schedules', 'routes', 'websockets', 'emails']
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
onMount(() => {
|
||||
@@ -39,84 +41,31 @@
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const triggerTypeConfig = {
|
||||
webhooks: { icon: Webhook, countKey: 'webhook_count' },
|
||||
schedules: { icon: Calendar, countKey: 'schedule_count' },
|
||||
routes: { icon: Route, countKey: 'http_routes_count' },
|
||||
websockets: { icon: Unplug, countKey: 'websocket_count' },
|
||||
emails: { icon: Mail, countKey: 'email_count' }
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !showOnlyWithCount || ($triggersCount?.webhook_count ?? 0) > 0}
|
||||
<Popover>
|
||||
<svelte:fragment slot="text">Webhooks</svelte:fragment>
|
||||
<TriggerButton
|
||||
on:click={() => {
|
||||
$selectedTrigger = 'webhooks'
|
||||
dispatch('select')
|
||||
}}
|
||||
selected={selected && $selectedTrigger === 'webhooks'}
|
||||
>
|
||||
<TriggerCount count={$triggersCount?.webhook_count} />
|
||||
<Webhook size={12} />
|
||||
</TriggerButton>
|
||||
</Popover>
|
||||
{/if}
|
||||
|
||||
{#if !showOnlyWithCount || ($triggersCount?.schedule_count ?? 0) > 0}
|
||||
<Popover>
|
||||
<svelte:fragment slot="text">Schedules</svelte:fragment>
|
||||
<TriggerButton
|
||||
on:click={() => {
|
||||
$selectedTrigger = 'schedules'
|
||||
dispatch('select')
|
||||
}}
|
||||
selected={selected && $selectedTrigger === 'schedules'}
|
||||
>
|
||||
<TriggerCount count={$triggersCount?.schedule_count} />
|
||||
<Calendar size={12} />
|
||||
</TriggerButton>
|
||||
</Popover>
|
||||
{/if}
|
||||
|
||||
{#if !showOnlyWithCount || ($triggersCount?.http_routes_count ?? 0) > 0}
|
||||
<Popover>
|
||||
<svelte:fragment slot="text">HTTP Routes</svelte:fragment>
|
||||
<TriggerButton
|
||||
on:click={() => {
|
||||
$selectedTrigger = 'routes'
|
||||
dispatch('select')
|
||||
}}
|
||||
selected={selected && $selectedTrigger === 'routes'}
|
||||
>
|
||||
<TriggerCount count={$triggersCount?.http_routes_count} />
|
||||
<Route size={12} />
|
||||
</TriggerButton>
|
||||
</Popover>
|
||||
{/if}
|
||||
|
||||
{#if !showOnlyWithCount || ($triggersCount?.websocket_count ?? 0) > 0}
|
||||
<Popover>
|
||||
<svelte:fragment slot="text">Websockets</svelte:fragment>
|
||||
<TriggerButton
|
||||
on:click={() => {
|
||||
$selectedTrigger = 'websockets'
|
||||
dispatch('select')
|
||||
}}
|
||||
selected={selected && $selectedTrigger === 'websockets'}
|
||||
>
|
||||
<TriggerCount count={$triggersCount?.websocket_count} />
|
||||
<Unplug size={12} />
|
||||
</TriggerButton>
|
||||
</Popover>
|
||||
{/if}
|
||||
|
||||
{#if !showOnlyWithCount || ($triggersCount?.email_count ?? 0) > 0}
|
||||
<Popover>
|
||||
<svelte:fragment slot="text">Emails</svelte:fragment>
|
||||
<TriggerButton
|
||||
on:click={() => {
|
||||
$selectedTrigger = 'emails'
|
||||
dispatch('select')
|
||||
}}
|
||||
selected={selected && $selectedTrigger === 'emails'}
|
||||
>
|
||||
<TriggerCount count={$triggersCount?.email_count} />
|
||||
<Mail size={12} />
|
||||
</TriggerButton>
|
||||
</Popover>
|
||||
{/if}
|
||||
{#each triggersToDisplay as type}
|
||||
{@const { icon, countKey } = triggerTypeConfig[type]}
|
||||
{#if !showOnlyWithCount || ($triggersCount?.[countKey] ?? 0) > 0}
|
||||
<Popover>
|
||||
<svelte:fragment slot="text">{type.charAt(0).toUpperCase() + type.slice(1)}</svelte:fragment>
|
||||
<TriggerButton
|
||||
on:click={() => {
|
||||
$selectedTrigger = type
|
||||
dispatch('select')
|
||||
}}
|
||||
selected={selected && $selectedTrigger === type}
|
||||
>
|
||||
<TriggerCount count={$triggersCount?.[countKey]} />
|
||||
<svelte:component this={icon} size={12} />
|
||||
</TriggerButton>
|
||||
</Popover>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
@@ -4,29 +4,51 @@
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
import TriggersBadge from './TriggersBadge.svelte'
|
||||
import InsertModuleButton from '$lib/components/flows/map/InsertModuleButton.svelte'
|
||||
import type { FlowModule } from '$lib/gen'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let path: string
|
||||
export let newItem: boolean
|
||||
export let isFlow: boolean
|
||||
export let selected: boolean
|
||||
export let isEditor: boolean = false
|
||||
export let disableAi: boolean = false
|
||||
export let modules: FlowModule[] = []
|
||||
export let bgColor: string
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<div style={`width: ${NODE.width}px;`}>
|
||||
<div class="flex flex-row mx-auto w-min">
|
||||
<button
|
||||
class="flex flex-row gap-2 px-2 border p-1 rounded-md bg-surface shadow-md items-center {selected
|
||||
? 'outline outline-offset-1 outline-1 outline-slate-900 dark:bg-white/5 dark:outline-slate-800/60 dark:border-gray-400'
|
||||
: ''}"
|
||||
on:click={() => {
|
||||
dispatch('select')
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-row items-center text-2xs font-normal"> Triggers </div>
|
||||
</div>
|
||||
<TriggersBadge showOnlyWithCount={false} {path} {newItem} {isFlow} {selected} on:select />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
style="background-color: {bgColor} !important;"
|
||||
class="flex w-full flex-row gap-2 px-2 p-1 items-center {selected
|
||||
? 'outline outline-2 outline-gray-600 rounded-sm dark:bg-white/5 dark:outline-gray-400'
|
||||
: ''}"
|
||||
on:click={() => {
|
||||
dispatch('select')
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col mr-1 ml-1">
|
||||
<div class="flex flex-row items-center text-2xs font-normal"> Triggers </div>
|
||||
</div>
|
||||
<TriggersBadge showOnlyWithCount={false} {path} {newItem} isFlow {selected} on:select />
|
||||
{#if isEditor}
|
||||
<InsertModuleButton
|
||||
{disableAi}
|
||||
on:new
|
||||
on:pickScript
|
||||
on:select
|
||||
on:open={() => {
|
||||
dispatch('openScheduledPoll')
|
||||
}}
|
||||
kind="trigger"
|
||||
index={0}
|
||||
{modules}
|
||||
class={twMerge(
|
||||
'hover:bg-surface-hover rounded-md border text-xs w-[23px] h-[23px] relative center-center cursor-pointer bg-surface outline-0'
|
||||
)}
|
||||
/>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<script>
|
||||
// You can pass props for flexibility (e.g., size, color)
|
||||
export let size = 24
|
||||
</script>
|
||||
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M4.96046 2.04913L6.96046 2.04913" />
|
||||
<path d="M5.96046 8.94553L4.96046 7.94553" />
|
||||
<path d="M13.1611 13.8974L17.2173 13.8974" />
|
||||
<path
|
||||
d="M20.4622 12.2749V9.84124C20.4622 9.62608 20.3767 9.41975 20.2246 9.26761C20.0725 9.11547 19.8661 9.03001 19.651 9.03001H18.0285C17.8134 9.03001 17.607 9.11547 17.4549 9.26761C17.3028 9.41975 17.2173 9.62608 17.2173 9.84124V12.2749"
|
||||
/>
|
||||
<path
|
||||
d="M20.8678 22.0097C21.1906 22.0097 21.5001 21.8632 21.7283 21.6024C21.9565 21.3415 22.0847 20.9878 22.0847 20.619V17.9412C22.0847 16.9747 20.8678 15.8816 20.8678 14.5834V12.9703C20.8678 12.7858 20.8037 12.609 20.6896 12.4786C20.5755 12.3482 20.4208 12.2749 20.2594 12.2749H17.8257C17.6644 12.2749 17.5096 12.3482 17.3955 12.4786C17.2814 12.609 17.2173 12.7858 17.2173 12.9703V20.619C17.2173 20.9878 17.3455 21.3415 17.5737 21.6024C17.8019 21.8632 18.1114 22.0097 18.4341 22.0097H20.8678Z"
|
||||
/>
|
||||
<path d="M22.0847 18.7648H8.29377" />
|
||||
<path
|
||||
d="M9.51061 22.0097C9.18789 22.0097 8.87838 21.8632 8.65018 21.6024C8.42197 21.3415 8.29377 20.9878 8.29377 20.619V17.9412C8.29377 16.9747 9.51061 15.8816 9.51061 14.5834V12.9703C9.51061 12.7858 9.57472 12.609 9.68882 12.4786C9.80292 12.3482 9.95767 12.2749 10.119 12.2749H12.5527C12.7141 12.2749 12.8688 12.3482 12.9829 12.4786C13.097 12.609 13.1611 12.7858 13.1611 12.9703V20.619C13.1611 20.9878 13.0329 21.3415 12.8047 21.6024C12.5765 21.8632 12.267 22.0097 11.9443 22.0097H9.51061Z"
|
||||
/>
|
||||
<path
|
||||
d="M13.1611 12.2749V9.84124C13.1611 9.62608 13.0757 9.41975 12.9235 9.26761C12.7714 9.11547 12.5651 9.03001 12.3499 9.03001H10.7275C10.5123 9.03001 10.306 9.11547 10.1538 9.26761C10.0017 9.41975 9.91622 9.62608 9.91622 9.84124V12.2749"
|
||||
/>
|
||||
<path
|
||||
d="M8.78889 6.11711C8.30848 5.63669 7.71362 5.28656 7.0604 5.09974C6.40719 4.91291 5.71715 4.89555 5.05536 5.04928C4.39358 5.20301 3.78186 5.52278 3.27789 5.97842C2.77392 6.43406 2.39431 7.01056 2.17486 7.65355C1.95542 8.29654 1.90337 8.98483 2.02362 9.65351C2.14387 10.3222 2.43246 10.9492 2.86217 11.4755C3.29188 12.0017 3.84856 12.4098 4.47969 12.6614C5.11083 12.9129 5.79563 12.9995 6.46951 12.913"
|
||||
/>
|
||||
</svg>
|
||||
@@ -19,6 +19,7 @@ import Slack from './Slack.svelte'
|
||||
import TogglIcon from './TogglIcon.svelte'
|
||||
import WindmillIcon from './WindmillIcon.svelte'
|
||||
import WindmillIcon2 from './WindmillIcon2.svelte'
|
||||
import SchedulePollIcon from './SchedulePollIcon.svelte'
|
||||
import MailchimpIcon from './MailchimpIcon.svelte'
|
||||
import SendgridIcon from './SendgridIcon.svelte'
|
||||
import SendflakeIcon from './SendflakeIcon.svelte'
|
||||
@@ -215,6 +216,7 @@ export {
|
||||
TogglIcon,
|
||||
WindmillIcon,
|
||||
WindmillIcon2,
|
||||
SchedulePollIcon,
|
||||
MailchimpIcon,
|
||||
SendgridIcon,
|
||||
LinkedinIcon,
|
||||
|
||||
@@ -53,7 +53,11 @@ export function scroll_into_view_if_needed_polyfill(elem: Element, centerIfNeede
|
||||
})
|
||||
}
|
||||
observer.disconnect()
|
||||
})
|
||||
}, {
|
||||
root: null, // or specify a scrolling parent if needed
|
||||
rootMargin: '0px 1000px', // Essentially making horizontal checks irrelevant
|
||||
threshold: 0.1 // Adjust threshold to control when observer should trigger
|
||||
})
|
||||
observer.observe(elem)
|
||||
|
||||
return observer // return for testing
|
||||
|
||||
@@ -24,16 +24,10 @@
|
||||
return { minTs, maxTs }
|
||||
}
|
||||
|
||||
const manualDates: {
|
||||
const fixedManualDates: {
|
||||
label: string
|
||||
computeMinMax: () => { minTs: string; maxTs: string | undefined } | undefined
|
||||
}[] = [
|
||||
{
|
||||
label: loadText ?? 'Last 1000 runs',
|
||||
computeMinMax: () => {
|
||||
return undefined
|
||||
}
|
||||
},
|
||||
...(!serviceLogsChoices
|
||||
? [
|
||||
{
|
||||
@@ -68,6 +62,17 @@
|
||||
}
|
||||
]
|
||||
|
||||
$: manualDates = [
|
||||
{
|
||||
label: loadText ?? 'Last 1000 runs',
|
||||
computeMinMax: () => {
|
||||
return undefined
|
||||
}
|
||||
},
|
||||
...fixedManualDates
|
||||
|
||||
]
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
import { scroll_into_view_if_needed_polyfill } from '../multiselect/utils'
|
||||
import { Alert } from '../common'
|
||||
import Popover from '../Popover.svelte'
|
||||
import ServiceLogsInner from '../ServiceLogsInner.svelte'
|
||||
import Logs from 'lucide-svelte/icons/logs'
|
||||
|
||||
let open: boolean = false
|
||||
|
||||
@@ -78,13 +78,6 @@
|
||||
action: () => switchMode('content'),
|
||||
shortcutKey: CONTENT_SEARCH_PREFIX,
|
||||
icon: SearchCode
|
||||
},
|
||||
{
|
||||
search_id: 'switchto:log-search',
|
||||
label: 'Search Windmill logs',
|
||||
action: () => switchMode('logs'),
|
||||
shortcutKey: LOGS_PREFIX,
|
||||
icon: Search
|
||||
}
|
||||
]
|
||||
let defaultMenuItems: quickMenuItem[] = [
|
||||
@@ -108,7 +101,14 @@
|
||||
action: () => gotoPage('/schedules'),
|
||||
icon: CalendarIcon
|
||||
},
|
||||
...switchModeItems
|
||||
...switchModeItems,
|
||||
{
|
||||
search_id: 'nav:service_logs',
|
||||
label: 'Explore windmill service logs',
|
||||
action: () => gotoPage('/service_logs'),
|
||||
shortcutKey: LOGS_PREFIX,
|
||||
icon: Logs
|
||||
}
|
||||
]
|
||||
|
||||
let itemMap = {
|
||||
@@ -483,7 +483,7 @@
|
||||
}
|
||||
|
||||
function maxModalWidth(tab: SearchMode) {
|
||||
if (tab === 'runs' || tab === 'logs') {
|
||||
if (tab === 'runs') {
|
||||
return 'max-w-7xl'
|
||||
} else {
|
||||
return 'max-w-4xl'
|
||||
@@ -491,7 +491,7 @@
|
||||
}
|
||||
|
||||
function maxModalHeight(tab: SearchMode) {
|
||||
if (tab === 'runs' || tab === 'logs') {
|
||||
if (tab === 'runs') {
|
||||
return ''
|
||||
} else if (tab === 'content') {
|
||||
return 'max-h-[70vh]'
|
||||
@@ -614,12 +614,19 @@
|
||||
<Alert title="Service logs are only available to superadmins" type="warning">
|
||||
Service logs are only available to superadmins
|
||||
</Alert>
|
||||
{:else if searchTerm.length == 1}
|
||||
<ServiceLogsInner />
|
||||
{:else}
|
||||
<Alert title="Not yet supported" type="info">
|
||||
Full-text search across Windmill logs is not yet supported
|
||||
</Alert>
|
||||
<QuickMenuItem
|
||||
on:select={() =>
|
||||
gotoPage(
|
||||
`/service_logs?query=${encodeURIComponent(removePrefix(searchTerm, '!'))}`
|
||||
)}
|
||||
id="goto_service_logs_search"
|
||||
hovered={true}
|
||||
label={searchTerm === '!'
|
||||
? 'Explore Windmill service logs'
|
||||
: `Search '${removePrefix(searchTerm, '!')}' in Windmill's service logs`}
|
||||
icon={searchTerm === '!' ? Logs : Search}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if tab === 'runs'}
|
||||
@@ -680,40 +687,40 @@
|
||||
</QuickMenuItem>
|
||||
{/each}
|
||||
</div>
|
||||
{#if selectedItem === undefined}
|
||||
Select a result to preview
|
||||
{:else}
|
||||
<div class="w-8/12 max-h-[70vh]">
|
||||
<div class="w-8/12 max-h-[70vh]">
|
||||
{#if selectedItem === undefined}
|
||||
Select a result to preview
|
||||
{:else}
|
||||
<div class="h-[95%] overflow-y-scroll">
|
||||
<JobPreview
|
||||
id={selectedItem?.document?.id[0]}
|
||||
workspace={selectedWorkspace}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-row pt-3 pl-4 items-center text-xs text-secondary">
|
||||
{#if indexMetadata.indexed_until}
|
||||
<span class="px-2">
|
||||
Most recent indexed job was created <TimeAgo
|
||||
{/if}
|
||||
<div class="flex flex-row pt-3 pl-4 items-center text-xs text-secondary">
|
||||
{#if indexMetadata.indexed_until}
|
||||
<span class="px-2">
|
||||
Most recently indexed job was created <TimeAgo
|
||||
agoOnlyIfRecent
|
||||
date={indexMetadata.indexed_until || ''}
|
||||
/>
|
||||
</span>
|
||||
{/if}
|
||||
{#if indexMetadata.lost_lock_ownership}
|
||||
<Popover notClickable placement="top">
|
||||
<AlertTriangle size={16} class="text-gray-500" />
|
||||
<svelte:fragment slot="text">
|
||||
The current indexer is no longer indexing new jobs. This is most likely
|
||||
because of an ongoing deployment and indexing will resume once it's
|
||||
complete.
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
</span>
|
||||
{/if}
|
||||
{#if indexMetadata.lost_lock_ownership}
|
||||
<Popover notClickable placement="top">
|
||||
<AlertTriangle size={16} class="text-gray-500" />
|
||||
<svelte:fragment slot="text">
|
||||
The current indexer is no longer indexing new jobs. This is most likely
|
||||
because of an ongoing deployment and indexing will resume once it's
|
||||
complete.
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex w-full justify-center items-center h-48">
|
||||
<div class="flex flex-col w-full justify-center items-center h-48">
|
||||
<div class="text-tertiary text-center">
|
||||
{#if searchTerm === RUNS_PREFIX}
|
||||
<div class="text-2xl font-bold">Enter your search terms</div>
|
||||
@@ -735,6 +742,26 @@
|
||||
</Alert>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-row pt-10 text-xs text-secondary">
|
||||
{#if indexMetadata.indexed_until}
|
||||
<span class="px-2">
|
||||
Most recently indexed job was created <TimeAgo
|
||||
agoOnlyIfRecent
|
||||
date={indexMetadata.indexed_until}
|
||||
/>
|
||||
</span>
|
||||
{/if}
|
||||
{#if indexMetadata.lost_lock_ownership}
|
||||
<Popover notClickable placement="top">
|
||||
<AlertTriangle size={16} class="text-gray-500" />
|
||||
<svelte:fragment slot="text">
|
||||
The current indexer is no longer indexing new jobs. This is most likely
|
||||
because of an ongoing deployment and indexing will resume once it's
|
||||
complete.
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
Calendar,
|
||||
DollarSign,
|
||||
Eye,
|
||||
Logs,
|
||||
FolderCog,
|
||||
FolderOpen,
|
||||
Github,
|
||||
@@ -167,7 +168,29 @@
|
||||
],
|
||||
disabled: $userStore?.operator
|
||||
},
|
||||
{ label: 'Audit Logs', href: `${base}/audit_logs`, icon: Eye, disabled: $userStore?.operator }
|
||||
!$superadmin
|
||||
? {
|
||||
label: 'Audit Logs',
|
||||
href: `${base}/audit_logs`,
|
||||
icon: Eye,
|
||||
disabled: $userStore?.operator
|
||||
}
|
||||
: {
|
||||
label: 'Logs',
|
||||
icon: Logs,
|
||||
subItems: [
|
||||
{
|
||||
label: 'Audit Logs',
|
||||
href: `${base}/audit_logs`,
|
||||
icon: Eye
|
||||
},
|
||||
{
|
||||
label: 'Service Logs',
|
||||
href: `${base}/service_logs`,
|
||||
icon: Logs
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
let hasNewChangelogs = false
|
||||
|
||||
@@ -10,7 +10,31 @@ export type ScheduleTrigger = {
|
||||
}
|
||||
|
||||
export type TriggerContext = {
|
||||
selectedTrigger: Writable<'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets'>
|
||||
selectedTrigger: Writable<
|
||||
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets' | 'scheduledPoll'
|
||||
>
|
||||
primarySchedule: Writable<ScheduleTrigger | undefined | false>
|
||||
triggersCount: Writable<TriggersCount | undefined>
|
||||
simplifiedPoll: Writable<boolean | undefined>
|
||||
}
|
||||
|
||||
export function setScheduledPollSchedule(
|
||||
primarySchedule: Writable<ScheduleTrigger | undefined | false>,
|
||||
triggersCount: Writable<TriggersCount | undefined>
|
||||
) {
|
||||
const cron = '0 */5 * * * *'
|
||||
primarySchedule.set({
|
||||
enabled: true,
|
||||
summary: 'Check for new events every 5 minutes',
|
||||
cron: cron,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
args: {}
|
||||
})
|
||||
triggersCount.update((triggersCount) => {
|
||||
return {
|
||||
...(triggersCount ?? {}),
|
||||
schedule_count: (triggersCount?.schedule_count ?? 0) + 1,
|
||||
primary_schedule: { schedule: cron }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
import Alert from '$lib/components/common/alert/Alert.svelte'
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import { base } from '$lib/base'
|
||||
|
||||
$: console.log('dbg base', base)
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col w-full gap-4 text-sm">
|
||||
<Alert
|
||||
title="Scheduled Poll"
|
||||
size="xs"
|
||||
documentationLink="https://www.windmill.dev/docs/flows/flow_trigger"
|
||||
>
|
||||
Sheduled Poll is a trigger that polls an external API at a given interval. It is useful for
|
||||
triggering jobs based on the change of external states. Under the hood, it is a script triggered
|
||||
by a schedule, and returning a list of the changed states so we can iterate over them.
|
||||
</Alert>
|
||||
|
||||
<Label label="Setting a scheduled poll" primary>
|
||||
<ul class="pt-4 space-y-2 text-sm">
|
||||
<li>
|
||||
1. Chose a trigger script using the script picker that will poll for new events regularly <img
|
||||
class="shadow-lg rounded"
|
||||
alt="script picker"
|
||||
src="{base}/script-picker.png"
|
||||
width="400"
|
||||
/>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
2. Add logic to process each new event <img
|
||||
class="shadow-lg rounded"
|
||||
alt="create action"
|
||||
src="{base}/create_action.png"
|
||||
width="500"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</Label>
|
||||
</div>
|
||||
@@ -11,6 +11,7 @@
|
||||
import { getContext } from 'svelte'
|
||||
import type { TriggerContext } from '$lib/components/triggers'
|
||||
import WebsocketTriggersPanel from './WebsocketTriggersPanel.svelte'
|
||||
import ScheduledPollPanel from './ScheduledPollPanel.svelte'
|
||||
|
||||
export let noEditor: boolean
|
||||
export let newItem = false
|
||||
@@ -19,67 +20,88 @@
|
||||
export let schema: any
|
||||
export let isFlow: boolean
|
||||
|
||||
const { selectedTrigger } = getContext<TriggerContext>('TriggerContext')
|
||||
const { selectedTrigger, simplifiedPoll } = getContext<TriggerContext>('TriggerContext')
|
||||
</script>
|
||||
|
||||
<FlowCard {noEditor} title="Triggers">
|
||||
<div class="pt-4">
|
||||
<Tabs bind:selected={$selectedTrigger}>
|
||||
<Tab value="webhooks" selectedClass="text-primary font-semibold">Webhooks</Tab>
|
||||
<Tab value="schedules" selectedClass="text-primary text-sm font-semibold">Schedules</Tab>
|
||||
<Tab value="routes" selectedClass="text-primary text-sm font-semibold">Routes</Tab>
|
||||
<Tab value="websockets" selectedClass="text-primary text-sm font-semibold">Websockets</Tab>
|
||||
<Tab value="emails" selectedClass="text-primary text-sm font-semibold">Email</Tab>
|
||||
{#if !$simplifiedPoll}
|
||||
<Tabs bind:selected={$selectedTrigger}>
|
||||
<Tab value="webhooks" selectedClass="text-primary font-semibold">Webhooks</Tab>
|
||||
<Tab value="schedules" selectedClass="text-primary text-sm font-semibold">Schedules</Tab>
|
||||
<Tab value="routes" selectedClass="text-primary text-sm font-semibold">Routes</Tab>
|
||||
<Tab value="websockets" selectedClass="text-primary text-sm font-semibold">Websockets</Tab>
|
||||
<Tab value="emails" selectedClass="text-primary text-sm font-semibold">Email</Tab>
|
||||
<Tab value="scheduledPoll" selectedClass="text-primary text-sm font-semibold"
|
||||
>Scheduled Poll</Tab
|
||||
>
|
||||
|
||||
<svelte:fragment slot="content">
|
||||
{#if $selectedTrigger === 'webhooks'}
|
||||
<div class="p-4">
|
||||
<WebhooksPanel
|
||||
scopes={isFlow ? [`run:flow/${currentPath}`] : [`run:script/${currentPath}`]}
|
||||
path={currentPath}
|
||||
{isFlow}
|
||||
args={{}}
|
||||
token=""
|
||||
{newItem}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<svelte:fragment slot="content">
|
||||
{#if $selectedTrigger === 'webhooks'}
|
||||
<div class="p-4">
|
||||
<WebhooksPanel
|
||||
scopes={isFlow ? [`run:flow/${currentPath}`] : [`run:script/${currentPath}`]}
|
||||
path={currentPath}
|
||||
{isFlow}
|
||||
args={{}}
|
||||
token=""
|
||||
{newItem}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $selectedTrigger === 'emails'}
|
||||
<div class="p-4">
|
||||
<EmailTriggerPanel
|
||||
token=""
|
||||
scopes={isFlow ? [`run:flow/${currentPath}`] : [`run:script/${currentPath}`]}
|
||||
path={currentPath}
|
||||
{isFlow}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $selectedTrigger === 'emails'}
|
||||
<div class="p-4">
|
||||
<EmailTriggerPanel
|
||||
token=""
|
||||
scopes={isFlow ? [`run:flow/${currentPath}`] : [`run:script/${currentPath}`]}
|
||||
path={currentPath}
|
||||
{isFlow}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $selectedTrigger === 'routes'}
|
||||
<div class="p-4">
|
||||
<RoutesPanel {newItem} path={currentPath} {isFlow} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $selectedTrigger === 'routes'}
|
||||
<div class="p-4">
|
||||
<RoutesPanel {newItem} path={currentPath} {isFlow} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $selectedTrigger === 'websockets'}
|
||||
<div class="p-4">
|
||||
<WebsocketTriggersPanel {newItem} path={currentPath} {isFlow} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $selectedTrigger === 'websockets'}
|
||||
<div class="p-4">
|
||||
<WebsocketTriggersPanel {newItem} path={currentPath} {isFlow} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $selectedTrigger === 'schedules'}
|
||||
<div class="p-2">
|
||||
<RunPageSchedules
|
||||
{schema}
|
||||
{isFlow}
|
||||
path={initialPath}
|
||||
{newItem}
|
||||
can_write={canWrite(currentPath, {}, $userStore)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
{#if $selectedTrigger === 'schedules'}
|
||||
<div class="p-4">
|
||||
<RunPageSchedules
|
||||
{schema}
|
||||
{isFlow}
|
||||
path={initialPath}
|
||||
{newItem}
|
||||
can_write={canWrite(currentPath, {}, $userStore)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $selectedTrigger === 'scheduledPoll'}
|
||||
<div class="p-4">
|
||||
<ScheduledPollPanel />
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
{:else}
|
||||
<div class="px-4 pb-2">
|
||||
<RunPageSchedules
|
||||
{schema}
|
||||
{isFlow}
|
||||
path={initialPath}
|
||||
{newItem}
|
||||
can_write={canWrite(currentPath, {}, $userStore)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</FlowCard>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export function load() {
|
||||
return {
|
||||
stuff: { title: 'Service Logs' }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
import { Alert } from '$lib/components/common'
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
import ServiceLogsInner from '$lib/components/ServiceLogsInner.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import { superadmin } from '$lib/stores'
|
||||
import { Search, AlertTriangle } from 'lucide-svelte'
|
||||
|
||||
let searchTerm = $page.url.searchParams.get('query') ?? ''
|
||||
let queryParseErrors: string[] | undefined = undefined
|
||||
</script>
|
||||
|
||||
<div class="w-full h-screen">
|
||||
<div class="px-2">
|
||||
<div class="flex items-center space-x-2 flex-row justify-between">
|
||||
<div class="flex flex-row flex-wrap justify-between py-2 my-4 px-4 gap-1 items-center">
|
||||
<h1 class="!text-2xl font-semibold leading-6 tracking-tight">Service logs</h1>
|
||||
<Tooltip >
|
||||
Explore and search Windmill service logs from within Windmill!
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !$superadmin}
|
||||
<Alert title="Service logs are only available to superadmins" type="warning">
|
||||
Service logs are only available to superadmins
|
||||
</Alert>
|
||||
{:else}
|
||||
<div class="m-1 px-2 flex flex-row gap-1 items-center border-2 rounded-lg">
|
||||
<Search size="16" />
|
||||
<input
|
||||
id="quickSearchInput"
|
||||
type="text"
|
||||
class="quick-search-input !bg-surface"
|
||||
bind:value={searchTerm}
|
||||
autocomplete="off"
|
||||
autofocus
|
||||
/>
|
||||
{#if searchTerm !== '' && queryParseErrors && queryParseErrors.length > 0}
|
||||
<Popover notClickable placement="bottom-start">
|
||||
<AlertTriangle size={16} class="text-yellow-500" />
|
||||
<svelte:fragment slot="text">
|
||||
Some of your search terms have been ignored because one or more parse errors:<br /><br
|
||||
/>
|
||||
<ul>
|
||||
{#each queryParseErrors as msg}
|
||||
<li>- {msg}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<ServiceLogsInner {searchTerm} bind:queryParseErrors />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.quick-search-input {
|
||||
outline: none;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.quick-search-input:focus-visible {
|
||||
outline: none !important;
|
||||
}
|
||||
</style>
|
||||
@@ -74,12 +74,13 @@
|
||||
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(undefined)
|
||||
const triggersCount = writable<TriggersCount | undefined>(undefined)
|
||||
const selectedTriggerStore = writable<
|
||||
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets'
|
||||
'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' | 'websockets' | 'scheduledPoll'
|
||||
>('webhooks')
|
||||
setContext<TriggerContext>('TriggerContext', {
|
||||
primarySchedule: primaryScheduleStore,
|
||||
selectedTrigger: selectedTriggerStore,
|
||||
triggersCount: triggersCount
|
||||
triggersCount: triggersCount,
|
||||
simplifiedPoll: writable(false)
|
||||
})
|
||||
|
||||
setContext<FlowEditorContext>('FlowEditorContext', {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
@@ -463,7 +463,8 @@ const config = {
|
||||
height: 'height'
|
||||
},
|
||||
fontSize: {
|
||||
'2xs': '0.7rem'
|
||||
'2xs': '0.7rem',
|
||||
'3xs': '0.65rem'
|
||||
},
|
||||
screens: {
|
||||
fhd: '1900px',
|
||||
|
||||
+2
-2
@@ -4,8 +4,8 @@ verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
wmill = ">=1.417.2"
|
||||
wmill_pg = ">=1.417.2"
|
||||
wmill = ">=1.418.0"
|
||||
wmill_pg = ">=1.418.0"
|
||||
sendgrid = "*"
|
||||
mysql-connector-python = "*"
|
||||
pymongo = "*"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.417.2
|
||||
version: 1.418.0
|
||||
title: OpenFlow Spec
|
||||
contact:
|
||||
name: Ruben Fiszel
|
||||
@@ -269,6 +269,8 @@ components:
|
||||
type: number
|
||||
custom_concurrency_key:
|
||||
type: string
|
||||
is_trigger:
|
||||
type: boolean
|
||||
required:
|
||||
- type
|
||||
- content
|
||||
@@ -292,6 +294,8 @@ components:
|
||||
- script
|
||||
tag_override:
|
||||
type: string
|
||||
is_trigger:
|
||||
type: boolean
|
||||
required:
|
||||
- type
|
||||
- path
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
RootModule = 'WindmillClient.psm1'
|
||||
|
||||
# Version number of this module.
|
||||
ModuleVersion = '1.417.2'
|
||||
ModuleVersion = '1.418.0'
|
||||
|
||||
# Supported PSEditions
|
||||
# CompatiblePSEditions = @()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "wmill"
|
||||
version = "1.417.2"
|
||||
version = "1.418.0"
|
||||
description = "A client library for accessing Windmill server wrapping the Windmill client API"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://windmill.dev"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "wmill-pg"
|
||||
version = "1.417.2"
|
||||
version = "1.418.0"
|
||||
description = "An extension client for the wmill client library focused on pg"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://windmill.dev"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@windmill/windmill",
|
||||
"version": "1.417.2",
|
||||
"version": "1.418.0",
|
||||
"exports": "./src/index.ts",
|
||||
"publish": {
|
||||
"exclude": ["!src", "./s3Types.ts", "./client.ts"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "windmill-client",
|
||||
"description": "Windmill SDK client for browsers and Node.js",
|
||||
"version": "1.417.2",
|
||||
"version": "1.418.0",
|
||||
"author": "Ruben Fiszel",
|
||||
"license": "Apache 2.0",
|
||||
"devDependencies": {
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.417.2
|
||||
1.418.0
|
||||
|
||||
Reference in New Issue
Block a user