diff --git a/backend/migrations/20230410122342_saved_inputs.down.sql b/backend/migrations/20230410122342_saved_inputs.down.sql new file mode 100644 index 0000000000..265e7852f7 --- /dev/null +++ b/backend/migrations/20230410122342_saved_inputs.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS input; +DROP TYPE RUNNABLE_TYPE; \ No newline at end of file diff --git a/backend/migrations/20230410122342_saved_inputs.up.sql b/backend/migrations/20230410122342_saved_inputs.up.sql new file mode 100644 index 0000000000..9294e821a0 --- /dev/null +++ b/backend/migrations/20230410122342_saved_inputs.up.sql @@ -0,0 +1,13 @@ +CREATE TYPE RUNNABLE_TYPE AS ENUM ('ScriptHash', 'ScriptPath', 'FlowPath'); + +CREATE TABLE IF NOT EXISTS input ( + id UUID PRIMARY KEY, + workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id), + runnable_id VARCHAR(255) NOT NULL, + runnable_type RUNNABLE_TYPE NOT NULL, + name TEXT NOT NULL, + args JSONB NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + created_by VARCHAR(50) NOT NULL, + is_public BOOLEAN NOT NULL DEFAULT FALSE +); \ No newline at end of file diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index aeffbf913a..d02afba444 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -2782,6 +2782,27 @@ paths: schema: type: string + /w/{workspace}/flows/input_history/p/{path}: + get: + summary: list inputs for previous completed flow jobs + operationId: getFlowInputHistoryByPath + tags: + - flow + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/ScriptPath" + - $ref: "#/components/parameters/Page" + - $ref: "#/components/parameters/PerPage" + responses: + "200": + description: input history for completed jobs with this flow path + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Input" + /w/{workspace}/apps/list: get: summary: list all available apps @@ -3322,23 +3343,6 @@ paths: schema: $ref: "#/components/schemas/Job" - # /w/{workspace}/jobs/flow/current_state/{id}: - # get: - # summary: get flow current step state - # operationId: getJob - # tags: - # - job - # parameters: - # - $ref: "#/components/parameters/WorkspaceId" - # - $ref: "#/components/parameters/JobId" - # responses: - # "200": - # description: state details - # content: - # application/json: - # schema: - # type: string - /w/{workspace}/jobs_u/getupdate/{id}: get: summary: get job updates @@ -4558,6 +4562,118 @@ paths: "200": description: unstar item + /w/{workspace}/inputs/history: + get: + summary: List Inputs used in previously completed jobs + operationId: getInputHistory + tags: + - input + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/RunnableId" + - $ref: "#/components/parameters/RunnableType" + - $ref: "#/components/parameters/Page" + - $ref: "#/components/parameters/PerPage" + responses: + "200": + description: Input history for completed jobs + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Input" + + /w/{workspace}/inputs/list: + get: + summary: List saved Inputs for a Runnable + operationId: listInputs + tags: + - input + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/RunnableId" + - $ref: "#/components/parameters/RunnableType" + - $ref: "#/components/parameters/Page" + - $ref: "#/components/parameters/PerPage" + responses: + "200": + description: Saved Inputs for a Runnable + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Input" + + /w/{workspace}/inputs/create: + post: + summary: Create an Input for future use in a script or flow + operationId: createInput + tags: + - input + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/RunnableId" + - $ref: "#/components/parameters/RunnableType" + requestBody: + description: Input + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateInput" + responses: + "201": + description: Input created + content: + text/plain: + schema: + type: string + format: uuid + + /w/{workspace}/inputs/update: + post: + summary: Update an Input + operationId: updateInput + tags: + - input + parameters: + - $ref: "#/components/parameters/WorkspaceId" + requestBody: + description: UpdateInput + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/UpdateInput" + responses: + "201": + description: Input updated + content: + text/plain: + schema: + type: string + format: uuid + + /w/{workspace}/inputs/delete/{input}: + post: + summary: Delete a Saved Input + operationId: deleteInput + tags: + - input + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/InputId" + responses: + "200": + description: Input deleted + content: + text/plain: + schema: + type: string + format: uuid + components: securitySchemes: bearerAuth: @@ -4567,6 +4683,7 @@ components: type: apiKey in: cookie name: token + parameters: WorkspaceId: name: workspace @@ -4685,7 +4802,6 @@ components: in: query schema: type: string - ScriptStartPath: name: script_path_start description: mask to filter matching starting path @@ -4742,7 +4858,6 @@ components: in: query schema: type: string - ResultFilter: name: result description: filter on jobs containing those result as a json subset (@> in postgres) @@ -4802,9 +4917,26 @@ components: # type: string # enum: ["preview", "script", "dependencies"] # explode: false + RunnableId: + name: runnable_id + in: query + schema: + type: string + RunnableType: + name: runnable_type + in: query + schema: + $ref: "#/components/schemas/RunnableType" + InputId: + name: input + in: path + required: true + schema: + type: string schemas: $ref: "../../openflow.openapi.yaml#/components/schemas" + Script: type: object properties: @@ -4875,6 +5007,60 @@ components: type: object additionalProperties: {} + Input: + type: object + properties: + id: + type: string + name: + type: string + args: + type: object + created_by: + type: string + created_at: + type: string + format: date-time + is_public: + type: boolean + required: + - id + - name + - args + - created_by + - created_at + - is_public + + CreateInput: + type: object + properties: + name: + type: string + args: + type: object + required: + - name + - args + - created_by + + UpdateInput: + type: object + properties: + id: + type: string + name: + type: string + is_public: + type: boolean + required: + - id + - name + - is_public + + RunnableType: + type: string + enum: ["ScriptHash", "ScriptPath", "FlowPath"] + QueuedJob: type: object properties: @@ -5896,6 +6082,7 @@ components: - extra_perms - edited_at - execution_mode + AppWithLastVersion: type: object properties: diff --git a/backend/windmill-api/src/flows.rs b/backend/windmill-api/src/flows.rs index dff0a32f2e..fc8f20b709 100644 --- a/backend/windmill-api/src/flows.rs +++ b/backend/windmill-api/src/flows.rs @@ -6,14 +6,20 @@ * LICENSE-AGPL for a copy of the license. */ -use hyper::StatusCode; -use sql_builder::prelude::*; - +use crate::{ + db::{UserDB, DB}, + schedule::clear_schedule, + users::{maybe_refresh_folders, require_owner_of_path, Authed}, + webhook_util::{WebhookMessage, WebhookShared}, + HTTP_CLIENT, +}; use axum::{ extract::{Extension, Path, Query}, routing::{delete, get, post}, Json, Router, }; +use hyper::StatusCode; +use sql_builder::prelude::*; use sql_builder::SqlBuilder; use sqlx::{Postgres, Transaction}; use windmill_audit::{audit_log, ActionKind}; @@ -27,14 +33,6 @@ use windmill_common::{ }; use windmill_queue::{push, schedule::push_scheduled_job, JobPayload, QueueTransaction}; -use crate::{ - db::{UserDB, DB}, - schedule::clear_schedule, - users::{maybe_refresh_folders, require_owner_of_path, Authed}, - webhook_util::{WebhookMessage, WebhookShared}, - HTTP_CLIENT, -}; - pub fn workspaced_service() -> Router { Router::new() .route("/list", get(list_flows)) diff --git a/backend/windmill-api/src/inputs.rs b/backend/windmill-api/src/inputs.rs new file mode 100644 index 0000000000..a5be6f70b8 --- /dev/null +++ b/backend/windmill-api/src/inputs.rs @@ -0,0 +1,282 @@ +/* + * Author: Ruben Fiszel + * Copyright: Windmill Labs, Inc 2022 + * This file and its contents are licensed under the AGPLv3 License. + * Please see the included NOTICE for copyright information and + * LICENSE-AGPL for a copy of the license. + */ + +use crate::{db::UserDB, jobs::CompletedJob, users::Authed}; +use axum::{ + extract::{Path, Query}, + routing::{get, post}, + Extension, Json, Router, +}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use sqlx::types::Uuid; +use std::{ + fmt::{Display, Formatter}, + vec, +}; +use windmill_common::{ + error::JsonResult, + scripts::to_i64, + utils::{paginate, Pagination}, +}; +use windmill_queue::JobKind; + +pub fn workspaced_service() -> Router { + Router::new() + .route("/history", get(get_input_history)) + .route("/list", get(list_saved_inputs)) + .route("/create", post(create_input)) + .route("/update", post(update_input)) + .route("/delete/:id", post(delete_input)) +} + +#[derive(Debug, sqlx::FromRow, Serialize, Deserialize)] +pub struct InputRow { + pub id: Uuid, + pub workspace_id: String, + pub runnable_id: String, + pub runnable_type: RunnableType, + pub name: String, + pub args: Value, + pub created_at: DateTime, + pub created_by: String, + pub is_public: bool, +} + +#[derive(Debug, Serialize, Deserialize, sqlx::Type)] +#[sqlx(type_name = "runnable_type")] +pub enum RunnableType { + ScriptHash, + ScriptPath, + FlowPath, +} + +impl Display for RunnableType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + RunnableType::ScriptHash => write!(f, "ScriptHash"), + RunnableType::ScriptPath => write!(f, "ScriptPath"), + RunnableType::FlowPath => write!(f, "FlowPath"), + } + } +} + +impl RunnableType { + fn job_kind(&self) -> JobKind { + match self { + RunnableType::ScriptHash => JobKind::Script, + RunnableType::ScriptPath => JobKind::Script, + RunnableType::FlowPath => JobKind::Flow, + } + } + + fn column_name(&self) -> &'static str { + match self { + RunnableType::ScriptHash => "script_hash", + RunnableType::ScriptPath => "script_path", + RunnableType::FlowPath => "script_path", + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct RunnableParams { + pub runnable_id: String, + pub runnable_type: RunnableType, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Input { + id: Uuid, + name: String, + created_at: chrono::DateTime, + args: serde_json::Value, + created_by: String, + is_public: bool, +} + +async fn get_input_history( + authed: Authed, + Extension(user_db): Extension, + Path(w_id): Path, + Query(pagination): Query, + Query(r): Query, +) -> JsonResult> { + let (per_page, offset) = paginate(pagination); + + let mut tx = user_db.begin(&authed).await?; + + let sql = &format!( + "select distinct on (args) * from completed_job \ + where {} = $1 and job_kind = $2 and workspace_id = $3 \ + order by args, started_at desc limit $4 offset $5", + r.runnable_type.column_name() + ); + + let query = sqlx::query_as::<_, CompletedJob>(sql); + + let query = match r.runnable_type { + RunnableType::ScriptHash => query.bind(to_i64(&r.runnable_id)?), + _ => query.bind(&r.runnable_id), + }; + + let rows = query + .bind(r.runnable_type.job_kind()) + .bind(&w_id) + .bind(per_page as i32) + .bind(offset as i32) + .fetch_all(&mut tx) + .await?; + + tx.commit().await?; + + let mut inputs = vec![]; + + for row in rows { + inputs.push(Input { + id: row.id, + name: format!( + "{} {}", + row.created_at.format("%H:%M %-d/%-m"), + row.created_by + ), + created_at: row.created_at, + args: row.args.unwrap_or(serde_json::json!({})), + created_by: row.created_by, + is_public: true, + }); + } + + Ok(Json(inputs)) +} + +async fn list_saved_inputs( + authed: Authed, + Extension(user_db): Extension, + Path(w_id): Path, + Query(pagination): Query, + Query(r): Query, +) -> JsonResult> { + let (per_page, offset) = paginate(pagination); + + let mut tx = user_db.begin(&authed).await?; + + let rows = sqlx::query_as::<_, InputRow>( + "select * from input \ + where runnable_id = $1 and runnable_type = $2 and workspace_id = $3 \ + and is_public IS true OR created_by = $4 \ + order by created_at desc limit $5 offset $6", + ) + .bind(&r.runnable_id) + .bind(&r.runnable_type) + .bind(&w_id) + .bind(&authed.username) + .bind(per_page as i32) + .bind(offset as i32) + .fetch_all(&mut tx) + .await?; + + tx.commit().await?; + + let mut inputs: Vec = Vec::new(); + + for row in rows { + inputs.push(Input { + id: row.id, + name: row.name, + args: row.args, + created_by: row.created_by, + created_at: row.created_at, + is_public: row.is_public, + }) + } + + Ok(Json(inputs)) +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct CreateInput { + name: String, + args: serde_json::Value, +} + +async fn create_input( + authed: Authed, + Extension(user_db): Extension, + Path(w_id): Path, + Query(r): Query, + Json(input): Json, +) -> JsonResult { + let mut tx = user_db.begin(&authed).await?; + + let id = Uuid::new_v4(); + + sqlx::query( + "INSERT INTO input (id, workspace_id, runnable_id, runnable_type, name, args, created_by) VALUES ($1, $2, $3, $4, $5, $6, $7)", + ) + .bind(&id) + .bind(&w_id) + .bind(&r.runnable_id) + .bind(&r.runnable_type) + .bind(&input.name) + .bind(&input.args) + .bind(&authed.username) + .execute(&mut tx) + .await?; + + tx.commit().await?; + + Ok(Json(id.to_string())) +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct UpdateInput { + id: Uuid, + name: String, + is_public: bool, +} + +async fn update_input( + authed: Authed, + Extension(user_db): Extension, + Path(w_id): Path, + Json(input): Json, +) -> JsonResult { + let mut tx = user_db.begin(&authed).await?; + + sqlx::query("UPDATE input SET name = $1, is_public = $2 WHERE id = $3 and workspace_id = $4") + .bind(&input.name) + .bind(&input.is_public) + .bind(&input.id) + .bind(&w_id) + .execute(&mut tx) + .await?; + + tx.commit().await?; + + Ok(Json(input.id.to_string())) +} + +async fn delete_input( + authed: Authed, + Extension(user_db): Extension, + Path((w_id, i_id)): Path<(String, Uuid)>, +) -> JsonResult { + let mut tx = user_db.begin(&authed).await?; + + sqlx::query("DELETE FROM input WHERE id = $1 and workspace_id = $2") + .bind(&i_id) + .bind(&w_id) + .execute(&mut tx) + .await?; + + tx.commit().await?; + + Ok(Json(i_id.to_string())) +} diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index 91b9c2c8d5..cc7aaf7a9b 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -6,6 +6,12 @@ * LICENSE-AGPL for a copy of the license. */ +use crate::{ + db::{UserDB, DB}, + users::{require_owner_of_path, Authed, OptAuthed}, + variables::get_workspace_key, + BASE_URL, +}; use anyhow::Context; use axum::{ extract::{FromRequest, Json, Path, Query}, @@ -34,13 +40,6 @@ use windmill_queue::{ get_queued_job, push, JobKind, JobPayload, QueueTransaction, QueuedJob, RawCode, }; -use crate::{ - db::{UserDB, DB}, - users::{require_owner_of_path, Authed, OptAuthed}, - variables::get_workspace_key, - BASE_URL, -}; - pub fn workspaced_service() -> Router { Router::new() .route("/run/f/*script_path", post(run_flow_by_path)) diff --git a/backend/windmill-api/src/lib.rs b/backend/windmill-api/src/lib.rs index 46f7ba2864..2759e4f2c5 100644 --- a/backend/windmill-api/src/lib.rs +++ b/backend/windmill-api/src/lib.rs @@ -7,6 +7,13 @@ */ use crate::oauth2::AllClients; +use crate::{ + db::UserDB, + oauth2::{build_oauth_clients, SlackVerifier}, + tracing_init::{MyMakeSpan, MyOnResponse}, + users::{Authed, OptAuthed}, + webhook_util::WebhookShared, +}; use argon2::Argon2; use axum::{middleware::from_extractor, routing::get, Extension, Router}; use db::DB; @@ -22,14 +29,6 @@ use tower_http::{ }; use windmill_common::utils::rd_string; -use crate::{ - db::UserDB, - oauth2::{build_oauth_clients, SlackVerifier}, - tracing_init::{MyMakeSpan, MyOnResponse}, - users::{Authed, OptAuthed}, - webhook_util::WebhookShared, -}; - mod apps; mod audit; mod capture; @@ -39,6 +38,7 @@ mod flows; mod folders; mod granular_acls; mod groups; +mod inputs; pub mod jobs; mod oauth2; mod resources; @@ -119,25 +119,27 @@ pub async fn run_server( .nest( "/w/:workspace_id", Router::new() - .nest("/scripts", scripts::workspaced_service()) + // Reordered alphabetically + .nest("/acls", granular_acls::workspaced_service()) + .nest("/apps", apps::workspaced_service()) + .nest("/audit", audit::workspaced_service()) + .nest("/capture", capture::workspaced_service()) + .nest("/favorites", favorite::workspaced_service()) + .nest("/flows", flows::workspaced_service()) + .nest("/folders", folders::workspaced_service()) + .nest("/groups", groups::workspaced_service()) + .nest("/inputs", inputs::workspaced_service()) .nest("/jobs", jobs::workspaced_service().layer(cors.clone())) + .nest("/oauth", oauth2::workspaced_service()) + .nest("/resources", resources::workspaced_service()) + .nest("/schedules", schedule::workspaced_service()) + .nest("/scripts", scripts::workspaced_service()) .nest( "/users", users::workspaced_service().layer(Extension(argon2.clone())), ) .nest("/variables", variables::workspaced_service()) - .nest("/oauth", oauth2::workspaced_service()) - .nest("/resources", resources::workspaced_service()) - .nest("/schedules", schedule::workspaced_service()) - .nest("/groups", groups::workspaced_service()) - .nest("/audit", audit::workspaced_service()) - .nest("/acls", granular_acls::workspaced_service()) - .nest("/workspaces", workspaces::workspaced_service()) - .nest("/apps", apps::workspaced_service()) - .nest("/flows", flows::workspaced_service()) - .nest("/capture", capture::workspaced_service()) - .nest("/favorites", favorite::workspaced_service()) - .nest("/folders", folders::workspaced_service()), + .nest("/workspaces", workspaces::workspaced_service()), ) .nest("/workspaces", workspaces::global_service()) .nest( diff --git a/backend/windmill-api/src/scripts.rs b/backend/windmill-api/src/scripts.rs index f2cb1733e2..3830a7578c 100644 --- a/backend/windmill-api/src/scripts.rs +++ b/backend/windmill-api/src/scripts.rs @@ -6,10 +6,6 @@ * LICENSE-AGPL for a copy of the license. */ -use sql_builder::prelude::*; -use windmill_audit::{audit_log, ActionKind}; -use windmill_parser::MainArgSignature; - use crate::{ db::{UserDB, DB}, schedule::clear_schedule, @@ -25,6 +21,7 @@ use axum::{ use hyper::StatusCode; use serde::Serialize; use serde_json::json; +use sql_builder::prelude::*; use sql_builder::SqlBuilder; use sqlx::{FromRow, Postgres, Transaction}; use std::{ @@ -32,6 +29,7 @@ use std::{ hash::{Hash, Hasher}, sync::Arc, }; +use windmill_audit::{audit_log, ActionKind}; use windmill_common::{ error::{Error, JsonResult, Result}, schedule::Schedule, @@ -44,6 +42,7 @@ use windmill_common::{ list_elems_from_hub, not_found_if_none, paginate, require_admin, Pagination, StripPath, }, }; +use windmill_parser::MainArgSignature; use windmill_queue::{self, schedule::push_scheduled_job, QueueTransaction}; const MAX_HASH_HISTORY_LENGTH_STORED: usize = 20; @@ -85,6 +84,7 @@ pub fn workspaced_service() -> Router { .route("/deployment_status/h/:hash", get(get_deployment_status)) .route("/list_paths", get(list_paths)) } + async fn list_scripts( authed: Authed, Extension(user_db): Extension, diff --git a/backend/windmill-common/src/utils.rs b/backend/windmill-common/src/utils.rs index c1c7db3592..23f750eb51 100644 --- a/backend/windmill-common/src/utils.rs +++ b/backend/windmill-common/src/utils.rs @@ -6,11 +6,10 @@ * LICENSE-AGPL for a copy of the license. */ -use rand::{distributions::Alphanumeric, thread_rng, Rng}; -use serde::Deserialize; -use sha2::{Digest, Sha256}; - use crate::error::{Error, Result}; +use rand::{distributions::Alphanumeric, thread_rng, Rng}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; pub const MAX_PER_PAGE: usize = 10000; pub const DEFAULT_PER_PAGE: usize = 1000; @@ -20,7 +19,8 @@ pub struct Pagination { pub page: Option, pub per_page: Option, } -#[derive(Deserialize)] + +#[derive(Debug, Serialize, Deserialize)] pub struct StripPath(pub String); impl StripPath { diff --git a/backend/windmill-worker/src/jobs.rs b/backend/windmill-worker/src/jobs.rs index 5008620015..826d96859b 100644 --- a/backend/windmill-worker/src/jobs.rs +++ b/backend/windmill-worker/src/jobs.rs @@ -223,14 +223,17 @@ pub async fn schedule_again_if_scheduled<'c, R: rsmq_async::RsmqConnection + Clo script_path: &str, w_id: &str, ) -> windmill_common::error::Result> { - let schedule = get_schedule_opt(tx.transaction_mut(), w_id, schedule_path) - .await? - .ok_or_else(|| { - Error::InternalErr(format!( - "Could not find schedule {:?} for workspace {}", - schedule_path, w_id - )) - })?; + let schedule = get_schedule_opt(tx.transaction_mut(), w_id, schedule_path).await?; + + if schedule.is_none() { + tracing::error!( + "Schedule {schedule_path} in {w_id} not found. Impossible to schedule again" + ); + return Ok(tx); + } + + let schedule = schedule.unwrap(); + if schedule.enabled && script_path == schedule.script_path { let res = windmill_queue::schedule::push_scheduled_job( tx, diff --git a/backend/windmill-worker/src/worker.rs b/backend/windmill-worker/src/worker.rs index 3fc38fec28..3f4fe6cd93 100644 --- a/backend/windmill-worker/src/worker.rs +++ b/backend/windmill-worker/src/worker.rs @@ -79,7 +79,7 @@ async fn copy_cache_from_bucket(bucket: &str, tx: Option>) -> Option: .arg("--size-only") .arg("--fast-list") .arg("--exclude") - .arg(format!("\"{TAR_CACHE_FILENAME}\"")) + .arg(format!("\"{TAR_CACHE_FILENAME},/deno/gen/file/**\"")) .stdin(Stdio::null()) .stdout(Stdio::null()) .spawn() @@ -128,7 +128,7 @@ async fn copy_cache_to_bucket(bucket: &str) { .arg("--size-only") .arg("--fast-list") .arg("--exclude") - .arg(format!("\"{TAR_CACHE_FILENAME}\"")) + .arg(format!("\"{TAR_CACHE_FILENAME},/deno/gen/file/**\"")) .stdin(Stdio::null()) .stdout(Stdio::null()) .spawn() @@ -1810,10 +1810,11 @@ async fn handle_deno_job( //do not cache local dependencies let reload = format!("--reload={base_internal_url}"); let child = async { - let mut args = Vec::new(); let script_path = format!("{job_dir}/wrapper.ts"); let import_map_path = format!("{job_dir}/import_map.json"); + let mut args = Vec::new(); args.push("run"); + args.push("--no-check"); args.push("--import-map"); args.push(&import_map_path); args.push(&reload); diff --git a/frontend/src/lib/components/AppConnect.svelte b/frontend/src/lib/components/AppConnect.svelte index 1dce2f2779..f30ffbbd4b 100644 --- a/frontend/src/lib/components/AppConnect.svelte +++ b/frontend/src/lib/components/AppConnect.svelte @@ -2,7 +2,12 @@ const apiTokenApps: Record = { airtable: { img: '/airtable_connect.png', - instructions: ['Click on the top-right avatar', 'Click on Account', 'Find "Api"'] + instructions: [ + 'Click to https://airtable.com/create/tokens', + 'Click on "Create new token"', + 'Set a name, specify the scopes or the access level and click on "Create token"', + 'Copy the token' + ] }, discord_webhook: { img: '/discord_webhook.png', diff --git a/frontend/src/lib/components/DisplayResult.svelte b/frontend/src/lib/components/DisplayResult.svelte index 2c78697979..5c886d1622 100644 --- a/frontend/src/lib/components/DisplayResult.svelte +++ b/frontend/src/lib/components/DisplayResult.svelte @@ -101,8 +101,8 @@ >{/if}{#if typeof result == 'object' && Object.keys(result).length > 0}
The result keys are: {truncate(Object.keys(result).join(', '), 50)} -
- +
+
{/if}{#if !forceJson && resultKind == 'table-col'}
+ import { Button } from '$lib/components/common' + import { InputService, type Input, RunnableType, type CreateInput } from '$lib/gen/index.js' + import { userStore, workspaceStore } from '$lib/stores.js' + import { classNames, displayDate, displayDaysAgo, sendUserToast } from '$lib/utils.js' + import { faSave } from '@fortawesome/free-solid-svg-icons' + import { createEventDispatcher } from 'svelte' + import { Pane, Splitpanes } from 'svelte-splitpanes' + import ObjectViewer from './propertyPicker/ObjectViewer.svelte' + import { ArrowLeftIcon, Edit, X } from 'lucide-svelte' + import Toggle from './Toggle.svelte' + import Tooltip from './Tooltip.svelte' + + export let scriptHash: string | null = null + export let scriptPath: string | null = null + export let flowPath: string | null = null + + let runnableId: string | undefined = scriptHash || scriptPath || flowPath || undefined + let runnableType: RunnableType | undefined = scriptHash + ? RunnableType.SCRIPT_HASH + : scriptPath + ? RunnableType.SCRIPT_PATH + : flowPath + ? RunnableType.FLOW_PATH + : undefined + + // Are the current Inputs valid and able to be saved? + export let isValid: boolean + export let args: object + + let previousInputs: Input[] = [] + interface EditableInput extends Input { + isEditing?: boolean + isSaving?: boolean + } + let savedInputs: EditableInput[] = [] + + let selectedInput: Input | null + + async function loadInputHistory() { + previousInputs = await InputService.getInputHistory({ + workspace: $workspaceStore!, + runnableId, + runnableType, + perPage: 10 + }) + } + + async function loadSavedInputs() { + savedInputs = await InputService.listInputs({ + workspace: $workspaceStore!, + runnableId, + runnableType, + perPage: 10 + }) + } + + let savingInputs = false + + async function saveInput(args: object) { + savingInputs = true + + const requestBody: CreateInput = { + name: 'Saved ' + displayDate(new Date()), + args + } + + try { + let id = await InputService.createInput({ + workspace: $workspaceStore!, + runnableId, + runnableType, + requestBody + }) + + const input = { + id, + created_by: '', + created_at: new Date().toISOString(), + is_public: false, + ...requestBody + } + savedInputs = [input, ...savedInputs] + } catch (err) { + console.error(err) + sendUserToast(`Failed to save Input: ${err}`, true) + } + + savingInputs = false + } + + async function updateInput(input: EditableInput) { + input.isSaving = true + + try { + await InputService.updateInput({ + workspace: $workspaceStore!, + requestBody: { + id: input.id, + name: input.name, + is_public: input.is_public + } + }) + } catch (err) { + console.error(err) + sendUserToast(`Failed to update Input: ${err}`, true) + } + + input.isSaving = false + } + + async function deleteInput(input: Input) { + try { + await InputService.deleteInput({ + workspace: $workspaceStore!, + input: input.id + }) + savedInputs = savedInputs.filter((i) => i.id !== input.id) + if (selectedInput === input) { + selectedInput = null + } + } catch (err) { + console.error(err) + sendUserToast(`Failed to delete Input: ${err}`, true) + } + } + + $: { + if ($workspaceStore && (scriptHash || scriptPath || flowPath)) { + loadInputHistory() + loadSavedInputs() + } + } + + const dispatch = createEventDispatcher() + + const selectArgs = (selected_args: object) => { + dispatch('selected_args', selected_args) + } + + +
+ + +
+
+ Saved Inputs Shared tooltips are available to anyone with access to the script + +
+ +
+ {#if savedInputs.length > 0} + {#each savedInputs as i} + + +
+ {:else} + By {i.created_by} + {/if} +
+ + {/each} + {:else} +
No saved Inputs
+ {/if} +
+
+ + + +
+ Previous Inputs + +
+ {#if previousInputs.length > 0} + {#each previousInputs as i} + + {/each} + {:else} +
No previous Inputs
+ {/if} +
+
+
+ + +
+ Preview + +
+ {#if Object.keys(selectedInput?.args || {}).length > 0} +
+ +
+ {:else} +
+ Select an Input to preview scripts arguments +
+ {/if} +
+
+ +
+ +
+
+ +
diff --git a/frontend/src/lib/components/Toast.svelte b/frontend/src/lib/components/Toast.svelte index b04fd77d33..d58790b196 100644 --- a/frontend/src/lib/components/Toast.svelte +++ b/frontend/src/lib/components/Toast.svelte @@ -22,18 +22,17 @@
-
+
-
+
{#if error} - + {:else} - + {/if}
-
-

{error ? 'Error' : 'Success'}

-

{message}

+
+

{message}

+ + + +
+ {#if flow} +
+
+
+
+
+
+ {#if !$userStore?.operator && can_write} +
+ +
+ {/if} + + + +
+
+
+

+ {defaultIfEmptyString(flow.summary, flow.path)} +

+ {#if !emptyString(flow.summary)} +

{flow.path}

+ {/if} +
+
+ + {#if flow} + Edited {displayDaysAgo(flow.edited_at || '')} by {flow.edited_by || 'unknown'} + {/if} + + +
- {/if} -
- -
-
-
-
-

- {defaultIfEmptyString(flow.summary, flow.path)} -

- {#if !emptyString(flow.summary)} -

{flow.path}

- {/if} -
-
- - {#if flow} - Edited {displayDaysAgo(flow.edited_at || '')} by {flow.edited_by || 'unknown'} - {/if} - - - + {#if !emptyString(flow.description)} +
+ {defaultIfEmptyString(flow.description, 'No description')} +
+ {/if}
-
+
+ +
+ + {:else} + + {/if}
- {#if !emptyString(flow.description)} -
- {defaultIfEmptyString(flow.description, 'No description')} -
- {/if} -
- - {:else} - - {/if} - + + + (args = e.detail)} /> + + + diff --git a/frontend/src/routes/(root)/(logged)/scripts/run/[...hash]/+page.svelte b/frontend/src/routes/(root)/(logged)/scripts/run/[...hash]/+page.svelte index e87185fd3b..a8fd61921e 100644 --- a/frontend/src/routes/(root)/(logged)/scripts/run/[...hash]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/scripts/run/[...hash]/+page.svelte @@ -1,10 +1,11 @@ - - {#if script} -
- {#if topHash} - - This hash is not HEAD (latest non-archived version at this path) : - Go to the HEAD of this path - - {/if} -
-
-
-
- {#if !$userStore?.operator && can_write} -
- + + + +
+ {#if script} +
+ {#if topHash} + + This hash is not HEAD (latest non-archived version at this path) : + Go to the HEAD of this path + + {/if} +
+
+
+
+ {#if !$userStore?.operator && can_write} +
+ +
+ {/if} +
+ +
+
+ +
+
+
+

+ {defaultIfEmptyString(script.summary, script.path)} +

+ {#if !emptyString(script.summary)} +

{script.path}

+ {/if} +
+
+
+ + {#if script} + Edited {displayDaysAgo(script.created_at || '')} by {script.created_by || + 'unknown'} + {/if} + + + {truncateHash(script?.hash ?? '')} + + {#if script?.is_template} + Template + {/if} + {#if script && script.kind !== 'script'} + + {script?.kind} + + {/if} + +
- {/if} -
- -
-
-
-
-

- {defaultIfEmptyString(script.summary, script.path)} -

- {#if !emptyString(script.summary)} -

{script.path}

- {/if} + {#if !emptyString(script.description)} +
+ {defaultIfEmptyString(script.description, 'No description')} +
+ {/if} +
+ + {#if script?.lock_error_logs} + + {:else if script && script?.lock == undefined} + + {:else} +
+
-
-
- - {#if script} - Edited {displayDaysAgo(script.created_at || '')} by {script.created_by || 'unknown'} - {/if} - - - {truncateHash(script?.hash ?? '')} - - {#if script?.is_template} - Template - {/if} - {#if script && script.kind !== 'script'} - - {script?.kind} - - {/if} - -
-
+ + {/if} + {:else} + + {/if}
- {#if !emptyString(script.description)} -
- {defaultIfEmptyString(script.description, 'No description')} -
- {/if} -
+ - {#if script?.lock_error_logs} - - {:else if script && script?.lock == undefined} - - {:else} - - {/if} - {:else} - - {/if} - + + (args = e.detail)} /> + + + diff --git a/frontend/static/airtable_connect.png b/frontend/static/airtable_connect.png index 2a5447d5b8..2818b315d7 100644 Binary files a/frontend/static/airtable_connect.png and b/frontend/static/airtable_connect.png differ