diff --git a/backend/.sqlx/query-01050e7057f3d1971ad9e47ac83bf6a3c3c9f41689c3607f0b264437ae6b3324.json b/backend/.sqlx/query-01050e7057f3d1971ad9e47ac83bf6a3c3c9f41689c3607f0b264437ae6b3324.json new file mode 100644 index 0000000000..1af0e078d6 --- /dev/null +++ b/backend/.sqlx/query-01050e7057f3d1971ad9e47ac83bf6a3c3c9f41689c3607f0b264437ae6b3324.json @@ -0,0 +1,24 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT data FROM app_bundles WHERE app_version_id = $1 AND file_type = $2 AND w_id = $3", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "data", + "type_info": "Bytea" + } + ], + "parameters": { + "Left": [ + "Int8", + "Text", + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "01050e7057f3d1971ad9e47ac83bf6a3c3c9f41689c3607f0b264437ae6b3324" +} diff --git a/backend/.sqlx/query-abaae3dde751a41b2dbb7856ece1c840d0ea8d59346ed9e88f6f609edb543d7e.json b/backend/.sqlx/query-abaae3dde751a41b2dbb7856ece1c840d0ea8d59346ed9e88f6f609edb543d7e.json new file mode 100644 index 0000000000..c68cdc0977 --- /dev/null +++ b/backend/.sqlx/query-abaae3dde751a41b2dbb7856ece1c840d0ea8d59346ed9e88f6f609edb543d7e.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO app_bundles (app_version_id, w_id, file_type, data) VALUES ($1, $2, $3, $4)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Varchar", + "Varchar", + "Bytea" + ] + }, + "nullable": [] + }, + "hash": "abaae3dde751a41b2dbb7856ece1c840d0ea8d59346ed9e88f6f609edb543d7e" +} diff --git a/backend/.sqlx/query-fad966db585b91c9ce143c9aa26a826aec1ddb193a7f4988c5f12b1a2d8ce071.json b/backend/.sqlx/query-fad966db585b91c9ce143c9aa26a826aec1ddb193a7f4988c5f12b1a2d8ce071.json new file mode 100644 index 0000000000..673a98eade --- /dev/null +++ b/backend/.sqlx/query-fad966db585b91c9ce143c9aa26a826aec1ddb193a7f4988c5f12b1a2d8ce071.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT app.versions[array_upper(app.versions, 1)] FROM app\n WHERE app.path = $1 AND app.workspace_id = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "versions", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "fad966db585b91c9ce143c9aa26a826aec1ddb193a7f4988c5f12b1a2d8ce071" +} diff --git a/backend/migrations/20251001140645_raw_app_bundles.down.sql b/backend/migrations/20251001140645_raw_app_bundles.down.sql new file mode 100644 index 0000000000..a107fc5c11 --- /dev/null +++ b/backend/migrations/20251001140645_raw_app_bundles.down.sql @@ -0,0 +1,2 @@ +-- Add down migration script here +DROP TABLE app_bundles; \ No newline at end of file diff --git a/backend/migrations/20251001140645_raw_app_bundles.up.sql b/backend/migrations/20251001140645_raw_app_bundles.up.sql new file mode 100644 index 0000000000..073faa27ee --- /dev/null +++ b/backend/migrations/20251001140645_raw_app_bundles.up.sql @@ -0,0 +1,8 @@ +-- Add up migration script here +CREATE TABLE app_bundles ( + app_version_id BIGINT NOT NULL, + w_id VARCHAR(255) NOT NULL, + file_type VARCHAR(10) NOT NULL, + data BYTEA NOT NULL, + PRIMARY KEY (app_version_id, file_type) +); diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index e804b651e7..3ab924fe19 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -7003,6 +7003,23 @@ paths: schema: type: string + /w/{workspace}/apps/secret_of_latest_version/{path}: + get: + summary: get public secret of latest version of an app bundle + operationId: getPublicSecretOfLatestVersionOfApp + tags: + - app + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/Path" + responses: + "200": + description: app secret + content: + text/plain: + schema: + type: string + /w/{workspace}/apps/get/v/{id}: get: summary: get app by version diff --git a/backend/windmill-api/src/apps.rs b/backend/windmill-api/src/apps.rs index ff14dc1ab8..21acc76b88 100644 --- a/backend/windmill-api/src/apps.rs +++ b/backend/windmill-api/src/apps.rs @@ -88,6 +88,10 @@ pub fn workspaced_service() -> Router { .route("/get/lite/*path", get(get_app_lite)) .route("/get/draft/*path", get(get_app_w_draft)) .route("/secret_of/*path", get(get_secret_id)) + .route( + "/secret_of_latest_version/*path", + get(get_latest_version_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)) @@ -389,19 +393,85 @@ async fn list_apps( Ok(Json(rows)) } -async fn get_raw_app_data(Path((w_id, version_id)): Path<(String, String)>) -> Result { - let file_path = format!("/tmp/wmill/{}/{}", w_id, version_id); - let file = tokio::fs::File::open(file_path).await?; - let stream = tokio_util::io::ReaderStream::new(file); - let res = Response::builder().header( - http::header::CONTENT_TYPE, - if version_id.ends_with(".css") { - "text/css" - } else { - "text/javascript" - }, - ); - Ok(res.body(Body::from_stream(stream)).unwrap()) +async fn get_raw_app_data( + Path((w_id, secret_with_ext)): Path<(String, String)>, + Extension(db): Extension, +) -> Result { + #[cfg(all(feature = "enterprise", feature = "parquet"))] + let object_store = windmill_common::s3_helpers::get_object_store().await; + #[cfg(not(all(feature = "enterprise", feature = "parquet")))] + let object_store: Option<()> = None; + + // tracing::info!("secret_with_ext: {}", secret_with_ext); + let mut splitted = secret_with_ext.split('.'); + let secret_id = splitted.next().unwrap_or(""); + + if secret_id.is_empty() { + return Err(Error::BadRequest("Invalid secret".to_string())); + } + + let id = get_id_from_secret( + &db, + &w_id, + secret_id.to_string(), + Some(BUNDLE_SECRET_PREFIX), + ) + .await?; + + let file_type = splitted.next().unwrap_or(""); + let file_type = if file_type == "css" { + "css" + } else if file_type == "js" { + "js" + } else { + return Err(Error::BadRequest( + "Invalid file type, only .css and .js are supported".to_string(), + )); + }; + // tracing::info!("file_type: {}", file_type); + let path = format!("/app_bundles/{}/{}.{}", w_id, id, file_type); + + #[allow(unused_assignments)] + let mut body: Option = None; + #[cfg(all(feature = "enterprise", feature = "parquet"))] + if let Some(os) = object_store { + let stream = os + .get(&object_store::path::Path::from(path)) + .await? + .bytes() + .await?; + tracing::info!("stream: {}", stream.len()); + body = Some(Body::from(stream)); + } + + if body.is_none() { + let get_raw_app_file = sqlx::query_scalar!( + "SELECT data FROM app_bundles WHERE app_version_id = $1 AND file_type = $2 AND w_id = $3", + id, + file_type, + &w_id, + ) + .fetch_optional(&db) + .await?; + if let Some(file) = get_raw_app_file { + body = Some(Body::from(file)); + } + } + + if let Some(body) = body { + // let stream = tokio_util::io::ReaderStream::new(file); + let res = Response::builder().header( + http::header::CONTENT_TYPE, + if file_type == "css" { + "text/css" + } else { + "text/javascript" + }, + ); + Ok(res.body(body).unwrap()) + } else { + return Err(Error::NotFound("File not found".to_string())); + } } // async fn get_app_version( @@ -692,14 +762,7 @@ async fn get_public_app_by_secret( Extension(db): Extension, Path((w_id, secret)): Path<(String, String)>, ) -> JsonResult { - let mc = build_crypt(&db, &w_id).await?; - - let decrypted = mc - .decrypt_bytes_to_bytes(&(hex::decode(secret)?)) - .map_err(|e| Error::internal_err(e.to_string()))?; - let bytes = str::from_utf8(&decrypted).map_err(to_anyhow)?; - - let id: i64 = bytes.parse().map_err(to_anyhow)?; + let id = get_id_from_secret(&db, &w_id, secret, None).await?; let app_o = sqlx::query_as::<_, AppWithLastVersion>( "SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path, @@ -747,6 +810,27 @@ async fn get_public_app_by_secret( Ok(Json(app)) } +async fn get_id_from_secret( + db: &DB, + w_id: &str, + secret: String, + prefix: Option<&str>, +) -> Result { + let mc = build_crypt(db, w_id).await?; + let decrypted = mc + .decrypt_bytes_to_bytes(&(hex::decode(secret)?)) + .map_err(|e| Error::internal_err(e.to_string()))?; + let mut bytes = str::from_utf8(&decrypted).map_err(to_anyhow)?; + if let Some(prefix) = prefix { + if !bytes.starts_with(prefix) { + return Err(Error::BadRequest("Invalid secret".to_string())); + } + bytes = bytes.strip_prefix(prefix).unwrap_or(""); + } + let id: i64 = bytes.parse().map_err(to_anyhow)?; + Ok(id) +} + async fn get_public_resource( Extension(db): Extension, Path((w_id, path)): Path<(String, StripPath)>, @@ -803,16 +887,87 @@ async fn get_secret_id( Ok(hx) } +const BUNDLE_SECRET_PREFIX: &str = "bundle_"; + +async fn get_latest_version_secret_id( + authed: ApiAuthed, + Extension(user_db): Extension, + Extension(db): Extension, + Path((w_id, path)): Path<(String, StripPath)>, +) -> Result { + let path = path.to_path(); + check_scopes(&authed, || format!("apps:read:{}", path))?; + let mut tx = user_db.begin(&authed).await?; + + let id_o = sqlx::query_scalar!( + "SELECT app.versions[array_upper(app.versions, 1)] FROM app + WHERE app.path = $1 AND app.workspace_id = $2", + path, + &w_id + ) + .fetch_optional(&mut *tx) + .await? + .flatten(); + + tx.commit().await?; + + let id = not_found_if_none(id_o, "App", path.to_string())?; + + let mc = build_crypt(&db, &w_id).await?; + + let hx = hex::encode(mc.encrypt_str_to_bytes(format!("{}{}", BUNDLE_SECRET_PREFIX, id))); + + Ok(hx) +} + +use windmill_common::error; + +async fn store_raw_app_file<'a>( + w_id: &str, + id: &i64, + file_type: &str, + data: bytes::Bytes, + tx: &mut sqlx::Transaction<'a, sqlx::Postgres>, +) -> Result<()> { + #[cfg(all(feature = "enterprise", feature = "parquet"))] + let object_store = windmill_common::s3_helpers::get_object_store().await; + #[cfg(not(all(feature = "enterprise", feature = "parquet")))] + let object_store: Option<()> = None; + + let path = format!("/app_bundles/{}/{}.{}", w_id, id, file_type); + #[cfg(all(feature = "enterprise", feature = "parquet"))] + if let Some(os) = object_store { + if let Err(e) = os + .put(&object_store::path::Path::from(path.clone()), data.into()) + .await + { + tracing::error!("Failed to put snapshot to s3 at {path}: {:?}", e); + return Err(error::Error::ExecutionErr(format!( + "Failed to put {path} to s3" + ))); + } + tracing::info!("Successfully put snapshot to s3 at {path}"); + return Ok(()); + } + + sqlx::query!( + "INSERT INTO app_bundles (app_version_id, w_id, file_type, data) VALUES ($1, $2, $3, $4)", + id, + w_id, + file_type, + data.to_vec() + ) + .execute(&mut **tx) + .await?; + + Ok(()) +} 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(); @@ -831,9 +986,8 @@ macro_rules! process_app_multipart { .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(); + if let Some((_npath, id, tx)) = saved_app.as_mut() { + store_raw_app_file($w_id, &id, "js", data, tx).await?; uploaded_js = true; } else { return Err(Error::BadRequest( @@ -841,9 +995,8 @@ macro_rules! process_app_multipart { )); } } 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(); + if let Some((_npath, id, tx)) = saved_app.as_mut() { + store_raw_app_file($w_id, &id, "css", data, tx).await?; } else { return Err(Error::BadRequest( "App payload need to be created first".to_string(), diff --git a/frontend/scripts/untar_ui_builder.js b/frontend/scripts/untar_ui_builder.js index f0ebbd8c87..632c7b9cdc 100644 --- a/frontend/scripts/untar_ui_builder.js +++ b/frontend/scripts/untar_ui_builder.js @@ -20,7 +20,7 @@ console.log('Running postinstall for root project'); import { x } from 'tar' -const tarUrl = 'https://pub-06154ed168a24e73a86ab84db6bf15d8.r2.dev/ui_builder-d44b577.tar.gz' +const tarUrl = 'https://pub-06154ed168a24e73a86ab84db6bf15d8.r2.dev/ui_builder-8957900.tar.gz' const outputTarPath = path.join(process.cwd(), 'ui_builder.tar.gz') const extractTo = path.join(process.cwd(), 'static/ui_builder/') diff --git a/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte b/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte index a4032730b6..864a5c78ef 100644 --- a/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte +++ b/frontend/src/lib/components/apps/editor/AppEditorHeader.svelte @@ -1,13 +1,9 @@ @@ -1028,45 +961,6 @@ {#if $appPath == ''} closeDraftDrawer()}> - - Choose a path to save the initial draft of the app. - -

Summary

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

Public URL

- -
-
- { - policy.execution_mode = e.detail ? 'anonymous' : 'publisher' - setPublishState() - }} - disabled={$appPath == ''} - /> -
- {#if $appPath == ''} - - {:else if secretUrlHref} - - {:else} - {/if} -
- Share this url directly or embed it using an iframe (if requiring login, top-level domain of - embedding app must be the same as the one of Windmill) -
- -
- {#if !($userStore?.is_admin || $userStore?.is_super_admin)} - - Custom path can only be set by workspace admins - -
- {/if} - {#if !$enterpriseLicense} -
- - EE only Enterprise Edition only feature -
- {/if} - { - customPath = detail ? '' : undefined - if (customPath === undefined) { - customPathError = '' - } - }} - checked={customPath !== undefined} - options={{ - right: 'Use a custom URL' - }} - disabled={!$enterpriseLicense || !($userStore?.is_admin || $userStore?.is_super_admin)} - /> - - {#if customPath !== undefined} -
-
Custom path
-
- { - dirtyCustomPath = true - }} - /> -
-
Custom public URL
-
- - -
{dirtyCustomPath ? customPathError : ''} -
- {/if} -
-
- - You will still need to deploy the app to make visible the latest changes - - - Embed this app in your own product to be used by your own users +
diff --git a/frontend/src/lib/components/apps/editor/AppEditorHeaderDeploy.svelte b/frontend/src/lib/components/apps/editor/AppEditorHeaderDeploy.svelte new file mode 100644 index 0000000000..9b6f5a9b2f --- /dev/null +++ b/frontend/src/lib/components/apps/editor/AppEditorHeaderDeploy.svelte @@ -0,0 +1,274 @@ + + +{#if !onLatest} + + By deploying, you may overwrite changes made by other users. Press 'Deploy' to see diff. + +
+{/if} +Summary +
+ + { + e.stopPropagation() + }} + onkeyup={() => { + if (appPath == '' && summary?.length > 0 && !dirtyPath) { + path?.setName( + summary + .toLowerCase() + .replace(/[^a-z0-9_]/g, '_') + .replace(/-+/g, '_') + .replace(/^-|-$/g, '') + ) + } + }} + /> +
+
+Deployment message +
+ + +
+
+Path + + +
+ + A viewer of the app will execute the runnables of the app on behalf of the publisher (you) + + It ensures that all required resources/runnable visible for publisher but not for viewer at time + of creating the app would prevent the execution of the app. To guarantee tight security, a + policy is computed at time of deployment of the app which only allow the scripts/flows referred + to in the app to be called on behalf of. Furthermore, static parameters are not overridable. + Hence, users will only be able to use the app as intended by the publisher without risk for + leaking resources not used in the app. + + + +
+ +{#if !hideSecretUrl} +

Public URL

+ +
+
+ { + policy.execution_mode = e.detail ? 'anonymous' : 'publisher' + setPublishState() + }} + disabled={appPath == ''} + /> +
+ {#if appPath == ''} + + {:else if secretUrlHref} + + {:else} + {/if} +
+ Share this url directly or embed it using an iframe (if requiring login, top-level domain of + embedding app must be the same as the one of Windmill) +
+ +
+ {#if !($userStore?.is_admin || $userStore?.is_super_admin)} + + Custom path can only be set by workspace admins + +
+ {/if} + {#if !$enterpriseLicense} +
+ + EE only Enterprise Edition only feature +
+ {/if} + { + customPath = detail ? '' : undefined + if (customPath === undefined) { + customPathError = '' + } + }} + checked={customPath !== undefined} + options={{ + right: 'Use a custom URL' + }} + disabled={!$enterpriseLicense || !($userStore?.is_admin || $userStore?.is_super_admin)} + /> + + {#if customPath !== undefined} +
+
Custom path
+
+ { + dirtyCustomPath = true + }} + /> +
+
Custom public URL
+
+ + +
{dirtyCustomPath ? customPathError : ''} +
+ {/if} +
+
+ + You will still need to deploy the app to make visible the latest changes + + + Embed this app in your own product to be used by your own users +{/if} diff --git a/frontend/src/lib/components/apps/editor/AppEditorHeaderDeployInitialDraft.svelte b/frontend/src/lib/components/apps/editor/AppEditorHeaderDeployInitialDraft.svelte new file mode 100644 index 0000000000..9552436e84 --- /dev/null +++ b/frontend/src/lib/components/apps/editor/AppEditorHeaderDeployInitialDraft.svelte @@ -0,0 +1,50 @@ + + + + Choose a path to save the initial draft of the app. + +

Summary

+
+ + { + e.stopPropagation() + }} + bind:value={$summary} + onkeyup={() => { + if ($appPath == '' && $summary?.length > 0 && !dirtyPath) { + path?.setName( + $summary + .toLowerCase() + .replace(/[^a-z0-9_]/g, '_') + .replace(/-+/g, '_') + .replace(/^-|-$/g, '') + ) + } + }} + /> +
+
+ +
diff --git a/frontend/src/lib/components/apps/editor/appDeploy.svelte.ts b/frontend/src/lib/components/apps/editor/appDeploy.svelte.ts new file mode 100644 index 0000000000..122c42bcf9 --- /dev/null +++ b/frontend/src/lib/components/apps/editor/appDeploy.svelte.ts @@ -0,0 +1,7 @@ +import { base } from "$lib/base" +import { workspaceStore } from "$lib/stores" +import { get } from "svelte/store" + +export function computeSecretUrl(secretUrl: string) { + return `${window.location.origin}${base}/public/${get(workspaceStore)}/${secretUrl}` +} diff --git a/frontend/src/lib/components/raw_apps/FileEditorIcon.svelte b/frontend/src/lib/components/raw_apps/FileEditorIcon.svelte index f1419b32c7..1c06463bf1 100644 --- a/frontend/src/lib/components/raw_apps/FileEditorIcon.svelte +++ b/frontend/src/lib/components/raw_apps/FileEditorIcon.svelte @@ -6,7 +6,11 @@ import SvelteIcon from '../icons/SvelteIcon.svelte' import VueIcon from '../icons/VueIcon.svelte' - export let file: string + interface Props { + file: string + } + + let { file }: Props = $props() {#if file.endsWith('.tsx')} diff --git a/frontend/src/lib/components/raw_apps/RawAppBackgroundRunner.svelte b/frontend/src/lib/components/raw_apps/RawAppBackgroundRunner.svelte index c80bd2c41b..1650eb225a 100644 --- a/frontend/src/lib/components/raw_apps/RawAppBackgroundRunner.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppBackgroundRunner.svelte @@ -5,13 +5,25 @@ 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 - export let jobs: string[] = [] - export let jobsById: Record = {} - export let editor: boolean - export let workspace: string + interface Props { + iframe: HTMLIFrameElement | undefined + path: string + runnables: Record + jobs?: string[] + jobsById?: Record + editor: boolean + workspace: string + } + + let { + iframe, + path, + runnables, + jobs = $bindable([]), + jobsById = $bindable({}), + editor, + workspace + }: Props = $props() let listener = async (event) => { const data = event.data @@ -87,4 +99,4 @@ } - + diff --git a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte index 1a24c438a6..dd2ec62cd1 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte @@ -2,7 +2,6 @@ import { run } from 'svelte/legacy' 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' @@ -52,7 +51,7 @@ }: Props = $props() export const version: number | undefined = undefined - let runnables = writable(initRunnables) + let runnables = $state(initRunnables) let files: Record | undefined = $state(initFiles) @@ -65,7 +64,7 @@ path != '' ? `rawapp-${path}` : 'rawapp', encodeState({ files, - runnables: $runnables + runnables: runnables }) ) } catch (err) { @@ -97,7 +96,7 @@ iframe?.contentWindow?.postMessage( { type: 'setRunnables', - dts: genWmillTs($runnables) + dts: genWmillTs(runnables) }, '*' ) @@ -129,7 +128,7 @@ let darkMode: boolean = $state(false) run(() => { - $runnables && files && saveFrontendDraft() + runnables && files && saveFrontendDraft() }) run(() => { iframe?.addEventListener('load', () => { @@ -140,7 +139,7 @@ iframe && iframeLoaded && initFiles && populateFiles() }) run(() => { - iframe && iframeLoaded && $runnables && populateRunnables() + iframe && iframeLoaded && runnables && populateRunnables() }) @@ -153,7 +152,7 @@ {iframe} bind:jobs bind:jobsById - runnables={$runnables} + {runnables} {path} />
diff --git a/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte b/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte index 2954af40c8..60027a2a12 100644 --- a/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppEditorHeader.svelte @@ -1,22 +1,10 @@ @@ -676,45 +638,6 @@ {#if appPath == ''} closeDraftDrawer()}> - - Choose a path to save the initial draft of the app. - -

Summary

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

Public URL

-
- -
- { - policy.execution_mode = e.detail ? 'anonymous' : 'publisher' - setPublishState() - }} - /> -
- -
-
-
Public URL
-
- {#if secretUrl} - {@const href = `${window.location.origin}${base}/public/${$workspaceStore}/${secretUrl}`} - - {:else} - {/if} -
- Share this url directly or embed it using an iframe (if requiring login, top-level domain - of embedding app must be the same as the one of Windmill) -
- -
- {#if !$enterpriseLicense} - - Custom path is an enterprise only feature. - -
- {:else if !($userStore?.is_admin || $userStore?.is_super_admin)} - - Custom path can only be set by workspace admins - -
- {/if} - { - customPath = detail ? '' : undefined - }} - checked={customPath !== undefined} - options={{ - right: 'Use a custom URL' - }} - disabled={!$enterpriseLicense || !($userStore?.is_admin || $userStore?.is_super_admin)} - /> - - {#if customPath !== undefined} -
-
Custom path
-
- { - dirtyCustomPath = true - }} - /> -
-
Custom public URL
-
- - -
{dirtyCustomPath ? customPathError : ''} -
- {/if} -
-
- - You will still need to deploy the app to make visible the latest changes - - - Embed this app in your own product to be used by your own users - {/if} +
@@ -988,7 +765,7 @@
- + {/snippet}