Compare commits
7 Commits
v1.87.0
...
dev-docker
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5131ee8b89 | ||
|
|
a5f6d73f7d | ||
|
|
670c84b901 | ||
|
|
92a293488e | ||
|
|
0cd1e65e46 | ||
|
|
6aa1008933 | ||
|
|
9434bbb18b |
2
backend/migrations/20230410122342_saved_inputs.down.sql
Normal file
2
backend/migrations/20230410122342_saved_inputs.down.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
DROP TABLE IF EXISTS input;
|
||||
DROP TYPE RUNNABLE_TYPE;
|
||||
13
backend/migrations/20230410122342_saved_inputs.up.sql
Normal file
13
backend/migrations/20230410122342_saved_inputs.up.sql
Normal file
@@ -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))
|
||||
|
||||
282
backend/windmill-api/src/inputs.rs
Normal file
282
backend/windmill-api/src/inputs.rs
Normal 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,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()
|
||||
|
||||
86
docker-compose-dev.yml
Normal file
86
docker-compose-dev.yml
Normal file
@@ -0,0 +1,86 @@
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:14
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- db_data:/var/lib/postgresql/data
|
||||
expose:
|
||||
- 5432
|
||||
ports:
|
||||
- 5432:5432
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
POSTGRES_DB: windmill
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
windmill_server:
|
||||
image: ghcr.io/windmill-labs/windmill:main
|
||||
deploy:
|
||||
replicas: 1
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- 8000
|
||||
ports:
|
||||
- 8000:8000
|
||||
environment:
|
||||
- DATABASE_URL=postgres://postgres:${DB_PASSWORD}@db/windmill?sslmode=disable
|
||||
- BASE_URL=http://${WM_BASE_URL}
|
||||
- RUST_LOG=info
|
||||
## You can set the number of workers to > 0 and not need any separate worker service
|
||||
- NUM_WORKERS=0
|
||||
- DISABLE_SERVER=false
|
||||
- METRICS_ADDR=false
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
# volumes:
|
||||
# - ./oauth.json/:/usr/src/app/oauth.json
|
||||
|
||||
windmill_worker:
|
||||
image: ghcr.io/windmill-labs/windmill:main
|
||||
deploy:
|
||||
replicas: 3
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- DATABASE_URL=postgres://postgres:${DB_PASSWORD}@db/windmill?sslmode=disable
|
||||
- BASE_URL=http://${WM_BASE_URL}
|
||||
- BASE_INTERNAL_URL=http://windmill_server:8000
|
||||
- RUST_LOG=info
|
||||
- NUM_WORKERS=1
|
||||
- DISABLE_SERVER=true
|
||||
- KEEP_JOB_DIR=false
|
||||
- DENO_PATH=/usr/bin/deno
|
||||
- PYTHON_PATH=/usr/local/bin/python3
|
||||
- METRICS_ADDR=false
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
# to mount the worker folder to debug,, KEEP_JOB_DIR=true and mount /tmp/windmill
|
||||
volumes:
|
||||
- worker_dependency_cache:/tmp/windmill/cache
|
||||
|
||||
lsp:
|
||||
image: ghcr.io/windmill-labs/windmill-lsp:latest
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- 3001
|
||||
caddy:
|
||||
image: caddy:2.5.2-alpine
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
environment:
|
||||
- BASE_URL=${WM_BASE_URL}
|
||||
|
||||
volumes:
|
||||
db_data: null
|
||||
worker_dependency_cache: null
|
||||
@@ -17,7 +17,7 @@ In the root folder:
|
||||
|
||||
```bash
|
||||
docker build . -t windmill
|
||||
docker compose up db windmill_server windmill_worker
|
||||
docker compose -f docker-compose-dev.yml up db windmill_server windmill_worker
|
||||
```
|
||||
|
||||
### 2. Backend is run by cargo
|
||||
@@ -28,20 +28,23 @@ docker compose up db windmill_server windmill_worker
|
||||
- Install llvm
|
||||
|
||||
**On OSX:**
|
||||
|
||||
```bash
|
||||
brew install llvm caddy gsed
|
||||
|
||||
|
||||
# make LLVM tools available on PATH
|
||||
echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.zshrc
|
||||
|
||||
|
||||
# now, restart your shell. You should now have the `lld` binary on your PATH.
|
||||
```
|
||||
|
||||
- To test that you have Rust and Cargo installed run `cargo --version`
|
||||
|
||||
- In your terminal, go to the backend directory and run `cargo build`
|
||||
- Run `cargo run`
|
||||
|
||||
**Known issue on M1 Mac while running `cargo build`**
|
||||
|
||||
- You may encounter `linking with cc failed` build time error.
|
||||
- To solve this run:
|
||||
```bash
|
||||
@@ -49,7 +52,6 @@ docker compose up db windmill_server windmill_worker
|
||||
source ~/.zshrc
|
||||
```
|
||||
|
||||
|
||||
**Do a Frontend Build**
|
||||
|
||||
In order to run the backend, you need to have a frontend build inside `frontend/build/`.
|
||||
@@ -70,11 +72,14 @@ npm run build
|
||||
```
|
||||
|
||||
**Known issue while running `npm run build`**
|
||||
|
||||
- You may encounter `FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory` error.
|
||||
- To solve this run:
|
||||
|
||||
```bash
|
||||
export NODE_OPTIONS=--max_old_space_size=8096
|
||||
```
|
||||
|
||||
- run `npm run build` again
|
||||
|
||||
In the root folder:
|
||||
@@ -86,7 +91,7 @@ docker-compose up db
|
||||
In the backend folder:
|
||||
|
||||
```bash
|
||||
DATABASE_URL=postgres://postgres:changeme@127.0.0.1:5433/windmill?sslmode=disable cargo run
|
||||
DATABASE_URL=postgres://postgres:changeme@127.0.0.1:5432/windmill?sslmode=disable cargo run
|
||||
```
|
||||
|
||||
You can now access [http://127.0.0.1:8000](http://127.0.0.1:8000).
|
||||
@@ -146,13 +151,13 @@ Recommended config for VS Code:
|
||||
|
||||
- turn _format on save_ on
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
The project is built with [SvelteKit](https://kit.svelte.dev/) and uses as output static files.
|
||||
There are others adapters for sveltekit, but we use the static adapter.
|
||||
|
||||
To build the frontend as static assets, use:
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
@@ -160,10 +165,11 @@ npm run build
|
||||
The output is in the `build` folder.
|
||||
|
||||
The default build assume you serve every non static files as the 200.html file which is catchall. If you prefer a normal layout, you can use:
|
||||
|
||||
```
|
||||
NOTCATCHALL=true npm run build
|
||||
```
|
||||
|
||||
which will generate an index.html and allow you to serve the frontend with any static server.
|
||||
|
||||
Env variables used for build are set in .env file. See [https://vitejs.dev/guide/env-and-mode.html#env-files](https://vitejs.dev/guide/env-and-mode.html#env-files) for more details.
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
326
frontend/src/lib/components/SavedInputs.svelte
Normal file
326
frontend/src/lib/components/SavedInputs.svelte
Normal file
@@ -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'}"
|
||||
|
||||
@@ -190,7 +190,7 @@
|
||||
|
||||
$state = $state
|
||||
} catch (e) {
|
||||
sendUserToast('Error running frontend script: ' + e.message, true)
|
||||
sendUserToast(`Error running frontend script ${id}: ` + e.message, true)
|
||||
|
||||
// Manually add a fake job to the job list to show the error
|
||||
|
||||
@@ -206,8 +206,7 @@
|
||||
}
|
||||
loading = false
|
||||
return
|
||||
}
|
||||
if (noBackend) {
|
||||
} else if (noBackend) {
|
||||
if (!noToast) {
|
||||
sendUserToast('This app is not connected to a windmill backend, it is a static preview')
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
faClipboard,
|
||||
faExternalLink,
|
||||
faFileExport,
|
||||
faGlobe,
|
||||
faSave
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import {
|
||||
@@ -42,7 +41,7 @@
|
||||
import { getContext } from 'svelte'
|
||||
import { Icon } from 'svelte-awesome'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import { appToHubUrl, classNames, copyToClipboard, sendUserToast } from '../../../utils'
|
||||
import { classNames, copyToClipboard, sendUserToast } from '../../../utils'
|
||||
import type {
|
||||
AppInput,
|
||||
ConnectedAppInput,
|
||||
@@ -607,14 +606,14 @@
|
||||
appExport.open($app)
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: 'Publish to Hub',
|
||||
icon: faGlobe,
|
||||
action: () => {
|
||||
const url = appToHubUrl(toStatic($app, $staticExporter, $summary))
|
||||
window.open(url.toString(), '_blank')
|
||||
}
|
||||
},
|
||||
// {
|
||||
// displayName: 'Publish to Hub',
|
||||
// icon: faGlobe,
|
||||
// action: () => {
|
||||
// const url = appToHubUrl(toStatic($app, $staticExporter, $summary))
|
||||
// window.open(url.toString(), '_blank')
|
||||
// }
|
||||
// },
|
||||
{
|
||||
displayName: 'Hub compatible JSON',
|
||||
icon: faFileExport,
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
app,
|
||||
mode,
|
||||
connectingInput,
|
||||
runnableComponents,
|
||||
summary,
|
||||
focusedGrid,
|
||||
parentWidth,
|
||||
@@ -39,40 +38,6 @@
|
||||
.flatMap((id) => dfs($app.grid, id, $app.subgrids ?? {}))
|
||||
.filter((x) => x != undefined) as string[]
|
||||
}
|
||||
|
||||
function removeGridElement(component) {
|
||||
if (component) {
|
||||
$app.grid = $app.grid.filter((gridComponent) => {
|
||||
if (gridComponent.data.id === component.id) {
|
||||
if (
|
||||
gridComponent.data.componentInput?.type === 'runnable' &&
|
||||
gridComponent.data.componentInput?.runnable?.type === 'runnableByName' &&
|
||||
gridComponent.data.componentInput?.runnable.inlineScript
|
||||
) {
|
||||
const { name, inlineScript } = gridComponent.data.componentInput?.runnable
|
||||
|
||||
if (!$app.unusedInlineScripts) {
|
||||
$app.unusedInlineScripts = []
|
||||
}
|
||||
|
||||
$app.unusedInlineScripts.push({
|
||||
name,
|
||||
inlineScript
|
||||
})
|
||||
|
||||
$app = $app
|
||||
}
|
||||
}
|
||||
|
||||
return gridComponent.data.id !== component?.id
|
||||
})
|
||||
|
||||
delete $runnableComponents[component.id]
|
||||
$runnableComponents = $runnableComponents
|
||||
|
||||
$selectedComponent = undefined
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative w-full z-20 overflow-visible">
|
||||
@@ -135,7 +100,6 @@
|
||||
component={dataItem.data}
|
||||
selected={Boolean($selectedComponent?.includes(dataItem.id))}
|
||||
locked={isFixed(dataItem)}
|
||||
on:delete={() => removeGridElement(dataItem.data)}
|
||||
on:lock={() => {
|
||||
const gridItem = findGridItem($app, dataItem.id)
|
||||
if (gridItem) {
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
bind:inlineScript={hiddenInlineScript.script.inlineScript}
|
||||
/>
|
||||
{:else}
|
||||
<span class="text-gray-600 text-xs">No hiddenInlineScript.script defined</span>
|
||||
<span class="text-gray-600 text-xs">No script defined</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if Object.keys(hiddenInlineScript.script.fields).length > 0}
|
||||
|
||||
@@ -32,7 +32,10 @@ export function dfs(
|
||||
for (const item of grid) {
|
||||
if (item.id === id) {
|
||||
return [id]
|
||||
} else if (item.data.type == 'tablecomponent' && item.data.actionButtons.find((x) => x.id)) {
|
||||
} else if (
|
||||
item.data.type == 'tablecomponent' &&
|
||||
item.data.actionButtons.find((x) => id == x.id)
|
||||
) {
|
||||
return [item.id, id]
|
||||
} else {
|
||||
for (let i = 0; i < (item.data.numberOfSubgrids ?? 0); i++) {
|
||||
@@ -133,11 +136,14 @@ export function findGridItem(app: App, id: string): GridItem | undefined {
|
||||
}
|
||||
|
||||
export function getNextGridItemId(app: App): string {
|
||||
const subgridsKeys = allItems(app.grid, app.subgrids).map((x) => x.id)
|
||||
const withoutDash = subgridsKeys.map((element) => element.split('-')[0])
|
||||
const id = getNextId([...new Set(withoutDash)])
|
||||
|
||||
return id
|
||||
const allIds = allItems(app.grid, app.subgrids).flatMap((x) => {
|
||||
if (x.data.type === 'tablecomponent') {
|
||||
return [x.id, ...x.data.actionButtons.map((x) => x.id)]
|
||||
} else {
|
||||
return [x.id]
|
||||
}
|
||||
})
|
||||
return getNextId(allIds)
|
||||
}
|
||||
|
||||
export function getAllRecomputeIdsForComponent(app: App, id: string | undefined) {
|
||||
|
||||
@@ -79,7 +79,6 @@
|
||||
{component}
|
||||
{selected}
|
||||
shouldHideActions={$connectingInput.opened}
|
||||
on:delete
|
||||
on:lock
|
||||
on:expand
|
||||
{locked}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
export let first: boolean = false
|
||||
</script>
|
||||
|
||||
<OutputHeader selectable={false} {id} {name} color="blue" {first}>
|
||||
<OutputHeader renamable={false} selectable={true} {id} {name} color="blue" {first}>
|
||||
<ComponentOutputViewer
|
||||
componentId={id}
|
||||
on:select={({ detail }) => {
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
import { getContext } from 'svelte'
|
||||
import { allsubIds, findGridItem } from '../../appUtils'
|
||||
import IdEditor from './IdEditor.svelte'
|
||||
import type { AppComponent } from '../../component'
|
||||
import type { Runnable } from '$lib/components/apps/inputType'
|
||||
|
||||
export let id: string
|
||||
export let name: string
|
||||
@@ -13,6 +15,7 @@
|
||||
export let nested: boolean = false
|
||||
export let color: 'blue' | 'indigo' = 'indigo'
|
||||
export let selectable: boolean = true
|
||||
export let renamable: boolean = true
|
||||
|
||||
const { manuallyOpened, search, hasResult } = getContext<ContextPanelContext>('ContextPanel')
|
||||
|
||||
@@ -47,66 +50,102 @@
|
||||
}
|
||||
|
||||
function renameId(newId: string): void {
|
||||
{
|
||||
const item = findGridItem($app, id)
|
||||
if (item) {
|
||||
item.data.id = newId
|
||||
item.id = newId
|
||||
}
|
||||
const oldSubgrids = Object.keys($app.subgrids ?? {}).filter((subgrid) =>
|
||||
subgrid.startsWith(id + '-')
|
||||
)
|
||||
oldSubgrids.forEach((subgrid) => {
|
||||
if ($app.subgrids) {
|
||||
$app.subgrids[subgrid.replace(id, newId)] = $app.subgrids[subgrid]
|
||||
delete $app.subgrids[subgrid]
|
||||
}
|
||||
})
|
||||
allItems($app.grid, $app.subgrids).forEach((item) => {
|
||||
if (item.data.componentInput?.type == 'connected') {
|
||||
if (item.data.componentInput.connection?.componentId === id) {
|
||||
item.data.componentInput.connection.componentId = newId
|
||||
}
|
||||
} else if (item.data.componentInput?.type == 'runnable') {
|
||||
if (
|
||||
item.data.componentInput?.runnable?.type === 'runnableByName' &&
|
||||
item.data.componentInput?.runnable?.inlineScript?.refreshOn
|
||||
?.map((x) => x.id)
|
||||
?.includes(id)
|
||||
) {
|
||||
item.data.componentInput.runnable.inlineScript.refreshOn =
|
||||
item.data.componentInput.runnable.inlineScript.refreshOn.map((x) => {
|
||||
if (x.id === id) {
|
||||
return {
|
||||
id: newId,
|
||||
key: x.key
|
||||
}
|
||||
}
|
||||
return x
|
||||
})
|
||||
}
|
||||
}
|
||||
const item = findGridItem($app, id)
|
||||
|
||||
Object.values(item.data.configuration ?? {}).forEach((config) => {
|
||||
if (config.type === 'connected') {
|
||||
if (config.connection?.componentId === id) {
|
||||
config.connection.componentId = newId
|
||||
}
|
||||
} else if (config.type == 'oneOf') {
|
||||
Object.values(config.configuration ?? {}).forEach((choices) => {
|
||||
Object.values(choices).forEach((c) => {
|
||||
if (c.type === 'connected') {
|
||||
if (c.connection?.componentId === id) {
|
||||
c.connection.componentId = newId
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
if (!item) {
|
||||
return
|
||||
}
|
||||
item.data.id = newId
|
||||
item.id = newId
|
||||
|
||||
const oldSubgrids = Object.keys($app.subgrids ?? {}).filter((subgrid) =>
|
||||
subgrid.startsWith(id + '-')
|
||||
)
|
||||
|
||||
oldSubgrids.forEach((subgrid) => {
|
||||
if ($app.subgrids) {
|
||||
$app.subgrids[subgrid.replace(id, newId)] = $app.subgrids[subgrid]
|
||||
delete $app.subgrids[subgrid]
|
||||
}
|
||||
})
|
||||
|
||||
function propagateRename(from: string, to: string) {
|
||||
allItems($app.grid, $app.subgrids).forEach((item) => {
|
||||
renameComponent(from, to, item.data)
|
||||
})
|
||||
|
||||
$app.hiddenInlineScripts.forEach((x) => {
|
||||
console.log('process', x.name, id)
|
||||
processRunnable(from, to, {
|
||||
name: x.name,
|
||||
inlineScript: x.inlineScript,
|
||||
type: 'runnableByName'
|
||||
})
|
||||
})
|
||||
$app = $app
|
||||
$selectedComponent = [newId]
|
||||
}
|
||||
propagateRename(id, newId)
|
||||
if (item?.data.type == 'tablecomponent') {
|
||||
for (let c of item.data.actionButtons) {
|
||||
let old = c.id
|
||||
c.id = c.id.replace(id + '_', newId + '_')
|
||||
propagateRename(old, c.id)
|
||||
}
|
||||
}
|
||||
|
||||
$app = $app
|
||||
$selectedComponent = [newId]
|
||||
}
|
||||
|
||||
function renameComponent(from: string, to: string, data: AppComponent) {
|
||||
if (data.type == 'tablecomponent') {
|
||||
for (let c of data.actionButtons) {
|
||||
renameComponent(from, to, c)
|
||||
}
|
||||
}
|
||||
let componentInput = data.componentInput
|
||||
if (componentInput?.type == 'connected') {
|
||||
if (componentInput.connection?.componentId === from) {
|
||||
componentInput.connection.componentId = to
|
||||
}
|
||||
} else if (componentInput?.type == 'runnable') {
|
||||
processRunnable(from, to, componentInput.runnable)
|
||||
}
|
||||
|
||||
Object.values(data.configuration ?? {}).forEach((config) => {
|
||||
if (config.type === 'connected') {
|
||||
if (config.connection?.componentId === from) {
|
||||
config.connection.componentId = to
|
||||
}
|
||||
} else if (config.type == 'oneOf') {
|
||||
Object.values(config.configuration ?? {}).forEach((choices) => {
|
||||
Object.values(choices).forEach((c) => {
|
||||
if (c.type === 'connected') {
|
||||
if (c.connection?.componentId === id) {
|
||||
c.connection.componentId = to
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function processRunnable(from: string, to: string, runnable: Runnable) {
|
||||
if (
|
||||
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,
|
||||
key: x.key
|
||||
}
|
||||
}
|
||||
return x
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -161,7 +200,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{#if selectable && ($selectedComponent?.includes(id) || $hoverStore === id)}
|
||||
{#if selectable && renamable && ($selectedComponent?.includes(id) || $hoverStore === id)}
|
||||
<IdEditor
|
||||
{id}
|
||||
on:selected={() => ($selectedComponent = [id])}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
export let first: boolean = false
|
||||
</script>
|
||||
|
||||
<OutputHeader {id} name={'Table action'} {first}>
|
||||
<OutputHeader renamable={false} {id} name={'Table action'} {first}>
|
||||
<ComponentOutputViewer
|
||||
componentId={id}
|
||||
on:select={({ detail }) => {
|
||||
|
||||
@@ -150,7 +150,7 @@
|
||||
startIcon={{ icon: faCodeBranch }}
|
||||
btnClasses="truncate"
|
||||
>
|
||||
fork a detached script
|
||||
Copy a script
|
||||
</Button>
|
||||
<Button
|
||||
on:click={() => dispatch('delete')}
|
||||
|
||||
@@ -14,8 +14,14 @@
|
||||
|
||||
function deleteBackgroundScript(index: number) {
|
||||
// remove the script from the array at the index
|
||||
$app.hiddenInlineScripts.splice(index, 1)
|
||||
$app.hiddenInlineScripts = [...$app.hiddenInlineScripts]
|
||||
if ($app.hiddenInlineScripts.length - 1 == index) {
|
||||
$app.hiddenInlineScripts.splice(index, 1)
|
||||
$app.hiddenInlineScripts = [...$app.hiddenInlineScripts]
|
||||
} else {
|
||||
$app.hiddenInlineScripts[index].inlineScript = undefined
|
||||
$app.hiddenInlineScripts[index].name = `Background Script ${index}`
|
||||
$app.hiddenInlineScripts = $app.hiddenInlineScripts
|
||||
}
|
||||
|
||||
delete $runnableComponents[`bg_${index}`]
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
push(history, $app)
|
||||
|
||||
if (componentSettings?.item.id) {
|
||||
delete $worldStore.outputsById[componentSettings?.item.id]
|
||||
$errorByComponent = clearErrorByComponentId(componentSettings?.item.id, $errorByComponent)
|
||||
$jobs = clearJobsByComponentId(componentSettings?.item.id, $jobs)
|
||||
|
||||
@@ -235,7 +236,6 @@
|
||||
<TableActions id={component.id} bind:components={componentSettings.item.data.actionButtons} />
|
||||
{/if}
|
||||
|
||||
<AlignmentEditor bind:component={componentSettings.item.data} />
|
||||
{#if componentSettings.item.data.type === 'buttoncomponent' || componentSettings.item.data.type === 'formcomponent' || componentSettings.item.data.type === 'formbuttoncomponent'}
|
||||
<Recompute
|
||||
bind:recomputeIds={componentSettings.item.data.recomputeIds}
|
||||
@@ -244,6 +244,7 @@
|
||||
{/if}
|
||||
|
||||
<div class="grow shrink" />
|
||||
<AlignmentEditor bind:component={componentSettings.item.data} />
|
||||
|
||||
{#if Object.keys(ccomponents[component.type].customCss ?? {}).length > 0}
|
||||
<PanelSection title="Styling">
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
export let recomputeOnInputChanged: boolean = false
|
||||
|
||||
const colors = {
|
||||
red: 'text-red-800 border-red-600 bg-red-100',
|
||||
green: 'text-green-800 border-green-600 bg-green-100',
|
||||
indigo: 'text-indigo-800 border-indigo-600 bg-indigo-100',
|
||||
blue: 'text-blue-800 border-blue-600 bg-blue-100'
|
||||
@@ -157,13 +156,13 @@
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 flex-wrap">
|
||||
<div class="flex flex-row gap-2 flex-wrap mt-2">
|
||||
{#each frontendDependencies as label, index}
|
||||
<span class={classNames(badgeClass, colors['red'])}>
|
||||
<span class={classNames(badgeClass, colors['blue'])}>
|
||||
{label}
|
||||
<button
|
||||
on:click={() => deleteDep(index)}
|
||||
class="bg-red-300 cursor-pointer hover:bg-red-400 ml-1 rounded-md"
|
||||
class="bg-blue-300 cursor-pointer hover:bg-blue-400 ml-1 rounded-md"
|
||||
>
|
||||
<X size={18} class="p-0.5" />
|
||||
</button>
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
displayDaysAgo,
|
||||
emptyString,
|
||||
encodeState,
|
||||
flowToHubUrl,
|
||||
sendUserToast
|
||||
} from '$lib/utils'
|
||||
import {
|
||||
@@ -19,7 +18,6 @@
|
||||
faClipboard,
|
||||
faCodeFork,
|
||||
faEdit,
|
||||
faGlobe,
|
||||
faList,
|
||||
faPlay,
|
||||
faShare,
|
||||
@@ -249,7 +247,7 @@
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-2 flex-wrap mt-2">
|
||||
<Button
|
||||
<!-- <Button
|
||||
target="_blank"
|
||||
href={flowToHubUrl(flow).toString()}
|
||||
variant="border"
|
||||
@@ -258,7 +256,7 @@
|
||||
startIcon={{ icon: faGlobe }}
|
||||
>
|
||||
Publish to Hub
|
||||
</Button>
|
||||
</Button> -->
|
||||
<Button
|
||||
on:click={() => shareModal.openDrawer(flow?.path ?? '', 'flow')}
|
||||
variant="border"
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user