mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-10 08:07:03 +00:00
feat(frontend): unify all triggers UX and simplify flow settings (#4259)
* feat(frontend): added list of triggers in the flow graph * feat(frontend): added list of triggers in the flow graph * feat(frontend): clean up * feat(frontend): improve UX * feat(frontend): triggers * feat(frontend): triggers * feat(frontend): done * feat(frontend): fix trigger when position when a preprocessor is presetn * Glm/rework flow settings v2 (#4497) * fat(frontend): simplify flow settings menu * improve scroll * changing mute toggle * Add advanced settings badge * Add nord theme colors * Add bage for advanced options * fix minor issue * fix minor issue * Add triggers menu to flow settings * Add quick trigger access * remove triggers in flow settings * fix minor issue * Move triggers settings to flow right panel * polishing * fix unset store * remove save up to for triggers * fix padding * reset default tag color * remove custom select component * revert path change * revert section modif * Revert unused feature --------- Co-authored-by: Guilhem <guilhem@mbp-de-windmill.home> * Connect top bar cron to schedules settings * Turn copilot into node * fix copilot placement * remove useless import * fix center copilot * fix binding * remove copilot on top of preprocessor * render copilot node on condition * quickfix * remove copilot node * fix minor issues * fix route count update * fix schedule sync * harmonize colors * fix alignment and add edges * recenter node summary * fix schedules sync * Add id title * all * all * all * iteration * all * all * done * fix * more fixes --------- Co-authored-by: Guilhem <guilhemlemouel@gmail.com> Co-authored-by: Guilhem <guilhem@mbp-de-windmill.home> Co-authored-by: Ruben Fiszel <ruben@windmill.dev> Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
This commit is contained in:
committed by
Ruben Fiszel
co-authored by
Guilhem
Guilhem
Ruben Fiszel
Ruben Fiszel
parent
f38b3d14e8
commit
91a3d06529
+24
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM schedule WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Bool",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "1ca5bc2d35c0498b587fd0618434def64233dc4f8fc3344d8d74be8e96ded659"
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM http_trigger WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Bool",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "31bc3dcea29be9cc0242771d25a232f173446d29c08fc29ddb8d55294f2c070e"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM token WHERE label LIKE 'webhook-%' AND workspace_id = $1 AND scopes @> ARRAY['run:' || $2]::text[]",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "5fc6b4a4dbb7875bdec76f876c18543435a95b019b20081f52f6ed6f4457e3c7"
|
||||
}
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "bool",
|
||||
"name": "?column?",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM token WHERE label LIKE 'email-%' AND workspace_id = $1 AND scopes @> ARRAY['run:flow/' || $2]::text[]",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "a7f5431e3b8960e9dc46fae69dd4391516d8b169186548ec44528c84078b80d8"
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT schedule FROM schedule WHERE path = $1 AND script_path = $1 AND is_flow = $2 AND workspace_id = $3",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "schedule",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Bool",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c060b8bbc5af7d2e7d0aaff64f0f62ec9db58611a99b0ba7f0375638b128ab89"
|
||||
}
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO token\n (token, email, label, expiration, super_admin, scopes)\n VALUES ($1, $2, $3, $4, $5, $6)",
|
||||
"query": "INSERT INTO token\n (token, email, label, expiration, super_admin, scopes, workspace_id)\n VALUES ($1, $2, $3, $4, $5, $6, $7)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -10,10 +10,11 @@
|
||||
"Varchar",
|
||||
"Timestamptz",
|
||||
"Bool",
|
||||
"TextArray"
|
||||
"TextArray",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "34ad8a2a5bd89b9b8e25847a7e5e94ef99e35a178ad6328c1bcde2a6d6f88cb5"
|
||||
"hash": "c624f15f3e321b1eecf123da9bf0b18e8c1d16ef25ffb9d04e5447d0d583d55c"
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, last_used_at, scopes, email FROM token WHERE workspace_id = $1 AND scopes @> ARRAY['run:script/' || $2]::text[]",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "label",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "token_prefix",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "expiration",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "last_used_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "scopes",
|
||||
"type_info": "TextArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
null,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "c7ee7ce64686cef41cebd99ad7ef31572fc1bf12e6ae473fd58fafb025989965"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM token WHERE label LIKE 'webhook-%' AND workspace_id = $1 AND scopes @> ARRAY['run:flow/' || $2]::text[]",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "d7a0f19f9e18d2ea49316012375ad78b69292ba091d69880945e42bebe890d66"
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, last_used_at, scopes, email FROM token WHERE workspace_id = $1 AND scopes @> ARRAY['run:flow/' || $2]::text[]",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "label",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "token_prefix",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "expiration",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "last_used_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "scopes",
|
||||
"type_info": "TextArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
null,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "eff32aeac25a75d06f73e08c26dd3fd25f6b85cbea870505751c6a82457ae1da"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM token WHERE label LIKE 'email-%' AND workspace_id = $1 AND scopes @> ARRAY['run:script/' || $2]::text[]",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "f06e0e4fa358b26792df22fff48b71a6fcfa1e5603ea472892917c1accd1aafb"
|
||||
}
|
||||
@@ -4020,6 +4020,42 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Script"
|
||||
|
||||
/w/{workspace}/scripts/get_triggers_count/{path}:
|
||||
get:
|
||||
summary: get triggers count of script
|
||||
operationId: getTriggersCountOfScript
|
||||
tags:
|
||||
- script
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/ScriptPath"
|
||||
responses:
|
||||
"200":
|
||||
description: triggers count
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/TriggersCount"
|
||||
|
||||
/w/{workspace}/scripts/list_tokens/{path}:
|
||||
get:
|
||||
summary: get tokens with script scope
|
||||
operationId: listTokensOfScript
|
||||
tags:
|
||||
- script
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/ScriptPath"
|
||||
responses:
|
||||
"200":
|
||||
description: tokens list
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/TruncatedToken"
|
||||
|
||||
/w/{workspace}/scripts/get/draft/{path}:
|
||||
get:
|
||||
summary: get script by path with draft
|
||||
@@ -4624,6 +4660,43 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Flow"
|
||||
|
||||
/w/{workspace}/flows/get_triggers_count/{path}:
|
||||
get:
|
||||
summary: get triggers count of flow
|
||||
operationId: getTriggersCountOfFlow
|
||||
tags:
|
||||
- flow
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/ScriptPath"
|
||||
responses:
|
||||
"200":
|
||||
description: triggers count
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/TriggersCount"
|
||||
|
||||
/w/{workspace}/flows/list_tokens/{path}:
|
||||
get:
|
||||
summary: get tokens with flow scope
|
||||
operationId: listTokensOfFlow
|
||||
tags:
|
||||
- flow
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/ScriptPath"
|
||||
responses:
|
||||
"200":
|
||||
description: tokens list
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/TruncatedToken"
|
||||
|
||||
|
||||
/w/{workspace}/flows/toggle_workspace_error_handler/{path}:
|
||||
post:
|
||||
summary: Toggle ON and OFF the workspace error handler for a given flow
|
||||
@@ -10254,6 +10327,8 @@ components:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
required:
|
||||
- token_prefix
|
||||
- created_at
|
||||
@@ -10271,6 +10346,8 @@ components:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
workspace_id:
|
||||
type: string
|
||||
|
||||
NewTokenImpersonate:
|
||||
type: object
|
||||
@@ -10282,6 +10359,8 @@ components:
|
||||
format: date-time
|
||||
impersonate_email:
|
||||
type: string
|
||||
workspace_id:
|
||||
type: string
|
||||
required:
|
||||
- impersonate_email
|
||||
|
||||
@@ -11118,6 +11197,23 @@ components:
|
||||
- requires_auth
|
||||
- http_method
|
||||
|
||||
TriggersCount:
|
||||
type: object
|
||||
properties:
|
||||
primary_schedule:
|
||||
type: object
|
||||
properties:
|
||||
schedule:
|
||||
type: string
|
||||
schedule_count:
|
||||
type: number
|
||||
http_routes_count:
|
||||
type: number
|
||||
webhook_count:
|
||||
type: number
|
||||
email_count:
|
||||
type: number
|
||||
|
||||
Group:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::db::ApiAuthed;
|
||||
use crate::triggers::{
|
||||
get_triggers_count_internal, list_tokens_internal, TriggersCount, TruncatedTokenWithEmail,
|
||||
};
|
||||
use crate::utils::WithStarredInfoQuery;
|
||||
use crate::{
|
||||
db::DB,
|
||||
@@ -53,6 +56,8 @@ pub fn workspaced_service() -> Router {
|
||||
.route("/update/*path", post(update_flow))
|
||||
.route("/archive/*path", post(archive_flow_by_path))
|
||||
.route("/delete/*path", delete(delete_flow_by_path))
|
||||
.route("/get_triggers_count/*path", get(get_triggers_count))
|
||||
.route("/list_tokens/*path", get(list_tokens))
|
||||
.route("/get/*path", get(get_flow_by_path))
|
||||
.route("/get/draft/*path", get(get_flow_by_path_w_draft))
|
||||
.route("/exists/*path", get(exists_flow_by_path))
|
||||
@@ -874,6 +879,22 @@ async fn update_flow(
|
||||
Ok(nf.path.to_string())
|
||||
}
|
||||
|
||||
async fn get_triggers_count(
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> JsonResult<TriggersCount> {
|
||||
let path = path.to_path();
|
||||
get_triggers_count_internal(&db, &w_id, &path, true).await
|
||||
}
|
||||
|
||||
async fn list_tokens(
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> JsonResult<Vec<TruncatedTokenWithEmail>> {
|
||||
let path = path.to_path();
|
||||
list_tokens_internal(&db, &w_id, &path, true).await
|
||||
}
|
||||
|
||||
async fn get_flow_by_path(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
|
||||
@@ -82,6 +82,7 @@ pub mod smtp_server_ee;
|
||||
mod static_assets;
|
||||
mod stripe_ee;
|
||||
mod tracing_init;
|
||||
mod triggers;
|
||||
mod users;
|
||||
mod utils;
|
||||
mod variables;
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
use crate::{
|
||||
db::{ApiAuthed, DB},
|
||||
schedule::clear_schedule,
|
||||
triggers::{
|
||||
get_triggers_count_internal, list_tokens_internal, TriggersCount, TruncatedTokenWithEmail,
|
||||
},
|
||||
users::{maybe_refresh_folders, require_owner_of_path, AuthCache},
|
||||
utils::WithStarredInfoQuery,
|
||||
webhook_util::{WebhookMessage, WebhookShared},
|
||||
@@ -132,6 +135,8 @@ pub fn workspaced_service() -> Router {
|
||||
.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("/get_triggers_count/*path", get(get_triggers_count))
|
||||
.route("/list_tokens/*path", get(list_tokens))
|
||||
.route("/raw/p/*path", get(raw_script_by_path))
|
||||
.route("/raw_unpinned/p/*path", get(raw_script_by_path_unpinned))
|
||||
.route("/exists/p/*path", get(exists_script_by_path))
|
||||
@@ -874,6 +879,22 @@ async fn get_script_by_path(
|
||||
Ok(Json(script))
|
||||
}
|
||||
|
||||
async fn list_tokens(
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> JsonResult<Vec<TruncatedTokenWithEmail>> {
|
||||
let path = path.to_path();
|
||||
list_tokens_internal(&db, &w_id, &path, false).await
|
||||
}
|
||||
|
||||
async fn get_triggers_count(
|
||||
Extension(db): Extension<DB>,
|
||||
Path((w_id, path)): Path<(String, StripPath)>,
|
||||
) -> JsonResult<TriggersCount> {
|
||||
let path = path.to_path();
|
||||
get_triggers_count_internal(&db, &w_id, &path, false).await
|
||||
}
|
||||
|
||||
async fn get_script_by_path_w_draft(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
use axum::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
use windmill_common::error::JsonResult;
|
||||
|
||||
use crate::db::DB;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct TriggerPrimarySchedule {
|
||||
schedule: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct TriggersCount {
|
||||
primary_schedule: Option<TriggerPrimarySchedule>,
|
||||
schedule_count: i64,
|
||||
http_routes_count: i64,
|
||||
webhook_count: i64,
|
||||
email_count: i64,
|
||||
}
|
||||
pub(crate) async fn get_triggers_count_internal(
|
||||
db: &DB,
|
||||
w_id: &str,
|
||||
path: &str,
|
||||
is_flow: bool,
|
||||
) -> JsonResult<TriggersCount> {
|
||||
let primary_schedule = sqlx::query_scalar!(
|
||||
"SELECT schedule FROM schedule WHERE path = $1 AND script_path = $1 AND is_flow = $2 AND workspace_id = $3",
|
||||
path,
|
||||
is_flow,
|
||||
w_id
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
|
||||
let schedule_count = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM schedule WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3",
|
||||
path,
|
||||
is_flow,
|
||||
w_id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
|
||||
let http_routes_count = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM http_trigger WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3",
|
||||
path,
|
||||
is_flow,
|
||||
w_id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
|
||||
let webhook_count = (if is_flow {
|
||||
sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM token WHERE label LIKE 'webhook-%' AND workspace_id = $1 AND scopes @> ARRAY['run:flow/' || $2]::text[]",
|
||||
w_id,
|
||||
path,
|
||||
)
|
||||
|
||||
} else {
|
||||
sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM token WHERE label LIKE 'webhook-%' AND workspace_id = $1 AND scopes @> ARRAY['run:' || $2]::text[]",
|
||||
w_id,
|
||||
path,
|
||||
)
|
||||
}).fetch_one(db)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
|
||||
let email_count = (if is_flow {
|
||||
sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM token WHERE label LIKE 'email-%' AND workspace_id = $1 AND scopes @> ARRAY['run:flow/' || $2]::text[]",
|
||||
w_id,
|
||||
path,
|
||||
)
|
||||
|
||||
} else {
|
||||
sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM token WHERE label LIKE 'email-%' AND workspace_id = $1 AND scopes @> ARRAY['run:script/' || $2]::text[]",
|
||||
w_id,
|
||||
path,
|
||||
)
|
||||
}).fetch_one(db)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
|
||||
Ok(Json(TriggersCount {
|
||||
primary_schedule: primary_schedule.map(|s| TriggerPrimarySchedule { schedule: s }),
|
||||
schedule_count,
|
||||
http_routes_count,
|
||||
webhook_count,
|
||||
email_count,
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize)]
|
||||
pub struct TruncatedTokenWithEmail {
|
||||
pub label: Option<String>,
|
||||
pub token_prefix: Option<String>,
|
||||
pub expiration: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub last_used_at: chrono::DateTime<chrono::Utc>,
|
||||
pub scopes: Option<Vec<String>>,
|
||||
pub email: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn list_tokens_internal(
|
||||
db: &DB,
|
||||
w_id: &str,
|
||||
path: &str,
|
||||
is_flow: bool,
|
||||
) -> JsonResult<Vec<TruncatedTokenWithEmail>> {
|
||||
let tokens = if is_flow {
|
||||
sqlx::query_as!(
|
||||
TruncatedTokenWithEmail,
|
||||
"SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, last_used_at, scopes, email FROM token WHERE workspace_id = $1 AND scopes @> ARRAY['run:flow/' || $2]::text[]",
|
||||
w_id, path).fetch_all(db)
|
||||
.await?
|
||||
} else {
|
||||
sqlx::query_as!(
|
||||
TruncatedTokenWithEmail,
|
||||
"SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, last_used_at, scopes, email FROM token WHERE workspace_id = $1 AND scopes @> ARRAY['run:script/' || $2]::text[]",
|
||||
w_id, path)
|
||||
.fetch_all(db)
|
||||
.await?
|
||||
};
|
||||
Ok(Json(tokens))
|
||||
}
|
||||
@@ -128,7 +128,13 @@ pub fn make_unauthed_service() -> Router {
|
||||
|
||||
fn username_override_from_label(label: Option<String>) -> Option<String> {
|
||||
match label {
|
||||
Some(label) if label.starts_with("webhook-") => Some(label),
|
||||
Some(label)
|
||||
if label.starts_with("webhook-")
|
||||
|| label.starts_with("http-")
|
||||
|| label.starts_with("email-") =>
|
||||
{
|
||||
Some(label)
|
||||
}
|
||||
Some(label) if label.starts_with("ephemeral-script-end-user-") => Some(
|
||||
label
|
||||
.trim_start_matches("ephemeral-script-end-user-")
|
||||
@@ -270,9 +276,10 @@ impl AuthCache {
|
||||
_ => {
|
||||
let user_o = sqlx::query_as::<_, (Option<String>, Option<String>, bool, Option<Vec<String>>, Option<String>)>(
|
||||
"UPDATE token SET last_used_at = now() WHERE token = $1 AND (expiration > NOW() \
|
||||
OR expiration IS NULL) RETURNING owner, email, super_admin, scopes, label",
|
||||
OR expiration IS NULL) AND (workspace_id IS NULL OR workspace_id = $2) RETURNING owner, email, super_admin, scopes, label",
|
||||
)
|
||||
.bind(token)
|
||||
.bind(w_id.as_ref())
|
||||
.fetch_optional(&self.db)
|
||||
.await
|
||||
.ok()
|
||||
@@ -837,6 +844,7 @@ pub struct NewToken {
|
||||
pub expiration: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub impersonate_email: Option<String>,
|
||||
pub scopes: Option<Vec<String>>,
|
||||
pub workspace_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -2389,14 +2397,15 @@ async fn create_token(
|
||||
.unwrap_or(false);
|
||||
sqlx::query!(
|
||||
"INSERT INTO token
|
||||
(token, email, label, expiration, super_admin, scopes)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)",
|
||||
(token, email, label, expiration, super_admin, scopes, workspace_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)",
|
||||
token,
|
||||
authed.email,
|
||||
new_token.label,
|
||||
new_token.expiration,
|
||||
is_super_admin,
|
||||
new_token.scopes.as_ref().map(|x| x.as_slice())
|
||||
new_token.scopes.as_ref().map(|x| x.as_slice()),
|
||||
new_token.workspace_id,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
Error parsing /git/windmill/backend/windmill-api/openapi.yaml: duplicated mapping key (8350:3)
|
||||
|
||||
8347 | application/json:
|
||||
8348 | schema: {}
|
||||
8349 |
|
||||
8350 | /w/{workspace}/job_helpers/loa ...
|
||||
----------^
|
||||
8351 | get:
|
||||
8352 | summary: Load a preview of ...
|
||||
ParserError: Error parsing /git/windmill/backend/windmill-api/openapi.yaml: duplicated mapping key (8350:3)
|
||||
|
||||
8347 | application/json:
|
||||
8348 | schema: {}
|
||||
8349 |
|
||||
8350 | /w/{workspace}/job_helpers/loa ...
|
||||
----------^
|
||||
8351 | get:
|
||||
8352 | summary: Load a preview of ...
|
||||
at Object.parse (/git/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/parsers/yaml.js:44:23)
|
||||
at getResult (/git/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/util/plugins.js:116:22)
|
||||
at runNextPlugin (/git/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/util/plugins.js:64:32)
|
||||
at /git/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/util/plugins.js:55:9
|
||||
at new Promise (<anonymous>)
|
||||
at Object.run (/git/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/util/plugins.js:54:12)
|
||||
at parseFile (/git/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/parse.js:130:38)
|
||||
at parse (/git/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/parse.js:56:30)
|
||||
at async $RefParser.parse (/git/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/index.js:115:28)
|
||||
at async $RefParser.resolve (/git/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/index.js:145:13)
|
||||
@@ -6,6 +6,7 @@
|
||||
import AssignableTagsInner from './AssignableTagsInner.svelte'
|
||||
|
||||
export let placement: 'bottom-end' | 'top-end' = 'bottom-end'
|
||||
export let color: 'nord' | 'dark' = 'dark'
|
||||
</script>
|
||||
|
||||
<Popup
|
||||
@@ -13,7 +14,7 @@
|
||||
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
|
||||
>
|
||||
<svelte:fragment slot="button">
|
||||
<Button color="dark" size="xs" nonCaptureEvent={true}>
|
||||
<Button {color} size="xs" nonCaptureEvent={true}>
|
||||
<div class="flex flex-row gap-1 items-center"
|
||||
><Pen size={14} /> Custom Tags <Tooltip light
|
||||
>Tags are assigned to scripts and flows. Workers only accept jobs that correspond to their
|
||||
|
||||
@@ -203,7 +203,7 @@
|
||||
|
||||
<div class="w-full flex space-x-8">
|
||||
<div class="w-full flex flex-col gap-4">
|
||||
<Label label="Cron">
|
||||
<Label label="Cron" class="font-semibold" primary={true}>
|
||||
<svelte:fragment slot="error">
|
||||
{#if !validCRON}
|
||||
<div class="text-red-600 text-xs"> Invalid cron syntax </div>
|
||||
@@ -219,10 +219,10 @@
|
||||
{disabled}
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Timezone">
|
||||
<Label label="Timezone" class="font-semibold" primary>
|
||||
{#if disabled}
|
||||
<div>
|
||||
<Badge>{timezone}</Badge>
|
||||
<Badge><span class="text-primary-inverse">{timezone}</span></Badge>
|
||||
</div>
|
||||
{:else}
|
||||
<Select
|
||||
@@ -390,9 +390,8 @@
|
||||
</div>
|
||||
|
||||
<div class="w-full flex flex-col space-y-2">
|
||||
<div class="text-sm font-semibold leading-none">Execution summary</div>
|
||||
<div class="text-sm font-semibold leading-none">Estimated upcoming events ({timezone})</div>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div class="text-sm">Estimated upcoming events ({timezone})</div>
|
||||
<div class="flex flex-col rounded-md p-4 border text-tertiary bg-surface-secondary gap-0.5">
|
||||
{#each preview as date}
|
||||
<span class="text-sm">{dateFormatter(new Date(date))}</span>
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
WorkspaceService,
|
||||
type InputTransform,
|
||||
type RawScript,
|
||||
type PathScript
|
||||
type PathScript,
|
||||
type TriggersCount
|
||||
} from '$lib/gen'
|
||||
import { inferArgs } from '$lib/infer'
|
||||
import { copilotInfo, userStore, workspaceStore } from '$lib/stores'
|
||||
@@ -41,14 +42,13 @@
|
||||
import { workspacedOpenai } from './copilot/lib'
|
||||
import type { FlowCopilotContext, FlowCopilotModule } from './copilot/flow'
|
||||
import { pickScript } from './flows/flowStateUtils'
|
||||
import type { Schedule } from './flows/scheduleUtils'
|
||||
import {
|
||||
approximateFindPythonRelativePath,
|
||||
isTypescriptRelativePath,
|
||||
parseTypescriptDeps
|
||||
} from '$lib/relative_imports'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
|
||||
import type { ScheduleTrigger, TriggerContext } from './triggers'
|
||||
$: token = $page.url.searchParams.get('wm_token') ?? undefined
|
||||
$: workspace = $page.url.searchParams.get('workspace') ?? undefined
|
||||
$: themeDarkRaw = $page.url.searchParams.get('activeColorTheme')
|
||||
@@ -467,13 +467,7 @@
|
||||
}
|
||||
|
||||
const flowStateStore = writable({} as FlowState)
|
||||
const scheduleStore = writable<Schedule>({
|
||||
args: {},
|
||||
cron: '',
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
enabled: false,
|
||||
summary: undefined
|
||||
})
|
||||
|
||||
const previewArgsStore = writable<Record<string, any>>({})
|
||||
const scriptEditorDrawer = writable(undefined)
|
||||
const moving = writable<{ module: FlowModule; modules: FlowModule[] } | undefined>(undefined)
|
||||
@@ -481,10 +475,19 @@
|
||||
|
||||
const testStepStore = writable<Record<string, any>>({})
|
||||
const selectedIdStore = writable('settings-metadata')
|
||||
const selectedTriggerStore = writable<'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes'>(
|
||||
'webhooks'
|
||||
)
|
||||
|
||||
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(undefined)
|
||||
const triggersCount = writable<TriggersCount | undefined>(undefined)
|
||||
setContext<TriggerContext>('TriggerContext', {
|
||||
primarySchedule: primaryScheduleStore,
|
||||
selectedTrigger: selectedTriggerStore,
|
||||
triggersCount: triggersCount
|
||||
})
|
||||
setContext<FlowEditorContext>('FlowEditorContext', {
|
||||
selectedId: selectedIdStore,
|
||||
schedule: scheduleStore,
|
||||
previewArgs: previewArgsStore,
|
||||
scriptEditorDrawer,
|
||||
moving,
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
ScriptService,
|
||||
type OpenFlow,
|
||||
type RawScript,
|
||||
type InputTransform
|
||||
type InputTransform,
|
||||
type TriggersCount
|
||||
} from '$lib/gen'
|
||||
import { initHistory, push, redo, undo } from '$lib/history'
|
||||
import {
|
||||
@@ -40,7 +41,6 @@
|
||||
import { dfs, getPreviousIds } from './flows/previousResults'
|
||||
import FlowImportExportMenu from './flows/header/FlowImportExportMenu.svelte'
|
||||
import FlowPreviewButtons from './flows/header/FlowPreviewButtons.svelte'
|
||||
import { loadFlowSchedule, type Schedule } from './flows/scheduleUtils'
|
||||
import type { FlowEditorContext, FlowInput } from './flows/types'
|
||||
import { cleanInputs, emptyFlowModuleState } from './flows/utils'
|
||||
import {
|
||||
@@ -82,6 +82,7 @@
|
||||
import Summary from './Summary.svelte'
|
||||
import type { FlowBuilderWhitelabelCustomUi } from './custom_ui'
|
||||
import FlowYamlEditor from './flows/header/FlowYamlEditor.svelte'
|
||||
import { type TriggerContext, type ScheduleTrigger } from './triggers'
|
||||
|
||||
export let initialPath: string = ''
|
||||
export let pathStoreInit: string | undefined = undefined
|
||||
@@ -100,30 +101,47 @@
|
||||
export let customUi: FlowBuilderWhitelabelCustomUi = {}
|
||||
export let disableAi: boolean = false
|
||||
export let disabledFlowInputs = false
|
||||
export let savedPrimarySchedule: ScheduleTrigger | undefined = undefined
|
||||
|
||||
$: setContext('customUi', customUi)
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
async function createSchedule(path: string) {
|
||||
const { cron, timezone, args, enabled, summary } = $scheduleStore
|
||||
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(savedPrimarySchedule)
|
||||
const triggersCount = writable<TriggersCount | undefined>(
|
||||
savedPrimarySchedule
|
||||
? { schedule_count: 1, primary_schedule: { schedule: savedPrimarySchedule.cron } }
|
||||
: undefined
|
||||
)
|
||||
|
||||
try {
|
||||
await ScheduleService.createSchedule({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
path: path,
|
||||
schedule: formatCron(cron),
|
||||
timezone,
|
||||
script_path: path,
|
||||
is_flow: true,
|
||||
args,
|
||||
enabled,
|
||||
summary
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
sendUserToast(`The primary schedule could not be created: ${err}`, true)
|
||||
export function setPrimarySchedule(schedule: ScheduleTrigger | undefined | false) {
|
||||
primaryScheduleStore.set(schedule)
|
||||
loadTriggers()
|
||||
}
|
||||
|
||||
async function createSchedule(path: string) {
|
||||
if ($primaryScheduleStore) {
|
||||
const { cron, timezone, args, enabled, summary } = $primaryScheduleStore
|
||||
|
||||
try {
|
||||
await ScheduleService.createSchedule({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
path: path,
|
||||
schedule: formatCron(cron),
|
||||
timezone,
|
||||
script_path: path,
|
||||
is_flow: true,
|
||||
args,
|
||||
enabled,
|
||||
summary
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
sendUserToast(`The primary schedule could not be created: ${err}`, true)
|
||||
}
|
||||
} else {
|
||||
sendUserToast('The primary schedule could not be created: no schedule data', true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,7 +205,8 @@
|
||||
typ: 'flow',
|
||||
value: {
|
||||
...flow,
|
||||
path: $pathStore
|
||||
path: $pathStore,
|
||||
primary_schedule: $primaryScheduleStore
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -241,7 +260,7 @@
|
||||
// console.log('flow', computeUnlockedSteps(flow)) // del
|
||||
// loadingSave = false // del
|
||||
// return
|
||||
const { cron, timezone, args, enabled, summary } = $scheduleStore
|
||||
|
||||
if (newFlow) {
|
||||
try {
|
||||
localStorage.removeItem('flow')
|
||||
@@ -264,7 +283,7 @@
|
||||
deployment_message: deploymentMsg || undefined
|
||||
}
|
||||
})
|
||||
if (enabled) {
|
||||
if ($primaryScheduleStore && $primaryScheduleStore.enabled) {
|
||||
await createSchedule($pathStore)
|
||||
}
|
||||
} else {
|
||||
@@ -284,31 +303,40 @@
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: initialPath
|
||||
})
|
||||
if (
|
||||
JSON.stringify(schedule.args) != JSON.stringify(args) ||
|
||||
schedule.schedule != cron ||
|
||||
schedule.timezone != timezone ||
|
||||
schedule.summary != summary
|
||||
) {
|
||||
await ScheduleService.updateSchedule({
|
||||
if ($primaryScheduleStore) {
|
||||
const { cron, timezone, args, enabled, summary } = $primaryScheduleStore
|
||||
|
||||
if (
|
||||
JSON.stringify(schedule.args) != JSON.stringify(args) ||
|
||||
schedule.schedule != cron ||
|
||||
schedule.timezone != timezone ||
|
||||
schedule.summary != summary
|
||||
) {
|
||||
await ScheduleService.updateSchedule({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: initialPath,
|
||||
requestBody: {
|
||||
schedule: formatCron(cron),
|
||||
timezone,
|
||||
args,
|
||||
summary
|
||||
}
|
||||
})
|
||||
}
|
||||
if (enabled != schedule.enabled) {
|
||||
await ScheduleService.setScheduleEnabled({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: initialPath,
|
||||
requestBody: { enabled }
|
||||
})
|
||||
}
|
||||
} else if (scheduleExists) {
|
||||
await ScheduleService.deleteSchedule({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: initialPath,
|
||||
requestBody: {
|
||||
schedule: formatCron(cron),
|
||||
timezone,
|
||||
args,
|
||||
summary
|
||||
}
|
||||
path: $pathStore
|
||||
})
|
||||
}
|
||||
if (enabled != schedule.enabled) {
|
||||
await ScheduleService.setScheduleEnabled({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: initialPath,
|
||||
requestBody: { enabled }
|
||||
})
|
||||
}
|
||||
} else if (enabled) {
|
||||
} else if ($primaryScheduleStore && $primaryScheduleStore.enabled) {
|
||||
await createSchedule(initialPath)
|
||||
}
|
||||
|
||||
@@ -359,7 +387,8 @@
|
||||
encodeState({
|
||||
flow: $flowStore,
|
||||
path: $pathStore,
|
||||
selectedId: $selectedIdStore
|
||||
selectedId: $selectedIdStore,
|
||||
primarySchedule: $primaryScheduleStore
|
||||
})
|
||||
)
|
||||
} catch (err) {
|
||||
@@ -369,18 +398,14 @@
|
||||
}
|
||||
|
||||
const selectedIdStore = writable<string>(selectedId ?? 'settings-metadata')
|
||||
const selectedTriggerStore = writable<'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes'>(
|
||||
'webhooks'
|
||||
)
|
||||
|
||||
export function getSelectedId() {
|
||||
return $selectedIdStore
|
||||
}
|
||||
|
||||
const scheduleStore = writable<Schedule>({
|
||||
summary: undefined,
|
||||
args: {},
|
||||
cron: '',
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
enabled: false
|
||||
})
|
||||
const previewArgsStore = writable<Record<string, any>>(initialArgs)
|
||||
const scriptEditorDrawer = writable<ScriptEditorDrawer | undefined>(undefined)
|
||||
const moving = writable<{ module: FlowModule; modules: FlowModule[] } | undefined>(undefined)
|
||||
@@ -395,10 +420,14 @@
|
||||
selectedIdStore.set(selectedId)
|
||||
}
|
||||
|
||||
function selectTrigger(selectedTrigger: 'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes') {
|
||||
selectedTriggerStore.set(selectedTrigger)
|
||||
}
|
||||
|
||||
let insertButtonOpen = writable<boolean>(false)
|
||||
|
||||
setContext<FlowEditorContext>('FlowEditorContext', {
|
||||
selectedId: selectedIdStore,
|
||||
schedule: scheduleStore,
|
||||
previewArgs: previewArgsStore,
|
||||
scriptEditorDrawer,
|
||||
moving,
|
||||
@@ -414,25 +443,31 @@
|
||||
insertButtonOpen
|
||||
})
|
||||
|
||||
async function loadSchedule() {
|
||||
loadFlowSchedule(initialPath, $workspaceStore!)
|
||||
.then((schedule: Schedule) => {
|
||||
scheduleStore.set(schedule)
|
||||
})
|
||||
.catch(() => {
|
||||
scheduleStore.set({
|
||||
summary: undefined,
|
||||
cron: '0 */5 * * *',
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
args: {},
|
||||
enabled: false
|
||||
})
|
||||
})
|
||||
setContext<TriggerContext>('TriggerContext', {
|
||||
selectedTrigger: selectedTriggerStore,
|
||||
primarySchedule: primaryScheduleStore,
|
||||
triggersCount
|
||||
})
|
||||
|
||||
async function loadTriggers() {
|
||||
$triggersCount = await FlowService.getTriggersCountOfFlow({
|
||||
workspace: $workspaceStore!,
|
||||
path: initialPath
|
||||
})
|
||||
if ($primaryScheduleStore && $triggersCount.primary_schedule == undefined) {
|
||||
$triggersCount = {
|
||||
...($triggersCount ?? {}),
|
||||
schedule_count: ($triggersCount.schedule_count ?? 0) + 1,
|
||||
primary_schedule: {
|
||||
schedule: $primaryScheduleStore.cron
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: selectedId && select(selectedId)
|
||||
|
||||
$: initialPath && initialPath != '' && $workspaceStore && loadSchedule()
|
||||
$: initialPath && initialPath != '' && $workspaceStore && loadTriggers()
|
||||
|
||||
function onKeyDown(event: KeyboardEvent) {
|
||||
let classes = event.target?.['className']
|
||||
@@ -699,10 +734,13 @@
|
||||
}
|
||||
|
||||
if (module.type === 'trigger') {
|
||||
if (!$scheduleStore.cron) {
|
||||
$scheduleStore.cron = '0 */15 * * *'
|
||||
$primaryScheduleStore = {
|
||||
summary: 'Scheduled poll of flow',
|
||||
args: {},
|
||||
cron: '0 */15 * * *',
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
enabled: true
|
||||
}
|
||||
$scheduleStore.enabled = true
|
||||
}
|
||||
|
||||
const flowModule: FlowModule & {
|
||||
@@ -1152,7 +1190,7 @@
|
||||
</div>
|
||||
|
||||
<div class="gap-4 flex-row hidden md:flex w-full max-w-md">
|
||||
{#if $scheduleStore.enabled}
|
||||
{#if $primaryScheduleStore != undefined ? $primaryScheduleStore && $primaryScheduleStore?.enabled : $triggersCount?.primary_schedule}
|
||||
<Button
|
||||
btnClasses="hidden lg:inline-flex"
|
||||
startIcon={{ icon: Calendar }}
|
||||
@@ -1160,10 +1198,15 @@
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={async () => {
|
||||
select('settings-schedule')
|
||||
select('triggers')
|
||||
selectTrigger('schedules')
|
||||
}}
|
||||
>
|
||||
{$scheduleStore.cron ?? ''}
|
||||
{$primaryScheduleStore != undefined
|
||||
? $primaryScheduleStore
|
||||
? $primaryScheduleStore?.cron
|
||||
: ''
|
||||
: $triggersCount?.primary_schedule?.schedule}
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
@@ -1334,6 +1377,7 @@
|
||||
on:reload={() => {
|
||||
renderCount += 1
|
||||
}}
|
||||
{newFlow}
|
||||
/>
|
||||
{:else}
|
||||
<CenteredPage>Loading...</CenteredPage>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
description?: string
|
||||
value: FlowValue
|
||||
schema?: any
|
||||
path?: string
|
||||
}
|
||||
|
||||
export let overflowAuto = false
|
||||
@@ -32,20 +33,25 @@
|
||||
class:overflow-auto={overflowAuto}
|
||||
>
|
||||
<FlowGraphV2
|
||||
path={flow?.path}
|
||||
{download}
|
||||
minHeight={400}
|
||||
modules={flow?.value?.modules}
|
||||
failureModule={flow?.value?.failure_module}
|
||||
preprocessorModule={flow?.value?.preprocessor_module}
|
||||
on:select={(e) => {
|
||||
if (e?.detail === 'failure') {
|
||||
let nodeId = e?.detail
|
||||
if (nodeId === 'triggers') {
|
||||
dispatch('triggerDetail')
|
||||
return
|
||||
} else if (nodeId === 'failure') {
|
||||
stepDetail = flow?.value?.failure_module
|
||||
} else if (e?.detail === 'preprocessor') {
|
||||
} else if (nodeId === 'preprocessor') {
|
||||
stepDetail = flow?.value?.preprocessor_module
|
||||
} else {
|
||||
stepDetail = dfs(flow?.value?.modules ?? [], (m) => m).find((m) => m?.id === e?.detail)
|
||||
}
|
||||
stepDetail = stepDetail ?? e?.detail
|
||||
stepDetail = stepDetail ?? nodeId
|
||||
dispatch('select', stepDetail)
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -76,9 +76,8 @@
|
||||
size="xs"
|
||||
startIcon={{ icon: Clipboard }}
|
||||
btnClasses="absolute top-2 right-2 w-min z-20"
|
||||
>
|
||||
Copy content
|
||||
</Button>
|
||||
iconOnly
|
||||
/>
|
||||
|
||||
<div class={shouldDisplayLoadMore ? 'code-container' : ''}>
|
||||
<Highlight
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script lang="ts">
|
||||
export let label: string | undefined = undefined
|
||||
export let primary = false
|
||||
</script>
|
||||
|
||||
<div class={$$props.class}>
|
||||
<div class="flex flex-row justify-between items-center w-full">
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
<span class="text-secondary text-sm leading-6">{label}</span>
|
||||
<span class="{primary ? 'text-primary' : 'text-secondary'} text-sm leading-6">{label}</span>
|
||||
<slot name="header" />
|
||||
</div>
|
||||
<slot name="error" />
|
||||
|
||||
@@ -24,8 +24,6 @@
|
||||
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import FolderEditor from './FolderEditor.svelte'
|
||||
import { random_adj } from './random_positive_adjetive'
|
||||
import Required from './Required.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import { Eye, Folder, Plus, SearchCode, User } from 'lucide-svelte'
|
||||
|
||||
type PathKind =
|
||||
@@ -162,7 +160,7 @@
|
||||
.map((x) => ({
|
||||
name: x,
|
||||
write:
|
||||
($userStore?.folders?.includes(x) == true ?? false) ||
|
||||
$userStore?.folders?.includes(x) == true ||
|
||||
($userStore?.is_admin ?? false) ||
|
||||
($userStore?.is_super_admin ?? false)
|
||||
}))
|
||||
@@ -322,54 +320,54 @@
|
||||
<div>
|
||||
<div class="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4 pb-0 mb-1">
|
||||
{#if meta != undefined}
|
||||
<div class="flex gap-x-4 shrink">
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
{#if !hideUser}
|
||||
<div class="block">
|
||||
<span class="text-secondary text-sm whitespace-nowrap"> </span>
|
||||
|
||||
<ToggleButtonGroup
|
||||
class="mt-0.5"
|
||||
bind:selected={meta.ownerKind}
|
||||
on:selected={(e) => {
|
||||
setDirty()
|
||||
const kind = e.detail
|
||||
if (meta) {
|
||||
if (kind === 'folder') {
|
||||
meta.owner = folders?.[0]?.name ?? ''
|
||||
} else if (kind === 'group') {
|
||||
meta.owner = 'all'
|
||||
} else {
|
||||
meta.owner = $userStore?.username?.split('@')[0] ?? ''
|
||||
}
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
{#if !hideUser}
|
||||
<div class="block">
|
||||
<ToggleButtonGroup
|
||||
class="mt-0.5"
|
||||
bind:selected={meta.ownerKind}
|
||||
on:selected={(e) => {
|
||||
setDirty()
|
||||
const kind = e.detail
|
||||
if (meta) {
|
||||
if (kind === 'folder') {
|
||||
meta.owner = folders?.[0]?.name ?? ''
|
||||
} else if (kind === 'group') {
|
||||
meta.owner = 'all'
|
||||
} else {
|
||||
meta.owner = $userStore?.username?.split('@')[0] ?? ''
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ToggleButton
|
||||
icon={User}
|
||||
{disabled}
|
||||
light
|
||||
size="xs"
|
||||
value="user"
|
||||
position="left"
|
||||
label="User"
|
||||
/>
|
||||
<!-- <ToggleButton light size="xs" value="group" position="center">Group</ToggleButton> -->
|
||||
<ToggleButton
|
||||
icon={Folder}
|
||||
{disabled}
|
||||
light
|
||||
size="xs"
|
||||
value="folder"
|
||||
position="right"
|
||||
label="Folder"
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
{/if}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ToggleButton
|
||||
icon={User}
|
||||
{disabled}
|
||||
light
|
||||
size="xs"
|
||||
value="user"
|
||||
position="left"
|
||||
label="User"
|
||||
/>
|
||||
<!-- <ToggleButton light size="xs" value="group" position="center">Group</ToggleButton> -->
|
||||
<ToggleButton
|
||||
icon={Folder}
|
||||
{disabled}
|
||||
light
|
||||
size="xs"
|
||||
value="folder"
|
||||
position="right"
|
||||
label="Folder"
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
{/if}
|
||||
{#if !hideUser}
|
||||
<div class="text-xl">/</div>
|
||||
{/if}
|
||||
<div>
|
||||
{#if meta.ownerKind === 'user'}
|
||||
<label class="block shrink min-w-0">
|
||||
<span class="text-secondary text-sm">User</span>
|
||||
<input
|
||||
class="!w-36"
|
||||
type="text"
|
||||
@@ -381,16 +379,6 @@
|
||||
</label>
|
||||
{:else if meta.ownerKind === 'folder'}
|
||||
<label class="block grow w-48">
|
||||
<span class="text-secondary text-sm">
|
||||
Folder
|
||||
<Tooltip
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/groups_and_folders"
|
||||
>
|
||||
Read and write permissions are given to groups and users at the folder level and
|
||||
shared by all items inside the folder.
|
||||
</Tooltip>
|
||||
</span>
|
||||
|
||||
<div class="flex flex-row items-center gap-1 w-full">
|
||||
<select class="grow w-full" {disabled} bind:value={meta.owner}>
|
||||
{#if folders?.length == 0}
|
||||
@@ -426,14 +414,8 @@
|
||||
</label>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="text-xl">/</span>
|
||||
<label class="block grow w-full max-w-md">
|
||||
<div class="text-secondary text-sm flex items-center gap-1 w-full justify-between">
|
||||
<div>
|
||||
Name
|
||||
<Required required={true} />
|
||||
</div>
|
||||
<div class="text-2xs text-tertiary"> '/' for subfolders </div>
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
{disabled}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<script lang="ts">
|
||||
import { Badge, Button } from './common'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { base } from '$lib/base'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import { ListOrdered, PenBox } from 'lucide-svelte'
|
||||
import JobArgs from './JobArgs.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import ScheduleEditor from './ScheduleEditor.svelte'
|
||||
import { type Schedule } from '$lib/gen'
|
||||
|
||||
export let schedule: any
|
||||
export let can_write: boolean
|
||||
export let path: string
|
||||
export let isFlow: boolean
|
||||
export let scheduleEditor: ScheduleEditor
|
||||
export let setScheduleEnabled: (path: string, enabled: boolean) => void
|
||||
|
||||
$: schedule = typeof schedule === 'boolean' ? undefined : (schedule as Schedule | undefined)
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-2 grow w-full">
|
||||
<div class="grid grid-cols-3 w-full">
|
||||
<div class="flex justify-start">
|
||||
<Badge color="indigo" small>
|
||||
Primary
|
||||
<Tooltip light>
|
||||
Share the same path as the script or flow it is attached to and its path get renamed
|
||||
whenever the source path is renamed
|
||||
</Tooltip>
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<input
|
||||
size="9"
|
||||
class="!text-xs !h-6 !text-primary"
|
||||
type="text"
|
||||
id="cron-schedule"
|
||||
name="cron-schedule"
|
||||
placeholder="*/30 * * * *"
|
||||
value={schedule?.schedule ?? ''}
|
||||
disabled={true}
|
||||
/>
|
||||
<Toggle
|
||||
checked={schedule?.enabled ?? false}
|
||||
on:change={(e) => {
|
||||
if (can_write) {
|
||||
setScheduleEnabled(path, e.detail)
|
||||
} else {
|
||||
sendUserToast('not enough permission', true)
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: 'On'
|
||||
}}
|
||||
size="xs"
|
||||
textClass="text-primary font-normal text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<Button size={'xs'} variant="border" color="light" href={`${base}/runs/${path}`}>
|
||||
<span>Runs</span>
|
||||
<ListOrdered size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
color="dark"
|
||||
on:click={() => scheduleEditor?.openEdit(path ?? '', isFlow)}
|
||||
>
|
||||
<PenBox size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if Object.keys(schedule?.args ?? {}).length > 0}
|
||||
<div class="">
|
||||
<JobArgs args={schedule?.args ?? {}} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-xs text-tertiary"> No arguments </div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,178 +1,294 @@
|
||||
<script lang="ts">
|
||||
import ScheduleEditor from './ScheduleEditor.svelte'
|
||||
import { Badge, Button } from './common'
|
||||
import { Button } from './common'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { ScheduleService, type Schedule } from '$lib/gen'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { base } from '$lib/base'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import { ListOrdered, Calendar, PenBox } from 'lucide-svelte'
|
||||
import JobArgs from './JobArgs.svelte'
|
||||
import { Calendar, Trash, Save } from 'lucide-svelte'
|
||||
import Skeleton from './common/skeleton/Skeleton.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import type { ScheduleTrigger, TriggerContext } from './triggers'
|
||||
import CronInput from './CronInput.svelte'
|
||||
import SchemaForm from './SchemaForm.svelte'
|
||||
import { emptyString, sendUserToast } from '$lib/utils'
|
||||
import Toggle from './Toggle.svelte'
|
||||
|
||||
export let schema: any
|
||||
export let isFlow: boolean
|
||||
export let path: string
|
||||
export let can_write: boolean
|
||||
export let newItem: boolean = false
|
||||
|
||||
const { primarySchedule, triggersCount } = getContext<TriggerContext>('TriggerContext')
|
||||
|
||||
let scheduleEditor: ScheduleEditor
|
||||
|
||||
let schedule: Schedule | false | undefined = undefined
|
||||
$: loadSchedules(false) || path
|
||||
|
||||
let schedules: Schedule[] | undefined = undefined
|
||||
|
||||
$: path && loadSchedule()
|
||||
$: path && loadSchedules()
|
||||
|
||||
async function loadSchedule() {
|
||||
try {
|
||||
let exists = await ScheduleService.existsSchedule({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path
|
||||
})
|
||||
if (exists) {
|
||||
schedule = await ScheduleService.getSchedule({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path
|
||||
})
|
||||
} else {
|
||||
schedule = false
|
||||
let initialPrimarySchedule: ScheduleTrigger | false | undefined = undefined
|
||||
async function loadSchedules(forceRefresh: boolean) {
|
||||
if (!path || path == '') {
|
||||
schedules = []
|
||||
if ($primarySchedule == undefined) {
|
||||
$primarySchedule = false
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('no primary schedule')
|
||||
initialPrimarySchedule = structuredClone($primarySchedule)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSchedules() {
|
||||
try {
|
||||
schedules = (
|
||||
await ScheduleService.listSchedules({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path,
|
||||
isFlow
|
||||
})
|
||||
).filter((s) => s.path != path)
|
||||
} catch (e) {
|
||||
console.error('impossible to load schedules')
|
||||
}
|
||||
}
|
||||
|
||||
async function setScheduleEnabled(path: string, enabled: boolean): Promise<void> {
|
||||
try {
|
||||
await ScheduleService.setScheduleEnabled({
|
||||
path,
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: { enabled }
|
||||
const allSchedules = await ScheduleService.listSchedules({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: path,
|
||||
isFlow
|
||||
})
|
||||
loadSchedule()
|
||||
const primary = allSchedules.find((s) => s.path == path)
|
||||
let remotePrimarySchedule = primary
|
||||
? {
|
||||
summary: primary.summary,
|
||||
args: primary.args ?? {},
|
||||
cron: primary.schedule,
|
||||
timezone: primary.timezone,
|
||||
enabled: primary.enabled
|
||||
}
|
||||
: false
|
||||
if ($primarySchedule == undefined || forceRefresh) {
|
||||
$primarySchedule = remotePrimarySchedule
|
||||
}
|
||||
initialPrimarySchedule = structuredClone(remotePrimarySchedule)
|
||||
|
||||
sendUserToast(`Schedule ${enabled ? 'enabled' : 'disabled'}`)
|
||||
} catch (err) {
|
||||
sendUserToast(`Cannot ` + (enabled ? 'disable' : 'enable') + ` schedule: ${err}`, true)
|
||||
loadSchedule()
|
||||
$triggersCount = {
|
||||
...($triggersCount ?? {}),
|
||||
schedule_count: allSchedules.length,
|
||||
primary_schedule: $primarySchedule ? { schedule: $primarySchedule.cron } : undefined
|
||||
}
|
||||
schedules = allSchedules.filter((s) => s.path != path)
|
||||
} catch (e) {
|
||||
console.error('impossible to load schedules', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function save() {
|
||||
const scheduleExists =
|
||||
path != '' &&
|
||||
!newItem &&
|
||||
(await ScheduleService.existsSchedule({
|
||||
workspace: $workspaceStore!,
|
||||
path
|
||||
}))
|
||||
if (scheduleExists) {
|
||||
console.log('primary schedule exists')
|
||||
if ($primarySchedule) {
|
||||
await ScheduleService.updateSchedule({
|
||||
workspace: $workspaceStore!,
|
||||
path,
|
||||
requestBody: {
|
||||
summary: $primarySchedule.summary,
|
||||
args: $primarySchedule.args,
|
||||
schedule: $primarySchedule.cron,
|
||||
timezone: $primarySchedule.timezone
|
||||
}
|
||||
})
|
||||
sendUserToast(`Primary schedule updated`)
|
||||
} else {
|
||||
await ScheduleService.deleteSchedule({ workspace: $workspaceStore!, path })
|
||||
sendUserToast(`Primary schedule deleted`)
|
||||
}
|
||||
} else {
|
||||
if ($primarySchedule) {
|
||||
await ScheduleService.createSchedule({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
path,
|
||||
script_path: path,
|
||||
is_flow: isFlow,
|
||||
summary: $primarySchedule.summary,
|
||||
args: $primarySchedule.args,
|
||||
schedule: $primarySchedule.cron,
|
||||
timezone: $primarySchedule.timezone,
|
||||
enabled: $primarySchedule.enabled
|
||||
}
|
||||
})
|
||||
sendUserToast(`Primary schedule created`)
|
||||
}
|
||||
}
|
||||
loadSchedules(true)
|
||||
}
|
||||
</script>
|
||||
|
||||
<ScheduleEditor
|
||||
on:update={() => {
|
||||
loadSchedule()
|
||||
loadSchedules()
|
||||
loadSchedules(true)
|
||||
}}
|
||||
bind:this={scheduleEditor}
|
||||
/>
|
||||
|
||||
<div class="p-2 flex flex-col">
|
||||
<Button
|
||||
on:click={() => scheduleEditor?.openNew(isFlow, path)}
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
startIcon={{ icon: Calendar }}
|
||||
>
|
||||
New Schedule
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4 w-full">
|
||||
{#if $primarySchedule == undefined}
|
||||
<Skeleton layout={[[12]]} />
|
||||
{:else if $primarySchedule}
|
||||
<div class="w-full flex flex-col mb-4">
|
||||
{#if can_write}
|
||||
<div class="w-full flex-row-reverse flex mb-2">
|
||||
<div class="flex flex-row gap-4">
|
||||
<Button
|
||||
on:click={() => {
|
||||
$primarySchedule = false
|
||||
$triggersCount = {
|
||||
...($triggersCount ?? {}),
|
||||
schedule_count: ($triggersCount?.schedule_count ?? 1) - 1,
|
||||
primary_schedule: undefined
|
||||
}
|
||||
}}
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
startIcon={{ icon: Trash }}
|
||||
/>
|
||||
{#if initialPrimarySchedule && !newItem}
|
||||
<Toggle
|
||||
disabled={emptyString($primarySchedule.cron)}
|
||||
bind:checked={$primarySchedule.enabled}
|
||||
options={{
|
||||
right: 'Enabled'
|
||||
}}
|
||||
on:change={async (e) => {
|
||||
if (!newItem) {
|
||||
await ScheduleService.setScheduleEnabled({
|
||||
path: path,
|
||||
workspace: $workspaceStore ?? '',
|
||||
requestBody: { enabled: e.detail }
|
||||
})
|
||||
|
||||
{#if schedule}
|
||||
<div class="p-2 flex flex-col gap-2">
|
||||
<div class="flex flex-row justify-between h-8">
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<input
|
||||
class="inline-block !w-32"
|
||||
type="text"
|
||||
id="cron-schedule"
|
||||
name="cron-schedule"
|
||||
placeholder="*/30 * * * *"
|
||||
value={schedule.schedule}
|
||||
disabled={true}
|
||||
/>
|
||||
<div>
|
||||
<Badge color="indigo" small
|
||||
>Primary <Tooltip light
|
||||
>Share the same path as the script or flow it is attached to and its path get renamed
|
||||
whenever the source path is renamed</Tooltip
|
||||
></Badge
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<Toggle
|
||||
checked={schedule.enabled}
|
||||
on:change={(e) => {
|
||||
if (can_write) {
|
||||
setScheduleEnabled(path, e.detail)
|
||||
} else {
|
||||
sendUserToast('not enough permission', true)
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: 'On'
|
||||
}}
|
||||
size="xs"
|
||||
/>
|
||||
<Button size="xs" variant="border" color="light" href={`${base}/runs/${path}`}>
|
||||
<div class="flex flex-row gap-2">
|
||||
<ListOrdered size={14} />
|
||||
Runs
|
||||
sendUserToast(`${e.detail ? 'enabled' : 'disabled'} schedule ${path}`)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if !newItem}
|
||||
<Button
|
||||
on:click={save}
|
||||
color="dark"
|
||||
size="sm"
|
||||
startIcon={{ icon: Save }}
|
||||
disabled={JSON.stringify({ ...$primarySchedule, enabled: true }) ==
|
||||
JSON.stringify({ ...initialPrimarySchedule, enabled: true })}
|
||||
>Apply changes now</Button
|
||||
>
|
||||
{:else}
|
||||
<div class="text-sm text-secondary mt-1 text-center"
|
||||
>Deployed automatically with {isFlow ? 'flow' : 'script'}</div
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
color="dark"
|
||||
on:click={() => scheduleEditor?.openEdit(path ?? '', isFlow)}
|
||||
>
|
||||
<PenBox size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{#if Object.keys(schedule?.args ?? {}).length > 0}
|
||||
<div class="">
|
||||
<JobArgs args={schedule.args ?? {}} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-xs texg-gray-700"> No arguments </div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if schedule == undefined}
|
||||
<Skeleton layout={[[6]]} />
|
||||
{/if}
|
||||
|
||||
<h3 class="px-2 pt-4">Other schedules</h3>
|
||||
|
||||
{#if schedules}
|
||||
{#if schedules.length == 0}
|
||||
<div class="text-xs text-secondary px-2"> No other schedules </div>
|
||||
{:else}
|
||||
<div class="flex flex-col divide-y px-2 pt-2">
|
||||
{#each schedules as schedule (schedule.path)}
|
||||
<div class="grid grid-cols-6 text-2xs items-center py-2"
|
||||
><div class="col-span-3 truncate">{schedule.path}</div><div>{schedule.schedule}</div>
|
||||
<div>{schedule.enabled ? 'on' : 'off'}</div>
|
||||
<button on:click={() => scheduleEditor?.openEdit(schedule.path, isFlow)}>Edit</button>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<Label label="Summary" class="font-semibold" primary>
|
||||
<input
|
||||
autofocus
|
||||
type="text"
|
||||
placeholder="Short summary to be displayed when listed"
|
||||
class="text-sm w-full"
|
||||
bind:value={$primarySchedule.summary}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
<CronInput bind:schedule={$primarySchedule.cron} bind:timezone={$primarySchedule.timezone} />
|
||||
<SchemaForm {schema} bind:args={$primarySchedule.args} />
|
||||
{#if emptyString($primarySchedule.cron)}
|
||||
<p class="text-xs text-tertiary mt-10">Define a schedule frequency first</p>
|
||||
{/if}
|
||||
|
||||
{#if initialPrimarySchedule != false}
|
||||
<div class="flex">
|
||||
<Button size="xs" color="light" on:click={() => scheduleEditor?.openEdit(path, isFlow)}
|
||||
>Advanced</Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex flex-row gap-4 mt-2">
|
||||
<Button
|
||||
on:click={() => {
|
||||
$primarySchedule = {
|
||||
summary: '',
|
||||
args: {},
|
||||
cron: '0 0 0 /1 * * *',
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
enabled: true
|
||||
}
|
||||
$triggersCount = {
|
||||
...($triggersCount ?? {}),
|
||||
schedule_count: ($triggersCount?.schedule_count ?? 0) + 1,
|
||||
primary_schedule: { schedule: $primarySchedule.cron }
|
||||
}
|
||||
}}
|
||||
variant="contained"
|
||||
color="dark"
|
||||
size="md"
|
||||
startIcon={{ icon: Calendar }}
|
||||
>
|
||||
Set Primary Schedule
|
||||
</Button>
|
||||
{#if initialPrimarySchedule != false && !newItem}
|
||||
<Button on:click={save} color="dark" size="md" startIcon={{ icon: Save }}>
|
||||
Apply changes now
|
||||
</Button>
|
||||
{:else}
|
||||
<div class="text-sm text-center text-secondary mt-2"
|
||||
>Deployed automatically with {isFlow ? 'flow' : 'script'}</div
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<CronInput schedule={''} disabled timezone={Intl.DateTimeFormat().resolvedOptions().timeZone} />
|
||||
|
||||
<SchemaForm disabled {schema} />
|
||||
{/if}
|
||||
{:else}
|
||||
<Skeleton layout={[[8]]} />
|
||||
{/if}
|
||||
|
||||
{#if !newItem}
|
||||
<div class="mt-10" />
|
||||
{#if $primarySchedule}
|
||||
<Button
|
||||
on:click={() => scheduleEditor?.openNew(isFlow, path)}
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
startIcon={{ icon: Calendar }}
|
||||
>
|
||||
New Schedule
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
<Label label="Other schedules">
|
||||
{#if schedules}
|
||||
{#if schedules?.length == 0 || schedules == undefined}
|
||||
<div class="text-xs text-tertiary"> No other schedules </div>
|
||||
{:else}
|
||||
<div class="flex flex-col divide-y">
|
||||
{#each schedules as schedule (schedule.path)}
|
||||
<div class="grid grid-cols-6 text-xs items-center py-2">
|
||||
<div class="col-span-3 truncate">{schedule.path}</div>
|
||||
<div class="col-span-2 flex flex-row gap-4 flex-nowrap">
|
||||
<div>{schedule.schedule}</div>
|
||||
<div>{schedule.enabled ? 'on' : 'off'}</div>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
on:click={() => scheduleEditor?.openEdit(schedule.path, isFlow)}
|
||||
class="px-2">Edit</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<Skeleton layout={[[8]]} />
|
||||
{/if}
|
||||
</Label>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -520,13 +520,14 @@
|
||||
<Toggle
|
||||
disabled={!can_write}
|
||||
checked={enabled}
|
||||
options={{ right: 'enable', left: 'disable' }}
|
||||
options={{ right: 'Enabled' }}
|
||||
on:change={async (e) => {
|
||||
await ScheduleService.setScheduleEnabled({
|
||||
path: initialPath,
|
||||
workspace: $workspaceStore ?? '',
|
||||
requestBody: { enabled: e.detail }
|
||||
})
|
||||
dispatch('update')
|
||||
sendUserToast(`${e.detail ? 'enabled' : 'disabled'} schedule ${initialPath}`)
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { Schema } from '$lib/common'
|
||||
import { emptySchema } from '$lib/utils'
|
||||
import { copyToClipboard, emptySchema } from '$lib/utils'
|
||||
|
||||
import Highlight from 'svelte-highlight'
|
||||
import json from 'svelte-highlight/languages/json'
|
||||
@@ -10,6 +10,8 @@
|
||||
import Tabs from './common/tabs/Tabs.svelte'
|
||||
import { Badge } from './common'
|
||||
import HighlightTheme from './HighlightTheme.svelte'
|
||||
import Button from './common/button/Button.svelte'
|
||||
import { ClipboardCopy } from 'lucide-svelte'
|
||||
|
||||
export let schema: Schema | undefined | any = emptySchema()
|
||||
|
||||
@@ -79,7 +81,19 @@
|
||||
{/if}
|
||||
</TabContent>
|
||||
<TabContent value="advanced">
|
||||
<Highlight language={json} code={JSON.stringify(schema, null, 4)} />
|
||||
<div class="h-full relative">
|
||||
<Button
|
||||
wrapperClasses="absolute top-2 right-2 z-20"
|
||||
on:click={() => copyToClipboard(JSON.stringify(schema, null, 4))}
|
||||
color="light"
|
||||
size="xs2"
|
||||
startIcon={{
|
||||
icon: ClipboardCopy
|
||||
}}
|
||||
iconOnly
|
||||
/>
|
||||
<Highlight language={json} code={JSON.stringify(schema, null, 4)} />
|
||||
</div>
|
||||
</TabContent>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
ScriptService,
|
||||
type NewScriptWithDraft,
|
||||
ScheduleService,
|
||||
type Script
|
||||
type Script,
|
||||
type TriggersCount
|
||||
} from '$lib/gen'
|
||||
import { inferArgs } from '$lib/infer'
|
||||
import { initialCode } from '$lib/script_helpers'
|
||||
@@ -55,19 +56,15 @@
|
||||
import type Editor from './Editor.svelte'
|
||||
import WorkerTagPicker from './WorkerTagPicker.svelte'
|
||||
import MetadataGen from './copilot/MetadataGen.svelte'
|
||||
import ScriptSchedules from './ScriptSchedules.svelte'
|
||||
import { writable } from 'svelte/store'
|
||||
import {
|
||||
type ScriptSchedule,
|
||||
loadScriptSchedule,
|
||||
defaultScriptLanguages,
|
||||
processLangs
|
||||
} from '$lib/scripts'
|
||||
import { defaultScriptLanguages, processLangs } from '$lib/scripts'
|
||||
import DefaultScripts from './DefaultScripts.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { createEventDispatcher, setContext } from 'svelte'
|
||||
import CustomPopover from './CustomPopover.svelte'
|
||||
import Summary from './Summary.svelte'
|
||||
import type { ScriptBuilderWhitelabelCustomUi } from './custom_ui'
|
||||
import TriggersEditor from './triggers/TriggersEditor.svelte'
|
||||
import type { ScheduleTrigger, TriggerContext } from './triggers'
|
||||
|
||||
export let script: NewScript
|
||||
export let fullyLoaded: boolean = true
|
||||
@@ -84,6 +81,7 @@
|
||||
export let replaceStateFn: (url: string) => void = (url) =>
|
||||
window.history.replaceState(null, '', url)
|
||||
export let customUi: ScriptBuilderWhitelabelCustomUi = {}
|
||||
export let savedPrimarySchedule: ScheduleTrigger | undefined = undefined
|
||||
|
||||
let metadataOpen =
|
||||
!neverShowMeta &&
|
||||
@@ -95,29 +93,47 @@
|
||||
let editor: Editor | undefined = undefined
|
||||
let scriptEditor: ScriptEditor | undefined = undefined
|
||||
|
||||
let scheduleStore = writable<ScriptSchedule>({
|
||||
summary: '',
|
||||
cron: '0 */5 * * *',
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
args: {},
|
||||
enabled: false
|
||||
})
|
||||
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(savedPrimarySchedule)
|
||||
const triggersCount = writable<TriggersCount | undefined>(
|
||||
savedPrimarySchedule
|
||||
? { schedule_count: 1, primary_schedule: { schedule: savedPrimarySchedule.cron } }
|
||||
: undefined
|
||||
)
|
||||
const selectedTriggerStore = writable<'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes'>(
|
||||
'webhooks'
|
||||
)
|
||||
|
||||
async function loadSchedule() {
|
||||
const scheduleRes = await loadScriptSchedule(initialPath, $workspaceStore!)
|
||||
if (scheduleRes) {
|
||||
scheduleStore.set(scheduleRes)
|
||||
}
|
||||
export function setPrimarySchedule(schedule: ScheduleTrigger | undefined | false) {
|
||||
primaryScheduleStore.set(schedule)
|
||||
loadTriggers()
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: {
|
||||
if (initialPath != '') {
|
||||
loadSchedule()
|
||||
$: initialPath != '' && loadTriggers()
|
||||
|
||||
async function loadTriggers() {
|
||||
$triggersCount = await ScriptService.getTriggersCountOfScript({
|
||||
workspace: $workspaceStore!,
|
||||
path: initialPath
|
||||
})
|
||||
if ($primaryScheduleStore && $triggersCount.primary_schedule == undefined) {
|
||||
$triggersCount = {
|
||||
...($triggersCount ?? {}),
|
||||
schedule_count: ($triggersCount.schedule_count ?? 0) + 1,
|
||||
primary_schedule: {
|
||||
schedule: $primaryScheduleStore.cron
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setContext<TriggerContext>('TriggerContext', {
|
||||
selectedTrigger: selectedTriggerStore,
|
||||
primarySchedule: primaryScheduleStore,
|
||||
triggersCount
|
||||
})
|
||||
|
||||
const enterpriseLangs = ['bigquery', 'snowflake', 'mssql']
|
||||
|
||||
export function setCode(code: string): void {
|
||||
@@ -180,7 +196,8 @@
|
||||
})
|
||||
}
|
||||
|
||||
$: !disableHistoryChange && replaceStateFn('#' + encodeState(script))
|
||||
$: !disableHistoryChange &&
|
||||
replaceStateFn('#' + encodeState({ script, primarySchedule: $primaryScheduleStore }))
|
||||
|
||||
if (script.content == '') {
|
||||
initContent(script.language, script.kind, template)
|
||||
@@ -200,7 +217,10 @@
|
||||
}
|
||||
|
||||
async function createSchedule(path: string) {
|
||||
const { cron, timezone, args, enabled, summary } = $scheduleStore
|
||||
if (!$primaryScheduleStore) {
|
||||
return
|
||||
}
|
||||
const { cron, timezone, args, enabled, summary } = $primaryScheduleStore
|
||||
|
||||
try {
|
||||
await ScheduleService.createSchedule({
|
||||
@@ -269,43 +289,53 @@
|
||||
}
|
||||
})
|
||||
|
||||
const { enabled, timezone, args, cron, summary } = $scheduleStore
|
||||
const scheduleExists = await ScheduleService.existsSchedule({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: script.path
|
||||
})
|
||||
console.log('initialPath', initialPath)
|
||||
const scheduleExists =
|
||||
initialPath != '' &&
|
||||
(await ScheduleService.existsSchedule({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: script.path
|
||||
}))
|
||||
if ($primaryScheduleStore) {
|
||||
const { enabled, timezone, args, cron, summary } = $primaryScheduleStore
|
||||
|
||||
if (scheduleExists) {
|
||||
const schedule = await ScheduleService.getSchedule({
|
||||
if (scheduleExists) {
|
||||
const schedule = await ScheduleService.getSchedule({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: script.path
|
||||
})
|
||||
if (
|
||||
JSON.stringify(schedule.args) != JSON.stringify(args) ||
|
||||
schedule.schedule != cron ||
|
||||
schedule.timezone != timezone ||
|
||||
schedule.summary != summary
|
||||
) {
|
||||
await ScheduleService.updateSchedule({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: script.path,
|
||||
requestBody: {
|
||||
schedule: formatCron(cron),
|
||||
timezone,
|
||||
args,
|
||||
summary
|
||||
}
|
||||
})
|
||||
}
|
||||
if (enabled != schedule.enabled) {
|
||||
await ScheduleService.setScheduleEnabled({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: script.path,
|
||||
requestBody: { enabled }
|
||||
})
|
||||
}
|
||||
} else if (enabled) {
|
||||
await createSchedule(script.path)
|
||||
}
|
||||
} else if (scheduleExists) {
|
||||
await ScheduleService.deleteSchedule({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: script.path
|
||||
})
|
||||
if (
|
||||
JSON.stringify(schedule.args) != JSON.stringify(args) ||
|
||||
schedule.schedule != cron ||
|
||||
schedule.timezone != timezone ||
|
||||
schedule.summary != summary
|
||||
) {
|
||||
await ScheduleService.updateSchedule({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: script.path,
|
||||
requestBody: {
|
||||
schedule: formatCron(cron),
|
||||
timezone,
|
||||
args,
|
||||
summary
|
||||
}
|
||||
})
|
||||
}
|
||||
if (enabled != schedule.enabled) {
|
||||
await ScheduleService.setScheduleEnabled({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: script.path,
|
||||
requestBody: { enabled }
|
||||
})
|
||||
}
|
||||
} else if (enabled) {
|
||||
await createSchedule(script.path)
|
||||
}
|
||||
|
||||
savedScript = structuredClone(script) as NewScriptWithDraft
|
||||
@@ -405,7 +435,7 @@
|
||||
requestBody: {
|
||||
path: initialPath == '' || savedScript?.draft_only ? script.path : initialPath,
|
||||
typ: 'script',
|
||||
value: script
|
||||
value: { ...script, primary_schedule: $primaryScheduleStore }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -481,7 +511,7 @@
|
||||
let path: Path | undefined = undefined
|
||||
let dirtyPath = false
|
||||
|
||||
let selectedTab: 'metadata' | 'runtime' | 'ui' | 'schedule' = 'metadata'
|
||||
let selectedTab: 'metadata' | 'runtime' | 'ui' | 'triggers' = 'metadata'
|
||||
|
||||
let deploymentMsg = ''
|
||||
let msgInput: HTMLInputElement | undefined = undefined
|
||||
@@ -520,7 +550,7 @@
|
||||
cannot be inferred from the type directly.
|
||||
</Tooltip>
|
||||
</Tab>
|
||||
<Tab value="schedule" active={$scheduleStore.enabled}>Schedule</Tab>
|
||||
<Tab value="triggers">Triggers</Tab>
|
||||
<svelte:fragment slot="content">
|
||||
<div
|
||||
class={selectedTab === 'ui' ? 'p-0' : 'p-4'}
|
||||
@@ -1026,8 +1056,16 @@
|
||||
<TabContent value="ui" class="h-full">
|
||||
<ScriptSchema bind:schema={script.schema} />
|
||||
</TabContent>
|
||||
<TabContent value="schedule">
|
||||
<ScriptSchedules {initialPath} schema={script.schema} schedule={scheduleStore} />
|
||||
<TabContent value="triggers">
|
||||
<TriggersEditor
|
||||
{initialPath}
|
||||
schema={script.schema}
|
||||
noEditor={true}
|
||||
isFlow={false}
|
||||
currentPath={script.path}
|
||||
newItem={initialPath == ''}
|
||||
/>
|
||||
<!-- <ScriptSchedules {initialPath} schema={script.schema} schedule={scheduleStore} /> -->
|
||||
</TabContent>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
@@ -1056,7 +1094,7 @@
|
||||
</div>
|
||||
|
||||
<div class="gap-4 flex">
|
||||
{#if $scheduleStore.enabled}
|
||||
{#if $primaryScheduleStore && $primaryScheduleStore?.enabled}
|
||||
<Button
|
||||
btnClasses="hidden lg:inline-flex"
|
||||
startIcon={{ icon: Calendar }}
|
||||
@@ -1065,10 +1103,11 @@
|
||||
size="xs"
|
||||
on:click={async () => {
|
||||
metadataOpen = true
|
||||
selectedTab = 'schedule'
|
||||
selectedTab = 'triggers'
|
||||
$selectedTriggerStore = 'schedules'
|
||||
}}
|
||||
>
|
||||
{$scheduleStore.cron ?? ''}
|
||||
{$primaryScheduleStore?.cron ?? ''}
|
||||
</Button>
|
||||
{/if}
|
||||
{#if customUi?.topBar?.path != false}
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
let args: Record<string, any> = initialArgs
|
||||
|
||||
let isValid: boolean = true
|
||||
let scriptProgress = undefined;
|
||||
let scriptProgress = undefined
|
||||
|
||||
// Test
|
||||
let testIsLoading = false
|
||||
@@ -106,7 +106,7 @@
|
||||
|
||||
function runTest() {
|
||||
// Not defined if JobProgressBar not loaded
|
||||
if (jobProgressReset) jobProgressReset();
|
||||
if (jobProgressReset) jobProgressReset()
|
||||
//@ts-ignore
|
||||
testJobLoader.runPreview(
|
||||
path,
|
||||
@@ -155,6 +155,7 @@
|
||||
|
||||
setLicense()
|
||||
export async function setCollaborationMode() {
|
||||
await setLicense()
|
||||
if (!$enterpriseLicense) {
|
||||
sendUserToast(`Multiplayer is an enterprise feature`, true, [
|
||||
{
|
||||
@@ -177,7 +178,7 @@
|
||||
|
||||
wsProvider = new WebsocketProvider(
|
||||
`${wsProtocol}://${window.location.host}/ws_mp/`,
|
||||
$workspaceStore + '/' + path ?? 'no-room-name',
|
||||
$workspaceStore + '/' + (path ?? 'no-room-name'),
|
||||
ydoc,
|
||||
{ connect: false }
|
||||
)
|
||||
@@ -432,10 +433,15 @@
|
||||
{diffEditor}
|
||||
{args}
|
||||
>
|
||||
{#if scriptProgress}
|
||||
<!-- Put to the slot in logpanel -->
|
||||
<JobProgressBar job={testJob} bind:scriptProgress bind:reset={jobProgressReset} compact={true} />
|
||||
{/if}
|
||||
{#if scriptProgress}
|
||||
<!-- Put to the slot in logpanel -->
|
||||
<JobProgressBar
|
||||
job={testJob}
|
||||
bind:scriptProgress
|
||||
bind:reset={jobProgressReset}
|
||||
compact={true}
|
||||
/>
|
||||
{/if}
|
||||
</LogPanel>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
<script lang="ts">
|
||||
import CronInput from '$lib/components/CronInput.svelte'
|
||||
import SchemaForm from '$lib/components/SchemaForm.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import { emptyString } from '$lib/utils'
|
||||
import { Alert, Button, Skeleton } from '$lib/components/common'
|
||||
import ScheduleEditor from '$lib/components/ScheduleEditor.svelte'
|
||||
import { ScheduleService, type Schedule } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { Calendar } from 'lucide-svelte'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import type { ScriptSchedule } from '$lib/scripts'
|
||||
import Label from './Label.svelte'
|
||||
|
||||
export let initialPath: string
|
||||
export let schema: Record<string, any> | undefined
|
||||
export let schedule: Writable<ScriptSchedule>
|
||||
// const { schedule, flowStore, pathStore, initialPath } =
|
||||
// getContext<FlowEditorContext>('FlowEditorContext')
|
||||
let schedules: Schedule[] | undefined = undefined
|
||||
|
||||
async function loadSchedules() {
|
||||
try {
|
||||
schedules = (
|
||||
await ScheduleService.listSchedules({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: initialPath,
|
||||
isFlow: false
|
||||
})
|
||||
).filter((s) => s.path != initialPath)
|
||||
} catch (e) {
|
||||
console.error('impossible to load schedules')
|
||||
}
|
||||
}
|
||||
|
||||
let scheduleEditor: ScheduleEditor
|
||||
|
||||
$: initialPath && loadSchedules()
|
||||
</script>
|
||||
|
||||
<div class="w-full flex flex-col gap-4 mb-4">
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<Label label="Summary">
|
||||
<input
|
||||
autofocus
|
||||
type="text"
|
||||
placeholder="Short summary to be displayed when listed"
|
||||
class="text-sm w-full"
|
||||
bind:value={$schedule.summary}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<CronInput bind:schedule={$schedule.cron} bind:timezone={$schedule.timezone} />
|
||||
<div class="mt-10" />
|
||||
<SchemaForm {schema} bind:args={$schedule.args} />
|
||||
{#if emptyString($schedule.cron)}
|
||||
<p class="text-xs text-tertiary mt-10">Define a schedule frequency first</p>
|
||||
{/if}
|
||||
<div class="mt-10" />
|
||||
<Toggle
|
||||
disabled={emptyString($schedule.cron)}
|
||||
bind:checked={$schedule.enabled}
|
||||
options={{
|
||||
right: 'Schedule enabled'
|
||||
}}
|
||||
/>
|
||||
<Alert bgClass="my-4" type="warning" title="Changes only applied upon deploy">
|
||||
Changes to the primary schedule are only applied upon deploy. Other schedules' changes are applied
|
||||
immediately.
|
||||
</Alert>
|
||||
|
||||
{#if initialPath != ''}
|
||||
<ScheduleEditor
|
||||
on:update={() => {
|
||||
loadSchedules()
|
||||
}}
|
||||
bind:this={scheduleEditor}
|
||||
/>
|
||||
<h2 class="pt-7">Other schedules</h2>
|
||||
<div class="py-4 flex">
|
||||
<Button
|
||||
on:click={() => scheduleEditor?.openNew(false, initialPath)}
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
startIcon={{ icon: Calendar }}
|
||||
>
|
||||
New Schedule
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if schedules}
|
||||
{#if schedules.length == 0}
|
||||
<div class="text-xs text-secondary px-2"> No other schedules </div>
|
||||
{:else}
|
||||
<div class="flex flex-col divide-y px-2 pt-2 max-w-lg">
|
||||
{#each schedules as schedule (schedule.path)}
|
||||
<div class="grid grid-cols-6 text-2xs items-center py-2"
|
||||
><div class="col-span-3 truncate">{schedule.path}</div><div>{schedule.schedule}</div>
|
||||
<div>{schedule.enabled ? 'on' : 'off'}</div>
|
||||
<button on:click={() => scheduleEditor?.openEdit(schedule.path, false)}>Edit</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<Skeleton layout={[[8]]} />
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -48,6 +48,9 @@
|
||||
{/if}
|
||||
</h2>
|
||||
<slot name="action" />
|
||||
{#if collapsable && collapsed}
|
||||
<slot name="badge" />
|
||||
{/if}
|
||||
</div>
|
||||
<div class={collapsable && collapsed ? `hidden ${$$props.class}` : `${$$props.class}`}>
|
||||
<slot />
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { onMount } from 'svelte'
|
||||
import Button from './common/button/Button.svelte'
|
||||
import type { ToastAction } from '$lib/toast'
|
||||
import { processMessage } from './toast'
|
||||
|
||||
export let message: string
|
||||
export let toastId: string
|
||||
@@ -37,7 +38,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="ml-3 flex-1 w-0">
|
||||
<p class="text-sm text-secondary break-words">{message}</p>
|
||||
<p class="text-sm text-secondary break-words">{@html processMessage(message)}</p>
|
||||
{#if errorMessage}
|
||||
<p
|
||||
class="text-sm text-secondary border bg-surface-secondary p-2 w-full overflow-auto mt-2"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import { AlertTriangle } from 'lucide-svelte'
|
||||
|
||||
export let options: {
|
||||
left?: string
|
||||
@@ -14,14 +15,17 @@
|
||||
export let disabled = false
|
||||
export let textClass = ''
|
||||
export let textStyle = ''
|
||||
export let color: 'blue' | 'red' = 'blue'
|
||||
export let color: 'blue' | 'red' | 'nord' = 'blue'
|
||||
export let id = (Math.random() + 1).toString(36).substring(10)
|
||||
export let lightMode: boolean = false
|
||||
export let eeOnly: boolean = false
|
||||
|
||||
export let size: 'sm' | 'xs' = 'sm'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const bothOptions = Boolean(options.left) && Boolean(options.right)
|
||||
|
||||
export let textDisabled = false
|
||||
</script>
|
||||
|
||||
<label
|
||||
@@ -34,7 +38,7 @@
|
||||
<span
|
||||
class={twMerge(
|
||||
'mr-2 font-medium duration-50 select-none',
|
||||
bothOptions ? (checked ? 'text-disabled' : 'text-primary') : 'text-primary',
|
||||
bothOptions || textDisabled ? (checked ? 'text-disabled' : 'text-primary') : 'text-primary',
|
||||
size === 'xs' ? 'text-xs' : 'text-sm',
|
||||
textClass
|
||||
)}
|
||||
@@ -64,10 +68,12 @@
|
||||
/>
|
||||
<div
|
||||
class={classNames(
|
||||
"transition-all bg-surface-selected rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:bg-surface after:border-white after:border after:rounded-full after:transition-all ",
|
||||
"transition-all bg-surface-selected rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:bg-surface after:border-white after:border after:rounded-full after:transition-all items-center",
|
||||
color == 'red'
|
||||
? 'peer-checked:bg-red-600'
|
||||
: 'peer-checked:bg-blue-600 dark:peer-checked:bg-blue-500',
|
||||
: color == 'blue'
|
||||
? 'peer-checked:bg-blue-600 dark:peer-checked:bg-blue-500'
|
||||
: 'peer-checked:bg-nord-950 dark:peer-checked:bg-nord-400',
|
||||
size === 'sm'
|
||||
? 'w-11 h-6 after:top-0.5 after:left-[2px] after:h-5 after:w-5'
|
||||
: 'w-7 h-4 after:top-0.5 after:left-[2px] after:h-3 after:w-3'
|
||||
@@ -78,7 +84,7 @@
|
||||
<span
|
||||
class={twMerge(
|
||||
'ml-2 font-medium duration-50 select-none',
|
||||
bothOptions ? (checked ? 'text-primary' : 'text-disabled') : 'text-primary',
|
||||
bothOptions || textDisabled ? (checked ? 'text-primary' : 'text-disabled') : 'text-primary',
|
||||
size === 'xs' ? 'text-xs' : 'text-sm',
|
||||
textClass
|
||||
)}
|
||||
@@ -92,3 +98,11 @@
|
||||
{/if}
|
||||
<slot name="right" />
|
||||
</label>
|
||||
{#if eeOnly && disabled}
|
||||
<span
|
||||
class="inline-flex text-xs text-primary items-center gap-1 !text-yellow-500 whitespace-nowrap ml-8"
|
||||
>
|
||||
<AlertTriangle size={16} />
|
||||
EE only <Tooltip>Enterprise Edition only feature</Tooltip>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
export let scopes: string[] | undefined = undefined
|
||||
export let newTokenLabel: string | undefined = undefined
|
||||
export let newTokenWorkspace: string | undefined = undefined
|
||||
export let newToken: string | undefined = undefined
|
||||
|
||||
let newPassword: string | undefined
|
||||
@@ -79,7 +80,12 @@
|
||||
date.setDate(date.getDate() + newTokenExpiration)
|
||||
}
|
||||
newToken = await UserService.createToken({
|
||||
requestBody: { label: newTokenLabel, expiration: date?.toISOString(), scopes } as NewToken
|
||||
requestBody: {
|
||||
label: newTokenLabel,
|
||||
expiration: date?.toISOString(),
|
||||
scopes,
|
||||
workspace_id: newTokenWorkspace
|
||||
} as NewToken
|
||||
})
|
||||
dispatch('tokenCreated', newToken)
|
||||
listTokens()
|
||||
|
||||
@@ -71,5 +71,5 @@
|
||||
}}
|
||||
startIcon={{ icon: RotateCw }}
|
||||
/>
|
||||
<AssignableTags placement={popupPlacement} />
|
||||
<AssignableTags placement={popupPlacement} color="nord" />
|
||||
</div>
|
||||
|
||||
@@ -49,9 +49,8 @@
|
||||
size="sm"
|
||||
startIcon={{ icon: Clipboard }}
|
||||
btnClasses="absolute top-2 right-2 w-min z-20"
|
||||
>
|
||||
Copy content
|
||||
</Button>
|
||||
iconOnly
|
||||
/>
|
||||
{#key rawType}
|
||||
<Highlight
|
||||
class="overflow-auto px-1 flex-1"
|
||||
|
||||
@@ -114,6 +114,13 @@
|
||||
contained:
|
||||
'bg-surface border-transparent hover:bg-surface-hover focus-visible:bg-surface-hover text-primary focus-visible:ring-surface-selected',
|
||||
divider: 'divide-x divide-gray-200 dark:divide-gray-700'
|
||||
},
|
||||
nord: {
|
||||
border:
|
||||
'border-nord-200 bg-surface hover:bg-surface-hover focus-visible:bg-surface-hover text-primary hover:text-secondary focus-visible:text-secondary focus-visible:ring-surface-selected-inverse dark:border-nord-200',
|
||||
contained:
|
||||
'bg-nord-300 hover:bg-nord-0 focus-visible:bg-surface-hover-inverse text-primary-inverse focus-visible:ring-surface-selected-inverse dark:bg-nord-400 dark:hover:bg-nord-600 dark:text-primary-inverse',
|
||||
divider: 'divide-x divide-gray-800 dark:divide-gray-200'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
<button
|
||||
class={twMerge(
|
||||
'border-b-2 py-1 px-2 cursor-pointer transition-all z-10 ease-linear font-normal text-primary',
|
||||
'border-b-2 py-1 px-2 cursor-pointer transition-all z-10 ease-linear font-normal text-tertiary',
|
||||
isSelected
|
||||
? 'wm-tab-active font-main'
|
||||
: 'border-gray-300 dark:border-gray-600 border-opacity-0 hover:border-opacity-100 ',
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="px-2 py-1 border flex items-center cursor-pointer bg-surface-secondary text-primary justify-between rounded-md"
|
||||
class="grow min-w-0 w-full px-2 py-1 border flex items-center cursor-pointer bg-surface-secondary text-primary justify-between rounded-md"
|
||||
on:click={(e) => {
|
||||
e.preventDefault()
|
||||
copyToClipboard(content)
|
||||
}}
|
||||
>
|
||||
<div class="text-xs truncate whitespace-pre-wrap w-11/12">{content}</div>
|
||||
<Clipboard size={14} class="w-1/12" />
|
||||
<div class="text-xs truncate whitespace-no-wrap grow">{content}</div>
|
||||
<Clipboard size={12} class="flex-shrink-0" />
|
||||
</div>
|
||||
|
||||
@@ -1,25 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Tabs, Tab, TabContent } from '$lib/components/common'
|
||||
import { CalendarCheck2, MailIcon, Route, Terminal, Webhook } from 'lucide-svelte'
|
||||
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import HighlightTheme from '../HighlightTheme.svelte'
|
||||
import FlowViewerInner from '../FlowViewerInner.svelte'
|
||||
import DetailPageTriggerPanel from './DetailPageTriggerPanel.svelte'
|
||||
|
||||
export let triggerSelected: 'webhooks' | 'email' | 'schedule' | 'cli' | 'routes' = 'webhooks'
|
||||
export let triggerSelected: 'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' = 'webhooks'
|
||||
export let flow_json: any | undefined = undefined
|
||||
export let hasStepDetails: boolean = false
|
||||
|
||||
export let isOperator: boolean = false
|
||||
|
||||
export let selected: string
|
||||
|
||||
$: if (hasStepDetails) {
|
||||
selected = 'flow_step'
|
||||
}
|
||||
|
||||
// When we no longer have a selected flow step, switch to saved inputs
|
||||
$: !hasStepDetails && selected === 'flow_step' && (selected = 'saved_inputs')
|
||||
</script>
|
||||
|
||||
<HighlightTheme />
|
||||
@@ -27,79 +19,38 @@
|
||||
<Splitpanes horizontal class="h-full">
|
||||
<Pane size={100}>
|
||||
<Tabs bind:selected>
|
||||
<Tab value="saved_inputs">Saved inputs</Tab>
|
||||
<Tab value="saved_inputs">Saved Inputs</Tab>
|
||||
{#if !isOperator}
|
||||
<Tab value="details">Details & Triggers</Tab>
|
||||
<Tab value="triggers">Triggers</Tab>
|
||||
{/if}
|
||||
{#if flow_json}
|
||||
<Tab value="raw">Raw</Tab>
|
||||
<Tab value="raw">Export</Tab>
|
||||
{/if}
|
||||
{#if hasStepDetails}
|
||||
{#if selected == 'flow_step'}
|
||||
<Tab value="flow_step">Step</Tab>
|
||||
{/if}
|
||||
{#if !flow_json && !isOperator}
|
||||
<Tab value="schema">Schema</Tab>
|
||||
{/if}
|
||||
|
||||
<svelte:fragment slot="content">
|
||||
<div class="overflow-hidden" style="height:calc(100% - 32px);">
|
||||
<TabContent value="saved_inputs" class="flex flex-col flex-1 h-full">
|
||||
<slot name="save_inputs" />
|
||||
</TabContent>
|
||||
<TabContent value="details" class="flex flex-col flex-1 h-full">
|
||||
<Splitpanes horizontal class="h-full">
|
||||
<Pane size={50} minSize={20}>
|
||||
<slot name="details" />
|
||||
</Pane>
|
||||
<Pane size={50} minSize={20}>
|
||||
<Tabs bind:selected={triggerSelected}>
|
||||
<Tab value="webhooks">
|
||||
<span class="flex flex-row gap-2 items-center">
|
||||
<Webhook size={14} />
|
||||
Webhooks
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="schedule">
|
||||
<span class="flex flex-row gap-2 items-center">
|
||||
<CalendarCheck2 size={14} />
|
||||
Schedules
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="routes">
|
||||
<span class="flex flex-row gap-2 items-center">
|
||||
<Route size={14} />
|
||||
HTTP
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="email">
|
||||
<span class="flex flex-row gap-2 items-center">
|
||||
<MailIcon size={14} />
|
||||
Email
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="cli">
|
||||
<span class="flex flex-row gap-2 items-center">
|
||||
<Terminal size={14} />
|
||||
CLI
|
||||
</span>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<div class="h-[calc(100%-32px)]">
|
||||
<div class="h-full overflow-auto">
|
||||
{#if triggerSelected === 'webhooks'}
|
||||
<slot name="webhooks" />
|
||||
{:else if triggerSelected === 'routes'}
|
||||
<slot name="routes" />
|
||||
{:else if triggerSelected === 'email'}
|
||||
<slot name="email" />
|
||||
{:else if triggerSelected === 'schedule'}
|
||||
<slot name="schedule" />
|
||||
{:else if triggerSelected === 'cli'}
|
||||
<slot name="cli" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
<TabContent value="schema" class="flex flex-col flex-1 h-full">
|
||||
<slot name="schema" />
|
||||
</TabContent>
|
||||
<TabContent value="raw" class="flex flex-col flex-1 h-full overflow-auto">
|
||||
<TabContent value="triggers" class="flex flex-col flex-1 h-full pt-2">
|
||||
<DetailPageTriggerPanel bind:triggerSelected>
|
||||
<slot slot="webhooks" name="webhooks" />
|
||||
<slot slot="routes" name="routes" />
|
||||
<slot slot="emails" name="emails" />
|
||||
<slot slot="schedules" name="schedules" />
|
||||
<slot slot="cli" name="cli" />
|
||||
</DetailPageTriggerPanel>
|
||||
</TabContent>
|
||||
<TabContent value="raw" class="flex flex-col flex-1 h-full overflow-auto p-2">
|
||||
<FlowViewerInner flow={flow_json} />
|
||||
</TabContent>
|
||||
<TabContent value="flow_step" class="flex flex-col flex-1 h-full">
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
import ErrorHandlerToggleButton from './ErrorHandlerToggleButton.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { userStore } from '$lib/stores'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import type { TriggerContext } from '../triggers'
|
||||
import { Calendar } from 'lucide-svelte'
|
||||
|
||||
type MainButton = {
|
||||
label: string
|
||||
@@ -22,6 +25,8 @@
|
||||
color?: 'red'
|
||||
}
|
||||
|
||||
const { triggersCount, selectedTrigger } = getContext<TriggerContext>('TriggerContext')
|
||||
|
||||
export let mainButtons: MainButton[] = []
|
||||
export let menuItems: MenuItemButton[] = []
|
||||
export let title: string
|
||||
@@ -30,6 +35,8 @@
|
||||
export let errorHandlerKind: 'flow' | 'script'
|
||||
export let scriptOrFlowPath: string
|
||||
export let errorHandlerMuted: boolean | undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<div class="border-b p-2 shadow-md">
|
||||
@@ -47,6 +54,22 @@
|
||||
<Badge>tag: {tag}</Badge>
|
||||
{/if}
|
||||
<slot />
|
||||
{#if $triggersCount?.primary_schedule}
|
||||
<Button
|
||||
btnClasses="inline-flex"
|
||||
startIcon={{ icon: Calendar }}
|
||||
variant="contained"
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
$selectedTrigger = 'schedules'
|
||||
dispatch('triggerDetail')
|
||||
}}
|
||||
>
|
||||
{$triggersCount?.primary_schedule?.schedule ?? ''}
|
||||
</Button>
|
||||
{/if}
|
||||
<slot name="trigger-badges" />
|
||||
</div>
|
||||
<div class="flex gap-1 md:gap-2 items-center">
|
||||
{#if menuItems.length > 0}
|
||||
|
||||
@@ -3,16 +3,31 @@
|
||||
import SplitPanesWrapper from '$lib/components/splitPanes/SplitPanesWrapper.svelte'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import DetailPageDetailPanel from './DetailPageDetailPanel.svelte'
|
||||
import type { ScheduleTrigger, TriggerContext } from '../triggers'
|
||||
import { setContext } from 'svelte'
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import type { TriggersCount } from '$lib/gen'
|
||||
import DetailPageTriggerPanel from './DetailPageTriggerPanel.svelte'
|
||||
|
||||
export let isOperator: boolean = false
|
||||
export let flow_json: any | undefined = undefined
|
||||
export let hasStepDetails: boolean = false
|
||||
export let selected: string
|
||||
export let triggerSelected: 'webhooks' | 'schedule' | 'cli' = 'webhooks'
|
||||
export let triggersCount: Writable<TriggersCount | undefined>
|
||||
|
||||
let mobileTab: 'form' | 'detail' = 'form'
|
||||
|
||||
let clientWidth = window.innerWidth
|
||||
|
||||
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(undefined)
|
||||
const selectedTriggerStore = writable<'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes'>(
|
||||
'webhooks'
|
||||
)
|
||||
|
||||
setContext<TriggerContext>('TriggerContext', {
|
||||
selectedTrigger: selectedTriggerStore,
|
||||
primarySchedule: primaryScheduleStore,
|
||||
triggersCount
|
||||
})
|
||||
</script>
|
||||
|
||||
<main class="h-screen w-full" bind:clientWidth>
|
||||
@@ -26,20 +41,20 @@
|
||||
</Pane>
|
||||
<Pane size={35} minSize={15}>
|
||||
<DetailPageDetailPanel
|
||||
bind:triggerSelected
|
||||
bind:triggerSelected={$selectedTriggerStore}
|
||||
bind:selected
|
||||
{isOperator}
|
||||
{flow_json}
|
||||
{hasStepDetails}
|
||||
>
|
||||
<slot slot="webhooks" name="webhooks" />
|
||||
<slot slot="routes" name="routes" />
|
||||
<slot slot="email" name="email" />
|
||||
<slot slot="schedule" name="schedule" />
|
||||
<slot slot="emails" name="emails" />
|
||||
<slot slot="schedules" name="schedules" />
|
||||
<slot slot="cli" name="cli" />
|
||||
<slot slot="details" name="details" />
|
||||
<slot slot="save_inputs" name="save_inputs" />
|
||||
<slot slot="flow_step" name="flow_step" />
|
||||
<slot slot="schema" name="schema" />
|
||||
</DetailPageDetailPanel>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
@@ -50,23 +65,26 @@
|
||||
<slot name="header" />
|
||||
<Tabs bind:selected={mobileTab}>
|
||||
<Tab value="form">Run form</Tab>
|
||||
<Tab value="detail">Details</Tab>
|
||||
<Tab value="saved_inputs">Saved Inputs</Tab>
|
||||
{#if !isOperator}
|
||||
<Tab value="triggers">Triggers</Tab>
|
||||
{/if}
|
||||
<svelte:fragment slot="content">
|
||||
<TabContent value="form" class="flex flex-col flex-1 h-full">
|
||||
<slot name="form" />
|
||||
</TabContent>
|
||||
|
||||
<TabContent value="detail" class="flex flex-col flex-1 h-full">
|
||||
<DetailPageDetailPanel bind:triggerSelected bind:selected {isOperator} {hasStepDetails}>
|
||||
<TabContent value="saved_inputs" class="flex flex-col flex-1 h-full">
|
||||
<slot name="save_inputs" />
|
||||
</TabContent>
|
||||
<TabContent value="triggers" class="flex flex-col flex-1 h-full">
|
||||
<DetailPageTriggerPanel bind:triggerSelected={$selectedTriggerStore}>
|
||||
<slot slot="webhooks" name="webhooks" />
|
||||
<slot slot="routes" name="routes" />
|
||||
<slot slot="email" name="email" />
|
||||
<slot slot="schedule" name="schedule" />
|
||||
<slot slot="emails" name="emails" />
|
||||
<slot slot="schedules" name="schedules" />
|
||||
<slot slot="cli" name="cli" />
|
||||
<slot slot="details" name="details" />
|
||||
<slot slot="save_inputs" name="save_inputs" />
|
||||
<slot slot="flow_step" name="flow_step" />
|
||||
</DetailPageDetailPanel>
|
||||
</DetailPageTriggerPanel>
|
||||
</TabContent>
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<script lang="ts">
|
||||
import { Tabs, Tab } from '$lib/components/common'
|
||||
import { CalendarCheck2, MailIcon, Route, Terminal, Webhook } from 'lucide-svelte'
|
||||
|
||||
import HighlightTheme from '../HighlightTheme.svelte'
|
||||
|
||||
export let triggerSelected: 'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes' = 'webhooks'
|
||||
</script>
|
||||
|
||||
<HighlightTheme />
|
||||
|
||||
<Tabs bind:selected={triggerSelected}>
|
||||
<Tab value="webhooks">
|
||||
<span class="flex flex-row gap-2 items-center">
|
||||
<Webhook size={14} />
|
||||
Webhooks
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="schedules">
|
||||
<span class="flex flex-row gap-2 items-center">
|
||||
<CalendarCheck2 size={14} />
|
||||
Schedules
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="routes">
|
||||
<span class="flex flex-row gap-2 items-center">
|
||||
<Route size={14} />
|
||||
HTTP
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="emails">
|
||||
<span class="flex flex-row gap-2 items-center">
|
||||
<MailIcon size={14} />
|
||||
Email
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="cli">
|
||||
<span class="flex flex-row gap-2 items-center">
|
||||
<Terminal size={14} />
|
||||
CLI
|
||||
</span>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<div class="h-[calc(100%-32px)]">
|
||||
<div class="h-full overflow-auto">
|
||||
{#if triggerSelected === 'webhooks'}
|
||||
<slot name="webhooks" />
|
||||
{:else if triggerSelected === 'routes'}
|
||||
<slot name="routes" />
|
||||
{:else if triggerSelected === 'emails'}
|
||||
<slot name="emails" />
|
||||
{:else if triggerSelected === 'schedules'}
|
||||
<slot name="schedules" />
|
||||
{:else if triggerSelected === 'cli'}
|
||||
<slot name="cli" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -14,7 +14,8 @@
|
||||
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import { AlertTriangle } from 'lucide-svelte'
|
||||
import Skeleton from '../common/skeleton/Skeleton.svelte'
|
||||
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import TriggerTokens from '../triggers/TriggerTokens.svelte'
|
||||
let userSettings: UserSettings
|
||||
|
||||
export let token: string
|
||||
@@ -33,6 +34,7 @@
|
||||
})) as any) ?? null
|
||||
loading = false
|
||||
}
|
||||
|
||||
getEmailDomain()
|
||||
|
||||
let requestType: 'hash' | 'path' = 'path'
|
||||
@@ -49,6 +51,12 @@
|
||||
.toLowerCase()
|
||||
return `${pathOrHash}+${encodedPrefix}@${emailDomain}`
|
||||
}
|
||||
|
||||
export let email: string = ''
|
||||
|
||||
$: email = emailAddress()
|
||||
|
||||
let triggerTokens: TriggerTokens | undefined = undefined
|
||||
</script>
|
||||
|
||||
<HighlightTheme />
|
||||
@@ -57,18 +65,20 @@
|
||||
bind:this={userSettings}
|
||||
on:tokenCreated={(e) => {
|
||||
token = e.detail
|
||||
triggerTokens?.listTokens()
|
||||
}}
|
||||
newTokenWorkspace={$workspaceStore}
|
||||
newTokenLabel={`${$userStore?.username ?? 'superadmin'}-${generateRandomString(4)}`}
|
||||
{scopes}
|
||||
/>
|
||||
|
||||
<div class="p-2 flex flex-col w-full gap-4">
|
||||
<div class="flex flex-col w-full gap-4">
|
||||
{#if loading}
|
||||
<Skeleton layout={[[18]]} />
|
||||
{:else}
|
||||
{#if emailDomain}
|
||||
{#if SCRIPT_VIEW_SHOW_CREATE_TOKEN_BUTTON}
|
||||
<div class="my-2">
|
||||
<Label label="Token">
|
||||
<div class="flex flex-row justify-between gap-2">
|
||||
<input
|
||||
bind:value={token}
|
||||
@@ -89,7 +99,7 @@
|
||||
Create/input a valid token before copying the email address below
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Label>
|
||||
{/if}
|
||||
|
||||
{#if !isFlow}
|
||||
@@ -103,20 +113,19 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-col gap-4">
|
||||
{#key requestType}
|
||||
{#key token}
|
||||
<div class="flex flex-col gap-2">
|
||||
<ClipboardPanel title="Email address" content={emailAddress()} />
|
||||
</div>
|
||||
{/key}
|
||||
|
||||
{#key requestType}
|
||||
{#key token}
|
||||
<Label label="Email address">
|
||||
<ClipboardPanel content={email} />
|
||||
</Label>
|
||||
{/key}
|
||||
<Alert title="Email trigger" size="xs">
|
||||
To trigger the job by email, send an email to the address above. The job will receive two
|
||||
arguments: `raw_email` containing the raw email as string, and `parsed_email` containing
|
||||
the parsed email as an object.
|
||||
</Alert>
|
||||
</div>
|
||||
{/key}
|
||||
<Alert title="Email trigger" size="xs">
|
||||
To trigger the job by email, send an email to the address above. The job will receive two
|
||||
arguments: `raw_email` containing the raw email as string, and `parsed_email` containing the
|
||||
parsed email as an object.
|
||||
</Alert>
|
||||
{:else}
|
||||
<div>
|
||||
<Alert title="Email triggers are disabled" size="xs" type="warning">
|
||||
@@ -133,5 +142,7 @@
|
||||
Email triggers on Windmill Community Edition are limited to 100 emails per day.
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
<TriggerTokens bind:this={triggerTokens} {isFlow} {path} labelPrefix="email" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<script lang="ts">
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
|
||||
import { FlowService, ScriptService } from '$lib/gen'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
|
||||
export let kind: 'script' | 'flow'
|
||||
export let scriptOrFlowPath: string
|
||||
export let errorHandlerMuted: boolean | undefined
|
||||
export let textDisabled = false
|
||||
let toggleState = errorHandlerMuted
|
||||
|
||||
export let color: 'nord' | 'red' | 'blue' = 'nord'
|
||||
|
||||
async function toggleErrorHandler(): Promise<void> {
|
||||
toggleState = !toggleState
|
||||
if ($workspaceStore !== undefined) {
|
||||
try {
|
||||
if (kind === 'flow') {
|
||||
await FlowService.toggleWorkspaceErrorHandlerForFlow({
|
||||
workspace: $workspaceStore,
|
||||
path: scriptOrFlowPath,
|
||||
requestBody: {
|
||||
muted: !errorHandlerMuted
|
||||
}
|
||||
})
|
||||
} else {
|
||||
await ScriptService.toggleWorkspaceErrorHandlerForScript({
|
||||
workspace: $workspaceStore,
|
||||
path: scriptOrFlowPath,
|
||||
requestBody: {
|
||||
muted: !errorHandlerMuted
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
sendUserToast(
|
||||
`Error while toggling Workspace Error Handler: ${error.body || error.message}`,
|
||||
true
|
||||
)
|
||||
toggleState = false
|
||||
return
|
||||
}
|
||||
errorHandlerMuted = !errorHandlerMuted
|
||||
toggleState = errorHandlerMuted
|
||||
sendUserToast(
|
||||
errorHandlerMuted ? 'Workspace error handler muted' : 'Workspace error handler active',
|
||||
false
|
||||
)
|
||||
}
|
||||
toggleState = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<Toggle
|
||||
{color}
|
||||
textClass="font-normal text-sm"
|
||||
size="xs"
|
||||
checked={toggleState}
|
||||
on:change={toggleErrorHandler}
|
||||
options={{
|
||||
right: 'Mute',
|
||||
rightTooltip: 'Disable workspace error handler, EE only'
|
||||
}}
|
||||
{textDisabled}
|
||||
/>
|
||||
@@ -18,6 +18,7 @@
|
||||
export let disableSettings = false
|
||||
export let disabledFlowInputs = false
|
||||
export let smallErrorHandler = false
|
||||
export let newFlow: boolean = false
|
||||
|
||||
let size = 50
|
||||
|
||||
@@ -48,6 +49,7 @@
|
||||
{disableAi}
|
||||
{disableSettings}
|
||||
{smallErrorHandler}
|
||||
{newFlow}
|
||||
bind:modules={$flowStore.value.modules}
|
||||
on:reload
|
||||
/>
|
||||
@@ -62,7 +64,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<FlowEditorPanel {disabledFlowInputs} enableAi={!disableAi} />
|
||||
<FlowEditorPanel {disabledFlowInputs} {newFlow} enableAi={!disableAi} />
|
||||
{/if}
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import FlowInput from './FlowInput.svelte'
|
||||
import FlowFailureModule from './FlowFailureModule.svelte'
|
||||
import FlowConstants from './FlowConstants.svelte'
|
||||
import TriggersEditor from '../../triggers/TriggersEditor.svelte'
|
||||
import type { FlowModule } from '$lib/gen'
|
||||
import { initFlowStepWarnings } from '../utils'
|
||||
import { dfs } from '../dfs'
|
||||
@@ -14,9 +15,10 @@
|
||||
|
||||
export let noEditor = false
|
||||
export let enableAi = false
|
||||
export let newFlow = false
|
||||
export let disabledFlowInputs = false
|
||||
|
||||
const { selectedId, flowStore, flowStateStore, flowInputsStore } =
|
||||
const { selectedId, flowStore, flowStateStore, flowInputsStore, pathStore, initialPath } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
function checkDup(modules: FlowModule[]): string | undefined {
|
||||
@@ -67,6 +69,15 @@
|
||||
<FlowFailureModule {noEditor} />
|
||||
{:else if $selectedId === 'preprocessor'}
|
||||
<FlowPreprocessorModule {noEditor} />
|
||||
{:else if $selectedId === 'triggers'}
|
||||
<TriggersEditor
|
||||
currentPath={$pathStore}
|
||||
{initialPath}
|
||||
schema={$flowStore.schema}
|
||||
{noEditor}
|
||||
newItem={newFlow}
|
||||
isFlow={true}
|
||||
/>
|
||||
{:else}
|
||||
{@const dup = checkDup($flowStore.value.modules)}
|
||||
{#if dup}
|
||||
|
||||
@@ -20,14 +20,17 @@
|
||||
import FlowWhileLoop from './FlowWhileLoop.svelte'
|
||||
import { initFlowStepWarnings } from '../utils'
|
||||
import { dfs } from '../dfs'
|
||||
import type { TriggerContext } from '$lib/components/triggers'
|
||||
|
||||
export let flowModule: FlowModule
|
||||
export let noEditor: boolean = false
|
||||
export let enableAi = false
|
||||
|
||||
const { selectedId, schedule, flowStateStore, flowInputsStore, flowStore } =
|
||||
const { selectedId, flowStateStore, flowInputsStore, flowStore } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
const { primarySchedule } = getContext<TriggerContext>('TriggerContext')
|
||||
|
||||
let scriptKind: 'script' | 'trigger' | 'approval' = 'script'
|
||||
let scriptTemplate: 'pgsql' | 'mysql' | 'script' | 'docker' | 'powershell' = 'script'
|
||||
|
||||
@@ -37,6 +40,26 @@
|
||||
// Pointer to previous module, for easy access to testing results
|
||||
export let previousModule: FlowModule | undefined = undefined
|
||||
|
||||
function initializePrimaryScheduleForTriggerScript(module: FlowModule) {
|
||||
if (!$primarySchedule) {
|
||||
$primarySchedule = {
|
||||
summary: 'Scheduled poll of flow',
|
||||
args: {},
|
||||
cron: '0 */15 * * *',
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
if (!$primarySchedule.cron) {
|
||||
$primarySchedule.cron = '0 */15 * * *'
|
||||
}
|
||||
$primarySchedule.enabled = true
|
||||
|
||||
module.stop_after_if = {
|
||||
expr: 'result == undefined || Array.isArray(result) && result.length == 0',
|
||||
skip_if_stopped: true
|
||||
}
|
||||
}
|
||||
async function createModuleFromScript(
|
||||
path: string,
|
||||
summary: string,
|
||||
@@ -50,15 +73,7 @@
|
||||
}
|
||||
|
||||
if (kind == 'trigger') {
|
||||
if (!$schedule.cron) {
|
||||
$schedule.cron = '0 */15 * * *'
|
||||
}
|
||||
$schedule.enabled = true
|
||||
|
||||
module.stop_after_if = {
|
||||
expr: 'result == undefined || Array.isArray(result) && result.length == 0',
|
||||
skip_if_stopped: true
|
||||
}
|
||||
initializePrimaryScheduleForTriggerScript(module)
|
||||
}
|
||||
|
||||
flowModule = module
|
||||
@@ -140,15 +155,7 @@
|
||||
scriptTemplate = subkind
|
||||
|
||||
if (kind == 'trigger') {
|
||||
if (!$schedule.cron) {
|
||||
$schedule.cron = '0 */15 * * *'
|
||||
}
|
||||
$schedule.enabled = true
|
||||
|
||||
module.stop_after_if = {
|
||||
expr: 'result == undefined || Array.isArray(result) && result.length == 0',
|
||||
skip_if_stopped: true
|
||||
}
|
||||
initializePrimaryScheduleForTriggerScript(module)
|
||||
}
|
||||
|
||||
if (kind == 'approval') {
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
<script lang="ts">
|
||||
import CronInput from '$lib/components/CronInput.svelte'
|
||||
import SchemaForm from '$lib/components/SchemaForm.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import { emptyString } from '$lib/utils'
|
||||
import { getContext } from 'svelte'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
import { Alert, Button, Skeleton } from '$lib/components/common'
|
||||
import ScheduleEditor from '$lib/components/ScheduleEditor.svelte'
|
||||
import { ScheduleService, type Schedule } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { Calendar } from 'lucide-svelte'
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
|
||||
const { schedule, flowStore, initialPath } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
let schedules: Schedule[] | undefined = undefined
|
||||
|
||||
async function loadSchedules() {
|
||||
try {
|
||||
schedules = (
|
||||
await ScheduleService.listSchedules({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path: initialPath,
|
||||
isFlow: true
|
||||
})
|
||||
).filter((s) => s.path != initialPath)
|
||||
} catch (e) {
|
||||
console.error('impossible to load schedules')
|
||||
}
|
||||
}
|
||||
|
||||
let scheduleEditor: ScheduleEditor
|
||||
|
||||
$: initialPath && loadSchedules()
|
||||
</script>
|
||||
|
||||
<div class="w-full flex flex-col gap-4 mb-4">
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<Label label="Summary">
|
||||
<input
|
||||
autofocus
|
||||
type="text"
|
||||
placeholder="Short summary to be displayed when listed"
|
||||
class="text-sm w-full"
|
||||
bind:value={$schedule.summary}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<CronInput bind:schedule={$schedule.cron} bind:timezone={$schedule.timezone} />
|
||||
<div class="mt-10" />
|
||||
<SchemaForm schema={$flowStore.schema} bind:args={$schedule.args} />
|
||||
{#if emptyString($schedule.cron)}
|
||||
<p class="text-xs text-tertiary mt-10">Define a schedule frequency first</p>
|
||||
{/if}
|
||||
<div class="mt-10" />
|
||||
<Toggle
|
||||
disabled={emptyString($schedule.cron)}
|
||||
bind:checked={$schedule.enabled}
|
||||
options={{
|
||||
right: 'Schedule enabled'
|
||||
}}
|
||||
/>
|
||||
<Alert bgClass="my-4" type="warning" title="Changes only applied upon deploy">
|
||||
Changes to the primary schedule are only applied upon deploy. Other schedules' changes are applied
|
||||
immediately.
|
||||
</Alert>
|
||||
|
||||
{#if initialPath != ''}
|
||||
<ScheduleEditor
|
||||
on:update={() => {
|
||||
loadSchedules()
|
||||
}}
|
||||
bind:this={scheduleEditor}
|
||||
/>
|
||||
<h2 class="pt-7">Other schedules</h2>
|
||||
<div class="py-4 flex">
|
||||
<Button
|
||||
on:click={() => scheduleEditor?.openNew(true, initialPath)}
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
startIcon={{ icon: Calendar }}
|
||||
>
|
||||
New Schedule
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if schedules}
|
||||
{#if schedules.length == 0}
|
||||
<div class="text-xs text-secondary px-2"> No other schedules </div>
|
||||
{:else}
|
||||
<div class="flex flex-col divide-y px-2 pt-2 max-w-lg">
|
||||
{#each schedules as schedule (schedule.path)}
|
||||
<div class="grid grid-cols-6 text-2xs items-center py-2"
|
||||
><div class="col-span-3 truncate">{schedule.path}</div><div>{schedule.schedule}</div>
|
||||
<div>{schedule.enabled ? 'on' : 'off'}</div>
|
||||
<button on:click={() => scheduleEditor?.openEdit(schedule.path, true)}>Edit</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<Skeleton layout={[[8]]} />
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -1,348 +1,195 @@
|
||||
<script lang="ts">
|
||||
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
|
||||
import Tab from '$lib/components/common/tabs/Tab.svelte'
|
||||
import TabContent from '$lib/components/common/tabs/TabContent.svelte'
|
||||
|
||||
import { BROWSER } from 'esm-env'
|
||||
import { base } from '$lib/base'
|
||||
import Path from '$lib/components/Path.svelte'
|
||||
import FlowCard from '../common/FlowCard.svelte'
|
||||
import FlowSchedules from './FlowSchedules.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import { Alert, Button, SecondsInput } from '$lib/components/common'
|
||||
import { getContext } from 'svelte'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
import Slider from '$lib/components/Slider.svelte'
|
||||
import { enterpriseLicense, workspaceStore } from '$lib/stores'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import { copyToClipboard } from '$lib/utils'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import { AlertTriangle, Clipboard } from 'lucide-svelte'
|
||||
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
|
||||
import { schemaToObject } from '$lib/schema'
|
||||
import type { Schema } from '$lib/common'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import ErrorHandlerToggleButton from '$lib/components/details/ErrorHandlerToggleButton.svelte'
|
||||
import ErrorHandlerToggleButtonV2 from '$lib/components/details/ErrorHandlerToggleButtonV2.svelte'
|
||||
import WorkerTagPicker from '$lib/components/WorkerTagPicker.svelte'
|
||||
import MetadataGen from '$lib/components/copilot/MetadataGen.svelte'
|
||||
import Badge from '$lib/components/Badge.svelte'
|
||||
import { AlertTriangle } from 'lucide-svelte'
|
||||
|
||||
export let noEditor: boolean
|
||||
|
||||
const { selectedId, flowStore, initialPath, previewArgs, pathStore, schedule, customUi } =
|
||||
const { flowStore, initialPath, previewArgs, pathStore, customUi } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
let hostname = BROWSER ? window.location.protocol + '//' + window.location.host : 'SSR'
|
||||
$: url = `${hostname}/api/w/${$workspaceStore}/jobs/run/f/${$pathStore}`
|
||||
$: syncedUrl = `${hostname}/api/w/${$workspaceStore}/jobs/run_wait_result/f/${$pathStore}`
|
||||
|
||||
function asSchema(x: any) {
|
||||
return x as Schema
|
||||
}
|
||||
let path: Path | undefined = undefined
|
||||
let dirtyPath = false
|
||||
|
||||
let displayWorkerTagPicker = false
|
||||
$: $flowStore.tag ? (displayWorkerTagPicker = true) : null
|
||||
|
||||
$: activeAdvancedOptions = [
|
||||
{
|
||||
name: 'High Priority',
|
||||
active: $flowStore.value.priority !== undefined && $flowStore.value.priority > 0
|
||||
},
|
||||
{ name: 'Error Handler Muted', active: Boolean($flowStore.ws_error_handler_muted) },
|
||||
{ name: 'Invisible to Others', active: Boolean($flowStore.visible_to_runner_only) },
|
||||
{ name: 'Shared Directory', active: Boolean($flowStore.value.same_worker) },
|
||||
{ name: 'Cache Results', active: Boolean($flowStore.value.cache_ttl) },
|
||||
{ name: 'Early Stop', active: Boolean($flowStore.value.skip_expr) },
|
||||
{ name: 'Early Return', active: Boolean($flowStore.value.early_return) },
|
||||
{ name: 'Dedicated Worker', active: Boolean($flowStore.dedicated_worker) },
|
||||
{ name: 'Concurrent Limit', active: Boolean($flowStore.value.concurrent_limit) },
|
||||
{ name: 'Worker Tag', active: displayWorkerTagPicker }
|
||||
]
|
||||
|
||||
$: numberOfAdvancedOptionsOn = activeAdvancedOptions.filter((option) => option.active).length
|
||||
$: activeAdvancedOptionNames = activeAdvancedOptions
|
||||
.filter((option) => option.active)
|
||||
.map((option) => option.name)
|
||||
</script>
|
||||
|
||||
<div class="h-full overflow-hidden">
|
||||
<div class="h-full overflow-y-auto flex flex-col">
|
||||
<FlowCard {noEditor} title="Settings">
|
||||
<div class="h-full flex-1">
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<Tabs bind:selected={$selectedId}>
|
||||
<Tab value="settings-metadata">Metadata</Tab>
|
||||
{#if !noEditor && customUi?.settingsTabs?.schedule != false}
|
||||
<Tab value="settings-schedule" active={$schedule.enabled}>Schedule</Tab>
|
||||
{/if}
|
||||
{#if customUi?.settingsTabs?.sharedDiretory != false}
|
||||
<Tab value="settings-same-worker" active={$flowStore.value.same_worker}>
|
||||
Shared Directory
|
||||
</Tab>
|
||||
{/if}
|
||||
{#if customUi?.settingsTabs?.earlyStop != false}
|
||||
<Tab value="settings-early-stop" active={Boolean($flowStore.value.skip_expr)}>
|
||||
Early Stop
|
||||
</Tab>
|
||||
{/if}
|
||||
{#if customUi?.settingsTabs?.earlyReturn != false}
|
||||
<Tab value="settings-early-return" active={Boolean($flowStore.value.early_return)}>
|
||||
Early Return
|
||||
</Tab>
|
||||
<div class="grow min-h-0 p-4 h-full flex flex-col gap-8">
|
||||
<!-- Metadata Section -->
|
||||
<div class="gap-8 flex flex-col">
|
||||
<Label label="Summary">
|
||||
<MetadataGen
|
||||
bind:content={$flowStore.summary}
|
||||
promptConfigName="flowSummary"
|
||||
flow={$flowStore.value}
|
||||
on:change={() => {
|
||||
if (initialPath == '' && $flowStore.summary?.length > 0 && !dirtyPath) {
|
||||
path?.setName(
|
||||
$flowStore.summary
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_]/g, '_')
|
||||
.replace(/-+/g, '_')
|
||||
.replace(/^-|-$/g, '')
|
||||
)
|
||||
}
|
||||
}}
|
||||
elementProps={{
|
||||
type: 'text',
|
||||
id: 'flow-summary',
|
||||
placeholder: 'Short summary to be displayed when listed'
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
{#if !noEditor}
|
||||
<Label label="Path">
|
||||
<Path
|
||||
autofocus={false}
|
||||
bind:this={path}
|
||||
bind:dirty={dirtyPath}
|
||||
bind:path={$pathStore}
|
||||
{initialPath}
|
||||
namePlaceholder="flow"
|
||||
kind="flow"
|
||||
/>
|
||||
</Label>
|
||||
{/if}
|
||||
|
||||
<Label label="Description">
|
||||
<MetadataGen
|
||||
bind:content={$flowStore.description}
|
||||
promptConfigName="flowDescription"
|
||||
flow={$flowStore.value}
|
||||
class="w-full"
|
||||
elementType="textarea"
|
||||
elementProps={{
|
||||
id: 'inp',
|
||||
placeholder: 'What this flow does and how to use it.'
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<!-- Deployable Section -->
|
||||
<Section
|
||||
label="Advanced"
|
||||
collapsable={true}
|
||||
small={true}
|
||||
class="h-full grow min-h-0 flex flex-col gap-4"
|
||||
>
|
||||
<!-- Worker Group Section -->
|
||||
{#if customUi?.settingsTabs?.workerGroup != false}
|
||||
<Tab value="settings-worker-group">Worker Group</Tab>
|
||||
{/if}
|
||||
{#if customUi?.settingsTabs?.concurrency != false}
|
||||
<Tab value="settings-concurrency">Concurrency</Tab>
|
||||
{/if}
|
||||
{#if customUi?.settingsTabs?.cache != false}
|
||||
<Tab value="settings-cache" active={Boolean($flowStore.value.cache_ttl)}>Cache</Tab>
|
||||
{/if}
|
||||
<svelte:fragment slot="content">
|
||||
<TabContent value="settings-metadata" class="p-4 h-full overflow-auto">
|
||||
<Section label="Metadata">
|
||||
<div class="h-full gap-8 flex flex-col">
|
||||
<Label label="Summary">
|
||||
<MetadataGen
|
||||
bind:content={$flowStore.summary}
|
||||
promptConfigName="flowSummary"
|
||||
flow={$flowStore.value}
|
||||
on:change={() => {
|
||||
if (initialPath == '' && $flowStore.summary?.length > 0 && !dirtyPath) {
|
||||
path?.setName(
|
||||
$flowStore.summary
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_]/g, '_')
|
||||
.replace(/-+/g, '_')
|
||||
.replace(/^-|-$/g, '')
|
||||
)
|
||||
}
|
||||
}}
|
||||
elementProps={{
|
||||
type: 'text',
|
||||
id: 'flow-summary',
|
||||
placeholder: 'Short summary to be displayed when listed'
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
<div>
|
||||
<Toggle
|
||||
textClass="font-normal text-sm"
|
||||
color="nord"
|
||||
size="xs"
|
||||
checked={displayWorkerTagPicker}
|
||||
on:change={() => {
|
||||
displayWorkerTagPicker = !displayWorkerTagPicker
|
||||
if (!displayWorkerTagPicker) {
|
||||
$flowStore.tag = undefined
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: 'Worker Group Tag (Queue)',
|
||||
rightTooltip:
|
||||
"When a worker group tag is defined at the flow level, any steps inside the flow will run on any worker group that listen to that tag, regardless of the steps tag. If no worker group tags is defined, the flow controls will be executed with the default tag 'flow' and the steps will be executed with their respective tag"
|
||||
}}
|
||||
class="py-1"
|
||||
/>
|
||||
|
||||
{#if !noEditor}
|
||||
<Label label="Path">
|
||||
<Path
|
||||
autofocus={false}
|
||||
bind:this={path}
|
||||
bind:dirty={dirtyPath}
|
||||
bind:path={$pathStore}
|
||||
{initialPath}
|
||||
namePlaceholder="flow"
|
||||
kind="flow"
|
||||
/>
|
||||
</Label>
|
||||
{/if}
|
||||
{#if displayWorkerTagPicker}
|
||||
<WorkerTagPicker bind:tag={$flowStore.tag} popupPlacement="top-end" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Label label="Description">
|
||||
<MetadataGen
|
||||
bind:content={$flowStore.description}
|
||||
promptConfigName="flowDescription"
|
||||
flow={$flowStore.value}
|
||||
class="w-full"
|
||||
elementType="textarea"
|
||||
elementProps={{
|
||||
id: 'inp',
|
||||
placeholder: 'What this flow does and how to use it.'
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
<!-- <Tooltip
|
||||
>In this mode, every scripts of this flow is run on the workers dedicated to this flow
|
||||
that keep the scripts "hot" so that there is not cold start cost incurred. Steps can run
|
||||
at >1500 rps in this mode.</Tooltip
|
||||
> -->
|
||||
{/if}
|
||||
|
||||
<!-- TODO: Add EE-only badge when we have it -->
|
||||
<Toggle
|
||||
disabled={!$enterpriseLicense || isCloudHosted()}
|
||||
checked={$flowStore.value.priority !== undefined && $flowStore.value.priority > 0}
|
||||
on:change={() => {
|
||||
if ($flowStore.value.priority) {
|
||||
$flowStore.value.priority = undefined
|
||||
} else {
|
||||
$flowStore.value.priority = 100
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: `Label as high priority`,
|
||||
rightTooltip: `All jobs scheduled by flows labeled as high priority take precedence over the other jobs in the jobs queue. ${
|
||||
!$enterpriseLicense
|
||||
? 'This is a feature only available on enterprise edition.'
|
||||
: ''
|
||||
}`
|
||||
}}
|
||||
<!-- Metadata Advanced Section -->
|
||||
<svelte:fragment slot="badge">
|
||||
{#if numberOfAdvancedOptionsOn > 0}
|
||||
<div class="flex grow min-w-0 w-full flex-wrap gap-1 ps-2">
|
||||
{#each activeAdvancedOptionNames as optionName}
|
||||
<Badge twBgColor="bg-nord-950 dark:bg-nord-400" twTextColor="text-primary-inverse"
|
||||
>{optionName}</Badge
|
||||
>
|
||||
<svelte:fragment slot="right">
|
||||
<input
|
||||
type="number"
|
||||
class="!w-16 ml-4"
|
||||
disabled={$flowStore.value.priority === undefined}
|
||||
bind:value={$flowStore.value.priority}
|
||||
on:focus
|
||||
on:change={() => {
|
||||
if ($flowStore.value.priority && $flowStore.value.priority > 100) {
|
||||
$flowStore.value.priority = 100
|
||||
} else if ($flowStore.value.priority && $flowStore.value.priority < 0) {
|
||||
$flowStore.value.priority = 0
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</Toggle>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<Toggle
|
||||
size="sm"
|
||||
checked={Boolean($flowStore.visible_to_runner_only)}
|
||||
on:change={() => {
|
||||
if ($flowStore.visible_to_runner_only) {
|
||||
$flowStore.visible_to_runner_only = undefined
|
||||
} else {
|
||||
$flowStore.visible_to_runner_only = true
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: 'Make runs invisible to others',
|
||||
rightTooltip:
|
||||
'When this option is enabled, manual executions of this script are invisible to users other than the user running it, including the owner(s). This setting can be overridden when this script is run manually from the advanced menu.'
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
<ErrorHandlerToggleButton
|
||||
kind="flow"
|
||||
scriptOrFlowPath={$pathStore}
|
||||
bind:errorHandlerMuted={$flowStore.ws_error_handler_muted}
|
||||
iconOnly={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Slider text="How to trigger flows?">
|
||||
<div class="text-sm text-tertiary border p-4 mb-20">
|
||||
On-demand:
|
||||
<ul class="pt-4">
|
||||
<li>
|
||||
1. <a
|
||||
href="https://www.windmill.dev/docs/core_concepts/auto_generated_uis"
|
||||
target="_blank">Auto-generated UIs</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
2. <a href="{base}/apps/add?nodraft=true" target="_blank"> App Editor</a> for
|
||||
customized-UIs
|
||||
</li>
|
||||
<li>
|
||||
3. <a href="{base}/schedules" target="_blank">Scheduling</a>
|
||||
</li>
|
||||
<li>
|
||||
4. <a href="https://www.windmill.dev/docs/advanced/cli" target="_blank"
|
||||
>Windmill CLI</a
|
||||
>
|
||||
</li>
|
||||
<br />
|
||||
<li class="mt-2">
|
||||
<div class="flex flex-col gap-2">
|
||||
<p> From external events: </p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="mt-2">
|
||||
5. Send a <a
|
||||
href="https://www.windmill.dev/docs/core_concepts/webhooks"
|
||||
target="_blank">webhook</a
|
||||
>
|
||||
after each event:
|
||||
<ul class="list-disc pl-4"
|
||||
><li
|
||||
>Async <Tooltip
|
||||
>Return an uuid instantly that you can use to fetch status and result</Tooltip
|
||||
>:
|
||||
<a
|
||||
on:click={(e) => {
|
||||
e.preventDefault()
|
||||
copyToClipboard(url)
|
||||
}}
|
||||
href={url}
|
||||
class="whitespace-nowrap text-ellipsis overflow-hidden mr-1"
|
||||
>
|
||||
{url}
|
||||
<span class="text-secondary ml-2">
|
||||
<Clipboard />
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
>Sync <Tooltip>Wait for result within a timeout of 20s</Tooltip>:
|
||||
<a
|
||||
on:click={(e) => {
|
||||
e.preventDefault()
|
||||
copyToClipboard(syncedUrl)
|
||||
}}
|
||||
href={syncedUrl}
|
||||
class="whitespace-nowrap text-ellipsis overflow-hidden mr-1"
|
||||
>
|
||||
{syncedUrl}
|
||||
<span class="text-secondary ml-2">
|
||||
<Clipboard />
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul></li
|
||||
>
|
||||
<br />
|
||||
<li>
|
||||
6. Use a <a
|
||||
href="https://www.windmill.dev/docs/flows/flow_trigger"
|
||||
target="_blank">trigger script</a
|
||||
>
|
||||
and schedule this flow to run as frequently as needed and compare a state persisted
|
||||
in Windmill to the state of the external system. If a difference is detected,
|
||||
then the rest of the flow is triggered. Oftentimes, the second step of a flow
|
||||
is a for-loop that will iterate over every elements. When using a trigger, a
|
||||
default schedule will be created.
|
||||
<img
|
||||
class="shadow-lg border rounded"
|
||||
alt="static button"
|
||||
src="{base}/trigger_button.png"
|
||||
/>
|
||||
</li></ul
|
||||
>
|
||||
</div>
|
||||
</Slider>
|
||||
</div>
|
||||
</Section>
|
||||
</TabContent>
|
||||
<TabContent value="settings-schedule" class="p-4 h-full overflow-scroll">
|
||||
<Alert
|
||||
type="info"
|
||||
title="Primary Schedule"
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/scheduling"
|
||||
>
|
||||
Flows can be triggered by any schedules, their webhooks or their UI but they have only
|
||||
one primary schedule with which they share the same path. The primary schedule can be
|
||||
set here.
|
||||
</Alert>
|
||||
<div class="mt-4" />
|
||||
<FlowSchedules />
|
||||
</TabContent>
|
||||
|
||||
<TabContent value="settings-same-worker" class="p-4 flex flex-col">
|
||||
<Section label="Shared Directory" class="flex flex-col">
|
||||
<Alert type="info" title="Shared Directory">
|
||||
Steps will share a folder at `./shared` in which they can store heavier data and
|
||||
pass them to the next step. <br /><br />Beware that the `./shared` folder is not
|
||||
preserved across suspends and sleeps. <br /><br />
|
||||
Furthermore, steps' worker groups is not respected and only the flow's worker group will
|
||||
be respected.
|
||||
</Alert>
|
||||
<Toggle
|
||||
bind:checked={$flowStore.value.same_worker}
|
||||
options={{
|
||||
right: 'Shared Directory on `./shared`'
|
||||
}}
|
||||
class="mt-2"
|
||||
/>
|
||||
</Section>
|
||||
</TabContent>
|
||||
<TabContent value="settings-cache" class="p-4 flex flex-col">
|
||||
<Section label="Cache">
|
||||
<svelte:fragment slot="action">
|
||||
<Toggle
|
||||
size="xs"
|
||||
checked={Boolean($flowStore.value.cache_ttl)}
|
||||
on:change={() => {
|
||||
if ($flowStore.value.cache_ttl && $flowStore.value.cache_ttl != undefined) {
|
||||
$flowStore.value.cache_ttl = undefined
|
||||
} else {
|
||||
$flowStore.value.cache_ttl = 300
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: 'Cache the results for each possible inputs'
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
|
||||
<div class="flex gap-x-4 flex-col gap-2">
|
||||
<div class="text-xs">How long to keep the cache valid</div>
|
||||
<!-- Cache Section -->
|
||||
{#if customUi?.settingsTabs?.cache != false}
|
||||
<div>
|
||||
<Toggle
|
||||
textClass="font-normal text-sm"
|
||||
color="nord"
|
||||
size="xs"
|
||||
checked={Boolean($flowStore.value.cache_ttl)}
|
||||
on:change={() => {
|
||||
if ($flowStore.value.cache_ttl && $flowStore.value.cache_ttl != undefined) {
|
||||
$flowStore.value.cache_ttl = undefined
|
||||
} else {
|
||||
$flowStore.value.cache_ttl = 300
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: 'Cache the results for each possible inputs'
|
||||
}}
|
||||
class="py-1"
|
||||
/>
|
||||
{#if $flowStore.value.cache_ttl}
|
||||
<div class="flex gap-x-4 flex-col gap-1">
|
||||
<div class="text-sm text-secondary">How long to keep the cache valid</div>
|
||||
<div>
|
||||
{#if $flowStore.value.cache_ttl}
|
||||
<SecondsInput bind:seconds={$flowStore.value.cache_ttl} />
|
||||
@@ -351,136 +198,87 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</TabContent>
|
||||
|
||||
<TabContent value="settings-worker-group" class="p-4 flex flex-col">
|
||||
<Alert type="info" title="Worker Group Tag (Queue)">
|
||||
When a worker group tag is defined at the flow level, any steps inside the flow will
|
||||
run on any worker group that listen to that tag, regardless of the steps' tag. If no
|
||||
worker group tags is defined, the flow controls will be executed with the default tag
|
||||
'flow' and the steps will be executed with their respective tag
|
||||
</Alert>
|
||||
<span class="my-4 text-lg font-bold">Worker Group Tag (Queue)</span>
|
||||
<WorkerTagPicker bind:tag={$flowStore.tag} />
|
||||
|
||||
<div class="py-6" />
|
||||
<span class="my-4 text-lg font-bold flex items-baseline gap-8"
|
||||
>Dedicated Worker {#if !$enterpriseLicense}<div
|
||||
class="flex text-xs items-center gap-1 text-yellow-500 whitespace-nowrap"
|
||||
>
|
||||
<AlertTriangle size={16} />
|
||||
EE only
|
||||
</div>{/if}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Early Stop Section -->
|
||||
{#if customUi?.settingsTabs?.earlyStop != false}
|
||||
<div>
|
||||
<!-- documentationLink="https://www.windmill.dev/docs/flows/early_stop -->
|
||||
<Toggle
|
||||
disabled={!$enterpriseLicense || isCloudHosted()}
|
||||
size="sm"
|
||||
checked={Boolean($flowStore.dedicated_worker)}
|
||||
textClass="font-normal text-sm"
|
||||
color="nord"
|
||||
size="xs"
|
||||
checked={Boolean($flowStore.value.skip_expr)}
|
||||
on:change={() => {
|
||||
if ($flowStore.dedicated_worker) {
|
||||
$flowStore.dedicated_worker = undefined
|
||||
if (Boolean($flowStore.value.skip_expr) && $flowStore.value.skip_expr) {
|
||||
$flowStore.value.skip_expr = undefined
|
||||
} else {
|
||||
$flowStore.dedicated_worker = true
|
||||
$flowStore.value.skip_expr = 'flow_input.foo == undefined'
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: 'Flow is run on dedicated workers'
|
||||
right: 'Early stop if condition met',
|
||||
rightTooltip:
|
||||
'If defined, at the beginning of the step the predicate expression will be evaluated' +
|
||||
'to decide if the flow should stop early.'
|
||||
}}
|
||||
class="py-1"
|
||||
/>
|
||||
{#if $flowStore.dedicated_worker}
|
||||
<div class="py-2">
|
||||
<Alert type="info" title="Require dedicated workers">
|
||||
One worker in a worker group needs to be configured with dedicated worker set to: <pre
|
||||
>{$workspaceStore}:flow/{$pathStore}</pre
|
||||
>
|
||||
</Alert>
|
||||
</div>
|
||||
{/if}
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip
|
||||
>In this mode, every scripts of this flow is run on the workers dedicated to this
|
||||
flow that keep the scripts "hot" so that there is not cold start cost incurred.
|
||||
Steps can run at >1500 rps in this mode.</Tooltip
|
||||
>
|
||||
</svelte:fragment>
|
||||
</TabContent>
|
||||
<TabContent value="settings-early-stop" class="p-4">
|
||||
<Section label="Early stop">
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip documentationLink="https://www.windmill.dev/docs/flows/early_stop">
|
||||
If defined, at the beginning of the step the predicate expression will be
|
||||
evaluated to decide if the flow should stop early.
|
||||
</Tooltip>
|
||||
</svelte:fragment>
|
||||
<Toggle
|
||||
checked={Boolean($flowStore.value.skip_expr)}
|
||||
on:change={() => {
|
||||
if (Boolean($flowStore.value.skip_expr) && $flowStore.value.skip_expr) {
|
||||
$flowStore.value.skip_expr = undefined
|
||||
} else {
|
||||
$flowStore.value.skip_expr = 'flow_input.foo == undefined'
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: 'Early stop if condition met'
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if $flowStore.value.skip_expr}
|
||||
<div
|
||||
class="w-full border mt-2 p-2 flex flex-col {$flowStore.value.skip_expr
|
||||
class="w-full border flex flex-col {$flowStore.value.skip_expr
|
||||
? ''
|
||||
: 'bg-surface-secondary'}"
|
||||
>
|
||||
{#if $flowStore.value.skip_expr}
|
||||
<div class="border w-full">
|
||||
<SimpleEditor
|
||||
lang="javascript"
|
||||
bind:code={$flowStore.value.skip_expr}
|
||||
class="small-editor"
|
||||
extraLib={`declare const flow_input = ${JSON.stringify(
|
||||
schemaToObject(asSchema($flowStore.schema), $previewArgs)
|
||||
)};
|
||||
declare const WM_SCHEDULED_FOR: string;`}
|
||||
/>
|
||||
<div class="text-xs text-tertiary mt-2">
|
||||
You can use the variable `flow_input` to access the inputs of the flow. <br
|
||||
/>The variable `WM_SCHEDULED_FOR` contains the time the flow was scheduled for
|
||||
which you can use to stop early non fresh jobs:
|
||||
<pre>new Date().getTime() - new Date(WM_SCHEDULED_FOR).getTime() {'>'} X</pre>
|
||||
</div>
|
||||
<div class="border w-full">
|
||||
<SimpleEditor
|
||||
lang="javascript"
|
||||
bind:code={$flowStore.value.skip_expr}
|
||||
class="small-editor"
|
||||
extraLib={`declare const flow_input = ${JSON.stringify(
|
||||
schemaToObject(asSchema($flowStore.schema), $previewArgs)
|
||||
)};
|
||||
declare const WM_SCHEDULED_FOR: string;`}
|
||||
/>
|
||||
<div class="text-xs text-tertiary mt-2">
|
||||
You can use the variable `flow_input` to access the inputs of the flow. <br
|
||||
/>The variable `WM_SCHEDULED_FOR` contains the time the flow was scheduled for
|
||||
which you can use to stop early non fresh jobs:
|
||||
<pre>new Date().getTime() - new Date(WM_SCHEDULED_FOR).getTime() {'>'} X</pre>
|
||||
</div>
|
||||
{:else}
|
||||
<textarea disabled rows="3" class="min-h-[80px]" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</TabContent>
|
||||
<TabContent value="settings-early-return" class="p-4">
|
||||
<Section label="Early Return">
|
||||
<div class="py-2">
|
||||
<Alert type="info" title="Return sync endpoints early">
|
||||
If defined, sync endpoints will return early at the node defined here while the
|
||||
rest of the flow continue asynchronously.
|
||||
</Alert>
|
||||
</div>
|
||||
<Toggle
|
||||
checked={Boolean($flowStore.value.early_return)}
|
||||
on:change={() => {
|
||||
if (Boolean($flowStore.value.early_return) && $flowStore.value.early_return) {
|
||||
$flowStore.value.early_return = undefined
|
||||
} else {
|
||||
$flowStore.value.early_return = $flowStore.value.modules?.[0]?.id ?? 'a'
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: 'Early return sync endpoint at a top-level step'
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Early Return Section -->
|
||||
{#if customUi?.settingsTabs?.earlyReturn != false}
|
||||
<div>
|
||||
<Toggle
|
||||
textClass="font-normal text-sm"
|
||||
color="nord"
|
||||
size="xs"
|
||||
checked={Boolean($flowStore.value.early_return)}
|
||||
on:change={() => {
|
||||
if (Boolean($flowStore.value.early_return) && $flowStore.value.early_return) {
|
||||
$flowStore.value.early_return = undefined
|
||||
} else {
|
||||
$flowStore.value.early_return = $flowStore.value.modules?.[0]?.id ?? 'a'
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: 'Early return for sync webhooks',
|
||||
rightTooltip:
|
||||
'If defined, sync endpoints will return early at the node defined here while the rest of the flow continue asynchronously.'
|
||||
}}
|
||||
class="py-1"
|
||||
/>
|
||||
{#if $flowStore.value.early_return}
|
||||
<div
|
||||
class="border max-w-[120px] mt-2 p-2 flex flex-col {$flowStore.value.early_return
|
||||
class="max-w-[120px] flex flex-col {$flowStore.value.early_return
|
||||
? ''
|
||||
: 'bg-surface-secondary'}"
|
||||
>
|
||||
@@ -488,6 +286,7 @@
|
||||
name="oauth_name"
|
||||
id="oauth_name"
|
||||
bind:value={$flowStore.value.early_return}
|
||||
class="text-xs"
|
||||
>
|
||||
<option value={undefined}>Node's id</option>
|
||||
{#each $flowStore.value?.modules?.map((x) => x.id) as name}
|
||||
@@ -495,14 +294,92 @@
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</Section>
|
||||
</TabContent>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<TabContent value="settings-concurrency" class="p-4 flex flex-col">
|
||||
<Section label="Concurrency Limits" eeOnly>
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip>Allowed concurrency within a given timeframe</Tooltip>
|
||||
</svelte:fragment>
|
||||
<!-- Shared Directory Section -->
|
||||
{#if customUi?.settingsTabs?.sharedDiretory != false}
|
||||
<Toggle
|
||||
textClass="font-normal text-sm"
|
||||
color="nord"
|
||||
size="xs"
|
||||
bind:checked={$flowStore.value.same_worker}
|
||||
options={{
|
||||
right: 'Same Worker + Shared Directory on `./shared`',
|
||||
rightTooltip:
|
||||
'Steps will share a folder at `./shared` in which they can store heavier data and ' +
|
||||
'pass them to the next step. Beware that the `./shared` folder is not ' +
|
||||
'preserved across suspends and sleeps.'
|
||||
}}
|
||||
class="py-1"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Visibility Section -->
|
||||
<Toggle
|
||||
textClass="font-normal text-sm"
|
||||
color="nord"
|
||||
size="xs"
|
||||
checked={Boolean($flowStore.visible_to_runner_only)}
|
||||
on:change={() => {
|
||||
if ($flowStore.visible_to_runner_only) {
|
||||
$flowStore.visible_to_runner_only = undefined
|
||||
} else {
|
||||
$flowStore.visible_to_runner_only = true
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: 'Make runs invisible to others',
|
||||
rightTooltip:
|
||||
'When this option is enabled, manual executions of this script are invisible to users other than the user running it, including the owner(s). This setting can be overridden when this script is run manually from the advanced menu.'
|
||||
}}
|
||||
class="py-1"
|
||||
/>
|
||||
|
||||
<!-- Error Handler Section -->
|
||||
<div class="flex flex-row items-center py-1">
|
||||
<ErrorHandlerToggleButtonV2
|
||||
color="nord"
|
||||
kind="flow"
|
||||
scriptOrFlowPath={$pathStore}
|
||||
bind:errorHandlerMuted={$flowStore.ws_error_handler_muted}
|
||||
/>
|
||||
{#if !$enterpriseLicense}
|
||||
<span
|
||||
class="inline-flex text-xs text-primary items-center gap-1 !text-yellow-500 whitespace-nowrap ml-8"
|
||||
>
|
||||
<AlertTriangle size={16} />
|
||||
EE only <Tooltip>Enterprise Edition only feature</Tooltip>
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Concurrency Section -->
|
||||
{#if customUi?.settingsTabs?.concurrency != false}
|
||||
<div>
|
||||
<Toggle
|
||||
textClass="font-normal text-sm"
|
||||
color="nord"
|
||||
size="xs"
|
||||
disabled={!$enterpriseLicense}
|
||||
checked={Boolean($flowStore.value.concurrent_limit)}
|
||||
on:change={() => {
|
||||
if ($flowStore.value.concurrent_limit) {
|
||||
$flowStore.value.concurrent_limit = undefined
|
||||
} else {
|
||||
$flowStore.value.concurrent_limit = 1
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: 'Concurrency Limits',
|
||||
rightTooltip: 'Allowed concurrency within a given timeframe'
|
||||
}}
|
||||
class="py-1"
|
||||
eeOnly={true}
|
||||
/>
|
||||
|
||||
{#if $flowStore.value.concurrent_limit}
|
||||
<div class="flex flex-col gap-4">
|
||||
<Label label="Max number of executions within the time window">
|
||||
<div class="flex flex-row gap-2 max-w-sm">
|
||||
@@ -544,10 +421,90 @@
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
</Section>
|
||||
</TabContent>
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Priority Section -->
|
||||
<Toggle
|
||||
textClass="font-normal text-sm"
|
||||
color="nord"
|
||||
size="xs"
|
||||
disabled={!$enterpriseLicense || isCloudHosted()}
|
||||
checked={$flowStore.value.priority !== undefined && $flowStore.value.priority > 0}
|
||||
on:change={() => {
|
||||
if ($flowStore.value.priority) {
|
||||
$flowStore.value.priority = undefined
|
||||
} else {
|
||||
$flowStore.value.priority = 100
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: `Label as high priority`,
|
||||
rightTooltip: `All jobs scheduled by flows labeled as high priority take precedence over the other jobs in the jobs queue. ${
|
||||
!$enterpriseLicense ? 'This is a feature only available on enterprise edition.' : ''
|
||||
}`
|
||||
}}
|
||||
class="py-1 relative"
|
||||
>
|
||||
<svelte:fragment slot="right">
|
||||
<input
|
||||
type="number"
|
||||
class="!w-16 text-xs ml-4 absolute left-52"
|
||||
disabled={$flowStore.value.priority === undefined}
|
||||
bind:value={$flowStore.value.priority}
|
||||
on:focus
|
||||
on:change={() => {
|
||||
if ($flowStore.value.priority && $flowStore.value.priority > 100) {
|
||||
$flowStore.value.priority = 100
|
||||
} else if ($flowStore.value.priority && $flowStore.value.priority < 0) {
|
||||
$flowStore.value.priority = 0
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{#if !$enterpriseLicense || isCloudHosted()}
|
||||
<span
|
||||
class="inline-flex absolute top-0 left-72 text-xs text-primary items-center gap-1 !text-yellow-500 whitespace-nowrap ml-8"
|
||||
>
|
||||
<AlertTriangle size={16} />
|
||||
EE only <Tooltip>Enterprise Edition only feature</Tooltip>
|
||||
</span>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Toggle>
|
||||
|
||||
<div>
|
||||
<Toggle
|
||||
textClass="font-normal text-sm"
|
||||
color="nord"
|
||||
size="xs"
|
||||
disabled={!$enterpriseLicense || isCloudHosted()}
|
||||
checked={Boolean($flowStore.dedicated_worker)}
|
||||
on:change={() => {
|
||||
if ($flowStore.dedicated_worker) {
|
||||
$flowStore.dedicated_worker = undefined
|
||||
} else {
|
||||
$flowStore.dedicated_worker = true
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: 'Flow is run on dedicated workers'
|
||||
}}
|
||||
class="py-1"
|
||||
eeOnly={true}
|
||||
/>
|
||||
|
||||
{#if $flowStore.dedicated_worker}
|
||||
<div>
|
||||
<Alert type="info" title="Require dedicated workers">
|
||||
One worker in a worker group needs to be configured with dedicated worker set to: <pre
|
||||
>{$workspaceStore}:flow/{$pathStore}</pre
|
||||
>
|
||||
</Alert>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
</FlowCard>
|
||||
</div>
|
||||
|
||||
@@ -45,7 +45,8 @@
|
||||
'preprocessor',
|
||||
'constants',
|
||||
'Result',
|
||||
'Input'
|
||||
'Input',
|
||||
'triggers'
|
||||
].includes($selectedId) ||
|
||||
$selectedId?.includes('branch')
|
||||
</script>
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import { copilotInfo } from '$lib/stores'
|
||||
import { ExternalLink, Wand2 } from 'lucide-svelte'
|
||||
import { base } from '$lib/base'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
let openNoCopilot = false
|
||||
export let className = ''
|
||||
@@ -15,7 +14,7 @@
|
||||
getContext<FlowCopilotContext | undefined>('FlowCopilotContext') || {}
|
||||
</script>
|
||||
|
||||
<div class={twMerge('absolute left-1/2 transform -translate-x-1/2 z-10', className)}>
|
||||
<div class={className}>
|
||||
<Popover>
|
||||
<div class={openNoCopilot ? 'z-10' : ''}>
|
||||
<Menu pointerDown noMinW placement="bottom-center" let:close bind:show={openNoCopilot}>
|
||||
|
||||
@@ -62,6 +62,11 @@
|
||||
let newId: string = id ?? ''
|
||||
|
||||
let hover = false
|
||||
|
||||
let idBadgeWidth: number | undefined = undefined
|
||||
let iconWidth: number | undefined = undefined
|
||||
|
||||
$: marginLeft = Math.max(iconWidth ?? 0, idBadgeWidth ?? 0) * 2 + 32
|
||||
</script>
|
||||
|
||||
{#if deletable && id && editId}
|
||||
@@ -230,16 +235,24 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex gap-1 justify-between items-center w-full overflow-hidden rounded-sm
|
||||
class="relative flex gap-1 justify-between items-center w-full overflow-hidden rounded-sm
|
||||
p-2 text-2xs module text-primary"
|
||||
>
|
||||
{#if $$slots.icon}
|
||||
<slot name="icon" />
|
||||
<div class="flex-none" bind:clientWidth={iconWidth}>
|
||||
<slot name="icon" />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="truncate" class:font-bold={bold}>{label}</div>
|
||||
<div class="flex items-center space-x-2 relative">
|
||||
<div
|
||||
class="absolute left-1/2 transform -translate-x-1/2 text-center truncate"
|
||||
class:font-bold={bold}
|
||||
style="max-width: calc(100% - {marginLeft}px)">{label}</div
|
||||
>
|
||||
<div class="flex items-center space-x-2 relative max-w-[25%]" bind:clientWidth={idBadgeWidth}>
|
||||
{#if id && id !== 'preprocessor' && !id.startsWith('failure')}
|
||||
<Badge color="indigo">{id}</Badge>
|
||||
<Badge color="indigo" wrapperClass="max-w-full" baseClass="max-w-full truncate" title={id}>
|
||||
<span class="max-w-full text-2xs truncate">{id}</span></Badge
|
||||
>
|
||||
{#if deletable}
|
||||
<button
|
||||
class="absolute -left-[28px] z-10 h-[20px] rounded-l rounded-t rounded-s w-[20px] trash center-center text-secondary bg-surface duration-150 hover:bg-blue-400 {editId
|
||||
|
||||
@@ -40,12 +40,12 @@
|
||||
export let disableTutorials = false
|
||||
export let disableAi = false
|
||||
export let disableSettings = false
|
||||
|
||||
export let newFlow: boolean = false
|
||||
export let smallErrorHandler = false
|
||||
|
||||
let flowTutorials: FlowTutorials | undefined = undefined
|
||||
|
||||
const { selectedId, moving, history, flowStateStore, flowStore, flowInputsStore } =
|
||||
const { selectedId, moving, history, flowStateStore, flowStore, flowInputsStore, pathStore } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
async function insertNewModuleAtIndex(
|
||||
@@ -320,6 +320,8 @@
|
||||
|
||||
<div class="z-10 flex-auto grow bg-surface-secondary" bind:clientHeight={minHeight}>
|
||||
<FlowGraphV2
|
||||
path={$pathStore}
|
||||
{newFlow}
|
||||
{disableAi}
|
||||
insertable
|
||||
scroll
|
||||
|
||||
@@ -11,11 +11,9 @@
|
||||
import { msToSec } from '$lib/utils'
|
||||
import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte'
|
||||
import FlowJobsMenu from './FlowJobsMenu.svelte'
|
||||
import FlowCopilotButton from './FlowCopilotButton.svelte'
|
||||
|
||||
export let mod: FlowModule
|
||||
export let insertable: boolean
|
||||
export let disableAi: boolean
|
||||
export let annotation: string | undefined = undefined
|
||||
export let bgColor: string = ''
|
||||
export let modules: FlowModule[]
|
||||
@@ -63,9 +61,6 @@
|
||||
|
||||
{#if mod}
|
||||
<div class="relative">
|
||||
{#if insertable && !disableAi && mod.id === 'preprocessor'}
|
||||
<FlowCopilotButton className="-top-10" />
|
||||
{/if}
|
||||
{#if moving == mod.id}
|
||||
<div class="absolute z-10 right-20 top-0.5 center-center">
|
||||
<Button color="dark" on:click={() => dispatch('move')} size="xs" variant="border">
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
import { ScheduleService } from '$lib/gen'
|
||||
|
||||
export type Schedule = {
|
||||
summary: string | undefined
|
||||
args: Record<string, any>
|
||||
cron: string
|
||||
timezone: string
|
||||
enabled: boolean
|
||||
}
|
||||
import type { ScheduleTrigger } from '../triggers'
|
||||
|
||||
// Load the schedule of a flow given its path and the workspace
|
||||
export async function loadFlowSchedule(path: string, workspace: string): Promise<Schedule> {
|
||||
export async function loadFlowSchedule(path: string, workspace: string): Promise<ScheduleTrigger> {
|
||||
const existsSchedule = await ScheduleService.existsSchedule({
|
||||
workspace,
|
||||
path
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { History } from '$lib/history'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import type ScriptEditorDrawer from './content/ScriptEditorDrawer.svelte'
|
||||
import type { FlowState } from './flowState'
|
||||
import type { Schedule } from './scheduleUtils'
|
||||
import type { FlowBuilderWhitelabelCustomUi } from '../custom_ui'
|
||||
|
||||
export type FlowInput = Record<
|
||||
@@ -29,7 +28,6 @@ export type ExtendedOpenFlow = OpenFlow & {
|
||||
export type FlowEditorContext = {
|
||||
selectedId: Writable<string>
|
||||
moving: Writable<{ module: FlowModule; modules: FlowModule[] } | undefined>
|
||||
schedule: Writable<Schedule>
|
||||
previewArgs: Writable<Record<string, any>>
|
||||
scriptEditorDrawer: Writable<ScriptEditorDrawer | undefined>
|
||||
history: History<OpenFlow>
|
||||
|
||||
@@ -32,10 +32,11 @@
|
||||
import { encodeState } from '$lib/utils'
|
||||
import BranchOneStart from './renderers/nodes/BranchOneStart.svelte'
|
||||
import NoBranchNode from './renderers/nodes/NoBranchNode.svelte'
|
||||
import HiddenBaseEdge from './renderers/edges/HiddenBaseEdge.svelte'
|
||||
import TriggersNode from './renderers/nodes/TriggersNode.svelte'
|
||||
import { Alert, Drawer } from '../common'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import FlowYamlEditor from '../flows/header/FlowYamlEditor.svelte'
|
||||
|
||||
export let success: boolean | undefined = undefined
|
||||
export let modules: FlowModule[] | undefined = []
|
||||
export let failureModule: FlowModule | undefined = undefined
|
||||
@@ -46,6 +47,8 @@
|
||||
export let flowModuleStates: Record<string, GraphModuleState> | undefined = undefined
|
||||
|
||||
export let selectedId: Writable<string | undefined> = writable<string | undefined>(undefined)
|
||||
export let path: string | undefined = undefined
|
||||
export let newFlow: boolean = false
|
||||
|
||||
export let insertable = false
|
||||
export let scroll = false
|
||||
@@ -130,7 +133,9 @@
|
||||
disableAi,
|
||||
insertable,
|
||||
flowModuleStates,
|
||||
selectedId: $selectedId
|
||||
selectedId: $selectedId,
|
||||
path,
|
||||
newFlow
|
||||
},
|
||||
failureModule,
|
||||
preprocessorModule,
|
||||
@@ -172,7 +177,10 @@
|
||||
success,
|
||||
$useDataflow,
|
||||
$selectedId,
|
||||
moving
|
||||
moving,
|
||||
{
|
||||
path
|
||||
}
|
||||
)
|
||||
|
||||
const nodes = writable<Node[]>([])
|
||||
@@ -205,13 +213,15 @@
|
||||
whileLoopEnd: ForLoopEndNode,
|
||||
branchOneStart: BranchOneStart,
|
||||
branchOneEnd: BranchAllEndNode,
|
||||
noBranch: NoBranchNode
|
||||
noBranch: NoBranchNode,
|
||||
trigger: TriggersNode
|
||||
} as any
|
||||
|
||||
const edgeTypes = {
|
||||
edge: BaseEdge,
|
||||
empty: EmptyEdge,
|
||||
dataflowedge: DataflowEdge
|
||||
dataflowedge: DataflowEdge,
|
||||
hiddenedge: HiddenBaseEdge
|
||||
} as any
|
||||
|
||||
const proOptions = { hideAttribution: true }
|
||||
@@ -222,7 +232,8 @@
|
||||
!$selectedId.startsWith('settings') &&
|
||||
$selectedId !== 'failure' &&
|
||||
$selectedId !== 'preprocessor' &&
|
||||
$selectedId !== 'Result'
|
||||
$selectedId !== 'Result' &&
|
||||
$selectedId !== 'triggers'
|
||||
|
||||
const viewport = writable<Viewport>({
|
||||
x: 0,
|
||||
|
||||
@@ -25,7 +25,10 @@ export default function graphBuilder(
|
||||
success: boolean | undefined,
|
||||
useDataflow: boolean | undefined,
|
||||
selectedId: string | undefined,
|
||||
moving: string | undefined
|
||||
moving: string | undefined,
|
||||
triggerProps?: {
|
||||
path?: string
|
||||
}
|
||||
): {
|
||||
nodes: Node[]
|
||||
edges: Edge[]
|
||||
@@ -152,6 +155,29 @@ export default function graphBuilder(
|
||||
}
|
||||
}
|
||||
|
||||
if (extra.path) {
|
||||
const triggerNode: Node = {
|
||||
id: 'Trigger',
|
||||
position: { x: -1, y: -1 },
|
||||
type: 'trigger',
|
||||
data: {
|
||||
path: triggerProps?.path,
|
||||
newFlow: extra.newFlow,
|
||||
eventHandlers: eventHandlers
|
||||
}
|
||||
}
|
||||
nodes.push(triggerNode)
|
||||
if (preprocessorModule != null && preprocessorModule != undefined) {
|
||||
addEdge('Trigger', preprocessorModule.id, {
|
||||
type: 'empty'
|
||||
})
|
||||
} else {
|
||||
addEdge('Trigger', 'Input', {
|
||||
type: 'empty'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const resultNode: Node = {
|
||||
id: 'result',
|
||||
data: {
|
||||
@@ -549,5 +575,4 @@ export default function graphBuilder(
|
||||
error: e
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { BaseEdge } from '@xyflow/svelte'
|
||||
export let markerEnd: string | undefined = undefined
|
||||
</script>
|
||||
|
||||
<BaseEdge path={''} {markerEnd} class="hidden" />
|
||||
@@ -6,7 +6,6 @@
|
||||
import { getStateColor } from '../../util'
|
||||
import { getContext } from 'svelte'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import FlowCopilotButton from '$lib/components/flows/map/FlowCopilotButton.svelte'
|
||||
import InsertModuleButton from '$lib/components/flows/map/InsertModuleButton.svelte'
|
||||
|
||||
export let data: {
|
||||
@@ -27,11 +26,8 @@
|
||||
</script>
|
||||
|
||||
<NodeWrapper let:darkMode>
|
||||
{#if data.insertable && !data.disableAi && !data.hasPreprocessor}
|
||||
<FlowCopilotButton className="-top-[4.25rem]" />
|
||||
{/if}
|
||||
{#if data.insertable && !data.hasPreprocessor}
|
||||
<div class="absolute -top-8 left-1/2 transform -translate-x-1/2 z-10">
|
||||
<div class="absolute bottom-full left-0 right-0 flex center-center mb-3.5">
|
||||
<InsertModuleButton
|
||||
disableAi={data.disableAi}
|
||||
index={data.index ?? 0}
|
||||
|
||||
@@ -61,7 +61,6 @@
|
||||
moving={data.moving}
|
||||
duration_ms={state?.duration_ms}
|
||||
retries={data.retries}
|
||||
disableAi={data.disableAi}
|
||||
{flowJobs}
|
||||
on:delete={(e) => {
|
||||
data.eventHandlers.delete(e.detail, '')
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
<script lang="ts">
|
||||
import DarkModeObserver from '$lib/components/DarkModeObserver.svelte'
|
||||
import { Handle, Position } from '@xyflow/svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let enableSourceHandle: boolean = true
|
||||
export let enableTargetHandle: boolean = true
|
||||
export let offset: number = 0
|
||||
export let wrapperClass: string = ''
|
||||
|
||||
let darkMode: boolean = false
|
||||
</script>
|
||||
|
||||
<DarkModeObserver bind:darkMode />
|
||||
|
||||
<div class="relative shadow-md" style={`margin-left: ${offset}px;`}>
|
||||
<div class={twMerge('relative shadow-md', wrapperClass)} style={`margin-left: ${offset}px;`}>
|
||||
<slot {darkMode} />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import NodeWrapper from './NodeWrapper.svelte'
|
||||
import TriggersWrapper from '../triggers/TriggersWrapper.svelte'
|
||||
import { type GraphEventHandlers } from '../../graphBuilder'
|
||||
import { getContext } from 'svelte'
|
||||
import type { Writable } from 'svelte/store'
|
||||
|
||||
export let data: {
|
||||
path: string
|
||||
isEditor: boolean
|
||||
newFlow: boolean
|
||||
extra_perms: Record<string, any>
|
||||
eventHandlers: GraphEventHandlers
|
||||
}
|
||||
|
||||
const { selectedId } = getContext<{
|
||||
selectedId: Writable<string | undefined>
|
||||
}>('FlowGraphContext')
|
||||
</script>
|
||||
|
||||
<NodeWrapper wrapperClass="shadow-none">
|
||||
<TriggersWrapper
|
||||
path={data.path}
|
||||
on:select={() => {
|
||||
data?.eventHandlers?.select('triggers')
|
||||
}}
|
||||
isFlow={true}
|
||||
selected={$selectedId == 'triggers'}
|
||||
newItem={data.newFlow}
|
||||
/>
|
||||
</NodeWrapper>
|
||||
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let selected: boolean
|
||||
</script>
|
||||
|
||||
<button
|
||||
on:click
|
||||
class={twMerge(
|
||||
'hover:bg-surface-hover rounded-md border text-xs w-6 h-6 relative center-center cursor-pointer bg-surface',
|
||||
selected ? 'outline-1 outline-tertiary outline' : 'outline-0'
|
||||
)}
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
|
||||
export let count: number | undefined = undefined
|
||||
</script>
|
||||
|
||||
{#if count && count > 0}
|
||||
<div class="absolute -right-2 -top-2">
|
||||
<div
|
||||
class="bg-surface-secondary font-bold border border-surface-border rounded-md h-4 w-4 p-1 flex center-center text-primary text-xs"
|
||||
>
|
||||
{#if count === undefined}
|
||||
<Loader2 class="animate-spin text-2xs" />
|
||||
{:else}
|
||||
{count}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,106 @@
|
||||
<script lang="ts">
|
||||
import { Calendar, Mail, Webhook } from 'lucide-svelte'
|
||||
import TriggerButton from './TriggerButton.svelte'
|
||||
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
import TriggerCount from './TriggerCount.svelte'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import { Route } from 'lucide-svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import { type TriggerContext } from '$lib/components/triggers'
|
||||
import { FlowService, ScriptService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
|
||||
const { selectedTrigger, triggersCount } = getContext<TriggerContext>('TriggerContext')
|
||||
|
||||
export let path: string
|
||||
export let newItem: boolean
|
||||
export let isFlow: boolean
|
||||
export let selected: boolean
|
||||
export let showOnlyWithCount: boolean
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
onMount(() => {
|
||||
if (!newItem) {
|
||||
loadCount()
|
||||
}
|
||||
})
|
||||
|
||||
async function loadCount() {
|
||||
if (isFlow) {
|
||||
$triggersCount = await FlowService.getTriggersCountOfFlow({
|
||||
workspace: $workspaceStore!,
|
||||
path
|
||||
})
|
||||
} else {
|
||||
$triggersCount = await ScriptService.getTriggersCountOfScript({
|
||||
workspace: $workspaceStore!,
|
||||
path
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !showOnlyWithCount || ($triggersCount?.webhook_count ?? 0) > 0}
|
||||
<Popover>
|
||||
<svelte:fragment slot="text">Webhooks</svelte:fragment>
|
||||
<TriggerButton
|
||||
on:click={() => {
|
||||
$selectedTrigger = 'webhooks'
|
||||
dispatch('select')
|
||||
}}
|
||||
selected={selected && $selectedTrigger === 'webhooks'}
|
||||
>
|
||||
<TriggerCount count={$triggersCount?.webhook_count} />
|
||||
<Webhook size={12} />
|
||||
</TriggerButton>
|
||||
</Popover>
|
||||
{/if}
|
||||
|
||||
{#if !showOnlyWithCount || ($triggersCount?.schedule_count ?? 0) > 0}
|
||||
<Popover>
|
||||
<svelte:fragment slot="text">Schedules</svelte:fragment>
|
||||
<TriggerButton
|
||||
on:click={() => {
|
||||
$selectedTrigger = 'schedules'
|
||||
dispatch('select')
|
||||
}}
|
||||
selected={selected && $selectedTrigger === 'schedules'}
|
||||
>
|
||||
<TriggerCount count={$triggersCount?.schedule_count} />
|
||||
<Calendar size={12} />
|
||||
</TriggerButton>
|
||||
</Popover>
|
||||
{/if}
|
||||
|
||||
{#if !showOnlyWithCount || ($triggersCount?.http_routes_count ?? 0) > 0}
|
||||
<Popover>
|
||||
<svelte:fragment slot="text">HTTP Routes</svelte:fragment>
|
||||
<TriggerButton
|
||||
on:click={() => {
|
||||
$selectedTrigger = 'routes'
|
||||
dispatch('select')
|
||||
}}
|
||||
selected={selected && $selectedTrigger === 'routes'}
|
||||
>
|
||||
<TriggerCount count={$triggersCount?.http_routes_count} />
|
||||
<Route size={12} />
|
||||
</TriggerButton>
|
||||
</Popover>
|
||||
{/if}
|
||||
|
||||
{#if !showOnlyWithCount || ($triggersCount?.email_count ?? 0) > 0}
|
||||
<Popover>
|
||||
<svelte:fragment slot="text">Emails</svelte:fragment>
|
||||
<TriggerButton
|
||||
on:click={() => {
|
||||
$selectedTrigger = 'emails'
|
||||
dispatch('select')
|
||||
}}
|
||||
selected={selected && $selectedTrigger === 'emails'}
|
||||
>
|
||||
<TriggerCount count={$triggersCount?.email_count} />
|
||||
<Mail size={12} />
|
||||
</TriggerButton>
|
||||
</Popover>
|
||||
{/if}
|
||||
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { NODE } from '../../util'
|
||||
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
import TriggersBadge from './TriggersBadge.svelte'
|
||||
|
||||
export let path: string
|
||||
export let newItem: boolean
|
||||
export let isFlow: boolean
|
||||
export let selected: boolean
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<div style={`width: ${NODE.width}px;`}>
|
||||
<div class="flex flex-row mx-auto w-min">
|
||||
<button
|
||||
class="flex flex-row gap-2 px-2 border p-1 rounded-md bg-surface shadow-md items-center {selected
|
||||
? 'outline outline-offset-1 outline-2 outline-slate-900 dark:bg-white/5 dark:outline-slate-800/60 dark:border-gray-400'
|
||||
: ''}"
|
||||
on:click={() => {
|
||||
dispatch('select')
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-row items-center text-2xs font-normal"> Triggers </div>
|
||||
</div>
|
||||
<TriggersBadge showOnlyWithCount={false} {path} {newItem} {isFlow} {selected} on:select />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,8 @@
|
||||
//regex that match path starting with u/ or f/ and with at least 2 /
|
||||
const pathRegex = /\b(u|f)\/[^\/]+\/[^\/]+\b/g
|
||||
|
||||
export function processMessage(message: string): string {
|
||||
return message.replaceAll(pathRegex, (path) => {
|
||||
return `<span class="bg-surface-secondary p-1 text-xs font-mono whitespace-nowrap rounded-md">${path}</span>`
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { TriggersCount } from '$lib/gen'
|
||||
import type { Writable } from 'svelte/store'
|
||||
|
||||
export type ScheduleTrigger = {
|
||||
summary: string | undefined
|
||||
args: Record<string, any>
|
||||
cron: string
|
||||
timezone: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export type TriggerContext = {
|
||||
selectedTrigger: Writable<'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes'>
|
||||
primarySchedule: Writable<ScheduleTrigger | undefined | false>
|
||||
triggersCount: Writable<TriggersCount | undefined>
|
||||
}
|
||||
@@ -15,6 +15,10 @@
|
||||
drawer?.openNew(is_flow, initial_script_path)
|
||||
}
|
||||
|
||||
export async function getTriggers() {
|
||||
return drawer?.getTriggers()
|
||||
}
|
||||
|
||||
let drawer: RouteEditorInner
|
||||
</script>
|
||||
|
||||
|
||||
@@ -7,18 +7,24 @@
|
||||
import Skeleton from '../common/skeleton/Skeleton.svelte'
|
||||
import RouteEditor from './RouteEditor.svelte'
|
||||
import { canWrite } from '$lib/utils'
|
||||
import Alert from '../common/alert/Alert.svelte'
|
||||
import type { TriggerContext } from '../triggers'
|
||||
import { getContext } from 'svelte'
|
||||
|
||||
export let isFlow: boolean
|
||||
export let path: string
|
||||
export let newItem: boolean = false
|
||||
|
||||
let routeEditor: RouteEditor
|
||||
|
||||
let triggers: (HttpTrigger & { canWrite: boolean })[] | undefined = undefined
|
||||
|
||||
$: path && loadTriggers()
|
||||
async function loadTriggers() {
|
||||
|
||||
const { triggersCount } = getContext<TriggerContext>('TriggerContext')
|
||||
|
||||
let httpTriggers: (HttpTrigger & { canWrite: boolean })[] | undefined = undefined
|
||||
export async function loadTriggers() {
|
||||
try {
|
||||
triggers = (
|
||||
httpTriggers = (
|
||||
await HttpTriggerService.listHttpTriggers({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path,
|
||||
@@ -27,8 +33,9 @@
|
||||
).map((x) => {
|
||||
return { canWrite: canWrite(x.path, x.extra_perms!, $userStore), ...x }
|
||||
})
|
||||
$triggersCount = { ...($triggersCount ?? {}), http_routes_count: httpTriggers?.length }
|
||||
} catch (e) {
|
||||
console.error('impossible to load http routes')
|
||||
console.error('impossible to load http routes', e)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -40,8 +47,8 @@
|
||||
bind:this={routeEditor}
|
||||
/>
|
||||
|
||||
<div class="p-2 flex flex-col">
|
||||
{#if $userStore?.is_admin || $userStore?.is_super_admin}
|
||||
<div class="flex flex-col gap-4">
|
||||
{#if !newItem && ($userStore?.is_admin || $userStore?.is_super_admin)}
|
||||
<Button
|
||||
on:click={() => routeEditor?.openNew(isFlow, path)}
|
||||
variant="border"
|
||||
@@ -52,30 +59,41 @@
|
||||
New Route
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if triggers}
|
||||
{#if triggers.length == 0}
|
||||
<div class="text-xs text-secondary px-2"> No http routes </div>
|
||||
{:else}
|
||||
<div class="flex flex-col divide-y px-2 pt-2">
|
||||
{#each triggers as trigger (trigger.path)}
|
||||
<div class="grid grid-cols-5 text-2xs items-center py-2">
|
||||
<div class="col-span-2 truncate">{trigger.path}</div>
|
||||
<div class="col-span-2 truncate">
|
||||
{trigger.http_method.toUpperCase()} /{trigger.route_path}
|
||||
{#if httpTriggers}
|
||||
{#if httpTriggers.length == 0}
|
||||
<div class="text-xs text-secondary"> No http routes </div>
|
||||
{:else}
|
||||
<div class="flex flex-col divide-y pt-2">
|
||||
{#each httpTriggers as httpTriggers (httpTriggers.path)}
|
||||
<div class="grid grid-cols-5 text-2xs items-center py-2">
|
||||
<div class="col-span-2 truncate">{httpTriggers.path}</div>
|
||||
<div class="col-span-2 truncate">
|
||||
{httpTriggers.http_method.toUpperCase()} /{httpTriggers.route_path}
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
on:click={() => routeEditor?.openEdit(httpTriggers.path, isFlow)}
|
||||
class="px-2"
|
||||
>
|
||||
{#if httpTriggers.canWrite}
|
||||
Edit
|
||||
{:else}
|
||||
View
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button on:click={() => routeEditor?.openEdit(trigger.path, isFlow)}>
|
||||
{#if trigger.canWrite}
|
||||
Edit
|
||||
{:else}
|
||||
View
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<Skeleton layout={[[8]]} />
|
||||
{/if}
|
||||
{:else}
|
||||
<Skeleton layout={[[8]]} />
|
||||
{/if}
|
||||
|
||||
{#if newItem}
|
||||
<Alert title="Triggers disabled" type="warning" size="xs">
|
||||
Deploy the {isFlow ? 'flow' : 'script'} to enable http routes triggers.
|
||||
</Alert>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<script lang="ts">
|
||||
import { FlowService, ScriptService, UserService, type TruncatedToken } from '$lib/gen'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { getContext } from 'svelte'
|
||||
import { Skeleton } from '../common'
|
||||
import Label from '../Label.svelte'
|
||||
import type { TriggerContext } from '../triggers'
|
||||
import { capitalize } from '$lib/utils'
|
||||
|
||||
export let isFlow: boolean
|
||||
export let path: string
|
||||
export let labelPrefix: 'email' | 'webhook'
|
||||
|
||||
const { triggersCount } = getContext<TriggerContext>('TriggerContext')
|
||||
|
||||
let tokens: TruncatedToken[] | undefined = undefined
|
||||
|
||||
export async function listTokens() {
|
||||
tokens = (
|
||||
isFlow
|
||||
? await FlowService.listTokensOfFlow({ workspace: $workspaceStore!, path })
|
||||
: await ScriptService.listTokensOfScript({ workspace: $workspaceStore!, path })
|
||||
).filter((x) => x.label && x.label.startsWith(labelPrefix + '-'))
|
||||
if (labelPrefix == 'email') {
|
||||
$triggersCount = { ...($triggersCount ?? {}), email_count: tokens?.length }
|
||||
} else {
|
||||
$triggersCount = { ...($triggersCount ?? {}), webhook_count: tokens?.length }
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteToken(tokenPrefix: string) {
|
||||
await UserService.deleteToken({
|
||||
tokenPrefix: tokenPrefix
|
||||
})
|
||||
await listTokens()
|
||||
}
|
||||
|
||||
$: $workspaceStore && listTokens()
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<Label label="Existing {capitalize(labelPrefix)} Tokens">
|
||||
{#if tokens}
|
||||
{#if tokens.length == 0}
|
||||
<div class="text-xs text-secondary">No {labelPrefix} specific tokens found</div>
|
||||
{:else}
|
||||
<div class="flex flex-col divide-y pt-2">
|
||||
<div class="grid grid-cols-6 text-2xs items-center py-2">
|
||||
<div class="col-span-2 truncate font-semibold">Prefix</div>
|
||||
<div class="col-span-2 truncate font-semibold">Label</div>
|
||||
<div class="col-span-1 truncate font-semibold">Owner</div>
|
||||
<div class="col-span-1" />
|
||||
</div>
|
||||
{#each tokens as token (token.token_prefix)}
|
||||
<div class="grid grid-cols-6 text-2xs items-center py-2">
|
||||
<div class="col-span-2 truncate">{token.token_prefix}***</div>
|
||||
<div class="col-span-2 truncate">
|
||||
{token.label}
|
||||
</div>
|
||||
<div class="col-span-1 truncate">
|
||||
{token.email}
|
||||
</div>
|
||||
{#if token.email == $userStore?.email}
|
||||
<button
|
||||
on:click={() => deleteToken(token.token_prefix)}
|
||||
class="text-xs text-secondary">delete</button
|
||||
>
|
||||
{:else}
|
||||
<div class="col-span-1" />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<Skeleton layout={[[8]]} />
|
||||
{/if}
|
||||
</Label>
|
||||
</div>
|
||||
@@ -0,0 +1,77 @@
|
||||
<script lang="ts">
|
||||
import Tab from '$lib/components/common/tabs/Tab.svelte'
|
||||
import { Tabs } from '$lib/components/common'
|
||||
import WebhooksPanel from '$lib/components/triggers/WebhooksPanel.svelte'
|
||||
import EmailTriggerPanel from '$lib/components/details/EmailTriggerPanel.svelte'
|
||||
import RoutesPanel from '$lib/components/triggers/RoutesPanel.svelte'
|
||||
import RunPageSchedules from '$lib/components/RunPageSchedules.svelte'
|
||||
import { canWrite } from '$lib/utils'
|
||||
import { userStore } from '$lib/stores'
|
||||
import FlowCard from '../flows/common/FlowCard.svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import type { TriggerContext } from '$lib/components/triggers'
|
||||
|
||||
export let noEditor: boolean
|
||||
export let newItem = false
|
||||
export let currentPath: string
|
||||
export let initialPath: string
|
||||
export let schema: any
|
||||
export let isFlow: boolean
|
||||
|
||||
const { selectedTrigger } = getContext<TriggerContext>('TriggerContext')
|
||||
</script>
|
||||
|
||||
<FlowCard {noEditor} title="Triggers">
|
||||
<div class="pt-4">
|
||||
<Tabs bind:selected={$selectedTrigger}>
|
||||
<Tab value="webhooks" selectedClass="text-primary font-semibold">Webhooks</Tab>
|
||||
<Tab value="schedules" selectedClass="text-primary text-sm font-semibold">Schedules</Tab>
|
||||
<Tab value="routes" selectedClass="text-primary text-sm font-semibold">Routes</Tab>
|
||||
<Tab value="emails" selectedClass="text-primary text-sm font-semibold">Email</Tab>
|
||||
|
||||
<svelte:fragment slot="content">
|
||||
{#if $selectedTrigger === 'webhooks'}
|
||||
<div class="p-4">
|
||||
<WebhooksPanel
|
||||
scopes={isFlow ? [`run:flow/${currentPath}`] : [`run:script/${currentPath}`]}
|
||||
path={currentPath}
|
||||
{isFlow}
|
||||
args={{}}
|
||||
token=""
|
||||
{newItem}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $selectedTrigger === 'emails'}
|
||||
<div class="p-4">
|
||||
<EmailTriggerPanel
|
||||
token=""
|
||||
scopes={isFlow ? [`run:flow/${currentPath}`] : [`run:script/${currentPath}`]}
|
||||
path={currentPath}
|
||||
{isFlow}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $selectedTrigger === 'routes'}
|
||||
<div class="p-4">
|
||||
<RoutesPanel {newItem} path={currentPath} {isFlow} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $selectedTrigger === 'schedules'}
|
||||
<div class="p-2">
|
||||
<RunPageSchedules
|
||||
{schema}
|
||||
{isFlow}
|
||||
path={initialPath}
|
||||
{newItem}
|
||||
can_write={canWrite(currentPath, {}, $userStore)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
</div>
|
||||
</FlowCard>
|
||||
+52
-24
@@ -3,7 +3,7 @@
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import bash from 'svelte-highlight/languages/bash'
|
||||
import { Tabs, Tab, TabContent, Button } from '$lib/components/common'
|
||||
import { Tabs, Tab, TabContent, Button, Alert } from '$lib/components/common'
|
||||
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
|
||||
import {
|
||||
@@ -15,10 +15,12 @@
|
||||
import UserSettings from '../UserSettings.svelte'
|
||||
import { Highlight } from 'svelte-highlight'
|
||||
import { typescript } from 'svelte-highlight/languages'
|
||||
import ClipboardPanel from './ClipboardPanel.svelte'
|
||||
import ClipboardPanel from '../details/ClipboardPanel.svelte'
|
||||
import { copyToClipboard, generateRandomString } from '$lib/utils'
|
||||
import HighlightTheme from '../HighlightTheme.svelte'
|
||||
import { base } from '$lib/base'
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import TriggerTokens from './TriggerTokens.svelte'
|
||||
let userSettings: UserSettings
|
||||
|
||||
export let token: string
|
||||
@@ -27,7 +29,8 @@
|
||||
export let isFlow: boolean = false
|
||||
export let hash: string | undefined = undefined
|
||||
export let path: string
|
||||
|
||||
export let url: string = ''
|
||||
export let newItem: boolean = false
|
||||
let selectedTab: string = 'rest'
|
||||
|
||||
let webhooks: {
|
||||
@@ -219,6 +222,8 @@ while true; do
|
||||
done`
|
||||
}`
|
||||
}
|
||||
|
||||
let triggerTokens: TriggerTokens | undefined = undefined
|
||||
</script>
|
||||
|
||||
<HighlightTheme />
|
||||
@@ -227,32 +232,36 @@ done`
|
||||
bind:this={userSettings}
|
||||
on:tokenCreated={(e) => {
|
||||
token = e.detail
|
||||
triggerTokens?.listTokens()
|
||||
}}
|
||||
newTokenWorkspace={$workspaceStore}
|
||||
newTokenLabel={`webhook-${$userStore?.username ?? 'superadmin'}-${generateRandomString(4)}`}
|
||||
{scopes}
|
||||
/>
|
||||
|
||||
<div class="p-2 flex flex-col w-full gap-4">
|
||||
<div class="flex flex-col w-full gap-4">
|
||||
{#if SCRIPT_VIEW_SHOW_CREATE_TOKEN_BUTTON}
|
||||
<div class="flex flex-row justify-between my-2 gap-2">
|
||||
<input
|
||||
bind:value={token}
|
||||
placeholder="paste your token here once created to alter examples below"
|
||||
class="!text-xs"
|
||||
/>
|
||||
<Button size="xs" color="light" variant="border" on:click={userSettings.openDrawer}>
|
||||
Create a Webhook-specific Token
|
||||
<Tooltip light>
|
||||
The token will have a scope such that it can only be used to trigger this script. It is
|
||||
safe to share as it cannot be used to impersonate you.
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</div>
|
||||
<Label label="Token">
|
||||
<div class="flex flex-row justify-between gap-2">
|
||||
<input
|
||||
bind:value={token}
|
||||
placeholder="paste your token here once created to alter examples below"
|
||||
class="!text-xs !font-normal"
|
||||
/>
|
||||
<Button size="xs" color="light" variant="border" on:click={userSettings.openDrawer}>
|
||||
Create a Webhook-specific Token
|
||||
<Tooltip light>
|
||||
The token will have a scope such that it can only be used to trigger this script. It is
|
||||
safe to share as it cannot be used to impersonate you.
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</div>
|
||||
</Label>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="text-xs font-semibold flex flex-row items-center">Request type</div>
|
||||
<div class="text-sm font-normal text-secondary flex flex-row items-center">Request type</div>
|
||||
<ToggleButtonGroup class="h-[30px] w-auto" bind:selected={webhookType}>
|
||||
<ToggleButton
|
||||
label="Async"
|
||||
@@ -267,7 +276,7 @@ done`
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="text-xs font-semibold flex flex-row items-center">Call method</div>
|
||||
<div class="text-sm font-normal text-secondary flex flex-row items-center">Call method</div>
|
||||
<ToggleButtonGroup class="h-[30px] w-auto" bind:selected={requestType}>
|
||||
<ToggleButton
|
||||
label="POST by path"
|
||||
@@ -294,7 +303,9 @@ done`
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="text-xs font-semibold flex flex-row items-center">Token configuration</div>
|
||||
<div class="text-sm font-normal text-secondary flex flex-row items-center"
|
||||
>Token configuration</div
|
||||
>
|
||||
<ToggleButtonGroup class="h-[30px] w-auto" bind:selected={tokenType}>
|
||||
<ToggleButton label="Token in Headers" value="headers" />
|
||||
<ToggleButton label="Token in Query" value="query" />
|
||||
@@ -314,14 +325,20 @@ done`
|
||||
{#key token}
|
||||
<TabContent value="rest" class="flex flex-col flex-1 h-full ">
|
||||
<div class="flex flex-col gap-2">
|
||||
<ClipboardPanel title="Url" content={url} />
|
||||
<Label label="Url">
|
||||
<ClipboardPanel content={url} />
|
||||
</Label>
|
||||
|
||||
{#if requestType !== 'get_path'}
|
||||
<ClipboardPanel title="Body" content={JSON.stringify(args, null, 2)} />
|
||||
<Label label="Body">
|
||||
<ClipboardPanel content={JSON.stringify(args, null, 2)} />
|
||||
</Label>
|
||||
{/if}
|
||||
{#key requestType}
|
||||
{#key tokenType}
|
||||
<ClipboardPanel title="Headers" content={JSON.stringify(headers(), null, 2)} />
|
||||
<Label label="Headers">
|
||||
<ClipboardPanel content={JSON.stringify(headers(), null, 2)} />
|
||||
</Label>
|
||||
{/key}
|
||||
{/key}
|
||||
</div>
|
||||
@@ -370,4 +387,15 @@ done`
|
||||
{/key}
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
|
||||
<div class="mt-10" />
|
||||
<TriggerTokens bind:this={triggerTokens} {isFlow} {path} labelPrefix="webhook" />
|
||||
|
||||
{#if newItem}
|
||||
<div class="mt-10" />
|
||||
<Alert type="warning" title="Attached to a deployed path">
|
||||
The webhooks are only valid for a given path and will only trigger the deployed version of the
|
||||
{isFlow ? 'flow' : 'script'}.
|
||||
</Alert>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -13,6 +13,7 @@
|
||||
import { decodeState, emptySchema } from '$lib/utils'
|
||||
import { tick } from 'svelte'
|
||||
import { writable } from 'svelte/store'
|
||||
import type { ScheduleTrigger } from '$lib/components/triggers'
|
||||
|
||||
let nodraft = $page.url.searchParams.get('nodraft')
|
||||
|
||||
@@ -53,6 +54,7 @@
|
||||
})
|
||||
const flowStateStore = writable<FlowState>({})
|
||||
|
||||
let savedPrimarySchedule: ScheduleTrigger | undefined = undefined
|
||||
async function loadFlow() {
|
||||
loading = true
|
||||
let flow: Flow = {
|
||||
@@ -97,6 +99,7 @@
|
||||
|
||||
flow = state.flow
|
||||
pathStoreInit = state.path
|
||||
savedPrimarySchedule = state.primarySchedule
|
||||
state?.selectedId && (selectedId = state?.selectedId)
|
||||
} else {
|
||||
if (templatePath) {
|
||||
@@ -172,6 +175,7 @@
|
||||
{flowStateStore}
|
||||
{selectedId}
|
||||
{loading}
|
||||
{savedPrimarySchedule}
|
||||
>
|
||||
<UnsavedConfirmationModal savedValue={savedFlow} modifiedValue={$flowStore} /></FlowBuilder
|
||||
>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import DiffDrawer from '$lib/components/DiffDrawer.svelte'
|
||||
import UnsavedConfirmationModal from '$lib/components/common/confirmationModal/UnsavedConfirmationModal.svelte'
|
||||
import type { ScheduleTrigger } from '$lib/components/triggers'
|
||||
|
||||
let nodraft = $page.url.searchParams.get('nodraft')
|
||||
const initialState = nodraft ? undefined : localStorage.getItem(`flow-${$page.params.path}`)
|
||||
@@ -54,6 +55,11 @@
|
||||
let selectedId: string = 'settings-metadata'
|
||||
|
||||
let nobackenddraft = false
|
||||
|
||||
let savedPrimarySchedule: ScheduleTrigger | undefined = stateLoadedFromUrl?.primarySchedule
|
||||
|
||||
let flowBuilder: FlowBuilder | undefined = undefined
|
||||
|
||||
async function loadFlow(): Promise<void> {
|
||||
loading = true
|
||||
let flow: Flow
|
||||
@@ -67,6 +73,7 @@
|
||||
const draftOrDeployed = cleanValueProperties(savedFlow?.draft || savedFlow)
|
||||
const urlScript = cleanValueProperties(stateLoadedFromUrl.flow)
|
||||
flow = stateLoadedFromUrl.flow
|
||||
savedPrimarySchedule = stateLoadedFromUrl.primarySchedule
|
||||
const reloadAction = () => {
|
||||
stateLoadedFromUrl = undefined
|
||||
goto(`/flows/edit/${statePath}`)
|
||||
@@ -113,6 +120,9 @@
|
||||
}
|
||||
if (flowWithDraft.draft != undefined && !nobackenddraft) {
|
||||
flow = flowWithDraft.draft
|
||||
savedPrimarySchedule = flowWithDraft?.draft?.['primary_schedule']
|
||||
flowBuilder?.setPrimarySchedule(savedPrimarySchedule)
|
||||
|
||||
if (!flowWithDraft.draft_only) {
|
||||
const deployed = cleanValueProperties(flowWithDraft)
|
||||
const draft = cleanValueProperties(flow)
|
||||
@@ -216,8 +226,10 @@
|
||||
{selectedId}
|
||||
{initialArgs}
|
||||
{loading}
|
||||
bind:this={flowBuilder}
|
||||
bind:savedFlow
|
||||
{diffDrawer}
|
||||
{savedPrimarySchedule}
|
||||
>
|
||||
<UnsavedConfirmationModal {diffDrawer} savedValue={savedFlow} modifiedValue={$flowStore} />
|
||||
</FlowBuilder>
|
||||
|
||||
@@ -6,15 +6,16 @@
|
||||
WorkspaceService,
|
||||
type Flow,
|
||||
type FlowModule,
|
||||
type TriggersCount,
|
||||
type WorkspaceDeployUISettings
|
||||
} from '$lib/gen'
|
||||
import { canWrite, defaultIfEmptyString, emptyString } from '$lib/utils'
|
||||
import { canWrite, copyToClipboard, defaultIfEmptyString, emptyString } from '$lib/utils'
|
||||
import { isDeployable, ALL_DEPLOYABLE } from '$lib/utils_deployable'
|
||||
|
||||
import DetailPageLayout from '$lib/components/details/DetailPageLayout.svelte'
|
||||
import { goto } from '$lib/navigation'
|
||||
import { base } from '$lib/base'
|
||||
import { Alert, Button, Badge as HeaderBadge, Skeleton } from '$lib/components/common'
|
||||
import { Alert, Badge as HeaderBadge, Skeleton } from '$lib/components/common'
|
||||
import MoveDrawer from '$lib/components/MoveDrawer.svelte'
|
||||
import RunForm from '$lib/components/RunForm.svelte'
|
||||
import ShareModal from '$lib/components/ShareModal.svelte'
|
||||
@@ -36,27 +37,29 @@
|
||||
Columns,
|
||||
Pen,
|
||||
Eye,
|
||||
Calendar,
|
||||
HistoryIcon
|
||||
HistoryIcon,
|
||||
ClipboardCopy
|
||||
} from 'lucide-svelte'
|
||||
|
||||
import DetailPageHeader from '$lib/components/details/DetailPageHeader.svelte'
|
||||
import WebhooksPanel from '$lib/components/details/WebhooksPanel.svelte'
|
||||
import WebhooksPanel from '$lib/components/triggers/WebhooksPanel.svelte'
|
||||
import CliHelpBox from '$lib/components/CliHelpBox.svelte'
|
||||
import FlowGraphViewer from '$lib/components/FlowGraphViewer.svelte'
|
||||
import SchemaViewer from '$lib/components/SchemaViewer.svelte'
|
||||
import RunPageSchedules from '$lib/components/RunPageSchedules.svelte'
|
||||
import { createAppFromFlow } from '$lib/components/details/createAppFromScript'
|
||||
import { importStore } from '$lib/components/apps/store'
|
||||
import TimeAgo from '$lib/components/TimeAgo.svelte'
|
||||
import ClipboardPanel from '$lib/components/details/ClipboardPanel.svelte'
|
||||
import FlowGraphViewerStep from '$lib/components/FlowGraphViewerStep.svelte'
|
||||
import { loadFlowSchedule, type Schedule } from '$lib/components/flows/scheduleUtils'
|
||||
import GfmMarkdown from '$lib/components/GfmMarkdown.svelte'
|
||||
import FlowHistory from '$lib/components/flows/FlowHistory.svelte'
|
||||
import EmailTriggerPanel from '$lib/components/details/EmailTriggerPanel.svelte'
|
||||
import Star from '$lib/components/Star.svelte'
|
||||
import RoutesPanel from '$lib/components/triggers/RoutesPanel.svelte'
|
||||
import { Highlight } from 'svelte-highlight'
|
||||
import json from 'svelte-highlight/languages/json'
|
||||
import { writable } from 'svelte/store'
|
||||
import TriggersBadge from '$lib/components/graph/renderers/triggers/TriggersBadge.svelte'
|
||||
|
||||
let flow: Flow | undefined
|
||||
let can_write = false
|
||||
@@ -68,6 +71,8 @@
|
||||
let invisible_to_owner: boolean | undefined = undefined
|
||||
let overrideTag: string | undefined = undefined
|
||||
|
||||
const triggersCount = writable<TriggersCount | undefined>(undefined)
|
||||
|
||||
$: cliCommand = `wmill flow run ${flow?.path} -d '${JSON.stringify(args)}'`
|
||||
|
||||
let previousPath: string | undefined = undefined
|
||||
@@ -76,6 +81,7 @@
|
||||
if (previousPath !== path) {
|
||||
previousPath = path
|
||||
loadFlow()
|
||||
loadTriggersCount()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,9 +101,15 @@
|
||||
goto('/')
|
||||
}
|
||||
|
||||
let schedule: Schedule | undefined = undefined
|
||||
let starred: boolean | undefined = undefined
|
||||
|
||||
async function loadTriggersCount() {
|
||||
$triggersCount = await FlowService.getTriggersCountOfFlow({
|
||||
workspace: $workspaceStore!,
|
||||
path
|
||||
})
|
||||
}
|
||||
|
||||
async function loadFlow(): Promise<void> {
|
||||
flow = await FlowService.getFlowByPath({
|
||||
workspace: $workspaceStore!,
|
||||
@@ -109,9 +121,6 @@
|
||||
invisible_to_owner = flow.visible_to_runner_only
|
||||
}
|
||||
can_write = canWrite(flow.path, flow.extra_perms!, $userStore)
|
||||
try {
|
||||
schedule = await loadFlowSchedule(path, $workspaceStore!)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
let isValid = true
|
||||
@@ -326,9 +335,7 @@
|
||||
}
|
||||
let stepDetail: FlowModule | string | undefined = undefined
|
||||
let token = 'TOKEN_TO_CREATE'
|
||||
let detailSelected = 'saved_inputs'
|
||||
|
||||
let triggerSelected: 'webhooks' | 'schedule' | 'cli' = 'webhooks'
|
||||
let rightPaneSelected = 'saved_inputs'
|
||||
|
||||
let flowHistory: FlowHistory | undefined = undefined
|
||||
</script>
|
||||
@@ -354,8 +361,8 @@
|
||||
|
||||
{#if flow}
|
||||
<DetailPageLayout
|
||||
bind:triggerSelected
|
||||
bind:selected={detailSelected}
|
||||
{triggersCount}
|
||||
bind:selected={rightPaneSelected}
|
||||
isOperator={$userStore?.operator}
|
||||
flow_json={{
|
||||
value: flow.value,
|
||||
@@ -363,10 +370,12 @@
|
||||
description: flow.description,
|
||||
schema: flow.schema
|
||||
}}
|
||||
hasStepDetails={Boolean(stepDetail)}
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
<DetailPageHeader
|
||||
on:seeTriggers={() => {
|
||||
rightPaneSelected = 'triggers'
|
||||
}}
|
||||
{mainButtons}
|
||||
menuItems={getMenuItems(flow, deployUiSettings)}
|
||||
title={defaultIfEmptyString(flow.summary, flow.path)}
|
||||
@@ -375,6 +384,18 @@
|
||||
errorHandlerKind="flow"
|
||||
tag={flow.tag}
|
||||
>
|
||||
<svelte:fragment slot="trigger-badges">
|
||||
<TriggersBadge
|
||||
showOnlyWithCount={true}
|
||||
{path}
|
||||
newItem={false}
|
||||
isFlow
|
||||
selected={rightPaneSelected == 'triggers'}
|
||||
on:select={() => {
|
||||
rightPaneSelected = 'triggers'
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
{#if $workspaceStore}
|
||||
<Star
|
||||
kind="flow"
|
||||
@@ -400,21 +421,6 @@
|
||||
</HeaderBadge>
|
||||
</div>
|
||||
{/if}
|
||||
{#if schedule?.enabled}
|
||||
<Button
|
||||
btnClasses="inline-flex"
|
||||
startIcon={{ icon: Calendar }}
|
||||
variant="contained"
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
detailSelected = 'details'
|
||||
triggerSelected = 'schedule'
|
||||
}}
|
||||
>
|
||||
{schedule.cron ?? ''}
|
||||
</Button>
|
||||
{/if}
|
||||
</DetailPageHeader>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="form">
|
||||
@@ -474,10 +480,15 @@
|
||||
on:select={(e) => {
|
||||
if (e.detail) {
|
||||
stepDetail = e.detail
|
||||
rightPaneSelected = 'flow_step'
|
||||
} else {
|
||||
stepDetail = undefined
|
||||
rightPaneSelected = 'saved_inputs'
|
||||
}
|
||||
}}
|
||||
on:triggerDetail={(e) => {
|
||||
rightPaneSelected = 'triggers'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -494,33 +505,47 @@
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="details">
|
||||
<div class="p-1">
|
||||
<SchemaViewer schema={flow.schema} />
|
||||
<svelte:fragment slot="schema">
|
||||
<div class="p-1 relative">
|
||||
<button
|
||||
on:click={() => copyToClipboard(JSON.stringify(flow?.schema, null, 4))}
|
||||
class="absolute top-2 right-2"
|
||||
>
|
||||
<ClipboardCopy size={14} />
|
||||
</button>
|
||||
<Highlight language={json} code={JSON.stringify(flow?.schema, null, 4)} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="webhooks">
|
||||
<WebhooksPanel
|
||||
bind:token
|
||||
scopes={[`run:flow/${flow?.path}`]}
|
||||
path={flow?.path}
|
||||
isFlow={true}
|
||||
{args}
|
||||
/>
|
||||
<div class="p-2">
|
||||
<WebhooksPanel
|
||||
bind:token
|
||||
scopes={[`run:flow/${flow?.path}`]}
|
||||
path={flow?.path}
|
||||
isFlow={true}
|
||||
{args}
|
||||
/>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="routes">
|
||||
<RoutesPanel path={flow.path ?? ''} isFlow />
|
||||
<div class="p-2">
|
||||
<RoutesPanel path={flow.path ?? ''} isFlow />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="email">
|
||||
<EmailTriggerPanel
|
||||
bind:token
|
||||
scopes={[`run:flow/${flow?.path}`]}
|
||||
path={flow?.path}
|
||||
isFlow={true}
|
||||
/>
|
||||
<svelte:fragment slot="emails">
|
||||
<div class="p-2">
|
||||
<EmailTriggerPanel
|
||||
bind:token
|
||||
scopes={[`run:flow/${flow?.path}`]}
|
||||
path={flow?.path}
|
||||
isFlow={true}
|
||||
/>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="schedule">
|
||||
<RunPageSchedules isFlow={true} path={flow.path ?? ''} {can_write} />
|
||||
<svelte:fragment slot="schedules">
|
||||
<div class="p-2">
|
||||
<RunPageSchedules schema={flow.schema} isFlow={true} path={flow.path ?? ''} {can_write} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="cli">
|
||||
<div class="p-2">
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import { goto } from '$lib/navigation'
|
||||
import { replaceState } from '$app/navigation'
|
||||
import UnsavedConfirmationModal from '$lib/components/common/confirmationModal/UnsavedConfirmationModal.svelte'
|
||||
import type { ScheduleTrigger } from '$lib/components/triggers'
|
||||
|
||||
// Default
|
||||
let schema: Schema = emptySchema()
|
||||
@@ -29,10 +30,13 @@
|
||||
const initialState = $page.url.hash != '' ? $page.url.hash.slice(1) : undefined
|
||||
|
||||
let scriptBuilder: ScriptBuilder | undefined = undefined
|
||||
let savedPrimarySchedule: ScheduleTrigger | undefined = undefined
|
||||
|
||||
function decodeStateAndHandleError(state) {
|
||||
try {
|
||||
return decodeState(state)
|
||||
const decoded = decodeState(state)
|
||||
savedPrimarySchedule = decoded.primarySchedule
|
||||
return decoded
|
||||
} catch (e) {
|
||||
console.error('Error decoding state', e)
|
||||
return defaultScript
|
||||
@@ -115,6 +119,7 @@
|
||||
searchParams={$page.url.searchParams}
|
||||
{script}
|
||||
{showMeta}
|
||||
{savedPrimarySchedule}
|
||||
replaceStateFn={(path) => replaceState(path, $page.state)}
|
||||
>
|
||||
<UnsavedConfirmationModal savedValue={savedScript} modifiedValue={script} />
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import DiffDrawer from '$lib/components/DiffDrawer.svelte'
|
||||
import UnsavedConfirmationModal from '$lib/components/common/confirmationModal/UnsavedConfirmationModal.svelte'
|
||||
import type { ScheduleTrigger } from '$lib/components/triggers'
|
||||
|
||||
let initialState = window.location.hash != '' ? window.location.hash.slice(1) : undefined
|
||||
let initialArgs = {}
|
||||
@@ -35,6 +36,8 @@
|
||||
let savedScript: NewScriptWithDraft | undefined = undefined
|
||||
let fullyLoaded = false
|
||||
|
||||
let savedPrimarySchedule: ScheduleTrigger | undefined = scriptLoadedFromUrl?.primarySchedule
|
||||
|
||||
async function loadScript(): Promise<void> {
|
||||
fullyLoaded = false
|
||||
if (scriptLoadedFromUrl != undefined && scriptLoadedFromUrl.path == $page.params.path) {
|
||||
@@ -94,6 +97,10 @@
|
||||
savedScript = structuredClone(scriptWithDraft)
|
||||
if (scriptWithDraft.draft != undefined) {
|
||||
script = scriptWithDraft.draft
|
||||
if (script['primary_schedule']) {
|
||||
savedPrimarySchedule = script['primary_schedule']
|
||||
scriptBuilder?.setPrimarySchedule(savedPrimarySchedule)
|
||||
}
|
||||
if (!scriptWithDraft.draft_only) {
|
||||
reloadAction = async () => {
|
||||
scriptLoadedFromUrl = undefined
|
||||
@@ -199,6 +206,7 @@
|
||||
bind:savedScript
|
||||
{initialArgs}
|
||||
{diffDrawer}
|
||||
{savedPrimarySchedule}
|
||||
searchParams={$page.url.searchParams}
|
||||
on:deploy={(e) => {
|
||||
let newHash = e.detail
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
ScriptService,
|
||||
WorkspaceService,
|
||||
type Script,
|
||||
type TriggersCount,
|
||||
type WorkspaceDeployUISettings
|
||||
} from '$lib/gen'
|
||||
import {
|
||||
@@ -42,7 +43,7 @@
|
||||
import DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte'
|
||||
|
||||
import SavedInputs from '$lib/components/SavedInputs.svelte'
|
||||
import WebhooksPanel from '$lib/components/details/WebhooksPanel.svelte'
|
||||
import WebhooksPanel from '$lib/components/triggers/WebhooksPanel.svelte'
|
||||
import DetailPageLayout from '$lib/components/details/DetailPageLayout.svelte'
|
||||
import DetailPageHeader from '$lib/components/details/DetailPageHeader.svelte'
|
||||
import CliHelpBox from '$lib/components/CliHelpBox.svelte'
|
||||
@@ -50,7 +51,6 @@
|
||||
Activity,
|
||||
Archive,
|
||||
ArchiveRestore,
|
||||
Calendar,
|
||||
Eye,
|
||||
FolderOpen,
|
||||
GitFork,
|
||||
@@ -75,12 +75,15 @@
|
||||
import TimeAgo from '$lib/components/TimeAgo.svelte'
|
||||
import ClipboardPanel from '$lib/components/details/ClipboardPanel.svelte'
|
||||
import PersistentScriptDrawer from '$lib/components/PersistentScriptDrawer.svelte'
|
||||
import { loadScriptSchedule, type ScriptSchedule } from '$lib/scripts'
|
||||
import GfmMarkdown from '$lib/components/GfmMarkdown.svelte'
|
||||
import EmailTriggerPanel from '$lib/components/details/EmailTriggerPanel.svelte'
|
||||
import Star from '$lib/components/Star.svelte'
|
||||
import LogViewer from '$lib/components/LogViewer.svelte'
|
||||
import RoutesPanel from '$lib/components/triggers/RoutesPanel.svelte'
|
||||
import { Highlight } from 'svelte-highlight'
|
||||
import json from 'svelte-highlight/languages/json'
|
||||
import { writable } from 'svelte/store'
|
||||
import TriggersBadge from '$lib/components/graph/renderers/triggers/TriggersBadge.svelte'
|
||||
|
||||
let script: Script | undefined
|
||||
let topHash: string | undefined
|
||||
@@ -106,6 +109,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
const triggersCount = writable<TriggersCount | undefined>(undefined)
|
||||
|
||||
async function deleteScript(hash: string): Promise<void> {
|
||||
try {
|
||||
await ScriptService.deleteScriptByHash({ workspace: $workspaceStore!, hash })
|
||||
@@ -150,9 +155,15 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
let schedule: ScriptSchedule | undefined = undefined
|
||||
let starred: boolean | undefined = undefined
|
||||
|
||||
async function loadTriggersCount(path: string) {
|
||||
$triggersCount = await ScriptService.getTriggersCountOfScript({
|
||||
workspace: $workspaceStore!,
|
||||
path: path
|
||||
})
|
||||
}
|
||||
|
||||
async function loadScript(hash: string): Promise<void> {
|
||||
try {
|
||||
script = await ScriptService.getScriptByHash({
|
||||
@@ -173,7 +184,7 @@
|
||||
can_write =
|
||||
script.workspace_id == $workspaceStore &&
|
||||
canWrite(script.path, script.extra_perms!, $userStore)
|
||||
schedule = await loadScriptSchedule(script.path, $workspaceStore!)
|
||||
loadTriggersCount(script.path)
|
||||
|
||||
if (script.path && script.archived) {
|
||||
const script_by_path = await ScriptService.getScriptByPath({
|
||||
@@ -477,8 +488,7 @@
|
||||
}
|
||||
|
||||
let token = 'TOKEN_TO_CREATE'
|
||||
let detailSelected = 'saved_inputs'
|
||||
let triggerSelected: 'webhooks' | 'schedule' | 'cli' = 'webhooks'
|
||||
let rightPaneSelected = 'saved_inputs'
|
||||
</script>
|
||||
|
||||
<MoveDrawer
|
||||
@@ -512,8 +522,8 @@
|
||||
</Drawer>
|
||||
{#key script.hash}
|
||||
<DetailPageLayout
|
||||
bind:triggerSelected
|
||||
bind:selected={detailSelected}
|
||||
{triggersCount}
|
||||
bind:selected={rightPaneSelected}
|
||||
isOperator={$userStore?.operator}
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
@@ -526,6 +536,18 @@
|
||||
scriptOrFlowPath={script.path}
|
||||
tag={script.tag}
|
||||
>
|
||||
<svelte:fragment slot="trigger-badges">
|
||||
<TriggersBadge
|
||||
showOnlyWithCount={true}
|
||||
path={script.path}
|
||||
newItem={false}
|
||||
isFlow={false}
|
||||
selected={rightPaneSelected == 'triggers'}
|
||||
on:select={() => {
|
||||
rightPaneSelected = 'triggers'
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
{#if $workspaceStore}
|
||||
<Star
|
||||
kind="script"
|
||||
@@ -565,21 +587,6 @@
|
||||
</Badge>
|
||||
</div>
|
||||
{/if}
|
||||
{#if schedule?.enabled}
|
||||
<Button
|
||||
btnClasses="inline-flex"
|
||||
startIcon={{ icon: Calendar }}
|
||||
variant="contained"
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
detailSelected = 'details'
|
||||
triggerSelected = 'schedule'
|
||||
}}
|
||||
>
|
||||
{schedule.cron ?? ''}
|
||||
</Button>
|
||||
{/if}
|
||||
</DetailPageHeader>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="form">
|
||||
@@ -678,6 +685,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="schema">
|
||||
<div class="p-1 relative">
|
||||
<button
|
||||
on:click={() => copyToClipboard(JSON.stringify(script?.schema, null, 4))}
|
||||
class="absolute top-2 right-2"
|
||||
>
|
||||
<ClipboardCopy size={14} />
|
||||
</button>
|
||||
<Highlight language={json} code={JSON.stringify(script?.schema, null, 4)} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="save_inputs">
|
||||
{#if args}
|
||||
<SavedInputs
|
||||
@@ -694,27 +712,40 @@
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="webhooks">
|
||||
<WebhooksPanel
|
||||
bind:token
|
||||
scopes={[`run:script/${script?.path}`]}
|
||||
hash={script.hash}
|
||||
path={script.path}
|
||||
{args}
|
||||
/>
|
||||
<div class="p-2">
|
||||
<WebhooksPanel
|
||||
bind:token
|
||||
scopes={[`run:script/${script?.path}`]}
|
||||
hash={script.hash}
|
||||
path={script.path}
|
||||
{args}
|
||||
/>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="routes">
|
||||
<RoutesPanel path={script.path ?? ''} isFlow={false} />
|
||||
<div class="p-2">
|
||||
<RoutesPanel path={script.path ?? ''} isFlow={false} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="email">
|
||||
<EmailTriggerPanel
|
||||
bind:token
|
||||
scopes={[`run:script/${script?.path}`]}
|
||||
hash={script.hash}
|
||||
path={script.path}
|
||||
/>
|
||||
<svelte:fragment slot="emails">
|
||||
<div class="p-2">
|
||||
<EmailTriggerPanel
|
||||
bind:token
|
||||
scopes={[`run:script/${script?.path}`]}
|
||||
hash={script.hash}
|
||||
path={script.path}
|
||||
/>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="schedule">
|
||||
<RunPageSchedules isFlow={false} path={script.path ?? ''} {can_write} />
|
||||
<svelte:fragment slot="schedules">
|
||||
<div class="p-2 mt-2">
|
||||
<RunPageSchedules
|
||||
schema={script.schema}
|
||||
isFlow={false}
|
||||
path={script.path ?? ''}
|
||||
{can_write}
|
||||
/>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="details">
|
||||
<div>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import FlowPreviewButtons from '$lib/components/flows/header/FlowPreviewButtons.svelte'
|
||||
import type { FlowEditorContext, FlowInput } from '$lib/components/flows/types'
|
||||
import { writable } from 'svelte/store'
|
||||
import { OpenAPI, type FlowModule, type OpenFlow } from '$lib/gen'
|
||||
import { OpenAPI, type FlowModule, type OpenFlow, type TriggersCount } from '$lib/gen'
|
||||
import { initHistory } from '$lib/history'
|
||||
import type { FlowState } from '$lib/components/flows/flowState'
|
||||
import FlowModuleSchemaMap from '$lib/components/flows/map/FlowModuleSchemaMap.svelte'
|
||||
@@ -16,7 +16,7 @@
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { getUserExt } from '$lib/user'
|
||||
import DarkModeToggle from '$lib/components/sidebar/DarkModeToggle.svelte'
|
||||
import type { Schedule } from '$lib/components/flows/scheduleUtils'
|
||||
import type { ScheduleTrigger, TriggerContext } from '$lib/components/triggers'
|
||||
|
||||
let token = $page.url.searchParams.get('wm_token') ?? undefined
|
||||
let workspace = $page.url.searchParams.get('workspace') ?? undefined
|
||||
@@ -63,13 +63,6 @@
|
||||
|
||||
let initialCode = JSON.stringify($flowStore, null, 4)
|
||||
const flowStateStore = writable({} as FlowState)
|
||||
const scheduleStore = writable<Schedule>({
|
||||
args: {},
|
||||
cron: '',
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
enabled: false,
|
||||
summary: undefined
|
||||
})
|
||||
|
||||
const previewArgsStore = writable<Record<string, any>>({})
|
||||
const scriptEditorDrawer = writable(undefined)
|
||||
@@ -78,14 +71,19 @@
|
||||
|
||||
const testStepStore = writable<Record<string, any>>({})
|
||||
const selectedIdStore = writable('settings-metadata')
|
||||
|
||||
// function select(selectedId: string) {
|
||||
// selectedIdStore.set(selectedId)
|
||||
// }
|
||||
const primaryScheduleStore = writable<ScheduleTrigger | undefined | false>(undefined)
|
||||
const triggersCount = writable<TriggersCount | undefined>(undefined)
|
||||
const selectedTriggerStore = writable<'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes'>(
|
||||
'webhooks'
|
||||
)
|
||||
setContext<TriggerContext>('TriggerContext', {
|
||||
primarySchedule: primaryScheduleStore,
|
||||
selectedTrigger: selectedTriggerStore,
|
||||
triggersCount: triggersCount
|
||||
})
|
||||
|
||||
setContext<FlowEditorContext>('FlowEditorContext', {
|
||||
selectedId: selectedIdStore,
|
||||
schedule: scheduleStore,
|
||||
previewArgs: previewArgsStore,
|
||||
scriptEditorDrawer,
|
||||
moving,
|
||||
|
||||
@@ -12,8 +12,8 @@ const lightTheme = {
|
||||
textTertiary: '#505c70',
|
||||
textDisabled: '#a0aec0',
|
||||
|
||||
border: '#ddd',
|
||||
borderHover: '#ccc'
|
||||
border: '#dddddd',
|
||||
borderHover: '#cccccc'
|
||||
}
|
||||
|
||||
const lightThemeRgb = makeRgb(lightTheme)
|
||||
@@ -384,6 +384,20 @@ const config = {
|
||||
400: '#394A6D',
|
||||
500: '#323F5B'
|
||||
},
|
||||
nord: {
|
||||
0: '#2E3440',
|
||||
100: '#3B4252',
|
||||
200: '#434C5E',
|
||||
300: '#4C566A',
|
||||
400: '#D8DEE9',
|
||||
500: '#E5E9F0',
|
||||
600: '#ECEFF4',
|
||||
700: '#8FBCBB',
|
||||
800: '#88C0D0',
|
||||
900: '#81A1C1',
|
||||
950: '#5E81AC'
|
||||
},
|
||||
|
||||
surface: 'rgb(var(--color-surface) / <alpha-value>)',
|
||||
'surface-secondary': 'rgb(var(--color-surface-secondary) / <alpha-value>)',
|
||||
'surface-hover': 'rgb(var(--color-surface-hover) / <alpha-value>)',
|
||||
|
||||
Reference in New Issue
Block a user