ui code builder v0 (at secret path) (#4964)

This commit is contained in:
Ruben Fiszel
2025-04-20 01:19:02 +02:00
committed by GitHub
parent e88909aa93
commit 3c68fef2ab
106 changed files with 6029 additions and 28628 deletions
@@ -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"
}
@@ -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"
}
@@ -0,0 +1,2 @@
-- Add down migration script here
ALTER TABLE app_version DROP COLUMN IF EXISTS raw_app;
@@ -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;
@@ -49,8 +49,12 @@ impl Visit for ImportsFinder {
pub fn parse_expr_for_imports(code: &str) -> anyhow::Result<Vec<String>> {
let cm: Lrc<SourceMap> = 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),
+94
View File
@@ -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
+248 -53
View File
@@ -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<String>,
#[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<Response> {
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<UserDB>,
// Path((w_id, path)): Path<(String, StripPath)>,
// ) -> JsonResult<i64> {
// 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<UserDB>,
@@ -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<UserDB>,
Extension(db): Extension<DB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
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<UserDB>,
@@ -755,17 +894,36 @@ async fn create_app(
Extension(db): Extension<DB>,
Extension(webhook): Extension<WebhookShared>,
Path(w_id): Path<String>,
Json(mut app): Json<CreateApp>,
Json(app): Json<CreateApp>,
) -> 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<sqlx::Postgres>,
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<String, Box<serde_json::value::RawValue>> = 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<DB>) -> impl IntoResponse {
@@ -1017,12 +1162,76 @@ async fn update_app(
Path((w_id, path)): Path<(String, StripPath)>,
Json(ns): Json<EditApp>,
) -> Result<String> {
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<UserDB>,
Extension(db): Extension<DB>,
Extension(webhook): Extension<WebhookShared>,
Path((w_id, path)): Path<(String, StripPath)>,
multipart: Multipart,
) -> Result<String> {
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<sqlx::Postgres>,
// user_db: UserDB,
// w_id: &String,
// app: &mut CreateApp,
// )
async fn update_app_internal<'a>(
authed: ApiAuthed,
db: sqlx::Pool<sqlx::Postgres>,
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<String, Box<serde_json::value::RawValue>> = 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)]
@@ -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)
+7
View File
@@ -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<bool>,
pub with_deployment_msg: Option<bool>,
}
#[derive(Deserialize)]
pub struct RawAppValue {
pub files: HashMap<String, String>,
}
@@ -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)
}
+1 -2
View File
@@ -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};
+1
View File
@@ -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()
@@ -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<CanceledBy>,
// job_dir: &str,
// db: &sqlx::Pool<sqlx::Postgres>,
// 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<String, String> = 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::<Vec<_>>();
// 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,
+3 -1
View File
@@ -9,4 +9,6 @@ tests-out/
storageState.json
.env.production
dist/
static/tsdocs/
static/tsdocs/
static/ui_builder/
ui_builder.tar.gz
+139 -267
View File
@@ -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": {
+3 -1
View File
@@ -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",
+48
View File
@@ -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)
+41 -26
View File
@@ -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<string[]> {
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))
+5 -2
View File
@@ -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,
@@ -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'
@@ -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<string, any> = {}
export let shouldHideNoInputs: boolean = false
export let noVariablePicker = false
@@ -439,7 +439,7 @@
</div>
{#if opened === argName}
<div class="p-4 border-t">
{#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]}
<PropertyEditor
bind:description={schema.properties[argName].description}
+240 -137
View File
@@ -7,22 +7,6 @@
languages.typescript.typescriptDefaults.addExtraLib(processStdContent, 'process.d.ts')
// languages.typescript.javascriptDefaults.setModeConfiguration({
// completionItems: true,
// hovers: true,
// documentSymbols: true,
// definitions: true,
// references: true,
// documentHighlights: true,
// rename: true,
// diagnostics: true,
// documentRangeFormattingEdits: true,
// signatureHelp: true,
// onTypeFormattingEdits: true,
// codeActions: true,
// inlayHints: true
// })
languages.typescript.typescriptDefaults.setModeConfiguration({
completionItems: true,
hovers: true,
@@ -52,8 +36,9 @@
languages.typescript.typescriptDefaults.setDiagnosticsOptions({
noSemanticValidation: false,
noSyntaxValidation: false,
noSuggestionDiagnostics: false,
diagnosticCodesToIgnore: [1108]
diagnosticCodesToIgnore: [1108, 7006, 7034, 7019, 7005]
})
languages.typescript.typescriptDefaults.setCompilerOptions({
@@ -80,23 +65,10 @@
strict: true,
noLib: false,
allowImportingTsExtensions: true,
moduleResolution: languages.typescript.ModuleResolutionKind.NodeJs
allowSyntheticDefaultImports: true,
moduleResolution: languages.typescript.ModuleResolutionKind.NodeJs,
jsx: languages.typescript.JsxEmit.React
})
// languages.typescript.javascriptDefaults.setCompilerOptions({
// target: languages.typescript.ScriptTarget.Latest,
// allowNonTsExtensions: true,
// noSemanticValidation: false,
// noSyntaxValidation: false,
// allowImportingTsExtensions: true,
// checkJs: true,
// allowJs: true,
// noUnusedParameters: true,
// noUnusedLocals: true,
// strict: true,
// noLib: true,
// moduleResolution: languages.typescript.ModuleResolutionKind.NodeJs
// })
</script>
<script lang="ts">
@@ -120,6 +92,8 @@
import { toSocket, WebSocketMessageReader, WebSocketMessageWriter } from 'vscode-ws-jsonrpc'
import { CloseAction, ErrorAction, RequestType } from 'vscode-languageclient'
import type { DocumentUri, MessageTransports } from 'vscode-languageclient'
import { MonacoBinding } from 'y-monaco'
import {
dbSchemas,
@@ -135,16 +109,18 @@
createHash as randomHash,
editorConfig,
langToExt,
updateOptions
updateOptions,
extToLang
} from '$lib/editorUtils'
import type { Disposable } from 'vscode'
import type { DocumentUri, MessageTransports } from 'vscode-languageclient'
import { workspaceStore } from '$lib/stores'
import { type Preview, ResourceService, UserService } from '$lib/gen'
import type { Text } from 'yjs'
import { initializeVscode } from '$lib/components/vscode'
import { initializeMode } from 'monaco-graphql/esm/initializeMode.js'
import type { MonacoGraphQLAPI } from 'monaco-graphql/esm/api.js'
import {
editor as meditor,
languages,
@@ -154,7 +130,6 @@
type IRange,
type IDisposable
} from 'monaco-editor'
import type { MonacoGraphQLAPI } from 'monaco-graphql/esm/api.js'
import EditorTheme from './EditorTheme.svelte'
import {
@@ -165,11 +140,16 @@
POSTGRES_TYPES,
SNOWFLAKE_TYPES
} from '$lib/consts'
import { setupTypeAcquisition } from '$lib/ata/index'
import { setupTypeAcquisition, type DepsToGet } from '$lib/ata/index'
import { initWasmTs } from '$lib/infer'
import { initVim } from './monaco_keybindings'
import { buildWorkerDefinition } from '$lib/monaco_workers/build_workers'
import { parseTypescriptDeps } from '$lib/relative_imports'
import { scriptLangToEditorLang } from '$lib/scripts'
import * as htmllang from '$lib/svelteMonarch'
import { conf, language } from '$lib/vueMonarch'
import { Autocompletor } from './copilot/autocomplete/monaco-adapter'
import { AIChatEditorHandler } from './copilot/chat/monaco-adapter'
import GlobalReviewButtons from './copilot/chat/GlobalReviewButtons.svelte'
@@ -181,22 +161,6 @@
let divEl: HTMLDivElement | null = null
let editor: meditor.IStandaloneCodeEditor | null = null
export let lang:
| 'typescript'
| 'python'
| 'go'
| 'shell'
| 'sql'
| 'graphql'
| 'powershell'
| 'php'
| 'css'
| 'javascript'
| 'rust'
| 'yaml'
| 'csharp'
| 'nu'
| 'java'
// for related places search: ADD_NEW_LANG
export let code: string = ''
export let cmdEnterAction: (() => void) | undefined = undefined
@@ -218,22 +182,20 @@
export let args: Record<string, any> | undefined = undefined
export let useWebsockets: boolean = true
export let small = false
export let scriptLang: Preview['language'] | 'bunnative'
export let scriptLang: Preview['language'] | 'bunnative' | 'tsx' | 'jsx' | 'json' | undefined
export let disabled: boolean = false
export let lineNumbersMinChars = 3
export let files: Record<string, { code: string; readonly?: boolean }> | undefined = {}
export let extraLib: string | undefined = undefined
export let changeTimeout: number = 500
export let isAiPanelOpen: boolean = false
export let loadAsync = false
const rHash = randomHash()
$: filePath = computePath(path)
let lang = scriptLangToEditorLang(scriptLang)
$: lang = scriptLangToEditorLang(scriptLang)
function computePath(path: string | undefined): string {
if (path == '' || path == undefined || path.startsWith('/')) {
return rHash
} else {
return path as string
}
}
let filePath = computePath(path)
$: filePath = computePath(path)
let initialPath: string | undefined = path
@@ -253,15 +215,59 @@
let dbSchema: DBSchema | undefined = undefined
let destroyed = false
const uri =
lang != 'go' && lang != 'typescript' && lang != 'python' && lang != 'nu'
? `file:///${filePath ?? rHash}.${langToExt(lang)}`
: `file:///tmp/monaco/${randomHash()}.${langToExt(lang)}`
const uri = computeUri(filePath, scriptLang)
console.log('uri', uri)
buildWorkerDefinition()
function computeUri(filePath: string, scriptLang: string | undefined) {
let file
if (filePath.includes('.')) {
file = filePath
} else {
file = `${filePath}.${scriptLang == 'tsx' ? 'tsx' : langToExt(lang)}`
}
if (file.startsWith('/')) {
file = file.slice(1)
}
return !['deno', 'go', 'python3'].includes(scriptLang ?? '')
? `file:///${file}`
: `file:///tmp/monaco/${file}`
}
function computePath(path: string | undefined): string {
if (
['deno', 'go', 'python3'].includes(scriptLang ?? '') ||
path == '' ||
path == undefined //||path.startsWith('/')
) {
return randomHash()
} else {
console.log('path', path)
return path as string
}
}
export function switchToFile(path: string, value: string, lang: string) {
if (editor) {
const uri = mUri.parse(path)
console.log('switching to file', path, lang)
// vscode.workspace.fs.writeFile(uri, new TextEncoder().encode(value))
let nmodel = meditor.getModel(uri)
if (nmodel) {
console.log('using existing model', path)
editor.setModel(nmodel)
} else {
console.log('creating model', path)
nmodel = meditor.createModel(value, lang, uri)
editor.setModel(nmodel)
}
model = nmodel
setTypescriptExtraLibs()
}
}
export function getCode(): string {
return editor?.getValue() ?? ''
}
@@ -718,9 +724,21 @@
}
export async function reloadWebsocket() {
console.log('reloadWebsocket')
await closeWebsockets()
if (
!useWebsockets ||
!(
(lang == 'typescript' && scriptLang === 'deno') ||
lang == 'python' ||
lang == 'go' ||
lang == 'shell'
)
) {
return
}
console.log('reloadWebsocket')
function createLanguageClient(
transports: MessageTransports,
name: string,
@@ -735,6 +753,7 @@
documentSelector: [lang],
errorHandler: {
error: () => ({ action: ErrorAction.Continue }),
closed: () => ({
action: CloseAction.Restart
})
@@ -866,11 +885,6 @@
const hostname = getHostname()
let encodedImportMap = ''
// if (lang == 'typescript') {
// let worker = await languages.typescript.getTypeScriptWorker()
// console.log(worker)
// }
if (useWebsockets) {
if (lang == 'typescript' && scriptLang === 'deno') {
@@ -1035,7 +1049,8 @@
!websocketAlive.go &&
!websocketAlive.shellcheck &&
!websocketAlive.ruff &&
scriptLang != 'bun'
scriptLang != 'bun' &&
scriptLang != 'tsx'
) {
console.log('reconnecting to language servers')
lastWsAttempt = new Date()
@@ -1110,7 +1125,7 @@
}
let initialized = false
let ata: ((s: string) => void) | undefined = undefined
let ata: ((s: string | DepsToGet) => void) | undefined = undefined
let statusDiv: Element | null = null
@@ -1132,16 +1147,93 @@
}
}
$: files && model && onFileChanges()
let svelteRegistered = false
let vueRegistered = false
function onFileChanges() {
if (files && Object.keys(files).find((x) => x.endsWith('.svelte')) != undefined) {
if (!svelteRegistered) {
svelteRegistered = true
languages.register({
id: 'svelte',
extensions: ['.svelte'],
aliases: ['Svelte', 'svelte'],
mimetypes: ['application/svelte']
})
languages.setLanguageConfiguration('svelte', htmllang.conf as any)
languages.setMonarchTokensProvider('svelte', htmllang.language as any)
}
}
if (files && Object.keys(files).find((x) => x.endsWith('.vue')) != undefined) {
if (!vueRegistered) {
vueRegistered = true
languages.register({
id: 'vue',
extensions: ['.vue'],
aliases: ['Vue', 'Vue'],
mimetypes: ['application/svelte']
})
languages.setLanguageConfiguration('vue', conf as any)
languages.setMonarchTokensProvider('vue', language as any)
}
}
if (files && model) {
for (const [path, { code, readonly }] of Object.entries(files)) {
const luri = mUri.file(path)
if (luri.toString() != model.uri.toString()) {
let nmodel = meditor.getModel(luri)
if (nmodel == undefined) {
const lmodel = meditor.createModel(code, extToLang(path?.split('.')?.pop()!), luri)
if (readonly) {
lmodel.onDidChangeContent((evt) => {
// This will effectively undo any new edits
if (lmodel.getValue() != code && code) {
lmodel.setValue(code)
}
})
}
} else {
const lmodel = meditor.getModel(luri)
if (lmodel && code) {
lmodel.setValue(code)
}
}
}
}
}
}
let timeoutModel: NodeJS.Timeout | undefined = undefined
async function loadMonaco() {
console.log('path', uri)
try {
console.log("Loading Monaco's language client")
await initializeVscode('editor')
await initializeVscode('editor', divEl!)
console.log('done loading Monaco and vscode')
} catch (e) {
console.log('error initializing services', e)
}
// vscode.languages.registerDefinitionProvider('*', {
// provideDefinition(document, position, token) {
// // Get the word under the cursor (this will be the import or function being clicked)
// const wordRange = document.getWordRangeAtPosition(position)
// const word = document.getText(wordRange)
// // Do something with the word (for example, log it or handle it)
// console.log('Clicked on import or symbol:', word)
// // Optionally, you can also return a definition location
// return null // If you don't want to override the default behavior
// }
// })
// console.log('bef ready')
// console.log('af ready')
@@ -1159,6 +1251,8 @@
}
model.updateOptions(lang == 'python' ? { tabSize: 4, insertSpaces: true } : updateOptions)
onFileChanges()
editor = meditor.create(divEl as HTMLDivElement, {
...editorConfig(code, lang, automaticLayout, fixedOverflowWidgets),
model,
@@ -1178,12 +1272,14 @@
timeoutModel = setTimeout(() => {
let ncode = getCode()
code = ncode
dispatch('change', code)
}, 500)
dispatch('change', ncode)
}, changeTimeout)
ataModel && clearTimeout(ataModel)
ataModel = setTimeout(() => {
ata?.(getCode())
if (scriptLang == 'bun') {
ata?.(getCode())
}
}, 1000)
})
@@ -1240,10 +1336,8 @@
!websocketAlive.ruff &&
!websocketAlive.shellcheck &&
!websocketAlive.go &&
!websocketInterval &&
scriptLang != 'bun'
!websocketInterval
) {
console.log('reconnecting to language servers on focus')
reloadWebsocket()
}
})
@@ -1267,6 +1361,10 @@
}
}
export async function fetchPackageDeps(deps: DepsToGet) {
ata?.(deps)
}
async function setTypescriptRTNamespace() {
if (
scriptLang &&
@@ -1289,73 +1387,78 @@
}
async function setTypescriptExtraLibs() {
if (lang === 'typescript' && scriptLang != 'deno') {
if (extraLib) {
const uri = mUri.parse('file:///extraLib.d.ts')
languages.typescript.typescriptDefaults.addExtraLib(extraLib, uri.toString())
}
if (lang === 'typescript' && (scriptLang == 'bun' || scriptLang == 'tsx') && ata == undefined) {
const hostname = getHostname()
if (scriptLang == 'bun' && ata == undefined) {
const addLibraryToRuntime = async (code: string, _path: string) => {
const path = 'file://' + _path
let uri = mUri.parse(path)
console.log('adding library to runtime', path)
languages.typescript.typescriptDefaults.addExtraLib(code, path)
try {
await vscode.workspace.fs.writeFile(uri, new TextEncoder().encode(code))
} catch (e) {
console.log('error writing file', e)
}
const addLibraryToRuntime = async (code: string, _path: string) => {
const path = 'file://' + _path
let uri = mUri.parse(path)
console.log('adding library to runtime', path)
languages.typescript.typescriptDefaults.addExtraLib(code, path)
try {
await vscode.workspace.fs.writeFile(uri, new TextEncoder().encode(code))
} catch (e) {
console.log('error writing file', e)
}
}
const addLocalFile = async (code: string, _path: string) => {
let p = new URL(_path, uri).href
// if (_path?.startsWith('/')) {
// p = 'file://' + p
// }
let nuri = mUri.parse(p)
console.log('adding local file', _path, nuri.toString())
if (editor) {
let localModel = meditor.getModel(nuri)
if (localModel) {
localModel.setValue(code)
} else {
meditor.createModel(code, 'typescript', nuri)
}
try {
if (model) {
model?.setValue(model.getValue())
}
} catch (e) {
console.log('error resetting model', e)
const addLocalFile = async (code: string, _path: string) => {
let p = new URL(_path, uri).href
// if (_path?.startsWith('/')) {
// p = 'file://' + p
// }
let nuri = mUri.parse(p)
console.log('adding local file', _path, nuri.toString())
if (editor) {
let localModel = meditor.getModel(nuri)
if (localModel) {
localModel.setValue(code)
} else {
meditor.createModel(code, 'typescript', nuri)
}
try {
if (model) {
model?.setValue(model.getValue())
}
} catch (e) {
console.log('error resetting model', e)
}
}
await initWasmTs()
const root = await genRoot(hostname)
console.log('SETUP TYPE ACQUISITION', { root, path })
ata = setupTypeAcquisition({
projectName: 'Windmill',
depsParser: (c) => {
return parseTypescriptDeps(c)
}
await initWasmTs()
const root = await genRoot(hostname)
console.log('SETUP TYPE ACQUISITION', { root, path })
ata = setupTypeAcquisition({
projectName: 'Windmill',
depsParser: (c) => {
return parseTypescriptDeps(c)
},
root,
scriptPath: path,
logger: console,
delegate: {
receivedFile: addLibraryToRuntime,
localFile: addLocalFile,
progress: (downloaded: number, total: number) => {
// console.log({ dl, ttl })
},
root,
scriptPath: path,
logger: console,
delegate: {
receivedFile: addLibraryToRuntime,
localFile: addLocalFile,
progress: (downloaded: number, total: number) => {
// console.log({ dl, ttl })
},
started: () => {
console.log('ATA start')
},
finished: (f) => {
console.log('ATA done')
}
started: () => {
console.log('ATA start')
},
finished: (f) => {
console.log('ATA done')
}
})
}
})
if (scriptLang == 'bun') {
ata?.('import "bun-types"')
ata?.(code)
}
dispatch('ataReady')
}
}
@@ -361,7 +361,7 @@
<Module.default
disabled={!$enterpriseLicense || !isSlackHandler(handlerPath)}
schema={slackHandlerSchema}
schemaSkippedValues={['slack']}
hiddenArgs={['slack']}
schemaFieldTooltip={{
channel: 'Slack channel name without the "#" - example: "windmill-alerts"'
}}
@@ -2,7 +2,7 @@
import { BROWSER } from 'esm-env'
import { editor as meditor } from 'monaco-editor'
import '@codingame/monaco-vscode-standalone-languages'
// import '@codingame/monaco-vscode-standalone-languages'
import { onDestroy, onMount } from 'svelte'
@@ -1,11 +1,12 @@
<script lang="ts">
import type { Schema } from '$lib/common'
import { ScriptService, type FlowModule, type Job, JobService } from '$lib/gen'
import { ScriptService, type FlowModule, type Job } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { getScriptByPath } from '$lib/scripts'
import { CornerDownLeft, Loader2 } from 'lucide-svelte'
import { getContext } from 'svelte'
import Button from './common/button/Button.svelte'
import type { FlowEditorContext } from './flows/types'
@@ -73,14 +74,7 @@
val.hash ?? script.hash
)
} else if (val.type == 'flow') {
await testJobLoader?.abstractRun(() =>
JobService.runFlowByPath({
workspace: $workspaceStore!,
path: val.path,
requestBody: args,
skipPreprocessor: true
})
)
await testJobLoader?.runFlowByPath(val.path, args)
} else {
throw Error('Not supported module type')
}
@@ -0,0 +1,33 @@
<script lang="ts">
import { Button } from '$lib/components/common'
import { CornerDownLeft, Loader2 } from 'lucide-svelte'
export let isLoading
export let hideShortcut = false
export let onRun: () => Promise<void>
export let onCancel: () => Promise<void>
</script>
{#if !isLoading}
<Button
loading={isLoading}
size="sm"
color="dark"
btnClasses="!px-2 !py-1"
on:click={() => onRun()}
shortCut={{ Icon: CornerDownLeft, hide: hideShortcut }}
>
Run
</Button>
{:else}
<Button
size="sm"
color="red"
variant="border"
btnClasses="!px-2 !py-1"
on:click={() => onCancel()}
>
<Loader2 size={14} class="animate-spin mr-2" />
Cancel
</Button>
{/if}
@@ -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<string, string> = {}
export let args: Record<string, any> = {}
export let disabledArgs: string[] = []
@@ -204,7 +204,7 @@
: ''}
>
<!-- svelte-ignore a11y-click-events-have-key-events -->
{#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}
<div class="px-2">
@@ -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}
@@ -535,7 +535,6 @@
dispatch('format')
}}
class="flex flex-1 h-full !overflow-visible"
lang={scriptLangToEditorLang(lang)}
scriptLang={lang}
automaticLayout={true}
{fixedOverflowWidgets}
@@ -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'
@@ -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'
@@ -748,7 +748,6 @@
disabled={!$superadmin}
class="flex flex-1 grow h-full w-full"
automaticLayout
lang="shell"
scriptLang={'bash'}
useWebsockets={false}
fixedOverflowWidgets={false}
@@ -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<any>
@@ -48,6 +48,7 @@
jobId?: Output<any> | 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<string, any> | 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 @@
<slot />
</div>
{/if}
{#if render && !initializing && autoRefresh === true && !hideRefreshButton}
{#if render && autoRefresh === true && !hideRefreshButton}
<div class="flex absolute top-1 right-1 z-50 app-component-refresh-btn">
<RefreshButton {loading} {id} />
</div>
@@ -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
}
@@ -1,15 +1,10 @@
<script lang="ts">
import { Alert, Badge, Drawer, DrawerContent, Tab, Tabs, UndoRedo } from '$lib/components/common'
import { Alert, Drawer, DrawerContent, UndoRedo } from '$lib/components/common'
import Button from '$lib/components/common/button/Button.svelte'
import DisplayResult from '$lib/components/DisplayResult.svelte'
import FlowProgressBar from '$lib/components/flows/FlowProgressBar.svelte'
import FlowStatusViewer from '$lib/components/FlowStatusViewer.svelte'
import JobArgs from '$lib/components/JobArgs.svelte'
import LogViewer from '$lib/components/LogViewer.svelte'
import Path from '$lib/components/Path.svelte'
import TestJobLoader from '$lib/components/TestJobLoader.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import { AppService, DraftService, type Job, type Policy } from '$lib/gen'
import { AppService, DraftService, type Policy } from '$lib/gen'
import { redo, undo } from '$lib/history'
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
import {
@@ -24,7 +19,6 @@
History,
Laptop2,
Loader2,
RefreshCw,
Save,
Smartphone,
FileClock,
@@ -34,47 +28,32 @@
Zap
} from 'lucide-svelte'
import { createEventDispatcher, getContext } from 'svelte'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import {
classNames,
cleanValueProperties,
truncateRev,
orderedJsonStringify,
type Value,
replaceFalseWithUndefined,
isFlowPreview
replaceFalseWithUndefined
} from '../../../utils'
import type {
AppInput,
ConnectedAppInput,
RowAppInput,
Runnable,
StaticAppInput,
UserAppInput
} from '../inputType'
import type { AppInput, Runnable } from '../inputType'
import type { App, AppEditorContext, AppViewerContext } from '../types'
import { BG_PREFIX, allItems, toStatic } from '../utils'
import AppExportButton from './AppExportButton.svelte'
import AppInputs from './AppInputs.svelte'
import type { AppComponent } from './component/components'
import PanelSection from './settingsPanel/common/PanelSection.svelte'
import PreviewToggle from './PreviewToggle.svelte'
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import { Sha256 } from '@aws-crypto/sha256-js'
import { sendUserToast } from '$lib/toast'
import DeploymentHistory from './DeploymentHistory.svelte'
import Awareness from '$lib/components/Awareness.svelte'
import { secondaryMenuLeftStore, secondaryMenuRightStore } from './settingsPanel/secondaryMenu'
import Dropdown from '$lib/components/DropdownV2.svelte'
import AppEditorTutorial from './AppEditorTutorial.svelte'
import AppTimeline from './AppTimeline.svelte'
import type DiffDrawer from '$lib/components/DiffDrawer.svelte'
import AppReportsDrawer from './AppReportsDrawer.svelte'
import HighlightCode from '$lib/components/HighlightCode.svelte'
import { type ColumnDef, getPrimaryKeys } from '../components/display/dbtable/utils'
import DebugPanel from './contextPanel/DebugPanel.svelte'
import { getCountInput } from '../components/display/dbtable/queries/count'
@@ -94,7 +73,10 @@
import { isCloudHosted } from '$lib/cloud'
import { base } from '$lib/base'
import ClipboardPanel from '$lib/components/details/ClipboardPanel.svelte'
import AppJobsDrawer from './AppJobsDrawer.svelte'
import { collectStaticFields, type TriggerableV2 } from './commonAppUtils'
import LazyModePanel from './contextPanel/LazyModePanel.svelte'
import { Sha256 } from '@aws-crypto/sha256-js'
async function hash(message) {
try {
@@ -169,6 +151,7 @@
selectedJobId = jobId
}
}
let selectedJobId: string | undefined = undefined
let pathError: string | undefined = undefined
let appExport: AppExportButton
@@ -189,24 +172,6 @@
draftDrawerOpen = false
}
function collectStaticFields(
fields: Record<string, StaticAppInput | ConnectedAppInput | RowAppInput | UserAppInput>
) {
return Object.fromEntries(
Object.entries(fields ?? {})
.filter(([k, v]) => v.type == 'static')
.map(([k, v]) => {
return [k, v['value']]
})
)
}
type TriggerableV2 = {
static_inputs: Record<string, any>
one_of_inputs?: Record<string, any[] | undefined>
allow_user_resources?: string[]
}
async function computeTriggerables() {
const items = allItems($app.grid, $app.subgrids)
@@ -734,17 +699,6 @@
$: saveDrawerOpen && compareVersions()
let selectedJobId: string | undefined = undefined
let testJobLoader: TestJobLoader
let job: Job | undefined = undefined
let testIsLoading = false
$: selectedJobId && !selectedJobId?.includes('Frontend') && testJobLoader?.watchJob(selectedJobId)
$: if (selectedJobId?.includes('Frontend') && selectedJobId) {
job = undefined
}
$: hasErrors = Object.keys($errorByComponent).length > 0
let lock = false
@@ -902,8 +856,6 @@
appEditorTutorial?.toggleTutorial()
}
let rightColumnSelect: 'timeline' | 'detail' = 'timeline'
let appReportingDrawerOpen = false
export function openTroubleshootPanel() {
@@ -961,8 +913,6 @@
<svelte:window on:keydown={onKeyDown} />
<TestJobLoader bind:this={testJobLoader} bind:isLoading={testIsLoading} bind:job />
{#if $$slots.unsavedConfirmationModal}
<slot
name="unsavedConfirmationModal"
@@ -1000,7 +950,7 @@
{#if $appPath == ''}
<Drawer bind:open={draftDrawerOpen} size="800px">
<DrawerContent title="Initial draft save" on:close={() => closeDraftDrawer()}>
<Alert title="Require path" type="info">
<Alert bgClass="mb-4" title="Require path" type="info">
Choose a path to save the initial draft of the app.
</Alert>
<h3>Summary</h3>
@@ -1051,6 +1001,23 @@
</DrawerContent>
</Drawer>
{/if}
<AppJobsDrawer
bind:open={$jobsDrawerOpen}
jobs={$jobs}
on:clear={() => {
$jobs = []
$errorByComponent = {}
}}
on:clearErrors={() => {
$errorByComponent = {}
}}
{hasErrors}
{selectedJobId}
refreshComponents={$refreshComponents}
jobsById={$jobsById}
errorByComponent={$errorByComponent}
/>
<Drawer bind:open={saveDrawerOpen} size="800px">
<DrawerContent title="Deploy" on:close={() => closeSaveDrawer()}>
{#if !onLatest}
@@ -1302,251 +1269,6 @@
<LazyModePanel />
</DrawerContent>
</Drawer>
<Drawer bind:open={$jobsDrawerOpen} size="900px">
<DrawerContent
noPadding
title="Debug Runs"
on:close={() => {
$jobsDrawerOpen = false
}}
tooltip="Look at latests runs to spot potential bugs."
documentationLink="https://www.windmill.dev/docs/apps/app_debugging"
>
<Splitpanes class="!overflow-visible">
<Pane size={25}>
<PanelSection title="Past Runs">
<div class="flex flex-col gap-2 w-full">
{#if $jobs.length > 0}
<div class="flex gap-2 flex-col-reverse">
{#each $jobs ?? [] as id}
{@const selectedJob = $jobsById[id]}
{#if selectedJob}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class={classNames(
'border flex gap-1 truncate justify-between flex-row w-full items-center p-2 rounded-md cursor-pointer hover:bg-surface-secondary hover:text-blue-400',
selectedJob.error ? 'border border-red-500 text-primary' : '',
selectedJob.error && $errorByComponent[selectedJob.component]?.id == id
? selectedJobId == id
? 'bg-red-600 !border-blue-600'
: 'bg-red-400'
: selectedJobId == id
? 'text-blue-600'
: ''
)}
on:click={() => {
selectedJobId = id
rightColumnSelect = 'detail'
}}
>
<span class="text-xs truncate">{truncateRev(selectedJob.job, 20)}</span>
<Badge color="indigo">{selectedJob.component}</Badge>
</div>
{/if}
{/each}
</div>
{:else}
<div class="text-sm text-tertiary">No items</div>
{/if}
</div>
</PanelSection>
</Pane>
<Pane size={75}>
<div class="w-full h-full flex flex-col">
<div>
<Tabs bind:selected={rightColumnSelect}>
<Tab value="timeline"><span class="font-semibold text-md">Timeline</span></Tab>
<Tab value="detail"><span class="font-semibold">Details</span></Tab>
</Tabs>
</div>
{#if rightColumnSelect == 'timeline'}
<div class="p-2 grow overflow-auto">
<AppTimeline />
</div>
{:else if rightColumnSelect == 'detail'}
<div class="grow flex flex-col w-full overflow-auto">
{#if selectedJobId}
{#if selectedJobId?.includes('Frontend')}
{@const jobResult = $jobsById[selectedJobId]}
{#if jobResult?.error !== undefined}
<Splitpanes horizontal class="grow border w-full">
<Pane size={10} minSize={10}>
<LogViewer
content={`Logs are avaiable in the browser console directly`}
isLoading={false}
tag={undefined}
/>
</Pane>
<Pane size={90} minSize={10} class="text-sm text-secondary">
<div class="relative h-full px-2">
<DisplayResult
result={{
error: { name: 'Frontend execution error', message: jobResult.error }
}}
/>
</div>
</Pane>
</Splitpanes>
{:else if jobResult !== undefined}
<Splitpanes horizontal class="grow border w-full">
<Pane size={10} minSize={10}>
<LogViewer
content={`Logs are avaiable in the browser console directly`}
isLoading={false}
tag={undefined}
/>
</Pane>
<Pane size={90} minSize={10} class="text-sm text-secondary">
<div class="relative h-full px-2">
<DisplayResult
workspaceId={$workspaceStore}
jobId={selectedJobId}
result={jobResult.result}
/>
</div>
</Pane>
</Splitpanes>
{:else}
<Loader2 class="animate-spin" />
{/if}
{:else}
<div class="flex flex-col h-full w-full mb-4">
{#if job?.['running']}
<div class="flex flex-row-reverse w-full">
<Button
color="red"
variant="border"
on:click={() => testJobLoader?.cancelJob()}
>
<Loader2 size={14} class="animate-spin mr-2" />
Cancel
</Button>
</div>
{/if}
{#if job?.args}
<div class="p-2">
<JobArgs
id={job.id}
workspace={job.workspace_id ?? $workspaceStore ?? 'no_w'}
args={job?.args}
/>
</div>
{/if}
{#if job?.raw_code}
<div class="pb-2 pl-2 pr-2 w-full overflow-auto h-full max-h-[80px]">
<HighlightCode language={job?.language} code={job?.raw_code} />
</div>
{/if}
{#if job?.job_kind !== 'flow' && !isFlowPreview(job?.job_kind)}
{@const jobResult = $jobsById[selectedJobId]}
<Splitpanes horizontal class="grow border w-full">
<Pane size={50} minSize={10}>
<LogViewer
duration={job?.['duration_ms']}
jobId={job?.id}
content={job?.logs}
isLoading={testIsLoading && job?.['running'] == false}
tag={job?.tag}
/>
</Pane>
<Pane size={50} minSize={10} class="text-sm text-secondary">
{#if job != undefined && 'result' in job && job.result != undefined}<div
class="relative h-full px-2"
><DisplayResult
workspaceId={$workspaceStore}
jobId={selectedJobId}
result={job.result}
/></div
>
{:else if testIsLoading}
<div class="p-2"><Loader2 class="animate-spin" /> </div>
{:else if job != undefined && 'result' in job && job?.['result'] == undefined}
<div class="p-2 text-tertiary">Result is undefined</div>
{:else}
<div class="p-2 text-tertiary">
<Loader2 size={14} class="animate-spin mr-2" />
</div>
{/if}
</Pane>
{#if jobResult?.transformer}
<Pane size={50} minSize={10} class="text-sm text-secondary p-2">
<div class="font-bold">Transformer results</div>
{#if job != undefined && 'result' in job && job.result != undefined}
<div class="relative h-full px-2">
<DisplayResult
workspaceId={$workspaceStore}
jobId={selectedJobId}
result={jobResult?.transformer}
/>
</div>
{:else if testIsLoading}
<div class="p-2"><Loader2 class="animate-spin" /> </div>
{:else if job != undefined && 'result' in job && job?.['result'] == undefined}
<div class="p-2 text-tertiary">Result is undefined</div>
{:else}
<div class="p-2 text-tertiary">
<Loader2 size={14} class="animate-spin mr-2" />
</div>
{/if}
</Pane>
{/if}
</Splitpanes>
{:else}
<div class="mt-10"></div>
<FlowProgressBar {job} class="py-4" />
<div class="w-full mt-10 mb-20">
<FlowStatusViewer
jobId={job?.id ?? ''}
on:jobsLoaded={({ detail }) => {
job = detail
}}
/>
</div>
{/if}
</div>
{/if}
{:else}
<div class="text-sm p-2 text-tertiary">Select a job to see its details</div>
{/if}
</div>
{/if}
</div>
</Pane>
</Splitpanes>
<svelte:fragment slot="actions">
<Button
size="md"
color="light"
variant="border"
on:click={() => {
$refreshComponents?.()
}}
title="Refresh App"
>
Refresh app&nbsp;<RefreshCw size={16} />
</Button>
<Button
size="md"
color="light"
variant="border"
on:click={() => {
errorByComponent.set({})
jobs.set([])
}}
>Clear jobs
</Button>
{#if hasErrors}
<Button size="md" color="light" variant="border" on:click={() => errorByComponent.set({})}
>Clear Errors &nbsp;<BellOff size={14} />
</Button>
{/if}
</svelte:fragment>
</DrawerContent>
</Drawer>
<AppReportsDrawer bind:open={appReportingDrawerOpen} appPath={$appPath ?? ''} />
@@ -7,7 +7,6 @@
import { Highlight } from 'svelte-highlight'
import json from 'svelte-highlight/languages/json'
import { Button } from '../../common'
import type { App } from '../types'
import { Clipboard } from 'lucide-svelte'
import { yaml } from 'svelte-highlight/languages'
import YAML from 'yaml'
@@ -17,11 +16,11 @@
let jsonViewerDrawer: Drawer
let app: App | undefined = undefined
let app: any | undefined = undefined
let rawType: 'json' | 'yaml' = 'yaml'
export function open(app_l: App) {
export function open(app_l: any) {
app = app_l
jsonViewerDrawer?.toggleDrawer()
}
@@ -0,0 +1,299 @@
<script lang="ts">
import { Badge, Drawer, DrawerContent, Tab, Tabs } from '$lib/components/common'
import Button from '$lib/components/common/button/Button.svelte'
import DisplayResult from '$lib/components/DisplayResult.svelte'
import FlowProgressBar from '$lib/components/flows/FlowProgressBar.svelte'
import FlowStatusViewer from '$lib/components/FlowStatusViewer.svelte'
import JobArgs from '$lib/components/JobArgs.svelte'
import LogViewer from '$lib/components/LogViewer.svelte'
import { workspaceStore } from '$lib/stores'
import { BellOff, Loader2, RefreshCw } from 'lucide-svelte'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import { classNames, truncateRev, isFlowPreview } from '../../../utils'
import PanelSection from './settingsPanel/common/PanelSection.svelte'
import AppTimeline from './AppTimeline.svelte'
import HighlightCode from '$lib/components/HighlightCode.svelte'
import TestJobLoader from '$lib/components/TestJobLoader.svelte'
import type { Job } from '$lib/gen'
import type { JobById } from '../types'
import { createEventDispatcher } from 'svelte'
export let open = false
export let jobs: string[]
export let jobsById: Record<string, JobById>
export let hasErrors: boolean = false
export let selectedJobId: string | undefined = undefined
export let refreshComponents: (() => void) | undefined = undefined
export let errorByComponent: Record<string, { id?: string; error: string }> = {}
const dispatch = createEventDispatcher()
let testJobLoader: TestJobLoader
let job: Job | undefined = undefined
let testIsLoading = false
let rightColumnSelect: 'timeline' | 'detail' = 'timeline'
$: selectedJobId && !selectedJobId?.includes('Frontend') && testJobLoader?.watchJob(selectedJobId)
$: if (selectedJobId?.includes('Frontend') && selectedJobId) {
job = undefined
}
</script>
<TestJobLoader bind:this={testJobLoader} bind:isLoading={testIsLoading} bind:job />
<Drawer bind:open size="900px">
<DrawerContent
noPadding
title="Debug Runs"
on:close={() => {
open = false
}}
tooltip="Look at latests runs to spot potential bugs."
documentationLink="https://www.windmill.dev/docs/apps/app_debugging"
>
<Splitpanes class="!overflow-visible">
<Pane size={25}>
<PanelSection title="Past Runs">
<div class="flex flex-col gap-2 w-full">
{#if jobs.length > 0}
<div class="flex gap-2 flex-col-reverse">
{#each jobs ?? [] as id}
{@const selectedJob = jobsById[id]}
{#if selectedJob}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class={classNames(
'border flex gap-1 truncate justify-between flex-row w-full items-center p-2 rounded-md cursor-pointer hover:bg-surface-secondary hover:text-blue-400',
selectedJob.error ? 'border border-red-500 text-primary' : '',
selectedJob.error && errorByComponent[selectedJob.component]?.id == id
? selectedJobId == id
? 'bg-red-600 !border-blue-600'
: 'bg-red-400'
: selectedJobId == id
? 'text-blue-600'
: ''
)}
on:click={() => {
selectedJobId = id
rightColumnSelect = 'detail'
}}
>
<span class="text-xs truncate">{truncateRev(selectedJob.job, 20)}</span>
<Badge color="indigo">{selectedJob.component}</Badge>
</div>
{/if}
{/each}
</div>
{:else}
<div class="text-sm text-tertiary">No items</div>
{/if}
</div>
</PanelSection>
</Pane>
<Pane size={75}>
<div class="w-full h-full flex flex-col">
<div>
<Tabs bind:selected={rightColumnSelect}>
<Tab value="timeline"><span class="font-semibold text-md">Timeline</span></Tab>
<Tab value="detail"><span class="font-semibold">Details</span></Tab>
</Tabs>
</div>
{#if rightColumnSelect == 'timeline'}
<div class="p-2 grow overflow-auto">
<AppTimeline {jobs} {jobsById} />
</div>
{:else if rightColumnSelect == 'detail'}
<div class="grow flex flex-col w-full overflow-auto">
{#if selectedJobId}
{#if selectedJobId?.includes('Frontend')}
{@const jobResult = jobsById[selectedJobId]}
{#if jobResult?.error !== undefined}
<Splitpanes horizontal class="grow border w-full">
<Pane size={10} minSize={10}>
<LogViewer
content={`Logs are avaiable in the browser console directly`}
isLoading={false}
tag={undefined}
/>
</Pane>
<Pane size={90} minSize={10} class="text-sm text-secondary">
<div class="relative h-full px-2">
<DisplayResult
result={{
error: { name: 'Frontend execution error', message: jobResult.error }
}}
/>
</div>
</Pane>
</Splitpanes>
{:else if jobResult !== undefined}
<Splitpanes horizontal class="grow border w-full">
<Pane size={10} minSize={10}>
<LogViewer
content={`Logs are avaiable in the browser console directly`}
isLoading={false}
tag={undefined}
/>
</Pane>
<Pane size={90} minSize={10} class="text-sm text-secondary">
<div class="relative h-full px-2">
<DisplayResult
workspaceId={$workspaceStore}
jobId={selectedJobId}
result={jobResult.result}
/>
</div>
</Pane>
</Splitpanes>
{:else}
<Loader2 class="animate-spin" />
{/if}
{:else}
<div class="flex flex-col h-full w-full mb-4">
{#if job?.['running']}
<div class="flex flex-row-reverse w-full">
<Button
color="red"
variant="border"
on:click={() => testJobLoader?.cancelJob()}
>
<Loader2 size={14} class="animate-spin mr-2" />
Cancel
</Button>
</div>
{/if}
{#if job?.args}
<div class="p-2">
<JobArgs
id={job.id}
workspace={job.workspace_id ?? $workspaceStore ?? 'no_w'}
args={job?.args}
/>
</div>
{/if}
{#if job?.raw_code}
<div class="pb-2 pl-2 pr-2 w-full overflow-auto h-full max-h-[80px]">
<HighlightCode language={job?.language} code={job?.raw_code} />
</div>
{/if}
{#if job?.job_kind !== 'flow' && !isFlowPreview(job?.job_kind)}
{@const jobResult = jobsById[selectedJobId]}
<Splitpanes horizontal class="grow border w-full">
<Pane size={50} minSize={10}>
<LogViewer
duration={job?.['duration_ms']}
jobId={job?.id}
content={job?.logs}
isLoading={testIsLoading && job?.['running'] == false}
tag={job?.tag}
/>
</Pane>
<Pane size={50} minSize={10} class="text-sm text-secondary">
{#if job != undefined && 'result' in job && job.result != undefined}<div
class="relative h-full px-2"
><DisplayResult
workspaceId={$workspaceStore}
jobId={selectedJobId}
result={job.result}
/></div
>
{:else if testIsLoading}
<div class="p-2"><Loader2 class="animate-spin" /> </div>
{:else if job != undefined && 'result' in job && job?.['result'] == undefined}
<div class="p-2 text-tertiary">Result is undefined</div>
{:else}
<div class="p-2 text-tertiary">
<Loader2 size={14} class="animate-spin mr-2" />
</div>
{/if}
</Pane>
{#if jobResult?.transformer}
<Pane size={50} minSize={10} class="text-sm text-secondary p-2">
<div class="font-bold">Transformer results</div>
{#if job != undefined && 'result' in job && job.result != undefined}
<div class="relative h-full px-2">
<DisplayResult
workspaceId={$workspaceStore}
jobId={selectedJobId}
result={jobResult?.transformer}
/>
</div>
{:else if testIsLoading}
<div class="p-2"><Loader2 class="animate-spin" /> </div>
{:else if job != undefined && 'result' in job && job?.['result'] == undefined}
<div class="p-2 text-tertiary">Result is undefined</div>
{:else}
<div class="p-2 text-tertiary">
<Loader2 size={14} class="animate-spin mr-2" />
</div>
{/if}
</Pane>
{/if}
</Splitpanes>
{:else}
<div class="mt-10" />
<FlowProgressBar {job} class="py-4" />
<div class="w-full mt-10 mb-20">
{#if job?.id}
<FlowStatusViewer
jobId={job?.id}
on:jobsLoaded={({ detail }) => {
job = detail
}}
/>
{:else}
<Loader2 class="animate-spin" />
{/if}
</div>
{/if}
</div>
{/if}
{:else}
<div class="text-sm p-2 text-tertiary">Select a job to see its details</div>
{/if}
</div>
{/if}
</div>
</Pane>
</Splitpanes>
<svelte:fragment slot="actions">
{#if refreshComponents}
<Button
size="md"
color="light"
variant="border"
on:click={() => {
refreshComponents?.()
}}
title="Refresh App"
>
Refresh app&nbsp;<RefreshCw size={16} />
</Button>
{/if}
<Button
size="md"
color="light"
variant="border"
on:click={() => {
dispatch('clear')
}}
>Clear jobs
</Button>
{#if hasErrors}
<Button size="md" color="light" variant="border" on:click={() => dispatch('clearErrors')}>
>Clear Errors &nbsp;<BellOff size={14} />
</Button>
{/if}
</svelte:fragment>
</DrawerContent>
</Drawer>
@@ -1,18 +1,19 @@
<script lang="ts">
import { debounce } from '$lib/utils'
import { getContext, onDestroy } from 'svelte'
import { onDestroy } from 'svelte'
import TimelineBar from '$lib/components/TimelineBar.svelte'
import type { AppViewerContext } from '../types'
import type { JobById } from '../types'
const { jobs, jobsById } = getContext<AppViewerContext>('AppViewerContext')
export let jobs: string[]
export let jobsById: Record<string, JobById>
let min: undefined | number = undefined
let max: undefined | number = undefined
let total: number | undefined = undefined
let debounced = debounce(() => computeItems($jobs), 30)
$: $jobs && $jobsById && debounced()
let debounced = debounce(() => computeItems(jobs), 30)
$: jobs && jobsById && debounced()
let items: Record<
string,
@@ -35,7 +36,7 @@
{ created_at?: number; started_at?: number; duration_ms?: number; id: string }[]
> = {}
jobs.forEach((k) => {
let v = $jobsById[k]
let v = jobsById[k]
if (v.created_at) {
if (!nmin) {
nmin = v.created_at
@@ -0,0 +1,37 @@
import type { StaticAppInput } from "../inputType"
import { Sha256 } from '@aws-crypto/sha256-js'
export function collectStaticFields(
fields: Record<string, StaticAppInput>
) {
return Object.fromEntries(
Object.entries(fields ?? {})
.filter(([k, v]) => v.type == 'static')
.map(([k, v]) => {
return [k, v['value']]
})
)
}
export type TriggerableV2 = {
static_inputs: Record<string, any>
one_of_inputs?: Record<string, any[] | undefined>
allow_user_resources?: string[]
}
export async function hash(message) {
try {
const msgUint8 = new TextEncoder().encode(message) // encode as (utf-8) Uint8Array
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8) // hash the message
const hashArray = Array.from(new Uint8Array(hashBuffer)) // convert buffer to byte array
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('') // convert bytes to hex string
return hashHex
} catch {
//subtle not available, trying pure js
const hash = new Sha256()
hash.update(message ?? '')
const result = Array.from(await hash.digest())
const hex = result.map((b) => b.toString(16).padStart(2, '0')).join('') // convert bytes to hex string
return hex
}
}
@@ -0,0 +1,39 @@
<script lang="ts">
import { getContext } from 'svelte'
import type {
AppEditorContext,
AppViewerContext,
CancelablePromise,
InlineScript
} from '../../types'
import RunButtonInner from '$lib/components/RunButton.svelte'
export let id: string
export let inlineScript: InlineScript | undefined = undefined
export let runLoading = false
export let hideShortcut = false
const { runnableComponents } = getContext<AppViewerContext>('AppViewerContext')
const { runnableJobEditorPanel } = getContext<AppEditorContext>('AppEditorContext')
let cancelable: CancelablePromise<void>[] | undefined = undefined
const onRun = async () => {
runLoading = true
try {
$runnableJobEditorPanel.focused = true
cancelable = $runnableComponents[id]?.cb?.map((f) => f(inlineScript, true))
await Promise.all(cancelable)
} catch {}
runLoading = false
}
const onCancel = async () => {
cancelable?.forEach((f) => f.cancel())
runLoading = false
}
</script>
{#if runnableComponents && $runnableComponents[id] != undefined}
<RunButtonInner isLoading={runLoading} {hideShortcut} {onRun} {onCancel} />
{/if}
@@ -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>('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 @@
<div>
<div class="max-w-6xl">
<Tabs bind:selected={tab}>
<Tab size="sm" value="inlinescripts">
<div class="flex gap-2 items-center my-1">
<Building size={18} />
Detached Inline Scripts
</div>
</Tab>
<Tab size="sm" value="workspacescripts">
<div class="flex gap-2 items-center my-1">
<Building size={18} />
@@ -131,14 +109,7 @@
<div class="my-2"></div>
<div class="flex flex-col gap-y-16">
<div class="flex flex-col">
{#if tab == 'inlinescripts'}
<InlineScriptList
on:pick={(e) => pickInlineScript(e.detail)}
inlineScripts={$app.unusedInlineScripts
? $app.unusedInlineScripts.map((uis) => uis.name)
: []}
/>
{:else if tab == 'workspacescripts'}
{#if tab == 'workspacescripts'}
<WorkspaceScriptList on:pick={(e) => pickScript(e.detail)} />
{:else if tab == 'hubscripts'}
<PickHubScript bind:filter on:pick={(e) => pickHubScript(e.detail.path)} />
@@ -159,7 +130,7 @@
<div class="font-bold items-baseline truncate">Choose a language</div>
<div class="flex gap-2">
{#if showScriptPicker}
<RunnableSelector on:pick hideCreateScript />
<RunnableSelector {unusedInlineScripts} {rawApps} on:pick hideCreateScript />
{/if}
<Button
on:click={() => picker?.openDrawer()}
@@ -194,30 +165,32 @@
{label}
{lang}
on:click={() => {
createInlineScriptByLanguage(lang, name)
createInlineScriptByLanguage(lang)
}}
id={`create-${lang}-script`}
/>
{/each}
</div>
</div>
<div id="app-editor-frontend-runnables">
<div class="mb-1 text-sm font-semibold">
Frontend
<Tooltip
documentationLink="https://www.windmill.dev/docs/apps/app-runnable-panel#frontend-scripts"
>
Frontend scripts are executed in the browser and can manipulate the app context directly.
</Tooltip>
</div>
{#if !rawApps}
<div id="app-editor-frontend-runnables">
<div class="mb-1 text-sm font-semibold">
Frontend
<Tooltip
documentationLink="https://www.windmill.dev/docs/apps/app-runnable-panel#frontend-scripts"
>
Frontend scripts are executed in the browser and can manipulate the app context
directly.
</Tooltip>
</div>
<div>
<FlowScriptPicker
label={`JavaScript`}
lang="javascript"
on:click={() => {
const newInlineScript = {
content: `// read outputs and ctx
<div>
<FlowScriptPicker
label={`JavaScript`}
lang="javascript"
on:click={() => {
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)
}}
/>
</div>
</div>
</div>
{/if}
</div>
</div>
@@ -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++
}
}
}
</script>
@@ -219,6 +223,8 @@
{#if inlineScript}
{#if inlineScript.language != 'frontend'}
<InlineScriptEditorDrawer
{id}
appPath={$appPath}
bind:isOpen={drawerIsOpen}
{editor}
bind:this={inlineScriptEditorDrawer}
@@ -241,7 +247,9 @@
class="!text-xs !rounded-sm !shadow-none"
on:keyup={() => {
$app = $app
$stateId++
if (stateId) {
$stateId++
}
}}
/>
<div
@@ -318,11 +326,10 @@
{#if !drawerIsOpen}
{#if inlineScript.language != 'frontend'}
<Editor
path={inlineScript.path}
path={$appPath + '/' + id}
bind:this={editor}
small
class="flex flex-1 grow h-full"
lang={scriptLangToEditorLang(inlineScript?.language)}
scriptLang={inlineScript.language}
bind:code={inlineScript.content}
fixedOverflowWidgets={true}
@@ -6,9 +6,11 @@
import { Save } from 'lucide-svelte'
let scriptEditorDrawer: Drawer
export let appPath: string
export let inlineScript: InlineScript
export let editor: Editor | undefined = undefined
export let isOpen: boolean | undefined = undefined
export let id: string
export function openDrawer() {
scriptEditorDrawer.openDrawer?.()
@@ -31,7 +33,7 @@
noHistory
noSyncFromGithub
lang={inlineScript.language}
path={inlineScript.path ? inlineScript.path + '_fullscreen' : undefined}
path={appPath + '/' + id + '_fullscreen'}
fixedOverflowWidgets={false}
bind:code={inlineScript.content}
bind:schema={inlineScript.schema}
@@ -76,8 +76,8 @@
/>
{:else}
<EmptyInlineScript
unusedInlineScripts={$app?.unusedInlineScripts}
{componentType}
name={componentInput.runnable.name}
on:delete={clear}
on:new={(e) => {
if (
@@ -10,7 +10,7 @@
export let id: string
export let transformer: boolean
const { runnableComponents } = getContext<AppViewerContext>('AppViewerContext')
const { runnableComponents, app } = getContext<AppViewerContext>('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}
<EmptyInlineScript
unusedInlineScripts={$app?.unusedInlineScripts}
on:pick={(e) => onPick(e.detail)}
name={runnable.name}
on:delete
showScriptPicker
on:new={(e) => {
@@ -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<string, StaticAppInput | ConnectedAppInput | RowAppInput | UserAppInput>
export let fields:
| Record<string, StaticAppInput | ConnectedAppInput | RowAppInput | UserAppInput>
| 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>('AppViewerContext')
const viewerContext = getContext<AppViewerContext>('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 @@
<div class="p-2 h-full flex flex-col gap-2">
<div class="flex flex-row-reverse w-full gap-2">
<RunButton hideShortcut {id} />
{#if !rawApps}
<AppRunButton hideShortcut {id} />
{:else}
<RunButton {isLoading} {onRun} {onCancel} />
{/if}
<Button
variant="border"
@@ -121,7 +136,9 @@
on:click={async () => {
sendUserToast('Refreshing inputs')
refresh(runnable)
$stateId = $stateId + 1
if (viewerContext) {
viewerContext.stateId.update((x) => x + 1)
}
await tick()
}}
/>
@@ -223,7 +240,7 @@
/>
</div>
<div class="w-full grow overflow-y-auto">
{#key $stateId}
{#key viewerContext?.stateId ? get(viewerContext.stateId) : 0}
{#if notFound}
<div class="text-red-400"
>{runnable.runType} not found at {runnable.path} in workspace {$workspaceStore}</div
@@ -1,6 +1,6 @@
<script lang="ts">
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>('AppViewerContext')
const { app, runnableComponents, appPath } = getContext<AppViewerContext>('AppViewerContext')
const { selectedComponentInEditor } = getContext<AppEditorContext>('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
</script>
@@ -114,13 +70,18 @@
<Pane size={75}>
{#if !$selectedComponentInEditor}
<div class="text-sm text-secondary text-center py-8 px-2">
Select a script on the left panel
Select a runnable on the left panel
</div>
{:else if gridItem}
{#key gridItem?.id}
<InlineScriptsPanelWithTable
on:createScriptFromInlineScript={(e) => {
createScriptFromInlineScript(gridItem?.id ?? 'unknown', e.detail)
createScriptFromInlineScript(
gridItem?.id ?? 'unknown',
e.detail,
$workspaceStore ?? '',
$appPath
)
}}
bind:gridItem
/>
@@ -145,7 +106,13 @@
{#if $app.hiddenInlineScripts?.[hiddenInlineScript]}
<InlineScriptHiddenRunnable
on:createScriptFromInlineScript={(e) => {
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}
<div class="text-sm text-tertiary text-center py-8 px-2">
No script found at id {$selectedComponentInEditor}
No runnable found at id {$selectedComponentInEditor}
</div>
{/if}
</Pane>
@@ -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}
</div>
</div>
<div>
<div class="w-full flex justify-between items-center mb-1">
<div class="text-xs text-secondary font-semibold truncate">
Background Runnables
<Tooltip
documentationLink="https://www.windmill.dev/docs/apps/app-runnable-panel#background-runnables"
>
@@ -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<string, GridItem[]> | 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
})
}
@@ -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) => {
@@ -41,7 +41,8 @@
const shortcuts = {
left: 'B',
right: 'U',
bottom: 'L'
bottom: 'L',
top: 'T'
}
</script>
@@ -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)
@@ -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>('AppEditorContext')
const { app } = getContext<AppViewerContext>('AppViewerContext')
function onPick({
runnable,
@@ -33,6 +34,7 @@
<SelectedRunnable {appComponent} bind:appInput />
{:else if appInput !== undefined}
<RunnableSelector
unusedInlineScripts={$app.unusedInlineScripts}
hideCreateScript={appComponent.type === 'flowstatuscomponent'}
onlyFlow={appComponent.type === 'flowstatuscomponent'}
{defaultUserInput}
@@ -34,12 +34,11 @@
export let format: string | undefined = undefined
export let id: string | undefined
const { onchange } = getContext<AppViewerContext>('AppViewerContext')
const appContext = getContext<AppViewerContext>('AppViewerContext')
$: componentInput && appContext?.onchange?.()
let s3FileUploadRawMode = false
let s3FilePicker: S3FilePicker | undefined = undefined
$: componentInput && onchange?.()
</script>
{#key subFieldType}
@@ -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>('AppViewerContext')
// const { app, workspace } = getContext<AppViewerContext>('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'}
<InlineScriptList
on:pick={(e) => 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"
>
@@ -142,6 +142,8 @@ export type RunnableByName = {
export type Runnable = RunnableByPath | RunnableByName | undefined
export type RunnableWithFields = Runnable & { fields?: Record<string, StaticAppInput> }
// Runnable input, set by the developer in the component panel
export type ResultInput = {
runnable: Runnable
+1 -2
View File
@@ -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
)
+12 -15
View File
@@ -196,6 +196,17 @@ export type ListInputs = {
export type GroupContext = { id: string; context: Writable<Record<string, any>> }
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<World>
app: Writable<App>
@@ -227,21 +238,7 @@ export type AppViewerContext = {
isEditor: boolean
jobs: Writable<string[]>
// jobByComponent: Writable<Record<string, string>>,
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<Record<string, JobById>>
noBackend: boolean
errorByComponent: Writable<Record<string, { id?: string; error: string }>>
openDebugRun: Writable<((jobID: string) => void) | undefined>
+4 -4
View File
@@ -42,9 +42,9 @@ export function allItems(
subgrids: Record<string, GridItem[]> | 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]
@@ -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'}
<div class="absolute top-4 right-4"><CloseButton on:close={() => (open = false)} /></div
>
{/if}
<div class="flex">
<div class="ml-4 text-left flex-1">
<div class="flex flex-row items-center justify-between">
@@ -81,21 +87,23 @@
</div>
</div>
</div>
<div class="flex items-center space-x-2 flex-row-reverse space-x-reverse mt-4">
<slot name="actions" />
<Button
on:click={() => {
dispatch('canceled')
open = false
}}
color="light"
size="sm"
>
<span class="inline-flex gap-2"
>{cancelText ?? 'Cancel'}<Badge color="dark-gray">Escape</Badge></span
{#if kind == 'button'}
<div class="flex items-center space-x-2 flex-row-reverse space-x-reverse mt-4">
<slot name="actions" />
<Button
on:click={() => {
dispatch('canceled')
open = false
}}
color="light"
size="sm"
>
</Button>
</div>
<span class="inline-flex gap-2"
>{cancelText ?? 'Cancel'}<Badge color="dark-gray">Escape</Badge></span
>
</Button>
</div>
{/if}
</div>
</div>
</div>
@@ -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 @@
</script>
{#if menuOpen}
{#await import('$lib/components/apps/editor/AppJsonEditor.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
{#await import('$lib/components/apps/editor/AppJsonEditor.svelte') then Module}
<Module.default on:change bind:this={appExport} />
{/await}
<AppDeploymentHistory bind:this={appDeploymentHistory} appPath={app.path} />
{/if}
<Row
href={`${base}/apps/get/${app.path}`}
href="{base}/apps{app.raw_app ? '_raw' : ''}/get/{app.path}"
kind="app"
{marked}
path={app.path}
@@ -84,6 +81,14 @@
</div></Badge
>
{/if}
{#if app.raw_app}
<Badge small>
<div class="flex gap-1 items-center">
<FileJson size={14} />
Raw
</div></Badge
>
{/if}
<SharedBadge canWrite={app.canWrite} extraPerms={app.extra_perms} />
<DraftBadge has_draft={app.has_draft} draft_only={app.draft_only} />
<div class="w-8 center-center"></div>
@@ -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
</Button>
@@ -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
</Button>
@@ -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
},
{
@@ -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
@@ -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
@@ -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 =
@@ -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?.()
}
</script>
<!-- Buttons -->
@@ -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')
// }
]}
>
<div class="flex flex-row items-center">
@@ -99,45 +78,3 @@
</svelte:fragment>
</DrawerContent>
</Drawer>
<!-- Raw JSON -->
<Drawer bind:this={rawAppDrawer} size="800px">
<DrawerContent
title="Import app in React/Vue/Svelte"
on:close={() => rawAppDrawer?.toggleDrawer?.()}
>
<Path bind:error={pathError} bind:path initialPath="" namePlaceholder={'app'} kind="resource" />
<h2 class="border-b pb-1 mt-10 mb-4">Summary</h2>
<input
type="text"
bind:value={summary}
placeholder="Short summary to be displayed when listed"
/>
<h2 class="border-b pb-1 mt-10 mb-4"
>IIFE JS code <Tooltip
documentationLink="https://www.windmill.dev/docs/react_vue_svelte_apps/react"
>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.</Tooltip
></h2
>
<FileInput
accept={'.js'}
multiple={false}
convertTo={'text'}
iconSize={24}
class="text-sm py-4"
on:change={({ detail }) => {
pendingCode = detail?.[0]
}}
/>
<svelte:fragment slot="actions">
<Button disabled={pathError != ''} size="sm" on:click={importRawApp}>Import</Button>
</svelte:fragment>
</DrawerContent>
</Drawer>
@@ -0,0 +1,16 @@
<script>
export let height = '24px'
export let width = '24px'
</script>
<svg {width} {height} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" version="1.1">
<path d="M0 0H32V32H0V0Z" fill="#F7DF1E" />
<path
d="M8.41397 26.7415L10.8628 25.2595C11.3353 26.0972 11.765 26.8059 12.7959 26.8059C13.784 26.8059 14.407 26.4193 14.407 24.9158V14.6911H17.4142V24.9584C17.4142 28.073 15.5885 29.4907 12.9248 29.4907C10.5192 29.4907 9.12274 28.2448 8.41392 26.7413"
fill="black"
/>
<path
d="M19.0476 26.4193L21.4962 25.0016C22.1408 26.0542 22.9785 26.8275 24.4606 26.8275C25.7066 26.8275 26.5011 26.2045 26.5011 25.3452C26.5011 24.3142 25.6849 23.949 24.3102 23.3477L23.5586 23.0253C21.3889 22.1018 19.9497 20.9419 19.9497 18.4931C19.9497 16.2376 21.6681 14.5191 24.3532 14.5191C26.265 14.5191 27.6397 15.1851 28.6277 16.925L26.2863 18.4286C25.7708 17.505 25.2124 17.1399 24.3533 17.1399C23.4726 17.1399 22.914 17.6984 22.914 18.4286C22.914 19.3308 23.4726 19.696 24.7612 20.2546L25.513 20.5767C28.0692 21.6723 29.5084 22.7892 29.5084 25.3023C29.5084 28.009 27.3819 29.491 24.5251 29.491C21.7326 29.491 19.9282 28.1593 19.0477 26.4193"
fill="black"
/>
</svg>
@@ -0,0 +1,11 @@
<script>
export let height = '24px'
export let width = '24px'
</script>
<svg {width} {height} xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"
><path
fill="#f9a825"
d="M560-160v-80h120q17 0 28.5-11.5T720-280v-80q0-38 22-69t58-44v-14q-36-13-58-44t-22-69v-80q0-17-11.5-28.5T680-720H560v-80h120q50 0 85 35t35 85v80q0 17 11.5 28.5T840-560h40v160h-40q-17 0-28.5 11.5T800-360v80q0 50-35 85t-85 35zm-280 0q-50 0-85-35t-35-85v-80q0-17-11.5-28.5T120-400H80v-160h40q17 0 28.5-11.5T160-600v-80q0-50 35-85t85-35h120v80H280q-17 0-28.5 11.5T240-680v80q0 38-22 69t-58 44v14q36 13 58 44t22 69v80q0 17 11.5 28.5T280-240h120v80z"
/></svg
>
@@ -0,0 +1,18 @@
<script>
export let height = '24px'
export let width = '24px'
</script>
<svg {width} {height} viewBox="0 0 600 600" xmlns="http://www.w3.org/2000/svg" version="1.1">
<g transform="translate(300, 300)">
<!-- Central circle -->
<circle fill="#61DAFB" r="50" />
<!-- Three ellipses -->
<g stroke="#61DAFB" stroke-width="20" fill="none">
<ellipse rx="225" ry="90" />
<ellipse rx="225" ry="90" transform="rotate(60)" />
<ellipse rx="225" ry="90" transform="rotate(120)" />
</g>
</g>
</svg>
@@ -0,0 +1,15 @@
<script>
export let height = '24px'
export let width = '24px'
</script>
<svg {width} {height} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" version="1.1">
<path
d="M27.5342 4.35822C24.589 0.144416 18.7724 -1.10443 14.5671 1.5745L7.1816 6.28105C5.16609 7.54695 3.77642 9.60267 3.35293 11.9448C2.99976 13.8998 3.30868 15.9168 4.23081 17.6765C3.59882 18.6352 3.16817 19.7123 2.96497 20.8425C2.53883 23.2342 3.09525 25.6967 4.50833 27.6729C7.45346 31.8867 13.2701 33.1356 17.4754 30.4566L24.8609 25.7699C26.8739 24.5014 28.2628 22.4469 28.6896 20.1062C29.0414 18.1518 28.7315 16.1358 27.8089 14.3773C28.4405 13.4181 28.872 12.3413 29.0775 11.2113C29.5024 8.8196 28.9461 6.35758 27.5342 4.38088"
fill="#FF3E00"
/>
<path
d="M13.845 28.0807C11.4634 28.6987 8.94924 27.766 7.5469 25.7444C6.69765 24.5563 6.36377 23.0758 6.62088 21.6382C6.66266 21.4068 6.72134 21.1787 6.79646 20.9558L6.93522 20.531L7.31469 20.8142C8.18692 21.451 9.16069 21.9354 10.1947 22.2471L10.4779 22.3292L10.4524 22.6124C10.4251 23.0007 10.5343 23.3863 10.7611 23.7027C11.1842 24.3108 11.9413 24.591 12.6584 24.405C12.8185 24.3619 12.9712 24.2951 13.1115 24.2067L20.4857 19.5002C20.8519 19.2697 21.1038 18.8954 21.1795 18.4694C21.2557 18.0356 21.1536 17.5893 20.8963 17.2319C20.4731 16.6237 19.716 16.3435 18.9989 16.5296C18.8386 16.5721 18.6858 16.6389 18.5458 16.7278L15.714 18.526C15.2508 18.8197 14.7457 19.0412 14.2159 19.183C11.8385 19.7972 9.3301 18.8662 7.9292 16.8496C7.08253 15.6604 6.75076 14.1802 7.00885 12.7434C7.26171 11.3318 8.09747 10.092 9.31115 9.32815L16.708 4.6216C17.1684 4.32851 17.6706 4.107 18.1975 3.96461C20.5782 3.3462 23.0919 4.27906 24.4927 6.30089C25.343 7.48856 25.6779 8.96911 25.4216 10.4071C25.3774 10.6399 25.3188 10.8697 25.246 11.0952L25.1044 11.52L24.7278 11.2368C23.8538 10.5952 22.877 10.1068 21.8393 9.79258L21.5561 9.71045L21.5816 9.42727C21.6143 9.03707 21.508 8.64787 21.2814 8.3285C20.8558 7.73103 20.1048 7.45928 19.3954 7.64603C19.2351 7.68852 19.0823 7.75538 18.9423 7.84426L11.554 12.5423C11.1892 12.7729 10.9375 13.1457 10.8602 13.5703C10.7854 14.0049 10.8873 14.4515 11.1434 14.8106C11.5644 15.413 12.3139 15.6925 13.0265 15.5129C13.1864 15.4693 13.3391 15.4025 13.4796 15.3147L16.3115 13.5193C16.7743 13.2224 17.2807 12.9998 17.8124 12.8595C20.1925 12.2396 22.7065 13.1715 24.1076 15.1929C24.9573 16.3809 25.2921 17.8612 25.0365 19.2991C24.7836 20.7107 23.9478 21.9505 22.7342 22.7143L15.3458 27.4209C14.8818 27.7155 14.3758 27.938 13.845 28.0807Z"
fill="white"
/>
</svg>
@@ -0,0 +1,13 @@
<script>
export let height = '24px'
export let width = '24px'
</script>
<svg {width} {height} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" version="1.1">
<path
d="M25.6 2.1875H32L16 29.7875L0 2.1875H6.32H12.24L16 8.5875L19.68 2.1875H25.6Z"
fill="#41B883"
/>
<path d="M0 2.1875L16 29.7875L32 2.1875H25.6L16 18.7475L6.32 2.1875H0Z" fill="#41B883" />
<path d="M6.32 2.1875L16 18.8275L25.6 2.1875H19.68L16 8.5875L12.24 2.1875H6.32Z" fill="#35495E" />
</svg>
@@ -0,0 +1,26 @@
<script lang="ts">
import TypeScript from '../common/languageIcons/TypeScript.svelte'
import JavaScriptIcon from '../icons/JavaScriptIcon.svelte'
import JsonIcon from '../icons/JsonIcon.svelte'
import ReactIcon from '../icons/ReactIcon.svelte'
import SvelteIcon from '../icons/SvelteIcon.svelte'
import VueIcon from '../icons/VueIcon.svelte'
export let file: string
</script>
{#if file.endsWith('.tsx')}
<ReactIcon width="16px" height="16px" />
{:else if file.endsWith('.json')}
<JsonIcon width="16px" height="16px" />
{:else if file.endsWith('.ts')}
<TypeScript width={16} height={16} />
{:else if file.endsWith('.js')}
<JavaScriptIcon width="16px" height="16px" />
{:else if file.endsWith('.vue')}
<VueIcon width="16px" height="16px" />
{:else if file.endsWith('.css')}
<span class="text-blue-600 ml-0.5" style="font-size: 16px;">#</span>
{:else if file.endsWith('.svelte')}
<SvelteIcon width="16px" height="16px" />
{/if}
@@ -0,0 +1,90 @@
<script lang="ts">
import { executeRunnable } from '../apps/components/helpers/executeRunnable'
import { userStore } from '$lib/stores'
import { waitJob } from '../waitJob'
import type { HiddenRunnable, JobById } from '../apps/types'
import { JobService } from '$lib/gen'
export let iframe: HTMLIFrameElement | undefined
export let path: string
export let runnables: Record<string, HiddenRunnable>
export let jobs: string[] = []
export let jobsById: Record<string, JobById> = {}
export let editor: boolean
export let workspace: string
let listener = async (event) => {
const data = event.data
function respond(o: object) {
iframe?.contentWindow?.postMessage({ type: data.type + 'Res', ...o, reqId: data.reqId }, '*')
}
async function respondWithResult(uuid: string) {
let error = false
let result
try {
result = await waitJob(uuid)
} catch (e) {
error = true
console.log('e', e)
result = e
}
if (event.data.type == 'runBg') {
respond({ result, error })
}
return result
}
if (event.data.type == 'runBg' || event.data.type == 'runBgAsync') {
const runnable_id = data.runnable_id
let runnable = runnables[runnable_id]
if (runnable) {
const uuid = await executeRunnable(
runnable,
workspace,
undefined,
$userStore?.username,
path,
runnable_id,
{
component: runnable_id,
args: data.v,
force_viewer_allow_user_resources: Object.keys(runnable.fields).filter(
(k) => runnable.fields[k]?.type == 'user' && runnable.fields[k]?.allowUserResources
),
force_viewer_one_of_fields: {},
force_viewer_static_fields: Object.fromEntries(
Object.entries(runnable.fields)
.filter(([k, v]) => v.type == 'static')
.map(([k, v]) => [k, v?.['value']])
)
},
undefined
)
let job: JobById = { component: runnable_id, created_at: Date.now(), job: uuid }
if (event.data.type == 'runBgAsync') {
let result = uuid
respond({ result })
}
if (editor) {
jobsById[uuid] = job
jobs = [...jobs, uuid]
}
const result = await respondWithResult(uuid)
if (editor) {
job.result = result
}
} else if (event.data.type == 'waitJob') {
await respondWithResult(data.jobId)
} else if (event.data.type == 'getJob') {
const job = JobService.getJob({ workspace, id: data.jobId })
respond({ result: job })
} else {
console.error('No runnable found for', runnable_id)
}
}
}
</script>
<svelte:window on:message={listener} />
@@ -0,0 +1,197 @@
<script lang="ts">
import { Pane, Splitpanes } from 'svelte-splitpanes'
import { writable } from 'svelte/store'
import RawAppInlineScriptsPanel from './RawAppInlineScriptsPanel.svelte'
import type { HiddenRunnable, JobById } from '../apps/types'
import RawAppEditorHeader from './RawAppEditorHeader.svelte'
import { type Policy } from '$lib/gen'
import DiffDrawer from '../DiffDrawer.svelte'
import { encodeState } from '$lib/utils'
// import { addWmillClient } from './utils'
import RawAppBackgroundRunner from './RawAppBackgroundRunner.svelte'
import { workspaceStore } from '$lib/stores'
import { genWmillTs } from './utils'
import HideButton from '../apps/editor/settingsPanel/HideButton.svelte'
import DarkModeObserver from '../DarkModeObserver.svelte'
export let initFiles: Record<string, string>
export let initRunnables: Record<string, HiddenRunnable>
export let newApp: boolean
export let policy: Policy
export let summary = ''
export let path: string
export let newPath: string | undefined = undefined
export let savedApp:
| {
value: any
draft?: any
path: string
summary: string
policy: any
draft_only?: boolean
custom_path?: string
}
| undefined = undefined
export let diffDrawer: DiffDrawer | undefined = undefined
export let version: number | undefined = undefined
let runnables = writable(initRunnables)
let files: Record<string, string> | undefined = initFiles
$: $runnables && files && saveFrontendDraft()
let draftTimeout: NodeJS.Timeout | undefined = undefined
function saveFrontendDraft() {
draftTimeout && clearTimeout(draftTimeout)
draftTimeout = setTimeout(() => {
try {
localStorage.setItem(
path != '' ? `rawapp-${path}` : 'rawapp',
encodeState({
files,
runnables: $runnables
})
)
} catch (err) {
console.error(err)
}
}, 500)
}
let iframe: HTMLIFrameElement | undefined = undefined
let appPanelSize = 70
let jobs: string[] = []
let jobsById: Record<string, JobById> = {}
let iframeLoaded = false // @hmr:keep
$: iframe && iframeLoaded && initFiles && populateFiles()
$: iframe && iframeLoaded && $runnables && populateRunnables()
$: iframe?.addEventListener('load', () => {
iframeLoaded = true
})
function populateFiles() {
iframe?.contentWindow?.postMessage(
{
type: 'setFiles',
files: initFiles
},
'*'
)
}
function populateRunnables() {
iframe?.contentWindow?.postMessage(
{
type: 'setRunnables',
dts: genWmillTs($runnables)
},
'*'
)
}
let selectedRunnable: string | undefined = undefined
function listener(e: MessageEvent) {
if (e.data.type === 'setFiles') {
files = e.data.files
} else if (e.data.type === 'getBundle') {
getBundleResolve?.(e.data.bundle)
}
}
let getBundleResolve: (({ css, js }: { css: string; js: string }) => void) | undefined = undefined
async function getBundle(): Promise<{ css: string; js: string }> {
return new Promise((resolve) => {
getBundleResolve = resolve
iframe?.contentWindow?.postMessage(
{
type: 'getBundle'
},
'*'
)
})
}
let darkMode: boolean | undefined = undefined
</script>
<svelte:window on:message={listener} />
<DarkModeObserver bind:darkMode />
<RawAppBackgroundRunner
workspace={$workspaceStore ?? ''}
editor
{iframe}
bind:jobs
bind:jobsById
runnables={$runnables}
{path}
/>
<div class="max-h-screen overflow-hidden h-screen min-h-0 flex flex-col">
<RawAppEditorHeader
bind:jobs
bind:jobsById
bind:savedApp
bind:summary
on:restore
on:savedNewAppPath
{policy}
{diffDrawer}
{newApp}
{newPath}
appPath={path}
{files}
{runnables}
{getBundle}
/>
<Splitpanes id="o2" horizontal class="grow">
<Pane bind:size={appPanelSize}>
<!-- <iframe
bind:this={iframe}
title="UI builder"
src="http://localhost:4000/ui_builder/index.html?dark={darkMode}"
class="w-full h-full"
></iframe> -->
<iframe
bind:this={iframe}
title="UI builder"
src="/ui_builder/index.html?dark={darkMode}"
class="w-full h-full"
></iframe>
</Pane>
<Pane>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="flex h-full w-full">
<RawAppInlineScriptsPanel
on:hidePanel={() => {
appPanelSize = 100
}}
appPath={path}
bind:selectedRunnable
{runnables}
/>
</div>
<!-- <div class="bg-red-400 h-full w-full" /> -->
</Pane>
</Splitpanes>
{#if appPanelSize == 100}
<div class="absolute bottom-0.5 left-0.5 z-50">
<HideButton
size="lg"
on:click={() => {
appPanelSize = 70
}}
direction="bottom"
hidden
btnClasses="border bg-surface"
/>
</div>
{/if}
</div>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,219 @@
<script lang="ts">
import Button from '$lib/components/common/button/Button.svelte'
import type { Preview } from '$lib/gen'
import { createEventDispatcher, onMount } from 'svelte'
import { Maximize2, Trash2 } from 'lucide-svelte'
import { inferArgs } from '$lib/infer'
import type { Schema } from '$lib/common'
import Editor from '$lib/components/Editor.svelte'
import { emptySchema } from '$lib/utils'
import { scriptLangToEditorLang } from '$lib/scripts'
import ScriptGen from '$lib/components/copilot/ScriptGen.svelte'
import DiffEditor from '$lib/components/DiffEditor.svelte'
import EditorSettings from '$lib/components/EditorSettings.svelte'
import InlineScriptEditorDrawer from '../apps/editor/inlineScriptsPanel/InlineScriptEditorDrawer.svelte'
import type { InlineScript } from '../apps/types'
import type { AppInput } from '../apps/inputType'
import CacheTtlPopup from '../apps/editor/inlineScriptsPanel/CacheTtlPopup.svelte'
import RunButton from '$lib/components/RunButton.svelte'
import { computeFields } from '../apps/editor/inlineScriptsPanel/utils'
let inlineScriptEditorDrawer: InlineScriptEditorDrawer
export let inlineScript: InlineScript | undefined
export let name: string | undefined = undefined
export let id: string
export let fields: Record<string, AppInput> = {}
export let path: string
export let isLoading: boolean = false
export let onRun: () => Promise<void>
export let onCancel: () => Promise<void>
export let editor: Editor | undefined = undefined
let diffEditor: DiffEditor
let validCode = true
async function inferInlineScriptSchema(
language: Preview['language'],
content: string,
schema: Schema
): Promise<Schema> {
try {
await inferArgs(language, content, schema)
validCode = true
} catch (e) {
console.error("Couldn't infer args", e)
validCode = false
}
return schema
}
onMount(async () => {
if (inlineScript && !inlineScript.schema) {
if (inlineScript.language != 'frontend') {
inlineScript.schema = await inferInlineScriptSchema(
inlineScript?.language,
inlineScript?.content,
emptySchema()
)
}
}
syncFields()
})
async function syncFields() {
if (inlineScript) {
const newSchema = inlineScript.schema ?? emptySchema()
fields = computeFields(newSchema, true, fields)
}
}
const dispatch = createEventDispatcher()
let drawerIsOpen: boolean | undefined = undefined
</script>
{#if inlineScript}
{#if inlineScript.language != 'frontend'}
<InlineScriptEditorDrawer
{id}
appPath={path}
bind:isOpen={drawerIsOpen}
{editor}
bind:this={inlineScriptEditorDrawer}
bind:inlineScript
on:createScriptFromInlineScript={() => {
dispatch('createScriptFromInlineScript')
drawerIsOpen = false
}}
/>
{/if}
<div class="h-full flex flex-col gap-1">
<div class="flex justify-between w-full gap-2 px-2 pt-1 flex-row items-center">
{#if name !== undefined}
<div class="flex flex-row gap-2 w-full items-center">
<input
on:keydown|stopPropagation
bind:value={name}
placeholder="Inline script name"
class="!text-xs !rounded-sm !shadow-none"
on:keyup={() => {
// $app = $app
// if (stateId) {
// $stateId++
// }
}}
/>
<div
title={validCode ? 'Main function parsable' : 'Main function not parsable'}
class="rounded-full !w-2 !h-2 {validCode ? 'bg-green-300' : 'bg-red-300'}"
/>
</div>
{/if}
<div class="flex w-full flex-row gap-1 items-center justify-end">
{#if inlineScript}
<CacheTtlPopup bind:cache_ttl={inlineScript.cache_ttl} />
{/if}
<ScriptGen
lang={inlineScript?.language}
{editor}
{diffEditor}
inlineScript
args={Object.entries(fields ?? {}).reduce((acc, [key, obj]) => {
acc[key] = obj.type === 'static' ? obj.value : undefined
return acc
}, {})}
/>
<EditorSettings />
<Button
title="Delete"
size="xs2"
color="light"
variant="contained"
aria-label="Delete"
on:click={() => dispatch('delete')}
endIcon={{ icon: Trash2 }}
iconOnly
/>
{#if inlineScript.language != 'frontend'}
<Button
size="xs2"
color="light"
title="Full Editor"
variant="contained"
on:click={() => {
inlineScriptEditorDrawer?.openDrawer()
}}
endIcon={{ icon: Maximize2 }}
iconOnly
/>
{/if}
<Button
variant="border"
size="xs2"
color="light"
on:click={async () => {
editor?.format()
}}
shortCut={{
key: 'S'
}}
>
Format
</Button>
<RunButton {isLoading} {onRun} {onCancel} />
</div>
</div>
<!-- {inlineScript.content} -->
<div class="border-y h-full w-full">
{#if !drawerIsOpen && inlineScript.language != 'frontend'}
<Editor
path={path + '/' + id}
bind:this={editor}
small
class="flex flex-1 grow h-full"
scriptLang={inlineScript.language}
bind:code={inlineScript.content}
fixedOverflowWidgets={true}
cmdEnterAction={() => 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
}, {})}
/>
<DiffEditor
open={false}
bind:this={diffEditor}
class="h-full"
automaticLayout
fixedOverflowWidgets
defaultLang={scriptLangToEditorLang(inlineScript?.language)}
/>
{/if}
</div>
</div>
{/if}
@@ -0,0 +1,90 @@
<script lang="ts">
import { Badge, Button } from '$lib/components/common'
import { Plus } from 'lucide-svelte'
import { createEventDispatcher } from 'svelte'
import PanelSection from '../apps/editor/settingsPanel/common/PanelSection.svelte'
import DocLink from '../apps/editor/settingsPanel/DocLink.svelte'
import HideButton from '../apps/editor/settingsPanel/HideButton.svelte'
import type { Writable } from 'svelte/store'
import type { Runnable } from '../apps/inputType'
import { getNextId } from '$lib/components/flows/idUtils'
export let selectedRunnable: string | undefined
export let runnables: Writable<Record<string, Runnable>>
function createBackgroundScript() {
const nid = getNextId(Object.keys($runnables ?? {}))
const newScriptPath = `Backend Runnable ${nid}`
runnables.update((r) => {
r[nid] = {
name: newScriptPath,
inlineScript: undefined,
type: 'runnableByName'
}
return r
})
console.log('BAR 2')
selectedRunnable = nid
}
const dispatch = createEventDispatcher()
</script>
<PanelSection title="Backend Runnables" id="app-editor-runnable-panel">
<svelte:fragment slot="action">
<div class="flex flex-row gap-1">
<HideButton
direction="bottom"
on:click={() => {
dispatch('hidePanel')
}}
/>
<DocLink
docLink="https://www.windmill.dev/docs/apps/app-runnable-panel#creating-a-runnable"
/>
<Button
size="xs"
color="light"
variant="border"
btnClasses="!rounded-full !p-1"
title="Create a new background runnable"
aria-label="Create a new background runnable"
on:click={createBackgroundScript}
id="create-background-runnable"
>
<Plus size={14} class="!text-primary" />
</Button>
</div>
</svelte:fragment>
<div class="w-full flex flex-col gap-6 py-1">
<div>
<div class="flex flex-col gap-1 w-full">
{#if Object.keys($runnables ?? {}).length > 0}
{#each Object.entries($runnables ?? {}) as [id, runnable]}
{#if runnable}
<button
{id}
class="panel-item
{selectedRunnable === id
? 'border-blue-500 bg-blue-100 dark:bg-frost-900/50'
: 'hover:bg-blue-50 dark:hover:bg-frost-900/50'}"
on:click={() => (selectedRunnable = id)}
>
<span class="text-2xs truncate">{runnable?.name}</span>
<Badge color="indigo">{id}</Badge>
</button>
{/if}
{/each}
{:else}
<div class="text-xs text-tertiary">No backend runnable</div>
{/if}
</div>
</div>
</div>
</PanelSection>
<style lang="postcss">
.panel-item {
@apply border flex gap-1 truncate font-normal justify-between w-full items-center py-1 px-2 rounded-sm duration-200;
}
</style>
@@ -0,0 +1,214 @@
<script lang="ts">
import EmptyInlineScript from '../apps/editor/inlineScriptsPanel/EmptyInlineScript.svelte'
import InlineScriptRunnableByPath from '../apps/editor/inlineScriptsPanel/InlineScriptRunnableByPath.svelte'
import type { Runnable, RunnableWithFields, StaticAppInput } from '../apps/inputType'
import { createEventDispatcher } from 'svelte'
import RawAppInlineScriptEditor from './RawAppInlineScriptEditor.svelte'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import Tabs from '../common/tabs/Tabs.svelte'
import { Tab } from '../common'
import RawAppInputsSpecEditor from './RawAppInputsSpecEditor.svelte'
import SplitPanesWrapper from '../splitPanes/SplitPanesWrapper.svelte'
import SchemaForm from '../SchemaForm.svelte'
import RunnableJobPanelInner from '../apps/editor/RunnableJobPanelInner.svelte'
import TestJobLoader from '../TestJobLoader.svelte'
import type { Job } from '$lib/gen'
export let runnable: RunnableWithFields | undefined
export let id: string
export let appPath: string
const dispatch = createEventDispatcher()
async function fork(nrunnable: RunnableWithFields) {
runnable = nrunnable == undefined ? undefined : { ...runnable, ...nrunnable }
}
function onPick(o: { runnable: Runnable; fields: Record<string, StaticAppInput> }) {
runnable =
o.runnable == undefined
? undefined
: {
...(runnable ?? {}),
...o.runnable,
fields: o.fields
}
}
let selectedTab = 'inputs'
let args = {}
function getSchema(runnable: RunnableWithFields) {
if (runnable?.type == 'runnableByPath') {
console.log('runnable.schema', runnable.schema)
return runnable.schema
} else if (runnable?.type == 'runnableByName' && runnable.inlineScript) {
return runnable.inlineScript.schema
}
return {}
}
let testJobLoader: TestJobLoader | undefined
let testJob: Job | undefined
let testIsLoading = false
let scriptProgress = 0
$: onFieldsChange(runnable?.fields ?? {})
function onFieldsChange(fields: Record<string, StaticAppInput>) {
if (args == undefined) {
args = {}
}
Object.entries(fields ?? {}).forEach(([k, v]) => {
if (v.type == 'static') {
args[k] = v.value
}
})
}
async function testPreview() {
selectedTab = 'test'
if (runnable?.type == 'runnableByName' && runnable.inlineScript?.language != 'frontend') {
await testJobLoader?.runPreview(
appPath + '/' + id,
runnable.inlineScript?.content ?? '',
runnable.inlineScript?.language,
args,
undefined
)
} else if (runnable?.type == 'runnableByPath') {
if (testJobLoader && runnable?.type == 'runnableByPath') {
if (runnable.runType == 'flow') {
await testJobLoader.runFlowByPath(runnable.path, args)
} else if (runnable.runType == 'script' || runnable.runType == 'hubscript') {
await testJobLoader.runScriptByPath(runnable.path, args)
}
}
}
}
</script>
<TestJobLoader
bind:scriptProgress
bind:this={testJobLoader}
bind:isLoading={testIsLoading}
bind:job={testJob}
/>
{#if runnable?.type == 'runnableByPath' || (runnable?.type == 'runnableByName' && runnable.inlineScript)}
<Splitpanes>
<Pane size={55}>
{#if runnable?.type === 'runnableByName' && runnable.inlineScript}
{#if runnable.inlineScript.language == 'frontend'}
<div class="text-sm text-tertiary">Frontend scripts not supported for raw apps</div>
{:else}
<RawAppInlineScriptEditor
on:createScriptFromInlineScript={() =>
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'}
<InlineScriptRunnableByPath
rawApps
bind:runnable
bind:fields={runnable.fields}
on:fork={(e) => fork(e.detail)}
on:delete
{id}
isLoading={testIsLoading}
onRun={testPreview}
onCancel={async () => {
if (testJobLoader) {
await testJobLoader.cancelJob()
}
}}
/>
{/if}
</Pane>
<Pane size={45}>
<Tabs bind:selected={selectedTab}>
<Tab value="inputs">Inputs</Tab>
<Tab value="test">Test</Tab>
<svelte:fragment slot="content">
{#if selectedTab == 'inputs'}
{#if runnable?.fields}
<div class="w-full flex flex-col gap-4 p-2">
{#each Object.keys(runnable.fields) as k}
{@const meta = runnable.fields[k]}
<RawAppInputsSpecEditor
key={k}
bind:componentInput={runnable.fields[k]}
{id}
shouldCapitalize
fieldType={meta?.['fieldType']}
subFieldType={meta?.['subFieldType']}
format={meta?.['format']}
selectOptions={meta?.['selectOptions']}
tooltip={meta?.['tooltip']}
placeholder={meta?.['placeholder']}
customTitle={meta?.['customTitle']}
loading={meta?.['loading']}
documentationLink={meta?.['documentationLink']}
markdownTooltip={meta?.['markdownTooltip']}
allowTypeChange={meta?.['allowTypeChange']}
displayType
/>
{/each}
</div>
{:else}
<div class="text-tertiary text-sm">No inputs</div>
{/if}
{:else if selectedTab == 'test'}
<SplitPanesWrapper>
<Splitpanes class="grow">
<Pane size={50}>
<div class="px-2 py-3 h-full overflow-auto">
<SchemaForm
on:keydownCmdEnter={testPreview}
disabledArgs={Object.entries(runnable.fields ?? {})
.filter(([k, v]) => v.type == 'static')
.map(([k]) => k)}
schema={getSchema(runnable)}
bind:args
shouldCapitalize
/>
</div>
</Pane>
<Pane size={50}>
<RunnableJobPanelInner frontendJob={false} {testJob} {testIsLoading} />
</Pane>
</Splitpanes>
</SplitPanesWrapper>
{/if}
</svelte:fragment>
</Tabs>
</Pane>
</Splitpanes>
{:else}
<EmptyInlineScript
unusedInlineScripts={[]}
rawApps
on:pick={(e) => onPick(e.detail)}
on:delete
showScriptPicker
on:new={(e) => {
runnable = {
type: 'runnableByName',
inlineScript: e.detail,
name: runnable?.name ?? 'Background Runnable'
}
}}
/>
{/if}
@@ -0,0 +1,61 @@
<script lang="ts">
import { Pane, Splitpanes } from 'svelte-splitpanes'
import { twMerge } from 'tailwind-merge'
import type { Writable } from 'svelte/store'
import { workspaceStore } from '$lib/stores'
import RawAppInlineScriptPanelList from './RawAppInlineScriptPanelList.svelte'
import RawAppInlineScripRunnable from './RawAppInlineScriptRunnable.svelte'
import { createScriptFromInlineScript } from '../apps/editor/inlineScriptsPanel/utils'
import type { Runnable } from '../apps/inputType'
export let runnables: Writable<Record<string, Runnable>>
export let selectedRunnable: string | undefined
export let appPath: string
export let width: number | undefined = undefined
</script>
<Splitpanes
class={twMerge('!overflow-visible')}
style={width !== undefined ? `width:${width}px;` : 'width: 100%;'}
>
<Pane size={20}>
<RawAppInlineScriptPanelList bind:selectedRunnable {runnables} on:hidePanel />
</Pane>
<Pane size={80}>
{#if !selectedRunnable}
<div class="text-sm text-secondary text-center py-8 px-2">
Select a runnable on the left panel
</div>
{:else if $runnables?.[selectedRunnable]}
{#key selectedRunnable}
<RawAppInlineScripRunnable
{appPath}
on:createScriptFromInlineScript={(e) => {
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}
<div class="text-sm text-tertiary text-center py-8 px-2">
No runnable at id {selectedRunnable}
</div>
{/if}
</Pane>
</Splitpanes>
@@ -0,0 +1,113 @@
<script lang="ts">
import { addWhitespaceBeforeCapitals, capitalize, classNames } from '$lib/utils'
import Tooltip from '$lib/components/Tooltip.svelte'
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
import { Loader2, Pen, User } from 'lucide-svelte'
import Toggle from '$lib/components/Toggle.svelte'
import StaticInputEditor from '../apps/editor/settingsPanel/inputEditor/StaticInputEditor.svelte'
import { fieldTypeToTsType } from '../apps/utils'
import type { RichConfiguration } from '../apps/types'
import type { InputType } from '../apps/inputType'
export let id: string
export let componentInput: RichConfiguration
export let key: string
export let shouldCapitalize: boolean = true
export let resourceOnly = false
export let tooltip: string | undefined = undefined
export let fieldType: InputType
export let subFieldType: InputType | undefined
export let format: string | undefined
export let selectOptions: string[] | undefined
export let placeholder: string | undefined
export let customTitle: string | undefined = undefined
export let displayType: boolean = false
export let allowTypeChange: boolean = true
export let loading: boolean = false
export let documentationLink: string | undefined = undefined
export let markdownTooltip: string | undefined = undefined
$: if (componentInput == undefined) {
//@ts-ignore
componentInput = {
type: 'user'
}
}
</script>
{#if !(resourceOnly && (fieldType !== 'object' || !format?.startsWith('resource-')))}
<div class={classNames('flex gap-1', 'flex-col')}>
<div class="flex justify-between items-end">
<div class="flex flex-row gap-4 items-center">
<div class="flex items-center">
<span class="!text-2xs font-semibold text-ellipsis text-primary">
{customTitle
? customTitle
: shouldCapitalize
? capitalize(addWhitespaceBeforeCapitals(key))
: key}
</span>
{#if loading}
<Loader2 size={14} class="animate-spin ml-2" />
{/if}
{#if tooltip || markdownTooltip}
<Tooltip small {documentationLink} {markdownTooltip}>
{tooltip}
</Tooltip>
{/if}
</div>
{#if displayType}
<div class="text-xs text-tertiary mr-1">
{fieldType === 'array' && subFieldType
? `${fieldTypeToTsType(subFieldType)}[]`
: fieldTypeToTsType(fieldType)}
</div>
{/if}
</div>
<div class={classNames('flex gap-x-2 gap-y-1 justify-end items-center')}>
{#if componentInput?.type && allowTypeChange !== false}
<ToggleButtonGroup class="h-7" bind:selected={componentInput.type} let:item>
<ToggleButton {item} value="user" icon={User} iconOnly tooltip="User Input" />
<ToggleButton {item} value="static" icon={Pen} iconOnly tooltip="Static" />
</ToggleButtonGroup>
{/if}
</div>
</div>
{#if componentInput?.type === 'static'}
<div class={'w-full flex flex-row-reverse'}>
<StaticInputEditor
{id}
{fieldType}
{subFieldType}
{selectOptions}
{format}
{placeholder}
bind:componentInput
/>
</div>
{:else if componentInput?.type === 'user' || componentInput?.type == undefined}
<span class="text-2xs italic text-tertiary">Field's value is a frontend input</span>
{/if}
{#if componentInput?.type === 'user' && ((fieldType == 'object' && format?.startsWith('resource-') && format !== 'resource-s3_object') || fieldType == 'resource')}
<div class="flex flex-row items-center">
<Toggle
size="xs"
bind:checked={componentInput.allowUserResources}
options={{
left: 'static resource select only',
right: 'resources from users allowed'
}}
/>
<Tooltip
>Apps are executed on behalf of publishers. If you want to accept resources from user, you
need to enable this (potentially dangerous!)</Tooltip
>
</div>
{/if}
</div>
{/if}
@@ -0,0 +1,23 @@
<script lang="ts">
import { type UserExt } from '$lib/stores'
import type { HiddenRunnable } from '../apps/types'
import RawAppBackgroundRunner from './RawAppBackgroundRunner.svelte'
import { htmlContent } from './utils'
export let workspace: string
export let user: UserExt | undefined
export let version: number
export let path: string
export let runnables: Record<string, HiddenRunnable>
let iframe: HTMLIFrameElement
</script>
<RawAppBackgroundRunner {workspace} editor={false} {iframe} {runnables} {path} />
<iframe
bind:this={iframe}
title="raw-app"
srcDoc={htmlContent(workspace, version, { ctx: user, workspace })}
class="w-full h-full min-h-screen bg-white border-none"
/>
@@ -0,0 +1,109 @@
import type { Schema } from '$lib/common'
import { schemaToTsType } from '$lib/schema'
import { capitalize } from '$lib/utils'
import type { HiddenRunnable } from '../apps/types'
export type RawApp = {
files: string[]
}
export function htmlContent(workspace: string, version: number, ctx: any) {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>App Preview</title>
<link rel="stylesheet" href="/api/w/${workspace}/apps/get_data/v/${version}.css" />
<script>
window.ctx = ${ctx ? JSON.stringify(ctx) : 'undefined'}
</script>
</head>
<body>
<div id="root"></div>
<script src="/api/w/${workspace}/apps/get_data/v/${version}.js"></script>
</body>
</html>
`
}
function removeStaticFields(schema: Schema, fields: Record<string, { type: string }>): Schema {
const staticFields = Object.keys(fields).filter((k) => fields[k].type == 'static')
return {
...schema,
properties: {
...Object.fromEntries(
Object.entries(schema.properties ?? {}).filter(([k]) => !staticFields.includes(k))
)
}
}
}
function hiddenRunnableToTsType(runnable: HiddenRunnable) {
if (runnable.type == 'runnableByName') {
if (runnable.inlineScript?.schema) {
return schemaToTsType(removeStaticFields(runnable.inlineScript?.schema, runnable.fields))
} else {
return '{}'
}
} else if (runnable.type == 'runnableByPath') {
return schemaToTsType(removeStaticFields(runnable.schema, runnable.fields))
} else {
return '{}'
}
}
export function genWmillTs(runnables: Record<string, HiddenRunnable>) {
return `// THIS FILE IS READ-ONLY
// AND GENERATED AUTOMATICALLY FROM YOUR RUNNABLES
${Object.entries(runnables)
.map(([k, v]) => `export type RunBg${capitalize(k)} = ${hiddenRunnableToTsType(v)}\n`)
.join('\n')}
export const runBg = {
${Object.keys(runnables)
.map((k) => ` ${k}: null as unknown as (data: RunBg${capitalize(k)}) => Promise<any>`)
.join(',\n')}
}
export const runBgAsync = {
${Object.keys(runnables)
.map((k) => ` ${k}: null as unknown as (data: RunBg${capitalize(k)}) => Promise<string>`)
.join(',\n')}
}
export type Job = {
type: 'QueuedJob' | 'CompletedJob'
id: string
created_at: number
started_at: number | undefined
duration_ms: number
success: boolean
args: any
result: any
}
/**
* Execute a job and wait for it to complete and return the completed job
* @param id
*/
// @ts-ignore
export function waitJob(id: string): Promise<Job> {
// implementation passed when bundling/deploying
return null as unknown as Promise<Job>
}
/**
* Get a job by id and return immediately with the current state of the job
* @param id
*/
// @ts-ignore
export function getJob(id: string): Promise<Job> {
// implementation passed when bundling/deploying
return null as unknown as Promise<Job>
}
`
}
@@ -13,7 +13,7 @@
export let onlyMaskPassword: boolean = false
export let disablePortal: boolean = false
export let disabled: boolean = false
export let schemaSkippedValues: string[] = []
export let hiddenArgs: string[] = []
export let nestedParent: { label: string; nestedParent: any | undefined } | undefined = undefined
export let disableDnd: boolean = false
export let shouldDispatchChanges: boolean = false
@@ -73,7 +73,7 @@
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<SchemaForm
{nestedClasses}
{schemaSkippedValues}
{hiddenArgs}
on:click
on:change
on:reorder
@@ -221,7 +221,7 @@ export function isAppTainted(app: App) {
return !deepEqual(app, emptyApp)
} else {
// For older apps,
return !(app.grid.length === 0 && app.hiddenInlineScripts?.length === 0)
return !(app.grid?.length === 0 && app.hiddenInlineScripts?.length === 0)
}
}
+3 -2
View File
@@ -3,13 +3,13 @@ import { initServices } from 'monaco-languageclient/vscode/services'
// import getTextmateServiceOverride from '@codingame/monaco-vscode-textmate-service-override'
import getMonarchServiceOverride from '@codingame/monaco-vscode-monarch-service-override'
import '@codingame/monaco-vscode-standalone-typescript-language-features'
import { editor as meditor } from 'monaco-editor/esm/vs/editor/editor.api'
import getConfigurationServiceOverride from '@codingame/monaco-vscode-configuration-service-override'
import { editor as meditor } from 'monaco-editor/esm/vs/editor/editor.api'
export let isInitialized = false
export let isInitializing = false
export async function initializeVscode(caller?: string) {
export async function initializeVscode(caller?: string, htmlContainer?: HTMLElement) {
if (!isInitialized && !isInitializing) {
console.log(`Initializing vscode-api from ${caller ?? 'unknown'}`)
isInitializing = true
@@ -30,6 +30,7 @@ export async function initializeVscode(caller?: string) {
})
}
})
isInitialized = true
meditor.defineTheme('nord', {
base: 'vs-dark',
+58 -1
View File
@@ -49,8 +49,12 @@ export function createLongHash() {
export function langToExt(lang: string): string {
switch (lang) {
case 'tsx':
return 'tsx'
case 'javascript':
return 'ts'
return 'js'
case 'jsx':
return 'js'
case 'bunnative':
return 'ts'
case 'json':
@@ -89,6 +93,59 @@ export function langToExt(lang: string): string {
return 'nu'
case 'java':
return 'java'
case 'svelte':
return 'svelte'
case 'vue':
return 'vue'
default:
return 'unknown'
}
}
export function extToLang(ext: string) {
switch (ext) {
case 'tsx':
return 'typescript'
case 'ts':
return 'typescript'
case 'js':
return 'javascript'
case 'jsx':
return 'javascript'
case 'json':
return 'json'
case 'sql':
return 'sql'
case 'yaml':
return 'yaml'
case 'py':
return 'python'
case 'go':
return 'go'
case 'sh':
return 'bash'
case 'ps1':
return 'powershell'
case 'php':
return 'php'
case 'rs':
return 'rust'
case 'gql':
return 'graphql'
case 'css':
return 'css'
case 'yml':
return 'ansible'
case 'cs':
return 'csharp'
case 'svelte':
return 'svelte'
case 'vue':
return 'vue'
case 'nu':
return 'nu'
case 'java':
return 'java'
// for related places search: ADD_NEW_LANG
default:
return 'unknown'
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -32,7 +32,7 @@ export function schemaToTsType(schema: Schema | SchemaProperty): string {
return `${prefix}: ${type}`
})
.join(';')
.join('; ')
return `{ ${types} }`
}
+15 -4
View File
@@ -5,16 +5,27 @@ import { FlowService, type Script, ScriptService, ScheduleService } from './gen'
import { workspaceStore } from './stores'
export function scriptLangToEditorLang(
lang: Script['language'] | 'bunnative' | 'frontend' | undefined
lang:
| Script['language']
| 'bunnative'
| 'javascript'
| 'frontend'
| 'jsx'
| 'tsx'
| 'text'
| 'json'
| undefined
) {
if (lang == 'deno') {
return 'typescript'
} else if (lang == 'bun' || lang == 'bunnative' || lang == 'frontend') {
} else if (lang == 'bun' || lang == 'bunnative' || lang == 'frontend' || lang == 'tsx') {
return 'typescript'
} else if (lang == 'nativets') {
return 'typescript'
// } else if (lang == 'graphql') {
// return 'typescript'
} else if (lang == 'text') {
return 'text'
} else if (lang == 'javascript' || lang == 'jsx') {
return 'javascript'
} else if (lang == 'postgresql') {
return 'sql'
} else if (lang == 'mysql') {
+313
View File
@@ -0,0 +1,313 @@
// taken from node_modules/monaco-editor/dev/vs/basic-languages/html/html.js
// modifications:
// 1: replaced 'monaco_editor_core_1.' with '' (added import)
// 2: replaced 'exports.' with 'export const '
// 3: replaced 'text/javascript' with 'text/typescript'
// 4: added basic moustache keywords
import { languages } from 'monaco-editor'
var EMPTY_ELEMENTS = [
'area',
'base',
'br',
'col',
'embed',
'hr',
'img',
'input',
'keygen',
'link',
'menuitem',
'meta',
'param',
'source',
'track',
'wbr'
]
export const conf = {
wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,
comments: {
blockComment: ['<!--', '-->']
},
brackets: [
['<!--', '-->'],
['<', '>'],
['{', '}'],
['(', ')']
],
autoClosingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '"', close: '"' },
{ open: "'", close: "'" }
],
surroundingPairs: [
{ open: '"', close: '"' },
{ open: "'", close: "'" },
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '<', close: '>' }
],
onEnterRules: [
{
beforeText: new RegExp(
'<(?!(?:' + EMPTY_ELEMENTS.join('|') + '))([_:\\w][_:\\w-.\\d]*)([^/>]*(?!/)>)[^<]*$',
'i'
),
afterText: /^<\/([_:\w][_:\w-.\d]*)\s*>$/i,
action: {
indentAction: languages.IndentAction.IndentOutdent
}
},
{
beforeText: new RegExp(
'<(?!(?:' + EMPTY_ELEMENTS.join('|') + '))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$',
'i'
),
action: { indentAction: languages.IndentAction.Indent }
}
],
folding: {
markers: {
start: new RegExp('^\\s*<!--\\s*#region\\b.*-->'),
end: new RegExp('^\\s*<!--\\s*#endregion\\b.*-->')
}
}
}
export const language = {
defaultToken: '',
tokenPostfix: '.html',
ignoreCase: true,
moustkeys: [
'#if',
'/if',
':else if',
':else',
'as',
'#each',
'/each',
'#await',
':then',
':catch',
'/await',
'#key',
'/key',
'@html',
'@debug'
],
// The main tokenizer for our languages
tokenizer: {
root: [
[/<!DOCTYPE/, 'metatag', '@doctype'],
[/<!--/, 'comment', '@comment'],
[/{/, 'delimiter', '@curly'],
[/(<)((?:[\w\-]+:)?[\w\-]+)(\s*)(\/>)/, ['delimiter', 'tag', '', 'delimiter']],
[/(<)(script)/, ['delimiter', { token: 'tag', next: '@script' }]],
[/(<)(style)/, ['delimiter', { token: 'tag', next: '@style' }]],
[/(<)((?:[\w\-]+:)?[\w\-]+)/, ['delimiter', { token: 'tag', next: '@otherTag' }]],
[/(<\/)((?:[\w\-]+:)?[\w\-]+)/, ['delimiter', { token: 'tag', next: '@otherTag' }]],
[/</, 'delimiter'],
[/[^<{]+/] // text
],
doctype: [
[/[^>]+/, 'metatag.content'],
[/>/, 'metatag', '@pop']
],
comment: [
[/-->/, 'comment', '@pop'],
[/[^-]+/, 'comment.content'],
[/./, 'comment.content']
],
curly: [
[/[ \t\r\n]+/],
[/{/, 'delimiter', '@push'],
[
/[^}\s]+/,
{
cases: {
'@moustkeys': 'keyword',
'@default': 'tag'
}
}
],
[/}/, 'delimiter', '@pop']
],
otherTag: [
[/\/?>/, 'delimiter', '@pop'],
[/"([^"]*)"/, 'attribute.value'],
[/'([^']*)'/, 'attribute.value'],
[/[\w\-]+/, 'attribute.name'],
[/=/, 'delimiter'],
[/[ \t\r\n]+/] // whitespace
],
// -- BEGIN <script> tags handling
// After <script
script: [
[/type/, 'attribute.name', '@scriptAfterType'],
[/"([^"]*)"/, 'attribute.value'],
[/'([^']*)'/, 'attribute.value'],
[/[\w\-]+/, 'attribute.name'],
[/=/, 'delimiter'],
[
/>/,
{
token: 'delimiter',
next: '@scriptEmbedded',
nextEmbedded: 'text/typescript'
}
],
[/[ \t\r\n]+/],
[/(<\/)(script\s*)(>)/, ['delimiter', 'tag', { token: 'delimiter', next: '@pop' }]]
],
// After <script ... type
scriptAfterType: [
[/=/, 'delimiter', '@scriptAfterTypeEquals'],
[
/>/,
{
token: 'delimiter',
next: '@scriptEmbedded',
nextEmbedded: 'text/typescript'
}
],
[/[ \t\r\n]+/],
[/<\/script\s*>/, { token: '@rematch', next: '@pop' }]
],
// After <script ... type =
scriptAfterTypeEquals: [
[
/"([^"]*)"/,
{
token: 'attribute.value',
switchTo: '@scriptWithCustomType.$1'
}
],
[
/'([^']*)'/,
{
token: 'attribute.value',
switchTo: '@scriptWithCustomType.$1'
}
],
[
/>/,
{
token: 'delimiter',
next: '@scriptEmbedded',
nextEmbedded: 'text/typescript'
}
],
[/[ \t\r\n]+/],
[/<\/script\s*>/, { token: '@rematch', next: '@pop' }]
],
// After <script ... type = $S2
scriptWithCustomType: [
[
/>/,
{
token: 'delimiter',
next: '@scriptEmbedded.$S2',
nextEmbedded: '$S2'
}
],
[/"([^"]*)"/, 'attribute.value'],
[/'([^']*)'/, 'attribute.value'],
[/[\w\-]+/, 'attribute.name'],
[/=/, 'delimiter'],
[/[ \t\r\n]+/],
[/<\/script\s*>/, { token: '@rematch', next: '@pop' }]
],
scriptEmbedded: [
[/<\/script/, { token: '@rematch', next: '@pop', nextEmbedded: '@pop' }],
[/[^<]+/, '']
],
// -- END <script> tags handling
// -- BEGIN <style> tags handling
// After <style
style: [
[/type/, 'attribute.name', '@styleAfterType'],
[/"([^"]*)"/, 'attribute.value'],
[/'([^']*)'/, 'attribute.value'],
[/[\w\-]+/, 'attribute.name'],
[/=/, 'delimiter'],
[
/>/,
{
token: 'delimiter',
next: '@styleEmbedded',
nextEmbedded: 'text/css'
}
],
[/[ \t\r\n]+/],
[/(<\/)(style\s*)(>)/, ['delimiter', 'tag', { token: 'delimiter', next: '@pop' }]]
],
// After <style ... type
styleAfterType: [
[/=/, 'delimiter', '@styleAfterTypeEquals'],
[
/>/,
{
token: 'delimiter',
next: '@styleEmbedded',
nextEmbedded: 'text/css'
}
],
[/[ \t\r\n]+/],
[/<\/style\s*>/, { token: '@rematch', next: '@pop' }]
],
// After <style ... type =
styleAfterTypeEquals: [
[
/"([^"]*)"/,
{
token: 'attribute.value',
switchTo: '@styleWithCustomType.$1'
}
],
[
/'([^']*)'/,
{
token: 'attribute.value',
switchTo: '@styleWithCustomType.$1'
}
],
[
/>/,
{
token: 'delimiter',
next: '@styleEmbedded',
nextEmbedded: 'text/css'
}
],
[/[ \t\r\n]+/],
[/<\/style\s*>/, { token: '@rematch', next: '@pop' }]
],
// After <style ... type = $S2
styleWithCustomType: [
[
/>/,
{
token: 'delimiter',
next: '@styleEmbedded.$S2',
nextEmbedded: '$S2'
}
],
[/"([^"]*)"/, 'attribute.value'],
[/'([^']*)'/, 'attribute.value'],
[/[\w\-]+/, 'attribute.name'],
[/=/, 'delimiter'],
[/[ \t\r\n]+/],
[/<\/style\s*>/, { token: '@rematch', next: '@pop' }]
],
styleEmbedded: [
[/<\/style/, { token: '@rematch', next: '@pop', nextEmbedded: '@pop' }],
[/[^<]+/, '']
]
// -- END <style> tags handling
}
}
+270
View File
@@ -0,0 +1,270 @@
// Register Vue language
import { languages } from 'monaco-editor'
const wordPattern = new RegExp(
'(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\#\\%\\^\\&\\*\\(\\)\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\"\\,\\.\\<\\>\\/\\?\\s]+)',
'g'
)
// Regex for auto indentation
const indentationRules = {
increaseIndentPattern: new RegExp('<(?!\\/(?:template|style|script))([\\w:\\-]+)[^>]*>[^<]*$'),
decreaseIndentPattern: new RegExp('^<\\/[\\w:\\-]+>$')
}
// Regex for enter rules
const enterRules = [
{
beforeText: new RegExp(
'<(?!(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr))([_:\\w][_:\\w-.]*)([^/>]*(?!/.)>)[^<]*$',
'i'
),
afterText: new RegExp('^<\\/([_:\\w][_:\\w-.]*)>\\s*$', 'i'),
action: { indentAction: languages.IndentAction.IndentOutdent }
},
{
beforeText: new RegExp(
'<(?!(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr))([_:\\w][_:\\w-.]*)([^/>]*(?!/.)>)[^<]*$',
'i'
),
action: { indentAction: languages.IndentAction.Indent }
}
]
export const conf = {
wordPattern,
indentationRules,
onEnterRules: enterRules,
// ... rest of the configuration remains the same
autoClosingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '"', close: '"' },
{ open: "'", close: "'" },
{ open: '`', close: '`' },
{ open: '<!--', close: '-->' },
{ open: '<template', close: '</template>' },
{ open: '<script', close: '</script>' },
{ open: '<style', close: '</style>' }
],
surroundingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '"', close: '"' },
{ open: "'", close: "'" },
{ open: '`', close: '`' },
{ open: '<', close: '>' }
],
comments: {
lineComment: '//',
blockComment: ['/*', '*/']
},
brackets: [
['{', '}'],
['[', ']'],
['(', ')'],
['<', '>']
]
}
// Define Vue tokens and language rules
export const language = {
defaultToken: '',
tokenPostfix: '.vue',
exponent: /[eE][\-+]?\d+/,
// Regular expressions for different token types
brackets: [
{ open: '{', close: '}', token: 'delimiter.curly' },
{ open: '[', close: ']', token: 'delimiter.square' },
{ open: '(', close: ')', token: 'delimiter.parenthesis' },
{ open: '<', close: '>', token: 'delimiter.angle' }
],
keywords: [
'template',
'script',
'style',
'props',
'setup',
'emit',
'computed',
'ref',
'reactive',
'watch',
'watchEffect',
'onMounted',
'onUpdated',
'onUnmounted',
'defineProps',
'defineEmits',
'defineExpose',
'withDefaults'
],
typeKeywords: ['boolean', 'number', 'string', 'object', 'array'],
operators: [
'=',
'>',
'<',
'!',
'~',
'?',
':',
'==',
'<=',
'>=',
'!=',
'&&',
'||',
'++',
'--',
'+',
'-',
'*',
'/',
'&',
'|',
'^',
'%',
'<<',
'>>',
'>>>',
'+=',
'-=',
'*=',
'/=',
'&=',
'|=',
'^=',
'%=',
'<<=',
'>>=',
'>>>='
],
// Symbols that can be used in identifiers
symbols: /[=><!~?:&|+\-*\/\^%]+/,
escapes: /\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,
// The main tokenizer
tokenizer: {
root: [
// Template block
[/<template.*>/, { token: 'tag', next: '@template' }],
// Script block
[/<script.*>/, { token: 'tag', next: '@script' }],
// Style block
[/<style.*>/, { token: 'tag', next: '@style' }],
// Other content
{ include: '@whitespace' },
{ include: '@tags' }
],
// Template section
template: [
[/<\/template>/, { token: 'tag', next: '@pop' }],
[/{{/, { token: 'delimiter.bracket', next: '@templateExpression' }],
[/<\/?[\w\-:.]+/, 'tag'],
[/\s+/, ''],
[/[^<{]+/, 'html']
],
// Script section
script: [[/<\/script>/, { token: 'tag', next: '@pop' }], { include: '@javascript' }],
// Style section
style: [[/<\/style>/, { token: 'tag', next: '@pop' }], { include: '@css' }],
// Template expressions (inside {{ }})
templateExpression: [
[/}}/, { token: 'delimiter.bracket', next: '@pop' }],
[/[a-zA-Z_]\w*/, 'identifier'],
{ include: '@javascript' }
],
// JavaScript rules
javascript: [
[/[{}]/, 'delimiter.bracket'],
[/[\[\]]/, 'delimiter.square'],
[/[()]/, 'delimiter.parenthesis'],
[
/[a-zA-Z_]\w*/,
{
cases: {
'@keywords': 'keyword',
'@typeKeywords': 'type',
'@default': 'identifier'
}
}
],
[/[<>](?!@symbols)/, 'tag'],
[
/@symbols/,
{
cases: {
'@operators': 'operator',
'@default': ''
}
}
],
[/\d+\.\d*(@exponent)?/, 'number.float'],
[/\.\d+(@exponent)?/, 'number.float'],
[/\d+@exponent/, 'number.float'],
[/\d+/, 'number'],
[/[;,.]/, 'delimiter'],
[/"([^"\\]|\\.)*$/, 'string.invalid'],
[/'([^'\\]|\\.)*$/, 'string.invalid'],
[/"/, 'string', '@string_double'],
[/'/, 'string', '@string_single']
],
// CSS rules
css: [
[/[{}]/, 'delimiter.bracket'],
[/[\[\]]/, 'delimiter.square'],
[/[()]/, 'delimiter.parenthesis'],
[/[-a-zA-Z_][\w-]*/, 'attribute.name'],
[/(url\()([^)]]*)(\))/, ['tag', 'string', 'tag']],
[/[@.][-a-zA-Z_][\w-]*/, 'tag'],
[/[<>](?!@symbols)/, 'tag'],
[/#[-a-zA-Z_][\w-]*/, 'tag'],
[/\d+\.\d*(@exponent)?/, 'number.float'],
[/\.\d+(@exponent)?/, 'number.float'],
[/\d+@exponent/, 'number.float'],
[/\d+/, 'number'],
[/[;,.]/, 'delimiter']
],
whitespace: [
[/[ \t\r\n]+/, 'white'],
[/\/\*/, 'comment', '@comment'],
[/\/\/.*$/, 'comment']
],
comment: [
[/[^\/*]+/, 'comment'],
[/\*\//, 'comment', '@pop'],
[/[\/*]/, 'comment']
],
string_double: [
[/[^\\"]+/, 'string'],
[/@escapes/, 'string.escape'],
[/\\./, 'string.escape.invalid'],
[/"/, 'string', '@pop']
],
string_single: [
[/[^\\']+/, 'string'],
[/@escapes/, 'string.escape'],
[/\\./, 'string.escape.invalid'],
[/'/, 'string', '@pop']
],
tags: [[/<\/?[\w\-:.]+/, 'tag']]
}
}
@@ -36,7 +36,8 @@
}
})
const initialState = nodraft ? undefined : localStorage.getItem(`app-${$page.params.path}`)
let stateLoadedFromUrl = initialState != undefined ? decodeState(initialState) : undefined
let stateLoadedFromLocalStorage =
initialState != undefined ? decodeState(initialState) : undefined
async function loadApp(): Promise<void> {
const app_w_draft = await AppService.getAppByPathWithDraft({
@@ -65,14 +66,14 @@
custom_path: app_w_draft_.custom_path
}
if (stateLoadedFromUrl) {
if (stateLoadedFromLocalStorage) {
const reloadAction = async () => {
stateLoadedFromUrl = undefined
stateLoadedFromLocalStorage = undefined
await loadApp()
redraw++
}
const actions: ToastAction[] = []
if (stateLoadedFromUrl) {
if (stateLoadedFromLocalStorage) {
actions.push({
label: 'Discard browser autosave and reload',
callback: reloadAction
@@ -81,7 +82,7 @@
const draftOrDeployed = cleanValueProperties(savedApp.draft || savedApp)
const urlScript = {
...draftOrDeployed,
value: stateLoadedFromUrl
value: stateLoadedFromLocalStorage
}
actions.push({
label: 'Show diff',
@@ -99,7 +100,7 @@
}
sendUserToast('App restored from browser storage', false, actions)
app_w_draft.value = stateLoadedFromUrl
app_w_draft.value = stateLoadedFromLocalStorage
app = app_w_draft
} else if (app_w_draft.draft) {
if (app_w_draft.summary !== undefined) {
@@ -117,7 +118,7 @@
if (!app_w_draft.draft_only) {
const reloadAction = () => {
stateLoadedFromUrl = undefined
stateLoadedFromLocalStorage = undefined
app = app_w_draft
redraw++
}
@@ -0,0 +1,190 @@
<script lang="ts">
import { importStore } from '$lib/components/apps/store'
import { AppService, type Policy } from '$lib/gen'
import { page } from '$app/stores'
import { decodeState } from '$lib/utils'
import { userStore, workspaceStore } from '$lib/stores'
import { afterNavigate, replaceState } from '$app/navigation'
import { goto } from '$lib/navigation'
import { sendUserToast } from '$lib/toast'
import RawAppEditor from '$lib/components/raw_apps/RawAppEditor.svelte'
import type { HiddenRunnable } from '$lib/components/apps/types'
import Modal from '$lib/components/common/modal/Modal.svelte'
import FileEditorIcon from '$lib/components/raw_apps/FileEditorIcon.svelte'
import { react18Template, react19Template, svelte5Template, vueTemplate } from './templates'
let nodraft = $page.url.searchParams.get('nodraft')
const templatePath = $page.url.searchParams.get('template')
const templateId = $page.url.searchParams.get('template_id')
const importRaw = $importStore
if ($importStore) {
$importStore = undefined
}
const state = nodraft ? undefined : localStorage.getItem('rawapp')
let summary = ''
let files: Record<string, string> = react19Template
afterNavigate(() => {
if (nodraft) {
let url = new URL($page.url.href)
url.search = ''
replaceState(url.toString(), $page.state)
}
})
let policy: Policy = {
on_behalf_of: $userStore?.username.includes('@')
? $userStore?.username
: `u/${$userStore?.username}`,
on_behalf_of_email: $userStore?.email,
execution_mode: 'publisher'
}
let runnables: Record<string, HiddenRunnable> = {
a: {
name: 'a',
fields: {},
recomputeIds: [],
type: 'runnableByName',
inlineScript: {
content:
'// import * as wmill from "windmill-client"\n\nexport async function main(x: string) {\n return x\n}\n',
language: 'bun',
schema: {
$schema: 'https://json-schema.org/draft/2020-12/schema',
properties: {
x: {
default: null,
description: '',
originalType: 'string',
type: 'string'
}
},
required: ['x'],
type: 'object'
}
}
}
}
loadApp()
function extractValue(value: any) {
files = value.files
runnables = value.runnables
}
async function loadApp() {
if (importRaw) {
sendUserToast('Loaded from YAML/JSON')
if ('value' in importRaw) {
summary = importRaw.summary
extractValue(importRaw.value)
policy = importRaw.policy
} else {
extractValue(importRaw)
}
console.log('importRaw', importRaw)
} else if (templatePath) {
const template = await AppService.getAppByPath({
workspace: $workspaceStore!,
path: templatePath
})
extractValue(template.value)
console.log('App loaded from template')
sendUserToast('App loaded from template path')
goto('?', { replaceState: true })
} else if (templateId) {
const template = await AppService.getAppByVersion({
workspace: $workspaceStore!,
id: parseInt(templateId)
})
extractValue(template.value)
console.log('App loaded from template id')
sendUserToast('App loaded from template')
goto('?', { replaceState: true })
} else if (!templatePath && state) {
console.log('App loaded from browser stored autosave')
sendUserToast('App restored from browser stored autosave', false, [
{
label: 'Start from blank',
callback: () => {
files = {}
runnables = {}
}
}
])
let decoded = decodeState(state)
extractValue(decoded)
}
}
const templates = [
{
name: 'React 19',
icon: 'tsx',
files: undefined,
selected: true
},
{
name: 'React 18',
icon: 'tsx',
files: react18Template
},
{
name: 'Svelte 5',
icon: 'svelte',
files: svelte5Template
},
{
name: 'Vue 3',
icon: 'vue',
files: vueTemplate
}
]
let templatePicker = nodraft != null
let hide = false
</script>
{#if templatePicker}
<Modal kind="X" open title="Templates">
<div class="flex flex-wrap gap-4 pb-4">
{#each templates as t}
<button
on:click={() => {
if (t.files) {
hide = true
files = t.files
hide = false
}
templatePicker = false
}}
class="w-24 h-24 flex justify-between py-5 flex-col {t.selected
? 'bg-surface-selected'
: ''} hover:bg-surface-hover border rounded-lg"
>
<div class="w-full flex items-center justify-center">
<FileEditorIcon file={'.' + t.icon} />
</div>
<div class="center-center w-full">{t.name}</div>
</button>
{/each}
</div>
</Modal>
{/if}
{#if !hide}
<RawAppEditor
on:savedNewAppPath={(event) => {
goto(`/apps_raw/edit/${event.detail}`)
}}
initFiles={files}
initRunnables={runnables}
{policy}
path={''}
{summary}
newApp
/>
{/if}
@@ -0,0 +1,585 @@
const reactIndex = `
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
const root = createRoot(document.getElementById('root')!);
root.render(<App/>);
`
const appTsx = `import React, { useState } from 'react'
import { runBg } from './wmill'
import './index.css'
const App = () => {
const [value, setValue] = useState(undefined as string | undefined)
const [loading, setLoading] = useState(false)
async function runA() {
setLoading(true)
try {
setValue(await runBg.a({ x: 42 }))
} catch (e) {
console.error()
}
setLoading(false)
}
return <div style={{ width: "100%" }}>
<h1>hello world</h1>
<button style={{ marginTop: "2px" }} onClick={runA}>Run 'a'</button>
<div style={{ marginTop: "20px", width: '250px' }} className='myclass'>
{loading ? 'Loading ...' : value ?? 'Click button to see value here'}
</div>
</div>;
};
export default App;
`;
const appSvelte = `<style>
h1 {
font-size: 1.5rem;
}
</style>
<main>
<h1>Hello {name}</h1>
</main>
<script>
let name = 'world';
</script>`
const indexSvelte = `
import { mount } from 'svelte';
import App from './App.svelte'
import './index.css'
const app = mount(App, { target: document.getElementById("root")! });
export default app;
`
const appVue = `<template>
<h1>Hello {{ msg }}</h1>
</template>
<script setup>
import { ref } from 'vue';
const msg = ref('world');
</script>`
const indexVue = `import { createApp } from 'vue'
import App from './App.vue'
import "./index.css";
createApp(App).mount('#root')`
const indexCss = `.myclass {
border: 1px solid gray;
padding: 2px;
}`;
export const react19Template = {
'/index.tsx': reactIndex,
'/App.tsx': appTsx,
'/index.css': indexCss,
'/package.json': `{
"dependencies": {
"react": "19.0.0",
"react-dom": "19.0.0",
"windmill-client": "^1"
},
"devDependencies": {
"@types/react-dom": "^19.0.0",
"@types/react": "^19.0.0"
}
}`,
}
export const react18Template = {
'/index.tsx': reactIndex,
'/App.tsx': appTsx,
'/index.css': indexCss,
'/package.json': `{
"dependencies": {
"react": "18.3.1",
"react-dom": "18.3.1"
},
"devDependencies": {
"@types/react-dom": "^19.0.0",
"@types/react": "^19.0.0"
}
}`,
}
export const svelte5Template = {
'/index.ts': indexSvelte,
'/App.svelte': appSvelte,
'/index.css': indexCss,
'/package.json': `{
"dependencies": {
"svelte": "5.16.1",
"windmill-client": "^1"
}
}`,
}
export const vueTemplate = {
'/index.ts': indexVue,
'/App.vue': appVue,
'/index.css': indexCss,
'/package.json': `{
"dependencies": {
"core-js": "3.26.1",
"vue": "3.5.13"
}
}`,
}
export const appVueRouter = `
<template>
<div class="container">
<!-- Navigation tabs -->
<nav class="tabs">
<button
v-for="tab in tabs"
:key="tab.id"
:class="{ active: currentTab === tab.id }"
@click="changeTab(tab.id)"
>
{{ tab.name }}
</button>
</nav>
<!-- Content sections -->
<div class="content">
<div v-if="currentTab === 'home'" class="tab-content">
<h2>Home</h2>
<p>Welcome to the home tab!</p>
<!-- Nested navigation example -->
<div class="sub-nav">
<button
v-for="subItem in ['latest', 'popular']"
:key="subItem"
:class="{ active: currentSort === subItem }"
@click="changeSort(subItem)"
>
{{ subItem }}
</button>
</div>
</div>
<div v-else-if="currentTab === 'about'" class="tab-content">
<h2>About</h2>
<p>This is the about section</p>
</div>
<div v-else-if="currentTab === 'contact'" class="tab-content">
<h2>Contact</h2>
<p>Contact information here</p>
</div>
</div>
<!-- Debug info -->
<div class="debug">
<p>Current Tab: {{ currentTab }}</p>
<p>Current Sort: {{ currentSort }}</p>
<p>Current Hash: {{ currentHash }}</p>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
// Available tabs
const tabs = [
{ id: 'home', name: 'Home' },
{ id: 'about', name: 'About' },
{ id: 'contact', name: 'Contact' }
];
const currentTab = ref('home');
const currentSort = ref('latest');
const currentHash = ref('');
// Navigation functions
function changeTab(tabId) {
currentTab.value = tabId;
updateHash();
}
function changeSort(sort) {
currentSort.value = sort;
updateHash();
}
function updateHash() {
const params = new URLSearchParams();
params.set('tab', currentTab.value);
if (currentSort.value !== 'latest') {
params.set('sort', currentSort.value);
}
window.location.hash = params.toString();
currentHash.value = window.location.hash;
}
function parseHash() {
const params = new URLSearchParams(window.location.hash.slice(1));
currentTab.value = params.get('tab') || 'home';
currentSort.value = params.get('sort') || 'latest';
currentHash.value = window.location.hash;
}
// Hash change handler
function handleHashChange() {
parseHash();
}
onMounted(() => {
if (window.location.hash) {
parseHash();
} else {
updateHash();
}
window.addEventListener('hashchange', handleHashChange);
});
onUnmounted(() => {
window.removeEventListener('hashchange', handleHashChange);
});
</script>
<style scoped>
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
button {
padding: 8px 16px;
border: 1px solid #ddd;
background: #fff;
border-radius: 4px;
cursor: pointer;
}
button.active {
background: #4CAF50;
color: white;
border-color: #4CAF50;
}
.content {
padding: 20px;
border: 1px solid #ddd;
border-radius: 4px;
}
.tab-content {
margin-bottom: 20px;
}
.sub-nav {
margin-top: 10px;
}
.debug {
margin-top: 20px;
padding: 10px;
background: #f5f5f5;
border-radius: 4px;
font-size: 0.9em;
color: #666;
}
</style>`
export const appSvelteRouter = `
<script>
import { onMount, onDestroy } from 'svelte';
// Available tabs
const tabs = [
{ id: 'home', name: 'Home' },
{ id: 'about', name: 'About' },
{ id: 'contact', name: 'Contact' }
];
let currentTab = 'home';
let currentSort = 'latest';
let currentHash = '';
// Navigation functions
function changeTab(tabId) {
currentTab = tabId;
updateHash();
}
function changeSort(sort) {
currentSort = sort;
updateHash();
}
function updateHash() {
const params = new URLSearchParams();
params.set('tab', currentTab);
if (currentSort !== 'latest') {
params.set('sort', currentSort);
}
window.location.hash = params.toString();
currentHash = window.location.hash;
}
function parseHash() {
const params = new URLSearchParams(window.location.hash.slice(1));
currentTab = params.get('tab') || 'home';
currentSort = params.get('sort') || 'latest';
currentHash = window.location.hash;
}
// Hash change handler
function handleHashChange() {
parseHash();
}
onMount(() => {
if (window.location.hash) {
parseHash();
} else {
updateHash();
}
window.addEventListener('hashchange', handleHashChange);
});
onDestroy(() => {
window.removeEventListener('hashchange', handleHashChange);
});
</script>
<div class="container">
<!-- Navigation tabs -->
<nav class="tabs">
{#each tabs as tab}
<button
class:active={currentTab === tab.id}
on:click={() => changeTab(tab.id)}
>
{tab.name}
</button>
{/each}
</nav>
<!-- Content sections -->
<div class="content">
{#if currentTab === 'home'}
<div class="tab-content">
<h2>Home</h2>
<p>Welcome to the home tab!</p>
<!-- Nested navigation example -->
<div class="sub-nav">
{#each ['latest', 'popular'] as subItem}
<button
class:active={currentSort === subItem}
on:click={() => changeSort(subItem)}
>
{subItem}
</button>
{/each}
</div>
</div>
{:else if currentTab === 'about'}
<div class="tab-content">
<h2>About</h2>
<p>This is the about section</p>
</div>
{:else if currentTab === 'contact'}
<div class="tab-content">
<h2>Contact</h2>
<p>Contact information here</p>
</div>
{/if}
</div>
<!-- Debug info -->
<div class="debug">
<p>Current Tab: {currentTab}</p>
<p>Current Sort: {currentSort}</p>
<p>Current Hash: {currentHash}</p>
</div>
</div>
<style>
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
button {
padding: 8px 16px;
border: 1px solid #ddd;
background: #fff;
border-radius: 4px;
cursor: pointer;
}
button.active {
background: #4CAF50;
color: white;
border-color: #4CAF50;
}
.content {
padding: 20px;
border: 1px solid #ddd;
border-radius: 4px;
}
.tab-content {
margin-bottom: 20px;
}
.sub-nav {
margin-top: 10px;
}
.debug {
margin-top: 20px;
padding: 10px;
background: #f5f5f5;
border-radius: 4px;
font-size: 0.9em;
color: #666;
}
</style>`
export const appReactRouter = `
import React, { useState, useEffect } from 'react';
const tabs = [
{ id: 'home', name: 'Home' },
{ id: 'about', name: 'About' },
{ id: 'contact', name: 'Contact' }
];
const App = () => {
const [currentTab, setCurrentTab] = useState('home');
const [currentSort, setCurrentSort] = useState('latest');
const [currentHash, setCurrentHash] = useState('');
const updateHash = (newTab?: string, newSort?: string) => {
const params = new URLSearchParams();
params.set('tab', newTab ?? currentTab);
if ((newSort ?? currentSort) !== 'latest') {
params.set('sort', newSort ?? currentSort);
}
window.location.hash = params.toString();
setCurrentHash(window.location.hash);
if (newTab) setCurrentTab(newTab);
if (newSort) setCurrentSort(newSort);
};
const parseHash = () => {
const params = new URLSearchParams(window.location.hash.slice(1));
setCurrentTab(params.get('tab') || 'home');
setCurrentSort(params.get('sort') || 'latest');
setCurrentHash(window.location.hash);
};
useEffect(() => {
if (!window.location.hash) {
updateHash(currentTab, currentSort);
} else {
parseHash();
}
const handleHashChange = () => parseHash();
window.addEventListener('hashchange', handleHashChange);
return () => window.removeEventListener('hashchange', handleHashChange);
}, []);
return (
<div className="max-w-3xl mx-auto p-5">
<nav className="flex gap-3 mb-5">
{tabs.map(tab => (
<button
key={tab.id}
onClick={() => updateHash(tab.id)}
className={\`px-4 py-2 border rounded-md cursor-pointer
$\{currentTab === tab.id
? 'bg-green-500 text-white border-green-500'
: 'bg-white border-gray-300 hover:bg-gray-50'}\`}
>
{tab.name}
</button>
))}
</nav>
<div className="p-5 border rounded-md">
{currentTab === 'home' && (
<div className="mb-5">
<h2 className="text-xl font-bold">Home</h2>
<p>Welcome to the home tab!</p>
<div className="mt-3 flex gap-3">
{['latest', 'popular'].map(sort => (
<button
key={sort}
onClick={() => updateHash(currentTab, sort)}
className={\`px-4 py-2 border rounded-md cursor-pointer
$\{
currentSort === sort
? 'bg-green-500 text-white border-green-500'
: 'bg-white border-gray-300 hover:bg-gray-50'
}\`}
>
{sort}
</button>
))}
</div>
</div>
)}
{currentTab === 'about' && (
<div>
<h2 className="text-xl font-bold">About</h2>
<p>This is the about section</p>
</div>
)}
{currentTab === 'contact' && (
<div>
<h2 className="text-xl font-bold">Contact</h2>
<p>Contact information here</p>
</div>
)}
</div>
<div className="mt-5 p-3 bg-gray-100 rounded-md text-sm text-gray-600">
<p>Current Tab: {currentTab}</p>
<p>Current Sort: {currentSort}</p>
<p>Current Hash: {currentHash}</p>
</div>
</div>
);
};
export default App
`
@@ -0,0 +1,5 @@
export function load({ params }) {
return {
stuff: { title: `Edit App ${params.path}` }
}
}
@@ -0,0 +1,226 @@
<script lang="ts">
import { AppService, DraftService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { page } from '$app/stores'
import { cleanValueProperties, decodeState, type Value } from '$lib/utils'
import { afterNavigate, replaceState } from '$app/navigation'
import { goto } from '$lib/navigation'
import { sendUserToast, type ToastAction } from '$lib/toast'
import DiffDrawer from '$lib/components/DiffDrawer.svelte'
import type { HiddenRunnable } from '$lib/components/apps/types'
import RawAppEditor from '$lib/components/raw_apps/RawAppEditor.svelte'
let files: Record<string, string> | undefined = undefined
let runnables = {}
let newPath = ''
let lastVersion = 0
let policy: any = {}
let summary = ''
let savedApp:
| {
value: {
files: Record<string, { code: string }>
runnables: Record<string, HiddenRunnable>
}
draft?: any
path: string
summary: string
policy: any
draft_only?: boolean
custom_path?: string
}
| undefined = undefined
let redraw = 0
let path = $page.params.path
let nodraft = $page.url.searchParams.get('nodraft')
afterNavigate(() => {
if (nodraft) {
let url = new URL($page.url.href)
url.search = ''
replaceState(url.toString(), $page.state)
}
})
function extractRawApp(app: any) {
runnables = app.value.runnables
files = app.value.files
summary = app.summary
lastVersion = app.version
policy = app.policy
newPath = app.path
}
const initialState = nodraft ? undefined : localStorage.getItem(`rawapp-${$page.params.path}`)
let stateLoadedFromLocalStorage =
initialState != undefined ? decodeState(initialState) : undefined
async function loadApp(): Promise<void> {
const app_w_draft = await AppService.getAppByPathWithDraft({
path,
workspace: $workspaceStore!
})
const app_w_draft_ = structuredClone(app_w_draft)
savedApp = {
summary: app_w_draft_.summary,
value: app_w_draft_.value as any,
path: app_w_draft_.path,
policy: app_w_draft_.policy,
draft_only: app_w_draft_.draft_only,
draft: app_w_draft_.draft,
custom_path: app_w_draft_.custom_path
}
if (stateLoadedFromLocalStorage) {
const reloadAction = async () => {
stateLoadedFromLocalStorage = undefined
await loadApp()
redraw++
}
const actions: ToastAction[] = []
if (stateLoadedFromLocalStorage) {
actions.push({
label: 'Discard browser autosave and reload',
callback: reloadAction
})
const draftOrDeployed = cleanValueProperties(savedApp.draft || savedApp)
const urlScript = {
...draftOrDeployed,
value: stateLoadedFromLocalStorage
}
actions.push({
label: 'Show diff',
callback: async () => {
diffDrawer.openDrawer()
diffDrawer.setDiff({
mode: 'simple',
original: draftOrDeployed,
current: urlScript,
title: `${savedApp?.draft ? 'Latest saved draft' : 'Deployed'} <> Autosave`,
button: { text: 'Discard autosave', onClick: reloadAction }
})
}
})
}
sendUserToast('App restored from browser storage', false, actions)
app_w_draft.value = stateLoadedFromLocalStorage
files = app_w_draft.value.files as any
runnables = app_w_draft.value.runnables as any
redraw += 1
} else if (app_w_draft.draft) {
extractRawApp(app_w_draft.draft)
if (!app_w_draft.draft_only) {
const reloadAction = () => {
stateLoadedFromLocalStorage = undefined
extractRawApp(app_w_draft)
redraw++
}
const deployed = cleanValueProperties(app_w_draft as Value)
const draft = cleanValueProperties({ files, runnables })
sendUserToast('app loaded from latest saved draft', false, [
{
label: 'Discard draft and load from latest deployed version',
callback: reloadAction
},
{
label: 'Show diff',
callback: async () => {
diffDrawer.openDrawer()
diffDrawer.setDiff({
mode: 'simple',
original: deployed,
current: draft,
title: 'Deployed <> Draft',
button: { text: 'Discard draft', onClick: reloadAction }
})
}
}
])
}
} else {
extractRawApp(app_w_draft)
}
}
$: {
if ($workspaceStore) {
loadApp()
}
}
async function restoreDraft() {
if (!savedApp || !savedApp.draft) {
sendUserToast('Could not restore to draft', true)
return
}
diffDrawer.closeDrawer()
goto(`/apps/edit/${savedApp.draft.path}`)
await loadApp()
redraw++
}
async function restoreDeployed() {
if (!savedApp) {
sendUserToast('Could not restore to deployed', true)
return
}
diffDrawer.closeDrawer()
if (savedApp.draft) {
await DraftService.deleteDraft({
workspace: $workspaceStore!,
kind: 'app',
path: savedApp.path
})
}
goto(`/apps/edit/${savedApp.path}`)
await loadApp()
redraw++
}
let diffDrawer: DiffDrawer
function onRestore(ev: any) {
sendUserToast('App restored from previous deployment')
let prev = ev.detail
extractRawApp(prev)
savedApp = {
summary: prev.summary,
value: structuredClone(prev.value),
path: prev.path,
policy: structuredClone(policy),
custom_path: prev.custom_path
}
redraw++
}
</script>
<DiffDrawer bind:this={diffDrawer} {restoreDeployed} {restoreDraft} />
{#if files}
{#key redraw}
<div class="h-screen">
<RawAppEditor
on:savedNewAppPath={(event) => {
goto(`/apps_raw/edit/${event.detail}`)
newPath = event.detail
}}
on:restore={onRestore}
initFiles={files}
initRunnables={runnables}
{summary}
{newPath}
path={$page.params.path}
{policy}
bind:savedApp
{diffDrawer}
version={lastVersion}
newApp={false}
/>
</div>
{/key}
{/if}
@@ -0,0 +1,5 @@
export function load({ params }) {
return {
stuff: { title: `App ${params.path}` }
}
}
@@ -0,0 +1,58 @@
<script lang="ts">
import { base } from '$app/paths'
import { page } from '$app/stores'
import { Button, Skeleton } from '$lib/components/common'
import { AppService, type AppWithLastVersion } from '$lib/gen'
import { userStore, workspaceStore } from '$lib/stores'
import { canWrite } from '$lib/utils'
import { Pen } from 'lucide-svelte'
import RawAppPreview from '$lib/components/raw_apps/RawAppPreview.svelte'
import type { HiddenRunnable } from '$lib/components/apps/types'
const hideEditBtn = $page.url.searchParams.get('hideEditBtn') === 'true'
let app: AppWithLastVersion | undefined = undefined
async function loadApp() {
console.log('Loading app')
app = await AppService.getAppLiteByPath({
workspace: $workspaceStore!,
path: $page.params.path
})
}
$: $workspaceStore && loadApp()
$: can_write = canWrite($page.params.path, app?.extra_perms ?? {}, $userStore)
function getRunnables(app: AppWithLastVersion) {
return (app?.value?.runnables ?? {}) as Record<string, HiddenRunnable>
}
function getVersion(app: AppWithLastVersion) {
return app?.value?.version as number
}
</script>
<div class="h-full min-h-[600px] w-full relative p-2bg-white">
{#if !$workspaceStore || !$userStore || !app}
<Skeleton layout={[10]} />
{:else}
<RawAppPreview
path={$page.params.path}
workspace={$workspaceStore}
user={$userStore}
runnables={getRunnables(app)}
version={getVersion(app)}
/>
{/if}
{#if can_write && !hideEditBtn}
<div id="app-edit-btn" class="absolute bottom-4 z-50 right-4">
<Button
size="sm"
startIcon={{ icon: Pen }}
variant="border"
btnClasses="bg-white"
href="{base}/apps_raw/edit/{$page.params.path}?nodraft=true">Edit</Button
>
</div>
{/if}
</div>
@@ -955,18 +955,22 @@
class="py-4 max-w-7xl mx-auto px-4"
/>
<div class="w-full mt-10">
<FlowStatusViewer
jobId={job?.id ?? ''}
on:jobsLoaded={({ detail }) => {
job = detail
}}
on:done={(e) => {
job = e.detail
}}
initialJob={job}
workspaceId={$workspaceStore}
bind:selectedJobStep
/>
{#if job?.id}
<FlowStatusViewer
jobId={job?.id ?? ''}
on:jobsLoaded={({ detail }) => {
job = detail
}}
on:done={(e) => {
job = e.detail
}}
initialJob={job}
workspaceId={$workspaceStore}
bind:selectedJobStep
/>
{:else}
<Skeleton layout={[[5]]} />
{/if}
</div>
{/if}
</div>
@@ -0,0 +1,5 @@
export function load({ params }) {
return {
stuff: { title: `Public App` }
}
}
@@ -0,0 +1,11 @@
<script lang="ts">
// import { page } from '$app/stores'
// import RawAppPreview from '$lib/components/raw_apps/RawAppPreview.svelte'
// import { userStore } from '$lib/stores'
</script>
<!-- <RawAppPreview
workspace={$page.params.workspace}
user={$userStore}
version={Number($page.params.version)}
/> -->

Some files were not shown because too many files have changed in this diff Show More