diff --git a/backend/.sqlx/query-2a3992b5e9abcfbb032d10e142d98efa969dae26a7242eb7ac12593ed5421ef3.json b/backend/.sqlx/query-56264b88a9f428e79c6e531a0011e0dbed634f3b12dedaa5fee0362d670887f6.json similarity index 62% rename from backend/.sqlx/query-2a3992b5e9abcfbb032d10e142d98efa969dae26a7242eb7ac12593ed5421ef3.json rename to backend/.sqlx/query-56264b88a9f428e79c6e531a0011e0dbed634f3b12dedaa5fee0362d670887f6.json index 326e45bf24..e5e1ecd5de 100644 --- a/backend/.sqlx/query-2a3992b5e9abcfbb032d10e142d98efa969dae26a7242eb7ac12593ed5421ef3.json +++ b/backend/.sqlx/query-56264b88a9f428e79c6e531a0011e0dbed634f3b12dedaa5fee0362d670887f6.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO app_version\n (app_id, value, created_by)\n VALUES ($1, $2::text::json, $3) RETURNING id", + "query": "INSERT INTO app_version\n (app_id, value, created_by, raw_app)\n VALUES ($1, $2::text::json, $3, $4) RETURNING id", "describe": { "columns": [ { @@ -13,12 +13,13 @@ "Left": [ "Int8", "Text", - "Varchar" + "Varchar", + "Bool" ] }, "nullable": [ false ] }, - "hash": "2a3992b5e9abcfbb032d10e142d98efa969dae26a7242eb7ac12593ed5421ef3" + "hash": "56264b88a9f428e79c6e531a0011e0dbed634f3b12dedaa5fee0362d670887f6" } diff --git a/backend/.sqlx/query-83d18f4e1cda2c1867168551d855d5626e766b5469c6976c986bdafc8b9407c5.json b/backend/.sqlx/query-a9f6e5720f748418ae52cf0e31778b9bbef26008803f2dd2a043bdb091b2ff20.json similarity index 63% rename from backend/.sqlx/query-83d18f4e1cda2c1867168551d855d5626e766b5469c6976c986bdafc8b9407c5.json rename to backend/.sqlx/query-a9f6e5720f748418ae52cf0e31778b9bbef26008803f2dd2a043bdb091b2ff20.json index 24cabff562..0310f2197b 100644 --- a/backend/.sqlx/query-83d18f4e1cda2c1867168551d855d5626e766b5469c6976c986bdafc8b9407c5.json +++ b/backend/.sqlx/query-a9f6e5720f748418ae52cf0e31778b9bbef26008803f2dd2a043bdb091b2ff20.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO app_version\n (app_id, value, created_by)\n VALUES ($1, $2::text::json, $3) RETURNING id", + "query": "INSERT INTO app_version\n (app_id, value, created_by, raw_app)\n VALUES ($1, $2::text::json, $3, $4) RETURNING id", "describe": { "columns": [ { @@ -13,12 +13,13 @@ "Left": [ "Int8", "Text", - "Varchar" + "Varchar", + "Bool" ] }, "nullable": [ false ] }, - "hash": "83d18f4e1cda2c1867168551d855d5626e766b5469c6976c986bdafc8b9407c5" + "hash": "a9f6e5720f748418ae52cf0e31778b9bbef26008803f2dd2a043bdb091b2ff20" } diff --git a/backend/migrations/20241223155748_raw_apps_v2.down.sql b/backend/migrations/20241223155748_raw_apps_v2.down.sql new file mode 100644 index 0000000000..7e7dbecf84 --- /dev/null +++ b/backend/migrations/20241223155748_raw_apps_v2.down.sql @@ -0,0 +1,2 @@ +-- Add down migration script here +ALTER TABLE app_version DROP COLUMN IF EXISTS raw_app; \ No newline at end of file diff --git a/backend/migrations/20241223155748_raw_apps_v2.up.sql b/backend/migrations/20241223155748_raw_apps_v2.up.sql new file mode 100644 index 0000000000..02b1124957 --- /dev/null +++ b/backend/migrations/20241223155748_raw_apps_v2.up.sql @@ -0,0 +1,2 @@ +-- Add up migration script here +ALTER TABLE app_version ADD COLUMN IF NOT EXISTS raw_app BOOLEAN NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/backend/parsers/windmill-parser-ts/src/lib.rs b/backend/parsers/windmill-parser-ts/src/lib.rs index 24c9eefab3..d4d34cecd6 100644 --- a/backend/parsers/windmill-parser-ts/src/lib.rs +++ b/backend/parsers/windmill-parser-ts/src/lib.rs @@ -49,8 +49,12 @@ impl Visit for ImportsFinder { pub fn parse_expr_for_imports(code: &str) -> anyhow::Result> { let cm: Lrc = Default::default(); let fm = cm.new_source_file(FileName::Custom("main.d.ts".into()).into(), code.into()); + let mut tss = TsSyntax::default(); + tss.disallow_ambiguous_jsx_like; + tss.tsx = true; + tss.no_early_errors = true; let lexer = Lexer::new( - Syntax::Typescript(TsSyntax::default()), + Syntax::Typescript(tss), // EsVersion defaults to es5 Default::default(), StringInput::from(&*fm), diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 4ad0ecc65d..329cd80968 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -5985,6 +5985,55 @@ paths: schema: type: string + /w/{workspace}/apps/create_raw: + post: + summary: create app raw + operationId: createAppRaw + tags: + - app + parameters: + - $ref: "#/components/parameters/WorkspaceId" + requestBody: + description: new app + required: true + content: + multipart/form-data: + schema: + type: object + properties: + app: + type: object + properties: + path: + type: string + value: {} + summary: + type: string + policy: + $ref: "#/components/schemas/Policy" + draft_only: + type: boolean + deployment_message: + type: string + custom_path: + type: string + required: + - path + - value + - summary + - policy + js: + type: string + css: + type: string + responses: + "201": + description: app created + content: + text/plain: + schema: + type: string + /w/{workspace}/apps/exists/{path}: get: summary: does an app exisst at path @@ -6344,6 +6393,49 @@ paths: schema: type: string + /w/{workspace}/apps/update_raw/{path}: + post: + summary: update app + operationId: updateAppRaw + tags: + - app + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/ScriptPath" + requestBody: + description: update app + required: true + content: + multipart/form-data: + schema: + type: object + properties: + app: + type: object + properties: + path: + type: string + summary: + type: string + value: {} + policy: + $ref: "#/components/schemas/Policy" + deployment_message: + type: string + custom_path: + type: string + js: + type: string + css: + type: string + responses: + "200": + description: app updated + content: + text/plain: + schema: + type: string + /w/{workspace}/apps/custom_path_exists/{custom_path}: get: summary: check if custom path exists @@ -15921,6 +16013,8 @@ components: execution_mode: type: string enum: [viewer, publisher, anonymous] + raw_app: + type: boolean required: - id - workspace_id diff --git a/backend/windmill-api/src/apps.rs b/backend/windmill-api/src/apps.rs index b3ffdb896b..533da11d1c 100644 --- a/backend/windmill-api/src/apps.rs +++ b/backend/windmill-api/src/apps.rs @@ -24,10 +24,10 @@ use crate::{ }, users::fetch_api_authed_from_permissioned_as, }; -#[cfg(feature = "parquet")] use axum::response::Response; use axum::{ - extract::{Extension, Json, Path, Query}, + body::Body, + extract::{Extension, Json, Multipart, Path, Query}, response::IntoResponse, routing::{delete, get, post}, Router, @@ -88,10 +88,13 @@ pub fn workspaced_service() -> Router { .route("/get/draft/*path", get(get_app_w_draft)) .route("/secret_of/*path", get(get_secret_id)) .route("/get/v/*id", get(get_app_by_id)) + .route("/get_data/v/*id", get(get_raw_app_data)) .route("/exists/*path", get(exists_app)) .route("/update/*path", post(update_app)) + .route("/update_raw/*path", post(update_app_raw)) .route("/delete/*path", delete(delete_app)) .route("/create", post(create_app)) + .route("/create_raw", post(create_app_raw)) .route("/history/p/*path", get(get_app_history)) .route("/get_latest_version/*path", get(get_latest_version)) .route("/history_update/a/:id/v/:version", post(update_app_history)) @@ -135,6 +138,12 @@ pub struct ListableApp { #[sqlx(default)] #[serde(skip_serializing_if = "Option::is_none")] pub deployment_msg: Option, + #[serde(skip_serializing_if = "is_false")] + pub raw_app: bool, +} + +fn is_false(b: &bool) -> bool { + !b } #[derive(FromRow, Serialize, Deserialize)] @@ -328,7 +337,8 @@ async fn list_apps( "app.extra_perms", "favorite.path IS NOT NULL as starred", "draft.path IS NOT NULL as has_draft", - "draft_only" + "draft_only", + "app_version.raw_app", ]) .left() .join("favorite") @@ -387,6 +397,44 @@ async fn list_apps( Ok(Json(rows)) } +async fn get_raw_app_data(Path((w_id, version_id)): Path<(String, String)>) -> Result { + let file_path = format!("/tmp/wmill/{}/{}", w_id, version_id); + let file = tokio::fs::File::open(file_path).await?; + let stream = tokio_util::io::ReaderStream::new(file); + let res = Response::builder().header( + http::header::CONTENT_TYPE, + if version_id.ends_with(".css") { + "text/css" + } else { + "text/javascript" + }, + ); + Ok(res.body(Body::from_stream(stream)).unwrap()) +} + +// async fn get_app_version( +// authed: ApiAuthed, +// Extension(user_db): Extension, +// Path((w_id, path)): Path<(String, StripPath)>, +// ) -> JsonResult { +// let path = path.to_path(); +// let mut tx = user_db.begin(&authed).await?; + +// let version_o = sqlx::query_scalar!( +// "SELECT app.versions[array_upper(app.versions, 1)] as version FROM app +// WHERE app.path = $1 AND app.workspace_id = $2", +// path, +// &w_id, +// ) +// .fetch_optional(&mut *tx) +// .await? +// .flatten(); +// tx.commit().await?; + +// let version = not_found_if_none(version_o, "App", path)?; +// Ok(Json(version)) +// } + async fn get_app( authed: ApiAuthed, Extension(user_db): Extension, @@ -727,6 +775,97 @@ async fn get_secret_id( Ok(hx) } +macro_rules! process_app_multipart { + ($authed:expr, $user_db:expr, $db:expr, $w_id:expr, $path:expr, $multipart:expr, $internal_fn:expr) => { + async { + let mut saved_app = None; + let mut uploaded_js = false; + + //todo: use s3 instead + let file_path = format!("/tmp/wmill/{}", $w_id); + std::fs::create_dir_all(&file_path).unwrap(); + + let mut multipart = $multipart; + while let Some(field) = multipart.next_field().await.unwrap() { + let name = field.name().unwrap().to_string(); + let data = field.bytes().await.unwrap(); + if name == "app" { + let app = serde_json::from_slice(&data).map_err(to_anyhow)?; + let (ntx, npath, nid) = $internal_fn( + $authed.clone(), + $db.clone(), + $user_db.clone(), + $w_id, + $path, + true, + app, + ) + .await?; + saved_app = Some((npath, nid, ntx)); + } else if name == "js" { + if let Some((_npath, id, _tx)) = saved_app.as_ref() { + let file_path = format!("{}/{}.js", file_path, id); + std::fs::write(file_path, data).unwrap(); + uploaded_js = true; + } else { + return Err(Error::BadRequest( + "App payload need to be created first".to_string(), + )); + } + } else if name == "css" { + if let Some((_npath, id, _tx)) = saved_app.as_ref() { + let file_path = format!("{}/{}.css", file_path, id); + std::fs::write(file_path, data).unwrap(); + } else { + return Err(Error::BadRequest( + "App payload need to be created first".to_string(), + )); + } + } else { + return Err(Error::BadRequest(format!("Unsupported field: {}", name))); + } + } + if !uploaded_js { + return Err(Error::BadRequest("js or css file not uploaded".to_string())); + } + if let Some((npath, id, tx)) = saved_app { + tx.commit().await?; + Ok((npath, id)) + } else { + Err(Error::BadRequest("App not created".to_string())) + } + } + }; +} + +async fn create_app_raw<'a>( + authed: ApiAuthed, + Extension(user_db): Extension, + Extension(db): Extension, + Extension(webhook): Extension, + Path(w_id): Path, + multipart: Multipart, +) -> Result<(StatusCode, String)> { + let (path, _id) = process_app_multipart!( + authed, + user_db, + db, + &w_id, + "", + multipart, + |authed, db, user_db, w_id, _path, raw_app, app| create_app_internal( + authed, db, user_db, w_id, raw_app, app + ) + ) + .await?; + + webhook.send_message( + w_id.clone(), + WebhookMessage::CreateApp { workspace: w_id, path: path.clone() }, + ); + Ok((StatusCode::CREATED, path)) +} + async fn list_paths_from_workspace_runnable( authed: ApiAuthed, Extension(user_db): Extension, @@ -755,17 +894,36 @@ async fn create_app( Extension(db): Extension, Extension(webhook): Extension, Path(w_id): Path, - Json(mut app): Json, + Json(app): Json, ) -> Result<(StatusCode, String)> { - let mut tx = user_db.clone().begin(&authed).await?; + let path = app.path.clone(); + let (new_tx, _path, _id) = create_app_internal(authed, db, user_db, &w_id, false, app).await?; + new_tx.commit().await?; + + webhook.send_message( + w_id.clone(), + WebhookMessage::CreateApp { workspace: w_id, path: path.clone() }, + ); + + Ok((StatusCode::CREATED, path)) +} + +async fn create_app_internal<'a>( + authed: ApiAuthed, + db: sqlx::Pool, + user_db: UserDB, + w_id: &String, + raw_app: bool, + mut app: CreateApp, +) -> Result<(sqlx::Transaction<'a, sqlx::Postgres>, String, i64)> { + let mut tx = user_db.clone().begin(&authed).await?; app.policy.on_behalf_of = Some(username_to_permissioned_as(&authed.username)); app.policy.on_behalf_of_email = Some(authed.email.clone()); - + let path = app.path.clone(); if &app.path == "" { return Err(Error::BadRequest("App path cannot be empty".to_string())); } - let exists = sqlx::query_scalar!( "SELECT EXISTS(SELECT 1 FROM app WHERE path = $1 AND workspace_id = $2)", &app.path, @@ -774,21 +932,19 @@ async fn create_app( .fetch_one(&mut *tx) .await? .unwrap_or(false); - if exists { return Err(Error::BadRequest(format!( "App with path {} already exists", &app.path ))); } - if let Some(custom_path) = &app.custom_path { require_admin(authed.is_admin, &authed.username)?; let exists = sqlx::query_scalar!( "SELECT EXISTS(SELECT 1 FROM app WHERE custom_path = $1 AND ($2::TEXT IS NULL OR workspace_id = $2))", custom_path, - if *CLOUD_HOSTED { Some(&w_id) } else { None } + if *CLOUD_HOSTED { Some(w_id) } else { None } ) .fetch_one(&mut *tx) .await?.unwrap_or(false); @@ -800,7 +956,6 @@ async fn create_app( ))); } } - sqlx::query!( "DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'app'", &app.path, @@ -808,7 +963,6 @@ async fn create_app( ) .execute(&mut *tx) .await?; - let id = sqlx::query_scalar!( "INSERT INTO app (workspace_id, path, summary, policy, versions, draft_only, custom_path) @@ -819,24 +973,24 @@ async fn create_app( json!(app.policy), app.draft_only, app.custom_path + .as_ref() .map(|s| if s.is_empty() { None } else { Some(s) }) .flatten() ) .fetch_one(&mut *tx) .await?; - let v_id = sqlx::query_scalar!( "INSERT INTO app_version - (app_id, value, created_by) - VALUES ($1, $2::text::json, $3) RETURNING id", + (app_id, value, created_by, raw_app) + VALUES ($1, $2::text::json, $3, $4) RETURNING id", id, //to preserve key orders serde_json::to_string(&app.value).unwrap(), authed.username, + raw_app ) .fetch_one(&mut *tx) .await?; - sqlx::query!( "UPDATE app SET versions = array_append(versions, $1::bigint) WHERE id = $2", v_id, @@ -850,22 +1004,20 @@ async fn create_app( &authed, "apps.create", ActionKind::Create, - &w_id, + w_id, Some(&app.path), None, ) .await?; - let mut args: HashMap> = HashMap::new(); - if let Some(dm) = app.deployment_message { + if let Some(dm) = &app.deployment_message { args.insert("deployment_message".to_string(), to_raw_value(&dm)); } - let tx = PushIsolationLevel::Transaction(tx); let (dependency_job_uuid, new_tx) = push( &db, tx, - &w_id, + w_id, JobPayload::AppDependencies { path: app.path.clone(), version: v_id }, PushArgs { args: &args, extra: None }, &authed.username, @@ -889,14 +1041,7 @@ async fn create_app( .await?; tracing::info!("Pushed app dependency job {}", dependency_job_uuid); - new_tx.commit().await?; - - webhook.send_message( - w_id.clone(), - WebhookMessage::CreateApp { workspace: w_id, path: app.path.clone() }, - ); - - Ok((StatusCode::CREATED, app.path)) + Ok((new_tx, path, v_id)) } async fn list_hub_apps(Extension(db): Extension) -> impl IntoResponse { @@ -1017,12 +1162,76 @@ async fn update_app( Path((w_id, path)): Path<(String, StripPath)>, Json(ns): Json, ) -> Result { - use sql_builder::prelude::*; - + // create_app_internal(authed, user_db, db, &w_id, &mut app).await?; let path = path.to_path(); + let opath = path.to_string(); + let (new_tx, npath, _v_id) = + update_app_internal(authed, db, user_db, &w_id, path, false, ns).await?; + new_tx.commit().await?; + webhook.send_message( + w_id.clone(), + WebhookMessage::UpdateApp { + workspace: w_id.clone(), + old_path: opath.clone(), + new_path: npath.clone(), + }, + ); + + Ok(format!("app {} updated (npath: {:?})", opath, npath)) +} + +async fn update_app_raw<'a>( + authed: ApiAuthed, + Extension(user_db): Extension, + Extension(db): Extension, + Extension(webhook): Extension, + Path((w_id, path)): Path<(String, StripPath)>, + multipart: Multipart, +) -> Result { + let path = path.to_path(); + let opath = path.to_string(); + let (npath, _id) = process_app_multipart!( + authed, + user_db, + db, + &w_id, + path, + multipart, + update_app_internal + ) + .await?; + + webhook.send_message( + w_id.clone(), + WebhookMessage::UpdateApp { + workspace: w_id.clone(), + old_path: opath.to_owned(), + new_path: npath.clone(), + }, + ); + + Ok(format!("app {} updated (npath: {:?})", opath, npath)) +} +// async fn create_app_internal<'a>( +// authed: ApiAuthed, +// db: sqlx::Pool, +// user_db: UserDB, +// w_id: &String, +// app: &mut CreateApp, +// ) + +async fn update_app_internal<'a>( + authed: ApiAuthed, + db: sqlx::Pool, + user_db: UserDB, + w_id: &str, + path: &str, + raw_app: bool, + ns: EditApp, +) -> Result<(sqlx::Transaction<'a, sqlx::Postgres>, String, i64)> { + use sql_builder::prelude::*; let mut tx = user_db.clone().begin(&authed).await?; - let npath = if ns.policy.is_some() || ns.path.is_some() || ns.summary.is_some() @@ -1069,7 +1278,7 @@ async fn update_app( let exists = sqlx::query_scalar!( "SELECT EXISTS(SELECT 1 FROM app WHERE custom_path = $1 AND ($2::TEXT IS NULL OR workspace_id = $2) AND NOT (path = $3 AND workspace_id = $4))", ncustom_path, - if *CLOUD_HOSTED { Some(&w_id) } else { None }, + if *CLOUD_HOSTED { Some(w_id) } else { None }, path, w_id ) @@ -1116,12 +1325,13 @@ async fn update_app( let v_id = sqlx::query_scalar!( "INSERT INTO app_version - (app_id, value, created_by) - VALUES ($1, $2::text::json, $3) RETURNING id", + (app_id, value, created_by, raw_app) + VALUES ($1, $2::text::json, $3, $4) RETURNING id", app_id, //to preserve key orders serde_json::to_string(&nvalue).unwrap(), authed.username, + raw_app ) .fetch_one(&mut *tx) .await?; @@ -1152,7 +1362,6 @@ async fn update_app( ))); } }; - sqlx::query!( "DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'app'", path, @@ -1160,29 +1369,26 @@ async fn update_app( ) .execute(&mut *tx) .await?; - audit_log( &mut *tx, &authed, "apps.update", ActionKind::Update, - &w_id, + w_id, Some(&npath), None, ) .await?; - let tx = PushIsolationLevel::Transaction(tx); let mut args: HashMap> = HashMap::new(); if let Some(dm) = ns.deployment_message { args.insert("deployment_message".to_string(), to_raw_value(&dm)); } args.insert("parent_path".to_string(), to_raw_value(&path)); - let (dependency_job_uuid, new_tx) = push( &db, tx, - &w_id, + w_id, JobPayload::AppDependencies { path: npath.clone(), version: v_id }, PushArgs { args: &args, extra: None }, &authed.username, @@ -1205,18 +1411,7 @@ async fn update_app( ) .await?; tracing::info!("Pushed app dependency job {}", dependency_job_uuid); - new_tx.commit().await?; - - webhook.send_message( - w_id.clone(), - WebhookMessage::UpdateApp { - workspace: w_id, - old_path: path.to_owned(), - new_path: npath.clone(), - }, - ); - - Ok(format!("app {} updated (npath: {:?})", path, npath)) + Ok((new_tx, npath, v_id)) } #[derive(Debug, Deserialize, Clone)] diff --git a/backend/windmill-api/src/workspaces_export.rs b/backend/windmill-api/src/workspaces_export.rs index 22357f2ed6..b799313933 100644 --- a/backend/windmill-api/src/workspaces_export.rs +++ b/backend/windmill-api/src/workspaces_export.rs @@ -501,7 +501,7 @@ pub(crate) async fn tarball_workspace( "SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path, app.extra_perms, app_version.value, app_version.created_at, app_version.created_by from app, app_version - WHERE app.workspace_id = $1 AND app_version.id = app.versions[array_upper(app.versions, 1)]", + WHERE app.workspace_id = $1 AND app_version.id = app.versions[array_upper(app.versions, 1)] AND app_version.raw_app IS false", ) .bind(&w_id) .fetch_all(&mut *tx) diff --git a/backend/windmill-common/src/apps.rs b/backend/windmill-common/src/apps.rs index bc8429c279..c5b22d434c 100644 --- a/backend/windmill-common/src/apps.rs +++ b/backend/windmill-common/src/apps.rs @@ -6,6 +6,8 @@ * LICENSE-AGPL for a copy of the license. */ +use std::collections::HashMap; + use serde::{Deserialize, Serialize}; /// Id in the `app_script` table. @@ -21,3 +23,8 @@ pub struct ListAppQuery { pub include_draft_only: Option, pub with_deployment_msg: Option, } + +#[derive(Deserialize)] +pub struct RawAppValue { + pub files: HashMap, +} diff --git a/backend/windmill-common/src/s3_helpers.rs b/backend/windmill-common/src/s3_helpers.rs index e3dff5350c..495f45912e 100644 --- a/backend/windmill-common/src/s3_helpers.rs +++ b/backend/windmill-common/src/s3_helpers.rs @@ -476,3 +476,7 @@ impl CredentialProvider for AwsCredentialAdapter { pub fn bundle(w_id: &str, hash: &str) -> String { format!("script_bundle/{}/{}", w_id, hash) } + +pub fn raw_app(w_id: &str, version: &i64) -> String { + format!("/home/rfiszel/raw_app/{}/{}", w_id, version) +} diff --git a/backend/windmill-common/src/utils.rs b/backend/windmill-common/src/utils.rs index 5018591072..62c193031a 100644 --- a/backend/windmill-common/src/utils.rs +++ b/backend/windmill-common/src/utils.rs @@ -19,8 +19,7 @@ use git_version::git_version; use chrono::Utc; use croner::Cron; -use rand::distr::Alphanumeric; -use rand::{rng, Rng}; +use rand::{distr::Alphanumeric, rng, Rng}; use reqwest::Client; use semver::Version; use serde::{Deserialize, Deserializer, Serialize}; diff --git a/backend/windmill-queue/src/jobs.rs b/backend/windmill-queue/src/jobs.rs index 9c7dd8a4fc..c92933ef92 100644 --- a/backend/windmill-queue/src/jobs.rs +++ b/backend/windmill-queue/src/jobs.rs @@ -4101,6 +4101,7 @@ pub async fn push<'c, 'd>( } else if job_kind == JobKind::Dependencies || job_kind == JobKind::FlowDependencies || job_kind == JobKind::DeploymentCallback + || job_kind == JobKind::AppDependencies { // using the dependency tag for deployment callback for now. We can create a separate tag when we need "dependency".to_string() diff --git a/backend/windmill-worker/src/worker_lockfiles.rs b/backend/windmill-worker/src/worker_lockfiles.rs index 6c56f1acef..cab717a94f 100644 --- a/backend/windmill-worker/src/worker_lockfiles.rs +++ b/backend/windmill-worker/src/worker_lockfiles.rs @@ -1731,6 +1731,90 @@ pub async fn handle_app_dependency_job( } } +// async fn upload_raw_app( +// app_value: &RawAppValue, +// job: &QueuedJob, +// mem_peak: &mut i32, +// canceled_by: &mut Option, +// job_dir: &str, +// db: &sqlx::Pool, +// worker_name: &str, +// occupancy_metrics: &mut Option<&mut OccupancyMetrics>, +// version: i64, +// ) -> Result<()> { +// let mut entrypoint = "index.ts"; +// for file in app_value.files.iter() { +// if file.0 == "/index.tsx" { +// entrypoint = "index.tsx"; +// } else if file.0 == "/index.js" { +// entrypoint = "index.js"; +// } +// write_file(&job_dir, file.0, &file.1)?; +// } +// let common_bun_proc_envs: HashMap = get_common_bun_proc_envs(None).await; + +// install_bun_lockfile( +// mem_peak, +// canceled_by, +// &job.id, +// &job.workspace_id, +// Some(db), +// job_dir, +// worker_name, +// common_bun_proc_envs, +// false, +// occupancy_metrics, +// ) +// .await?; +// let mut cmd = tokio::process::Command::new("esbuild"); +// let mut args = "--bundle --minify --outdir=dist/" +// .split(' ') +// .collect::>(); +// args.push(entrypoint); +// cmd.current_dir(job_dir) +// .env_clear() +// .args(args) +// .stdout(Stdio::piped()) +// .stderr(Stdio::piped()); +// let child = start_child_process(cmd, "esbuild").await?; + +// crate::handle_child::handle_child( +// &job.id, +// db, +// mem_peak, +// canceled_by, +// child, +// false, +// worker_name, +// &job.workspace_id, +// "esbuild", +// Some(30), +// false, +// occupancy_metrics, +// ) +// .await?; +// let output_dir = format!("{}/dist", job_dir); +// let target_dir = format!("/home/rfiszel/wmill/{}/{}", job.workspace_id, version); + +// tokio::fs::create_dir_all(&target_dir).await?; + +// tracing::info!("Copying files from {} to {}", output_dir, target_dir); + +// let index_ts = format!("{}/index.js", output_dir); +// let index_css = format!("{}/index.css", output_dir); + +// if tokio::fs::metadata(&index_ts).await.is_ok() { +// tokio::fs::copy(&index_ts, format!("{}/index.js", target_dir)).await?; +// } + +// if tokio::fs::metadata(&index_css).await.is_ok() { +// tokio::fs::copy(&index_css, format!("{}/index.css", target_dir)).await?; +// } +// // let file_path = format!("/home/rfiszel/wmill/{}/{}", job.workspace_id, version); + +// Ok(()) +// } + #[cfg(feature = "python")] async fn python_dep( reqs: String, diff --git a/frontend/.gitignore b/frontend/.gitignore index 32ae7e5b89..f11fc8c669 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -9,4 +9,6 @@ tests-out/ storageState.json .env.production dist/ -static/tsdocs/ \ No newline at end of file +static/tsdocs/ +static/ui_builder/ +ui_builder.tar.gz \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 467211a069..149b11d498 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "windmill-components", "version": "1.483.1", + "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { "@aws-crypto/sha256-js": "^4.0.0", @@ -74,7 +75,7 @@ "windmill-parser-wasm-py": "^1.477.1", "windmill-parser-wasm-regex": "^1.481.0", "windmill-parser-wasm-rust": "^1.429.0", - "windmill-parser-wasm-ts": "^1.429.0", + "windmill-parser-wasm-ts": "^1.438.2", "windmill-parser-wasm-yaml": "^1.429.0", "windmill-sql-datatype-parser-wasm": "^1.318.0", "y-monaco": "^0.1.4", @@ -131,6 +132,7 @@ "svelte-splitpanes": "^8.0.9", "svelte2tsx": "^0.6.16", "tailwindcss": "^3.4.1", + "tar": "^7.4.3", "tslib": "^2.6.1", "typescript": "^5.5.0", "vite": "^6.3.2", @@ -988,6 +990,27 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@isaacs/fs-minipass/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.8", "license": "MIT", @@ -1207,200 +1230,6 @@ "version": "0.0.1", "license": "SEE LICENSE IN LICENSE" }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.35.0.tgz", - "integrity": "sha512-uYQ2WfPaqz5QtVgMxfN6NpLD+no0MYHDBywl7itPYd3K5TjjSghNKmX8ic9S8NU8w81NVhJv/XojcHptRly7qQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.35.0.tgz", - "integrity": "sha512-FtKddj9XZudurLhdJnBl9fl6BwCJ3ky8riCXjEw3/UIbjmIY58ppWwPEvU3fNu+W7FUsAsB1CdH+7EQE6CXAPA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.35.0.tgz", - "integrity": "sha512-Uk+GjOJR6CY844/q6r5DR/6lkPFOw0hjfOIzVx22THJXMxktXG6CbejseJFznU8vHcEBLpiXKY3/6xc+cBm65Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.35.0.tgz", - "integrity": "sha512-3IrHjfAS6Vkp+5bISNQnPogRAW5GAV1n+bNCrDwXmfMHbPl5EhTmWtfmwlJxFRUCBZ+tZ/OxDyU08aF6NI/N5Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.35.0.tgz", - "integrity": "sha512-sxjoD/6F9cDLSELuLNnY0fOrM9WA0KrM0vWm57XhrIMf5FGiN8D0l7fn+bpUeBSU7dCgPV2oX4zHAsAXyHFGcQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.35.0.tgz", - "integrity": "sha512-2mpHCeRuD1u/2kruUiHSsnjWtHjqVbzhBkNVQ1aVD63CcexKVcQGwJ2g5VphOd84GvxfSvnnlEyBtQCE5hxVVw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.35.0.tgz", - "integrity": "sha512-mrA0v3QMy6ZSvEuLs0dMxcO2LnaCONs1Z73GUDBHWbY8tFFocM6yl7YyMu7rz4zS81NDSqhrUuolyZXGi8TEqg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.35.0.tgz", - "integrity": "sha512-DnYhhzcvTAKNexIql8pFajr0PiDGrIsBYPRvCKlA5ixSS3uwo/CWNZxB09jhIapEIg945KOzcYEAGGSmTSpk7A==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.35.0", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.35.0.tgz", - "integrity": "sha512-XQxVOCd6VJeHQA/7YcqyV0/88N6ysSVzRjJ9I9UA/xXpEsjvAgDTgH3wQYz5bmr7SPtVK2TsP2fQ2N9L4ukoUg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.35.0.tgz", - "integrity": "sha512-5pMT5PzfgwcXEwOaSrqVsz/LvjDZt+vQ8RT/70yhPU06PTuq8WaHhfT1LW+cdD7mW6i/J5/XIkX/1tCAkh1W6g==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.35.0.tgz", - "integrity": "sha512-c+zkcvbhbXF98f4CtEIP1EBA/lCic5xB0lToneZYvMeKu5Kamq3O8gqrxiYYLzlZH6E3Aq+TSW86E4ay8iD8EA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.35.0.tgz", - "integrity": "sha512-s91fuAHdOwH/Tad2tzTtPX7UZyytHIRR6V4+2IGlV0Cej5rkG0R61SX4l4y9sh0JBibMiploZx3oHKPnQBKe4g==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.35.0.tgz", - "integrity": "sha512-hQRkPQPLYJZYGP+Hj4fR9dDBMIM7zrzJDWFEMPdTnTy95Ljnv0/4w/ixFw3pTBMEuuEuoqtBINYND4M7ujcuQw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.39.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.39.0.tgz", @@ -1428,48 +1257,6 @@ "linux" ] }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.35.0.tgz", - "integrity": "sha512-OUOlGqPkVJCdJETKOCEf1mw848ZyJ5w50/rZ/3IBQVdLfR5jk/6Sr5m3iO2tdPgwo0x7VcncYuOvMhBWZq8ayg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.35.0.tgz", - "integrity": "sha512-2/lsgejMrtwQe44glq7AFFHLfJBPafpsTa6JvP2NGef/ifOa4KBoglVf7AKN7EV9o32evBPRqfg96fEHzWo5kw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.35.0.tgz", - "integrity": "sha512-PIQeY5XDkrOysbQblSW7v3l1MDZzkTEzAfTPkj5VAu3FW8fS4ynyLg2sINp0fp3SjZ8xkRYpLqoKcYqAkhU1dw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@smithy/types": { "version": "4.1.0", "license": "Apache-2.0", @@ -2887,11 +2674,12 @@ } }, "node_modules/chownr": { - "version": "2.0.0", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "dev": true, - "license": "ISC", "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/citty": { @@ -4396,8 +4184,9 @@ }, "node_modules/fs-minipass": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", "dev": true, - "license": "ISC", "dependencies": { "minipass": "^3.0.0" }, @@ -4407,8 +4196,9 @@ }, "node_modules/fs-minipass/node_modules/minipass": { "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, - "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, @@ -4426,7 +4216,6 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "hasInstallScript": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -4516,11 +4305,74 @@ "giget": "dist/cli.mjs" } }, + "node_modules/giget/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/giget/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/giget/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/giget/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/giget/node_modules/pathe": { "version": "2.0.3", "dev": true, "license": "MIT" }, + "node_modules/giget/node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/glob": { "version": "7.2.3", "dev": true, @@ -6504,37 +6356,39 @@ } }, "node_modules/minizlib": { - "version": "2.1.2", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", "dev": true, - "license": "MIT", "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" + "minipass": "^7.1.2" }, "engines": { - "node": ">= 8" + "node": ">= 18" } }, "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, "node_modules/mkdirp": { - "version": "1.0.4", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", "dev": true, - "license": "MIT", "bin": { - "mkdirp": "bin/cmd.js" + "mkdirp": "dist/cjs/src/bin.js" }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/mlly": { @@ -7885,7 +7739,6 @@ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, "hasInstallScript": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -10115,19 +9968,38 @@ } }, "node_modules/tar": { - "version": "6.2.1", + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", "dev": true, - "license": "ISC", "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "engines": { + "node": ">=18" } }, "node_modules/text-table": { diff --git a/frontend/package.json b/frontend/package.json index f4012baed1..9ba7dd084d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,6 +5,7 @@ "dev": "vite dev", "build": "vite build", "preview": "vite preview", + "postinstall": "node scripts/untar_ui_builder.js", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --threshold warning", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .", @@ -64,6 +65,7 @@ "svelte-splitpanes": "^8.0.9", "svelte2tsx": "^0.6.16", "tailwindcss": "^3.4.1", + "tar": "^7.4.3", "tslib": "^2.6.1", "typescript": "^5.5.0", "vite": "^6.3.2", @@ -142,7 +144,7 @@ "windmill-parser-wasm-py": "^1.477.1", "windmill-parser-wasm-regex": "^1.481.0", "windmill-parser-wasm-rust": "^1.429.0", - "windmill-parser-wasm-ts": "^1.429.0", + "windmill-parser-wasm-ts": "^1.438.2", "windmill-parser-wasm-yaml": "^1.429.0", "windmill-sql-datatype-parser-wasm": "^1.318.0", "y-monaco": "^0.1.4", diff --git a/frontend/scripts/untar_ui_builder.js b/frontend/scripts/untar_ui_builder.js new file mode 100644 index 0000000000..c64b1b1484 --- /dev/null +++ b/frontend/scripts/untar_ui_builder.js @@ -0,0 +1,48 @@ +import path from 'path' +import fs from 'fs' + +import { x } from 'tar' + +const tarUrl = 'https://pub-06154ed168a24e73a86ab84db6bf15d8.r2.dev/ui_builder-d44b577.tar.gz' +const outputTarPath = path.join(process.cwd(), 'ui_builder.tar.gz') +const extractTo = path.join(process.cwd(), 'static/ui_builder/') + +import { fileURLToPath } from 'url' +import { dirname } from 'path' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +// Download the tar file +const response = await fetch(tarUrl) +const buffer = await response.arrayBuffer() +await fs.promises.writeFile(outputTarPath, Buffer.from(buffer)) + +// Check if this script is being run from the package root +const isRootInstall = process.cwd() + '/scripts' === __dirname + +if (isRootInstall) { + console.log('Running postinstall: direct install') + // Your postinstall logic here +} else { + console.log('Skipping postinstall: installed as dependency') + process.exit(0) +} + +// Create extract directory if it doesn't exist +try { + await fs.promises.mkdir(extractTo, { recursive: true }) +} catch (err) { + if (err.code !== 'EEXIST') { + throw err + } +} + +await x({ + file: outputTarPath, + cwd: extractTo, + sync: false, + gzip: true +}) + +await fs.promises.unlink(outputTarPath) diff --git a/frontend/src/lib/ata/index.ts b/frontend/src/lib/ata/index.ts index 5414df8b18..3099094bf5 100644 --- a/frontend/src/lib/ata/index.ts +++ b/frontend/src/lib/ata/index.ts @@ -38,6 +38,30 @@ export interface ATABootstrapConfig { type ModuleMeta = { state: 'loading' } +export type DepsToGet = { + raw: string + module: string + version: string | undefined +}[] + +function getVersionFromRaw(d: string) { + if (d.lastIndexOf('@') > 0) { + const splitted = d.split('@') + let version = splitted.pop() + if (version?.startsWith('^') || version?.startsWith('~')) { + version = version.slice(1) + } + return version + } + return 'latest' +} + +export function versionRangeToVersion(version: string) { + if (version.startsWith('^') || version.startsWith('~')) { + return version.slice(1) + } + return version +} /** * The function which starts up type acquisition, * returns a function which you then pass the initial @@ -57,11 +81,11 @@ export const setupTypeAcquisition = (config: ATABootstrapConfig) => { let resLimit = { usage: 0 } - return async (initialSourceFile: string) => { + return async (initialSourceFile: string | DepsToGet) => { estimatedToDownload = 0 estimatedDownloaded = 0 - let todo: string[] = [initialSourceFile] + let todo: (string | DepsToGet)[] = [initialSourceFile] let next: string[] = [] let i = 0 let nb = 0 @@ -84,34 +108,25 @@ export const setupTypeAcquisition = (config: ATABootstrapConfig) => { } } - function getVersion(d: string) { - if (d.lastIndexOf('@') > 0) { - const splitted = d.split('@') - let version = splitted.pop() - if (version?.startsWith('^') || version?.startsWith('~')) { - version = version.slice(1) - } - return version - } - return 'latest' - } - async function resolveDeps( - initialSourceFile: string, + depsSource: string | DepsToGet, depth: number, resLimit: ResLimit ): Promise { - let depsToGet = config - .depsParser(initialSourceFile) - .map((d: string) => { - let raw = mapModuleNameToModule(d) - return { - raw, - module: raw.lastIndexOf('@') > 0 ? raw.split('@').slice(0, -1).join('@') : raw, - version: getVersion(d) - } - }) - .filter((f) => !moduleMap.has(f.raw)) + let depsToGet = + typeof depsSource == 'object' + ? depsSource + : config + .depsParser(depsSource) + .map((d: string) => { + let raw = mapModuleNameToModule(d) + return { + raw, + module: raw.lastIndexOf('@') > 0 ? raw.split('@').slice(0, -1).join('@') : raw, + version: getVersionFromRaw(raw) + } + }) + .filter((f) => !moduleMap.has(f.raw)) if (depth == 0) { const relativeDeps = depsToGet.filter((f) => isTypescriptRelativePath(f.raw)) diff --git a/frontend/src/lib/components/ArgInput.svelte b/frontend/src/lib/components/ArgInput.svelte index f7d100c1a4..dbbf7e9e89 100644 --- a/frontend/src/lib/components/ArgInput.svelte +++ b/frontend/src/lib/components/ArgInput.svelte @@ -375,6 +375,9 @@ (e.ctrlKey || e.metaKey) && (e.key == 'Enter' || e.key == 'c' || e.key == 'v' || e.key == 'x') ) { + if (e.key == 'Enter') { + dispatch('keydownCmdEnter') + } return } e.stopPropagation() @@ -846,7 +849,7 @@ }} bind:args={value} dndType={`nested-${title}`} - schemaSkippedValues={['label']} + hiddenArgs={['label']} on:reorder={(e) => { if (oneOf && oneOf[objIdx]) { const keys = e.detail @@ -865,7 +868,7 @@ {onlyMaskPassword} {disablePortal} {disabled} - schemaSkippedValues={['label']} + hiddenArgs={['label']} schema={{ properties: obj.properties, order: obj.order, diff --git a/frontend/src/lib/components/DiffEditor.svelte b/frontend/src/lib/components/DiffEditor.svelte index 92acf052e6..3b24ce9180 100644 --- a/frontend/src/lib/components/DiffEditor.svelte +++ b/frontend/src/lib/components/DiffEditor.svelte @@ -2,7 +2,7 @@ import { BROWSER } from 'esm-env' import { createEventDispatcher, onMount } from 'svelte' - import '@codingame/monaco-vscode-standalone-languages' + // import '@codingame/monaco-vscode-standalone-languages' import '@codingame/monaco-vscode-standalone-json-language-features' import '@codingame/monaco-vscode-standalone-typescript-language-features' import { editor as meditor } from 'monaco-editor' diff --git a/frontend/src/lib/components/EditableSchemaForm.svelte b/frontend/src/lib/components/EditableSchemaForm.svelte index 8af0b2bcbd..a39314824c 100644 --- a/frontend/src/lib/components/EditableSchemaForm.svelte +++ b/frontend/src/lib/components/EditableSchemaForm.svelte @@ -27,7 +27,7 @@ import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted' export let schema: Schema | any - export let schemaSkippedValues: string[] = [] + export let hiddenArgs: string[] = [] export let args: Record = {} export let shouldHideNoInputs: boolean = false export let noVariablePicker = false @@ -439,7 +439,7 @@ {#if opened === argName}
- {#if !schemaSkippedValues.includes(argName) && Object.keys(schema?.properties ?? {}).includes(argName)} + {#if !hiddenArgs.includes(argName) && Object.keys(schema?.properties ?? {}).includes(argName)} {#if typeof args == 'object' && schema?.properties[argName]} + +{#if !isLoading} + +{:else} + +{/if} diff --git a/frontend/src/lib/components/SchemaForm.svelte b/frontend/src/lib/components/SchemaForm.svelte index a3aa090914..f9852cf3a5 100644 --- a/frontend/src/lib/components/SchemaForm.svelte +++ b/frontend/src/lib/components/SchemaForm.svelte @@ -18,7 +18,7 @@ import type { ComponentCustomCSS } from './apps/types' export let schema: Schema | any - export let schemaSkippedValues: string[] = [] + export let hiddenArgs: string[] = [] export let schemaFieldTooltip: Record = {} export let args: Record = {} export let disabledArgs: string[] = [] @@ -204,7 +204,7 @@ : ''} > - {#if !schemaSkippedValues.includes(argName) && keys.includes(argName)} + {#if !hiddenArgs.includes(argName) && keys.includes(argName)} {#if typeof diff[argName] === 'object' && diff[argName].oldSchema} {@const formerProperty = diff[argName].oldSchema}
@@ -278,6 +278,7 @@ }} on:acceptChange={(e) => dispatch('acceptChange', e.detail)} on:rejectChange={(e) => dispatch('rejectChange', e.detail)} + on:keydownCmdEnter={() => dispatch('keydownCmdEnter')} {disablePortal} {resourceTypes} {prettifyHeader} diff --git a/frontend/src/lib/components/ScriptEditor.svelte b/frontend/src/lib/components/ScriptEditor.svelte index 67943052b8..b464baece4 100644 --- a/frontend/src/lib/components/ScriptEditor.svelte +++ b/frontend/src/lib/components/ScriptEditor.svelte @@ -535,7 +535,6 @@ dispatch('format') }} class="flex flex-1 h-full !overflow-visible" - lang={scriptLangToEditorLang(lang)} scriptLang={lang} automaticLayout={true} {fixedOverflowWidgets} diff --git a/frontend/src/lib/components/SimpleEditor.svelte b/frontend/src/lib/components/SimpleEditor.svelte index ecd40cf5b8..2a7f3f37ae 100644 --- a/frontend/src/lib/components/SimpleEditor.svelte +++ b/frontend/src/lib/components/SimpleEditor.svelte @@ -2,7 +2,7 @@ let cssClassesLoaded = $state(false) let tailwindClassesLoaded = $state(false) - import '@codingame/monaco-vscode-standalone-languages' + // import '@codingame/monaco-vscode-standalone-languages' import '@codingame/monaco-vscode-standalone-json-language-features' import '@codingame/monaco-vscode-standalone-css-language-features' import '@codingame/monaco-vscode-standalone-typescript-language-features' @@ -54,6 +54,7 @@ type IDisposable } from 'monaco-editor' + import { allClasses } from './apps/editor/componentsPanel/cssUtils' import { createEventDispatcher, onDestroy, onMount } from 'svelte' diff --git a/frontend/src/lib/components/TemplateEditor.svelte b/frontend/src/lib/components/TemplateEditor.svelte index a8d312a1f5..1d82563e38 100644 --- a/frontend/src/lib/components/TemplateEditor.svelte +++ b/frontend/src/lib/components/TemplateEditor.svelte @@ -13,7 +13,7 @@ import { createEventDispatcher, getContext, onDestroy, onMount } from 'svelte' import type { AppViewerContext } from './apps/types' import { writable } from 'svelte/store' - import '@codingame/monaco-vscode-standalone-languages' + // import '@codingame/monaco-vscode-standalone-languages' import '@codingame/monaco-vscode-standalone-typescript-language-features' import { initializeVscode } from './vscode' diff --git a/frontend/src/lib/components/WorkerGroup.svelte b/frontend/src/lib/components/WorkerGroup.svelte index fbb6154e19..017584d57f 100644 --- a/frontend/src/lib/components/WorkerGroup.svelte +++ b/frontend/src/lib/components/WorkerGroup.svelte @@ -748,7 +748,6 @@ disabled={!$superadmin} class="flex flex-1 grow h-full w-full" automaticLayout - lang="shell" scriptLang={'bash'} useWebsockets={false} fixedOverflowWidgets={false} diff --git a/frontend/src/lib/components/apps/components/helpers/RunnableComponent.svelte b/frontend/src/lib/components/apps/components/helpers/RunnableComponent.svelte index d1f191fe08..8ff15f0832 100644 --- a/frontend/src/lib/components/apps/components/helpers/RunnableComponent.svelte +++ b/frontend/src/lib/components/apps/components/helpers/RunnableComponent.svelte @@ -2,7 +2,7 @@ import type { Schema } from '$lib/common' import Alert from '$lib/components/common/alert/Alert.svelte' import Popover from '$lib/components/Popover.svelte' - import { AppService, type ExecuteComponentData } from '$lib/gen' + import { type ExecuteComponentData } from '$lib/gen' import { classNames, defaultIfEmptyString, emptySchema, sendUserToast } from '$lib/utils' import { deepEqual } from 'fast-equals' import { Bug } from 'lucide-svelte' @@ -26,6 +26,7 @@ import RefreshButton from '$lib/components/apps/components/helpers/RefreshButton.svelte' import { ctxRegex } from '../../utils' import { computeWorkspaceS3FileInputPolicy } from '../../editor/appUtilsS3' + import { executeRunnable } from './executeRunnable' import SchemaForm from '$lib/components/SchemaForm.svelte' import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted' @@ -40,7 +41,6 @@ export let forceSchemaDisplay: boolean = false export let wrapperClass = '' export let wrapperStyle = '' - export let initializing: boolean | undefined = undefined export let render: boolean export let outputs: { result: Output @@ -48,6 +48,7 @@ jobId?: Output | undefined } export let extraKey = '' + export let initializing: boolean = false export let recomputeOnInputChanged: boolean = true export let loading = false export let refreshOnStart: boolean = false @@ -350,84 +351,22 @@ try { jobId = await resultJobLoader?.abstractRun(async () => { - const nonStaticRunnableInputs = dynamicArgsOverride ?? {} - const staticRunnableInputs = {} - const allowUserResources: string[] = [] - for (const k of Object.keys(fields ?? {})) { - let field = fields[k] - if (field?.type == 'static' && fields[k]) { - if (isEditor) { - staticRunnableInputs[k] = field.value - } - } else if (field?.type == 'user') { - nonStaticRunnableInputs[k] = args?.[k] - if (isEditor && field.allowUserResources) { - allowUserResources.push(k) - } - } else if (field?.type == 'eval' || (field?.type == 'evalv2' && inputValues[k])) { - const ctxMatch = field.expr.match(ctxRegex) - if (ctxMatch) { - nonStaticRunnableInputs[k] = '$ctx:' + ctxMatch[1] - } else { - nonStaticRunnableInputs[k] = await inputValues[k]?.computeExpr() - } - if (isEditor && field?.type == 'evalv2' && field.allowUserResources) { - allowUserResources.push(k) - } - } else { - if (isEditor && field?.type == 'connected' && field.allowUserResources) { - allowUserResources.push(k) - } - nonStaticRunnableInputs[k] = runnableInputValues[k] - } - } - - const oneOfRunnableInputs = isEditor ? collectOneOfFields(fields, $app) : {} - - const requestBody: ExecuteComponentData['requestBody'] = { - args: nonStaticRunnableInputs, - component: id, - force_viewer_static_fields: !isEditor ? undefined : staticRunnableInputs, - force_viewer_one_of_fields: !isEditor ? undefined : oneOfRunnableInputs, - force_viewer_allow_user_resources: !isEditor ? undefined : allowUserResources - } - - if (runnable?.type === 'runnableByName') { - const { inlineScript } = inlineScriptOverride - ? { inlineScript: inlineScriptOverride } - : runnable - - if (inlineScript) { - if (inlineScript.id !== undefined) { - requestBody['id'] = inlineScript.id - } - requestBody['raw_code'] = { - content: inlineScript.id === undefined ? inlineScript.content : '', - language: inlineScript.language ?? '', - path: inlineScript.path, - lock: inlineScript.id === undefined ? inlineScript.lock : undefined, - cache_ttl: inlineScript.cache_ttl - } - } - } else if (runnable?.type === 'runnableByPath') { - const { path, runType } = runnable - requestBody['path'] = runType !== 'hubscript' ? `${runType}/${path}` : `script/${path}` - } - - if ($app.version !== undefined) { - requestBody['version'] = $app.version - } - - const uuid = await AppService.executeComponent({ + const uuid = await executeRunnable( + runnable, workspace, - path: defaultIfEmptyString($appPath, `u/${$userStore?.username ?? 'unknown'}/newapp`), - requestBody - }) + $app.version, + $userStore?.username, + $appPath, + id, + await buildRequestBody(dynamicArgsOverride), + inlineScriptOverride + ) if (isEditor) { addJob(uuid) } return uuid }, callbacks) + if (setRunnableJobEditorPanel && editorContext) { editorContext.runnableJobEditorPanel.update((p) => { return { @@ -449,6 +388,51 @@ } type Callbacks = { done: (x: any) => void; cancel: () => void; error: (e: any) => void } + export async function buildRequestBody(dynamicArgsOverride: Record | undefined) { + const nonStaticRunnableInputs = dynamicArgsOverride ?? {} + const staticRunnableInputs = {} + const allowUserResources: string[] = [] + for (const k of Object.keys(fields ?? {})) { + let field = fields[k] + if (field?.type == 'static' && fields[k]) { + if (isEditor) { + staticRunnableInputs[k] = field.value + } + } else if (field?.type == 'user') { + nonStaticRunnableInputs[k] = args?.[k] + if (isEditor && field.allowUserResources) { + allowUserResources.push(k) + } + } else if (field?.type == 'eval' || (field?.type == 'evalv2' && inputValues[k])) { + const ctxMatch = field.expr.match(ctxRegex) + if (ctxMatch) { + nonStaticRunnableInputs[k] = '$ctx:' + ctxMatch[1] + } else { + nonStaticRunnableInputs[k] = await inputValues[k]?.computeExpr() + } + if (isEditor && field?.type == 'evalv2' && field.allowUserResources) { + allowUserResources.push(k) + } + } else { + if (isEditor && field?.type == 'connected' && field.allowUserResources) { + allowUserResources.push(k) + } + nonStaticRunnableInputs[k] = runnableInputValues[k] + } + } + + const oneOfRunnableInputs = isEditor ? collectOneOfFields(fields, $app) : {} + + const requestBody: ExecuteComponentData['requestBody'] = { + args: nonStaticRunnableInputs, + component: id, + force_viewer_static_fields: !isEditor ? undefined : staticRunnableInputs, + force_viewer_one_of_fields: !isEditor ? undefined : oneOfRunnableInputs, + force_viewer_allow_user_resources: !isEditor ? undefined : allowUserResources + } + return requestBody + } + export async function runComponent( noToast = true, inlineScriptOverride?: InlineScript, @@ -647,15 +631,16 @@ } } + const nautoRefresh = (autoRefresh && recomputableByRefreshButton) || overrideAutoRefresh if (replaceCallback) { $runnableComponents[id] = { - autoRefresh: (autoRefresh && recomputableByRefreshButton) || overrideAutoRefresh, + autoRefresh: nautoRefresh, refreshOnStart: refreshOnStart, cb: [cancellableRun] } } else { $runnableComponents[id] = { - autoRefresh: (autoRefresh && recomputableByRefreshButton) || overrideAutoRefresh, + autoRefresh: nautoRefresh, refreshOnStart: refreshOnStart, cb: [...($runnableComponents[id]?.cb ?? []), cancellableRun] } @@ -664,6 +649,10 @@ if (!noInitialize && !$initialized.initializedComponents.includes(id)) { $initialized.initializedComponents = [...$initialized.initializedComponents, id] } + // console.log(initializing, $initialized.initialized, refreshOnStart) + if (initializing && $initialized.initialized && (refreshOnStart || nautoRefresh)) { + setDebouncedExecute() + } }) onDestroy(() => { @@ -850,7 +839,8 @@
{/if} - {#if render && !initializing && autoRefresh === true && !hideRefreshButton} + + {#if render && autoRefresh === true && !hideRefreshButton}
diff --git a/frontend/src/lib/components/apps/components/helpers/executeRunnable.ts b/frontend/src/lib/components/apps/components/helpers/executeRunnable.ts new file mode 100644 index 0000000000..397f690ae2 --- /dev/null +++ b/frontend/src/lib/components/apps/components/helpers/executeRunnable.ts @@ -0,0 +1,50 @@ +import { AppService, type ExecuteComponentData } from '$lib/gen' +import { defaultIfEmptyString } from '$lib/utils' +import type { Runnable } from '../../inputType' +import type { InlineScript } from '../../types' + +export async function executeRunnable( + runnable: Runnable, + workspace: string, + version: number | undefined, + username: string | undefined, + path: string, + id: string, + requestBody: ExecuteComponentData['requestBody'], + inlineScriptOverride?: InlineScript +) { + let appPath = defaultIfEmptyString(path, `u/${username ?? 'unknown'}/newapp`) + if (runnable?.type === 'runnableByName') { + const { inlineScript } = inlineScriptOverride + ? { inlineScript: inlineScriptOverride } + : runnable + + if (inlineScript) { + if (inlineScript.id !== undefined) { + requestBody['id'] = inlineScript.id + } + requestBody['raw_code'] = { + content: inlineScript.id === undefined ? inlineScript.content : '', + language: inlineScript.language ?? '', + path: appPath + '/' + id, + lock: inlineScript.id === undefined ? inlineScript.lock : undefined, + cache_ttl: inlineScript.cache_ttl + } + } + } else if (runnable?.type === 'runnableByPath') { + const { path, runType } = runnable + requestBody['path'] = runType !== 'hubscript' ? `${runType}/${path}` : `script/${path}` + } + + if (version !== undefined) { + requestBody['version'] = version + } + + const uuid = await AppService.executeComponent({ + workspace, + path: appPath, + requestBody + }) + + return uuid +} diff --git a/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte b/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte index 46fde6f024..48bbfc0288 100644 --- a/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte +++ b/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte @@ -1,15 +1,10 @@ + + + + + { + open = false + }} + tooltip="Look at latests runs to spot potential bugs." + documentationLink="https://www.windmill.dev/docs/apps/app_debugging" + > + + + +
+ {#if jobs.length > 0} +
+ {#each jobs ?? [] as id} + {@const selectedJob = jobsById[id]} + {#if selectedJob} + + +
{ + selectedJobId = id + rightColumnSelect = 'detail' + }} + > + {truncateRev(selectedJob.job, 20)} + {selectedJob.component} +
+ {/if} + {/each} +
+ {:else} +
No items
+ {/if} +
+
+
+ +
+
+ + Timeline + Details + +
+ {#if rightColumnSelect == 'timeline'} +
+ +
+ {:else if rightColumnSelect == 'detail'} +
+ {#if selectedJobId} + {#if selectedJobId?.includes('Frontend')} + {@const jobResult = jobsById[selectedJobId]} + {#if jobResult?.error !== undefined} + + + + + +
+ +
+
+
+ {:else if jobResult !== undefined} + + + + + +
+ +
+
+
+ {:else} + + {/if} + {:else} +
+ {#if job?.['running']} +
+ +
+ {/if} + {#if job?.args} +
+ +
+ {/if} + {#if job?.raw_code} +
+ +
+ {/if} + + {#if job?.job_kind !== 'flow' && !isFlowPreview(job?.job_kind)} + {@const jobResult = jobsById[selectedJobId]} + + + + + + {#if job != undefined && 'result' in job && job.result != undefined}
+ {:else if testIsLoading} +
+ {:else if job != undefined && 'result' in job && job?.['result'] == undefined} +
Result is undefined
+ {:else} +
+ +
+ {/if} +
+ {#if jobResult?.transformer} + +
Transformer results
+ {#if job != undefined && 'result' in job && job.result != undefined} +
+ +
+ {:else if testIsLoading} +
+ {:else if job != undefined && 'result' in job && job?.['result'] == undefined} +
Result is undefined
+ {:else} +
+ +
+ {/if} +
+ {/if} +
+ {:else} +
+ +
+ {#if job?.id} + { + job = detail + }} + /> + {:else} + + {/if} +
+ {/if} +
+ {/if} + {:else} +
Select a job to see its details
+ {/if} +
+ {/if} +
+ + + + {#if refreshComponents} + + {/if} + + {#if hasErrors} + + {/if} + + + diff --git a/frontend/src/lib/components/apps/editor/AppTimeline.svelte b/frontend/src/lib/components/apps/editor/AppTimeline.svelte index 7d2f4b06f1..bf41491c29 100644 --- a/frontend/src/lib/components/apps/editor/AppTimeline.svelte +++ b/frontend/src/lib/components/apps/editor/AppTimeline.svelte @@ -1,18 +1,19 @@ + +{#if runnableComponents && $runnableComponents[id] != undefined} + +{/if} diff --git a/frontend/src/lib/components/apps/editor/inlineScriptsPanel/EmptyInlineScript.svelte b/frontend/src/lib/components/apps/editor/inlineScriptsPanel/EmptyInlineScript.svelte index 4e02887b4f..12cfedd2cf 100644 --- a/frontend/src/lib/components/apps/editor/inlineScriptsPanel/EmptyInlineScript.svelte +++ b/frontend/src/lib/components/apps/editor/inlineScriptsPanel/EmptyInlineScript.svelte @@ -10,26 +10,25 @@ import { defaultScriptLanguages, getScriptByPath, processLangs } from '$lib/scripts' import { Building, GitFork, Globe2 } from 'lucide-svelte' - import { createEventDispatcher, getContext } from 'svelte' + import { createEventDispatcher } from 'svelte' import { fly } from 'svelte/transition' - import type { AppViewerContext } from '../../types' import { defaultCode } from '../component' - import InlineScriptList from '../settingsPanel/mainInput/InlineScriptList.svelte' import WorkspaceScriptList from '../settingsPanel/mainInput/WorkspaceScriptList.svelte' import RunnableSelector from '../settingsPanel/mainInput/RunnableSelector.svelte' import { defaultScripts } from '$lib/stores' import DefaultScripts from '$lib/components/DefaultScripts.svelte' import type { Preview } from '$lib/gen' + import type { InlineScript } from '../../types' - export let name: string export let componentType: string | undefined = undefined export let showScriptPicker = false + export let rawApps = false + export let unusedInlineScripts: { name: string; inlineScript: InlineScript }[] - let tab = 'inlinescripts' + let tab = 'workspacescripts' let filter: string = '' let picker: Drawer - const { appPath, app } = getContext('AppViewerContext') const dispatch = createEventDispatcher() async function inferInlineScriptSchema( @@ -48,26 +47,22 @@ async function createInlineScriptByLanguage( language: Preview['language'], - path: string, subkind: 'pgsql' | 'mysql' | 'fetch' | undefined = undefined ) { const content = defaultCode(componentType ?? '', (subkind || language) ?? '') ?? initialCode(language, 'script', subkind ?? 'flow') - return newInlineScript(content, language, path) + return newInlineScript(content, language) } - async function newInlineScript(content: string, language: Preview['language'], path: string) { - const fullPath = `${$appPath}/${path}` - + async function newInlineScript(content: string, language: Preview['language']) { let schema: Schema = emptySchema() schema = await inferInlineScriptSchema(language, content, schema) const newInlineScript = { content, language, - path: fullPath, schema } dispatch('new', newInlineScript) @@ -75,23 +70,12 @@ async function pickScript(path: string) { const script = await getScriptByPath(path) - newInlineScript(script.content, script.language, path) + newInlineScript(script.content, script.language) } async function pickHubScript(path: string) { const script = await getScriptByPath(path) - newInlineScript(script.content, script.language, path) - } - - function pickInlineScript(name: string) { - const unusedInlineScriptIndex = $app.unusedInlineScripts?.findIndex( - (script) => script.name === name - ) - const unusedInlineScript = $app.unusedInlineScripts?.[unusedInlineScriptIndex] - - $app.unusedInlineScripts.splice(unusedInlineScriptIndex, 1) - $app = $app - dispatch('new', unusedInlineScript.inlineScript) + newInlineScript(script.content, script.language) } $: langs = processLangs(undefined, $defaultScripts?.order ?? Object.keys(defaultScriptLanguages)) @@ -108,12 +92,6 @@
- -
- - Detached Inline Scripts -
-
@@ -131,14 +109,7 @@
- {#if tab == 'inlinescripts'} - pickInlineScript(e.detail)} - inlineScripts={$app.unusedInlineScripts - ? $app.unusedInlineScripts.map((uis) => uis.name) - : []} - /> - {:else if tab == 'workspacescripts'} + {#if tab == 'workspacescripts'} pickScript(e.detail)} /> {:else if tab == 'hubscripts'} pickHubScript(e.detail.path)} /> @@ -159,7 +130,7 @@
Choose a language
{#if showScriptPicker} - + {/if}
-
-
- Frontend - - Frontend scripts are executed in the browser and can manipulate the app context directly. - -
+ {#if !rawApps} +
+
+ Frontend + + Frontend scripts are executed in the browser and can manipulate the app context + directly. + +
-
- { - const newInlineScript = { - content: `// read outputs and ctx +
+ { + const newInlineScript = { + content: `// read outputs and ctx console.log(ctx.email) // access a global state store @@ -235,14 +208,15 @@ state.foo += 1 // all helpers can be found at https://www.windmill.dev/docs/apps/app-runnable-panel#frontend-scripts-helpers return state.foo`, - language: 'frontend', - path: 'frontend script', - schema: undefined - } - dispatch('new', newInlineScript) - }} - /> + language: 'frontend', + path: 'frontend script', + schema: undefined + } + dispatch('new', newInlineScript) + }} + /> +
-
+ {/if}
diff --git a/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptEditor.svelte b/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptEditor.svelte index cdf071618e..670ebb2445 100644 --- a/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptEditor.svelte +++ b/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptEditor.svelte @@ -14,13 +14,13 @@ import type { AppInput } from '../../inputType' import SimpleEditor from '$lib/components/SimpleEditor.svelte' import { buildExtraLib } from '../../utils' - import RunButton from './RunButton.svelte' + import RunButton from './AppRunButton.svelte' import { scriptLangToEditorLang } from '$lib/scripts' import ScriptGen from '$lib/components/copilot/ScriptGen.svelte' import DiffEditor from '$lib/components/DiffEditor.svelte' - import { userStore } from '$lib/stores' import CacheTtlPopup from './CacheTtlPopup.svelte' import EditorSettings from '$lib/components/EditorSettings.svelte' + import { userStore } from '$lib/stores' let inlineScriptEditorDrawer: InlineScriptEditorDrawer @@ -179,7 +179,9 @@ if (!deepEqual(newFields, fields)) { fields = newFields - $stateId++ + if (stateId) { + $stateId++ + } } } } @@ -211,7 +213,9 @@ ] } } - $stateId++ + if (stateId) { + $stateId++ + } } } @@ -219,6 +223,8 @@ {#if inlineScript} {#if inlineScript.language != 'frontend'} { $app = $app - $stateId++ + if (stateId) { + $stateId++ + } }} />
{:else} { if ( diff --git a/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptHiddenRunnable.svelte b/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptHiddenRunnable.svelte index 3a04c55389..aca0f9c043 100644 --- a/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptHiddenRunnable.svelte +++ b/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptHiddenRunnable.svelte @@ -10,7 +10,7 @@ export let id: string export let transformer: boolean - const { runnableComponents } = getContext('AppViewerContext') + const { runnableComponents, app } = getContext('AppViewerContext') async function fork(nrunnable: Runnable) { runnable = { ...runnable, ...nrunnable, autoRefresh: true, recomputeOnInputChanged: true } } @@ -37,9 +37,11 @@ bind:inlineScript={runnable.transformer} name="Transformer" on:delete={() => { - delete $runnableComponents[id] - runnable.transformer = undefined - runnable = runnable + if (runnableComponents) { + delete $runnableComponents[id] + runnable.transformer = undefined + runnable = runnable + } }} /> {:else} @@ -67,8 +69,8 @@ /> {:else} onPick(e.detail)} - name={runnable.name} on:delete showScriptPicker on:new={(e) => { diff --git a/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptRunnableByPath.svelte b/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptRunnableByPath.svelte index 16fe10c594..866141b231 100644 --- a/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptRunnableByPath.svelte +++ b/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptRunnableByPath.svelte @@ -18,18 +18,26 @@ import { deepEqual } from 'fast-equals' import { computeFields } from './utils' import { inferArgs, loadSchema } from '$lib/infer' - import RunButton from './RunButton.svelte' + import AppRunButton from './AppRunButton.svelte' import { getScriptByPath } from '$lib/scripts' import { sendUserToast } from '$lib/toast' import { autoPlacement } from '@floating-ui/core' import { ExternalLink, Eye, GitFork, Pen, RefreshCw, Trash } from 'lucide-svelte' + import { get } from 'svelte/store' + import RunButton from '$lib/components/RunButton.svelte' import Popover from '$lib/components/meltComponents/Popover.svelte' export let runnable: RunnableByPath - export let fields: Record + export let fields: + | Record + | undefined export let id: string + export let rawApps = false + export let isLoading = false + export let onRun = async () => {} + export let onCancel = async () => {} - const { stateId } = getContext('AppViewerContext') + const viewerContext = getContext('AppViewerContext') let drawerFlowViewer: Drawer let flowPath: string = '' @@ -37,15 +45,16 @@ const dispatch = createEventDispatcher() - async function refreshScript(x: RunnableByPath) { + async function refreshScript(runnable: RunnableByPath) { try { - let { schema } = await getScriptByPath(x.path) - if (!deepEqual(x.schema, schema)) { - x.schema = schema - if (!x.schema.order) { - x.schema.order = Object.keys(x.schema.properties ?? {}) + let { schema } = await getScriptByPath(runnable.path) + console.log('schema1', schema) + if (!deepEqual(runnable.schema, schema)) { + runnable.schema = schema + if (!runnable.schema.order) { + runnable.schema.order = Object.keys(runnable.schema.properties ?? {}) } - fields = computeFields(schema, false, fields) + fields = computeFields(schema, false, fields ?? {}) } } catch (e) { notFound = true @@ -53,15 +62,16 @@ } } - async function refreshFlow(x: RunnableByPath) { + async function refreshFlow(runnable: RunnableByPath) { try { - const { schema } = (await loadSchema($workspaceStore ?? '', x.path, 'flow')) ?? emptySchema() - if (!deepEqual(x.schema, schema)) { - x.schema = schema - if (!x.schema.order) { - x.schema.order = Object.keys(x.schema.properties ?? {}) + const { schema } = + (await loadSchema($workspaceStore ?? '', runnable.path, 'flow')) ?? emptySchema() + if (!deepEqual(runnable.schema, schema)) { + runnable.schema = schema + if (!runnable.schema.order) { + runnable.schema.order = Object.keys(runnable.schema.properties ?? {}) } - fields = computeFields(schema, false, fields) + fields = computeFields(schema, false, fields ?? {}) } } catch (e) { notFound = true @@ -92,6 +102,7 @@ if (deepEqual(runnable, lastRunnable)) { return } + console.log('runnable', runnable) notFound = false if (runnable.runType == 'script') { refreshScript(runnable) @@ -111,7 +122,11 @@
- + {#if !rawApps} + + {:else} + + {/if}
- {#key $stateId} + {#key viewerContext?.stateId ? get(viewerContext.stateId) : 0} {#if notFound}
{runnable.runType} not found at {runnable.path} in workspace {$workspaceStore}
import { getContext } from 'svelte' - import type { AppEditorContext, AppViewerContext, HiddenRunnable } from '../../types' + import type { AppEditorContext, AppViewerContext } from '../../types' import { Pane, Splitpanes } from 'svelte-splitpanes' import InlineScriptsPanelList from './InlineScriptsPanelList.svelte' import InlineScriptEditor from './InlineScriptEditor.svelte' @@ -9,13 +9,11 @@ import InlineScriptHiddenRunnable from './InlineScriptHiddenRunnable.svelte' import { BG_PREFIX } from '../../utils' import { sendUserToast } from '$lib/toast' - import type { RunnableByName } from '../../inputType' - import { ScriptService } from '$lib/gen' import { workspaceStore } from '$lib/stores' - import { findNextAvailablePath } from '$lib/path' import { twMerge } from 'tailwind-merge' + import { createScriptFromInlineScript } from './utils' - const { app, runnableComponents } = getContext('AppViewerContext') + const { app, runnableComponents, appPath } = getContext('AppViewerContext') const { selectedComponentInEditor } = getContext('AppEditorContext') function deleteBackgroundScript(index: number) { @@ -36,8 +34,10 @@ } $selectedComponentInEditor = undefined - delete $runnableComponents[BG_PREFIX + index] - $runnableComponents = $runnableComponents + if (runnableComponents) { + delete $runnableComponents[BG_PREFIX + index] + $runnableComponents = $runnableComponents + } } $: gridItem = @@ -57,50 +57,6 @@ (k_, index) => `unused-${index}` === $selectedComponentInEditor ) - async function createScriptFromInlineScript( - id: string, - runnable: HiddenRunnable | RunnableByName - ) { - if (runnable.type != 'runnableByName') { - sendUserToast('Only inline scripts can be saved to workspace', true) - return - } - if (!runnable.inlineScript) { - sendUserToast('No inline script found', true) - return - } - let path = `${runnable.inlineScript.path}/inline_${id}` - path = await findNextAvailablePath(path) - let language = runnable.inlineScript.language - if (language == 'frontend') { - sendUserToast('Frontend scripts can not be saved to workspace', true) - return - } - await ScriptService.createScript({ - workspace: $workspaceStore!, - requestBody: { - path: path, - summary: runnable.name ?? '', - description: '', - content: runnable.inlineScript.content, - parent_hash: undefined, - schema: runnable.inlineScript.schema, - is_template: false, - language: language! - } - }) - - Object.assign(runnable, { - type: 'runnableByPath', - schema: runnable.inlineScript.schema, - runType: 'script', - recomputeIds: undefined, - path - }) - - $app = $app - } - export let width: number | undefined = undefined @@ -114,13 +70,18 @@ {#if !$selectedComponentInEditor}
- Select a script on the left panel + Select a runnable on the left panel
{:else if gridItem} {#key gridItem?.id} { - createScriptFromInlineScript(gridItem?.id ?? 'unknown', e.detail) + createScriptFromInlineScript( + gridItem?.id ?? 'unknown', + e.detail, + $workspaceStore ?? '', + $appPath + ) }} bind:gridItem /> @@ -145,7 +106,13 @@ {#if $app.hiddenInlineScripts?.[hiddenInlineScript]} { - createScriptFromInlineScript(BG_PREFIX + hiddenInlineScript, e.detail) + createScriptFromInlineScript( + BG_PREFIX + hiddenInlineScript, + e.detail, + $workspaceStore ?? '', + $appPath + ) + $app = $app }} transformer={$selectedComponentInEditor?.endsWith('_transformer')} on:delete={() => deleteBackgroundScript(hiddenInlineScript)} @@ -154,7 +121,7 @@ />{/if}{/key} {:else}
- No script found at id {$selectedComponentInEditor} + No runnable found at id {$selectedComponentInEditor}
{/if}
diff --git a/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptsPanelList.svelte b/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptsPanelList.svelte index e5a83cc178..4ba20786dc 100644 --- a/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptsPanelList.svelte +++ b/frontend/src/lib/components/apps/editor/inlineScriptsPanel/InlineScriptsPanelList.svelte @@ -21,7 +21,7 @@ function selectScript(id: string) { $selectedComponentInEditor = id - if (!id.startsWith('unused-') || !id.startsWith(BG_PREFIX)) { + if ((selectedComponent && !id.startsWith('unused-')) || !id.startsWith(BG_PREFIX)) { $selectedComponent = [$selectedComponentInEditor.split('_transformer')[0]] } } @@ -29,7 +29,7 @@ $: runnables = getAppScripts($app.grid, $app.subgrids) // When selected component changes, update selectedScriptComponentId - $: handleSelectedComponent($selectedComponent) + $: selectedComponent && handleSelectedComponent($selectedComponent) function handleSelectedComponent(selectedComponent: string[] | undefined) { if ( @@ -184,11 +184,11 @@ {/if}
-
Background Runnables + diff --git a/frontend/src/lib/components/apps/editor/inlineScriptsPanel/utils.ts b/frontend/src/lib/components/apps/editor/inlineScriptsPanel/utils.ts index 15b7c44237..7609ce4beb 100644 --- a/frontend/src/lib/components/apps/editor/inlineScriptsPanel/utils.ts +++ b/frontend/src/lib/components/apps/editor/inlineScriptsPanel/utils.ts @@ -1,6 +1,8 @@ import type { Schema } from '$lib/common' -import type { AppInputs, Runnable } from '../../inputType' -import type { GridItem, InlineScript } from '../../types' +import { ScriptService } from '$lib/gen' +import { sendUserToast } from '$lib/toast' +import type { AppInputs, Runnable, RunnableByName } from '../../inputType' +import type { GridItem, HiddenRunnable, InlineScript } from '../../types' import { fieldTypeToTsType, schemaToInputsSpec } from '../../utils' import type { AppComponent } from '../component' @@ -111,7 +113,7 @@ export function getAppScripts( grid: GridItem[], subgrids: Record | undefined ): AppScriptsList { - const scriptsList = grid.reduce( + const scriptsList = (grid ?? []).reduce( (acc, gridComponent) => processGridItemRunnable(gridComponent, acc), { inline: [], imported: [], transformer: false } as AppScriptsList ) @@ -143,3 +145,45 @@ function processRunnable( transformer: transformer !== undefined }) } + +export async function createScriptFromInlineScript( + id: string, + runnable: HiddenRunnable | RunnableByName, + workspace: string, + appPath: string +) { + if (runnable.type != 'runnableByName') { + sendUserToast('Only inline scripts can be saved to workspace', true) + return + } + if (!runnable.inlineScript) { + sendUserToast('No inline script found', true) + return + } + let language = runnable.inlineScript.language + if (language == 'frontend') { + sendUserToast('Frontend scripts can not be saved to workspace', true) + return + } + await ScriptService.createScript({ + workspace, + requestBody: { + path: appPath + '/' + id, + summary: runnable.name ?? '', + description: '', + content: runnable.inlineScript.content, + parent_hash: undefined, + schema: runnable.inlineScript.schema, + is_template: false, + language: language! + } + }) + + Object.assign(runnable, { + type: 'runnableByPath', + schema: runnable.inlineScript.schema, + runType: 'script', + recomputeIds: undefined, + path: appPath + '/' + id + }) +} diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/ComponentPanel.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/ComponentPanel.svelte index e9fe80912d..b706095172 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/ComponentPanel.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/ComponentPanel.svelte @@ -156,7 +156,6 @@ type: 'runnableByName', name: `Eval of ${id}`, inlineScript: { - path: `${id}_eval`, content: `return ${componentSettings?.item.data.componentInput?.['expr']}`, language: 'frontend', refreshOn: componentSettings?.item.data.componentInput?.['connections']?.map((c) => { diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/HideButton.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/HideButton.svelte index 2a998e75e3..f473894312 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/HideButton.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/HideButton.svelte @@ -41,7 +41,8 @@ const shortcuts = { left: 'B', right: 'U', - bottom: 'L' + bottom: 'L', + top: 'T' } diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/SelectedRunnable.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/SelectedRunnable.svelte index 84cec1ed75..94c0af27cf 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/SelectedRunnable.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/SelectedRunnable.svelte @@ -46,36 +46,25 @@ appInput?.runnable?.type === 'runnableByPath' || (appInput?.runnable?.type === 'runnableByName' && appInput.runnable?.inlineScript !== undefined) - function getActions(hasScript: boolean): ActionType[] { - if (hasScript) { - return [ - ...(appInput.runnable?.type === 'runnableByName' && appInput.runnable.inlineScript - ? ([ - { - label: 'Detach', - icon: ExternalLink, - color: 'light', - callback: detach - } - ] as const) - : []), - { - label: 'Clear', - icon: X, - color: 'red', - callback: clear - } - ] - } else { - return [ - { - label: 'Clear', - icon: X, - color: 'red', - callback: clear - } - ] - } + function getActions(_hasScript: boolean): ActionType[] { + return [ + ...(appInput.runnable?.type === 'runnableByName' && appInput.runnable.inlineScript + ? ([ + { + label: 'Detach', + icon: ExternalLink, + color: 'light', + callback: detach + } + ] as const) + : []), + { + label: 'Clear', + icon: X, + color: 'red', + callback: clear + } + ] } $: actions = getActions(hasScript) diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/RunnableInputEditor.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/RunnableInputEditor.svelte index 6f9a629261..8028ea9657 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/RunnableInputEditor.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/inputEditor/RunnableInputEditor.svelte @@ -5,13 +5,14 @@ import type { AppComponent } from '../../component' import RunnableSelector from '../mainInput/RunnableSelector.svelte' import SelectedRunnable from '../SelectedRunnable.svelte' - import type { AppEditorContext } from '$lib/components/apps/types' + import type { AppEditorContext, AppViewerContext } from '$lib/components/apps/types' export let appInput: ResultAppInput export let defaultUserInput = false export let appComponent: AppComponent const { selectedComponentInEditor } = getContext('AppEditorContext') + const { app } = getContext('AppViewerContext') function onPick({ runnable, @@ -33,6 +34,7 @@ {:else if appInput !== undefined} ('AppViewerContext') + const appContext = getContext('AppViewerContext') + $: componentInput && appContext?.onchange?.() let s3FileUploadRawMode = false let s3FilePicker: S3FilePicker | undefined = undefined - - $: componentInput && onchange?.() {#key subFieldType} diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/mainInput/RunnableSelector.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/mainInput/RunnableSelector.svelte index 3934666353..176b8c670b 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/mainInput/RunnableSelector.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/mainInput/RunnableSelector.svelte @@ -6,12 +6,13 @@ import type { Runnable, StaticAppInput } from '$lib/components/apps/inputType' import WorkspaceScriptList from './WorkspaceScriptList.svelte' import WorkspaceFlowList from './WorkspaceFlowList.svelte' - import type { AppViewerContext } from '$lib/components/apps/types' - import { createEventDispatcher, getContext } from 'svelte' + import { createEventDispatcher } from 'svelte' import type { Schema } from '$lib/common' - import { getAllScriptNames, schemaToInputsSpec } from '$lib/components/apps/utils' + import { schemaToInputsSpec } from '$lib/components/apps/utils' import { defaultIfEmptyString, emptySchema } from '$lib/utils' import { loadSchema } from '$lib/infer' + import { workspaceStore } from '$lib/stores' + import type { InlineScript } from '$lib/components/apps/types' type Tab = 'hubscripts' | 'workspacescripts' | 'workspaceflows' | 'inlinescripts' @@ -19,14 +20,15 @@ export let hideCreateScript = false export let onlyFlow = false export let rawApps = false + export let unusedInlineScripts: { name: string; inlineScript: InlineScript }[] - const { app, workspace } = getContext('AppViewerContext') + // const { app, workspace } = getContext('AppViewerContext') let tab: Tab = onlyFlow ? 'workspaceflows' - : $app?.unusedInlineScripts?.length > 0 - ? 'inlinescripts' - : 'workspacescripts' + : unusedInlineScripts?.length > 0 + ? 'inlinescripts' + : 'workspacescripts' let filter: string = '' let picker: Drawer @@ -41,7 +43,7 @@ path: string, runType: 'script' | 'flow' | 'hubscript' ): Promise<{ schema: Schema; summary: string | undefined }> { - const schema = await loadSchema(workspace, path, runType) + const schema = await loadSchema($workspaceStore!, path, runType) if (!schema.schema.order) { schema.schema.order = Object.keys(schema.schema.properties ?? {}) } @@ -88,7 +90,7 @@ type: 'runnableByPath', path, runType: 'hubscript', - schema, + schema: schema.schema, name: defaultIfEmptyString(schema.summary, path) } as const dispatch('pick', { @@ -98,10 +100,8 @@ } function pickInlineScript(name: string) { - const unusedInlineScriptIndex = $app.unusedInlineScripts?.findIndex( - (script) => script.name === name - ) - const unusedInlineScript = $app.unusedInlineScripts?.[unusedInlineScriptIndex] + const unusedInlineScriptIndex = unusedInlineScripts?.findIndex((script) => script.name === name) + const unusedInlineScript = unusedInlineScripts?.[unusedInlineScriptIndex] dispatch('pick', { runnable: { type: 'runnableByName', @@ -111,25 +111,17 @@ fields: {} }) - $app.unusedInlineScripts.splice(unusedInlineScriptIndex, 1) - $app.unusedInlineScripts = $app.unusedInlineScripts + unusedInlineScripts.splice(unusedInlineScriptIndex, 1) + unusedInlineScripts = unusedInlineScripts } function createScript() { - let index = 0 - let newScriptPath = `Inline Script ${index}` - - const names = getAllScriptNames($app) - - // Find a name that is not used by any other inline script - while (names.includes(newScriptPath)) { - newScriptPath = `Inline Script ${++index}` - } + let newScriptName = `Inline Script` dispatch('pick', { runnable: { type: 'runnableByName', - name: newScriptPath, + name: newScriptName, inlineScript: undefined }, fields: {} @@ -179,8 +171,8 @@ {#if tab == 'inlinescripts'} pickInlineScript(e.detail)} - inlineScripts={$app.unusedInlineScripts - ? $app.unusedInlineScripts.map((uis) => uis.name) + inlineScripts={unusedInlineScripts + ? unusedInlineScripts.map((uis) => uis.name) : []} /> {:else if tab == 'workspacescripts'} @@ -215,7 +207,7 @@ on:click={() => picker?.openDrawer()} size="xs" color="blue" - variant="border" + variant={rawApps ? 'contained' : 'border'} startIcon={{ icon: MousePointer }} btnClasses="truncate w-full" > diff --git a/frontend/src/lib/components/apps/inputType.ts b/frontend/src/lib/components/apps/inputType.ts index e348300628..96e051a90a 100644 --- a/frontend/src/lib/components/apps/inputType.ts +++ b/frontend/src/lib/components/apps/inputType.ts @@ -142,6 +142,8 @@ export type RunnableByName = { export type Runnable = RunnableByPath | RunnableByName | undefined +export type RunnableWithFields = Runnable & { fields?: Record } + // Runnable input, set by the developer in the component panel export type ResultInput = { runnable: Runnable diff --git a/frontend/src/lib/components/apps/store.ts b/frontend/src/lib/components/apps/store.ts index 54356aa2a5..69b064e2f9 100644 --- a/frontend/src/lib/components/apps/store.ts +++ b/frontend/src/lib/components/apps/store.ts @@ -1,7 +1,6 @@ import type { Policy } from '$lib/gen' import { writable } from 'svelte/store' -import type { App } from './types' -export const importStore = writable<{ summary: string; value: App; policy: Policy } | undefined>( +export const importStore = writable<{ summary: string; value: any; policy: Policy } | undefined>( undefined ) diff --git a/frontend/src/lib/components/apps/types.ts b/frontend/src/lib/components/apps/types.ts index e58618f99b..b000fc2326 100644 --- a/frontend/src/lib/components/apps/types.ts +++ b/frontend/src/lib/components/apps/types.ts @@ -196,6 +196,17 @@ export type ListInputs = { export type GroupContext = { id: string; context: Writable> } +export type JobById = { + job: string + component: string + result?: any + error?: any + transformer?: { result?: any; error?: string } + created_at?: number + started_at?: number + duration_ms?: number +} + export type AppViewerContext = { worldStore: Writable app: Writable @@ -227,21 +238,7 @@ export type AppViewerContext = { isEditor: boolean jobs: Writable // jobByComponent: Writable>, - jobsById: Writable< - Record< - string, - { - job: string - component: string - result?: string - error?: any - transformer?: { result?: string; error?: string } - created_at?: number - started_at?: number - duration_ms?: number - } - > - > + jobsById: Writable> noBackend: boolean errorByComponent: Writable> openDebugRun: Writable<((jobID: string) => void) | undefined> diff --git a/frontend/src/lib/components/apps/utils.ts b/frontend/src/lib/components/apps/utils.ts index cf9e218c4d..dc512b8645 100644 --- a/frontend/src/lib/components/apps/utils.ts +++ b/frontend/src/lib/components/apps/utils.ts @@ -42,9 +42,9 @@ export function allItems( subgrids: Record | undefined ): GridItem[] { if (subgrids == undefined) { - return grid + return grid ?? [] } - return [...grid, ...Object.values(subgrids).flat()] + return [...(grid ?? []), ...Object.values(subgrids).flat()] } export function schemaToInputsSpec( @@ -377,7 +377,7 @@ declare const result: any; } export function getAllScriptNames(app: App): string[] { - const names = allItems(app.grid, app?.subgrids).reduce((acc, gridItem: GridItem) => { + const names = (allItems(app.grid, app?.subgrids) ?? []).reduce((acc, gridItem: GridItem) => { const { componentInput } = gridItem.data if ( @@ -426,7 +426,7 @@ export function getAllScriptNames(app: App): string[] { return acc }, [] as string[]) - const unusedNames = app.unusedInlineScripts.map((x) => x.name) + const unusedNames = app.unusedInlineScripts?.map((x) => x.name) ?? [] const backgroundNames = app.hiddenInlineScripts?.map((x) => x.name) ?? [] return [...names, ...unusedNames, ...backgroundNames] diff --git a/frontend/src/lib/components/common/modal/Modal.svelte b/frontend/src/lib/components/common/modal/Modal.svelte index 46593e49cd..47a2fe09a0 100644 --- a/frontend/src/lib/components/common/modal/Modal.svelte +++ b/frontend/src/lib/components/common/modal/Modal.svelte @@ -4,6 +4,7 @@ import Button from '../button/Button.svelte' import Badge from '../badge/Badge.svelte' import { twMerge } from 'tailwind-merge' + import CloseButton from '../CloseButton.svelte' export let title: string export let open: boolean = false @@ -11,6 +12,7 @@ export { c as class } export let style = '' export let cancelText: string | undefined = undefined + export let kind: 'button' | 'X' = 'button' const dispatch = createEventDispatcher() @@ -69,6 +71,10 @@ )} {style} > + {#if kind == 'X'} +
(open = false)} />
+ {/if}
@@ -81,21 +87,23 @@
-
- - -
+ {cancelText ?? 'Cancel'}Escape + +
+ {/if}
diff --git a/frontend/src/lib/components/common/table/AppRow.svelte b/frontend/src/lib/components/common/table/AppRow.svelte index 4e629dc205..12d13cbf38 100644 --- a/frontend/src/lib/components/common/table/AppRow.svelte +++ b/frontend/src/lib/components/common/table/AppRow.svelte @@ -23,8 +23,7 @@ Pen, Share, Trash, - Clipboard, - Loader2 + Clipboard } from 'lucide-svelte' import { goto as gotoUrl } from '$app/navigation' import { page } from '$app/stores' @@ -55,16 +54,14 @@ {#if menuOpen} - {#await import('$lib/components/apps/editor/AppJsonEditor.svelte')} - - {:then Module} + {#await import('$lib/components/apps/editor/AppJsonEditor.svelte') then Module} {/await} {/if} {/if} + {#if app.raw_app} + +
+ + Raw +
+ {/if}
@@ -98,7 +103,7 @@ size="xs" variant="border" startIcon={{ icon: Pen }} - href="{base}/apps/edit/{app.path}?nodraft=true" + href="{base}/apps{app.raw_app ? '_raw' : ''}/edit/{app.path}?nodraft=true" > Edit @@ -110,7 +115,7 @@ size="xs" variant="border" startIcon={{ icon: GitFork }} - href="{base}/apps/add?template={app.path}" + href="{base}/apps{app.raw_app ? '_raw' : ''}/add?template={app.path}" > Fork @@ -157,7 +162,7 @@ { displayName: 'Duplicate/Fork', icon: GitFork, - href: `${base}/apps/add?template=${path}`, + href: `${base}/apps${app.raw_app ? '_raw' : ''}/add?template=${path}`, hide: $userStore?.operator }, { diff --git a/frontend/src/lib/components/copilot/autocomplete/monaco-adapter.ts b/frontend/src/lib/components/copilot/autocomplete/monaco-adapter.ts index 8d421b450f..9952199fd2 100644 --- a/frontend/src/lib/components/copilot/autocomplete/monaco-adapter.ts +++ b/frontend/src/lib/components/copilot/autocomplete/monaco-adapter.ts @@ -154,22 +154,22 @@ const MAX_PATCHES = 4 export class Autocompletor { editor: meditor.IStandaloneCodeEditor language: string - scriptLang: ScriptLang | 'bunnative' + scriptLang: ScriptLang | 'bunnative' | 'jsx' | 'tsx' | 'json' viewZoneIds: string[] = [] decorationsCollection: meditor.IEditorDecorationsCollection | undefined = undefined visualChanges: VisualChange[] = [] modifiedCode: string = '' applyZone: | { - startLineNumber: number - endLineNumber: number - } + startLineNumber: number + endLineNumber: number + } | undefined = undefined lastChangePosition: | { - lineNumber: number - column: number - } + lineNumber: number + column: number + } | undefined = undefined abortController: AbortController | undefined = undefined @@ -180,19 +180,19 @@ export class Autocompletor { predictedChange: | { - position: { - lineNumber: number - column: number - } - distance: number - } + position: { + lineNumber: number + column: number + } + distance: number + } | undefined = undefined tabWidget: meditor.IContentWidget | undefined = undefined constructor( editor: meditor.IStandaloneCodeEditor, language: string, - scriptLang: ScriptLang | 'bunnative' + scriptLang: ScriptLang | 'bunnative' | 'jsx' | 'tsx' | 'json' ) { this.editor = editor this.language = language @@ -240,18 +240,18 @@ export class Autocompletor { let closestPosition: | { - lineNumber: number - column: number - } + lineNumber: number + column: number + } | undefined = undefined let closestDistance = Infinity for (const change of this.visualChanges) { if (change.type === 'deleted') { const distance = Math.min( Math.abs(change.range.startLine - position.lineNumber) + - Math.abs(change.range.startColumn - position.column) / 10000, + Math.abs(change.range.startColumn - position.column) / 10000, Math.abs(change.range.endLine - position.lineNumber) + - Math.abs(change.range.endColumn - position.column) / 10000 + Math.abs(change.range.endColumn - position.column) / 10000 ) if (distance < closestDistance) { closestDistance = distance diff --git a/frontend/src/lib/components/copilot/autocomplete/request.ts b/frontend/src/lib/components/copilot/autocomplete/request.ts index b670f5ed06..0d6700a2b4 100644 --- a/frontend/src/lib/components/copilot/autocomplete/request.ts +++ b/frontend/src/lib/components/copilot/autocomplete/request.ts @@ -64,7 +64,7 @@ export async function autocompleteRequest( modifiableSuffix: string suffix: string language: string - scriptLang: ScriptLang | 'bunnative' + scriptLang: ScriptLang | 'bunnative' | 'jsx' | 'tsx' | 'json' events: string[] }, abortController: AbortController diff --git a/frontend/src/lib/components/copilot/chat/core.ts b/frontend/src/lib/components/copilot/chat/core.ts index ed5c968b88..ee0b67c710 100644 --- a/frontend/src/lib/components/copilot/chat/core.ts +++ b/frontend/src/lib/components/copilot/chat/core.ts @@ -93,7 +93,7 @@ export const SUPPORTED_CHAT_SCRIPT_LANGUAGES = [ ] export function getLangContext( - lang: ScriptLang | 'bunnative', + lang: ScriptLang | 'bunnative' | 'jsx' | 'tsx' | 'json', { allowResourcesFetch = false }: { allowResourcesFetch?: boolean } = {} ) { const tsContext = diff --git a/frontend/src/lib/components/flows/CreateActionsApp.svelte b/frontend/src/lib/components/flows/CreateActionsApp.svelte index c8260b0fc0..d68356e786 100644 --- a/frontend/src/lib/components/flows/CreateActionsApp.svelte +++ b/frontend/src/lib/components/flows/CreateActionsApp.svelte @@ -2,24 +2,16 @@ import { goto } from '$lib/navigation' import { base } from '$lib/base' - import { Button, FileInput } from '$lib/components/common' + import { Button } from '$lib/components/common' import Drawer from '$lib/components/common/drawer/Drawer.svelte' import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte' import { LayoutDashboard, Loader2, Plus } from 'lucide-svelte' import { importStore } from '../apps/store' - import { RawAppService } from '$lib/gen' - import { workspaceStore } from '$lib/stores' - import Path from '../Path.svelte' - import Tooltip from '../Tooltip.svelte' + import YAML from 'yaml' let drawer: Drawer | undefined = undefined - let rawAppDrawer: Drawer | undefined = undefined let pendingRaw: string = '' - let pendingCode: string = '' - let summary: string = '' - let path: string = '' - let pathError: string = '' let importType: 'yaml' | 'json' = 'yaml' @@ -28,19 +20,6 @@ await goto('/apps/add?nodraft=true') drawer?.closeDrawer?.() } - - async function importRawApp() { - await RawAppService.createRawApp({ - workspace: $workspaceStore!, - requestBody: { - path, - summary, - value: pendingCode - } - }) - await goto(`/apps/get_raw/0/${path}`) - rawAppDrawer?.closeDrawer?.() - } @@ -65,11 +44,11 @@ drawer?.toggleDrawer?.() importType = 'json' } - }, - { - label: 'Import app in React/Vue/Svelte', - onClick: () => rawAppDrawer?.toggleDrawer?.() } + // { + // label: 'Build app in React/Vue/Svelte (alpha)', + // onClick: () => goto('/apps_raw/add?nodraft=true') + // } ]} >
@@ -99,45 +78,3 @@ - - - - rawAppDrawer?.toggleDrawer?.()} - > - -

Summary

- - - -

IIFE JS code Bundle that contains an IIFE code that will mount itself to a "root" element. Any framework - or vanilla JS can be used to create an app and templates are provided for the major - frameworks: React/Vue/Svelte. In those frontend apps, it is possible to inline scripts - directly to be executed by windmill backend which makes it a convenient way of building apps - with both frontend and backend all-in-one.

- { - pendingCode = detail?.[0] - }} - /> - - - - -
-
diff --git a/frontend/src/lib/components/icons/CssIcon.svelte b/frontend/src/lib/components/icons/CssIcon.svelte new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/src/lib/components/icons/JavaScriptIcon.svelte b/frontend/src/lib/components/icons/JavaScriptIcon.svelte new file mode 100644 index 0000000000..23dce0a347 --- /dev/null +++ b/frontend/src/lib/components/icons/JavaScriptIcon.svelte @@ -0,0 +1,16 @@ + + + + + + + diff --git a/frontend/src/lib/components/icons/JsonIcon.svelte b/frontend/src/lib/components/icons/JsonIcon.svelte new file mode 100644 index 0000000000..2736d80406 --- /dev/null +++ b/frontend/src/lib/components/icons/JsonIcon.svelte @@ -0,0 +1,11 @@ + + + diff --git a/frontend/src/lib/components/icons/ReactIcon.svelte b/frontend/src/lib/components/icons/ReactIcon.svelte new file mode 100644 index 0000000000..fc286af4e0 --- /dev/null +++ b/frontend/src/lib/components/icons/ReactIcon.svelte @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + diff --git a/frontend/src/lib/components/icons/SvelteIcon.svelte b/frontend/src/lib/components/icons/SvelteIcon.svelte new file mode 100644 index 0000000000..6f6dfcc528 --- /dev/null +++ b/frontend/src/lib/components/icons/SvelteIcon.svelte @@ -0,0 +1,15 @@ + + + + + + diff --git a/frontend/src/lib/components/icons/VueIcon.svelte b/frontend/src/lib/components/icons/VueIcon.svelte new file mode 100644 index 0000000000..a04b3dd918 --- /dev/null +++ b/frontend/src/lib/components/icons/VueIcon.svelte @@ -0,0 +1,13 @@ + + + + + + + diff --git a/frontend/src/lib/components/raw_apps/FileEditorIcon.svelte b/frontend/src/lib/components/raw_apps/FileEditorIcon.svelte new file mode 100644 index 0000000000..f1419b32c7 --- /dev/null +++ b/frontend/src/lib/components/raw_apps/FileEditorIcon.svelte @@ -0,0 +1,26 @@ + + +{#if file.endsWith('.tsx')} + +{:else if file.endsWith('.json')} + +{:else if file.endsWith('.ts')} + +{:else if file.endsWith('.js')} + +{:else if file.endsWith('.vue')} + +{:else if file.endsWith('.css')} + # +{:else if file.endsWith('.svelte')} + +{/if} diff --git a/frontend/src/lib/components/raw_apps/RawAppBackgroundRunner.svelte b/frontend/src/lib/components/raw_apps/RawAppBackgroundRunner.svelte new file mode 100644 index 0000000000..c80bd2c41b --- /dev/null +++ b/frontend/src/lib/components/raw_apps/RawAppBackgroundRunner.svelte @@ -0,0 +1,90 @@ + + + diff --git a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte new file mode 100644 index 0000000000..019d442fee --- /dev/null +++ b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte @@ -0,0 +1,197 @@ + + + + + + +
+ + + + + + + + +
+ { + appPanelSize = 100 + }} + appPath={path} + bind:selectedRunnable + {runnables} + /> +
+ +
+
+ {#if appPanelSize == 100} +
+ { + appPanelSize = 70 + }} + direction="bottom" + hidden + btnClasses="border bg-surface" + /> +
+ {/if} +
diff --git a/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte b/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte new file mode 100644 index 0000000000..a7bb51be65 --- /dev/null +++ b/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte @@ -0,0 +1,1078 @@ + + + + + + +{#if appPath == ''} + + closeDraftDrawer()}> + + Choose a path to save the initial draft of the app. + +

Summary

+
+ + { + if (appPath == '' && summary?.length > 0 && !dirtyPath) { + path?.setName( + summary + .toLowerCase() + .replace(/[^a-z0-9_]/g, '_') + .replace(/-+/g, '_') + .replace(/^-|-$/g, '') + ) + } + }} + /> +
+
+ +
+ +
+ +
+ + +{/if} + + closeSaveDrawer()}> + {#if !onLatest} + + By deploying, you may overwrite changes made by other users. Press 'Deploy' to see diff. + +
+ {/if} + Summary +
+ + { + if (appPath == '' && summary?.length > 0 && !dirtyPath) { + path?.setName( + summary + .toLowerCase() + .replace(/[^a-z0-9_]/g, '_') + .replace(/-+/g, '_') + .replace(/^-|-$/g, '') + ) + } + }} + /> +
+
+ Deployment message +
+ + +
+
+ Path + + +
+ + +
+
+ {#if appPath == ''} + + Save this app once before you can publish it + + {:else} + + A viewer of the app will execute the runnables of the app on behalf of the publisher (you) + + It ensures that all required resources/runnable visible for publisher but not for viewer + at time of creating the app would prevent the execution of the app. To guarantee tight + security, a policy is computed at time of deployment of the app which only allow the + scripts/flows referred to in the app to be called on behalf of. Furthermore, static + parameters are not overridable. Hence, users will only be able to use the app as intended + by the publisher without risk for leaking resources not used in the app. + + + +
+ +

Public URL

+
+ +
+ { + policy.execution_mode = e.detail ? 'anonymous' : 'publisher' + setPublishState() + }} + /> +
+ +
+
+
Public URL
+
+ {#if secretUrl} + {@const href = `${window.location.origin}${base}/public/${$workspaceStore}/${secretUrl}`} + + {:else} + {/if} +
+ Share this url directly or embed it using an iframe (if requiring login, top-level domain + of embedding app must be the same as the one of Windmill) +
+ +
+ {#if !$enterpriseLicense} + + Custom path is an enterprise only feature. + +
+ {:else if !($userStore?.is_admin || $userStore?.is_super_admin)} + + Custom path can only be set by workspace admins + +
+ {/if} + { + customPath = detail ? '' : undefined + }} + checked={customPath !== undefined} + options={{ + right: 'Use a custom URL' + }} + disabled={!$enterpriseLicense || !($userStore?.is_admin || $userStore?.is_super_admin)} + /> + + {#if customPath !== undefined} +
+
Custom path
+
+ { + dirtyCustomPath = true + }} + /> +
+
Custom public URL
+
+ + +
{dirtyCustomPath ? customPathError : ''} +
+ {/if} +
+
+ + You will still need to deploy the app to make visible the latest changes + + + Embed this app in your own product to be used by your own users + {/if} + + + + + (historyBrowserDrawerOpen = false)}> + + + + + { + jobs = [] + }} + on:clearErrors={() => { + console.log('todo clear errors') + }} + {jobs} + hasErrors={false} + {jobsById} + errorByComponent={{}} +/> + +
+
+ +
+ +
+ {#if newPath || newEditedPath} +
+
+ +
+ { + currentTarget.select() + }} + /> +
+ {/if} +
+ {#if $enterpriseLicense && appPath != ''} + + {/if} +
+ + + + + + + + + + +
+
diff --git a/frontend/src/lib/components/raw_apps/RawAppInlineScriptEditor.svelte b/frontend/src/lib/components/raw_apps/RawAppInlineScriptEditor.svelte new file mode 100644 index 0000000000..7754e965e3 --- /dev/null +++ b/frontend/src/lib/components/raw_apps/RawAppInlineScriptEditor.svelte @@ -0,0 +1,219 @@ + + +{#if inlineScript} + {#if inlineScript.language != 'frontend'} + { + dispatch('createScriptFromInlineScript') + drawerIsOpen = false + }} + /> + {/if} +
+
+ {#if name !== undefined} +
+ { + // $app = $app + // if (stateId) { + // $stateId++ + // } + }} + /> +
+
+ {/if} +
+ {#if inlineScript} + + {/if} + { + acc[key] = obj.type === 'static' ? obj.value : undefined + return acc + }, {})} + /> + + + + +
+
+ + + +
+ {#if !drawerIsOpen && inlineScript.language != 'frontend'} + onRun()} + on:change={async (e) => { + if (inlineScript && inlineScript.language != 'frontend') { + if (inlineScript.lock != undefined) { + inlineScript.lock = undefined + } + const oldSchema = JSON.stringify(inlineScript.schema) + if (inlineScript.schema == undefined) { + inlineScript.schema = emptySchema() + } + await inferInlineScriptSchema(inlineScript?.language, e.detail, inlineScript.schema) + if (JSON.stringify(inlineScript.schema) != oldSchema) { + inlineScript = inlineScript + syncFields() + } + } + // $app = $app + }} + args={Object.entries(fields ?? {}).reduce((acc, [key, obj]) => { + acc[key] = obj.type === 'static' ? obj.value : undefined + return acc + }, {})} + /> + + + {/if} +
+
+{/if} diff --git a/frontend/src/lib/components/raw_apps/RawAppInlineScriptPanelList.svelte b/frontend/src/lib/components/raw_apps/RawAppInlineScriptPanelList.svelte new file mode 100644 index 0000000000..8bece2ea3a --- /dev/null +++ b/frontend/src/lib/components/raw_apps/RawAppInlineScriptPanelList.svelte @@ -0,0 +1,90 @@ + + + + +
+ { + dispatch('hidePanel') + }} + /> + + +
+
+
+
+
+ {#if Object.keys($runnables ?? {}).length > 0} + {#each Object.entries($runnables ?? {}) as [id, runnable]} + {#if runnable} + + {/if} + {/each} + {:else} +
No backend runnable
+ {/if} +
+
+
+
+ + diff --git a/frontend/src/lib/components/raw_apps/RawAppInlineScriptRunnable.svelte b/frontend/src/lib/components/raw_apps/RawAppInlineScriptRunnable.svelte new file mode 100644 index 0000000000..1c96d7b9c9 --- /dev/null +++ b/frontend/src/lib/components/raw_apps/RawAppInlineScriptRunnable.svelte @@ -0,0 +1,214 @@ + + + +{#if runnable?.type == 'runnableByPath' || (runnable?.type == 'runnableByName' && runnable.inlineScript)} + + + {#if runnable?.type === 'runnableByName' && runnable.inlineScript} + {#if runnable.inlineScript.language == 'frontend'} +
Frontend scripts not supported for raw apps
+ {:else} + + dispatch('createScriptFromInlineScript', runnable)} + {id} + bind:inlineScript={runnable.inlineScript} + bind:name={runnable.name} + bind:fields={runnable.fields} + isLoading={testIsLoading} + onRun={testPreview} + onCancel={async () => { + if (testJobLoader) { + await testJobLoader.cancelJob() + } + }} + on:delete + path={appPath} + /> + {/if} + {:else if runnable?.type == 'runnableByPath'} + fork(e.detail)} + on:delete + {id} + isLoading={testIsLoading} + onRun={testPreview} + onCancel={async () => { + if (testJobLoader) { + await testJobLoader.cancelJob() + } + }} + /> + {/if} +
+ + + Inputs + Test + + {#if selectedTab == 'inputs'} + {#if runnable?.fields} +
+ {#each Object.keys(runnable.fields) as k} + {@const meta = runnable.fields[k]} + + {/each} +
+ {:else} +
No inputs
+ {/if} + {:else if selectedTab == 'test'} + + + +
+ v.type == 'static') + .map(([k]) => k)} + schema={getSchema(runnable)} + bind:args + shouldCapitalize + /> +
+
+ + + +
+
+ {/if} +
+
+
+
+{:else} + onPick(e.detail)} + on:delete + showScriptPicker + on:new={(e) => { + runnable = { + type: 'runnableByName', + inlineScript: e.detail, + name: runnable?.name ?? 'Background Runnable' + } + }} + /> +{/if} diff --git a/frontend/src/lib/components/raw_apps/RawAppInlineScriptsPanel.svelte b/frontend/src/lib/components/raw_apps/RawAppInlineScriptsPanel.svelte new file mode 100644 index 0000000000..b3c00b5cc6 --- /dev/null +++ b/frontend/src/lib/components/raw_apps/RawAppInlineScriptsPanel.svelte @@ -0,0 +1,61 @@ + + + + + + + + {#if !selectedRunnable} +
+ Select a runnable on the left panel +
+ {:else if $runnables?.[selectedRunnable]} + {#key selectedRunnable} + { + createScriptFromInlineScript( + selectedRunnable ?? '', + e.detail, + $workspaceStore ?? '', + appPath + ) + }} + on:delete={() => { + runnables.update((runnables) => { + if (selectedRunnable) { + delete runnables[selectedRunnable] + } + selectedRunnable = undefined + return { ...runnables } + }) + }} + id={selectedRunnable} + bind:runnable={$runnables[selectedRunnable]} + />{/key} + {:else} +
+ No runnable at id {selectedRunnable} +
+ {/if} +
+
diff --git a/frontend/src/lib/components/raw_apps/RawAppInputsSpecEditor.svelte b/frontend/src/lib/components/raw_apps/RawAppInputsSpecEditor.svelte new file mode 100644 index 0000000000..7160917bcb --- /dev/null +++ b/frontend/src/lib/components/raw_apps/RawAppInputsSpecEditor.svelte @@ -0,0 +1,113 @@ + + +{#if !(resourceOnly && (fieldType !== 'object' || !format?.startsWith('resource-')))} +
+
+
+
+ + {customTitle + ? customTitle + : shouldCapitalize + ? capitalize(addWhitespaceBeforeCapitals(key)) + : key} + + {#if loading} + + {/if} + {#if tooltip || markdownTooltip} + + {tooltip} + + {/if} +
+ {#if displayType} +
+ {fieldType === 'array' && subFieldType + ? `${fieldTypeToTsType(subFieldType)}[]` + : fieldTypeToTsType(fieldType)} +
+ {/if} +
+ +
+ {#if componentInput?.type && allowTypeChange !== false} + + + + + {/if} +
+
+ + {#if componentInput?.type === 'static'} +
+ +
+ {:else if componentInput?.type === 'user' || componentInput?.type == undefined} + Field's value is a frontend input + {/if} + {#if componentInput?.type === 'user' && ((fieldType == 'object' && format?.startsWith('resource-') && format !== 'resource-s3_object') || fieldType == 'resource')} +
+ + Apps are executed on behalf of publishers. If you want to accept resources from user, you + need to enable this (potentially dangerous!) +
+ {/if} +
+{/if} diff --git a/frontend/src/lib/components/raw_apps/RawAppPreview.svelte b/frontend/src/lib/components/raw_apps/RawAppPreview.svelte new file mode 100644 index 0000000000..4957a520ba --- /dev/null +++ b/frontend/src/lib/components/raw_apps/RawAppPreview.svelte @@ -0,0 +1,23 @@ + + + + +