feat: introduce backend persisted draft systems for scripts

This commit is contained in:
Ruben Fiszel
2023-04-30 15:29:46 +02:00
parent e73ae67346
commit ec89b656b6
33 changed files with 867 additions and 340 deletions

View File

@@ -0,0 +1,3 @@
-- Add down migration script here
DROP TABLE draft;
DROP TYPE DRAFT_TYPE;

View 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;

View File

@@ -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": [

View File

@@ -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: {}

View File

@@ -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,

View 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))
// }

View File

@@ -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,

View File

@@ -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())),

View File

@@ -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,

View File

@@ -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},

View File

@@ -1165,6 +1165,8 @@ where
"edited_at",
"edited_by",
"archived",
"has_draft",
"draft_only",
] {
if obj.contains_key(key) {
obj.remove(key);

View File

@@ -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)]

View 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}

View File

@@ -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

View File

@@ -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&nbsp;<Kbd small>Ctrl</Kbd><Kbd small>S</Kbd>
</Button>
</div>
</div>

View File

@@ -87,7 +87,7 @@
</Button>
{:else}
<Button btnClasses="w-full truncate" size="sm" on:click={() => runTest(stepArgs)}
>Run&nbsp;<Kbd>{getModifierKey()}</Kbd>+<Kbd>Enter</Kbd></Button
>Run&nbsp;<Kbd small>{getModifierKey()}</Kbd><Kbd small>Enter</Kbd></Button
>
{/if}

View File

@@ -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&nbsp;<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}

View File

@@ -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&nbsp;<Kbd small>Ctrl</Kbd><Kbd small>Enter</Kbd>
{/if}
</Button>
{/if}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 />

View File

@@ -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>

View File

@@ -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
},
{

View File

@@ -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?.()

View File

@@ -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
}
})

View File

@@ -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

View File

@@ -740,7 +740,7 @@ export function isMac(): boolean {
}
export function getModifierKey(): string {
return isMac() ? '⌘' : 'CTRL'
return isMac() ? '⌘' : 'Ctrl'
}
export function isValidHexColor(color: string): boolean {

View File

@@ -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',

View File

@@ -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}

View File

@@ -1,5 +1,5 @@
export function load({ params }) {
return {
stuff: { title: `Edit Script ${params.hash}` }
stuff: { title: `Edit Script ${params.path}` }
}
}

View File

@@ -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}

View File

@@ -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">

View File

@@ -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}