feat: introduce backend persisted draft systems for scripts
This commit is contained in:
3
backend/migrations/20230429082953_add_drafts.down.sql
Normal file
3
backend/migrations/20230429082953_add_drafts.down.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
-- Add down migration script here
|
||||
DROP TABLE draft;
|
||||
DROP TYPE DRAFT_TYPE;
|
||||
18
backend/migrations/20230429082953_add_drafts.up.sql
Normal file
18
backend/migrations/20230429082953_add_drafts.up.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
-- Add up migration script here
|
||||
CREATE TYPE DRAFT_TYPE AS ENUM ('script', 'flow', 'app');
|
||||
|
||||
CREATE TABLE draft (
|
||||
workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id),
|
||||
path VARCHAR(255) NOT NULL,
|
||||
typ DRAFT_TYPE NOT NULL,
|
||||
value JSONB NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (workspace_id, path, typ)
|
||||
);
|
||||
|
||||
GRANT ALL ON draft TO windmill_admin;
|
||||
GRANT ALL ON draft TO windmill_user;
|
||||
|
||||
ALTER TABLE script ADD COLUMN draft_only BOOLEAN;
|
||||
ALTER TABLE flow ADD COLUMN draft_only BOOLEAN;
|
||||
ALTER TABLE app ADD COLUMN draft_only BOOLEAN;
|
||||
@@ -692,6 +692,32 @@
|
||||
},
|
||||
"query": "SELECT * FROM workspace_settings WHERE workspace_id = $1"
|
||||
},
|
||||
"17c2a1abcff52cfb39ae55cfafb6b5945bb87eebfdb2e030e7de3db938d0c6c9": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
{
|
||||
"Custom": {
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"script",
|
||||
"flow",
|
||||
"app"
|
||||
]
|
||||
},
|
||||
"name": "draft_type"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO draft\n (workspace_id, path, value, typ)\n VALUES ($1, $2, $3, $4)\n ON CONFLICT (workspace_id, path, typ) DO UPDATE SET value = $3"
|
||||
},
|
||||
"187a27d18f78f068319a0ec684f71a76c49dd09c5c74c2c777e35233c4f5a281": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -1459,6 +1485,19 @@
|
||||
},
|
||||
"query": "DELETE FROM pip_resolution_cache WHERE expiration <= now() RETURNING hash"
|
||||
},
|
||||
"39a72ff9bd2ab9bdf59a73ea32821645ce8b1bbcaccee13fb4f2c0eed28a6096": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'app'"
|
||||
},
|
||||
"39bae7dff750565183ac3893b51a8846d7db2f130a6795b54ef94acc232dec8b": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -1504,6 +1543,19 @@
|
||||
},
|
||||
"query": "DELETE FROM folder WHERE workspace_id = $1"
|
||||
},
|
||||
"3d04f6c1a77c9a0bcc999913d6df253b58b82c09fe23a6ab99b5af78d4604732": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'script'"
|
||||
},
|
||||
"3d363466d79075df3f74f946eff43ca89faefca3bcdf2c533425ca3868b0369a": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -2169,6 +2221,19 @@
|
||||
},
|
||||
"query": "INSERT INTO token\n (token, email, label, expiration, super_admin)\n VALUES ($1, $2, $3, $4, $5)"
|
||||
},
|
||||
"567ded2a717af9370a80c00bdb50d965fba9a3422c58e67ed2ed06dd107ae139": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "DELETE FROM script WHERE hash = $1 AND workspace_id = $2"
|
||||
},
|
||||
"56839d3aec6c0177d14589aedda8d5c431d841b6d5d0d99ce3836bb42d4d83d9": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -2356,57 +2421,6 @@
|
||||
},
|
||||
"query": "SELECT language as \"language: ScriptLang\" FROM script WHERE hash = $1 AND workspace_id = $2"
|
||||
},
|
||||
"63ab3bad7ab712f8050648851d8c69a1e8071060c2db57a7d2aa42e0361e2c18": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Int8",
|
||||
"Varchar",
|
||||
"Int8Array",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"python3",
|
||||
"deno",
|
||||
"go",
|
||||
"bash"
|
||||
]
|
||||
},
|
||||
"name": "script_lang"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Custom": {
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"script",
|
||||
"trigger",
|
||||
"failure",
|
||||
"command",
|
||||
"approval"
|
||||
]
|
||||
},
|
||||
"name": "script_kind"
|
||||
}
|
||||
},
|
||||
"Varchar"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15)"
|
||||
},
|
||||
"64920b845c0ce81fb99497c03b249bb6cb06581079b5fc5bea5ddd8e7a895b79": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -3220,6 +3234,19 @@
|
||||
},
|
||||
"query": "SELECT workspace_id, name, display_name, owners, extra_perms FROM folder WHERE workspace_id = $1 ORDER BY name desc LIMIT $2 OFFSET $3"
|
||||
},
|
||||
"855427180b549115923e57cdde6fa2780069b049948bae4140e866c570977a90": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'flow'"
|
||||
},
|
||||
"866b26e56fd376368c759db6eee13f92373e308f584903121b4546ad51aef86e": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -3677,6 +3704,58 @@
|
||||
},
|
||||
"query": "SELECT email, login_type::TEXT, super_admin, verified, name, company FROM password WHERE email = $1"
|
||||
},
|
||||
"90c5f1eea5d2cceb157ce0667a625f0fa4506961e2ad181765022a20b11d314d": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Int8",
|
||||
"Varchar",
|
||||
"Int8Array",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Bool",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"python3",
|
||||
"deno",
|
||||
"go",
|
||||
"bash"
|
||||
]
|
||||
},
|
||||
"name": "script_lang"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Custom": {
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"script",
|
||||
"trigger",
|
||||
"failure",
|
||||
"command",
|
||||
"approval"
|
||||
]
|
||||
},
|
||||
"name": "script_kind"
|
||||
}
|
||||
},
|
||||
"Varchar",
|
||||
"Bool"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16)"
|
||||
},
|
||||
"911b1e1f2a5ba6d5159916e5598020e680c45043b0736ad0153ee261a151dd90": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -3959,6 +4038,27 @@
|
||||
},
|
||||
"query": "UPDATE schedule SET enabled = $1, email = $2 WHERE path = $3 AND workspace_id = $4 RETURNING *"
|
||||
},
|
||||
"97966407e9f1fa80fd227f75686cc9ecbb767c69ee294638c1c0957f29fd440f": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "draft_only",
|
||||
"ordinal": 0,
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
true
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT draft_only FROM script WHERE path = $1 AND workspace_id = $2"
|
||||
},
|
||||
"97e364c703bdcdfb5345810659cbe0477a28b8199ef0b297f9a22c88a43b6b5c": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
|
||||
@@ -2125,6 +2125,41 @@ paths:
|
||||
items:
|
||||
type: string
|
||||
|
||||
|
||||
/w/{workspace}/drafts/create:
|
||||
post:
|
||||
summary: create draft
|
||||
operationId: createDraft
|
||||
tags:
|
||||
- draft
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
typ:
|
||||
type: string
|
||||
enum: ["flow", "script", "app"]
|
||||
value: {}
|
||||
required:
|
||||
- path
|
||||
- typ
|
||||
- enum
|
||||
responses:
|
||||
"201":
|
||||
description: draft created
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
|
||||
/w/{workspace}/scripts/create:
|
||||
post:
|
||||
summary: create script
|
||||
@@ -2139,40 +2174,8 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
parent_hash:
|
||||
type: string
|
||||
summary:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
content:
|
||||
type: string
|
||||
schema:
|
||||
type: object
|
||||
is_template:
|
||||
type: boolean
|
||||
lock:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
language:
|
||||
type: string
|
||||
enum: [python3, deno, go, bash]
|
||||
kind:
|
||||
type: string
|
||||
enum: [script, failure, trigger, command, approval]
|
||||
tag:
|
||||
type: string
|
||||
required:
|
||||
- path
|
||||
- summary
|
||||
- description
|
||||
- content
|
||||
- language
|
||||
$ref: "#/components/schemas/NewScript"
|
||||
|
||||
responses:
|
||||
"201":
|
||||
description: script created
|
||||
@@ -2366,6 +2369,34 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Script"
|
||||
|
||||
/w/{workspace}/scripts/get/draft/{path}:
|
||||
get:
|
||||
summary: get script by path with draft
|
||||
operationId: getScriptByPathWithDraft
|
||||
tags:
|
||||
- script
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/ScriptPath"
|
||||
responses:
|
||||
"200":
|
||||
description: script details
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/NewScript"
|
||||
- type: object
|
||||
properties:
|
||||
draft:
|
||||
$ref: "#/components/schemas/NewScript"
|
||||
hash:
|
||||
type: string
|
||||
required:
|
||||
- hash
|
||||
|
||||
|
||||
|
||||
/w/{workspace}/scripts/raw/p/{path}:
|
||||
get:
|
||||
summary: raw script by path
|
||||
@@ -5026,6 +5057,10 @@ components:
|
||||
type: boolean
|
||||
tag:
|
||||
type: string
|
||||
has_draft:
|
||||
type: boolean
|
||||
draft_only:
|
||||
type: boolean
|
||||
required:
|
||||
- hash
|
||||
- path
|
||||
@@ -5042,6 +5077,43 @@ components:
|
||||
- kind
|
||||
- starred
|
||||
|
||||
NewScript:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
parent_hash:
|
||||
type: string
|
||||
summary:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
content:
|
||||
type: string
|
||||
schema:
|
||||
type: object
|
||||
is_template:
|
||||
type: boolean
|
||||
lock:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
language:
|
||||
type: string
|
||||
enum: [python3, deno, go, bash]
|
||||
kind:
|
||||
type: string
|
||||
enum: [script, failure, trigger, command, approval]
|
||||
tag:
|
||||
type: string
|
||||
draft_only:
|
||||
type: boolean
|
||||
required:
|
||||
- path
|
||||
- summary
|
||||
- description
|
||||
- content
|
||||
- language
|
||||
ScriptArgs:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
|
||||
@@ -517,6 +517,15 @@ async fn update_app(
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'app'",
|
||||
path,
|
||||
&w_id
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut tx,
|
||||
&authed.username,
|
||||
|
||||
94
backend/windmill-api/src/drafts.rs
Normal file
94
backend/windmill-api/src/drafts.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Author: Ruben Fiszel
|
||||
* Copyright: Windmill Labs, Inc 2042
|
||||
* 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, DB},
|
||||
users::{maybe_refresh_folders, require_owner_of_path, Authed},
|
||||
};
|
||||
|
||||
use axum::{
|
||||
extract::{Extension, Path},
|
||||
routing::post,
|
||||
Json, Router,
|
||||
};
|
||||
use hyper::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use windmill_common::error::Result;
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
Router::new().route("/create", post(create_draft))
|
||||
}
|
||||
|
||||
#[derive(sqlx::Type, Serialize, Deserialize, Debug, PartialEq, Clone)]
|
||||
#[sqlx(type_name = "DRAFT_TYPE", rename_all = "lowercase")]
|
||||
#[serde(rename_all(serialize = "lowercase", deserialize = "lowercase"))]
|
||||
pub enum DraftType {
|
||||
Script,
|
||||
Flow,
|
||||
App,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct Draft {
|
||||
pub path: String,
|
||||
pub value: serde_json::Value,
|
||||
pub typ: DraftType,
|
||||
}
|
||||
|
||||
async fn create_draft(
|
||||
authed: Authed,
|
||||
Extension(db): Extension<DB>,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path(w_id): Path<String>,
|
||||
Json(draft): Json<Draft>,
|
||||
) -> Result<(StatusCode, String)> {
|
||||
let authed = maybe_refresh_folders(&draft.path, &w_id, authed, &db).await;
|
||||
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
require_owner_of_path(&authed, &draft.path)?;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO draft
|
||||
(workspace_id, path, value, typ)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (workspace_id, path, typ) DO UPDATE SET value = $3",
|
||||
&w_id,
|
||||
draft.path,
|
||||
draft.value,
|
||||
draft.typ: DraftType,
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok((StatusCode::CREATED, format!("draft {} created", draft.path)))
|
||||
}
|
||||
|
||||
// async fn get_draft(
|
||||
// authed: Authed,
|
||||
// Extension(user_db): Extension<UserDB>,
|
||||
// Path((w_id, path)): Path<(String, StripPath)>,
|
||||
// ) -> JsonResult<Draft> {
|
||||
// let path = path.to_path();
|
||||
// let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
// let script_o = sqlx::query_as!(
|
||||
// Draft,
|
||||
// r#"SELECT path, value, typ as "typ: DraftType" FROM draft WHERE path = $1 AND workspace_id = $2"#,
|
||||
// path,
|
||||
// w_id
|
||||
// )
|
||||
// .fetch_optional(&mut tx)
|
||||
// .await?;
|
||||
// tx.commit().await?;
|
||||
|
||||
// let draft = not_found_if_none(script_o, "draft", path)?;
|
||||
// Ok(Json(draft))
|
||||
// }
|
||||
@@ -354,14 +354,21 @@ async fn update_flow(
|
||||
}
|
||||
|
||||
for schedule in schedulables.into_iter() {
|
||||
// TODO: Why is this in the loop in the first place? Seems like it's just doing nothing after the first iteration? Should this use schedule.path?
|
||||
clear_schedule(tx.transaction_mut(), flow_path, true).await?;
|
||||
clear_schedule(tx.transaction_mut(), &schedule.path, true).await?;
|
||||
|
||||
if schedule.enabled {
|
||||
tx = push_scheduled_job(tx, schedule).await?;
|
||||
}
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'flow'",
|
||||
nf.path,
|
||||
&w_id
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut tx,
|
||||
&authed.username,
|
||||
|
||||
@@ -33,6 +33,7 @@ mod apps;
|
||||
mod audit;
|
||||
mod capture;
|
||||
mod db;
|
||||
mod drafts;
|
||||
mod favorite;
|
||||
mod flows;
|
||||
mod folders;
|
||||
@@ -134,6 +135,7 @@ pub async fn run_server(
|
||||
.nest("/resources", resources::workspaced_service())
|
||||
.nest("/schedules", schedule::workspaced_service())
|
||||
.nest("/scripts", scripts::workspaced_service())
|
||||
.nest("/drafts", drafts::workspaced_service())
|
||||
.nest(
|
||||
"/users",
|
||||
users::workspaced_service().layer(Extension(argon2.clone())),
|
||||
|
||||
@@ -35,7 +35,7 @@ use windmill_common::{
|
||||
jobs::JobPayload,
|
||||
schedule::Schedule,
|
||||
scripts::{
|
||||
to_i64, HubScript, ListScriptQuery, ListableScript, NewScript, Script, ScriptHash,
|
||||
to_i64, HubScript, ListScriptQuery, ListableScript, NewScript, Schema, Script, ScriptHash,
|
||||
ScriptKind, ScriptLang,
|
||||
},
|
||||
users::username_to_permissioned_as,
|
||||
@@ -55,6 +55,21 @@ lazy_static::lazy_static! {
|
||||
|
||||
const MAX_HASH_HISTORY_LENGTH_STORED: usize = 20;
|
||||
|
||||
#[derive(Serialize, sqlx::FromRow)]
|
||||
pub struct ScriptWDraft {
|
||||
pub hash: ScriptHash,
|
||||
pub path: String,
|
||||
pub summary: String,
|
||||
pub description: String,
|
||||
pub content: String,
|
||||
pub language: ScriptLang,
|
||||
pub kind: ScriptKind,
|
||||
pub tag: Option<String>,
|
||||
pub draft: Option<serde_json::Value>,
|
||||
pub schema: Option<Schema>,
|
||||
pub draft_only: Option<bool>,
|
||||
}
|
||||
|
||||
pub fn global_service() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
@@ -81,6 +96,7 @@ pub fn workspaced_service() -> Router {
|
||||
.route("/list", get(list_scripts))
|
||||
.route("/create", post(create_script))
|
||||
.route("/archive/p/*path", post(archive_script_by_path))
|
||||
.route("/get/draft/*path", get(get_script_by_path_w_draft))
|
||||
.route("/get/p/*path", get(get_script_by_path))
|
||||
.route("/raw/p/*path", get(raw_script_by_path))
|
||||
.route("/exists/p/*path", get(exists_script_by_path))
|
||||
@@ -104,23 +120,18 @@ async fn list_scripts(
|
||||
|
||||
let mut sqlb = SqlBuilder::select_from("script as o")
|
||||
.fields(&[
|
||||
"o.workspace_id",
|
||||
"hash",
|
||||
"o.path",
|
||||
"array_remove(array[parent_hashes[1]], NULL) as parent_hashes",
|
||||
"summary",
|
||||
"description",
|
||||
"created_by",
|
||||
"created_at",
|
||||
"COALESCE(draft.created_at, o.created_at) as created_at",
|
||||
"archived",
|
||||
"deleted",
|
||||
"is_template",
|
||||
"extra_perms",
|
||||
"CASE WHEN lock_error_logs IS NOT NULL THEN 'error' ELSE null END as lock_error_logs",
|
||||
"CASE WHEN lock_error_logs IS NOT NULL THEN true ELSE false END as has_deploy_errors",
|
||||
"language",
|
||||
"kind",
|
||||
"favorite.path IS NOT NULL as starred",
|
||||
"tag"
|
||||
"tag",
|
||||
"draft.path IS NOT NULL as has_draft",
|
||||
"draft_only"
|
||||
])
|
||||
.left()
|
||||
.join("favorite")
|
||||
@@ -128,6 +139,11 @@ async fn list_scripts(
|
||||
"favorite.favorite_kind = 'script' AND favorite.workspace_id = o.workspace_id AND favorite.path = o.path AND favorite.usr = ?"
|
||||
.bind(&authed.username),
|
||||
)
|
||||
.left()
|
||||
.join("draft")
|
||||
.on(
|
||||
"draft.path = o.path AND draft.workspace_id = o.workspace_id AND draft.typ = 'script'"
|
||||
)
|
||||
.order_desc("favorite.path IS NOT NULL")
|
||||
.order_by("created_at", lq.order_desc.unwrap_or(true))
|
||||
.and_where("o.workspace_id = ?".bind(&w_id))
|
||||
@@ -243,10 +259,20 @@ async fn create_script(
|
||||
}
|
||||
let parent_hashes_and_perms: Option<ParentInfo> = match (&ns.parent_hash, clashing_script) {
|
||||
(None, None) => Ok(None),
|
||||
(None, Some(s)) => Err(Error::BadRequest(format!(
|
||||
(None, Some(s)) if !s.draft_only.unwrap_or(false) => Err(Error::BadRequest(format!(
|
||||
"Path conflict for {} with non-archived hash {}",
|
||||
&ns.path, &s.hash
|
||||
))),
|
||||
(None, Some(s)) => {
|
||||
sqlx::query!(
|
||||
"DELETE FROM script WHERE hash = $1 AND workspace_id = $2",
|
||||
s.hash.0,
|
||||
&w_id
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
Ok(None)
|
||||
}
|
||||
(Some(p_hash), o) => {
|
||||
if sqlx::query_scalar!(
|
||||
"SELECT 1 FROM script WHERE hash = $1 AND workspace_id = $2",
|
||||
@@ -338,8 +364,8 @@ async fn create_script(
|
||||
//::text::json is to ensure we use serde_json with preserve order
|
||||
sqlx::query!(
|
||||
"INSERT INTO script (workspace_id, hash, path, parent_hashes, summary, description, \
|
||||
content, created_by, schema, is_template, extra_perms, lock, language, kind, tag) \
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15)",
|
||||
content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only) \
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::text::json, $10, $11, $12, $13, $14, $15, $16)",
|
||||
&w_id,
|
||||
&hash.0,
|
||||
ns.path,
|
||||
@@ -355,6 +381,15 @@ async fn create_script(
|
||||
ns.language: ScriptLang,
|
||||
ns.kind.unwrap_or(ScriptKind::Script): ScriptKind,
|
||||
ns.tag,
|
||||
ns.draft_only
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM draft WHERE path = $1 AND workspace_id = $2 AND typ = 'script'",
|
||||
ns.path,
|
||||
&w_id
|
||||
)
|
||||
.execute(&mut tx)
|
||||
.await?;
|
||||
@@ -495,6 +530,31 @@ async fn get_script_by_path(
|
||||
Ok(Json(script))
|
||||
}
|
||||
|
||||
async fn get_script_by_path_w_draft(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> JsonResult<ScriptWDraft> {
|
||||
let path = path.to_path();
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let script_o = sqlx::query_as::<_, ScriptWDraft>(
|
||||
"SELECT hash, script.path, summary, description, content, language, kind, tag, schema, draft_only, draft.value as draft FROM script LEFT JOIN draft ON
|
||||
script.path = draft.path AND script.workspace_id = draft.workspace_id AND draft.typ = 'script'
|
||||
WHERE script.path = $1 AND script.workspace_id = $2 \
|
||||
AND script.created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND \
|
||||
workspace_id = $2)",
|
||||
)
|
||||
.bind(path)
|
||||
.bind(w_id)
|
||||
.fetch_optional(&mut tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
let script = not_found_if_none(script_o, "Script", path)?;
|
||||
Ok(Json(script))
|
||||
}
|
||||
|
||||
async fn list_paths(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
@@ -759,7 +819,19 @@ async fn delete_script_by_path(
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
let path = path.to_path();
|
||||
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
let draft_only = sqlx::query_scalar!(
|
||||
"SELECT draft_only FROM script WHERE path = $1 AND workspace_id = $2",
|
||||
path,
|
||||
w_id
|
||||
)
|
||||
.fetch_one(&db)
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
|
||||
if !draft_only {
|
||||
require_admin(authed.is_admin, &authed.username)?;
|
||||
}
|
||||
|
||||
let script = sqlx::query_scalar!(
|
||||
"DELETE FROM script WHERE path = $1 AND workspace_id = $2 RETURNING path",
|
||||
path,
|
||||
|
||||
@@ -12,13 +12,6 @@ use crate::{
|
||||
users::{maybe_refresh_folders, require_owner_of_path, Authed},
|
||||
webhook_util::{WebhookMessage, WebhookShared},
|
||||
};
|
||||
/*
|
||||
* 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 axum::{
|
||||
extract::{Extension, Path, Query},
|
||||
|
||||
@@ -1165,6 +1165,8 @@ where
|
||||
"edited_at",
|
||||
"edited_by",
|
||||
"archived",
|
||||
"has_draft",
|
||||
"draft_only",
|
||||
] {
|
||||
if obj.contains_key(key) {
|
||||
obj.remove(key);
|
||||
|
||||
@@ -137,28 +137,26 @@ pub struct Script {
|
||||
pub language: ScriptLang,
|
||||
pub kind: ScriptKind,
|
||||
pub tag: Option<String>,
|
||||
pub draft_only: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
|
||||
pub struct ListableScript {
|
||||
pub workspace_id: String,
|
||||
pub hash: ScriptHash,
|
||||
pub path: String,
|
||||
pub parent_hashes: Option<ScriptHashes>,
|
||||
pub summary: String,
|
||||
pub description: String,
|
||||
pub created_by: String,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub archived: bool,
|
||||
pub deleted: bool,
|
||||
pub is_template: bool,
|
||||
pub extra_perms: serde_json::Value,
|
||||
pub lock_error_logs: Option<String>,
|
||||
pub language: ScriptLang,
|
||||
pub kind: ScriptKind,
|
||||
pub starred: bool,
|
||||
pub tag: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub has_draft: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub draft_only: Option<bool>,
|
||||
pub has_deploy_errors: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
@@ -189,6 +187,7 @@ pub struct NewScript {
|
||||
pub language: ScriptLang,
|
||||
pub kind: Option<ScriptKind>,
|
||||
pub tag: Option<String>,
|
||||
pub draft_only: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
||||
21
frontend/src/lib/components/DraftBadge.svelte
Normal file
21
frontend/src/lib/components/DraftBadge.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import Popover from './Popover.svelte'
|
||||
import { Badge } from './common'
|
||||
|
||||
export let has_draft: boolean = false
|
||||
export let draft_only: boolean = false
|
||||
</script>
|
||||
|
||||
{#if has_draft}
|
||||
{#if draft_only}
|
||||
<Popover notClickable>
|
||||
<svelte:fragment slot="text">Never deployed and is only a draft</svelte:fragment>
|
||||
<Badge small color="indigo" baseClass="border border-indigo-200">Draft only</Badge>
|
||||
</Popover>
|
||||
{:else}
|
||||
<Popover notClickable>
|
||||
<svelte:fragment slot="text">Is deployed and has a draft</svelte:fragment>
|
||||
<Badge color="indigo" baseClass="border border-indigo-200">+Draft</Badge>
|
||||
</Popover>
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -4,10 +4,9 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { ResourceService, VariableService } from '$lib/gen'
|
||||
import { getModifierKey, getScriptByPath, sendUserToast } from '$lib/utils'
|
||||
import { getScriptByPath, sendUserToast } from '$lib/utils'
|
||||
|
||||
import {
|
||||
faBroom,
|
||||
faCube,
|
||||
faDollarSign,
|
||||
faEye,
|
||||
@@ -30,7 +29,6 @@
|
||||
import ToggleHubWorkspace from './ToggleHubWorkspace.svelte'
|
||||
import Skeleton from './common/skeleton/Skeleton.svelte'
|
||||
import Popover from './Popover.svelte'
|
||||
import Kbd from './common/kbd/Kbd.svelte'
|
||||
import { SCRIPT_EDITOR_SHOW_EXPLORE_OTHER_SCRIPTS } from '$lib/consts'
|
||||
|
||||
export let lang: 'python3' | 'deno' | 'go' | 'bash'
|
||||
@@ -357,7 +355,7 @@
|
||||
</Button>
|
||||
<svelte:fragment slot="text">Reload assistant</svelte:fragment>
|
||||
</Popover>
|
||||
<Popover
|
||||
<!-- <Popover
|
||||
notClickable
|
||||
placement="bottom"
|
||||
disappearTimeout={0}
|
||||
@@ -380,7 +378,7 @@
|
||||
S
|
||||
</Kbd>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
</Popover> -->
|
||||
</div>
|
||||
</div>
|
||||
<Popover
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { setContext } from 'svelte'
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import CenteredPage from './CenteredPage.svelte'
|
||||
import { Badge, Button, UndoRedo } from './common'
|
||||
import { Badge, Button, Kbd, UndoRedo } from './common'
|
||||
import { dirtyStore } from './common/confirmationModal/dirtyStore'
|
||||
import FlowEditor from './flows/FlowEditor.svelte'
|
||||
import ScriptEditorDrawer from './flows/content/ScriptEditorDrawer.svelte'
|
||||
@@ -369,7 +369,7 @@
|
||||
on:click={() => saveFlow(false)}
|
||||
{dropdownItems}
|
||||
>
|
||||
Save
|
||||
Save <Kbd small>Ctrl</Kbd><Kbd small>S</Kbd>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
</Button>
|
||||
{:else}
|
||||
<Button btnClasses="w-full truncate" size="sm" on:click={() => runTest(stepArgs)}
|
||||
>Run <Kbd>{getModifierKey()}</Kbd>+<Kbd>Enter</Kbd></Button
|
||||
>Run <Kbd small>{getModifierKey()}</Kbd><Kbd small>Enter</Kbd></Button
|
||||
>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { Script, ScriptService, WorkerService } from '$lib/gen'
|
||||
import { DraftService, NewScript, Script, ScriptService, WorkerService } from '$lib/gen'
|
||||
import { goto } from '$app/navigation'
|
||||
import { page } from '$app/stores'
|
||||
import { inferArgs } from '$lib/infer'
|
||||
import { initialCode } from '$lib/script_helpers'
|
||||
import { userStore, workerTags, workspaceStore } from '$lib/stores'
|
||||
import { emptySchema, encodeState, sendUserToast, setQueryWithoutLoad } from '$lib/utils'
|
||||
import {
|
||||
emptySchema,
|
||||
encodeState,
|
||||
getModifierKey,
|
||||
sendUserToast,
|
||||
setQueryWithoutLoad
|
||||
} from '$lib/utils'
|
||||
import Path from './Path.svelte'
|
||||
import ScriptEditor from './ScriptEditor.svelte'
|
||||
import ScriptSchema from './ScriptSchema.svelte'
|
||||
import { dirtyStore } from './common/confirmationModal/dirtyStore'
|
||||
import { Badge, Button, Drawer } from './common'
|
||||
import { Badge, Button, Drawer, Kbd } from './common'
|
||||
import { faSave } from '@fortawesome/free-solid-svg-icons'
|
||||
import LanguageIcon from './common/languageIcons/LanguageIcon.svelte'
|
||||
import type { SupportedLanguage } from '$lib/common'
|
||||
@@ -25,8 +31,9 @@
|
||||
SCRIPT_SHOW_PSQL,
|
||||
SCRIPT_CUSTOMISE_SHOW_KIND
|
||||
} from '$lib/consts'
|
||||
import UnsavedConfirmationModal from './common/confirmationModal/UnsavedConfirmationModal.svelte'
|
||||
|
||||
export let script: Script
|
||||
export let script: NewScript
|
||||
export let initialPath: string = ''
|
||||
export let template: 'pgsql' | 'mysql' | 'script' = 'script'
|
||||
export let initialArgs: Record<string, any> = {}
|
||||
@@ -63,7 +70,12 @@
|
||||
if (SCRIPT_SHOW_BASH) {
|
||||
langs.push(['Bash', Script.language.BASH])
|
||||
}
|
||||
const scriptKindOptions: { value: Script.kind; title: string; desc?: string; documentationLink?: string; }[] = [
|
||||
const scriptKindOptions: {
|
||||
value: Script.kind
|
||||
title: string
|
||||
desc?: string
|
||||
documentationLink?: string
|
||||
}[] = [
|
||||
{
|
||||
value: Script.kind.SCRIPT,
|
||||
title: 'Action'
|
||||
@@ -72,25 +84,25 @@
|
||||
value: Script.kind.TRIGGER,
|
||||
title: 'Trigger',
|
||||
desc: 'First module of flows to trigger them based on external changes. These kind of scripts are usually running on a schedule to periodically look for changes.',
|
||||
documentationLink:"https://docs.windmill.dev/docs/flows/flow_trigger"
|
||||
|
||||
documentationLink: 'https://docs.windmill.dev/docs/flows/flow_trigger'
|
||||
},
|
||||
{
|
||||
value: Script.kind.APPROVAL,
|
||||
title: 'Approval',
|
||||
desc: 'Send notifications externally to ask for approval to continue a flow.',
|
||||
documentationLink:"https://docs.windmill.dev/docs/flows/flow_approval"
|
||||
documentationLink: 'https://docs.windmill.dev/docs/flows/flow_approval'
|
||||
},
|
||||
{
|
||||
value: Script.kind.FAILURE,
|
||||
title: 'Error Handler',
|
||||
desc: 'Handle errors in flows after all retry attempts have been exhausted.',
|
||||
documentationLink:"https://docs.windmill.dev/docs/flows/flow_error_handler"
|
||||
documentationLink: 'https://docs.windmill.dev/docs/flows/flow_error_handler'
|
||||
}
|
||||
]
|
||||
|
||||
let pathError = ''
|
||||
let loadingSave = false
|
||||
let loadingDraft = false
|
||||
|
||||
$: setQueryWithoutLoad($page.url, [{ key: 'state', value: encodeState(script) }])
|
||||
|
||||
@@ -100,25 +112,14 @@
|
||||
|
||||
function initContent(
|
||||
language: 'deno' | 'python3' | 'go' | 'bash',
|
||||
kind: Script.kind,
|
||||
kind: Script.kind | undefined,
|
||||
template: 'pgsql' | 'mysql' | 'script'
|
||||
) {
|
||||
script.content = initialCode(language, kind, template)
|
||||
scriptEditor?.inferSchema(script.content, language)
|
||||
}
|
||||
|
||||
async function leaveF(leave: boolean, newHash: string) {
|
||||
if (leave) {
|
||||
history.replaceState(history.state, '', `/scripts/edit/${newHash}`)
|
||||
goto(`/scripts/get/${newHash}?workspace=${$workspaceStore}`)
|
||||
} else {
|
||||
await goto(`/scripts/edit/${newHash}`)
|
||||
script.hash = newHash
|
||||
topHash = undefined
|
||||
}
|
||||
}
|
||||
|
||||
async function editScript(leave: boolean): Promise<void> {
|
||||
async function editScript(): Promise<void> {
|
||||
loadingSave = true
|
||||
try {
|
||||
$dirtyStore = false
|
||||
@@ -140,7 +141,7 @@
|
||||
summary: script.summary,
|
||||
description: script.description ?? '',
|
||||
content: script.content,
|
||||
parent_hash: script.hash != '' ? topHash ?? script.hash : undefined,
|
||||
parent_hash: topHash,
|
||||
schema: script.schema,
|
||||
is_template: script.is_template,
|
||||
language: script.language,
|
||||
@@ -148,40 +149,96 @@
|
||||
tag: script.tag
|
||||
}
|
||||
})
|
||||
leaveF(leave, newHash)
|
||||
goto(`/scripts/get/${newHash}?workspace=${$workspaceStore}`)
|
||||
} catch (error) {
|
||||
sendUserToast(`Error while saving the script: ${error.body || error.message}`, true)
|
||||
}
|
||||
loadingSave = false
|
||||
}
|
||||
|
||||
function computeDropdownItems() {
|
||||
let dropdownItems: { label: string; onClick: () => void }[] = []
|
||||
async function saveDraft(): Promise<void> {
|
||||
loadingDraft = true
|
||||
try {
|
||||
$dirtyStore = false
|
||||
localStorage.removeItem(script.path)
|
||||
|
||||
if ($dirtyStore) {
|
||||
dropdownItems.push({
|
||||
label: 'Save and see details',
|
||||
onClick: () => editScript(true)
|
||||
script.schema = script.schema ?? emptySchema()
|
||||
try {
|
||||
await inferArgs(script.language, script.content, script.schema)
|
||||
} catch (error) {
|
||||
sendUserToast(
|
||||
`The main signature was not parsable. This script is considered to be without main function`
|
||||
)
|
||||
}
|
||||
|
||||
if (initialPath == '') {
|
||||
await ScriptService.createScript({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
path: script.path,
|
||||
summary: script.summary,
|
||||
description: script.description ?? '',
|
||||
content: script.content,
|
||||
schema: script.schema,
|
||||
is_template: script.is_template,
|
||||
language: script.language,
|
||||
kind: script.kind,
|
||||
tag: script.tag,
|
||||
draft_only: true
|
||||
}
|
||||
})
|
||||
}
|
||||
await DraftService.createDraft({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: { path: script.path, typ: 'script', value: script }
|
||||
})
|
||||
if (initialPath == '') {
|
||||
goto(`/scripts/edit/${script.path}`)
|
||||
}
|
||||
sendUserToast('Saved as draft')
|
||||
} catch (error) {
|
||||
sendUserToast(
|
||||
`Error while saving the script as a draft: ${error.body || error.message}`,
|
||||
true
|
||||
)
|
||||
}
|
||||
loadingDraft = false
|
||||
}
|
||||
|
||||
dropdownItems.push({
|
||||
label: 'See details',
|
||||
onClick: () => leaveF(true, script.hash)
|
||||
})
|
||||
|
||||
if (initialPath != '') {
|
||||
dropdownItems.push({
|
||||
function computeDropdownItems() {
|
||||
let dropdownItems: { label: string; onClick: () => void }[] = [
|
||||
{
|
||||
label: 'Fork',
|
||||
onClick: () => {
|
||||
window.open(`/scripts/add?template=${initialPath}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Exit & See details',
|
||||
onClick: () => {
|
||||
goto(`/scripts/get/${initialPath}?workspace=${$workspaceStore}`)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
return dropdownItems
|
||||
}
|
||||
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
switch (event.key) {
|
||||
case 's':
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
saveDraft()
|
||||
event.preventDefault()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
|
||||
<UnsavedConfirmationModal />
|
||||
{#if !$userStore?.operator}
|
||||
<Drawer placement="right" bind:open={metadataOpen} size="800px">
|
||||
<DrawerContent title="Metadata" on:close={() => (metadataOpen = false)}>
|
||||
@@ -369,15 +426,25 @@
|
||||
>
|
||||
Customise
|
||||
</Button>
|
||||
<Button
|
||||
color="dark"
|
||||
loading={loadingDraft}
|
||||
size="sm"
|
||||
startIcon={{ icon: faSave }}
|
||||
on:click={() => saveDraft()}
|
||||
>
|
||||
Save as draft <Kbd>{getModifierKey()}</Kbd>
|
||||
<Kbd>S</Kbd>
|
||||
</Button>
|
||||
<Button
|
||||
color="dark"
|
||||
loading={loadingSave}
|
||||
size="sm"
|
||||
startIcon={{ icon: faSave }}
|
||||
on:click={() => editScript(false)}
|
||||
dropdownItems={computeDropdownItems}
|
||||
on:click={() => editScript()}
|
||||
dropdownItems={initialPath != '' ? computeDropdownItems : undefined}
|
||||
>
|
||||
Save
|
||||
Deploy
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -428,6 +495,9 @@
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
<ScriptEditor
|
||||
on:format={() => {
|
||||
saveDraft()
|
||||
}}
|
||||
bind:editor
|
||||
bind:this={scriptEditor}
|
||||
bind:schema={script.schema}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import { faGithub } from '@fortawesome/free-brands-svg-icons'
|
||||
import EditorBar, { EDITOR_BAR_WIDTH_THRESHOLD } from './EditorBar.svelte'
|
||||
import TestJobLoader from './TestJobLoader.svelte'
|
||||
import { onMount } from 'svelte'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import { Button, Kbd } from './common'
|
||||
import SplitPanesWrapper from './splitPanes/SplitPanesWrapper.svelte'
|
||||
import WindmillIcon from './icons/WindmillIcon.svelte'
|
||||
@@ -102,6 +102,8 @@
|
||||
loadPastTests()
|
||||
})
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function asKind(str: string | undefined) {
|
||||
return str as 'script' | 'approval' | 'trigger' | undefined
|
||||
}
|
||||
@@ -169,6 +171,7 @@
|
||||
console.error('Could not save last_save to local storage', e)
|
||||
}
|
||||
lastSave = code
|
||||
dispatch('format')
|
||||
}}
|
||||
class="flex flex-1 h-full !overflow-visible"
|
||||
lang={scriptLangToEditorLang(lang)}
|
||||
@@ -205,7 +208,7 @@
|
||||
{#if testIsLoading}
|
||||
Running
|
||||
{:else}
|
||||
Test <Kbd class="ml-4 text-5xs -my-0.5">Ctrl+Enter</Kbd>
|
||||
Test <Kbd small>Ctrl</Kbd><Kbd small>Enter</Kbd>
|
||||
{/if}
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
<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-2 min-h-[60px]">
|
||||
<div class="flex items-start">
|
||||
<div class="p-2 min-h-[60px] flex flex-col">
|
||||
<div class="flex items-start w-full">
|
||||
<div class="flex-shrink-0 mt-0.5">
|
||||
{#if error}
|
||||
<XCircleIcon class="h-4 w-4 text-red-400" />
|
||||
@@ -34,21 +34,8 @@
|
||||
<CheckCircle2 class="h-4 w-4 text-green-400" />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="ml-3 w-0 flex-1">
|
||||
<div class="ml-3 flex-1 w-0">
|
||||
<p class="text-sm text-gray-500">{message}</p>
|
||||
<div class="mt-2 flex flex-row gap-2 h-15">
|
||||
{#each actions as action, index (index)}
|
||||
<Button
|
||||
on:click={() => {
|
||||
action.callback()
|
||||
toast.pop(toastId)
|
||||
}}
|
||||
class="text-sm !text-black"
|
||||
>
|
||||
{action.label}
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4 flex flex-shrink-0">
|
||||
<button
|
||||
@@ -65,5 +52,18 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 flex flex-row gap-2 h-15">
|
||||
{#each actions as action, index (index)}
|
||||
<Button
|
||||
on:click={() => {
|
||||
action.callback()
|
||||
toast.pop(toastId)
|
||||
}}
|
||||
class="text-sm !text-black"
|
||||
>
|
||||
{action.label}
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -48,9 +48,6 @@
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col w-full space-y-4">
|
||||
<span>
|
||||
Are you sure you want to discard change you have made? (A draft has been temporarily and
|
||||
locally saved)
|
||||
</span>
|
||||
<span> Are you sure you want to discard the changes you have made? </span>
|
||||
</div>
|
||||
</ConfirmationModal>
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
<script lang="ts">
|
||||
export let kbdClass = ''
|
||||
export let small = false
|
||||
if (small) {
|
||||
kbdClass += ' text-5xs -my-0.5'
|
||||
}
|
||||
</script>
|
||||
|
||||
<span
|
||||
class="{$$props.class} rounded border bg-white/70 px-1.5 !text-xs text-gray-600 shadow-sm font-light transition-all group-hover:border-primary-500 group-hover:text-primary-500"
|
||||
class="{$$props.class} ml-0.5 rounded border bg-white/70 px-1.5 !text-xs text-gray-600 shadow-sm font-light transition-all group-hover:border-primary-500 group-hover:text-primary-500"
|
||||
>
|
||||
<kbd class={kbdClass}>
|
||||
<slot />
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="hover:bg-gray-50 w-full inline-flex items-center p-4 gap-4 first-of-type:!border-t-0
|
||||
class="hover:bg-gray-50 w-full inline-flex items-center p-4 gap-4 first-of-type:!border-t-0
|
||||
first-of-type:rounded-t-md last-of-type:rounded-b-md {color}"
|
||||
>
|
||||
<RowIcon {href} {kind} />
|
||||
|
||||
<a {href} class="min-w-0 grow hover:underline decoration-gray-400">
|
||||
<div class="text-gray-900 flex-wrap text-left text-md font-semibold mb-1 truncate ">
|
||||
<div class="text-gray-900 flex-wrap text-left text-md font-semibold mb-1 truncate">
|
||||
{#if marked}
|
||||
{@html marked}
|
||||
{:else}
|
||||
@@ -40,7 +40,7 @@ first-of-type:rounded-t-md last-of-type:rounded-b-md {color}"
|
||||
</div>
|
||||
</a>
|
||||
{#if $$slots.badges}
|
||||
<div class="hidden lg:flex flex-row gap-1 items-start">
|
||||
<div class="hidden lg:flex flex-row gap-1 items-center">
|
||||
<slot name="badges" />
|
||||
</div>
|
||||
{/if}
|
||||
@@ -60,5 +60,7 @@ first-of-type:rounded-t-md last-of-type:rounded-b-md {color}"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="w-9" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import { ScriptService, type Script } from '$lib/gen'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { capitalize, isOwner, sendUserToast } from '$lib/utils'
|
||||
import { isOwner, sendUserToast } from '$lib/utils'
|
||||
import {
|
||||
faArchive,
|
||||
faCalendarAlt,
|
||||
@@ -26,6 +26,7 @@
|
||||
import Button from '../button/Button.svelte'
|
||||
import LanguageBadge from './LanguageBadge.svelte'
|
||||
import Row from './Row.svelte'
|
||||
import DraftBadge from '$lib/components/DraftBadge.svelte'
|
||||
|
||||
export let script: Script & { canWrite: boolean }
|
||||
export let marked: string | undefined
|
||||
@@ -42,9 +43,9 @@
|
||||
extra_perms,
|
||||
canWrite,
|
||||
lock_error_logs,
|
||||
kind,
|
||||
workspace_id,
|
||||
archived
|
||||
archived,
|
||||
has_draft,
|
||||
draft_only
|
||||
} = script
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
@@ -88,17 +89,16 @@
|
||||
{path}
|
||||
{summary}
|
||||
{starred}
|
||||
workspaceId={workspace_id ?? $workspaceStore ?? ''}
|
||||
workspaceId={$workspaceStore ?? ''}
|
||||
on:change
|
||||
canFavorite={!draft_only}
|
||||
>
|
||||
<svelte:fragment slot="badges">
|
||||
<SharedBadge {canWrite} extraPerms={extra_perms} />
|
||||
<DraftBadge {has_draft} {draft_only} />
|
||||
{#if lock_error_logs}
|
||||
<Badge color="red" baseClass="border border-red-200">Deployment failed</Badge>
|
||||
{/if}
|
||||
{#if kind !== 'script'}
|
||||
<Badge color="gray" baseClass="border">{capitalize(kind)}</Badge>
|
||||
{/if}
|
||||
|
||||
{#if archived}
|
||||
<Badge color="red" baseClass="border">archived</Badge>
|
||||
@@ -117,7 +117,7 @@
|
||||
size="xs"
|
||||
variant="border"
|
||||
startIcon={{ icon: faEdit }}
|
||||
href="/scripts/edit/{hash}"
|
||||
href="/scripts/edit/{path}"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
@@ -137,30 +137,51 @@
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<Button
|
||||
href="/scripts/get/{hash}?workspace={$workspaceStore}"
|
||||
color="light"
|
||||
variant="border"
|
||||
size="xs"
|
||||
spacingSize="md"
|
||||
startIcon={{ icon: faEye }}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
<Button
|
||||
href="/scripts/run/{hash}"
|
||||
color="dark"
|
||||
size="xs"
|
||||
spacingSize="md"
|
||||
endIcon={{ icon: faPlay }}
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
{#if !draft_only}
|
||||
<Button
|
||||
href="/scripts/get/{hash}?workspace={$workspaceStore}"
|
||||
color="light"
|
||||
variant="border"
|
||||
size="xs"
|
||||
spacingSize="md"
|
||||
startIcon={{ icon: faEye }}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
<Button
|
||||
href="/scripts/run/{hash}"
|
||||
color="dark"
|
||||
size="xs"
|
||||
spacingSize="md"
|
||||
endIcon={{ icon: faPlay }}
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
{/if}
|
||||
</span>
|
||||
<Dropdown
|
||||
placement="bottom-end"
|
||||
dropdownItems={() => {
|
||||
let owner = isOwner(path, $userStore, $workspaceStore)
|
||||
if (draft_only) {
|
||||
return [
|
||||
{
|
||||
displayName: 'Delete',
|
||||
icon: faTrashAlt,
|
||||
action: (event) => {
|
||||
if (event?.shiftKey) {
|
||||
deleteScript(path)
|
||||
} else {
|
||||
deleteConfirmedCallback = () => {
|
||||
deleteScript(path)
|
||||
}
|
||||
}
|
||||
},
|
||||
type: dlt,
|
||||
disabled: !canWrite
|
||||
}
|
||||
]
|
||||
}
|
||||
return [
|
||||
{
|
||||
displayName: 'View script',
|
||||
@@ -171,13 +192,7 @@
|
||||
{
|
||||
displayName: 'Edit',
|
||||
icon: faEdit,
|
||||
href: `/scripts/edit/${hash}`,
|
||||
disabled: !canWrite || archived
|
||||
},
|
||||
{
|
||||
displayName: 'Edit code',
|
||||
icon: faEdit,
|
||||
href: `/scripts/edit/${hash}`,
|
||||
href: `/scripts/edit/${path}`,
|
||||
disabled: !canWrite || archived
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Button, Drawer, DrawerContent } from '$lib/components/common'
|
||||
import ScriptEditor from '$lib/components/ScriptEditor.svelte'
|
||||
import { ScriptService, type Preview } from '$lib/gen'
|
||||
import { ScriptService, type Preview, Script } from '$lib/gen'
|
||||
import { inferArgs } from '$lib/infer'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { emptySchema, sendUserToast } from '$lib/utils'
|
||||
@@ -62,7 +62,7 @@
|
||||
schema: script.schema,
|
||||
is_template: false,
|
||||
language: script.language,
|
||||
kind: script.kind
|
||||
kind: script.kind as Script.kind | undefined
|
||||
}
|
||||
})
|
||||
callback?.()
|
||||
|
||||
@@ -64,10 +64,7 @@
|
||||
|
||||
scripts = loadedScripts.map((script: Script) => {
|
||||
return {
|
||||
canWrite:
|
||||
canWrite(script.path, script.extra_perms, $userStore) &&
|
||||
script.workspace_id == $workspaceStore &&
|
||||
!$userStore?.operator,
|
||||
canWrite: canWrite(script.path, script.extra_perms, $userStore) && !$userStore?.operator,
|
||||
...script
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Script } from './gen'
|
||||
import { Script } from './gen'
|
||||
|
||||
import PYTHON_INIT_CODE from '$lib/init_scripts/python_init_code'
|
||||
import PYTHON_INIT_CODE_CLEAR from '$lib/init_scripts/python_init_code_clear'
|
||||
@@ -12,7 +12,7 @@ export {
|
||||
PYTHON_FAILURE_MODULE_CODE
|
||||
}
|
||||
|
||||
export const DENO_INIT_CODE = `// Ctrl/CMD+. to cache dependencies on imports hover, Ctrl/CMD+S to format.
|
||||
export const DENO_INIT_CODE = `// Ctrl/CMD+. to cache dependencies on imports hover.
|
||||
|
||||
// import { toWords } from "npm:number-to-words@1"
|
||||
// import * as wmill from "https://deno.land/x/windmill@v${__pkg__.version}/mod.ts"
|
||||
@@ -250,9 +250,12 @@ export function isInitialCode(content: string): boolean {
|
||||
|
||||
export function initialCode(
|
||||
language: 'deno' | 'python3' | 'go' | 'bash',
|
||||
kind: Script.kind,
|
||||
kind: Script.kind | undefined,
|
||||
subkind: 'pgsql' | 'mysql' | 'flow' | 'script' | 'fetch' | undefined
|
||||
): string {
|
||||
if (!kind) {
|
||||
kind = Script.kind.SCRIPT
|
||||
}
|
||||
if (language === 'deno') {
|
||||
if (kind === 'trigger') {
|
||||
return DENO_INIT_CODE_TRIGGER
|
||||
|
||||
@@ -740,7 +740,7 @@ export function isMac(): boolean {
|
||||
}
|
||||
|
||||
export function getModifierKey(): string {
|
||||
return isMac() ? '⌘' : 'CTRL'
|
||||
return isMac() ? '⌘' : 'Ctrl'
|
||||
}
|
||||
|
||||
export function isValidHexColor(color: string): boolean {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Script, ScriptService } from '$lib/gen'
|
||||
import { NewScript, Script, ScriptService } from '$lib/gen'
|
||||
|
||||
import { page } from '$app/stores'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
let scriptBuilder: ScriptBuilder | undefined = undefined
|
||||
|
||||
let script: Script =
|
||||
let script: NewScript =
|
||||
initialState != undefined
|
||||
? decodeState(initialState)
|
||||
: {
|
||||
@@ -28,10 +28,6 @@
|
||||
summary: '',
|
||||
content: '',
|
||||
schema: schema,
|
||||
created_by: '',
|
||||
created_at: '',
|
||||
archived: false,
|
||||
deleted: false,
|
||||
is_template: false,
|
||||
extra_perms: {},
|
||||
language: 'deno',
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { ScriptService, type Script } from '$lib/gen'
|
||||
|
||||
import { page } from '$app/stores'
|
||||
import { runFormStore, workspaceStore } from '$lib/stores'
|
||||
import ScriptBuilder from '$lib/components/ScriptBuilder.svelte'
|
||||
import { decodeState } from '$lib/utils'
|
||||
|
||||
const initialState = $page.url.searchParams.get('state')
|
||||
let initialArgs = {}
|
||||
if ($runFormStore) {
|
||||
initialArgs = $runFormStore
|
||||
$runFormStore = undefined
|
||||
}
|
||||
let topHash = $page.url.searchParams.get('topHash') ?? undefined
|
||||
|
||||
let scriptLoadedFromUrl = initialState != undefined ? decodeState(initialState) : undefined
|
||||
|
||||
let script: Script | undefined
|
||||
|
||||
let initialPath: string = ''
|
||||
|
||||
let scriptBuilder: ScriptBuilder | undefined = undefined
|
||||
|
||||
async function loadScript(): Promise<void> {
|
||||
script =
|
||||
scriptLoadedFromUrl != undefined && scriptLoadedFromUrl.hash == $page.params.hash
|
||||
? scriptLoadedFromUrl
|
||||
: await ScriptService.getScriptByHash({
|
||||
workspace: $workspaceStore!,
|
||||
hash: $page.params.hash
|
||||
})
|
||||
if (script) {
|
||||
initialPath = script.path
|
||||
scriptBuilder?.setCode(script.content)
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if ($workspaceStore) {
|
||||
loadScript()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if script}
|
||||
<ScriptBuilder bind:this={scriptBuilder} bind:topHash {initialPath} {script} {initialArgs} />
|
||||
{/if}
|
||||
@@ -1,5 +1,5 @@
|
||||
export function load({ params }) {
|
||||
return {
|
||||
stuff: { title: `Edit Script ${params.hash}` }
|
||||
stuff: { title: `Edit Script ${params.path}` }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<script lang="ts">
|
||||
import { ScriptService, NewScript } from '$lib/gen'
|
||||
|
||||
import { page } from '$app/stores'
|
||||
import { runFormStore, workspaceStore } from '$lib/stores'
|
||||
import ScriptBuilder from '$lib/components/ScriptBuilder.svelte'
|
||||
import { decodeState, sendUserToast } from '$lib/utils'
|
||||
import { goto } from '$app/navigation'
|
||||
|
||||
const initialState = $page.url.searchParams.get('state')
|
||||
let initialArgs = {}
|
||||
if ($runFormStore) {
|
||||
initialArgs = $runFormStore
|
||||
$runFormStore = undefined
|
||||
}
|
||||
let topHash = $page.url.searchParams.get('topHash') ?? undefined
|
||||
|
||||
let hash = $page.url.searchParams.get('hash') ?? undefined
|
||||
|
||||
let scriptLoadedFromUrl = initialState != undefined ? decodeState(initialState) : undefined
|
||||
|
||||
let script: NewScript | undefined = undefined
|
||||
|
||||
let initialPath: string = ''
|
||||
|
||||
let scriptBuilder: ScriptBuilder | undefined = undefined
|
||||
|
||||
async function loadScript(): Promise<void> {
|
||||
if (scriptLoadedFromUrl != undefined && scriptLoadedFromUrl.path == $page.params.path) {
|
||||
script = scriptLoadedFromUrl
|
||||
sendUserToast('Script loaded from latest state stored in the URL', false, [
|
||||
{
|
||||
label: 'Discard state and reload',
|
||||
callback: () => {
|
||||
scriptLoadedFromUrl = undefined
|
||||
goto(`/scripts/edit/${script!.path}`)
|
||||
loadScript()
|
||||
}
|
||||
}
|
||||
])
|
||||
} else {
|
||||
if (hash) {
|
||||
const scriptByHash = await ScriptService.getScriptByHash({
|
||||
workspace: $workspaceStore!,
|
||||
hash
|
||||
})
|
||||
script = { ...scriptByHash, lock: undefined }
|
||||
} else {
|
||||
const scriptWithDraft = await ScriptService.getScriptByPathWithDraft({
|
||||
workspace: $workspaceStore!,
|
||||
path: $page.params.path
|
||||
})
|
||||
if (scriptWithDraft.draft != undefined) {
|
||||
script = scriptWithDraft.draft
|
||||
} else {
|
||||
script = scriptWithDraft
|
||||
}
|
||||
if (!scriptWithDraft.draft_only) {
|
||||
sendUserToast('Script loaded from latest saved draft', false, [
|
||||
{
|
||||
label: 'Ignore draft and load from latest deployed version',
|
||||
callback: () => {
|
||||
scriptLoadedFromUrl = undefined
|
||||
hash = scriptWithDraft.hash
|
||||
console.log(hash)
|
||||
goto(`/scripts/edit/${script!.path}`)
|
||||
loadScript()
|
||||
}
|
||||
}
|
||||
])
|
||||
}
|
||||
topHash = scriptWithDraft.hash
|
||||
}
|
||||
}
|
||||
// hash
|
||||
// ? await ScriptService.getScriptByHash({
|
||||
// workspace: $workspaceStore!,
|
||||
// hash: $page.params.hash
|
||||
// })
|
||||
// : await ScriptService.getScriptByPathWithDraft({
|
||||
// workspace: $workspaceStore!,
|
||||
// path: $page.params.path
|
||||
// })
|
||||
|
||||
if (script) {
|
||||
initialPath = script.path
|
||||
scriptBuilder?.setCode(script.content)
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if ($workspaceStore) {
|
||||
loadScript()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if script}
|
||||
<ScriptBuilder bind:this={scriptBuilder} bind:topHash {initialPath} {script} {initialArgs} />
|
||||
{/if}
|
||||
@@ -225,8 +225,8 @@
|
||||
</Button>
|
||||
{#if !$userStore?.operator}
|
||||
<Button
|
||||
href={`/scripts/edit/${script.hash}?args=${encodeState(args)}${
|
||||
topHash ? '&topHash=' + topHash : ''
|
||||
href={`/scripts/edit/${script.path}?args=${encodeState(args)}${
|
||||
topHash ? `&hash=${script.hash}&topHash=` + topHash : ''
|
||||
}`}
|
||||
color="blue"
|
||||
size="md"
|
||||
@@ -291,25 +291,25 @@
|
||||
|
||||
<div class="flex gap-2 flex-wrap mt-4">
|
||||
{#if SCRIPT_VIEW_SHOW_PUBLISH_TO_HUB}
|
||||
<Button
|
||||
disabled={deploymentInProgress}
|
||||
target="_blank"
|
||||
href={scriptToHubUrl(
|
||||
script.content,
|
||||
script.summary,
|
||||
script.description ?? '',
|
||||
script.kind,
|
||||
script.language,
|
||||
script.schema,
|
||||
script.language == 'deno' ? '' : script.lock
|
||||
).toString()}
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
startIcon={{ icon: faGlobe }}
|
||||
>
|
||||
Publish to Hub
|
||||
</Button>
|
||||
<Button
|
||||
disabled={deploymentInProgress}
|
||||
target="_blank"
|
||||
href={scriptToHubUrl(
|
||||
script.content,
|
||||
script.summary,
|
||||
script.description ?? '',
|
||||
script.kind,
|
||||
script.language,
|
||||
script.schema,
|
||||
script.language == 'deno' ? '' : script.lock
|
||||
).toString()}
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
startIcon={{ icon: faGlobe }}
|
||||
>
|
||||
Publish to Hub
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
on:click={() => shareModal.openDrawer(script?.path ?? '', 'script')}
|
||||
@@ -322,15 +322,15 @@
|
||||
Share
|
||||
</Button>
|
||||
{#if SCRIPT_VIEW_SHOW_SCHEDULE}
|
||||
<Button
|
||||
on:click={() => scheduleEditor?.openNew(false, script?.path ?? '')}
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
startIcon={{ icon: faCalendar }}
|
||||
>
|
||||
Schedule
|
||||
</Button>
|
||||
<Button
|
||||
on:click={() => scheduleEditor?.openNew(false, script?.path ?? '')}
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
startIcon={{ icon: faCalendar }}
|
||||
>
|
||||
Schedule
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
on:click={() => moveDrawer.openDrawer(script?.path ?? '', script?.summary, 'script')}
|
||||
@@ -463,9 +463,7 @@
|
||||
Webhooks
|
||||
<Tooltip>
|
||||
{SCRIPT_VIEW_WEBHOOK_INFO_TIP}
|
||||
<a href={SCRIPT_VIEW_WEBHOOK_INFO_LINK} class="text-blue-500">
|
||||
See docs
|
||||
</a>
|
||||
<a href={SCRIPT_VIEW_WEBHOOK_INFO_LINK} class="text-blue-500"> See docs </a>
|
||||
</Tooltip>
|
||||
</h3>
|
||||
<Skeleton {loading} layout={[[8.5]]} />
|
||||
@@ -516,23 +514,23 @@
|
||||
{/each}
|
||||
</ul>
|
||||
{#if SCRIPT_VIEW_SHOW_CREATE_TOKEN_BUTTON}
|
||||
<div class="flex flex-row-reverse mt-2">
|
||||
<Button size="xs" on:click={userSettings.openDrawer}>Create token</Button>
|
||||
</div>
|
||||
<div class="flex flex-row-reverse mt-2">
|
||||
<Button size="xs" on:click={userSettings.openDrawer}>Create token</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</TabContent>
|
||||
{/each}
|
||||
{#if SCRIPT_VIEW_SHOW_EXAMPLE_CURL}
|
||||
<div class="flex">
|
||||
<Button
|
||||
color="light"
|
||||
size="sm"
|
||||
endIcon={{ icon: viewWebhookCommand ? faChevronUp : faChevronDown }}
|
||||
on:click={() => (viewWebhookCommand = !viewWebhookCommand)}
|
||||
>
|
||||
See example curl command
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<Button
|
||||
color="light"
|
||||
size="sm"
|
||||
endIcon={{ icon: viewWebhookCommand ? faChevronUp : faChevronDown }}
|
||||
on:click={() => (viewWebhookCommand = !viewWebhookCommand)}
|
||||
>
|
||||
See example curl command
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if viewWebhookCommand}
|
||||
<div transition:slide|local class="px-4">
|
||||
|
||||
@@ -146,7 +146,7 @@
|
||||
startIcon={{ icon: faPen }}
|
||||
disabled={script == undefined}
|
||||
variant="border"
|
||||
href="/scripts/edit/{script?.hash}">Edit</Button
|
||||
href="/scripts/edit/{script?.path}">Edit</Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user