mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
ui code builder v0 (at secret path) (#4964)
This commit is contained in:
+4
-3
@@ -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"
|
||||
}
|
||||
+4
-3
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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
@@ -9,4 +9,6 @@ tests-out/
|
||||
storageState.json
|
||||
.env.production
|
||||
dist/
|
||||
static/tsdocs/
|
||||
static/tsdocs/
|
||||
static/ui_builder/
|
||||
ui_builder.tar.gz
|
||||
Generated
+139
-267
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 <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 <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 <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 <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}
|
||||
+39
-65
@@ -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
-7
@@ -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}
|
||||
|
||||
+3
-1
@@ -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}
|
||||
|
||||
+1
-1
@@ -76,8 +76,8 @@
|
||||
/>
|
||||
{:else}
|
||||
<EmptyInlineScript
|
||||
unusedInlineScripts={$app?.unusedInlineScripts}
|
||||
{componentType}
|
||||
name={componentInput.runnable.name}
|
||||
on:delete={clear}
|
||||
on:new={(e) => {
|
||||
if (
|
||||
|
||||
+7
-5
@@ -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) => {
|
||||
|
||||
+37
-20
@@ -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
|
||||
|
||||
+22
-55
@@ -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>
|
||||
|
||||
+3
-3
@@ -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)
|
||||
|
||||
+3
-1
@@ -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}
|
||||
|
||||
+2
-3
@@ -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}
|
||||
|
||||
+20
-28
@@ -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,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
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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',
|
||||
|
||||
@@ -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
@@ -32,7 +32,7 @@ export function schemaToTsType(schema: Schema | SchemaProperty): string {
|
||||
|
||||
return `${prefix}: ${type}`
|
||||
})
|
||||
.join(';')
|
||||
.join('; ')
|
||||
|
||||
return `{ ${types} }`
|
||||
}
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user