mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-25 16:02:11 +00:00
Merge branch 'main' into fix-icon-only
This commit is contained in:
@@ -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
|
||||
);
|
||||
@@ -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:
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,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))
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
+1
-2
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-2
@@ -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}
|
||||
|
||||
@@ -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 |
Reference in New Issue
Block a user