Merge branch 'main' into fr/xyflow

This commit is contained in:
Faton Ramadani
2024-08-06 16:48:10 +02:00
38 changed files with 963 additions and 121 deletions
+10
View File
@@ -1,3 +1,13 @@
{
layer4 {
:25 {
proxy {
to windmill_server:2525
}
}
}
}
{$BASE_URL} {
bind {$ADDRESS}
reverse_proxy /ws/* http://lsp:3001
+4 -4
View File
@@ -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"]
+14
View File
@@ -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",
+3
View File
@@ -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
View File
@@ -1 +1 @@
1a2febc371c907789b25860366872fca888d6477
70e475a5cec356d2fe992038f303aea8988b5046
+1
View File
@@ -405,6 +405,7 @@ Windmill Community Edition {GIT_VERSION}
server_killpill_rx,
base_internal_tx,
server_mode,
base_internal_url.clone(),
)
.await?;
} else {
+1
View File
@@ -128,6 +128,7 @@ impl ApiServer {
rx,
port_tx,
false,
format!("http://localhost:{}", addr.port()),
));
_port_rx.await.unwrap();
+4
View File
@@ -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
+117 -24
View File
@@ -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,
+17 -3
View File
@@ -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 = {
+5 -1
View File
@@ -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",
+1
View File
@@ -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 {
+3 -4
View File
@@ -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 {
+1 -1
View File
@@ -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!(
+46 -19
View File
@@ -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
View File
@@ -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
View File
@@ -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"
+16 -4
View File
@@ -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' : ''
+8 -9
View File
@@ -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
View File
@@ -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; \