mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-04 00:03:02 +00:00
Merge branch 'main' into fr/xyflow
This commit is contained in:
@@ -1,3 +1,13 @@
|
||||
{
|
||||
layer4 {
|
||||
:25 {
|
||||
proxy {
|
||||
to windmill_server:2525
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{$BASE_URL} {
|
||||
bind {$ADDRESS}
|
||||
reverse_proxy /ws/* http://lsp:3001
|
||||
|
||||
+4
-4
@@ -127,13 +127,13 @@ RUN set -eux; \
|
||||
arch="$(dpkg --print-architecture)"; arch="${arch##*-}"; \
|
||||
case "$arch" in \
|
||||
'amd64') \
|
||||
targz='go1.21.6.linux-amd64.tar.gz'; \
|
||||
targz='go1.22.5.linux-amd64.tar.gz'; \
|
||||
;; \
|
||||
'arm64') \
|
||||
targz='go1.21.6.linux-arm64.tar.gz'; \
|
||||
targz='go1.22.5.linux-arm64.tar.gz'; \
|
||||
;; \
|
||||
'armhf') \
|
||||
targz='go1.21.6.linux-armv6l.tar.gz'; \
|
||||
targz='go1.22.5.linux-armv6l.tar.gz'; \
|
||||
;; \
|
||||
*) echo >&2 "error: unsupported architecture '$arch' (likely packaging update needed)"; exit 1 ;; \
|
||||
esac; \
|
||||
@@ -173,4 +173,4 @@ RUN windmill cache
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["windmill"]
|
||||
CMD ["windmill"]
|
||||
|
||||
Generated
+14
@@ -4856,6 +4856,16 @@ dependencies = [
|
||||
"gethostname",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mail-parser"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5a1335c3a964788c90cb42ae04a34b5f2628e89566949ce3bd4ada695c0bcd"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mail-send"
|
||||
version = "0.4.9"
|
||||
@@ -10455,9 +10465,12 @@ dependencies = [
|
||||
"jsonwebtoken",
|
||||
"lazy_static",
|
||||
"magic-crypt",
|
||||
"mail-parser",
|
||||
"mime_guess",
|
||||
"native-tls",
|
||||
"object_store",
|
||||
"openidconnect",
|
||||
"openssl",
|
||||
"pin-project",
|
||||
"prometheus",
|
||||
"quick_cache",
|
||||
@@ -10479,6 +10492,7 @@ dependencies = [
|
||||
"tinyvector",
|
||||
"tokenizers",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-tar",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
|
||||
@@ -240,6 +240,9 @@ candle-nn = "0.3.0"
|
||||
tiberius = { git = "https://github.com/prisma/tiberius", rev = "8f66a699dfa041e7b5f736c7e94f92c945453c9e", default-features = false, features = ["rustls", "tds73", "chrono", "sql-browser-tokio"]}
|
||||
pin-project = "1"
|
||||
indexmap = { version = "2.2.5", features = ["serde"]}
|
||||
tokio-native-tls = "^0"
|
||||
openssl = "=0.10"
|
||||
mail-parser = "^0"
|
||||
|
||||
datafusion = "39.0.0"
|
||||
object_store = { version = "0.10.0", features = ["aws", "azure"] }
|
||||
|
||||
@@ -1 +1 @@
|
||||
1a2febc371c907789b25860366872fca888d6477
|
||||
70e475a5cec356d2fe992038f303aea8988b5046
|
||||
@@ -405,6 +405,7 @@ Windmill Community Edition {GIT_VERSION}
|
||||
server_killpill_rx,
|
||||
base_internal_tx,
|
||||
server_mode,
|
||||
base_internal_url.clone(),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
|
||||
@@ -128,6 +128,7 @@ impl ApiServer {
|
||||
rx,
|
||||
port_tx,
|
||||
false,
|
||||
format!("http://localhost:{}", addr.port()),
|
||||
));
|
||||
|
||||
_port_rx.await.unwrap();
|
||||
|
||||
@@ -59,6 +59,10 @@ tracing-subscriber.workspace = true
|
||||
quick_cache.workspace = true
|
||||
rand.workspace = true
|
||||
time.workspace = true
|
||||
native-tls.workspace = true
|
||||
tokio-native-tls.workspace = true
|
||||
openssl.workspace = true
|
||||
mail-parser = { workspace = true, features = ["serde_support"] }
|
||||
magic-crypt.workspace = true
|
||||
tempfile.workspace = true
|
||||
tokio-util.workspace = true
|
||||
|
||||
@@ -184,7 +184,10 @@ pub fn workspaced_service() -> Router {
|
||||
.layer(ce_headers.clone()),
|
||||
)
|
||||
.route("/run/preview", post(run_preview_script))
|
||||
.route("/run/preview_bundle", post(run_bundle_preview_script))
|
||||
.route(
|
||||
"/run/preview_bundle",
|
||||
post(run_bundle_preview_script).layer(axum::extract::DefaultBodyLimit::disable()),
|
||||
)
|
||||
.route("/add_batch_jobs/:n", post(add_batch_jobs))
|
||||
.route("/run/preview_flow", post(run_preview_flow_job))
|
||||
.route(
|
||||
@@ -1031,7 +1034,7 @@ pub struct ListableCompletedJob {
|
||||
pub labels: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
#[derive(Deserialize, Clone, Default)]
|
||||
pub struct RunJobQuery {
|
||||
scheduled_for: Option<chrono::DateTime<chrono::Utc>>,
|
||||
scheduled_in_secs: Option<i64>,
|
||||
@@ -1459,7 +1462,6 @@ async fn cancel_selection(
|
||||
Path(w_id): Path<String>,
|
||||
Json(jobs): Json<Vec<Uuid>>,
|
||||
) -> error::JsonResult<Vec<Uuid>> {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let jobs_to_cancel = sqlx::query_scalar!(
|
||||
@@ -2611,6 +2613,7 @@ enum PreviewKind {
|
||||
Http,
|
||||
Noop,
|
||||
Bundle,
|
||||
Tarbundle,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -2748,6 +2751,23 @@ pub async fn run_flow_by_path(
|
||||
Path((w_id, flow_path)): Path<(String, StripPath)>,
|
||||
Query(run_query): Query<RunJobQuery>,
|
||||
args: PushArgsOwned,
|
||||
) -> error::Result<(StatusCode, String)> {
|
||||
run_flow_by_path_inner(
|
||||
authed, db, user_db, rsmq, w_id, flow_path, run_query, args, None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn run_flow_by_path_inner(
|
||||
authed: ApiAuthed,
|
||||
db: DB,
|
||||
user_db: UserDB,
|
||||
rsmq: Option<rsmq_async::MultiplexedRsmq>,
|
||||
w_id: String,
|
||||
flow_path: StripPath,
|
||||
run_query: RunJobQuery,
|
||||
args: PushArgsOwned,
|
||||
label_prefix: Option<String>,
|
||||
) -> error::Result<(StatusCode, String)> {
|
||||
#[cfg(feature = "enterprise")]
|
||||
check_license_key_valid().await?;
|
||||
@@ -2781,7 +2801,9 @@ pub async fn run_flow_by_path(
|
||||
&w_id,
|
||||
JobPayload::Flow { path: flow_path.to_string(), dedicated_worker },
|
||||
PushArgs { args: &args.args, extra: args.extra },
|
||||
authed.display_username(),
|
||||
&label_prefix
|
||||
.map(|x| x + authed.display_username())
|
||||
.unwrap_or_else(|| authed.display_username().to_string()),
|
||||
&authed.email,
|
||||
username_to_permissioned_as(&authed.username),
|
||||
scheduled_for,
|
||||
@@ -2907,6 +2929,31 @@ pub async fn run_script_by_path(
|
||||
Path((w_id, script_path)): Path<(String, StripPath)>,
|
||||
Query(run_query): Query<RunJobQuery>,
|
||||
args: PushArgsOwned,
|
||||
) -> error::Result<(StatusCode, String)> {
|
||||
run_script_by_path_inner(
|
||||
authed,
|
||||
db,
|
||||
user_db,
|
||||
rsmq,
|
||||
w_id,
|
||||
script_path,
|
||||
run_query,
|
||||
args,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn run_script_by_path_inner(
|
||||
authed: ApiAuthed,
|
||||
db: DB,
|
||||
user_db: UserDB,
|
||||
rsmq: Option<rsmq_async::MultiplexedRsmq>,
|
||||
w_id: String,
|
||||
script_path: StripPath,
|
||||
run_query: RunJobQuery,
|
||||
args: PushArgsOwned,
|
||||
label_prefix: Option<String>,
|
||||
) -> error::Result<(StatusCode, String)> {
|
||||
#[cfg(feature = "enterprise")]
|
||||
check_license_key_valid().await?;
|
||||
@@ -2932,7 +2979,9 @@ pub async fn run_script_by_path(
|
||||
&w_id,
|
||||
job_payload,
|
||||
PushArgs { args: &args.args, extra: args.extra },
|
||||
authed.display_username(),
|
||||
&label_prefix
|
||||
.map(|x| x + authed.display_username())
|
||||
.unwrap_or_else(|| authed.display_username().to_string()),
|
||||
&authed.email,
|
||||
username_to_permissioned_as(&authed.username),
|
||||
scheduled_for,
|
||||
@@ -3810,6 +3859,8 @@ async fn run_bundle_preview_script(
|
||||
Query(run_query): Query<RunJobQuery>,
|
||||
mut multipart: axum::extract::Multipart,
|
||||
) -> error::Result<(StatusCode, String)> {
|
||||
use windmill_common::scripts::PREVIEW_IS_TAR_CODEBASE_HASH;
|
||||
|
||||
check_license_key_valid().await?;
|
||||
|
||||
check_scopes(&authed, || format!("runscript"))?;
|
||||
@@ -3822,9 +3873,12 @@ async fn run_bundle_preview_script(
|
||||
let mut job_id = None;
|
||||
let mut tx = None;
|
||||
let mut uploaded = false;
|
||||
let mut is_tar = false;
|
||||
|
||||
while let Some(field) = multipart.next_field().await.unwrap() {
|
||||
let name = field.name().unwrap().to_string();
|
||||
let data = field.bytes().await.unwrap();
|
||||
let data = field.bytes().await;
|
||||
let data = data.map_err(to_anyhow)?;
|
||||
if name == "preview" {
|
||||
let preview: Preview = serde_json::from_slice(&data).map_err(to_anyhow)?;
|
||||
|
||||
@@ -3836,27 +3890,33 @@ async fn run_bundle_preview_script(
|
||||
|
||||
let args = preview.args.unwrap_or_default();
|
||||
|
||||
is_tar = match preview.kind {
|
||||
Some(PreviewKind::Tarbundle) => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
// tracing::info!("is_tar 1: {is_tar}");
|
||||
// hmap.insert("")
|
||||
let (uuid, ntx) = push(
|
||||
&db,
|
||||
ltx,
|
||||
&w_id,
|
||||
match preview.kind {
|
||||
Some(PreviewKind::Identity) => JobPayload::Identity,
|
||||
Some(PreviewKind::Noop) => JobPayload::Noop,
|
||||
_ => JobPayload::Code(RawCode {
|
||||
hash: Some(PREVIEW_IS_CODEBASE_HASH),
|
||||
content: preview.content.unwrap_or_default(),
|
||||
path: preview.path,
|
||||
language: preview.language.unwrap_or(ScriptLang::Deno),
|
||||
lock: preview.lock,
|
||||
concurrent_limit: None, // TODO(gbouv): once I find out how to store limits in the content of a script, should be easy to plug limits here
|
||||
concurrency_time_window_s: None, // TODO(gbouv): same as above
|
||||
cache_ttl: None,
|
||||
dedicated_worker: preview.dedicated_worker,
|
||||
custom_concurrency_key: None,
|
||||
}),
|
||||
},
|
||||
JobPayload::Code(RawCode {
|
||||
hash: if is_tar {
|
||||
Some(PREVIEW_IS_TAR_CODEBASE_HASH)
|
||||
} else {
|
||||
Some(PREVIEW_IS_CODEBASE_HASH)
|
||||
},
|
||||
content: preview.content.unwrap_or_default(),
|
||||
path: preview.path,
|
||||
language: preview.language.unwrap_or(ScriptLang::Deno),
|
||||
lock: preview.lock,
|
||||
concurrent_limit: None, // TODO(gbouv): once I find out how to store limits in the content of a script, should be easy to plug limits here
|
||||
concurrency_time_window_s: None, // TODO(gbouv): same as above
|
||||
cache_ttl: None,
|
||||
dedicated_worker: preview.dedicated_worker,
|
||||
custom_concurrency_key: None,
|
||||
}),
|
||||
PushArgs::from(&args),
|
||||
authed.display_username(),
|
||||
&authed.email,
|
||||
@@ -3881,7 +3941,7 @@ async fn run_bundle_preview_script(
|
||||
tx = Some(ntx);
|
||||
}
|
||||
if name == "file" {
|
||||
let id = job_id
|
||||
let mut id = job_id
|
||||
.as_ref()
|
||||
.ok_or_else(|| {
|
||||
Error::BadRequest(
|
||||
@@ -3890,6 +3950,12 @@ async fn run_bundle_preview_script(
|
||||
})?
|
||||
.to_string();
|
||||
|
||||
// tracing::info!("is_tar 2: {is_tar}");
|
||||
|
||||
if is_tar {
|
||||
id = format!("{}.tar", id);
|
||||
}
|
||||
|
||||
uploaded = true;
|
||||
|
||||
if let Some(os) = windmill_common::s3_helpers::OBJECT_STORE_CACHE_SETTINGS
|
||||
@@ -4332,6 +4398,31 @@ pub async fn run_job_by_hash(
|
||||
Path((w_id, script_hash)): Path<(String, ScriptHash)>,
|
||||
Query(run_query): Query<RunJobQuery>,
|
||||
args: PushArgsOwned,
|
||||
) -> error::Result<(StatusCode, String)> {
|
||||
run_job_by_hash_inner(
|
||||
authed,
|
||||
db,
|
||||
user_db,
|
||||
rsmq,
|
||||
w_id,
|
||||
script_hash,
|
||||
run_query,
|
||||
args,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn run_job_by_hash_inner(
|
||||
authed: ApiAuthed,
|
||||
db: DB,
|
||||
user_db: UserDB,
|
||||
rsmq: Option<rsmq_async::MultiplexedRsmq>,
|
||||
w_id: String,
|
||||
script_hash: ScriptHash,
|
||||
run_query: RunJobQuery,
|
||||
args: PushArgsOwned,
|
||||
label_prefix: Option<String>,
|
||||
) -> error::Result<(StatusCode, String)> {
|
||||
#[cfg(feature = "enterprise")]
|
||||
check_license_key_valid().await?;
|
||||
@@ -4378,7 +4469,9 @@ pub async fn run_job_by_hash(
|
||||
priority,
|
||||
},
|
||||
PushArgs { args: &args.args, extra: args.extra },
|
||||
authed.display_username(),
|
||||
&label_prefix
|
||||
.map(|x| x + authed.display_username())
|
||||
.unwrap_or_else(|| authed.display_username().to_string()),
|
||||
&authed.email,
|
||||
username_to_permissioned_as(&authed.username),
|
||||
scheduled_for,
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::ee::ExternalJwks;
|
||||
#[cfg(feature = "embedding")]
|
||||
use crate::embeddings::load_embeddings_db;
|
||||
use crate::oauth2_ee::AllClients;
|
||||
use crate::smtp_server_ee::SmtpServer;
|
||||
use crate::tracing_init::MyOnFailure;
|
||||
use crate::{
|
||||
oauth2_ee::SlackVerifier,
|
||||
@@ -76,6 +77,7 @@ mod schedule;
|
||||
mod scim_ee;
|
||||
mod scripts;
|
||||
mod settings;
|
||||
pub mod smtp_server_ee;
|
||||
mod static_assets;
|
||||
mod stripe_ee;
|
||||
mod tracing_init;
|
||||
@@ -163,6 +165,7 @@ pub async fn run_server(
|
||||
mut rx: tokio::sync::broadcast::Receiver<()>,
|
||||
port_tx: tokio::sync::oneshot::Sender<String>,
|
||||
server_mode: bool,
|
||||
base_internal_url: String,
|
||||
) -> anyhow::Result<()> {
|
||||
if let Some(mut rsmq) = rsmq.clone() {
|
||||
for tag in ALL_TAGS.read().await.iter() {
|
||||
@@ -194,8 +197,8 @@ pub async fn run_server(
|
||||
|
||||
let middleware_stack = ServiceBuilder::new()
|
||||
.layer(Extension(db.clone()))
|
||||
.layer(Extension(rsmq))
|
||||
.layer(Extension(user_db))
|
||||
.layer(Extension(rsmq.clone()))
|
||||
.layer(Extension(user_db.clone()))
|
||||
.layer(Extension(auth_cache.clone()))
|
||||
.layer(Extension(index_reader))
|
||||
.layer(Extension(index_writer))
|
||||
@@ -214,7 +217,18 @@ pub async fn run_server(
|
||||
|
||||
if server_mode {
|
||||
#[cfg(feature = "embedding")]
|
||||
load_embeddings_db(&db)
|
||||
load_embeddings_db(&db);
|
||||
|
||||
let smtp_server = Arc::new(SmtpServer {
|
||||
db: db.clone(),
|
||||
user_db: user_db,
|
||||
auth_cache: auth_cache.clone(),
|
||||
rsmq: rsmq,
|
||||
base_internal_url: base_internal_url.clone(),
|
||||
});
|
||||
if let Err(err) = smtp_server.start_listener_thread(addr).await {
|
||||
tracing::error!("Error starting SMTP server: {err:#}");
|
||||
}
|
||||
}
|
||||
|
||||
let job_helpers_service = {
|
||||
|
||||
@@ -24,7 +24,10 @@ use axum::{
|
||||
use serde::Deserialize;
|
||||
use windmill_common::{
|
||||
error::{self, JsonResult, Result},
|
||||
global_settings::{AUTOMATE_USERNAME_CREATION_SETTING, ENV_SETTINGS, HUB_BASE_URL_SETTING},
|
||||
global_settings::{
|
||||
AUTOMATE_USERNAME_CREATION_SETTING, EMAIL_DOMAIN_SETTING, ENV_SETTINGS,
|
||||
HUB_BASE_URL_SETTING,
|
||||
},
|
||||
server::Smtp,
|
||||
utils::send_email,
|
||||
};
|
||||
@@ -256,6 +259,7 @@ pub async fn get_global_setting(
|
||||
&& !key.starts_with("default_recovery_handler_")
|
||||
&& key != AUTOMATE_USERNAME_CREATION_SETTING
|
||||
&& key != HUB_BASE_URL_SETTING
|
||||
&& key != EMAIL_DOMAIN_SETTING
|
||||
{
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
use crate::{db::DB, users::AuthCache};
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
use windmill_common::db::UserDB;
|
||||
|
||||
pub struct SmtpServer {
|
||||
pub auth_cache: Arc<AuthCache>,
|
||||
pub db: DB,
|
||||
pub user_db: UserDB,
|
||||
pub rsmq: Option<rsmq_async::MultiplexedRsmq>,
|
||||
pub base_internal_url: String,
|
||||
}
|
||||
|
||||
impl SmtpServer {
|
||||
pub async fn start_listener_thread(self: Arc<Self>, _addr: SocketAddr) -> anyhow::Result<()> {
|
||||
Err(anyhow::anyhow!("Implementation not open source"))
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ pub const HUB_BASE_URL_SETTING: &str = "hub_base_url";
|
||||
pub const CRITICAL_ERROR_CHANNELS_SETTING: &str = "critical_error_channels";
|
||||
pub const DEV_INSTANCE_SETTING: &str = "dev_instance";
|
||||
pub const JWT_SECRET_SETTING: &str = "jwt_secret";
|
||||
pub const EMAIL_DOMAIN_SETTING: &str = "email_domain";
|
||||
|
||||
pub const ENV_SETTINGS: [&str; 50] = [
|
||||
"DISABLE_NSJAIL",
|
||||
|
||||
@@ -133,6 +133,7 @@ impl Display for ScriptKind {
|
||||
}
|
||||
|
||||
pub const PREVIEW_IS_CODEBASE_HASH: i64 = -42;
|
||||
pub const PREVIEW_IS_TAR_CODEBASE_HASH: i64 = -43;
|
||||
|
||||
#[derive(Serialize, sqlx::FromRow)]
|
||||
pub struct Script {
|
||||
|
||||
@@ -967,11 +967,10 @@ try {{
|
||||
};
|
||||
|
||||
let reserved_variables_args_out_f = async {
|
||||
if annotation.native_mode {
|
||||
return Ok(HashMap::new()) as error::Result<HashMap<String, String>>;
|
||||
}
|
||||
let args_and_out_f = async {
|
||||
create_args_and_out_file(&client, job, job_dir, db).await?;
|
||||
if !annotation.native_mode {
|
||||
create_args_and_out_file(&client, job, job_dir, db).await?;
|
||||
}
|
||||
Ok(()) as Result<()>
|
||||
};
|
||||
let reserved_variables_f = async {
|
||||
|
||||
@@ -111,7 +111,7 @@ pub async fn extract_tar(tar: bytes::Bytes, folder: &str) -> error::Result<()> {
|
||||
tracing::info!("Failed to untar to {folder}. Error: {:?}", e);
|
||||
fs::remove_dir_all(&folder).await?;
|
||||
return Err(error::Error::ExecutionErr(format!(
|
||||
"Failed to untar piptar {folder}"
|
||||
"Failed to untar tar {folder}"
|
||||
)));
|
||||
}
|
||||
tracing::info!(
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
*/
|
||||
|
||||
use windmill_common::{
|
||||
auth::{fetch_authed_from_permissioned_as, JWTAuthClaims, JobPerms, JWT_SECRET},
|
||||
worker::{get_windmill_memory_usage, get_worker_memory_usage, TMP_DIR},
|
||||
auth::{fetch_authed_from_permissioned_as, JWTAuthClaims, JobPerms, JWT_SECRET}, scripts::PREVIEW_IS_TAR_CODEBASE_HASH, worker::{get_windmill_memory_usage, get_worker_memory_usage, TMP_DIR}
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
@@ -2279,6 +2278,34 @@ async fn handle_queued_job<R: rsmq_async::RsmqConnection + Send + Sync + Clone>(
|
||||
return Err(Error::ExecutionErr(e.to_string()));
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
if job.created_by.starts_with("email-trigger-") {
|
||||
let daily_count = sqlx::query!(
|
||||
"SELECT value FROM metrics WHERE id = 'email_trigger_usage' AND created_at > NOW() - INTERVAL '1 day' ORDER BY created_at DESC LIMIT 1"
|
||||
).fetch_optional(db).await?.map(|x| serde_json::from_value::<i64>(x.value).unwrap_or(1));
|
||||
|
||||
if let Some(count) = daily_count {
|
||||
if count >= 100 {
|
||||
return Err(error::Error::QuotaExceeded(format!(
|
||||
"Email trigger usage limit of 100 per day has been reached."
|
||||
)));
|
||||
} else {
|
||||
sqlx::query!(
|
||||
"UPDATE metrics SET value = $1 WHERE id = 'email_trigger_usage' AND created_at > NOW() - INTERVAL '1 day'",
|
||||
serde_json::json!(count + 1)
|
||||
)
|
||||
.execute(db)
|
||||
.await?;
|
||||
}
|
||||
} else {
|
||||
sqlx::query!(
|
||||
"INSERT INTO metrics (id, value) VALUES ('email_trigger_usage', to_jsonb(1))"
|
||||
)
|
||||
.execute(db)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
let step = if job.is_flow_step {
|
||||
let r = update_flow_status_in_progress(
|
||||
db,
|
||||
@@ -2792,23 +2819,23 @@ async fn handle_code_execution_job(
|
||||
envs,
|
||||
codebase,
|
||||
} = match job.job_kind {
|
||||
JobKind::Preview => ContentReqLangEnvs {
|
||||
content: job
|
||||
.raw_code
|
||||
.clone()
|
||||
.unwrap_or_else(|| "no raw code".to_owned()),
|
||||
lockfile: job.raw_lock.clone(),
|
||||
language: job.language.to_owned(),
|
||||
envs: None,
|
||||
codebase: if job
|
||||
.script_hash
|
||||
.is_some_and(|y| y.0 == PREVIEW_IS_CODEBASE_HASH)
|
||||
{
|
||||
Some(job.id.to_string())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
JobKind::Preview => {
|
||||
let codebase = match job.script_hash.map(|x| x.0) {
|
||||
Some(PREVIEW_IS_CODEBASE_HASH) => Some(job.id.to_string()),
|
||||
Some(PREVIEW_IS_TAR_CODEBASE_HASH) => Some(format!("{}.tar", job.id)),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
ContentReqLangEnvs {
|
||||
content: job
|
||||
.raw_code
|
||||
.clone()
|
||||
.unwrap_or_else(|| "no raw code".to_owned()),
|
||||
lockfile: job.raw_lock.clone(),
|
||||
language: job.language.to_owned(),
|
||||
envs: None,
|
||||
codebase
|
||||
}},
|
||||
JobKind::Script_Hub => {
|
||||
get_hub_script_content_and_requirements(job.script_path.clone(), db).await?
|
||||
}
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ import {
|
||||
} from "./conf.ts";
|
||||
import { SyncCodebase, listSyncCodebases } from "./codebase.ts";
|
||||
import fs from "node:fs";
|
||||
import { Tarball } from "npm:@ayonli/jsext/archive";
|
||||
import { type Tarball } from "npm:@ayonli/jsext/archive";
|
||||
|
||||
export interface ScriptFile {
|
||||
parent_hash?: string;
|
||||
|
||||
+3
-2
@@ -31,6 +31,7 @@ services:
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- 8000
|
||||
- 2525
|
||||
environment:
|
||||
- DATABASE_URL=${DATABASE_URL}
|
||||
- MODE=server
|
||||
@@ -154,9 +155,8 @@ services:
|
||||
- 3002
|
||||
|
||||
caddy:
|
||||
image: caddy:2.5.2-alpine
|
||||
image: ghcr.io/windmill-labs/caddy-l4:latest
|
||||
restart: unless-stopped
|
||||
|
||||
# Configure the mounted Caddyfile and the exposed ports or use another reverse proxy if needed
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile
|
||||
@@ -164,6 +164,7 @@ services:
|
||||
ports:
|
||||
# To change the exposed port, simply change 80:80 to <desired_port>:80. No other changes needed
|
||||
- 80:80
|
||||
- 25:25
|
||||
# - 443:443 # Uncomment to enable HTTPS handling by Caddy
|
||||
environment:
|
||||
- BASE_URL=":80"
|
||||
|
||||
@@ -196,11 +196,12 @@
|
||||
replaceScript(event.data)
|
||||
} else if (event.data.type == 'testBundle') {
|
||||
if (event.data.id == lastBundleCommandId) {
|
||||
testBundle(event.data.file)
|
||||
testBundle(event.data.file, event.data.isTar)
|
||||
} else {
|
||||
sendUserToast(`Bundle received ${lastBundleCommandId} was obsolete, ignoring`, true)
|
||||
}
|
||||
} else if (event.data.type == 'testBundleError') {
|
||||
loadingCodebaseButton = false
|
||||
sendUserToast(
|
||||
typeof event.data.error == 'object' ? JSON.stringify(event.data.error) : event.data.error,
|
||||
true
|
||||
@@ -244,7 +245,7 @@
|
||||
window.parent?.postMessage({ type: 'refresh' }, '*')
|
||||
})
|
||||
|
||||
async function testBundle(file: string) {
|
||||
async function testBundle(file: string, isTar: boolean) {
|
||||
testJobLoader?.abstractRun(async () => {
|
||||
try {
|
||||
const form = new FormData()
|
||||
@@ -252,14 +253,25 @@
|
||||
'preview',
|
||||
JSON.stringify({
|
||||
content: currentScript?.content,
|
||||
kind: 'bundle',
|
||||
kind: isTar ? 'tarbundle' : 'bundle',
|
||||
path: currentScript?.path,
|
||||
args,
|
||||
language: currentScript?.language,
|
||||
tag: currentScript?.tag
|
||||
})
|
||||
)
|
||||
form.append('file', file)
|
||||
// sendUserToast(JSON.stringify(file))
|
||||
if (isTar) {
|
||||
var array: number[] = []
|
||||
file = atob(file)
|
||||
for (var i = 0; i < file.length; i++) {
|
||||
array.push(file.charCodeAt(i))
|
||||
}
|
||||
let blob = new Blob([new Uint8Array(array)], { type: 'application/octet-stream' })
|
||||
form.append('file', blob)
|
||||
} else {
|
||||
form.append('file', file)
|
||||
}
|
||||
|
||||
const url = '/api/w/' + workspace + '/jobs/run/preview_bundle'
|
||||
|
||||
|
||||
@@ -96,18 +96,15 @@
|
||||
|
||||
$: render && changeDefaultValue(inputCat, defaultValue)
|
||||
|
||||
$: rawValue && evalRawValueToValue()
|
||||
$: (rawValue || inputCat === 'object') && evalRawValueToValue()
|
||||
|
||||
$: validateInput(pattern, value, required)
|
||||
|
||||
$: {
|
||||
if (inputCat === 'object') {
|
||||
evalValueToRaw()
|
||||
}
|
||||
}
|
||||
|
||||
function evalRawValueToValue() {
|
||||
if (rawValue) {
|
||||
if (!rawValue || rawValue === '') {
|
||||
value = undefined
|
||||
error = ''
|
||||
} else {
|
||||
try {
|
||||
value = JSON.parse(rawValue)
|
||||
error = ''
|
||||
@@ -124,7 +121,7 @@
|
||||
} else {
|
||||
// If value is undefined, set rawValue to empty object
|
||||
// This is to prevent the textarea from being empty
|
||||
rawValue = '{}'
|
||||
rawValue = ''
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -186,6 +186,7 @@
|
||||
label={null}
|
||||
folder={null}
|
||||
concurrencyKey={null}
|
||||
tag={null}
|
||||
success="running"
|
||||
argFilter={undefined}
|
||||
bind:loading
|
||||
|
||||
@@ -27,12 +27,8 @@
|
||||
export let overrideAllowKindChange: boolean = true
|
||||
export let originalType: string | undefined = undefined
|
||||
|
||||
let kind: 'none' | 'pattern' | 'enum' | 'resource' | 'format' | 'base64' = computeKind(
|
||||
enum_,
|
||||
contentEncoding,
|
||||
pattern,
|
||||
format
|
||||
)
|
||||
let kind: 'none' | 'pattern' | 'enum' | 'resource' | 'format' | 'base64' | 'date-time' =
|
||||
computeKind(enum_, contentEncoding, pattern, format)
|
||||
|
||||
const allowKindChange = overrideAllowKindChange || originalType === 'string'
|
||||
|
||||
@@ -55,6 +51,15 @@
|
||||
// 'jsonpointer',
|
||||
]
|
||||
|
||||
const FIELD_SETTINGS = [
|
||||
['None', 'none'],
|
||||
['File', 'base64', 'Encoded as Base 64'],
|
||||
['Enum', 'enum'],
|
||||
['Datetime', 'date-time'],
|
||||
['Format', 'format'],
|
||||
['Pattern', 'pattern']
|
||||
]
|
||||
|
||||
$: format =
|
||||
kind == 'resource' ? (resource != undefined ? `resource-${resource}` : 'resource') : format
|
||||
$: pattern = patternStr == '' ? undefined : patternStr
|
||||
@@ -111,6 +116,9 @@
|
||||
if (e.detail != 'enum') {
|
||||
enum_ = undefined
|
||||
}
|
||||
if (e.detail == 'date-time') {
|
||||
format = 'date-time'
|
||||
}
|
||||
if (e.detail == 'none') {
|
||||
pattern = undefined
|
||||
format = undefined
|
||||
@@ -122,7 +130,7 @@
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#each [['None', 'none'], ['File', 'base64', 'Encoded as Base 64'], ['Enum', 'enum'], ['Format', 'format'], ['Pattern', 'pattern']] as x}
|
||||
{#each FIELD_SETTINGS as x}
|
||||
<ToggleButton value={x[1]} label={x[0]} tooltip={x[2]} showTooltipIcon={Boolean(x[2])} />
|
||||
{/each}
|
||||
</ToggleButtonGroup>
|
||||
|
||||
@@ -8,13 +8,14 @@
|
||||
RichConfigurations
|
||||
} from '../../../types'
|
||||
import { initCss } from '../../../utils'
|
||||
import { getContext } from 'svelte'
|
||||
import { getContext, tick } from 'svelte'
|
||||
import { initConfig, initOutput } from '../../../editor/appUtils'
|
||||
import { components } from '../../../editor/component'
|
||||
import ResolveConfig from '../../helpers/ResolveConfig.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import ResolveStyle from '../../helpers/ResolveStyle.svelte'
|
||||
import type { AgChartOptions, AgChartInstance } from 'ag-charts-community'
|
||||
import DarkModeObserver from '$lib/components/DarkModeObserver.svelte'
|
||||
|
||||
export let id: string
|
||||
export let componentInput: AppInput | undefined
|
||||
@@ -58,6 +59,48 @@
|
||||
let css = initCss($app.css?.agchartscomponent, customCss)
|
||||
let chartInstance: AgChartInstance | undefined = undefined
|
||||
|
||||
function getChartStyleByTheme() {
|
||||
const gridColor = darkMode ? '#555555' : '#dddddd'
|
||||
const axisColor = darkMode ? '#555555' : '#dddddd'
|
||||
const textColor = darkMode ? '#eeeeee' : '#333333'
|
||||
|
||||
return {
|
||||
axes: [
|
||||
{
|
||||
type: 'category',
|
||||
position: 'bottom',
|
||||
label: { color: textColor },
|
||||
line: { color: axisColor },
|
||||
tick: { color: axisColor },
|
||||
gridLine: {
|
||||
style: [
|
||||
{
|
||||
stroke: gridColor
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
position: 'left',
|
||||
label: { color: textColor },
|
||||
line: { color: axisColor },
|
||||
tick: { color: axisColor },
|
||||
gridLine: {
|
||||
style: [
|
||||
{
|
||||
stroke: gridColor
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
background: {
|
||||
visible: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateChart() {
|
||||
if (!chartInstance) {
|
||||
return
|
||||
@@ -111,7 +154,8 @@
|
||||
yName: d.name
|
||||
}
|
||||
}
|
||||
}) as any[]) ?? []
|
||||
}) as any[]) ?? [],
|
||||
...getChartStyleByTheme()
|
||||
}
|
||||
|
||||
outputs.result.set({
|
||||
@@ -220,6 +264,7 @@
|
||||
}
|
||||
const options = {
|
||||
container: document.getElementById(`agchart-${id}`) as HTMLElement,
|
||||
...getChartStyleByTheme(),
|
||||
...result
|
||||
}
|
||||
|
||||
@@ -252,7 +297,8 @@
|
||||
const options: AgChartOptions = {
|
||||
container: document.getElementById(`agchart-${id}`) as HTMLElement,
|
||||
data: [],
|
||||
series: []
|
||||
series: [],
|
||||
...getChartStyleByTheme()
|
||||
}
|
||||
|
||||
chartInstance = AgChartsInstance?.create(options)
|
||||
@@ -274,8 +320,19 @@
|
||||
initChart()
|
||||
})
|
||||
}
|
||||
|
||||
let darkMode = false
|
||||
</script>
|
||||
|
||||
<DarkModeObserver
|
||||
bind:darkMode
|
||||
on:change={(e) => {
|
||||
tick().then(() => {
|
||||
updateChart()
|
||||
})
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if datasets}
|
||||
<ResolveConfig
|
||||
{id}
|
||||
|
||||
@@ -536,6 +536,7 @@
|
||||
}
|
||||
|
||||
let runnableJobEnterTimeout: NodeJS.Timeout | undefined = undefined
|
||||
let stillInJobEnter = false
|
||||
</script>
|
||||
|
||||
<DarkModeObserver on:change={onThemeChange} />
|
||||
@@ -679,9 +680,15 @@
|
||||
class="relative h-full w-full overflow-x-visible"
|
||||
on:mouseenter={() => {
|
||||
runnableJobEnterTimeout && clearTimeout(runnableJobEnterTimeout)
|
||||
$runnableJob.focused = true
|
||||
stillInJobEnter = true
|
||||
runnableJobEnterTimeout = setTimeout(() => {
|
||||
if (stillInJobEnter) {
|
||||
$runnableJob.focused = true
|
||||
}
|
||||
}, 200)
|
||||
}}
|
||||
on:mouseleave={() => {
|
||||
stillInJobEnter = false
|
||||
runnableJobEnterTimeout = setTimeout(
|
||||
() => ($runnableJob.focused = false),
|
||||
200
|
||||
|
||||
@@ -156,6 +156,7 @@
|
||||
{/if}
|
||||
<PanelSection
|
||||
title={`Event handlers`}
|
||||
fullHeight={false}
|
||||
tooltip="Event handlers are used to trigger actions on other components when a specific event occurs. For example, you can trigger a recompute on a component when a script has successfully run."
|
||||
>
|
||||
<EventHandlerItem
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
export let title: string
|
||||
export let noPadding: boolean = false
|
||||
export let fullHeight: boolean = true
|
||||
export let titlePadding: string = ''
|
||||
export let tooltip = ''
|
||||
export let documentationLink: string | undefined = undefined
|
||||
@@ -13,8 +14,9 @@
|
||||
<div
|
||||
class={classNames(
|
||||
$$props.class,
|
||||
'flex flex-col h-full gap-2 items-start',
|
||||
noPadding ? '' : 'p-3'
|
||||
'flex flex-col gap-2 items-start',
|
||||
noPadding ? '' : 'p-3',
|
||||
fullHeight ? 'h-full' : ''
|
||||
)}
|
||||
{id}
|
||||
>
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
import ClipboardPanel from './ClipboardPanel.svelte'
|
||||
import { copyToClipboard, generateRandomString } from '$lib/utils'
|
||||
import HighlightTheme from '../HighlightTheme.svelte'
|
||||
import Alert from '../common/alert/Alert.svelte'
|
||||
import { SettingService } from '$lib/gen'
|
||||
|
||||
let userSettings: UserSettings
|
||||
|
||||
@@ -28,6 +30,8 @@
|
||||
export let hash: string | undefined = undefined
|
||||
export let path: string
|
||||
|
||||
let selectedTab: string = 'rest'
|
||||
|
||||
let webhooks: {
|
||||
async: {
|
||||
hash?: string
|
||||
@@ -40,6 +44,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
let emailDomain: string = "mail." + $page.url.hostname
|
||||
async function getEmailDomain() {
|
||||
emailDomain =
|
||||
((await SettingService.getGlobal({
|
||||
key: 'email_domain'
|
||||
})) as any) ?? ("mail." + $page.url.hostname)
|
||||
}
|
||||
getEmailDomain()
|
||||
|
||||
$: webhooks = isFlow ? computeFlowWebhooks(path) : computeScriptWebhooks(hash, path)
|
||||
|
||||
function computeScriptWebhooks(hash: string | undefined, path: string) {
|
||||
@@ -82,6 +95,10 @@
|
||||
requestType = 'hash'
|
||||
}
|
||||
|
||||
$: if (webhookType === 'sync' && selectedTab === 'email') {
|
||||
webhookType = 'async'
|
||||
}
|
||||
|
||||
$: url =
|
||||
webhooks[webhookType][requestType] +
|
||||
(tokenType === 'query'
|
||||
@@ -108,6 +125,12 @@
|
||||
return headers
|
||||
}
|
||||
|
||||
function emailAddress() {
|
||||
return `${$workspaceStore}+${
|
||||
requestType === 'hash' ? 'hash.' + hash : (isFlow ? 'flow.' : '') + path.replaceAll('/', '.')
|
||||
}+${token}@${emailDomain}`
|
||||
}
|
||||
|
||||
function fetchCode() {
|
||||
if (webhookType === 'sync') {
|
||||
return `
|
||||
@@ -261,6 +284,7 @@ done`
|
||||
label="Sync"
|
||||
value="sync"
|
||||
tooltip="Triggers the execution, wait for the job to complete and return it as a response."
|
||||
disabled={selectedTab === 'email'}
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
@@ -291,22 +315,25 @@ done`
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="text-xs font-semibold flex flex-row items-center">Token configuration</div>
|
||||
<ToggleButtonGroup class="h-[30px] w-auto" bind:selected={tokenType}>
|
||||
<ToggleButton label="Token in Headers" value="headers" />
|
||||
<ToggleButton label="Token in Query" value="query" />
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
{#if selectedTab !== 'email'}
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="text-xs font-semibold flex flex-row items-center">Token configuration</div>
|
||||
<ToggleButtonGroup class="h-[30px] w-auto" bind:selected={tokenType}>
|
||||
<ToggleButton label="Token in Headers" value="headers" />
|
||||
<ToggleButton label="Token in Query" value="query" />
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<Tabs selected="rest">
|
||||
<Tabs bind:selected={selectedTab}>
|
||||
<Tab value="rest" size="xs">REST</Tab>
|
||||
{#if SCRIPT_VIEW_SHOW_EXAMPLE_CURL}
|
||||
<Tab value="curl" size="xs">Curl</Tab>
|
||||
{/if}
|
||||
<Tab value="fetch" size="xs">Fetch</Tab>
|
||||
<Tab value="email" size="xs">Email</Tab>
|
||||
|
||||
<svelte:fragment slot="content">
|
||||
{#key token}
|
||||
@@ -365,6 +392,28 @@ done`
|
||||
{/key}{/key}{/key}{/key}
|
||||
{/key}
|
||||
</TabContent>
|
||||
<TabContent value="email">
|
||||
<div class="flex flex-col gap-4">
|
||||
{#key args}
|
||||
{#key requestType}
|
||||
{#key webhookType}
|
||||
{#key tokenType}
|
||||
{#key token}
|
||||
<div class="flex flex-col gap-2">
|
||||
<ClipboardPanel title="Email address" content={emailAddress()} />
|
||||
</div>
|
||||
{/key}
|
||||
{/key}
|
||||
{/key}
|
||||
{/key}
|
||||
{/key}
|
||||
<Alert title="Email triggers" size="xs">
|
||||
To trigger the job by email, send an email to the address above. The job will receive
|
||||
two arguments: `raw_email` containing the raw email as string, and `parsed_email`
|
||||
containing the parsed email as an object.
|
||||
</Alert>
|
||||
</div>
|
||||
</TabContent>
|
||||
{/key}
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
|
||||
@@ -5,7 +5,7 @@ export function createAppFromScript(path: string, schema: Record<string, any> |
|
||||
'3': {
|
||||
fixed: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
y: 2,
|
||||
w: 2,
|
||||
h: 8,
|
||||
fullHeight: false
|
||||
@@ -13,7 +13,7 @@ export function createAppFromScript(path: string, schema: Record<string, any> |
|
||||
'12': {
|
||||
fixed: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
y: 2,
|
||||
w: 12,
|
||||
h: 21,
|
||||
fullHeight: false
|
||||
@@ -27,6 +27,38 @@ export function createAppFromScript(path: string, schema: Record<string, any> |
|
||||
id: 'a'
|
||||
},
|
||||
id: 'a'
|
||||
},
|
||||
{
|
||||
'3': {
|
||||
fixed: false,
|
||||
x: 0,
|
||||
y: 8,
|
||||
fullHeight: false,
|
||||
w: 6,
|
||||
h: 2
|
||||
},
|
||||
'12': {
|
||||
fixed: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
fullHeight: false,
|
||||
w: 12,
|
||||
h: 2
|
||||
},
|
||||
data: {
|
||||
type: 'containercomponent',
|
||||
configuration: {},
|
||||
customCss: {
|
||||
container: {
|
||||
class: '!p-0',
|
||||
style: ''
|
||||
}
|
||||
},
|
||||
actions: [],
|
||||
numberOfSubgrids: 1,
|
||||
id: 'g'
|
||||
},
|
||||
id: 'g'
|
||||
}
|
||||
],
|
||||
fullscreen: false,
|
||||
@@ -34,6 +66,7 @@ export function createAppFromScript(path: string, schema: Record<string, any> |
|
||||
hiddenInlineScripts: [],
|
||||
css: {},
|
||||
norefreshbar: false,
|
||||
hideLegacyTopBar: true,
|
||||
subgrids: {
|
||||
'a-0': [
|
||||
{
|
||||
@@ -376,6 +409,116 @@ export function createAppFromScript(path: string, schema: Record<string, any> |
|
||||
},
|
||||
id: 'f'
|
||||
}
|
||||
],
|
||||
'g-0': [
|
||||
{
|
||||
'3': {
|
||||
fixed: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
fullHeight: false,
|
||||
w: 6,
|
||||
h: 1
|
||||
},
|
||||
'12': {
|
||||
fixed: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
fullHeight: false,
|
||||
w: 6,
|
||||
h: 1
|
||||
},
|
||||
data: {
|
||||
type: 'textcomponent',
|
||||
configuration: {
|
||||
style: {
|
||||
type: 'static',
|
||||
value: 'Body'
|
||||
},
|
||||
copyButton: {
|
||||
type: 'static',
|
||||
value: false
|
||||
},
|
||||
tooltip: {
|
||||
type: 'evalv2',
|
||||
value: '',
|
||||
fieldType: 'text',
|
||||
expr: '`Author: ${ctx.author}`',
|
||||
connections: [
|
||||
{
|
||||
componentId: 'ctx',
|
||||
id: 'author'
|
||||
}
|
||||
]
|
||||
},
|
||||
disableNoText: {
|
||||
type: 'static',
|
||||
value: true,
|
||||
fieldType: 'boolean'
|
||||
}
|
||||
},
|
||||
componentInput: {
|
||||
type: 'templatev2',
|
||||
fieldType: 'template',
|
||||
eval: '${ctx.summary}',
|
||||
connections: [
|
||||
{
|
||||
id: 'summary',
|
||||
componentId: 'ctx'
|
||||
}
|
||||
]
|
||||
},
|
||||
customCss: {
|
||||
text: {
|
||||
class: 'text-xl font-semibold whitespace-nowrap truncate',
|
||||
style: ''
|
||||
},
|
||||
container: {
|
||||
class: '',
|
||||
style: ''
|
||||
}
|
||||
},
|
||||
actions: [],
|
||||
horizontalAlignment: 'left',
|
||||
verticalAlignment: 'center',
|
||||
id: 'h'
|
||||
},
|
||||
id: 'h'
|
||||
},
|
||||
{
|
||||
'3': {
|
||||
fixed: false,
|
||||
x: 0,
|
||||
y: 1,
|
||||
fullHeight: false,
|
||||
w: 3,
|
||||
h: 1
|
||||
},
|
||||
'12': {
|
||||
fixed: false,
|
||||
x: 6,
|
||||
y: 0,
|
||||
fullHeight: false,
|
||||
w: 6,
|
||||
h: 1
|
||||
},
|
||||
data: {
|
||||
type: 'recomputeallcomponent',
|
||||
configuration: {},
|
||||
customCss: {
|
||||
container: {
|
||||
style: '',
|
||||
class: ''
|
||||
}
|
||||
},
|
||||
actions: [],
|
||||
menuItems: [],
|
||||
horizontalAlignment: 'right',
|
||||
verticalAlignment: 'center',
|
||||
id: 'i'
|
||||
},
|
||||
id: 'i'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -424,7 +567,7 @@ export function createAppFromFlow(path: string, schema: Record<string, any> | un
|
||||
'3': {
|
||||
fixed: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
y: 2,
|
||||
w: 2,
|
||||
h: 8,
|
||||
fullHeight: false
|
||||
@@ -432,7 +575,7 @@ export function createAppFromFlow(path: string, schema: Record<string, any> | un
|
||||
'12': {
|
||||
fixed: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
y: 2,
|
||||
w: 12,
|
||||
h: 21,
|
||||
fullHeight: false
|
||||
@@ -446,6 +589,38 @@ export function createAppFromFlow(path: string, schema: Record<string, any> | un
|
||||
id: 'a'
|
||||
},
|
||||
id: 'a'
|
||||
},
|
||||
{
|
||||
'3': {
|
||||
fixed: false,
|
||||
x: 0,
|
||||
y: 8,
|
||||
fullHeight: false,
|
||||
w: 6,
|
||||
h: 2
|
||||
},
|
||||
'12': {
|
||||
fixed: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
fullHeight: false,
|
||||
w: 12,
|
||||
h: 2
|
||||
},
|
||||
data: {
|
||||
type: 'containercomponent',
|
||||
configuration: {},
|
||||
customCss: {
|
||||
container: {
|
||||
class: '!p-0',
|
||||
style: ''
|
||||
}
|
||||
},
|
||||
actions: [],
|
||||
numberOfSubgrids: 1,
|
||||
id: 'g'
|
||||
},
|
||||
id: 'g'
|
||||
}
|
||||
],
|
||||
fullscreen: false,
|
||||
@@ -453,6 +628,7 @@ export function createAppFromFlow(path: string, schema: Record<string, any> | un
|
||||
hiddenInlineScripts: [],
|
||||
css: {},
|
||||
norefreshbar: false,
|
||||
hideLegacyTopBar: true,
|
||||
subgrids: {
|
||||
'a-0': [
|
||||
{
|
||||
@@ -796,6 +972,116 @@ export function createAppFromFlow(path: string, schema: Record<string, any> | un
|
||||
},
|
||||
id: 'f'
|
||||
}
|
||||
],
|
||||
'g-0': [
|
||||
{
|
||||
'3': {
|
||||
fixed: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
fullHeight: false,
|
||||
w: 6,
|
||||
h: 1
|
||||
},
|
||||
'12': {
|
||||
fixed: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
fullHeight: false,
|
||||
w: 6,
|
||||
h: 1
|
||||
},
|
||||
data: {
|
||||
type: 'textcomponent',
|
||||
configuration: {
|
||||
style: {
|
||||
type: 'static',
|
||||
value: 'Body'
|
||||
},
|
||||
copyButton: {
|
||||
type: 'static',
|
||||
value: false
|
||||
},
|
||||
tooltip: {
|
||||
type: 'evalv2',
|
||||
value: '',
|
||||
fieldType: 'text',
|
||||
expr: '`Author: ${ctx.author}`',
|
||||
connections: [
|
||||
{
|
||||
componentId: 'ctx',
|
||||
id: 'author'
|
||||
}
|
||||
]
|
||||
},
|
||||
disableNoText: {
|
||||
type: 'static',
|
||||
value: true,
|
||||
fieldType: 'boolean'
|
||||
}
|
||||
},
|
||||
componentInput: {
|
||||
type: 'templatev2',
|
||||
fieldType: 'template',
|
||||
eval: '${ctx.summary}',
|
||||
connections: [
|
||||
{
|
||||
id: 'summary',
|
||||
componentId: 'ctx'
|
||||
}
|
||||
]
|
||||
},
|
||||
customCss: {
|
||||
text: {
|
||||
class: 'text-xl font-semibold whitespace-nowrap truncate',
|
||||
style: ''
|
||||
},
|
||||
container: {
|
||||
class: '',
|
||||
style: ''
|
||||
}
|
||||
},
|
||||
actions: [],
|
||||
horizontalAlignment: 'left',
|
||||
verticalAlignment: 'center',
|
||||
id: 'h'
|
||||
},
|
||||
id: 'h'
|
||||
},
|
||||
{
|
||||
'3': {
|
||||
fixed: false,
|
||||
x: 0,
|
||||
y: 1,
|
||||
fullHeight: false,
|
||||
w: 3,
|
||||
h: 1
|
||||
},
|
||||
'12': {
|
||||
fixed: false,
|
||||
x: 6,
|
||||
y: 0,
|
||||
fullHeight: false,
|
||||
w: 6,
|
||||
h: 1
|
||||
},
|
||||
data: {
|
||||
type: 'recomputeallcomponent',
|
||||
configuration: {},
|
||||
customCss: {
|
||||
container: {
|
||||
style: '',
|
||||
class: ''
|
||||
}
|
||||
},
|
||||
actions: [],
|
||||
menuItems: [],
|
||||
horizontalAlignment: 'right',
|
||||
verticalAlignment: 'center',
|
||||
id: 'i'
|
||||
},
|
||||
id: 'i'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
<Section label="Prompt">
|
||||
A prompt is simply an approval step that can be self-approved. To do this, include the
|
||||
resume url in the returned payload of the step. The UX will automatically adapt and show the
|
||||
prompt to the operator when running the flow. e.g:
|
||||
prompt to the operator when running the flow. Additionally, adding the cancel url will also
|
||||
render a cancel button, providing the operator with an option to cancel the step. e.g:
|
||||
<Tabs selected="bun" class="pt-4">
|
||||
<Tab value="bun">TypeScript (Bun)</Tab>
|
||||
<Tab value="deno">TypeScript (Deno)</Tab>
|
||||
@@ -49,6 +50,7 @@ export async function main() {
|
||||
|
||||
return {
|
||||
resume: urls['resume'],
|
||||
cancel: urls['cancel'],
|
||||
default_args: {}, // optional, see below
|
||||
enums: {} // optional, see below
|
||||
}
|
||||
@@ -65,6 +67,7 @@ export async function main() {
|
||||
|
||||
return {
|
||||
resume: urls['resume'],
|
||||
cancel: urls['cancel'],
|
||||
default_args: {}, // optional, see below
|
||||
enums: {} // optional, see below
|
||||
}
|
||||
@@ -80,6 +83,7 @@ def main():
|
||||
urls = wmill.get_resume_urls()
|
||||
return {
|
||||
"resume": urls["resume"],
|
||||
"cancel": urls["cancel"],
|
||||
"default_args": {}, # optional, see below
|
||||
"enums": {} # optional, see below
|
||||
}
|
||||
|
||||
@@ -44,6 +44,15 @@ export const settings: Record<string, Setting[]> = {
|
||||
!value?.endsWith(' ')
|
||||
: false
|
||||
},
|
||||
{
|
||||
label: 'Email domain',
|
||||
description:
|
||||
'Domain to display in webhooks for email triggers, default is the webpage domain prefixed by "mail."',
|
||||
key: 'email_domain',
|
||||
fieldType: 'text',
|
||||
storage: 'setting',
|
||||
placeholder: 'mail.windmill.com'
|
||||
},
|
||||
{
|
||||
label: 'Request Size Limit In MB',
|
||||
description: 'Maximum size of HTTP requests in MB.',
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
export let completedJobs: CompletedJob[] | undefined = undefined
|
||||
export let externalJobs: Job[] | undefined = undefined
|
||||
export let concurrencyKey: string | null
|
||||
export let tag: string | null
|
||||
export let extendedJobs: ExtendedJobs | undefined = undefined
|
||||
export let argError = ''
|
||||
export let resultError = ''
|
||||
@@ -58,6 +59,7 @@
|
||||
isSkipped != undefined &&
|
||||
jobKinds &&
|
||||
concurrencyKey &&
|
||||
tag &&
|
||||
lookback &&
|
||||
user &&
|
||||
folder &&
|
||||
@@ -143,6 +145,7 @@
|
||||
? true
|
||||
: undefined,
|
||||
label: label === null || label === '' ? undefined : label,
|
||||
tag: tag === null || tag === '' ? undefined : tag,
|
||||
isNotSchedule: showSchedules == false ? true : undefined,
|
||||
scheduledForBeforeNow: showFutureJobs == false ? true : undefined,
|
||||
args:
|
||||
@@ -190,6 +193,7 @@
|
||||
isSkipped: isSkipped ? undefined : false,
|
||||
isFlowStep: jobKindsCat != 'all' ? false : undefined,
|
||||
label: label === null || label === '' ? undefined : label,
|
||||
tag: tag === null || tag === '' ? undefined : tag,
|
||||
isNotSchedule: showSchedules == false ? true : undefined,
|
||||
scheduledForBeforeNow: showFutureJobs == false ? true : undefined,
|
||||
args:
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
export let path: string | null = null
|
||||
export let label: string | null = null
|
||||
export let concurrencyKey: string | null = null
|
||||
export let tag: string | null = null
|
||||
export let success: 'running' | 'success' | 'failure' | undefined = undefined
|
||||
export let isSkipped: boolean | undefined = undefined
|
||||
export let argFilter: string
|
||||
@@ -37,11 +38,12 @@
|
||||
|
||||
$: displayedLabel = label
|
||||
$: displayedConcurrencyKey = concurrencyKey
|
||||
$: displayedTag = tag
|
||||
|
||||
let copyArgFilter = argFilter
|
||||
let copyResultFilter = resultFilter
|
||||
|
||||
export let filterBy: 'path' | 'user' | 'folder' | 'label' | 'concurrencyKey' = 'path'
|
||||
export let filterBy: 'path' | 'user' | 'folder' | 'label' | 'concurrencyKey' | 'tag' = 'path'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -63,11 +65,15 @@
|
||||
} else if (concurrencyKey !== null && concurrencyKey !== '' && filterBy !== 'concurrencyKey') {
|
||||
manuallySet = true
|
||||
filterBy = 'concurrencyKey'
|
||||
} else if (tag !== null && tag !== '' && filterBy !== 'tag') {
|
||||
manuallySet = true
|
||||
filterBy = 'tag'
|
||||
}
|
||||
}
|
||||
|
||||
let labelTimeout: NodeJS.Timeout | undefined = undefined
|
||||
let concurrencyKeyTimeout: NodeJS.Timeout | undefined = undefined
|
||||
let tagTimeout: NodeJS.Timeout | undefined = undefined
|
||||
</script>
|
||||
|
||||
<div class="flex gap-4">
|
||||
@@ -94,6 +100,7 @@
|
||||
folder = null
|
||||
label = null
|
||||
concurrencyKey = null
|
||||
tag = null
|
||||
} else {
|
||||
manuallySet = false
|
||||
}
|
||||
@@ -105,7 +112,8 @@
|
||||
<ToggleButtonMore
|
||||
togglableItems={[
|
||||
{ label: 'Concurrency key', value: 'concurrencyKey' },
|
||||
{ label: 'Label', value: 'label' }
|
||||
{ label: 'Label', value: 'label' },
|
||||
{ label: 'Tag', value: 'tag' }
|
||||
]}
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
@@ -295,6 +303,39 @@
|
||||
/>
|
||||
</div>
|
||||
{/key}
|
||||
{:else if filterBy === 'tag'}
|
||||
{#key tag}
|
||||
<div class="relative">
|
||||
{#if tag}
|
||||
<button
|
||||
class="absolute top-2 right-2 z-50"
|
||||
on:click={() => {
|
||||
tag = null
|
||||
dispatch('reset')
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
<span class="text-xs absolute -top-4"> Tag </span>
|
||||
|
||||
<input
|
||||
autofocus
|
||||
type="text"
|
||||
class="!h-[32px] py-1 !text-xs !w-64"
|
||||
bind:value={displayedTag}
|
||||
on:keydown={(e) => {
|
||||
if (tagTimeout) {
|
||||
clearTimeout(tagTimeout)
|
||||
}
|
||||
|
||||
tagTimeout = setTimeout(() => {
|
||||
tag = displayedTag
|
||||
}, 1000)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="relative">
|
||||
@@ -383,6 +424,8 @@
|
||||
user = null
|
||||
folder = null
|
||||
label = null
|
||||
concurrencyKey = null
|
||||
tag = null
|
||||
} else {
|
||||
manuallySet = false
|
||||
}
|
||||
@@ -391,6 +434,9 @@
|
||||
<ToggleButton value="path" label="Path" />
|
||||
<ToggleButton value="user" label="User" />
|
||||
<ToggleButton value="folder" label="Folder" />
|
||||
<ToggleButton value="concurrencyKey" label="Concurrency" />
|
||||
<ToggleButton value="tag" label="Tag" />
|
||||
<ToggleButton value="label" label="Label" />
|
||||
</ToggleButtonGroup>
|
||||
</Label>
|
||||
|
||||
@@ -415,10 +461,10 @@
|
||||
items={usernames}
|
||||
value={user}
|
||||
bind:selectedItem={user}
|
||||
inputClassName="!h-[32px] py-1 !text-xs !w-64"
|
||||
inputClassName="!h-[32px] py-1 !text-xs !w-80"
|
||||
hideArrow
|
||||
className={user ? '!font-bold' : ''}
|
||||
dropdownClassName="!font-normal !w-64 !max-w-64"
|
||||
dropdownClassName="!font-normal !w-80 !max-w-80"
|
||||
/>
|
||||
</div>
|
||||
</Label>
|
||||
@@ -445,10 +491,10 @@
|
||||
items={folders}
|
||||
value={folder}
|
||||
bind:selectedItem={folder}
|
||||
inputClassName="!h-[32px] py-1 !text-xs !w-64"
|
||||
inputClassName="!h-[32px] py-1 !text-xs !w-80"
|
||||
hideArrow
|
||||
className={folder ? '!font-bold' : ''}
|
||||
dropdownClassName="!font-normal !w-64 !max-w-64"
|
||||
dropdownClassName="!font-normal !w-80 !max-w-80"
|
||||
/>
|
||||
</div>
|
||||
</Label>
|
||||
@@ -483,6 +529,107 @@
|
||||
</div>
|
||||
</Label>
|
||||
{/key}
|
||||
{:else if filterBy === 'tag'}
|
||||
{#key tag}
|
||||
<Label label="Tag">
|
||||
<div class="relative w-full">
|
||||
{#if tag}
|
||||
<button
|
||||
class="absolute top-2 right-2 z-50"
|
||||
on:click={() => {
|
||||
tag = null
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<input
|
||||
autofocus
|
||||
type="text"
|
||||
class="!h-[32px] py-1 !text-xs !w-80"
|
||||
bind:value={displayedTag}
|
||||
on:keydown={(e) => {
|
||||
if (tagTimeout) {
|
||||
clearTimeout(tagTimeout)
|
||||
}
|
||||
|
||||
tagTimeout = setTimeout(() => {
|
||||
tag = displayedTag
|
||||
console.log(tag)
|
||||
}, 1000)
|
||||
}}
|
||||
/>
|
||||
</div></Label
|
||||
>
|
||||
{/key}
|
||||
{:else if filterBy === 'label'}
|
||||
{#key label}
|
||||
<Label label="Label">
|
||||
<div class="relative w-full">
|
||||
{#if label}
|
||||
<button
|
||||
class="absolute top-2 right-2 z-50"
|
||||
on:click={() => {
|
||||
label = null
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<input
|
||||
autofocus
|
||||
type="text"
|
||||
class="!h-[32px] py-1 !text-xs !w-80"
|
||||
bind:value={displayedLabel}
|
||||
on:keydown={(e) => {
|
||||
if (labelTimeout) {
|
||||
clearTimeout(labelTimeout)
|
||||
}
|
||||
|
||||
labelTimeout = setTimeout(() => {
|
||||
label = displayedLabel
|
||||
}, 1000)
|
||||
}}
|
||||
/>
|
||||
</div></Label
|
||||
>
|
||||
{/key}
|
||||
{:else if filterBy === 'concurrencyKey'}
|
||||
{#key concurrencyKey}
|
||||
<Label label="Concurrency Key">
|
||||
<div class="relative w-full">
|
||||
{#if concurrencyKey}
|
||||
<button
|
||||
class="absolute top-2 right-2 z-50"
|
||||
on:click={() => {
|
||||
concurrencyKey = null
|
||||
// dispatch('reset')
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<input
|
||||
autofocus
|
||||
type="text"
|
||||
class="!h-[32px] py-1 !text-xs !w-80"
|
||||
bind:value={displayedConcurrencyKey}
|
||||
on:keydown={(e) => {
|
||||
if (concurrencyKeyTimeout) {
|
||||
clearTimeout(concurrencyKeyTimeout)
|
||||
}
|
||||
|
||||
concurrencyKeyTimeout = setTimeout(() => {
|
||||
concurrencyKey = displayedConcurrencyKey
|
||||
}, 1000)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Label>
|
||||
{/key}
|
||||
{/if}
|
||||
|
||||
<Label label="Kind">
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import { clickOutside, displayDateOnly, isMac, sendUserToast } from '$lib/utils'
|
||||
import TimeAgo from '../TimeAgo.svelte'
|
||||
import {
|
||||
AlertTriangle,
|
||||
BoxesIcon,
|
||||
CalendarIcon,
|
||||
Code2Icon,
|
||||
@@ -36,6 +37,7 @@
|
||||
import BarsStaggered from '../icons/BarsStaggered.svelte'
|
||||
import { scroll_into_view_if_needed_polyfill } from '../multiselect/utils'
|
||||
import { Alert } from '../common'
|
||||
import Popover from '../Popover.svelte'
|
||||
|
||||
let open: boolean = false
|
||||
|
||||
@@ -176,7 +178,11 @@
|
||||
let debounceTimeout: any = undefined
|
||||
const debouncePeriod: number = 1000
|
||||
let loadingCompletedRuns: boolean = false
|
||||
let queryParseErrors: string[] = []
|
||||
|
||||
async function handleSearch() {
|
||||
queryParseErrors = []
|
||||
|
||||
if (
|
||||
tab !== 'default' &&
|
||||
(searchTerm === '' ||
|
||||
@@ -231,6 +237,7 @@
|
||||
workspace: $workspaceStore!
|
||||
})
|
||||
itemMap['runs'] = searchResults.hits
|
||||
queryParseErrors = searchResults.query_parse_errors
|
||||
} catch (e) {
|
||||
sendUserToast(e, true)
|
||||
}
|
||||
@@ -325,12 +332,20 @@
|
||||
goto(path)
|
||||
}
|
||||
|
||||
let mouseMoved: boolean = false
|
||||
function handleMouseMove () {
|
||||
mouseMoved = true
|
||||
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('keydown', handleKeydown)
|
||||
window.addEventListener('mousemove', handleMouseMove)
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('keydown', handleKeydown)
|
||||
window.removeEventListener('mousemove', handleMouseMove)
|
||||
})
|
||||
|
||||
$: searchTerm, handleSearch()
|
||||
@@ -512,12 +527,25 @@
|
||||
>{placeholderFromPrefix(searchTerm)}</label
|
||||
>
|
||||
</div>
|
||||
{#if 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>
|
||||
<div class="overflow-y-auto relative {maxModalHeight(tab)}">
|
||||
{#if tab === 'default' || tab === 'switch-mode'}
|
||||
{@const items = (itemMap[tab] ?? []).filter((e) => defaultMenuItems.includes(e))}
|
||||
{#if items.length > 0}
|
||||
<div class="p-2 border-b">
|
||||
<div class={tab === 'switch-mode' ? "p-2" : "p-2 border-b"}>
|
||||
{#each items as el}
|
||||
<QuickMenuItem
|
||||
on:select={el?.action}
|
||||
@@ -527,6 +555,7 @@
|
||||
label={el?.label}
|
||||
icon={el?.icon}
|
||||
shortcutKey={el?.shortcutKey}
|
||||
bind:mouseMoved
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -549,6 +578,7 @@
|
||||
el.path +
|
||||
(el.starred ? ' ★' : '')}
|
||||
icon={iconForWindmillItem(el.type)}
|
||||
bind:mouseMoved
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
@@ -600,6 +630,7 @@
|
||||
hovered={selectedItem && r?.document.id[0] === selectedItem?.document.id[0]}
|
||||
icon={r?.icon}
|
||||
containerClass="rounded-md px-2 py-1 my-2"
|
||||
bind:mouseMoved
|
||||
>
|
||||
<svelte:fragment slot="itemReplacement">
|
||||
<div
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
export let icon: any = undefined
|
||||
export let shortcutKey: string | undefined = undefined
|
||||
export let containerClass: string | undefined = undefined
|
||||
export let mouseMoved = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -49,7 +50,12 @@
|
||||
<div
|
||||
{id}
|
||||
on:click|stopPropagation={runAction}
|
||||
on:mouseenter={() => dispatch('hover')}
|
||||
on:mouseenter={() => {
|
||||
if (mouseMoved) {
|
||||
dispatch('hover')
|
||||
}
|
||||
mouseMoved=false
|
||||
}}
|
||||
class={twMerge(
|
||||
`rounded-md w-full transition-all cursor-pointer ${
|
||||
hovered ? 'bg-surface-hover' : ''
|
||||
|
||||
@@ -495,7 +495,7 @@ export function isObject(obj: any) {
|
||||
|
||||
export function debounce(func: (...args: any[]) => any, wait: number) {
|
||||
let timeout: any
|
||||
return function(...args: any[]) {
|
||||
return function (...args: any[]) {
|
||||
// @ts-ignore
|
||||
const context = this
|
||||
clearTimeout(timeout)
|
||||
@@ -505,7 +505,7 @@ export function debounce(func: (...args: any[]) => any, wait: number) {
|
||||
|
||||
export function throttle<T>(func: (...args: any[]) => T, wait: number) {
|
||||
let timeout: any
|
||||
return function(...args: any[]) {
|
||||
return function (...args: any[]) {
|
||||
if (!timeout) {
|
||||
timeout = setTimeout(() => {
|
||||
timeout = null
|
||||
@@ -721,7 +721,7 @@ export async function tryEvery({
|
||||
try {
|
||||
await tryCode()
|
||||
break
|
||||
} catch (err) { }
|
||||
} catch (err) {}
|
||||
i++
|
||||
}
|
||||
if (i >= times) {
|
||||
@@ -883,7 +883,7 @@ export function computeKind(
|
||||
contentEncoding: 'base64' | 'binary' | undefined,
|
||||
pattern: string | undefined,
|
||||
format: string | undefined
|
||||
): 'base64' | 'none' | 'pattern' | 'enum' | 'resource' | 'format' {
|
||||
): 'base64' | 'none' | 'pattern' | 'enum' | 'resource' | 'format' | 'date-time' {
|
||||
if (enum_ != undefined) {
|
||||
return 'enum'
|
||||
}
|
||||
@@ -893,6 +893,9 @@ export function computeKind(
|
||||
if (pattern != undefined) {
|
||||
return 'pattern'
|
||||
}
|
||||
if (format == 'date-time') {
|
||||
return 'date-time'
|
||||
}
|
||||
if (format != undefined && format != '') {
|
||||
if (format?.startsWith('resource')) {
|
||||
return 'resource'
|
||||
@@ -950,10 +953,7 @@ export function isDeployable(
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
deployUiSettings.include_type != undefined &&
|
||||
!deployUiSettings.include_type.includes(type)
|
||||
) {
|
||||
if (deployUiSettings.include_type != undefined && !deployUiSettings.include_type.includes(type)) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -972,4 +972,3 @@ export const ALL_DEPLOYABLE: WorkspaceDeployUISettings = {
|
||||
include_path: [],
|
||||
include_type: ['script', 'flow', 'app', 'resource', 'variable', 'secret']
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
let folder: string | null = $page.url.searchParams.get('folder')
|
||||
let label: string | null = $page.url.searchParams.get('label')
|
||||
let concurrencyKey: string | null = $page.url.searchParams.get('concurrency_key')
|
||||
let tag: string | null = $page.url.searchParams.get('tag')
|
||||
// Rest of filters handled by RunsFilter
|
||||
let success: 'running' | 'success' | 'failure' | undefined = ($page.url.searchParams.get(
|
||||
'success'
|
||||
@@ -125,6 +126,7 @@
|
||||
schedulePath ||
|
||||
jobKindsCat ||
|
||||
concurrencyKey ||
|
||||
tag ||
|
||||
graph ||
|
||||
minTs ||
|
||||
maxTs ||
|
||||
@@ -218,6 +220,12 @@
|
||||
searchParams.delete('concurrency_key')
|
||||
}
|
||||
|
||||
if (tag) {
|
||||
searchParams.set('tag', tag)
|
||||
} else {
|
||||
searchParams.delete('tag')
|
||||
}
|
||||
|
||||
if (label) {
|
||||
searchParams.set('label', label)
|
||||
} else {
|
||||
@@ -287,6 +295,7 @@
|
||||
folder = null
|
||||
label = null
|
||||
concurrencyKey = null
|
||||
tag = null
|
||||
}
|
||||
|
||||
function filterByUser(e: CustomEvent<string>) {
|
||||
@@ -295,6 +304,7 @@
|
||||
user = e.detail
|
||||
label = null
|
||||
concurrencyKey = null
|
||||
tag = null
|
||||
}
|
||||
|
||||
function filterByFolder(e: CustomEvent<string>) {
|
||||
@@ -303,6 +313,7 @@
|
||||
folder = e.detail
|
||||
label = null
|
||||
concurrencyKey = null
|
||||
tag = null
|
||||
}
|
||||
|
||||
function filterByLabel(e: CustomEvent<string>) {
|
||||
@@ -311,6 +322,7 @@
|
||||
folder = null
|
||||
label = e.detail
|
||||
concurrencyKey = null
|
||||
tag = null
|
||||
}
|
||||
|
||||
function filterByConcurrencyKey(e: CustomEvent<string>) {
|
||||
@@ -319,6 +331,16 @@
|
||||
folder = null
|
||||
label = null
|
||||
concurrencyKey = e.detail
|
||||
tag = null
|
||||
}
|
||||
|
||||
function filterByTag(e: CustomEvent<string>) {
|
||||
path = null
|
||||
user = null
|
||||
folder = null
|
||||
label = null
|
||||
concurrencyKey = null
|
||||
tag = e.detail
|
||||
}
|
||||
|
||||
let calendarChangeTimeout: NodeJS.Timeout | undefined = undefined
|
||||
@@ -369,7 +391,8 @@
|
||||
? resultFilter
|
||||
: undefined,
|
||||
allWorkspaces: allWorkspaces ? true : undefined,
|
||||
concurrencyKey: concurrencyKey ?? undefined
|
||||
concurrencyKey: concurrencyKey ?? undefined,
|
||||
tag: tag ?? undefined
|
||||
}
|
||||
|
||||
selectedFiltersString = JSON.stringify(selectedFilters, null, 4)
|
||||
@@ -395,7 +418,7 @@
|
||||
}
|
||||
|
||||
const warnJobLimitMsg =
|
||||
'The exact number of concurrent job at the beginning of the time range may be incorrect as only the last 1000 jobs are taken into account: a job that was started earlier than this limit will not be taken into account'
|
||||
'The exact number of concurrent jobs at the beginning of the time range may be incorrect as only the last 1000 jobs are taken into account: a job that was started earlier than this limit will not be taken into account'
|
||||
|
||||
$: warnJobLimit =
|
||||
graph === 'ConcurrencyChart' &&
|
||||
@@ -430,6 +453,7 @@
|
||||
{concurrencyKey}
|
||||
{argError}
|
||||
{resultError}
|
||||
{tag}
|
||||
bind:loading
|
||||
bind:this={jobLoader}
|
||||
lookback={graphIsRunsChart ? 0 : lookback}
|
||||
@@ -449,6 +473,7 @@
|
||||
selectedIds = []
|
||||
jobLoader?.loadJobs(minTs, maxTs, true, true)
|
||||
sendUserToast(`Canceled ${uuids.length} jobs`)
|
||||
isSelectingJobsToCancel = false
|
||||
}}
|
||||
loading={fetchingFilteredJobs}
|
||||
on:canceled={() => {
|
||||
@@ -472,6 +497,7 @@
|
||||
selectedIds = []
|
||||
jobLoader?.loadJobs(minTs, maxTs, true, true)
|
||||
sendUserToast(`Canceled ${uuids.length} jobs`)
|
||||
isSelectingJobsToCancel = false
|
||||
}}
|
||||
on:canceled={() => {
|
||||
isCancelingVisibleJobs = false
|
||||
@@ -521,6 +547,7 @@
|
||||
bind:folder
|
||||
bind:label
|
||||
bind:concurrencyKey
|
||||
bind:tag
|
||||
bind:path
|
||||
bind:success
|
||||
bind:argFilter
|
||||
@@ -815,6 +842,7 @@
|
||||
on:filterByFolder={filterByFolder}
|
||||
on:filterByLabel={filterByLabel}
|
||||
on:filterByConcurrencyKey={filterByConcurrencyKey}
|
||||
on:filterByTag={filterByTag}
|
||||
/>
|
||||
{:else}
|
||||
<div class="gap-1 flex flex-col">
|
||||
@@ -872,6 +900,9 @@
|
||||
bind:folder
|
||||
bind:path
|
||||
bind:user
|
||||
bind:label
|
||||
bind:concurrencyKey
|
||||
bind:tag
|
||||
bind:success
|
||||
bind:argFilter
|
||||
bind:resultFilter
|
||||
@@ -1158,6 +1189,7 @@
|
||||
on:filterByFolder={filterByFolder}
|
||||
on:filterByLabel={filterByLabel}
|
||||
on:filterByConcurrencyKey={filterByConcurrencyKey}
|
||||
on:filterByTag={filterByTag}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+3
-3
@@ -9,13 +9,13 @@ RUN set -eux; \
|
||||
url=; \
|
||||
case "$arch" in \
|
||||
'amd64') \
|
||||
targz='go1.21.0.linux-amd64.tar.gz'; \
|
||||
targz='go1.22.5.linux-amd64.tar.gz'; \
|
||||
;; \
|
||||
'arm64') \
|
||||
targz='go1.21.0.linux-arm64.tar.gz'; \
|
||||
targz='go1.22.5.linux-arm64.tar.gz'; \
|
||||
;; \
|
||||
'armhf') \
|
||||
targz='go1.21.0.linux-armv6l.tar.gz'; \
|
||||
targz='go1.22.5.linux-armv6l.tar.gz'; \
|
||||
;; \
|
||||
*) echo >&2 "error: unsupported architecture '$arch' (likely packaging update needed)"; exit 1 ;; \
|
||||
esac; \
|
||||
|
||||
Reference in New Issue
Block a user