Merge branch 'main' into fix-icon-only

This commit is contained in:
Faton Ramadani
2023-04-13 09:27:45 +02:00
committed by GitHub
27 changed files with 1206 additions and 314 deletions
@@ -0,0 +1,2 @@
DROP TABLE IF EXISTS input;
DROP TYPE RUNNABLE_TYPE;
@@ -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
);
+206 -19
View File
@@ -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:
+9 -11
View File
@@ -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))
+282
View File
@@ -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<Utc>,
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<chrono::Utc>,
args: serde_json::Value,
created_by: String,
is_public: bool,
}
async fn get_input_history(
authed: Authed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Query(pagination): Query<Pagination>,
Query(r): Query<RunnableParams>,
) -> JsonResult<Vec<Input>> {
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<UserDB>,
Path(w_id): Path<String>,
Query(pagination): Query<Pagination>,
Query(r): Query<RunnableParams>,
) -> JsonResult<Vec<Input>> {
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<Input> = 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<UserDB>,
Path(w_id): Path<String>,
Query(r): Query<RunnableParams>,
Json(input): Json<CreateInput>,
) -> JsonResult<String> {
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<UserDB>,
Path(w_id): Path<String>,
Json(input): Json<UpdateInput>,
) -> JsonResult<String> {
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<UserDB>,
Path((w_id, i_id)): Path<(String, Uuid)>,
) -> JsonResult<String> {
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()))
}
+6 -7
View File
@@ -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))
+23 -21
View File
@@ -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(
+4 -4
View File
@@ -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<UserDB>,
+5 -5
View File
@@ -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<usize>,
pub per_page: Option<usize>,
}
#[derive(Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
pub struct StripPath(pub String);
impl StripPath {
+11 -8
View File
@@ -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<QueueTransaction<'c, R>> {
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,
+4 -3
View File
@@ -79,7 +79,7 @@ async fn copy_cache_from_bucket(bucket: &str, tx: Option<Sender<()>>) -> 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);
@@ -2,7 +2,12 @@
const apiTokenApps: Record<string, { img?: string; instructions: string[]; key?: string }> = {
airtable: {
img: '/airtable_connect.png',
instructions: ['Click on the top-right avatar', 'Click on Account', 'Find "Api"']
instructions: [
'Click to <a href="https://airtable.com/create/tokens" target="_blank" rel=”noopener noreferrer”>https://airtable.com/create/tokens</a>',
'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',
@@ -101,8 +101,8 @@
>{/if}{#if typeof result == 'object' && Object.keys(result).length > 0}<div
class="mb-2 w-full text-sm text-gray-700 relative"
>The result keys are: <b>{truncate(Object.keys(result).join(', '), 50)}</b>
<div class="text-gray-500 text-sm absolute top-6 right-0">
<button on:click={jsonViewer.openDrawer}>Expand JSON</button>
<div class="text-gray-500 text-xs absolute top-5 right-0">
<button on:click={jsonViewer.openDrawer}>Expand</button>
</div></div
>{/if}{#if !forceJson && resultKind == 'table-col'}<div
class="grid grid-flow-col-dense border border-gray-200 rounded-md"
@@ -0,0 +1,326 @@
<script lang="ts">
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)
}
</script>
<div class="min-w-[300px] h-full">
<Splitpanes horizontal={true}>
<Pane>
<div class="w-full flex flex-col gap-4 p-4">
<div class="w-full flex justify-between items-center gap-4 flex-wrap">
<span class="text-sm font-extrabold flex-shrink-0"
>Saved Inputs <Tooltip
>Shared tooltips are available to anyone with access to the script</Tooltip
></span
>
<Button
on:click={() => saveInput(args)}
disabled={!isValid}
loading={savingInputs}
startIcon={{ icon: faSave }}
color="blue"
size="xs"
>
<span>Save Current Input</span>
</Button>
</div>
<div class="w-full flex flex-col gap-2 h-full overflow-y-auto p">
{#if savedInputs.length > 0}
{#each savedInputs as i}
<button
class={classNames(
`w-full flex items-center group justify-between gap-4 py-2 px-4 text-left border rounded-md hover:bg-gray-100 transition-all`,
selectedInput === i ? 'border-blue-500 bg-blue-50' : ''
)}
on:click={() => {
if (!i.isEditing) {
if (selectedInput === i) {
selectedInput = null
} else {
selectedInput = i
}
}
}}
>
<div class="w-full h-full items-center justify-between flex gap-1 min-w-0">
{#if i.isEditing}
<form
on:submit={() => {
updateInput(i)
i.isEditing = false
i.isSaving = false
}}
class="w-full"
>
<input type="text" bind:value={i.name} class="text-gray-700" />
</form>
{:else}
<small
class="whitespace-nowrap overflow-hidden text-ellipsis flex-shrink text-left"
>
{i.name}
</small>
{/if}
{#if i.created_by == $userStore?.username || $userStore?.is_admin || $userStore?.is_super_admin}
<div class="items-center flex gap-2">
{#if !i.isEditing}
<div class="group-hover:block hidden -my-2">
<Toggle
size="xs"
options={{ right: 'shared' }}
bind:checked={i.is_public}
on:change={() => {
updateInput(i)
}}
/>
</div>
{/if}
<Button
loading={i.isSaving}
color="gray"
size="xs"
variant="border"
spacingSize="xs2"
btnClasses={'group-hover:block hidden'}
on:click={(e) => {
e.stopPropagation()
i.isEditing = !i.isEditing
if (!i.isEditing) {
updateInput(i)
i.isSaving = false
}
}}
>
<Edit class="w-4 h-4" />
</Button>
<Button
color="red"
size="xs"
spacingSize="xs2"
variant="border"
btnClasses={i.isEditing ? 'block' : 'group-hover:block hidden'}
on:click={() => deleteInput(i)}
>
<X class="w-4 h-4" />
</Button>
</div>
{:else}
<span class="text-xs text-gray-600">By {i.created_by}</span>
{/if}
</div>
</button>
{/each}
{:else}
<div class="text-center text-gray-500">No saved Inputs</div>
{/if}
</div>
</div>
</Pane>
<Pane>
<div class="w-full flex flex-col gap-4 p-4">
<span class="text-sm font-extrabold">Previous Inputs</span>
<div class="w-full flex flex-col gap-1 p-0 h-full overflow-y-auto">
{#if previousInputs.length > 0}
{#each previousInputs as i}
<button
class={classNames(
`w-full flex items-center justify-between gap-4 py-2 px-4 text-left border rounded-sm hover:bg-gray-100 transition-a`,
selectedInput === i ? 'border-blue-500 bg-blue-50' : ''
)}
on:click={() => {
if (selectedInput === i) {
selectedInput = null
} else {
selectedInput = i
}
}}
>
<div class="w-full h-full items-center flex gap-4 min-w-0">
<small
class="whitespace-nowrap overflow-hidden text-ellipsis flex-shrink text-left"
>
{displayDaysAgo(i.created_at)} by {i.created_by}
</small>
</div>
</button>
{/each}
{:else}
<div class="text-center text-gray-500">No previous Inputs</div>
{/if}
</div>
</div>
</Pane>
<Pane class="flex flex-col justify-between">
<div class="w-full flex flex-col gap-4 p-4 h-full">
<span class="text-sm font-extrabold">Preview</span>
<div class="w-full h-full overflow-auto">
{#if Object.keys(selectedInput?.args || {}).length > 0}
<div class="border h-full p-2">
<ObjectViewer json={selectedInput?.args} />
</div>
{:else}
<div class="text-center text-gray-500">
Select an Input to preview scripts arguments
</div>
{/if}
</div>
</div>
<div class="w-full flex flex-col p-4">
<Button
color="blue"
btnClasses="w-full"
size="sm"
spacingSize="xl"
on:click={() => selectArgs(selectedInput?.args)}
disabled={Object.keys(selectedInput?.args || {}).length === 0}
>
<ArrowLeftIcon class="w-4 h-4 mr-2" />
Use Input
</Button>
</div>
</Pane>
</Splitpanes>
</div>
+6 -7
View File
@@ -22,18 +22,17 @@
<div
class="pointer-events-auto w-full max-w-sm overflow-hidden bg-white shadow-lg ring-1 ring-black ring-opacity-5 border"
>
<div class="p-4">
<div class="p-2 min-h-[60px]">
<div class="flex items-start">
<div class="flex-shrink-0">
<div class="flex-shrink-0 mt-0.5">
{#if error}
<XCircleIcon class="h-6 w-6 text-red-400" />
<XCircleIcon class="h-4 w-4 text-red-400" />
{:else}
<CheckCircle2 class="h-6 w-6 text-green-400" />
<CheckCircle2 class="h-4 w-4 text-green-400" />
{/if}
</div>
<div class="ml-3 w-0 flex-1 pt-0.5">
<p class="text-sm font-medium text-gray-900">{error ? 'Error' : 'Success'}</p>
<p class="mt-1 text-sm text-gray-500">{message}</p>
<div class="ml-3 w-0 flex-1">
<p class="text-sm text-gray-500">{message}</p>
</div>
<div class="ml-4 flex flex-shrink-0">
<button
+3 -3
View File
@@ -53,9 +53,9 @@
}}
/>
<div
class="w-11 h-6 bg-gray-200 rounded-full peer peer-focus:ring-4 peer-focus:ring-blue-300
peer-checked:after:translate-x-full peer-checked:after:border-white after:content-['']
after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300
class="w-11 h-6 bg-gray-200 rounded-full peer peer-focus:ring-4 peer-focus:ring-blue-300
peer-checked:after:translate-x-full peer-checked:after:border-white after:content-['']
after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300
after:border after:rounded-full after:h-5 after:w-5 after:transition-all {color == 'red'
? 'peer-checked:bg-red-600'
: 'peer-checked:bg-blue-600'}"
@@ -11,7 +11,7 @@
import { sendUserToast } from '$lib/utils'
import { gridColumns } from '../../gridUtils'
const { app, selectedComponent, worldStore, focusedGrid, componentControl } =
const { app, selectedComponent, focusedGrid, componentControl } =
getContext<AppViewerContext>('AppViewerContext')
const { history, movingcomponents } = getContext<AppEditorContext>('AppEditorContext')
@@ -209,7 +209,6 @@
$selectedComponent = nitems.map((x) => x)
}
$worldStore = $worldStore
$app = $app
}
@@ -15,8 +15,7 @@
import { push } from '$lib/history'
import { flip } from 'svelte/animate'
const { app, selectedComponent, focusedGrid, worldStore } =
getContext<AppViewerContext>('AppViewerContext')
const { app, selectedComponent, focusedGrid } = getContext<AppViewerContext>('AppViewerContext')
const { history } = getContext<AppEditorContext>('AppEditorContext')
@@ -33,7 +32,6 @@
$selectedComponent = [id]
$app = $app
$worldStore = $worldStore
}
let search = ''
@@ -135,9 +135,7 @@
runnable?.type === 'runnableByName' &&
runnable?.inlineScript?.refreshOn?.find((x) => x.id === from)
) {
console.log('processss')
runnable.inlineScript.refreshOn = runnable.inlineScript.refreshOn.map((x) => {
console.log('renaming', x)
if (x.id === from) {
return {
id: to,
@@ -87,22 +87,6 @@
inlineScript?.language == 'frontend' && worldStore
? buildExtraLib($worldStore?.outputsById ?? {}, id, false, $state, true)
: undefined
let refreshOn: string = inlineScript?.refreshOn?.map((x) => `${x.id}.${x.key}`).join(' ') ?? ''
$: handleRefreshOn(refreshOn)
function handleRefreshOn(refreshOn: string) {
if (refreshOn && refreshOn != '' && inlineScript) {
inlineScript.refreshOn = refreshOn
.split(' ')
.filter((x) => x.split('.').length == 2)
.map((x) => {
const [id, key] = x.split('.')
return { id, key }
})
}
}
</script>
{#if inlineScript}
@@ -7,7 +7,7 @@
export let componentInput: ConnectedAppInput
const { connectingInput, app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
const { connectingInput, app } = getContext<AppViewerContext>('AppViewerContext')
function applyConnection() {
if (
@@ -22,7 +22,6 @@
hoveredComponent: undefined
}
$app = $app
$worldStore = $worldStore
}
}
@@ -21,7 +21,7 @@
let badgeClass = 'inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium border'
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
const { app } = getContext<AppViewerContext>('AppViewerContext')
const { connectingInput } = getContext<AppViewerContext>('AppViewerContext')
@@ -60,7 +60,6 @@
hoveredComponent: undefined
}
$app = $app
$worldStore = $worldStore
}
}
@@ -11,9 +11,11 @@
<div
class={'bg-white py-3 ' +
(stickToTop ? 'lg:sticky lg:top-0 z-[500] border-b border-gray-200 border-opacity-0 duration-300 '
+ (scrollY >= 30 ? 'border-opacity-100 ' : '') : '')
+ ($$props.class || '')}
(stickToTop
? 'lg:sticky lg:top-0 z-[500] border-b border-gray-200 border-opacity-0 duration-300 ' +
(scrollY >= 30 ? 'border-opacity-100 ' : '')
: '') +
($$props.class || '')}
>
<div class={'w-full flex flex-wrap justify-between items-center gap-4 ' + wide}>
<div class="flex flex-wrap items-center gap-2">
@@ -26,7 +28,7 @@
<slot name="middle" />
{/if}
</div>
<div class="flex flex-wrap items-center gap-2">
<div class="flex flex-wrap items-center gap-2 lg:gap-4">
{#if $$slots.right}
<slot name="right" />
{/if}
+2 -1
View File
@@ -20,8 +20,9 @@ export const DENO_INIT_CODE = `// Ctrl/CMD+. to cache dependencies on imports ho
export async function main(
a: number,
b: "my" | "enum",
//c: Resource<'postgresql'>,
d = "inferred type string from default arg",
c = { nested: "object" },
e = { nested: "object" },
//e: wmill.Base64
) {
// let x = await wmill.getVariable('u/user/foo')
@@ -1,5 +1,12 @@
<script lang="ts">
import { goto } from '$app/navigation'
import { page } from '$app/stores'
import SavedInputs from '$lib/components/SavedInputs.svelte'
import RunForm from '$lib/components/RunForm.svelte'
import SharedBadge from '$lib/components/SharedBadge.svelte'
import { Button, Kbd, Skeleton } from '$lib/components/common'
import { FlowService, JobService, type Flow } from '$lib/gen'
import { userStore, workspaceStore } from '$lib/stores'
import {
canWrite,
defaultIfEmptyString,
@@ -8,20 +15,19 @@
getModifierKey,
sendUserToast
} from '$lib/utils'
import { FlowService, type Flow, JobService } from '$lib/gen'
import { goto } from '$app/navigation'
import { userStore, workspaceStore } from '$lib/stores'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import RunForm from '$lib/components/RunForm.svelte'
import { Button, Kbd, Skeleton } from '$lib/components/common'
import { faEye, faPen, faPlay } from '@fortawesome/free-solid-svg-icons'
import SharedBadge from '$lib/components/SharedBadge.svelte'
import SplitPanesWrapper from '$lib/components/splitPanes/SplitPanesWrapper.svelte'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import { tweened } from 'svelte/motion'
import { cubicOut } from 'svelte/easing'
import { ArrowLeftIcon, ArrowRightIcon } from 'lucide-svelte'
const path = $page.params.path
let flow: Flow | undefined
let runForm: RunForm | undefined
let isValid = true
let can_write = false
let args: object = {}
async function loadFlow() {
try {
@@ -76,87 +82,125 @@
break
}
}
let savedInputPaneSize = tweened(0, {
duration: 200,
easing: cubicOut
})
</script>
<svelte:window on:keydown={onKeyDown} />
<CenteredPage>
{#if flow}
<div class="flex flex-row flex-wrap justify-between gap-4 mb-6">
<div class="w-full">
<div class="flex flex-col mt-6 mb-2 w-full">
<div
class="flex flex-row-reverse w-full flex-wrap md:flex-nowrap justify-between gap-x-1"
>
<div class="flex flex-row gap-4">
{#if !$userStore?.operator && can_write}
<div>
<Button
size="sm"
startIcon={{ icon: faPen }}
disabled={flow == undefined}
variant="border"
href="/flows/edit/{flow?.path}">Edit</Button
>
<SplitPanesWrapper class="h-screen">
<Splitpanes class="overflow-hidden">
<Pane class="px-4 flex justify-center" size={100 - $savedInputPaneSize} minSize={50}>
<div class="w-full max-w-4xl flex flex-col">
{#if flow}
<div class="flex flex-row flex-wrap justify-between gap-4 mb-6">
<div class="w-full">
<div class="flex flex-col mt-6 mb-2 w-full">
<div
class="flex flex-row-reverse w-full flex-wrap md:flex-nowrap justify-between gap-x-1"
>
<div class="flex flex-row gap-4">
<div class="flex flex-row items-center gap-2">
{#if !$userStore?.operator && can_write}
<div>
<Button
size="sm"
startIcon={{ icon: faPen }}
disabled={flow == undefined}
variant="border"
href="/flows/edit/{flow?.path}"
>
Edit
</Button>
</div>
{/if}
<Button
size="sm"
startIcon={{ icon: faEye }}
disabled={flow == undefined}
variant="border"
href="/flows/get/{flow?.path}?workspace_id={$workspaceStore}"
>
Flow
</Button>
<Button
startIcon={{ icon: faPlay }}
disabled={runForm == undefined || !isValid}
on:click={() => runForm?.run()}
>Run <Kbd class="ml-2">{getModifierKey()}+Enter</Kbd></Button
>
</div>
</div>
<div class="flex flex-col">
<h1 class="break-words py-2 mr-2">
{defaultIfEmptyString(flow.summary, flow.path)}
</h1>
{#if !emptyString(flow.summary)}
<h2 class="font-bold pb-4">{flow.path}</h2>
{/if}
</div></div
>
<div class="flex items-center gap-2">
<span class="text-sm text-gray-500">
{#if flow}
Edited {displayDaysAgo(flow.edited_at || '')} by {flow.edited_by || 'unknown'}
{/if}
</span>
<SharedBadge canWrite={can_write} extraPerms={flow?.extra_perms ?? {}} />
</div>
{/if}
<div class="md:pr-4">
<Button
size="sm"
startIcon={{ icon: faEye }}
disabled={flow == undefined}
btnClasses="mr-4"
variant="border"
href="/flows/get/{flow?.path}?workspace_id={$workspaceStore}">View flow</Button
>
</div>
<div>
<Button
startIcon={{ icon: faPlay }}
disabled={runForm == undefined || !isValid}
on:click={() => runForm?.run()}
>Run <Kbd class="ml-2">{getModifierKey()}+Enter</Kbd></Button
>
</div>
</div>
<div class="flex flex-col">
<h1 class="break-words py-2 mr-2">
{defaultIfEmptyString(flow.summary, flow.path)}
</h1>
{#if !emptyString(flow.summary)}
<h2 class="font-bold pb-4">{flow.path}</h2>
{/if}
</div></div
>
<div class="flex items-center gap-2">
<span class="text-sm text-gray-500">
{#if flow}
Edited {displayDaysAgo(flow.edited_at || '')} by {flow.edited_by || 'unknown'}
{/if}
</span>
<SharedBadge canWrite={can_write} extraPerms={flow?.extra_perms ?? {}} />
{#if !emptyString(flow.description)}
<div class="prose text-sm box max-w-6xl w-full mt-8">
{defaultIfEmptyString(flow.description, 'No description')}
</div>
{/if}
</div>
</div>
<div class="flex justify-end">
<Button
size="xs"
variant="border"
disabled={flow == undefined}
color="dark"
on:click={() => {
//savedInputPaneSize = savedInputPaneSize == 0 ? 30 : 0
savedInputPaneSize.set($savedInputPaneSize === 0 ? 30 : 0)
}}
>
<div class="flex flex-row gap-2 items-center">
{$savedInputPaneSize === 0 ? 'Open input library' : 'Close input library'}
{#if $savedInputPaneSize === 0}
<ArrowRightIcon class="w-4 h-4" />
{:else}
<ArrowLeftIcon class="w-4 h-4" />
{/if}
</div>
</Button>
</div>
<RunForm
{loading}
autofocus
bind:this={runForm}
bind:isValid
detailed={false}
runnable={flow}
runAction={runFlow}
viewCliRun
isFlow
bind:args
/>
{:else}
<Skeleton layout={[2, [3], 1, [2], 4, [4], 3, [8]]} />
{/if}
</div>
{#if !emptyString(flow.description)}
<div class="prose text-sm box max-w-6xl w-full mt-8">
{defaultIfEmptyString(flow.description, 'No description')}
</div>
{/if}
</div>
<RunForm
{loading}
autofocus
bind:this={runForm}
bind:isValid
detailed={false}
runnable={flow}
runAction={runFlow}
viewCliRun
isFlow
/>
{:else}
<Skeleton layout={[2, [3], 1, [2], 4, [4], 3, [8]]} />
{/if}
</CenteredPage>
</Pane>
<Pane size={$savedInputPaneSize}>
<SavedInputs flowPath={path} {isValid} {args} on:selected_args={(e) => (args = e.detail)} />
</Pane>
</Splitpanes>
</SplitPanesWrapper>
@@ -1,10 +1,11 @@
<script lang="ts">
import { goto } from '$app/navigation'
import { page } from '$app/stores'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import { Alert, Badge, Button, Kbd, Skeleton } from '$lib/components/common'
import RunForm from '$lib/components/RunForm.svelte'
import SavedInputs from '$lib/components/SavedInputs.svelte'
import SharedBadge from '$lib/components/SharedBadge.svelte'
import { Alert, Badge, Button, Kbd, Skeleton } from '$lib/components/common'
import SplitPanesWrapper from '$lib/components/splitPanes/SplitPanesWrapper.svelte'
import { JobService, ScriptService, type Script } from '$lib/gen'
import { inferArgs } from '$lib/infer'
import { userStore, workspaceStore } from '$lib/stores'
@@ -19,6 +20,10 @@
truncateHash
} from '$lib/utils'
import { faEye, faPen, faPlay } from '@fortawesome/free-solid-svg-icons'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import { tweened } from 'svelte/motion'
import { cubicOut } from 'svelte/easing'
import { ArrowLeftIcon, ArrowRightIcon } from 'lucide-svelte'
$: hash = $page.params.hash
let script: Script | undefined
@@ -26,15 +31,18 @@
let isValid = true
let can_write = false
let topHash: string | undefined
let args: object = {}
async function loadScript() {
if (hash) {
script = await ScriptService.getScriptByHash({ workspace: $workspaceStore!, hash })
if (script.schema == undefined) {
script.schema = emptySchema()
await inferArgs(script.language, script.content, script.schema)
script = script
}
if (script.path && script.archived) {
const script_by_path = await ScriptService.getScriptByPath({
workspace: $workspaceStore!,
@@ -44,6 +52,7 @@
} else {
topHash = undefined
}
can_write =
script.workspace_id == $workspaceStore &&
canWrite(script.path, script.extra_perms!, $userStore)
@@ -95,121 +104,163 @@
break
}
}
let savedInputPaneSize = tweened(0, {
duration: 200,
easing: cubicOut
})
</script>
<svelte:window on:keydown={onKeyDown} />
<CenteredPage>
{#if script}
<div class="flex flex-col justify-between gap-4 mb-6">
{#if topHash}
<Alert type="warning" title="Not HEAD">
This hash is not HEAD (latest non-archived version at this path) :
<a href="/scripts/run/{topHash}">Go to the HEAD of this path</a>
</Alert>
{/if}
<div class="w-full">
<div class="flex flex-col mt-6 mb-2 w-full">
<div
class="flex flex-row-reverse w-full flex-wrap md:flex-nowrap justify-between gap-x-1 gap-y-2"
>
<div class="flex flex-row gap-4">
{#if !$userStore?.operator && can_write}
<div>
<Button
size="sm"
startIcon={{ icon: faPen }}
disabled={script == undefined}
variant="border"
href="/scripts/edit/{script?.hash}">Edit</Button
>
<SplitPanesWrapper class="h-screen">
<Splitpanes class="overflow-hidden">
<Pane class="px-4 flex justify-center" size={100 - $savedInputPaneSize} minSize={50}>
<div class="w-full max-w-4xl flex flex-col">
{#if script}
<div class="flex flex-col justify-between gap-4 mb-6">
{#if topHash}
<Alert type="warning" title="Not HEAD">
This hash is not HEAD (latest non-archived version at this path) :
<a href="/scripts/run/{topHash}">Go to the HEAD of this path</a>
</Alert>
{/if}
<div class="w-full">
<div class="flex flex-col mt-6 mb-2 w-full">
<div
class="flex flex-row-reverse w-full flex-wrap md:flex-nowrap justify-between gap-x-1 gap-y-2"
>
<div class="flex flex-row gap-4">
{#if !$userStore?.operator && can_write}
<div>
<Button
size="sm"
startIcon={{ icon: faPen }}
disabled={script == undefined}
variant="border"
href="/scripts/edit/{script?.hash}">Edit</Button
>
</div>
{/if}
<div class="md:pr-4">
<Button
size="sm"
startIcon={{ icon: faEye }}
disabled={script == undefined}
variant="border"
href="/scripts/get/{script?.hash}?workspace_id={$workspaceStore}"
>Script</Button
>
</div>
<div>
<Button
startIcon={{ icon: faPlay }}
disabled={runForm == undefined || !isValid}
on:click={() => runForm?.run()}
>
Run <Kbd class="ml-2">{getModifierKey()}+Enter</Kbd>
</Button>
</div>
</div>
<div class="flex flex-col grow">
<h1 class="break-words py-2 mr-2">
{defaultIfEmptyString(script.summary, script.path)}
</h1>
{#if !emptyString(script.summary)}
<h2 class="font-bold pb-4">{script.path}</h2>
{/if}
</div>
</div>
<div class="flex items-center gap-2">
<span class="text-sm text-gray-500">
{#if script}
Edited {displayDaysAgo(script.created_at || '')} by {script.created_by ||
'unknown'}
{/if}
</span>
<Badge color="dark-gray">
{truncateHash(script?.hash ?? '')}
</Badge>
{#if script?.is_template}
<Badge color="blue">Template</Badge>
{/if}
{#if script && script.kind !== 'script'}
<Badge color="blue">
{script?.kind}
</Badge>
{/if}
<SharedBadge canWrite={can_write} extraPerms={script?.extra_perms ?? {}} />
</div>
{/if}
<div class="md:pr-4">
<Button
size="sm"
startIcon={{ icon: faEye }}
disabled={script == undefined}
variant="border"
href="/scripts/get/{script?.hash}?workspace_id={$workspaceStore}">View</Button
>
</div>
<div>
<Button
startIcon={{ icon: faPlay }}
disabled={runForm == undefined || !isValid}
on:click={() => runForm?.run()}
>
Run <Kbd class="ml-2">{getModifierKey()}+Enter</Kbd>
</Button>
</div>
</div>
<div class="flex flex-col grow">
<h1 class="break-words py-2 mr-2">
{defaultIfEmptyString(script.summary, script.path)}
</h1>
{#if !emptyString(script.summary)}
<h2 class="font-bold pb-4">{script.path}</h2>
{/if}
{#if !emptyString(script.description)}
<div class="prose text-sm box max-w-6xl w-full mb-4 mt-8">
{defaultIfEmptyString(script.description, 'No description')}
</div>
{/if}
</div>
{#if script?.lock_error_logs}
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4" role="alert">
<p class="font-bold">Not deployed properly</p>
<p>
This version of this script is unable to be run because because the deployment had
the following errors:
</p>
<pre class="w-full text-xs mt-2 whitespace-pre-wrap">{script.lock_error_logs}</pre>
</div>
{:else if script && script?.lock == undefined}
<div
class="bg-orange-100 border-l-4 border-orange-500 text-orange-700 p-4"
role="alert"
>
<p class="font-bold">Deployment in progress</p>
<p>Refresh this page in a few seconds.</p>
</div>
{:else}
<div class="flex justify-end">
<Button
variant="border"
size="xs"
color="dark"
on:click={() => {
//savedInputPaneSize = savedInputPaneSize == 0 ? 30 : 0
savedInputPaneSize.set($savedInputPaneSize === 0 ? 30 : 0)
}}
>
<div class="flex flex-row gap-2 items-center">
{$savedInputPaneSize === 0 ? 'Open input library' : 'Close input library'}
{#if $savedInputPaneSize === 0}
<ArrowRightIcon class="w-4 h-4" />
{:else}
<ArrowLeftIcon class="w-4 h-4" />
{/if}
</div>
</Button>
</div>
</div>
<div class="flex items-center gap-2">
<span class="text-sm text-gray-500">
{#if script}
Edited {displayDaysAgo(script.created_at || '')} by {script.created_by || 'unknown'}
{/if}
</span>
<Badge color="dark-gray">
{truncateHash(script?.hash ?? '')}
</Badge>
{#if script?.is_template}
<Badge color="blue">Template</Badge>
{/if}
{#if script && script.kind !== 'script'}
<Badge color="blue">
{script?.kind}
</Badge>
{/if}
<SharedBadge canWrite={can_write} extraPerms={script?.extra_perms ?? {}} />
</div>
</div>
<RunForm
{loading}
autofocus
detailed={false}
bind:isValid
bind:this={runForm}
runnable={script}
runAction={runScript}
viewCliRun
isFlow={false}
bind:args
/>
{/if}
{:else}
<Skeleton layout={[2, [3], 1, [2], 4, [4], 3, [8]]} />
{/if}
</div>
{#if !emptyString(script.description)}
<div class="prose text-sm box max-w-6xl w-full mb-4 mt-8">
{defaultIfEmptyString(script.description, 'No description')}
</div>
{/if}
</div>
</Pane>
{#if script?.lock_error_logs}
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4" role="alert">
<p class="font-bold">Not deployed properly</p>
<p>
This version of this script is unable to be run because because the deployment had the
following errors:
</p>
<pre class="w-full text-xs mt-2 whitespace-pre-wrap">{script.lock_error_logs}</pre>
</div>
{:else if script && script?.lock == undefined}
<div class="bg-orange-100 border-l-4 border-orange-500 text-orange-700 p-4" role="alert">
<p class="font-bold">Deployment in progress</p>
<p>Refresh this page in a few seconds.</p>
</div>
{:else}
<RunForm
{loading}
autofocus
detailed={false}
bind:isValid
bind:this={runForm}
runnable={script}
runAction={runScript}
viewCliRun
isFlow={false}
/>
{/if}
{:else}
<Skeleton layout={[2, [3], 1, [2], 4, [4], 3, [8]]} />
{/if}
</CenteredPage>
<Pane size={$savedInputPaneSize}>
<SavedInputs scriptHash={hash} {isValid} {args} on:selected_args={(e) => (args = e.detail)} />
</Pane>
</Splitpanes>
</SplitPanesWrapper>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 76 KiB