mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
fix(ui): capture v2 UX (#4954)
* feat: captures * flow UI and improvements * fix: build * fix sqlx * Move Capture WIP * Add capture to webhook and websocket * Move connection status viewer in the head * change trigger section label * Add popover capture picker using melt ui * Add shortcut to triggers capture from input form * remove capture tab in input * Allways show capture * remove useless logs * Add email capture * Add kafka capture into triggers * Add edit option in capture table * use light header for arg input * Add prefilled group id * Change button label * fix logic * Open resource drawer if prototype has fields * fix default completion * Change name Prototype * Dissociate Editor Mode for triggers * Fix bug for script * Fix apply args * fix apply schema * Add capture table to script * fix apply args * Add capture button for script * Delete capture tab * Set capture on when opening triggers capture * fix connection indicator * fix minor issues * Add preprocessor logic * Use slot in log Panel for captures * handle capture refresh in script * Delete capture tab from script editor * reset kafka resource on toggle static * fix minor issue * Allow resource in kafka capture * use simple capture button in flow * Remove capture panel * Polish route trigger editor * Fix resource saving * Remove excessive padding * merge nits * Add history tab # Conflicts: # frontend/src/lib/components/EditableSchemaForm.svelte * add workflow_dispatch to build * add workflow_dispatch to build * Move input copy from in a panel * better capture UI * Add smoothing animation on open input editor * fix bad wrapping in input editor menu * clean code * restore captureTable * clean * fix sqlx * fix build * fix build * build * make initial_messages optional in line with db * fix npm check * better handle args for capture webhook and http * add capture pagination * Add capture table * Add table for history inputs * nit * simplify capture table * stop capturing on closing capture section * show only 10 items per page for captures * set focus to logs on script test * Fix collapsable section * nit * open captures when applying args from triggers * update edit input drawer * fix all EditableSchemaForm * fix panel init animation * Add capture popover * change open tab button * clean code * fix bad table display * Fix JSON input bad sync * make capture icon bigger * nit * nit * revert unwanted change * Change capture name and nits * infinite scroll and cleaning * Add toast when failing to retreive first step input * add icons to dropdown edit input list * remove unused log * Add click outside to all schema pickers * fix click oustide * Fix migrations * solve run button not refreshing properly * Add a capture drawer * change apply args and schema logic * change apply args and schema logic * fix toggle display * fix schema display * improve preview mode * Add first step panel * nit * clean * Add hint for captures * clean * nit * nit * fix script node page height * improve animation on redirecting to capture * improve saved input picker * nit * add Json placeholder * nit * add list animations * delete useless logs * add animated list to captures history and saved inputs * Delete unused component * clean * nit * clean code * improve tab scroll experience * nit * nits * nits * fix script args update * nit * nit: include preview in history inputs in edit mode --------- Co-authored-by: HugoCasa <hugo@casademont.ch> Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com> Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
This commit is contained in:
+77
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT id, created_at, trigger_kind as \"trigger_kind: _\", payload as \"payload: _\", trigger_extra as \"trigger_extra: _\"\n FROM capture\n WHERE workspace_id = $1\n AND path = $2 AND is_flow = $3\n AND ($4::trigger_kind IS NULL OR trigger_kind = $4)\n ORDER BY created_at DESC\n OFFSET $5\n LIMIT $6",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "trigger_kind: _",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "trigger_kind",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"webhook",
|
||||
"http",
|
||||
"websocket",
|
||||
"kafka",
|
||||
"email"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "payload: _",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "trigger_extra: _",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Bool",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "trigger_kind",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"webhook",
|
||||
"http",
|
||||
"websocket",
|
||||
"kafka",
|
||||
"email"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "e08dddf6af2656b561c453460ff928e7a158bd29d9035bee136f9952fb96bad4"
|
||||
}
|
||||
@@ -283,7 +283,7 @@ paths:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
|
||||
/users/set_password_of/{user}:
|
||||
post:
|
||||
summary: set password for a specific user (require super admin)
|
||||
@@ -348,7 +348,6 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
|
||||
/users/create:
|
||||
post:
|
||||
summary: create user
|
||||
@@ -530,7 +529,7 @@ paths:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
|
||||
/users/export:
|
||||
get:
|
||||
summary: global export users (require super admin and EE)
|
||||
@@ -877,7 +876,7 @@ paths:
|
||||
alerts:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/CriticalAlert'
|
||||
$ref: "#/components/schemas/CriticalAlert"
|
||||
total_rows:
|
||||
type: integer
|
||||
description: Total number of rows matching the query.
|
||||
@@ -998,7 +997,6 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
|
||||
/settings/latest_key_renewal_attempt:
|
||||
get:
|
||||
summary: get latest key renewal attempt
|
||||
@@ -1084,7 +1082,6 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
|
||||
/settings/list_global:
|
||||
get:
|
||||
summary: list global settings
|
||||
@@ -2808,7 +2805,7 @@ paths:
|
||||
alerts:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/CriticalAlert'
|
||||
$ref: "#/components/schemas/CriticalAlert"
|
||||
total_rows:
|
||||
type: integer
|
||||
description: Total number of rows matching the query.
|
||||
@@ -2879,7 +2876,7 @@ paths:
|
||||
description: Whether critical alerts should be muted.
|
||||
example: true
|
||||
responses:
|
||||
'200':
|
||||
"200":
|
||||
description: Successfully updated mute critical alert settings.
|
||||
content:
|
||||
application/json:
|
||||
@@ -2958,7 +2955,6 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
|
||||
/oauth/connect_slack_callback:
|
||||
post:
|
||||
summary: connect slack callback instance
|
||||
@@ -3175,7 +3171,7 @@ paths:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: get
|
||||
description: get
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -3188,9 +3184,6 @@ paths:
|
||||
items:
|
||||
type: string
|
||||
|
||||
|
||||
|
||||
|
||||
/w/{workspace}/resources/create:
|
||||
post:
|
||||
summary: create resource
|
||||
@@ -3481,7 +3474,6 @@ paths:
|
||||
application/json:
|
||||
schema: {}
|
||||
|
||||
|
||||
/w/{workspace}/resources/type/delete/{path}:
|
||||
delete:
|
||||
summary: delete resource_type
|
||||
@@ -4469,7 +4461,6 @@ paths:
|
||||
description: Script version/hash
|
||||
content:
|
||||
application/json:
|
||||
|
||||
required: false
|
||||
|
||||
schema:
|
||||
@@ -5040,7 +5031,6 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
|
||||
/w/{workspace}/flows/get/{path}:
|
||||
get:
|
||||
summary: get flow by path
|
||||
@@ -5098,7 +5088,6 @@ paths:
|
||||
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
|
||||
@@ -5269,7 +5258,6 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
|
||||
/w/{workspace}/raw_apps/list:
|
||||
get:
|
||||
summary: list all raw apps
|
||||
@@ -6319,7 +6307,6 @@ paths:
|
||||
schema:
|
||||
type: integer
|
||||
|
||||
|
||||
/w/{workspace}/jobs/queue/list_filtered_uuids:
|
||||
get:
|
||||
summary: get the ids of all jobs matching the given filters
|
||||
@@ -6631,7 +6618,6 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
|
||||
/w/{workspace}/jobs_u/get_args/{id}:
|
||||
get:
|
||||
summary: get job args
|
||||
@@ -7156,7 +7142,6 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
|
||||
/w/{workspace}/jobs_u/cancel/{id}/{resume_id}/{signature}:
|
||||
get:
|
||||
summary: cancel a job for a suspended flow
|
||||
@@ -7527,7 +7512,6 @@ paths:
|
||||
"201":
|
||||
description: default error handler set
|
||||
|
||||
|
||||
/w/{workspace}/http_triggers/create:
|
||||
post:
|
||||
summary: create http trigger
|
||||
@@ -7580,7 +7564,7 @@ paths:
|
||||
summary: delete http trigger
|
||||
operationId: deleteHttpTrigger
|
||||
tags:
|
||||
- http_trigger
|
||||
- http_trigger
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/Path"
|
||||
@@ -7609,7 +7593,6 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/HttpTrigger"
|
||||
|
||||
|
||||
/w/{workspace}/http_triggers/list:
|
||||
get:
|
||||
summary: list http triggers
|
||||
@@ -7644,7 +7627,6 @@ paths:
|
||||
items:
|
||||
$ref: "#/components/schemas/HttpTrigger"
|
||||
|
||||
|
||||
/w/{workspace}/http_triggers/exists/{path}:
|
||||
get:
|
||||
summary: does http trigger exists
|
||||
@@ -7747,7 +7729,7 @@ paths:
|
||||
summary: delete websocket trigger
|
||||
operationId: deleteWebsocketTrigger
|
||||
tags:
|
||||
- websocket_trigger
|
||||
- websocket_trigger
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/Path"
|
||||
@@ -7776,7 +7758,6 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/WebsocketTrigger"
|
||||
|
||||
|
||||
/w/{workspace}/websocket_triggers/list:
|
||||
get:
|
||||
summary: list websocket triggers
|
||||
@@ -7811,7 +7792,6 @@ paths:
|
||||
items:
|
||||
$ref: "#/components/schemas/WebsocketTrigger"
|
||||
|
||||
|
||||
/w/{workspace}/websocket_triggers/exists/{path}:
|
||||
get:
|
||||
summary: does websocket trigger exists
|
||||
@@ -7910,7 +7890,7 @@ paths:
|
||||
summary: delete kafka trigger
|
||||
operationId: deleteKafkaTrigger
|
||||
tags:
|
||||
- kafka_trigger
|
||||
- kafka_trigger
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/Path"
|
||||
@@ -7939,7 +7919,6 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/KafkaTrigger"
|
||||
|
||||
|
||||
/w/{workspace}/kafka_triggers/list:
|
||||
get:
|
||||
summary: list kafka triggers
|
||||
@@ -7974,7 +7953,6 @@ paths:
|
||||
items:
|
||||
$ref: "#/components/schemas/KafkaTrigger"
|
||||
|
||||
|
||||
/w/{workspace}/kafka_triggers/exists/{path}:
|
||||
get:
|
||||
summary: does kafka trigger exists
|
||||
@@ -8880,7 +8858,7 @@ paths:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
values:
|
||||
values:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
@@ -8907,12 +8885,11 @@ paths:
|
||||
description: queue counts
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: integer
|
||||
|
||||
|
||||
/configs/list_worker_groups:
|
||||
get:
|
||||
summary: list worker groups
|
||||
@@ -9025,7 +9002,6 @@ paths:
|
||||
items:
|
||||
$ref: "#/components/schemas/AutoscalingEvent"
|
||||
|
||||
|
||||
/w/{workspace}/acls/get/{kind}/{path}:
|
||||
get:
|
||||
summary: get granular acls
|
||||
@@ -9184,11 +9160,11 @@ paths:
|
||||
properties:
|
||||
trigger_kind:
|
||||
$ref: "#/components/schemas/CaptureTriggerKind"
|
||||
path:
|
||||
path:
|
||||
type: string
|
||||
is_flow:
|
||||
type: boolean
|
||||
trigger_config:
|
||||
trigger_config:
|
||||
type: object
|
||||
required:
|
||||
- trigger_kind
|
||||
@@ -9198,7 +9174,6 @@ paths:
|
||||
"200":
|
||||
description: capture config set
|
||||
|
||||
|
||||
/w/{workspace}/capture/ping_config/{trigger_kind}/{runnable_kind}/{path}:
|
||||
post:
|
||||
summary: ping capture config
|
||||
@@ -9252,6 +9227,8 @@ paths:
|
||||
in: query
|
||||
schema:
|
||||
$ref: "#/components/schemas/CaptureTriggerKind"
|
||||
- $ref: "#/components/parameters/Page"
|
||||
- $ref: "#/components/parameters/PerPage"
|
||||
responses:
|
||||
"200":
|
||||
description: list of captures for a script or flow
|
||||
@@ -9262,7 +9239,6 @@ paths:
|
||||
items:
|
||||
$ref: "#/components/schemas/Capture"
|
||||
|
||||
|
||||
/w/{workspace}/capture/{id}:
|
||||
delete:
|
||||
summary: delete a capture
|
||||
@@ -9338,6 +9314,10 @@ paths:
|
||||
- $ref: "#/components/parameters/RunnableTypeQuery"
|
||||
- $ref: "#/components/parameters/Page"
|
||||
- $ref: "#/components/parameters/PerPage"
|
||||
- name: include_preview
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
responses:
|
||||
"200":
|
||||
description: Input history for completed jobs
|
||||
@@ -10265,7 +10245,7 @@ paths:
|
||||
tags:
|
||||
- service_logs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Path"
|
||||
- $ref: "#/components/parameters/Path"
|
||||
responses:
|
||||
"200":
|
||||
description: log stream
|
||||
@@ -11082,7 +11062,6 @@ components:
|
||||
- no_main_func
|
||||
- has_preprocessor
|
||||
|
||||
|
||||
NewScript:
|
||||
type: object
|
||||
properties:
|
||||
@@ -12439,7 +12418,7 @@ components:
|
||||
type: boolean
|
||||
requires_auth:
|
||||
type: boolean
|
||||
|
||||
|
||||
required:
|
||||
- path
|
||||
- edited_by
|
||||
@@ -12488,7 +12467,7 @@ components:
|
||||
type: boolean
|
||||
requires_auth:
|
||||
type: boolean
|
||||
|
||||
|
||||
required:
|
||||
- path
|
||||
- script_path
|
||||
@@ -12614,7 +12593,7 @@ components:
|
||||
$ref: "#/components/schemas/WebsocketTriggerInitialMessage"
|
||||
url_runnable_args:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
|
||||
|
||||
required:
|
||||
- path
|
||||
- edited_by
|
||||
@@ -12658,7 +12637,7 @@ components:
|
||||
$ref: "#/components/schemas/WebsocketTriggerInitialMessage"
|
||||
url_runnable_args:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
|
||||
|
||||
required:
|
||||
- path
|
||||
- script_path
|
||||
@@ -12694,7 +12673,7 @@ components:
|
||||
$ref: "#/components/schemas/WebsocketTriggerInitialMessage"
|
||||
url_runnable_args:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
|
||||
|
||||
required:
|
||||
- path
|
||||
- script_path
|
||||
@@ -12706,7 +12685,7 @@ components:
|
||||
anyOf:
|
||||
- type: object
|
||||
properties:
|
||||
raw_message:
|
||||
raw_message:
|
||||
type: string
|
||||
required:
|
||||
- raw_message
|
||||
@@ -12715,7 +12694,7 @@ components:
|
||||
runnable_result:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
path:
|
||||
type: string
|
||||
args:
|
||||
$ref: "#/components/schemas/ScriptArgs"
|
||||
@@ -12725,7 +12704,7 @@ components:
|
||||
- path
|
||||
- args
|
||||
- is_flow
|
||||
required:
|
||||
required:
|
||||
- runnable_result
|
||||
|
||||
KafkaTrigger:
|
||||
@@ -12767,7 +12746,7 @@ components:
|
||||
type: string
|
||||
enabled:
|
||||
type: boolean
|
||||
|
||||
|
||||
required:
|
||||
- path
|
||||
- edited_by
|
||||
@@ -12801,7 +12780,7 @@ components:
|
||||
type: string
|
||||
enabled:
|
||||
type: boolean
|
||||
|
||||
|
||||
required:
|
||||
- path
|
||||
- script_path
|
||||
@@ -12827,7 +12806,7 @@ components:
|
||||
type: string
|
||||
is_flow:
|
||||
type: boolean
|
||||
|
||||
|
||||
required:
|
||||
- path
|
||||
- script_path
|
||||
@@ -13794,14 +13773,14 @@ components:
|
||||
obscured_jobs:
|
||||
type: array
|
||||
items:
|
||||
$ref : "#/components/schemas/ObscuredJob"
|
||||
$ref: "#/components/schemas/ObscuredJob"
|
||||
omitted_obscured_jobs:
|
||||
description: "Obscured jobs omitted for security because of too specific filtering"
|
||||
type: boolean
|
||||
required:
|
||||
- jobs
|
||||
- obscured_jobs
|
||||
|
||||
|
||||
ExportedUser:
|
||||
type: object
|
||||
properties:
|
||||
@@ -13847,7 +13826,7 @@ components:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
|
||||
|
||||
ExportedInstanceGroup:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -27,7 +27,7 @@ use windmill_common::error::Error;
|
||||
use windmill_common::{
|
||||
db::UserDB,
|
||||
error::{JsonResult, Result},
|
||||
utils::{not_found_if_none, StripPath},
|
||||
utils::{not_found_if_none, paginate, Pagination, StripPath},
|
||||
worker::{to_raw_value, CLOUD_HOSTED},
|
||||
};
|
||||
use windmill_queue::{PushArgs, PushArgsOwned};
|
||||
@@ -280,6 +280,8 @@ enum RunnableKind {
|
||||
#[derive(Deserialize)]
|
||||
struct ListCapturesQuery {
|
||||
trigger_kind: Option<TriggerKind>,
|
||||
page: Option<usize>,
|
||||
per_page: Option<usize>,
|
||||
}
|
||||
|
||||
async fn list_captures(
|
||||
@@ -290,6 +292,8 @@ async fn list_captures(
|
||||
) -> JsonResult<Vec<Capture>> {
|
||||
let mut tx = user_db.begin(&authed).await?;
|
||||
|
||||
let (per_page, offset) = paginate(Pagination { page: query.page, per_page: query.per_page });
|
||||
|
||||
let captures = sqlx::query_as!(
|
||||
Capture,
|
||||
r#"SELECT id, created_at, trigger_kind as "trigger_kind: _", payload as "payload: _", trigger_extra as "trigger_extra: _"
|
||||
@@ -297,11 +301,15 @@ async fn list_captures(
|
||||
WHERE workspace_id = $1
|
||||
AND path = $2 AND is_flow = $3
|
||||
AND ($4::trigger_kind IS NULL OR trigger_kind = $4)
|
||||
ORDER BY created_at DESC"#,
|
||||
ORDER BY created_at DESC
|
||||
OFFSET $5
|
||||
LIMIT $6"#,
|
||||
&w_id,
|
||||
&path.to_path(),
|
||||
matches!(runnable_kind, RunnableKind::Flow),
|
||||
query.trigger_kind as Option<TriggerKind>,
|
||||
offset as i64,
|
||||
per_page as i64,
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?;
|
||||
|
||||
@@ -115,12 +115,18 @@ pub struct CompletedJobMini {
|
||||
success: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct GetInputHistory {
|
||||
include_preview: Option<bool>,
|
||||
}
|
||||
|
||||
async fn get_input_history(
|
||||
authed: ApiAuthed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path(w_id): Path<String>,
|
||||
Query(pagination): Query<Pagination>,
|
||||
Query(r): Query<RunnableParams>,
|
||||
Query(g): Query<GetInputHistory>,
|
||||
) -> JsonResult<Vec<Input>> {
|
||||
let (per_page, offset) = paginate(pagination);
|
||||
|
||||
@@ -128,7 +134,7 @@ async fn get_input_history(
|
||||
|
||||
let sql = &format!(
|
||||
"select id, created_at, created_by, 'null'::jsonb as args, success from completed_job \
|
||||
where {} = $1 and job_kind = $2 and workspace_id = $3 \
|
||||
where {} = $1 and job_kind = any($2) and workspace_id = $3 \
|
||||
order by created_at desc limit $4 offset $5",
|
||||
r.runnable_type.column_name()
|
||||
);
|
||||
@@ -140,8 +146,18 @@ async fn get_input_history(
|
||||
_ => query.bind(&r.runnable_id),
|
||||
};
|
||||
|
||||
let job_kinds = match r.runnable_type.job_kind() {
|
||||
kind @ JobKind::Script if g.include_preview.unwrap_or(false) => {
|
||||
vec![kind, JobKind::Preview]
|
||||
}
|
||||
kind @ JobKind::Flow if g.include_preview.unwrap_or(false) => {
|
||||
vec![kind, JobKind::FlowPreview]
|
||||
}
|
||||
kind => vec![kind],
|
||||
};
|
||||
|
||||
let rows = query
|
||||
.bind(r.runnable_type.job_kind())
|
||||
.bind(job_kinds)
|
||||
.bind(&w_id)
|
||||
.bind(per_page as i32)
|
||||
.bind(offset as i32)
|
||||
|
||||
@@ -395,19 +395,21 @@
|
||||
</div>
|
||||
{/if}
|
||||
{:else if inputCat == 'boolean'}
|
||||
<Toggle
|
||||
on:pointerdown={(e) => {
|
||||
e?.stopPropagation()
|
||||
}}
|
||||
{disabled}
|
||||
class={valid
|
||||
? ''
|
||||
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-30 bg-red-100'}
|
||||
bind:checked={value}
|
||||
/>
|
||||
{#if type == 'boolean' && value == undefined}
|
||||
<span> Not set</span>
|
||||
{/if}
|
||||
<div class="w-full">
|
||||
<Toggle
|
||||
on:pointerdown={(e) => {
|
||||
e?.stopPropagation()
|
||||
}}
|
||||
{disabled}
|
||||
class={valid
|
||||
? ''
|
||||
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-30 bg-red-100'}
|
||||
bind:checked={value}
|
||||
/>
|
||||
{#if type == 'boolean' && value == undefined}
|
||||
<span> Not set</span>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if inputCat == 'list' && !isListJson}
|
||||
<div class="w-full flex gap-4">
|
||||
<div class="w-full">
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import CaptureTable from '$lib/components/triggers/CaptureTable.svelte'
|
||||
|
||||
export let flowPath: string | null = null
|
||||
export let headless: boolean = true
|
||||
export let addButton: boolean = false
|
||||
</script>
|
||||
|
||||
<div class="h-full">
|
||||
<CaptureTable
|
||||
path={flowPath ?? ''}
|
||||
on:select
|
||||
isFlow={true}
|
||||
{headless}
|
||||
{addButton}
|
||||
on:openTriggers
|
||||
/>
|
||||
</div>
|
||||
@@ -33,7 +33,7 @@
|
||||
import { writable } from 'svelte/store'
|
||||
import type { FlowState } from './flows/flowState'
|
||||
import { initHistory } from '$lib/history'
|
||||
import type { FlowEditorContext, FlowInput } from './flows/types'
|
||||
import type { FlowEditorContext, FlowInput, FlowInputEditorState } from './flows/types'
|
||||
import { dfs } from './flows/dfs'
|
||||
import { loadSchemaFromModule } from './flows/flowInfers'
|
||||
import { CornerDownLeft, Play } from 'lucide-svelte'
|
||||
@@ -490,7 +490,8 @@
|
||||
triggersCount: triggersCount,
|
||||
simplifiedPoll: writable(false),
|
||||
defaultValues: writable(undefined),
|
||||
captureOn: writable(undefined)
|
||||
captureOn: writable(undefined),
|
||||
showCaptureHint: writable(undefined)
|
||||
})
|
||||
setContext<FlowEditorContext>('FlowEditorContext', {
|
||||
selectedId: selectedIdStore,
|
||||
@@ -508,6 +509,11 @@
|
||||
customUi: {},
|
||||
insertButtonOpen: writable(false),
|
||||
executionCount: writable(0),
|
||||
flowInputEditorState: writable<FlowInputEditorState>({
|
||||
selectedTab: undefined,
|
||||
editPanelSize: undefined,
|
||||
payloadData: undefined
|
||||
})
|
||||
})
|
||||
setContext<PropPickerContext>('PropPickerContext', {
|
||||
flowPropPickerConfig: writable<FlowPropPickerConfig | undefined>(undefined),
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import { Button } from './common'
|
||||
import ItemPicker from './ItemPicker.svelte'
|
||||
import VariableEditor from './VariableEditor.svelte'
|
||||
|
||||
import { Pen, Plus, X } from 'lucide-svelte'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
@@ -23,6 +22,7 @@
|
||||
import Popup from './common/popup/Popup.svelte'
|
||||
import SchemaFormDnd from './schema/SchemaFormDND.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { tweened } from 'svelte/motion'
|
||||
|
||||
export let schema: Schema | any
|
||||
export let schemaSkippedValues: string[] = []
|
||||
@@ -33,12 +33,22 @@
|
||||
export let uiOnly: boolean = false
|
||||
export let isFlowInput: boolean = false
|
||||
export let noPreview: boolean = false
|
||||
export let offset = 48 + 31 + 31 + 16 + 1
|
||||
export let jsonEnabled: boolean = true
|
||||
export let isAppInput: boolean = false
|
||||
export let lightweightMode: boolean = false
|
||||
export let displayWebhookWarning: boolean = false
|
||||
export let dndType: string | undefined = undefined
|
||||
export let editTab:
|
||||
| 'inputEditor'
|
||||
| 'history'
|
||||
| 'savedInputs'
|
||||
| 'json'
|
||||
| 'captures'
|
||||
| 'firstStepInputs'
|
||||
| undefined
|
||||
export let previewSchema: Record<string, any> | undefined = undefined
|
||||
export let editPanelInitialSize: number | undefined = undefined
|
||||
export let editPanelSize = 0
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -134,13 +144,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: if (opened && schema.properties[opened]) {
|
||||
$: opened && updateSelected(schema.properties[opened])
|
||||
function updateSelected(property: any) {
|
||||
if (!property) return
|
||||
selected = opened
|
||||
? schema.properties[opened].type !== 'object'
|
||||
? schema.properties[opened].type
|
||||
: schema.properties[opened].format === 'resource-s3_object'
|
||||
? property.type !== 'object'
|
||||
? property.type
|
||||
: property.format === 'resource-s3_object'
|
||||
? 'S3'
|
||||
: schema.properties[opened].oneOf && schema.properties[opened].oneOf.length >= 2
|
||||
: property.oneOf && property.oneOf.length >= 2
|
||||
? 'oneOf'
|
||||
: 'object'
|
||||
: ''
|
||||
@@ -189,15 +201,63 @@
|
||||
let schemaString: string = JSON.stringify(schema, null, '\t')
|
||||
let error: string | undefined = undefined
|
||||
let editor: SimpleEditor | undefined = undefined
|
||||
|
||||
const editTabDefaultSize = noPreview ? 100 : 50
|
||||
editPanelSize = editTab ? editPanelInitialSize ?? editTabDefaultSize : 0
|
||||
let inputPanelSize = 100 - editPanelSize
|
||||
let editPanelSizeSmooth = tweened(editPanelSize, {
|
||||
duration: 150
|
||||
})
|
||||
let inputPanelSizeSmooth = tweened(inputPanelSize, { duration: 150 })
|
||||
|
||||
function openEditTab() {
|
||||
if (editPanelSize > 0) return
|
||||
editPanelSizeSmooth.set(editTabDefaultSize)
|
||||
inputPanelSizeSmooth.set(100 - editTabDefaultSize)
|
||||
}
|
||||
|
||||
function closeEditTab() {
|
||||
editPanelSizeSmooth.set(0)
|
||||
inputPanelSizeSmooth.set(100)
|
||||
}
|
||||
|
||||
function updatePanelSizes(editSize: number, inputSize: number) {
|
||||
editPanelSize = editSize
|
||||
inputPanelSize = inputSize
|
||||
dispatch('editPanelSizeChanged', editSize)
|
||||
}
|
||||
$: updatePanelSizes($editPanelSizeSmooth, $inputPanelSizeSmooth)
|
||||
|
||||
$: !!editTab ? openEditTab() : closeEditTab()
|
||||
|
||||
let pannelButtonWidth: number = 0
|
||||
export let pannelExtraButtonWidth: number = 0
|
||||
</script>
|
||||
|
||||
<div style={offset ? `height: calc(100vh - ${offset}px);` : 'h-full'}>
|
||||
<Splitpanes>
|
||||
<div class="w-full h-full">
|
||||
<div class="relative z-[100000]">
|
||||
<div
|
||||
class="absolute"
|
||||
style="right: calc({editPanelSize}% - 2px - {pannelExtraButtonWidth}px); top: 0px;"
|
||||
bind:clientWidth={pannelButtonWidth}
|
||||
>
|
||||
<slot name="openEditTab" />
|
||||
</div>
|
||||
</div>
|
||||
<Splitpanes class="splitter-hidden w-full">
|
||||
{#if !noPreview}
|
||||
<Pane size={50} minSize={20}>
|
||||
<div class="p-4">
|
||||
<Pane bind:size={inputPanelSize} minSize={20}>
|
||||
<div class="flex flex-col pr-2 gap-2">
|
||||
{#if $$slots.addProperty}
|
||||
<div class="w-full justify-left pr-2">
|
||||
<div style={`width: calc(100% - ${pannelButtonWidth - pannelExtraButtonWidth}px);`}>
|
||||
<slot name="addProperty" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<SchemaFormDnd
|
||||
{schema}
|
||||
schema={previewSchema ? previewSchema : schema}
|
||||
{dndType}
|
||||
bind:args
|
||||
on:click={(e) => {
|
||||
@@ -214,314 +274,336 @@
|
||||
}}
|
||||
{lightweightMode}
|
||||
prettifyHeader={isAppInput}
|
||||
disabled={!!previewSchema}
|
||||
/>
|
||||
|
||||
<slot name="runButton" />
|
||||
</div>
|
||||
</Pane>
|
||||
{/if}
|
||||
<Pane size={noPreview ? 100 : 50} minSize={noPreview ? 100 : 20}>
|
||||
{#if jsonEnabled}
|
||||
<div class="w-full p-2 flex justify-end">
|
||||
<Toggle
|
||||
bind:checked={jsonView}
|
||||
label="JSON View"
|
||||
size="xs"
|
||||
options={{
|
||||
right: 'JSON editor',
|
||||
rightTooltip:
|
||||
'Arguments can be edited either using the wizard, or by editing their JSON Schema.'
|
||||
}}
|
||||
lightMode
|
||||
on:change={() => {
|
||||
schemaString = JSON.stringify(schema, null, '\t')
|
||||
editor?.setCode(schemaString)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !jsonView}
|
||||
<div class="w-full {clazz} {flexWrap ? 'flex flex-row flex-wrap gap-x-6 ' : ''} divide-y">
|
||||
{#if keys.length > 0}
|
||||
{#each keys as argName, i (argName)}
|
||||
<div>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class={twMerge(
|
||||
'w-full flex bg-gray-50 dark:bg-gray-800 px-4 py-1 justify-between items-center hover:bg-gray-100 cursor-pointer',
|
||||
opened === argName ? 'bg-gray-100 hover:bg-gray-200' : ''
|
||||
)}
|
||||
on:click={() => {
|
||||
if (opened === argName) {
|
||||
opened = undefined
|
||||
} else {
|
||||
opened = argName
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-row gap-2">
|
||||
{argName}
|
||||
{#if !uiOnly}
|
||||
<div on:click|stopPropagation|preventDefault>
|
||||
<Popup
|
||||
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
|
||||
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
|
||||
let:close
|
||||
>
|
||||
<svelte:fragment slot="button">
|
||||
<Button
|
||||
color="light"
|
||||
size="xs2"
|
||||
nonCaptureEvent
|
||||
startIcon={{ icon: Pen }}
|
||||
iconOnly
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<Label label="Name">
|
||||
<div class="flex flex-col gap-2">
|
||||
<input
|
||||
type="text"
|
||||
class="w-full !bg-surface"
|
||||
value={argName}
|
||||
id={argName + i}
|
||||
on:keydown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
renameProperty(argName, argName + i)
|
||||
close(null)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
renameProperty(argName, argName + i)
|
||||
close(null)
|
||||
}}
|
||||
>
|
||||
Rename
|
||||
</Button>
|
||||
</div>
|
||||
</Label>
|
||||
</Popup>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if editPanelSize > 0}
|
||||
<Pane
|
||||
bind:size={editPanelSize}
|
||||
minSize={noPreview ? 100 : 50}
|
||||
class={twMerge('border rounded-md', pannelButtonWidth > 0 ? 'rounded-tl-none' : '')}
|
||||
>
|
||||
{#if editTab !== 'inputEditor'}
|
||||
<slot name="extraTab" />
|
||||
{:else}
|
||||
<!-- WIP -->
|
||||
{#if jsonEnabled}
|
||||
<div class="w-full p-3 flex justify-end">
|
||||
<Toggle
|
||||
bind:checked={jsonView}
|
||||
label="JSON View"
|
||||
size="xs"
|
||||
options={{
|
||||
right: 'JSON editor',
|
||||
rightTooltip:
|
||||
'Arguments can be edited either using the wizard, or by editing their JSON Schema.'
|
||||
}}
|
||||
lightMode
|
||||
on:change={() => {
|
||||
schemaString = JSON.stringify(schema, null, '\t')
|
||||
editor?.setCode(schemaString)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if schema.required?.includes(argName)}
|
||||
<span class="text-red-500 text-xs"> Required </span>
|
||||
{/if}
|
||||
|
||||
{#if !uiOnly}
|
||||
<button
|
||||
class="rounded-full p-1 text-gray-500 bg-white
|
||||
duration-200 hover:bg-gray-600 focus:bg-gray-600 hover:text-white dark:bg-gray-700 dark:text-white dark:hover:bg-gray-800"
|
||||
aria-label="Clear"
|
||||
{#if !jsonView}
|
||||
<div
|
||||
class="w-full {clazz} {flexWrap ? 'flex flex-row flex-wrap gap-x-6 ' : ''} divide-y"
|
||||
>
|
||||
{#if keys.length > 0}
|
||||
{#each keys as argName, i (argName)}
|
||||
<div>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class={twMerge(
|
||||
'w-full flex bg-gray-50 dark:bg-gray-800 px-4 py-1 justify-between items-center hover:bg-gray-100 cursor-pointer',
|
||||
opened === argName ? 'bg-gray-100 hover:bg-gray-200' : ''
|
||||
)}
|
||||
on:click={() => {
|
||||
dispatch('delete', argName)
|
||||
if (opened === argName) {
|
||||
opened = undefined
|
||||
} else {
|
||||
opened = argName
|
||||
}
|
||||
}}
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if opened === argName}
|
||||
<div class="p-4 border-t">
|
||||
{#if !schemaSkippedValues.includes(argName) && Object.keys(schema?.properties ?? {}).includes(argName)}
|
||||
{#if typeof args == 'object' && schema?.properties[argName]}
|
||||
<PropertyEditor
|
||||
bind:description={schema.properties[argName].description}
|
||||
type={schema.properties[argName].type}
|
||||
bind:oneOf={schema.properties[argName].oneOf}
|
||||
bind:pattern={schema.properties[argName].pattern}
|
||||
bind:enum_={schema.properties[argName].enum}
|
||||
bind:format={schema.properties[argName].format}
|
||||
bind:contentEncoding={schema.properties[argName].contentEncoding}
|
||||
bind:customErrorMessage={schema.properties[argName].customErrorMessage}
|
||||
bind:itemsType={schema.properties[argName].items}
|
||||
bind:extra={schema.properties[argName]}
|
||||
bind:title={schema.properties[argName].title}
|
||||
bind:placeholder={schema.properties[argName].placeholder}
|
||||
bind:properties={schema.properties[argName].properties}
|
||||
bind:order={schema.properties[argName].order}
|
||||
{isFlowInput}
|
||||
{isAppInput}
|
||||
on:change={() => {
|
||||
schema = schema
|
||||
// console.log('schema', schema)
|
||||
dispatch('change', schema)
|
||||
<div class="flex flex-row gap-2">
|
||||
{argName}
|
||||
{#if !uiOnly}
|
||||
<div on:click|stopPropagation|preventDefault>
|
||||
<Popup
|
||||
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
|
||||
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
|
||||
let:close
|
||||
>
|
||||
<svelte:fragment slot="button">
|
||||
<Button
|
||||
color="light"
|
||||
size="xs2"
|
||||
nonCaptureEvent
|
||||
startIcon={{ icon: Pen }}
|
||||
iconOnly
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<Label label="Name">
|
||||
<div class="flex flex-col gap-2">
|
||||
<input
|
||||
type="text"
|
||||
class="w-full !bg-surface"
|
||||
value={argName}
|
||||
id={argName + i}
|
||||
on:keydown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
renameProperty(argName, argName + i)
|
||||
close(null)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
renameProperty(argName, argName + i)
|
||||
close(null)
|
||||
}}
|
||||
>
|
||||
Rename
|
||||
</Button>
|
||||
</div>
|
||||
</Label>
|
||||
</Popup>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if schema.required?.includes(argName)}
|
||||
<span class="text-red-500 text-xs"> Required </span>
|
||||
{/if}
|
||||
|
||||
{#if !uiOnly}
|
||||
<button
|
||||
class="rounded-full p-1 text-gray-500 bg-white
|
||||
duration-200 hover:bg-gray-600 focus:bg-gray-600 hover:text-white dark:bg-gray-700 dark:text-white dark:hover:bg-gray-800"
|
||||
aria-label="Clear"
|
||||
on:click={() => {
|
||||
dispatch('delete', argName)
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="typeeditor">
|
||||
{#if isFlowInput || isAppInput}
|
||||
<Label label="Type">
|
||||
<ToggleButtonGroup
|
||||
tabListClass="flex-wrap"
|
||||
class="h-auto"
|
||||
bind:selected
|
||||
on:selected={(e) => {
|
||||
const isS3 = e.detail == 'S3'
|
||||
const isOneOf = e.detail == 'oneOf'
|
||||
<X size={16} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if opened === argName}
|
||||
<div class="p-4 border-t">
|
||||
{#if !schemaSkippedValues.includes(argName) && Object.keys(schema?.properties ?? {}).includes(argName)}
|
||||
{#if typeof args == 'object' && schema?.properties[argName]}
|
||||
<PropertyEditor
|
||||
bind:description={schema.properties[argName].description}
|
||||
type={schema.properties[argName].type}
|
||||
bind:oneOf={schema.properties[argName].oneOf}
|
||||
bind:pattern={schema.properties[argName].pattern}
|
||||
bind:enum_={schema.properties[argName].enum}
|
||||
bind:format={schema.properties[argName].format}
|
||||
bind:contentEncoding={schema.properties[argName].contentEncoding}
|
||||
bind:customErrorMessage={schema.properties[argName]
|
||||
.customErrorMessage}
|
||||
bind:itemsType={schema.properties[argName].items}
|
||||
bind:extra={schema.properties[argName]}
|
||||
bind:title={schema.properties[argName].title}
|
||||
bind:placeholder={schema.properties[argName].placeholder}
|
||||
bind:properties={schema.properties[argName].properties}
|
||||
bind:order={schema.properties[argName].order}
|
||||
{isFlowInput}
|
||||
{isAppInput}
|
||||
on:change={() => {
|
||||
schema = schema
|
||||
// console.log('schema', schema)
|
||||
dispatch('change', schema)
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="typeeditor">
|
||||
{#if isFlowInput || isAppInput}
|
||||
<Label label="Type">
|
||||
<ToggleButtonGroup
|
||||
tabListClass="flex-wrap"
|
||||
class="h-auto"
|
||||
bind:selected
|
||||
on:selected={(e) => {
|
||||
const isS3 = e.detail == 'S3'
|
||||
const isOneOf = e.detail == 'oneOf'
|
||||
|
||||
selected = e.detail
|
||||
selected = e.detail
|
||||
|
||||
const emptyProperty = {
|
||||
contentEncoding: undefined,
|
||||
enum_: undefined,
|
||||
pattern: undefined,
|
||||
default: undefined,
|
||||
min: undefined,
|
||||
max: undefined,
|
||||
currency: undefined,
|
||||
currencyLocale: undefined,
|
||||
multiselect: undefined,
|
||||
password: undefined,
|
||||
dateFormat: undefined,
|
||||
...(e.detail == 'array' ? { items: { type: 'string' } } : {}),
|
||||
showExpr: undefined,
|
||||
nullable: undefined,
|
||||
required: undefined
|
||||
}
|
||||
const emptyProperty = {
|
||||
contentEncoding: undefined,
|
||||
enum_: undefined,
|
||||
pattern: undefined,
|
||||
default: undefined,
|
||||
min: undefined,
|
||||
max: undefined,
|
||||
currency: undefined,
|
||||
currencyLocale: undefined,
|
||||
multiselect: undefined,
|
||||
password: undefined,
|
||||
dateFormat: undefined,
|
||||
...(e.detail == 'array'
|
||||
? { items: { type: 'string' } }
|
||||
: {}),
|
||||
showExpr: undefined,
|
||||
nullable: undefined,
|
||||
required: undefined
|
||||
}
|
||||
|
||||
if (isS3) {
|
||||
schema.properties[argName] = {
|
||||
...emptyProperty,
|
||||
type: 'object',
|
||||
format: 'resource-s3_object'
|
||||
}
|
||||
} else if (isOneOf) {
|
||||
schema.properties[argName] = {
|
||||
...emptyProperty,
|
||||
type: 'object',
|
||||
oneOf: [
|
||||
{
|
||||
title: 'Option 1',
|
||||
if (isS3) {
|
||||
schema.properties[argName] = {
|
||||
...emptyProperty,
|
||||
type: 'object',
|
||||
properties: {
|
||||
label: {
|
||||
type: 'string',
|
||||
enum: ['Option 1']
|
||||
},
|
||||
property_1: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Option 2',
|
||||
type: 'object',
|
||||
properties: {
|
||||
label: {
|
||||
type: 'string',
|
||||
enum: ['Option 2']
|
||||
},
|
||||
property_2: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
format: 'resource-s3_object'
|
||||
}
|
||||
]
|
||||
}
|
||||
} else if (isOneOf) {
|
||||
schema.properties[argName] = {
|
||||
...emptyProperty,
|
||||
type: 'object',
|
||||
oneOf: [
|
||||
{
|
||||
title: 'Option 1',
|
||||
type: 'object',
|
||||
properties: {
|
||||
label: {
|
||||
type: 'string',
|
||||
enum: ['Option 1']
|
||||
},
|
||||
property_1: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Option 2',
|
||||
type: 'object',
|
||||
properties: {
|
||||
label: {
|
||||
type: 'string',
|
||||
enum: ['Option 2']
|
||||
},
|
||||
property_2: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
} else {
|
||||
schema.properties[argName] = {
|
||||
...emptyProperty,
|
||||
format: undefined,
|
||||
type: e.detail
|
||||
}
|
||||
}
|
||||
schema = schema
|
||||
dispatch('change', schema)
|
||||
}}
|
||||
>
|
||||
{#each [['String', 'string'], ['Number', 'number'], ['Integer', 'integer'], ['Object', 'object'], ['OneOf', 'oneOf'], ['Array', 'array'], ['Boolean', 'boolean'], ['S3 Object', 'S3']] as x}
|
||||
<ToggleButton value={x[1]} label={x[0]} />
|
||||
{/each}
|
||||
</ToggleButtonGroup>
|
||||
</Label>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
{#if isFlowInput || isAppInput}
|
||||
<FlowPropertyEditor
|
||||
bind:defaultValue={schema.properties[argName].default}
|
||||
{variableEditor}
|
||||
{itemPicker}
|
||||
{lightweightMode}
|
||||
bind:nullable={schema.properties[argName].nullable}
|
||||
bind:disabled={schema.properties[argName].disabled}
|
||||
type={schema.properties[argName].type}
|
||||
bind:oneOf={schema.properties[argName].oneOf}
|
||||
bind:format={schema.properties[argName].format}
|
||||
contentEncoding={schema.properties[argName].contentEncoding}
|
||||
required={schema.required?.includes(argName) ?? false}
|
||||
pattern={schema.properties[argName].pattern}
|
||||
password={schema.properties[argName].password}
|
||||
propsNames={schema.properties[argName].propsNames}
|
||||
bind:showExpr={schema.properties[argName].showExpr}
|
||||
extra={schema.properties[argName]}
|
||||
customErrorMessage={schema.properties[argName].customErrorMessage}
|
||||
itemsType={schema.properties[argName].items}
|
||||
bind:properties={schema.properties[argName].properties}
|
||||
bind:order={schema.properties[argName].order}
|
||||
bind:requiredProperty={schema.properties[argName].required}
|
||||
{displayWebhookWarning}
|
||||
on:requiredChange={(event) => {
|
||||
if (event.detail.required) {
|
||||
schema.required = schema.required ?? []
|
||||
schema.required.push(argName)
|
||||
} else {
|
||||
schema.properties[argName] = {
|
||||
...emptyProperty,
|
||||
format: undefined,
|
||||
type: e.detail
|
||||
}
|
||||
schema.required = schema.required?.filter(
|
||||
(x) => x !== argName
|
||||
)
|
||||
}
|
||||
dispatch('change', schema)
|
||||
}}
|
||||
on:schemaChange={(e) => {
|
||||
schema = schema
|
||||
dispatch('change', schema)
|
||||
}}
|
||||
>
|
||||
{#each [['String', 'string'], ['Number', 'number'], ['Integer', 'integer'], ['Object', 'object'], ['OneOf', 'oneOf'], ['Array', 'array'], ['Boolean', 'boolean'], ['S3 Object', 'S3']] as x}
|
||||
<ToggleButton value={x[1]} label={x[0]} />
|
||||
{/each}
|
||||
</ToggleButtonGroup>
|
||||
</Label>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
{#if isFlowInput || isAppInput}
|
||||
<FlowPropertyEditor
|
||||
bind:defaultValue={schema.properties[argName].default}
|
||||
{variableEditor}
|
||||
{itemPicker}
|
||||
{lightweightMode}
|
||||
bind:nullable={schema.properties[argName].nullable}
|
||||
bind:disabled={schema.properties[argName].disabled}
|
||||
type={schema.properties[argName].type}
|
||||
bind:oneOf={schema.properties[argName].oneOf}
|
||||
bind:format={schema.properties[argName].format}
|
||||
contentEncoding={schema.properties[argName].contentEncoding}
|
||||
required={schema.required?.includes(argName) ?? false}
|
||||
pattern={schema.properties[argName].pattern}
|
||||
password={schema.properties[argName].password}
|
||||
propsNames={schema.properties[argName].propsNames}
|
||||
bind:showExpr={schema.properties[argName].showExpr}
|
||||
extra={schema.properties[argName]}
|
||||
customErrorMessage={schema.properties[argName].customErrorMessage}
|
||||
itemsType={schema.properties[argName].items}
|
||||
bind:properties={schema.properties[argName].properties}
|
||||
bind:order={schema.properties[argName].order}
|
||||
bind:requiredProperty={schema.properties[argName].required}
|
||||
{displayWebhookWarning}
|
||||
on:requiredChange={(event) => {
|
||||
if (event.detail.required) {
|
||||
schema.required = schema.required ?? []
|
||||
schema.required.push(argName)
|
||||
} else {
|
||||
schema.required = schema.required?.filter((x) => x !== argName)
|
||||
}
|
||||
dispatch('change', schema)
|
||||
}}
|
||||
on:schemaChange={(e) => {
|
||||
schema = schema
|
||||
dispatch('change', schema)
|
||||
}}
|
||||
/>
|
||||
/>
|
||||
{/if}
|
||||
</PropertyEditor>
|
||||
{/if}
|
||||
</PropertyEditor>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{:else if !shouldHideNoInputs}
|
||||
<div class="text-secondary text-sm p-2">No inputs</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="p-2">
|
||||
<div class="border rounded h-full">
|
||||
<SimpleEditor
|
||||
bind:this={editor}
|
||||
small
|
||||
fixedOverflowWidgets={false}
|
||||
on:change={() => {
|
||||
try {
|
||||
schema = JSON.parse(schemaString)
|
||||
dispatch('change', schema)
|
||||
error = ''
|
||||
} catch (err) {
|
||||
error = err.message
|
||||
}
|
||||
}}
|
||||
bind:code={schemaString}
|
||||
lang="json"
|
||||
autoHeight
|
||||
automaticLayout
|
||||
/>
|
||||
</div>
|
||||
{#if !emptyString(error)}
|
||||
<div class="text-red-400 text-xs">{error}</div>
|
||||
{/each}
|
||||
{:else if !shouldHideNoInputs}
|
||||
<div class="text-secondary text-sm p-2">No inputs</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div><br /> </div>
|
||||
<div class="p-2">
|
||||
<div class="border rounded h-full">
|
||||
<SimpleEditor
|
||||
bind:this={editor}
|
||||
small
|
||||
fixedOverflowWidgets={false}
|
||||
on:change={() => {
|
||||
try {
|
||||
schema = JSON.parse(schemaString)
|
||||
dispatch('change', schema)
|
||||
error = ''
|
||||
} catch (err) {
|
||||
error = err.message
|
||||
}
|
||||
}}
|
||||
bind:code={schemaString}
|
||||
lang="json"
|
||||
autoHeight
|
||||
automaticLayout
|
||||
/>
|
||||
</div>
|
||||
{#if !emptyString(error)}
|
||||
<div class="text-red-400 text-xs">{error}</div>
|
||||
{:else}
|
||||
<div><br /> </div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</Pane>
|
||||
{/if}
|
||||
</Pane>
|
||||
{/if}
|
||||
</Splitpanes>
|
||||
</div>
|
||||
|
||||
@@ -558,3 +640,11 @@
|
||||
|
||||
<VariableEditor bind:this={variableEditor} />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
:global(.splitter-hidden .splitpanes__splitter) {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
opacity: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onDestroy, getContext } from 'svelte'
|
||||
import { getFirstStepSchema } from '$lib/components/flows/flowStore'
|
||||
import type { FlowEditorContext } from '$lib/components/flows/types'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { clickOutside } from '$lib/utils'
|
||||
import { Alert } from '$lib/components/common'
|
||||
import FlowModuleSchemaItemViewer from '$lib/components/flows/map/FlowModuleSchemaItemViewer.svelte'
|
||||
import LanguageIcon from '$lib/components/common/languageIcons/LanguageIcon.svelte'
|
||||
import { prettyLanguage } from '$lib/common'
|
||||
import IconedResourceType from '$lib/components/IconedResourceType.svelte'
|
||||
import { Building } from 'lucide-svelte'
|
||||
const { flowStore, flowStateStore } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let schema: Record<string, any> | undefined = undefined
|
||||
|
||||
let error: string | undefined = undefined
|
||||
let mod: any | undefined = undefined
|
||||
async function loadSchema() {
|
||||
try {
|
||||
const res = await getFirstStepSchema($flowStateStore, flowStore)
|
||||
schema = res.schema
|
||||
mod = res.mod
|
||||
dispatch('connectFirstNode', { connectFirstNode: res.connectFirstNode })
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
}
|
||||
loadSchema()
|
||||
|
||||
function handleClick() {
|
||||
selected = !selected
|
||||
dispatch('select', selected ? schema : undefined)
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
selected = false
|
||||
dispatch('select', undefined)
|
||||
})
|
||||
|
||||
let selected: boolean = false
|
||||
|
||||
async function getPropPickerElements(): Promise<HTMLElement[]> {
|
||||
return Array.from(
|
||||
document.querySelectorAll('[data-schema-picker], [data-schema-picker] *')
|
||||
) as HTMLElement[]
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape' && selected) {
|
||||
selected = false
|
||||
dispatch('select', undefined)
|
||||
}
|
||||
}
|
||||
|
||||
let firstUpdate = true
|
||||
$: if (schema && !selected && firstUpdate) {
|
||||
firstUpdate = false
|
||||
setTimeout(() => {
|
||||
selected = true
|
||||
dispatch('select', schema)
|
||||
}, 200)
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeydown} />
|
||||
|
||||
<div class="h-full">
|
||||
{#if schema && mod}
|
||||
<button
|
||||
class={twMerge(
|
||||
'w-full shadow-md rounded-sm',
|
||||
selected ? 'bg-surface-selected' : 'hover:bg-surface-hover'
|
||||
)}
|
||||
disabled={!schema}
|
||||
on:click={handleClick}
|
||||
use:clickOutside={{ capture: false, exclude: getPropPickerElements }}
|
||||
on:click_outside={() => {
|
||||
if (selected) {
|
||||
selected = false
|
||||
dispatch('select', undefined)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FlowModuleSchemaItemViewer
|
||||
on:click={handleClick}
|
||||
deletable={false}
|
||||
id={mod.id}
|
||||
label={mod.summary ||
|
||||
(`path` in mod.value ? mod.value.path : undefined) ||
|
||||
(mod.value.type === 'rawscript'
|
||||
? `Inline ${prettyLanguage(mod.value.language)}`
|
||||
: 'To be defined')}
|
||||
path={`path` in mod.value && mod.summary ? mod.value.path : ''}
|
||||
>
|
||||
<div slot="icon">
|
||||
{#if mod.value.type === 'rawscript'}
|
||||
<LanguageIcon lang={mod.value.language} width={16} height={16} />
|
||||
{:else if mod.value.type === 'script'}
|
||||
{#if mod.value.path.startsWith('hub/')}
|
||||
<div>
|
||||
<IconedResourceType
|
||||
width="20px"
|
||||
height="20px"
|
||||
name={mod.value.path.split('/')[2]}
|
||||
silent={true}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<Building size={14} />
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</FlowModuleSchemaItemViewer>
|
||||
</button>
|
||||
{:else}
|
||||
<Alert type="warning" title="Cannot load first step's inputs" size="xs">
|
||||
{error}
|
||||
</Alert>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -44,7 +44,7 @@
|
||||
import { dfs, getPreviousIds } from './flows/previousResults'
|
||||
import FlowImportExportMenu from './flows/header/FlowImportExportMenu.svelte'
|
||||
import FlowPreviewButtons from './flows/header/FlowPreviewButtons.svelte'
|
||||
import type { FlowEditorContext, FlowInput } from './flows/types'
|
||||
import type { FlowEditorContext, FlowInput, FlowInputEditorState } from './flows/types'
|
||||
import { cleanInputs, emptyFlowModuleState } from './flows/utils'
|
||||
import {
|
||||
Calendar,
|
||||
@@ -490,7 +490,13 @@
|
||||
const moving = writable<{ module: FlowModule; modules: FlowModule[] } | undefined>(undefined)
|
||||
const history = initHistory($flowStore)
|
||||
const pathStore = writable<string>(pathStoreInit ?? initialPath)
|
||||
|
||||
const captureOn = writable<boolean>(false)
|
||||
const showCaptureHint = writable<boolean | undefined>(undefined)
|
||||
const flowInputEditorStateStore = writable<FlowInputEditorState>({
|
||||
selectedTab: undefined,
|
||||
editPanelSize: 0,
|
||||
payloadData: undefined
|
||||
})
|
||||
$: initialPath && ($pathStore = initialPath)
|
||||
|
||||
const testStepStore = writable<Record<string, any>>({})
|
||||
@@ -529,7 +535,8 @@
|
||||
flowInputsStore: writable<FlowInput>({}),
|
||||
customUi,
|
||||
insertButtonOpen,
|
||||
executionCount: writable(0)
|
||||
executionCount: writable(0),
|
||||
flowInputEditorState: flowInputEditorStateStore
|
||||
})
|
||||
|
||||
setContext<TriggerContext>('TriggerContext', {
|
||||
@@ -538,7 +545,8 @@
|
||||
triggersCount,
|
||||
simplifiedPoll,
|
||||
defaultValues: writable(undefined),
|
||||
captureOn: writable(undefined)
|
||||
captureOn,
|
||||
showCaptureHint
|
||||
})
|
||||
|
||||
async function loadTriggers() {
|
||||
@@ -1206,6 +1214,7 @@
|
||||
|
||||
let deploymentMsg = ''
|
||||
let msgInput: HTMLInputElement | undefined = undefined
|
||||
|
||||
let flowPreviewButtons: FlowPreviewButtons
|
||||
</script>
|
||||
|
||||
@@ -1418,7 +1427,15 @@
|
||||
{abortController}
|
||||
/>
|
||||
{/if}
|
||||
<FlowPreviewButtons bind:this={flowPreviewButtons} />
|
||||
<FlowPreviewButtons
|
||||
on:openTriggers={(e) => {
|
||||
select('triggers')
|
||||
selectTrigger(e.detail.kind)
|
||||
captureOn.set(true)
|
||||
showCaptureHint.set(true)
|
||||
}}
|
||||
bind:this={flowPreviewButtons}
|
||||
/>
|
||||
<Button
|
||||
loading={loadingDraft}
|
||||
size="xs"
|
||||
@@ -1487,11 +1504,12 @@
|
||||
if (ev.detail.kind === 'preprocessor') {
|
||||
$testStepStore['preprocessor'] = ev.detail.args ?? {}
|
||||
$selectedIdStore = 'preprocessor'
|
||||
} else {
|
||||
$previewArgsStore = ev.detail.args ?? {}
|
||||
flowPreviewButtons?.openPreview()
|
||||
}
|
||||
}}
|
||||
on:testWithArgs={(e) => {
|
||||
$previewArgsStore = JSON.parse(JSON.stringify(e.detail))
|
||||
flowPreviewButtons?.openPreview(true)
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<CenteredPage>Loading...</CenteredPage>
|
||||
|
||||
@@ -8,12 +8,22 @@
|
||||
import SchemaForm from './SchemaForm.svelte'
|
||||
import FlowStatusViewer from '../components/FlowStatusViewer.svelte'
|
||||
import FlowProgressBar from './flows/FlowProgressBar.svelte'
|
||||
import { AlertTriangle, ArrowRight, CornerDownLeft, Play, RefreshCw, X } from 'lucide-svelte'
|
||||
import {
|
||||
AlertTriangle,
|
||||
ArrowRight,
|
||||
CornerDownLeft,
|
||||
Play,
|
||||
RefreshCw,
|
||||
X,
|
||||
ArrowLeftIcon
|
||||
} from 'lucide-svelte'
|
||||
import { emptyString, sendUserToast } from '$lib/utils'
|
||||
import DrawerContent from './common/drawer/DrawerContent.svelte'
|
||||
import SavedInputs from './SavedInputs.svelte'
|
||||
import { dfs } from './flows/dfs'
|
||||
import { sliceModules } from './flows/flowStateUtils'
|
||||
import CapturesInputs from '$lib/components/CapturesInputs.svelte'
|
||||
import ObjectViewer from './propertyPicker/ObjectViewer.svelte'
|
||||
|
||||
export let previewMode: 'upTo' | 'whole'
|
||||
export let open: boolean
|
||||
@@ -120,7 +130,10 @@
|
||||
$: selectedJobStep !== undefined && onSelectedJobStepChange()
|
||||
|
||||
let inputLibraryDrawer: Drawer
|
||||
let captureLibraryDrawer: Drawer
|
||||
let renderCount: number = 0
|
||||
|
||||
let selectedCapture: Record<string, any> | undefined = undefined
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
@@ -140,6 +153,65 @@
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<Drawer bind:this={captureLibraryDrawer}>
|
||||
<DrawerContent
|
||||
title="Trigger captures library {initialPath}"
|
||||
on:close={captureLibraryDrawer?.toggleDrawer}
|
||||
>
|
||||
<div class="h-full flex flex-col gap-2">
|
||||
<CapturesInputs
|
||||
flowPath={$pathStore}
|
||||
headless={false}
|
||||
addButton={true}
|
||||
on:select={(e) => {
|
||||
selectedCapture = e.detail
|
||||
}}
|
||||
on:openTriggers={(e) => {
|
||||
dispatch('openTriggers', e.detail)
|
||||
captureLibraryDrawer?.closeDrawer()
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="h-full overflow-hidden min-h-0 flex flex-col justify-between">
|
||||
<div class="w-full flex flex-col min-h-0 gap-2 px-2 py-2 grow">
|
||||
<div class="w-full flex flex-col">
|
||||
<Button
|
||||
color="blue"
|
||||
btnClasses="w-full"
|
||||
size="sm"
|
||||
spacingSize="xl"
|
||||
on:click={async () => {
|
||||
$previewArgs = JSON.parse(JSON.stringify(selectedCapture))
|
||||
captureLibraryDrawer?.closeDrawer()
|
||||
renderCount++
|
||||
}}
|
||||
disabled={!selectedCapture}
|
||||
>
|
||||
<ArrowLeftIcon class="w-4 h-4 mr-2" />
|
||||
Use input
|
||||
</Button>
|
||||
</div>
|
||||
<div class="w-full min-h-0 grow overflow-auto">
|
||||
{#if typeof selectedCapture == 'string' && selectedCapture == 'WINDMILL_TOO_BIG'}
|
||||
<div class="text-secondary mt-2">
|
||||
Payload too big to preview but can still be loaded</div
|
||||
>
|
||||
{:else if Object.keys(selectedCapture || {}).length > 0}
|
||||
<div class=" overflow-auto h-full p-2">
|
||||
<ObjectViewer json={selectedCapture} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-center text-tertiary">
|
||||
Select an Input to preview scripts arguments
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<div class="flex flex-col space-y-2 h-screen bg-surface px-6 py-2 w-full" id="flow-preview-content">
|
||||
<div class="flex flex-row justify-between w-full items-center gap-x-2">
|
||||
<div class="w-8">
|
||||
@@ -280,6 +352,14 @@
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
btnClasses="h-full truncate"
|
||||
size="sm"
|
||||
variant="border"
|
||||
on:click={() => {
|
||||
captureLibraryDrawer?.openDrawer()
|
||||
}}>Trigger captures library</Button
|
||||
>
|
||||
{#if initialPath != ''}
|
||||
<Button
|
||||
btnClasses="h-full truncate"
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
<script lang="ts">
|
||||
import { InputService, type RunnableType, type Job } from '$lib/gen/index.js'
|
||||
import { workspaceStore } from '$lib/stores.js'
|
||||
import { sendUserToast } from '$lib/utils.js'
|
||||
import JobSchemaPicker from '$lib/components/schema/JobSchemaPicker.svelte'
|
||||
import RunningJobSchemaPicker from '$lib/components/schema/RunningJobSchemaPicker.svelte'
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
import JobLoader from './runs/JobLoader.svelte'
|
||||
import { DataTable } from '$lib/components/table'
|
||||
import { clickOutside } from '$lib/utils'
|
||||
import InfiniteList from './InfiniteList.svelte'
|
||||
|
||||
export let scriptHash: string | null = null
|
||||
export let scriptPath: string | null = null
|
||||
export let flowPath: string | null = null
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let jobs: Job[] = []
|
||||
let loading: boolean = false
|
||||
let hasMoreCurrentRuns = false
|
||||
let page = 1
|
||||
let infiniteList: InfiniteList | undefined = undefined
|
||||
let loadInputsPageFn: ((page: number, perPage: number) => Promise<any>) | undefined = undefined
|
||||
|
||||
let runnableType: RunnableType | undefined = undefined
|
||||
$: runnableType = scriptHash
|
||||
? 'ScriptHash'
|
||||
: scriptPath
|
||||
? 'ScriptPath'
|
||||
: flowPath
|
||||
? 'FlowPath'
|
||||
: undefined
|
||||
|
||||
let runnableId: string | undefined = undefined
|
||||
function initLoadInputs() {
|
||||
runnableId = scriptHash || scriptPath || flowPath || undefined
|
||||
|
||||
loadInputsPageFn = async (page: number, perPage: number) => {
|
||||
const inputs = await InputService.getInputHistory({
|
||||
workspace: $workspaceStore!,
|
||||
runnableId,
|
||||
runnableType,
|
||||
page,
|
||||
perPage,
|
||||
includePreview: true
|
||||
})
|
||||
|
||||
const inputsWithPayload = await Promise.all(
|
||||
inputs.map(async (input) => {
|
||||
const payloadData = await loadArgsFromHistory(input.id, undefined, false)
|
||||
return {
|
||||
...input,
|
||||
payloadData
|
||||
}
|
||||
})
|
||||
)
|
||||
return inputsWithPayload
|
||||
}
|
||||
infiniteList?.setLoader(loadInputsPageFn)
|
||||
}
|
||||
|
||||
function handleSelected(data: any) {
|
||||
if (selected === data.id) {
|
||||
selected = undefined
|
||||
dispatch('select', undefined)
|
||||
return
|
||||
}
|
||||
selected = data.id
|
||||
dispatch('select', data.payloadData)
|
||||
}
|
||||
|
||||
let selected: string | undefined = undefined
|
||||
|
||||
onDestroy(() => {
|
||||
selected = undefined
|
||||
dispatch('select', undefined)
|
||||
})
|
||||
|
||||
async function getPropPickerElements(): Promise<HTMLElement[]> {
|
||||
return Array.from(
|
||||
document.querySelectorAll('[data-schema-picker], [data-schema-picker] *')
|
||||
) as HTMLElement[]
|
||||
}
|
||||
|
||||
async function loadArgsFromHistory(
|
||||
id: string | undefined,
|
||||
input: boolean | undefined,
|
||||
allowLarge: boolean
|
||||
): Promise<any> {
|
||||
if (!id) return
|
||||
const payloadData = await InputService.getArgsFromHistoryOrSavedInput({
|
||||
jobOrInputId: id,
|
||||
workspace: $workspaceStore!,
|
||||
input,
|
||||
allowLarge
|
||||
})
|
||||
return payloadData
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape' && selected) {
|
||||
selected = undefined
|
||||
dispatch('select', undefined)
|
||||
}
|
||||
}
|
||||
|
||||
function handleError(e: { type: string; error: any }) {
|
||||
if (e.type === 'load') {
|
||||
sendUserToast(`Failed to load input history: ${e.error}`, true)
|
||||
}
|
||||
}
|
||||
|
||||
let jobHovered: string | undefined = undefined
|
||||
|
||||
function refresh() {
|
||||
if (infiniteList) {
|
||||
infiniteList.loadData('refresh')
|
||||
}
|
||||
}
|
||||
|
||||
$: !loading && refresh()
|
||||
$: $workspaceStore && (scriptHash || scriptPath || flowPath) && infiniteList && initLoadInputs()
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeydown} />
|
||||
|
||||
{#if runnableId}
|
||||
<JobLoader
|
||||
bind:jobs
|
||||
path={runnableId}
|
||||
isSkipped={false}
|
||||
jobKindsCat="all"
|
||||
user={null}
|
||||
label={null}
|
||||
folder={null}
|
||||
concurrencyKey={null}
|
||||
tag={null}
|
||||
success="running"
|
||||
argFilter={undefined}
|
||||
bind:loading
|
||||
syncQueuedRunsCount={false}
|
||||
refreshRate={10000}
|
||||
computeMinAndMax={undefined}
|
||||
perPage={5}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="h-full w-full flex flex-col gap-4"
|
||||
use:clickOutside={{ capture: false, exclude: getPropPickerElements }}
|
||||
on:click_outside={() => {
|
||||
if (selected) {
|
||||
selected = undefined
|
||||
dispatch('select', undefined)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="grow-0">
|
||||
<DataTable size="xs" bind:currentPage={page} hasMore={hasMoreCurrentRuns} tableFixed={true}>
|
||||
{#if loading && (jobs == undefined || jobs?.length == 0)}
|
||||
<div class="text-center text-tertiary text-xs py-2">Loading current runs...</div>
|
||||
{:else if jobs?.length > 0}
|
||||
<colgroup>
|
||||
<col class="w-8" />
|
||||
<col class="w-20" />
|
||||
<col />
|
||||
</colgroup>
|
||||
|
||||
<tbody class="w-full overflow-y-auto">
|
||||
{#each jobs as job (job.id)}
|
||||
<RunningJobSchemaPicker
|
||||
{job}
|
||||
selected={selected === job.id}
|
||||
hovering={jobHovered === job.id}
|
||||
on:select={(e) => handleSelected(e.detail)}
|
||||
/>
|
||||
{/each}
|
||||
{#if jobs?.length == 5}
|
||||
<div class="text-left text-tertiary text-xs"
|
||||
>... there may be more runs not displayed here as the limit is 5</div
|
||||
>
|
||||
{/if}
|
||||
</tbody>
|
||||
{:else}
|
||||
<div class="text-center text-tertiary text-xs py-2">No job currently running</div>
|
||||
{/if}
|
||||
</DataTable>
|
||||
</div>
|
||||
|
||||
<div class="min-h-0 grow">
|
||||
<InfiniteList
|
||||
bind:this={infiniteList}
|
||||
selectedItemId={selected}
|
||||
on:error={(e) => handleError(e.detail)}
|
||||
on:select={(e) => handleSelected(e.detail)}
|
||||
>
|
||||
<svelte:fragment slot="columns">
|
||||
<colgroup>
|
||||
<col class="w-8" />
|
||||
<col class="w-20" />
|
||||
<col />
|
||||
</colgroup>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment let:item let:hover>
|
||||
<JobSchemaPicker
|
||||
job={item}
|
||||
selected={selected === item.id}
|
||||
hovering={hover}
|
||||
payloadData={item.payloadData}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="empty">
|
||||
<div class="text-center text-tertiary text-xs py-2">No previous Runs</div>
|
||||
</svelte:fragment>
|
||||
</InfiniteList>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,172 @@
|
||||
<script lang="ts">
|
||||
import DataTable from './table/DataTable.svelte'
|
||||
import Skeleton from './common/skeleton/Skeleton.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Row } from './table/index'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let loading = false
|
||||
export let items: any[] | undefined = undefined
|
||||
export let selectedItemId: any | undefined = undefined
|
||||
export let isEmpty: boolean = true
|
||||
export let length: number = 0
|
||||
|
||||
let hasMore = false
|
||||
let page = 1
|
||||
let perPage = 10
|
||||
let hasAlreadyFailed = false
|
||||
let hovered: any | undefined = undefined
|
||||
let initLoad = false
|
||||
let loadInputs: ((page: number, perPage: number) => Promise<any[]>) | undefined = undefined
|
||||
let deleteItemFn: ((id: any) => Promise<any>) | undefined = undefined
|
||||
|
||||
export async function loadData(loadOption: 'refresh' | 'forceRefresh' | 'loadMore' = 'loadMore') {
|
||||
if (!loadInputs) return
|
||||
loading = true
|
||||
hasMore = hasMore
|
||||
|
||||
if (hasMore && loadOption === 'loadMore') {
|
||||
page++
|
||||
}
|
||||
|
||||
try {
|
||||
const newItems = await loadInputs(1, page * perPage)
|
||||
|
||||
if (
|
||||
loadOption === 'refresh' &&
|
||||
items &&
|
||||
items?.length > 0 &&
|
||||
newItems.length === items?.length &&
|
||||
newItems.every((i, index) => i.id === items?.[index]?.id)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const existingIds = new Set(items?.map((i) => i.id) || [])
|
||||
items = newItems.map((item) => ({
|
||||
...item,
|
||||
isNew: initLoad && !existingIds.has(item.id)
|
||||
}))
|
||||
|
||||
setTimeout(() => {
|
||||
if (items) {
|
||||
items = items.map((item) => ({
|
||||
...item,
|
||||
isNew: false
|
||||
}))
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
page = Math.floor(items.length / perPage) + 1
|
||||
hasMore = items.length === perPage * (page - 1)
|
||||
initLoad = true
|
||||
isEmpty = items.length === 0
|
||||
length = items.length
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
if (hasAlreadyFailed) return
|
||||
hasAlreadyFailed = true
|
||||
dispatch('error', { type: 'load', error: err })
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteItem(id: string) {
|
||||
if (!deleteItemFn) return
|
||||
try {
|
||||
items = items?.map((i) => (i.id === id ? { ...i, isDeleting: true } : i)) ?? []
|
||||
|
||||
setTimeout(async () => {
|
||||
deleteItemFn ? await deleteItemFn(id) : null
|
||||
if (selectedItemId === id) {
|
||||
selectedItemId = null
|
||||
}
|
||||
loadData('refresh')
|
||||
}, 100)
|
||||
} catch (err) {
|
||||
dispatch('error', { type: 'delete', error: err })
|
||||
}
|
||||
}
|
||||
|
||||
export async function setLoader(loader: (page: number, perPage: number) => Promise<any[]>) {
|
||||
loadInputs = loader
|
||||
loadData('forceRefresh')
|
||||
}
|
||||
|
||||
export async function setDeleteItemFn(fn: (id: any) => Promise<any>) {
|
||||
deleteItemFn = fn
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="h-full">
|
||||
{#if items === undefined && loading}
|
||||
<Skeleton layout={[[8]]} />
|
||||
{:else if items && items.length > 0}
|
||||
<DataTable
|
||||
size="xs"
|
||||
infiniteScroll
|
||||
{hasMore}
|
||||
tableFixed={true}
|
||||
on:loadMore={() => {
|
||||
loadData()
|
||||
}}
|
||||
>
|
||||
<slot name="columns" />
|
||||
|
||||
<tbody class="h-full w-full">
|
||||
{#each items ?? [] as item, index}
|
||||
{@const hover = item.id === hovered}
|
||||
<Row
|
||||
on:click={() => dispatch('select', item)}
|
||||
class={twMerge(
|
||||
selectedItemId === item.id ? 'bg-surface-selected' : 'hover:bg-surface-hover',
|
||||
'cursor-pointer rounded-md',
|
||||
item.isNew && index === 0 ? 'animate-slideIn' : 'group'
|
||||
)}
|
||||
on:hover={(e) => (hovered = e.detail ? item.id : undefined)}
|
||||
>
|
||||
<slot {item} {hover} />
|
||||
</Row>
|
||||
{/each}
|
||||
</tbody>
|
||||
</DataTable>
|
||||
{:else}
|
||||
<slot name="empty" {items} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@keyframes slideOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
max-height: 100px;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateX(-100%);
|
||||
max-height: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes greenHighlight {
|
||||
0% {
|
||||
background-color: rgba(70, 255, 138, 0.5);
|
||||
box-shadow: 0 0 15px rgb(34 197 94 / 0.3);
|
||||
}
|
||||
100% {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.animate-slideIn) {
|
||||
animation: greenHighlight 2s ease-out forwards;
|
||||
will-change: transform, opacity, background-color, box-shadow;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
@@ -309,19 +309,21 @@
|
||||
/>
|
||||
{/if}
|
||||
{:else if inputCat == 'boolean'}
|
||||
<Toggle
|
||||
on:pointerdown={(e) => {
|
||||
e?.stopPropagation()
|
||||
}}
|
||||
class={valid && error == ''
|
||||
? ''
|
||||
: 'border !border-red-700 !border-opacity-70 focus:!border-red-700 focus:!border-opacity-30'}
|
||||
bind:checked={value}
|
||||
{disabled}
|
||||
/>
|
||||
{#if type == 'boolean' && value == undefined}
|
||||
<span> Not set</span>
|
||||
{/if}
|
||||
<div class="w-full">
|
||||
<Toggle
|
||||
on:pointerdown={(e) => {
|
||||
e?.stopPropagation()
|
||||
}}
|
||||
class={valid && error == ''
|
||||
? ''
|
||||
: 'border !border-red-700 !border-opacity-70 focus:!border-red-700 focus:!border-opacity-30'}
|
||||
bind:checked={value}
|
||||
{disabled}
|
||||
/>
|
||||
{#if type == 'boolean' && value == undefined}
|
||||
<span> Not set</span>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if inputCat == 'list'}
|
||||
<div class="w-full">
|
||||
{#if Array.isArray(itemsType?.multiselect) && Array.isArray(value)}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
import { displayDate } from '$lib/utils.js'
|
||||
import { InputService, type CreateInput, type RunnableType } from '$lib/gen/index.js'
|
||||
import { workspaceStore } from '$lib/stores.js'
|
||||
import { Button } from '$lib/components/common'
|
||||
import { Save } from 'lucide-svelte'
|
||||
import { sendUserToast } from '$lib/utils.js'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let runnableId: string | undefined
|
||||
export let runnableType: RunnableType | undefined
|
||||
export let args: object
|
||||
export let disabled: boolean = false
|
||||
export let small: boolean | undefined = undefined
|
||||
export let showTooltip: boolean | undefined = undefined
|
||||
|
||||
let savingInputs = false
|
||||
|
||||
async function saveInput(args: object) {
|
||||
savingInputs = true
|
||||
|
||||
const requestBody: CreateInput = {
|
||||
name: 'Saved ' + displayDate(new Date()),
|
||||
args: args as any
|
||||
}
|
||||
|
||||
try {
|
||||
await InputService.createInput({
|
||||
workspace: $workspaceStore!,
|
||||
runnableId,
|
||||
runnableType,
|
||||
requestBody
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
sendUserToast(`Failed to save Input: ${err}`, true)
|
||||
}
|
||||
|
||||
savingInputs = false
|
||||
dispatch('update')
|
||||
}
|
||||
</script>
|
||||
|
||||
<Button
|
||||
on:click={() => saveInput(args)}
|
||||
{disabled}
|
||||
loading={savingInputs}
|
||||
startIcon={{ icon: Save }}
|
||||
variant={small ? 'border' : 'contained'}
|
||||
color="light"
|
||||
size="xs"
|
||||
>
|
||||
<span>{small ? 'Save inputs' : 'Save current input'}</span>
|
||||
{#if showTooltip}
|
||||
<Tooltip>Shared inputs are available to anyone with access to the script</Tooltip>
|
||||
{/if}
|
||||
</Button>
|
||||
@@ -4,22 +4,22 @@
|
||||
InputService,
|
||||
type Input,
|
||||
type RunnableType,
|
||||
type CreateInput,
|
||||
type Job,
|
||||
JobService
|
||||
} from '$lib/gen/index.js'
|
||||
import { userStore, workspaceStore } from '$lib/stores.js'
|
||||
import { base } from '$lib/base'
|
||||
import { classNames, displayDate, displayDateOnly, sendUserToast } from '$lib/utils.js'
|
||||
import { classNames, displayDateOnly, sendUserToast } from '$lib/utils.js'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import ObjectViewer from './propertyPicker/ObjectViewer.svelte'
|
||||
import { ArrowLeftIcon, Edit, ExternalLink, Save, X } from 'lucide-svelte'
|
||||
import { ArrowLeftIcon, Edit, ExternalLink, X } from 'lucide-svelte'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import TimeAgo from './TimeAgo.svelte'
|
||||
import JobLoader from './runs/JobLoader.svelte'
|
||||
import Skeleton from './common/skeleton/Skeleton.svelte'
|
||||
import SaveInputsButton from './SaveInputsButton.svelte'
|
||||
|
||||
export let scriptHash: string | null = null
|
||||
export let scriptPath: string | null = null
|
||||
@@ -40,7 +40,6 @@
|
||||
let selectedInput: Input | null
|
||||
let jobs: Job[] = []
|
||||
let loading: boolean = false
|
||||
let savingInputs = false
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: runnableId = scriptHash || scriptPath || flowPath || undefined
|
||||
@@ -80,38 +79,6 @@
|
||||
})
|
||||
}
|
||||
|
||||
async function saveInput(args: object) {
|
||||
savingInputs = true
|
||||
|
||||
const requestBody: CreateInput = {
|
||||
name: 'Saved ' + displayDate(new Date()),
|
||||
args: args as any
|
||||
}
|
||||
|
||||
try {
|
||||
let id = await InputService.createInput({
|
||||
workspace: $workspaceStore!,
|
||||
runnableId,
|
||||
runnableType,
|
||||
requestBody
|
||||
})
|
||||
|
||||
const input = {
|
||||
id,
|
||||
created_by: '',
|
||||
created_at: new Date().toISOString(),
|
||||
is_public: false,
|
||||
...requestBody
|
||||
}
|
||||
savedInputs = [input, ...(savedInputs ?? [])]
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
sendUserToast(`Failed to save Input: ${err}`, true)
|
||||
}
|
||||
|
||||
savingInputs = false
|
||||
}
|
||||
|
||||
async function updateInput(input: EditableInput) {
|
||||
input.isSaving = true
|
||||
|
||||
@@ -176,25 +143,26 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<JobLoader
|
||||
bind:jobs
|
||||
path={runnableId ?? null}
|
||||
isSkipped={false}
|
||||
jobKindsCat="jobs"
|
||||
jobKinds="all"
|
||||
user={null}
|
||||
label={null}
|
||||
folder={null}
|
||||
concurrencyKey={null}
|
||||
tag={null}
|
||||
success="running"
|
||||
argFilter={undefined}
|
||||
bind:loading
|
||||
syncQueuedRunsCount={false}
|
||||
refreshRate={10000}
|
||||
computeMinAndMax={undefined}
|
||||
perPage={5}
|
||||
/>
|
||||
{#if runnableId}
|
||||
<JobLoader
|
||||
bind:jobs
|
||||
path={runnableId}
|
||||
isSkipped={false}
|
||||
jobKindsCat="all"
|
||||
user={null}
|
||||
label={null}
|
||||
folder={null}
|
||||
concurrencyKey={null}
|
||||
tag={null}
|
||||
success="running"
|
||||
argFilter={undefined}
|
||||
bind:loading
|
||||
syncQueuedRunsCount={false}
|
||||
refreshRate={10000}
|
||||
computeMinAndMax={undefined}
|
||||
perPage={5}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="min-w-[300px] h-full">
|
||||
<Splitpanes horizontal={true}>
|
||||
@@ -202,21 +170,20 @@
|
||||
<div class="w-full flex flex-col gap-4 p-2">
|
||||
<div class="w-full flex justify-between items-center gap-4 flex-wrap">
|
||||
<span class="text-sm font-semibold flex-shrink-0"
|
||||
>Saved Inputs <Tooltip
|
||||
>Saved Inputs<Tooltip
|
||||
>Shared inputs are available to anyone with access to the script</Tooltip
|
||||
></span
|
||||
>
|
||||
{#if canSaveInputs}
|
||||
<Button
|
||||
on:click={() => saveInput(args)}
|
||||
<SaveInputsButton
|
||||
{args}
|
||||
disabled={!isValid}
|
||||
loading={savingInputs}
|
||||
startIcon={{ icon: Save }}
|
||||
color="light"
|
||||
size="xs"
|
||||
>
|
||||
<span>Save Current Input</span>
|
||||
</Button>
|
||||
{runnableId}
|
||||
{runnableType}
|
||||
on:update={() => {
|
||||
loadSavedInputs()
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -385,7 +352,7 @@
|
||||
>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="text-left text-tertiary text-xs">No running runs</div>
|
||||
<div class="text-left text-tertiary text-xs">No job currently running</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/common'
|
||||
import { InputService, type Input, type RunnableType } from '$lib/gen/index.js'
|
||||
import { userStore, workspaceStore } from '$lib/stores.js'
|
||||
import { sendUserToast } from '$lib/utils.js'
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
import { Edit, Trash2, Save } from 'lucide-svelte'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import { Cell } from './table/index'
|
||||
import { clickOutside } from '$lib/utils'
|
||||
import SaveInputsButton from '$lib/components/SaveInputsButton.svelte'
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
import InfiniteList from './InfiniteList.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let flowPath: string | null = null
|
||||
export let previewArgs: any = undefined
|
||||
|
||||
interface EditableInput extends Input {
|
||||
isEditing?: boolean
|
||||
isSaving?: boolean
|
||||
isNew?: boolean
|
||||
isDeleting?: boolean
|
||||
}
|
||||
|
||||
let infiniteList: InfiniteList | null = null
|
||||
let draft = true
|
||||
let selectedInput: string | null = null
|
||||
let isEditing: EditableInput | null = null
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: runnableId = flowPath || undefined
|
||||
let runnableType: RunnableType | undefined = undefined
|
||||
$: runnableType = flowPath ? 'FlowPath' : undefined
|
||||
|
||||
async function updateInput(input: EditableInput | null) {
|
||||
if (!input) return
|
||||
input.isSaving = true
|
||||
|
||||
try {
|
||||
await InputService.updateInput({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
id: input.id,
|
||||
name: input.name,
|
||||
is_public: input.is_public
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
sendUserToast(`Failed to update Input: ${err}`, true)
|
||||
}
|
||||
|
||||
input.isSaving = false
|
||||
}
|
||||
|
||||
function initLoadInputs() {
|
||||
const loadInputsPageFn = async (page: number, perPage: number) => {
|
||||
return await InputService.listInputs({
|
||||
workspace: $workspaceStore!,
|
||||
runnableId,
|
||||
runnableType,
|
||||
page,
|
||||
perPage
|
||||
})
|
||||
}
|
||||
infiniteList?.setLoader(loadInputsPageFn)
|
||||
|
||||
const deleteInputFn = async (id: string) => {
|
||||
await InputService.deleteInput({
|
||||
workspace: $workspaceStore!,
|
||||
input: id
|
||||
})
|
||||
}
|
||||
infiniteList?.setDeleteItemFn(deleteInputFn)
|
||||
}
|
||||
|
||||
async function loadLargeArgs(
|
||||
id: string | undefined,
|
||||
input: boolean | undefined,
|
||||
allowLarge: boolean
|
||||
): Promise<any> {
|
||||
if (!id) return
|
||||
return await InputService.getArgsFromHistoryOrSavedInput({
|
||||
jobOrInputId: id,
|
||||
workspace: $workspaceStore!,
|
||||
input,
|
||||
allowLarge
|
||||
})
|
||||
}
|
||||
|
||||
let selectedArgs: any = undefined
|
||||
async function handleSelect(input: EditableInput) {
|
||||
if (input.isEditing) return
|
||||
|
||||
if (selectedInput === input.id) {
|
||||
selectedInput = null
|
||||
selectedArgs = undefined
|
||||
dispatch('select', undefined)
|
||||
} else {
|
||||
selectedInput = input.id
|
||||
selectedArgs = await loadLargeArgs(input.id, true, false)
|
||||
dispatch('select', selectedArgs)
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
selectedInput = null
|
||||
selectedArgs = undefined
|
||||
dispatch('select', undefined)
|
||||
})
|
||||
|
||||
async function getPropPickerElements(): Promise<HTMLElement[]> {
|
||||
const elements = document.querySelectorAll('[data-schema-picker], [data-schema-picker] *')
|
||||
return elements ? (Array.from(elements) as HTMLElement[]) : []
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape' && isEditing) {
|
||||
setEditing(null)
|
||||
} else if (event.key === 'Escape' && selectedInput) {
|
||||
selectedInput = null
|
||||
selectedArgs = undefined
|
||||
dispatch('select', undefined)
|
||||
}
|
||||
|
||||
if (event.key === 'Enter' && isEditing) {
|
||||
updateInput(isEditing).then(() => {
|
||||
setEditing(null)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function setEditing(input: EditableInput | null) {
|
||||
isEditing = input
|
||||
dispatch('isEditing', !!input)
|
||||
}
|
||||
|
||||
function handleError(error: { type: string; error: any }) {
|
||||
if (error.type === 'delete') {
|
||||
sendUserToast(`Failed to delete saved input: ${error.error}`, true)
|
||||
} else if (error.type === 'load') {
|
||||
sendUserToast(`Failed to load saved inputs: ${error.error}`, true)
|
||||
}
|
||||
}
|
||||
|
||||
$: $workspaceStore && flowPath && (infiniteList && initLoadInputs(), (draft = false))
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeydown} />
|
||||
|
||||
<div
|
||||
class="w-full flex flex-col gap-1 h-full overflow-y-auto p"
|
||||
use:clickOutside={{ capture: false, exclude: getPropPickerElements }}
|
||||
on:click_outside={() => {
|
||||
selectedInput = null
|
||||
selectedArgs = undefined
|
||||
dispatch('select', undefined)
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Popover class="w-full" placement="bottom" disablePopup={flowPath && previewArgs}>
|
||||
<svelte:fragment slot="text">
|
||||
{#if !flowPath}
|
||||
Save draft first before you can save inputs
|
||||
{:else if !previewArgs}
|
||||
Add inputs before saving
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<SaveInputsButton
|
||||
{runnableId}
|
||||
{runnableType}
|
||||
args={previewArgs ?? {}}
|
||||
disabled={!previewArgs || !flowPath}
|
||||
on:update={() => {
|
||||
infiniteList?.loadData('refresh')
|
||||
}}
|
||||
showTooltip={true}
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
<div class="grow min-h-0">
|
||||
{#if !draft}
|
||||
<InfiniteList
|
||||
bind:this={infiniteList}
|
||||
selectedItemId={selectedInput}
|
||||
on:error={(e) => handleError(e.detail)}
|
||||
on:select={(e) => handleSelect(e.detail)}
|
||||
>
|
||||
<svelte:fragment slot="columns">
|
||||
<colgroup>
|
||||
<col class="w-8" />
|
||||
<col />
|
||||
</colgroup>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment let:item>
|
||||
<Cell>
|
||||
<div class="center-center">
|
||||
<Save size={12} />
|
||||
</div>
|
||||
</Cell>
|
||||
<Cell>
|
||||
<div
|
||||
class="w-full flex items-center text-sm justify-between gap-4 px-4 text-left transition-all"
|
||||
>
|
||||
<div class="w-full h-full items-center justify-between flex gap-1 min-w-0 p-1">
|
||||
{#if isEditing && isEditing.id === item.id}
|
||||
<form
|
||||
on:submit={() => {
|
||||
updateInput(isEditing)
|
||||
setEditing(null)
|
||||
}}
|
||||
class="w-full"
|
||||
>
|
||||
<input type="text" bind:value={isEditing.name} class="text-secondary" />
|
||||
</form>
|
||||
{:else}
|
||||
<small
|
||||
class="whitespace-nowrap overflow-hidden text-ellipsis flex-shrink text-left"
|
||||
>
|
||||
{item.name}
|
||||
</small>
|
||||
{/if}
|
||||
{#if item.created_by == $userStore?.username || $userStore?.is_admin || $userStore?.is_super_admin}
|
||||
<div class="items-center flex gap-2">
|
||||
{#if !isEditing || isEditing?.id !== item.id}
|
||||
<div class="group-hover:block hidden -my-2">
|
||||
<Toggle
|
||||
size="xs"
|
||||
options={{ right: 'shared' }}
|
||||
checked={item.is_public}
|
||||
on:change={(e) => {
|
||||
updateInput({ ...item, is_public: e.detail })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Button
|
||||
loading={isEditing?.id === item.id && isEditing?.isSaving}
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="border"
|
||||
spacingSize="xs2"
|
||||
btnClasses={'group-hover:block hidden -my-2'}
|
||||
on:click={(e) => {
|
||||
e.stopPropagation()
|
||||
if (isEditing?.id === item.id) {
|
||||
updateInput(isEditing)
|
||||
setEditing(null)
|
||||
} else {
|
||||
setEditing(item)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Edit class="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
spacingSize="xs2"
|
||||
variant="contained"
|
||||
btnClasses={twMerge(
|
||||
isEditing?.id === item.id ? 'block' : 'group-hover:block hidden -my-2',
|
||||
'hover:text-white hover:bg-red-500 text-red-500'
|
||||
)}
|
||||
on:click={() => {
|
||||
infiniteList?.deleteItem(item.id)
|
||||
}}
|
||||
>
|
||||
<Trash2 class="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<span class="text-xs text-tertiary">By {item.created_by}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Cell>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="empty">
|
||||
<div class="text-center text-xs text-tertiary">No saved Inputs</div>
|
||||
</svelte:fragment>
|
||||
</InfiniteList>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -118,6 +118,7 @@
|
||||
selectedTab === 'preprocessor' ? { _ENTRYPOINT_OVERRIDE: 'preprocessor', ...args } : args,
|
||||
tag
|
||||
)
|
||||
setFocusToLogs()
|
||||
}
|
||||
|
||||
async function loadPastTests(): Promise<void> {
|
||||
@@ -249,6 +250,14 @@
|
||||
$: selectedTab && inferSchema(code)
|
||||
|
||||
let argsRender = 0
|
||||
export async function updateArgs(newArgs: Record<string, any>) {
|
||||
if (Object.keys(newArgs).length > 0) {
|
||||
args = { ...newArgs }
|
||||
argsRender++
|
||||
}
|
||||
}
|
||||
|
||||
let setFocusToLogs = () => {}
|
||||
</script>
|
||||
|
||||
<TestJobLoader
|
||||
@@ -425,7 +434,7 @@
|
||||
</div>
|
||||
<Splitpanes horizontal class="!max-h-[calc(100%-43px)]">
|
||||
<Pane size={33}>
|
||||
<div class="px-2">
|
||||
<div class="px-4">
|
||||
<div class="break-words relative font-sans">
|
||||
{#key argsRender}
|
||||
<SchemaForm
|
||||
@@ -447,6 +456,7 @@
|
||||
</Pane>
|
||||
<Pane size={67} class="relative">
|
||||
<LogPanel
|
||||
bind:setFocusToLogs
|
||||
{lang}
|
||||
previewJob={testJob}
|
||||
{pastPreviews}
|
||||
@@ -469,12 +479,10 @@
|
||||
<div class="h-full p-2">
|
||||
<CaptureTable
|
||||
bind:this={captureTable}
|
||||
captureType="all"
|
||||
{hasPreprocessor}
|
||||
canHavePreprocessor={lang === 'bun' || lang === 'deno' || lang === 'python3'}
|
||||
isFlow={false}
|
||||
path={path ?? ''}
|
||||
hideCapturesWhenEmpty={false}
|
||||
canEdit={true}
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
export function setSchema(newSchema: Schema) {
|
||||
schema = newSchema
|
||||
}
|
||||
const yOffset = 80
|
||||
</script>
|
||||
|
||||
<EditableSchemaForm bind:schema uiOnly offset={yOffset} />
|
||||
<EditableSchemaForm bind:schema uiOnly editTab="inputEditor" />
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
export let headless: boolean = false
|
||||
</script>
|
||||
|
||||
<div class="w-full">
|
||||
<div class="w-full h-full flex flex-col">
|
||||
{#if !headless}
|
||||
<div class="flex flex-row justify-between items-center mb-2">
|
||||
<h2
|
||||
@@ -55,7 +55,9 @@
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div class={collapsable && collapsed ? `hidden ${$$props.class}` : `${$$props.class}`}>
|
||||
<slot />
|
||||
</div>
|
||||
{#if !collapsable || !collapsed}
|
||||
<div class={twMerge('grow min-h-0', $$props.class)}>
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
export let formatAction: (() => void) | undefined = undefined
|
||||
export let automaticLayout = true
|
||||
export let extraLib: string = ''
|
||||
export let placeholder: string = ''
|
||||
|
||||
export let shouldBindKey: boolean = true
|
||||
export let autoHeight = false
|
||||
@@ -106,6 +107,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
let placeholderVisible = false
|
||||
function updatePlaceholderVisibility(value: string) {
|
||||
if (!value) return
|
||||
placeholderVisible = value.trim() === ''
|
||||
}
|
||||
|
||||
export function format() {
|
||||
if (editor) {
|
||||
code = getCode()
|
||||
@@ -302,6 +309,13 @@
|
||||
$tailwindClassesLoaded = true
|
||||
addTailwindClassCompletions()
|
||||
}
|
||||
|
||||
if (placeholder) {
|
||||
editor.onDidChangeModelContent(() => {
|
||||
const value = editor.getValue()
|
||||
updatePlaceholderVisibility(value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function addCSSClassCompletions() {
|
||||
@@ -417,6 +431,8 @@
|
||||
editor.focus()
|
||||
}
|
||||
}
|
||||
|
||||
updatePlaceholderVisibility(code)
|
||||
</script>
|
||||
|
||||
<EditorTheme />
|
||||
@@ -431,9 +447,20 @@
|
||||
{/if}
|
||||
<div
|
||||
bind:this={divEl}
|
||||
class="{$$props.class ?? ''} editor simple-editor {!allowVim ? 'nonmain-editor' : ''}"
|
||||
class="relative {$$props.class ?? ''} editor simple-editor {!allowVim ? 'nonmain-editor' : ''}"
|
||||
bind:clientWidth={width}
|
||||
/>
|
||||
>
|
||||
{#if placeholder}
|
||||
<div
|
||||
id="placeholder"
|
||||
class="absolute left-[24px] text-gray-500 text-sm pointer-events-none font-mono z-10 {placeholderVisible
|
||||
? ''
|
||||
: 'hidden'}"
|
||||
>
|
||||
{@html placeholder}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if allowVim && $vimMode}
|
||||
<div class="fixed bottom-0 z-30" bind:this={statusDiv} />
|
||||
{/if}
|
||||
|
||||
@@ -108,9 +108,11 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-2 w-full">
|
||||
{#if allowKindChange}
|
||||
<ToggleButtonGroup
|
||||
tabListClass="flex-wrap"
|
||||
class="h-auto"
|
||||
bind:selected={kind}
|
||||
on:selected={(e) => {
|
||||
if (e.detail != 'enum') {
|
||||
|
||||
@@ -59,70 +59,6 @@
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
// Order of classes: border, border modifier, bg, bg modifier, text, text modifier, everything else
|
||||
const colorVariants: Record<ButtonType.Color, Record<ButtonType.Variant, string>> = {
|
||||
none: {
|
||||
border: '',
|
||||
contained: '',
|
||||
divider: ''
|
||||
},
|
||||
|
||||
blue: {
|
||||
border:
|
||||
'border-frost-500 dark:border-frost-300 hover:border-frost-700 dark:hover:border-frost-400 focus-visible:border-frost-700 bg-surface hover:bg-frost-100 dark:hover:bg-frost-900 focus-visible:bg-frost-100 focus-visible:dark:text-frost-100 dark:focus-visible:bg-frost-900 text-frost-500 dark:text-frost-300 dark:hover:text-frost-400 hover:text-frost-700 focus-visible:text-frost-700 focus-visible:ring-frost-300',
|
||||
contained:
|
||||
'bg-frost-500 hover:bg-frost-700 focus-visible:bg-frost-700 text-white focus-visible:ring-frost-300 dark:bg-frost-500/90 dark:hover:bg-frost-600/90',
|
||||
divider: 'divide-x divide-frost-600'
|
||||
},
|
||||
marine: {
|
||||
border:
|
||||
'border-marine-300 dark:border-marine-200 hover:border-marine-500 dark:hover:border-marine-400 focus-visible:border-marine-500 bg-surface hover:bg-marine-500 dark:hover:bg-marine-400 focus-visible:bg-marine-100 focus-visible:dark:text-marine-50 dark:focus-visible:bg-marine-500 text-marine-50 dark:text-marine-50 dark:hover:text-marine-50 hover:text-marine-50 focus-visible:text-marine-300 focus-visible:ring-marine-200',
|
||||
contained:
|
||||
'bg-marine-300 hover:bg-marine-500 focus-visible:bg-marine-500 text-gray-50 focus-visible:ring-marine-200 dark:bg-marine-200 dark:hover:bg-marine-400',
|
||||
divider: 'divide-x divide-marine-500'
|
||||
},
|
||||
red: {
|
||||
border:
|
||||
'border-red-600/60 hover:border-red-600 bg-surface hover:bg-red-100 text-red-600 hover:text-red-700 focus-visible:ring-red-300 dark:border-red-400/90 dark:text-red-400 dark:hover:border-red-400 dark:hover:bg-red-500/60 dark:hover:text-red-100',
|
||||
contained:
|
||||
'bg-red-600 hover:bg-red-600 text-white focus-visible:ring-red-300 dark:border-red-400/70 dark:bg-red-700/90 dark:hover:bg-red-900 dark:hover:border-red-300 dark:text-primary',
|
||||
divider: 'divide-x divide-red-700'
|
||||
},
|
||||
green: {
|
||||
border:
|
||||
'border-green-600 hover:border-green-700 bg-surface hover:bg-green-100 text-green-600 hover:text-green-700 focus-visible:ring-green-300 dark:hover:bg-green-800 dark:border-green-400/90',
|
||||
contained:
|
||||
'bg-green-600 hover:bg-green-700 text-white focus-visible:ring-green-300 dark:bg-green-700/90 dark:hover:bg-green-800',
|
||||
divider: 'divide-x divide-green-700'
|
||||
},
|
||||
dark: {
|
||||
border:
|
||||
'border-marine-300 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-marine-200',
|
||||
contained:
|
||||
'bg-marine-400 hover:bg-marine-200 focus-visible:bg-surface-hover-inverse text-primary-inverse focus-visible:ring-surface-selected-inverse dark:bg-marine-50 dark:hover:bg-marine-50/70 dark:text-primary-inverse',
|
||||
divider: 'divide-x divide-gray-800 dark:divide-gray-200'
|
||||
},
|
||||
gray: {
|
||||
border:
|
||||
'border-gray-500 dark:border-gray-300 hover:border-gray-700 dark:hover:border-gray-400 focus-visible:border-gray-700 bg-surface hover:bg-gray-100 dark:hover:bg-gray-900 focus-visible:bg-gray-100 focus-visible:dark:text-gray-100 dark:focus-visible:bg-gray-900 text-gray-500 dark:text-gray-300 dark:hover:text-gray-400 hover:text-gray-700 focus-visible:text-gray-700 focus-visible:ring-gray-300',
|
||||
contained:
|
||||
'bg-gray-500 hover:bg-gray-700 focus-visible:bg-gray-700 text-white focus-visible:ring-gray-300',
|
||||
divider: 'divide-x divide-gray-600'
|
||||
},
|
||||
light: {
|
||||
border:
|
||||
'border 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',
|
||||
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'
|
||||
}
|
||||
}
|
||||
|
||||
async function onClick(event: MouseEvent) {
|
||||
if (!nonCaptureEvent) {
|
||||
@@ -136,8 +72,8 @@
|
||||
}
|
||||
|
||||
function getColorClass(color, variant) {
|
||||
if (color in colorVariants) {
|
||||
return colorVariants[color][variant]
|
||||
if (color in ButtonType.ColorVariants) {
|
||||
return ButtonType.ColorVariants[color][variant]
|
||||
} else {
|
||||
return color
|
||||
}
|
||||
@@ -182,9 +118,14 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="{dropdownItems && dropdownItems.length > 0 && variant === 'contained'
|
||||
? colorVariants[color].divider
|
||||
: ''} {wrapperClasses} flex flex-row"
|
||||
class={twMerge(
|
||||
dropdownItems && dropdownItems.length > 0 && variant === 'contained'
|
||||
? ButtonType.ColorVariants[color].divider
|
||||
: '',
|
||||
wrapperClasses,
|
||||
'flex flex-row',
|
||||
disabled ? 'divide-text-disabled' : ''
|
||||
)}
|
||||
style={wrapperStyle}
|
||||
>
|
||||
{#if href}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<script>
|
||||
export let pulseDuration = 2
|
||||
export let numberOfPulses = 3
|
||||
export let scale = 1.2
|
||||
|
||||
let isPulsing = false
|
||||
let pulseCount = 1
|
||||
|
||||
export function triggerPulse(count) {
|
||||
if (pulseCount < 1) return
|
||||
if (!isPulsing) {
|
||||
pulseCount = count
|
||||
isPulsing = true
|
||||
setTimeout(() => {
|
||||
isPulsing = false
|
||||
}, pulseDuration * count * 1000)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative">
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
{#each Array(numberOfPulses) as _, i}
|
||||
<div
|
||||
class:deactivate={isPulsing}
|
||||
style={`--pulse-duration: ${pulseDuration}s; --i:${i}; --scale:${scale}; --number-of-pulses:${numberOfPulses}; --pulse-count:${pulseCount};`}
|
||||
>
|
||||
<div class="pulse" class:active={isPulsing} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.relative {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.pulse {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgb(81, 151, 255);
|
||||
border-radius: 8px;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.pulse.active {
|
||||
animation: pulse var(--pulse-duration) ease-out infinite;
|
||||
animation-delay: calc(var(--i) * var(--pulse-duration) / var(--number-of-pulses));
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.deactivate {
|
||||
animation: fade calc(var(--pulse-duration) * 0.3) ease-out;
|
||||
animation-delay: calc(var(--pulse-duration) * var(--pulse-count) - var(--pulse-duration) * 0.2);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@keyframes fade {
|
||||
0% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
100% {
|
||||
transform: translate(-50%, -50%) scale(var(--scale));
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
export let color: 'light' | 'dark' | undefined = 'light'
|
||||
export let border: boolean = false
|
||||
|
||||
$: colorClasses = {
|
||||
light: 'text-tertiary bg-surface hover:bg-surface-hover text-tertiary',
|
||||
dark: 'text-tertiary hover:bg-surface-hover-dark text-tertiary'
|
||||
}[color ?? 'light']
|
||||
</script>
|
||||
|
||||
<div class="rounded-full p-1 {colorClasses} {border ? 'border border-tertiary' : ''}">
|
||||
<slot />
|
||||
</div>
|
||||
@@ -64,4 +64,69 @@ export namespace ButtonType {
|
||||
size: Size
|
||||
color: Color
|
||||
}
|
||||
|
||||
export const ColorVariants: Record<ButtonType.Color, Record<ButtonType.Variant, string>> = {
|
||||
none: {
|
||||
border: '',
|
||||
contained: '',
|
||||
divider: ''
|
||||
},
|
||||
|
||||
blue: {
|
||||
border:
|
||||
'border-frost-500 dark:border-frost-300 hover:border-frost-700 dark:hover:border-frost-400 focus-visible:border-frost-700 bg-surface hover:bg-frost-100 dark:hover:bg-frost-900 focus-visible:bg-frost-100 focus-visible:dark:text-frost-100 dark:focus-visible:bg-frost-900 text-frost-500 dark:text-frost-300 dark:hover:text-frost-400 hover:text-frost-700 focus-visible:text-frost-700 focus-visible:ring-frost-300',
|
||||
contained:
|
||||
'bg-frost-500 hover:bg-frost-700 focus-visible:bg-frost-700 text-white focus-visible:ring-frost-300 dark:bg-frost-500/90 dark:hover:bg-frost-600/90',
|
||||
divider: 'divide-x divide-frost-600'
|
||||
},
|
||||
marine: {
|
||||
border:
|
||||
'border-marine-300 dark:border-marine-200 hover:border-marine-500 dark:hover:border-marine-400 focus-visible:border-marine-500 bg-surface hover:bg-marine-500 dark:hover:bg-marine-400 focus-visible:bg-marine-100 focus-visible:dark:text-marine-50 dark:focus-visible:bg-marine-500 text-marine-50 dark:text-marine-50 dark:hover:text-marine-50 hover:text-marine-50 focus-visible:text-marine-300 focus-visible:ring-marine-200',
|
||||
contained:
|
||||
'bg-marine-300 hover:bg-marine-500 focus-visible:bg-marine-500 text-gray-50 focus-visible:ring-marine-200 dark:bg-marine-200 dark:hover:bg-marine-400',
|
||||
divider: 'divide-x divide-marine-500'
|
||||
},
|
||||
red: {
|
||||
border:
|
||||
'border-red-600/60 hover:border-red-600 bg-surface hover:bg-red-100 text-red-600 hover:text-red-700 focus-visible:ring-red-300 dark:border-red-400/90 dark:text-red-400 dark:hover:border-red-400 dark:hover:bg-red-500/60 dark:hover:text-red-100',
|
||||
contained:
|
||||
'bg-red-600 hover:bg-red-600 text-white focus-visible:ring-red-300 dark:border-red-400/70 dark:bg-red-700/90 dark:hover:bg-red-900 dark:hover:border-red-300 dark:text-primary',
|
||||
divider: 'divide-x divide-red-700'
|
||||
},
|
||||
green: {
|
||||
border:
|
||||
'border-green-600 hover:border-green-700 bg-surface hover:bg-green-100 text-green-600 hover:text-green-700 focus-visible:ring-green-300 dark:hover:bg-green-800 dark:border-green-400/90',
|
||||
contained:
|
||||
'bg-green-600 hover:bg-green-700 text-white focus-visible:ring-green-300 dark:bg-green-700/90 dark:hover:bg-green-800',
|
||||
divider: 'divide-x divide-green-700'
|
||||
},
|
||||
dark: {
|
||||
border:
|
||||
'border-marine-300 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-marine-200',
|
||||
contained:
|
||||
'bg-marine-400 hover:bg-marine-200 focus-visible:bg-surface-hover-inverse text-primary-inverse focus-visible:ring-surface-selected-inverse dark:bg-marine-50 dark:hover:bg-marine-50/70 dark:text-primary-inverse',
|
||||
divider: 'divide-x divide-gray-800 dark:divide-gray-200'
|
||||
},
|
||||
gray: {
|
||||
border:
|
||||
'border-gray-500 dark:border-gray-300 hover:border-gray-700 dark:hover:border-gray-400 focus-visible:border-gray-700 bg-surface hover:bg-gray-100 dark:hover:bg-gray-900 focus-visible:bg-gray-100 focus-visible:dark:text-gray-100 dark:focus-visible:bg-gray-900 text-gray-500 dark:text-gray-300 dark:hover:text-gray-400 hover:text-gray-700 focus-visible:text-gray-700 focus-visible:ring-gray-300',
|
||||
contained:
|
||||
'bg-gray-500 hover:bg-gray-700 focus-visible:bg-gray-700 text-white focus-visible:ring-gray-300',
|
||||
divider: 'divide-x divide-gray-600'
|
||||
},
|
||||
light: {
|
||||
border:
|
||||
'border 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',
|
||||
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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
export let horizontal: boolean = false
|
||||
export let gap: 'none' | 'sm' | 'md' | 'lg' = 'sm'
|
||||
export let justify: 'start' | 'center' | 'end' | 'between' = 'start'
|
||||
export let wFull = true
|
||||
export let hFull = true
|
||||
|
||||
const gapMap = {
|
||||
none: '',
|
||||
@@ -29,9 +31,13 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="flex flex-col h-full {wFull ? 'w-full' : ''} {gapMap[gap]} items-center {justifyMap[
|
||||
justify
|
||||
]}"
|
||||
class={twMerge(
|
||||
'flex flex-col w-full',
|
||||
hFull ? 'h-full' : '',
|
||||
gapMap[gap],
|
||||
'items-center',
|
||||
justifyMap[justify]
|
||||
)}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -25,23 +25,23 @@
|
||||
|
||||
<HighlightTheme />
|
||||
|
||||
<Tabs bind:selected>
|
||||
<Tab value="saved_inputs">Saved Inputs</Tab>
|
||||
{#if !isOperator}
|
||||
<Tab value="triggers">Triggers</Tab>
|
||||
{/if}
|
||||
{#if flow_json}
|
||||
<Tab value="raw">Export</Tab>
|
||||
{:else}
|
||||
<Tab value="script">Script</Tab>
|
||||
{/if}
|
||||
{#if selected == 'flow_step'}
|
||||
<Tab value="flow_step">Step</Tab>
|
||||
{/if}
|
||||
<div class="flex flex-col h-full">
|
||||
<Tabs bind:selected wrapperClass="flex-none w-full">
|
||||
<Tab value="saved_inputs">Saved Inputs</Tab>
|
||||
{#if !isOperator}
|
||||
<Tab value="triggers">Triggers</Tab>
|
||||
{/if}
|
||||
{#if flow_json}
|
||||
<Tab value="raw">Export</Tab>
|
||||
{:else}
|
||||
<Tab value="script">Script</Tab>
|
||||
{/if}
|
||||
{#if selected == 'flow_step'}
|
||||
<Tab value="flow_step">Step</Tab>
|
||||
{/if}
|
||||
|
||||
<svelte:fragment slot="content">
|
||||
<div class="h-[calc(100%-32px)]">
|
||||
<div class="h-full overflow-auto">
|
||||
<svelte:fragment slot="content">
|
||||
<div class="min-h-0 grow">
|
||||
<TabContent value="saved_inputs" class="h-full">
|
||||
<slot name="save_inputs" />
|
||||
</TabContent>
|
||||
@@ -69,6 +69,6 @@
|
||||
</TabContent>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
triggersCount,
|
||||
simplifiedPoll,
|
||||
defaultValues: writable(undefined),
|
||||
captureOn: writable(undefined)
|
||||
captureOn: writable(undefined),
|
||||
showCaptureHint: writable(undefined)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -40,79 +40,81 @@
|
||||
<HighlightTheme />
|
||||
|
||||
{#if !simplfiedPoll}
|
||||
<Tabs bind:selected={triggerSelected}>
|
||||
<Tab value="webhooks">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<Webhook size={12} />
|
||||
Webhooks
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="schedules">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<CalendarCheck2 size={12} />
|
||||
Schedules
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="routes">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<Route size={12} />
|
||||
HTTP
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="websockets">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<Unplug size={12} />
|
||||
Websockets
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="kafka" otherValues={['nats']}>
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<PlugZap size={12} />
|
||||
Event streams
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="emails">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<MailIcon size={12} />
|
||||
Email
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="cli">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<Terminal size={12} />
|
||||
CLI
|
||||
</span>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
<div class="flex flex-col h-full">
|
||||
<Tabs bind:selected={triggerSelected} wrapperClass="flex-none w-full">
|
||||
<Tab value="webhooks">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<Webhook size={12} />
|
||||
Webhooks
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="schedules">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<CalendarCheck2 size={12} />
|
||||
Schedules
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="routes">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<Route size={12} />
|
||||
HTTP
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="websockets">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<Unplug size={12} />
|
||||
Websockets
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="kafka" otherValues={['nats']}>
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<PlugZap size={12} />
|
||||
Event streams
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="emails">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<MailIcon size={12} />
|
||||
Email
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab value="cli">
|
||||
<span class="flex flex-row gap-2 items-center text-xs">
|
||||
<Terminal size={12} />
|
||||
CLI
|
||||
</span>
|
||||
</Tab>
|
||||
|
||||
<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 === 'websockets'}
|
||||
<slot name="websockets" />
|
||||
{:else if triggerSelected === 'kafka' || triggerSelected === 'nats'}
|
||||
<div class="m-1.5">
|
||||
<ToggleButtonGroup bind:selected={eventStreamType}>
|
||||
<ToggleButton value="kafka" label="Kafka" icon={KafkaIcon} />
|
||||
<ToggleButton value="nats" label="NATS" icon={NatsIcon} />
|
||||
</ToggleButtonGroup>
|
||||
<svelte:fragment slot="content">
|
||||
<div class="min-h-0 grow overflow-y-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 === 'websockets'}
|
||||
<slot name="websockets" />
|
||||
{:else if triggerSelected === 'kafka' || triggerSelected === 'nats'}
|
||||
<div class="m-1.5">
|
||||
<ToggleButtonGroup bind:selected={eventStreamType}>
|
||||
<ToggleButton value="kafka" label="Kafka" icon={KafkaIcon} />
|
||||
<ToggleButton value="nats" label="NATS" icon={NatsIcon} />
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
{#if eventStreamType === 'kafka'}
|
||||
<slot name="kafka" />
|
||||
{:else if eventStreamType === 'nats'}
|
||||
<slot name="nats" />
|
||||
{/if}
|
||||
{:else if triggerSelected === 'cli'}
|
||||
<slot name="cli" />
|
||||
{/if}
|
||||
</div>
|
||||
{#if eventStreamType === 'kafka'}
|
||||
<slot name="kafka" />
|
||||
{:else if eventStreamType === 'nats'}
|
||||
<slot name="nats" />
|
||||
{/if}
|
||||
{:else if triggerSelected === 'cli'}
|
||||
<slot name="cli" />
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
</div>
|
||||
{:else}
|
||||
<slot name="schedules" />
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
on:testWithArgs
|
||||
>
|
||||
<Label label="Email address">
|
||||
<ClipboardPanel content={captureEmail} disabled={!captureInfo.active} />
|
||||
|
||||
@@ -71,6 +71,8 @@
|
||||
{hasPreprocessor}
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
on:updateSchema
|
||||
on:testWithArgs
|
||||
{newItem}
|
||||
/>
|
||||
{:else}
|
||||
|
||||
@@ -67,12 +67,18 @@
|
||||
<Pane class="relative z-10" size={100 - size} minSize={40}>
|
||||
{#if loading}
|
||||
<div class="w-full h-full">
|
||||
<div class="block m-auto mt-40 w-10">
|
||||
<div class="block m-auto pt-40 w-10">
|
||||
<WindmillIcon height="40px" width="40px" spin="fast" />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<FlowEditorPanel {disabledFlowInputs} {newFlow} enableAi={!disableAi} on:applyArgs />
|
||||
<FlowEditorPanel
|
||||
{disabledFlowInputs}
|
||||
{newFlow}
|
||||
enableAi={!disableAi}
|
||||
on:applyArgs
|
||||
on:testWithArgs
|
||||
/>
|
||||
{/if}
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
|
||||
@@ -7,10 +7,16 @@
|
||||
export let noEditor: boolean
|
||||
</script>
|
||||
|
||||
{#if !noEditor}
|
||||
<FlowCardHeader on:reload {title} bind:flowModule>
|
||||
<slot name="header" />
|
||||
</FlowCardHeader>
|
||||
{/if}
|
||||
<div class="flex flex-col h-full">
|
||||
{#if !noEditor}
|
||||
<div>
|
||||
<FlowCardHeader on:reload {title} bind:flowModule>
|
||||
<slot name="header" />
|
||||
</FlowCardHeader>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<slot />
|
||||
<div class="min-h-0 flex-grow">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onMount } from 'svelte'
|
||||
|
||||
import type { FlowEditorContext } from '../types'
|
||||
import FlowModuleWrapper from './FlowModuleWrapper.svelte'
|
||||
import FlowSettings from './FlowSettings.svelte'
|
||||
@@ -20,10 +19,19 @@
|
||||
export let newFlow = false
|
||||
export let disabledFlowInputs = false
|
||||
|
||||
const { selectedId, flowStore, flowStateStore, flowInputsStore, pathStore, initialPath } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
const {
|
||||
selectedId,
|
||||
flowStore,
|
||||
flowStateStore,
|
||||
flowInputsStore,
|
||||
pathStore,
|
||||
initialPath,
|
||||
previewArgs,
|
||||
flowInputEditorState
|
||||
} = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
const { selectedTrigger, defaultValues, captureOn } = getContext<TriggerContext>('TriggerContext')
|
||||
const { selectedTrigger, defaultValues, captureOn, showCaptureHint } =
|
||||
getContext<TriggerContext>('TriggerContext')
|
||||
function checkDup(modules: FlowModule[]): string | undefined {
|
||||
let seenModules: string[] = []
|
||||
for (const m of modules) {
|
||||
@@ -71,15 +79,9 @@
|
||||
selectedTrigger.set(ev.detail.kind)
|
||||
defaultValues.set(ev.detail.config)
|
||||
captureOn.set(true)
|
||||
showCaptureHint.set(true)
|
||||
}}
|
||||
on:applyArgs
|
||||
on:updateSchema={(e) => {
|
||||
const { schema, redirect } = e.detail
|
||||
$flowStore.schema = schema
|
||||
if (redirect) {
|
||||
$selectedId = 'Input'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{:else if $selectedId === 'Result'}
|
||||
<p class="p-4 text-secondary">The result of the flow will be the result of the last node.</p>
|
||||
@@ -99,6 +101,18 @@
|
||||
})
|
||||
$selectedId = 'preprocessor'
|
||||
}}
|
||||
on:updateSchema={(e) => {
|
||||
const { payloadData, redirect } = e.detail
|
||||
if (payloadData) {
|
||||
$previewArgs = JSON.parse(JSON.stringify(payloadData))
|
||||
}
|
||||
if (redirect) {
|
||||
$selectedId = 'Input'
|
||||
$flowInputEditorState.selectedTab = 'captures'
|
||||
$flowInputEditorState.payloadData = payloadData
|
||||
}
|
||||
}}
|
||||
on:testWithArgs
|
||||
currentPath={$pathStore}
|
||||
{initialPath}
|
||||
schema={$flowStore.schema}
|
||||
|
||||
@@ -1,138 +1,517 @@
|
||||
<script lang="ts">
|
||||
import { Button, DrawerContent } from '$lib/components/common'
|
||||
import { Button } from '$lib/components/common'
|
||||
import { ButtonType } from '$lib/components/common/button/model'
|
||||
import { getContext } from 'svelte'
|
||||
import FlowCard from '../common/FlowCard.svelte'
|
||||
import { copyFirstStepSchema } from '../flowStore'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
|
||||
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
|
||||
import { convert } from '@redocly/json-to-json-schema'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import SavedInputs from '$lib/components/SavedInputs.svelte'
|
||||
import EditableSchemaForm from '$lib/components/EditableSchemaForm.svelte'
|
||||
import AddProperty from '$lib/components/schema/AddProperty.svelte'
|
||||
import AddPropertyV2 from '$lib/components/schema/AddPropertyV2.svelte'
|
||||
import FlowInputViewer from '$lib/components/FlowInputViewer.svelte'
|
||||
import HistoricInputs from '$lib/components/HistoricInputs.svelte'
|
||||
import SavedInputsPicker from '$lib/components/SavedInputsPicker.svelte'
|
||||
import FirstStepInputs from '$lib/components/FirstStepInputs.svelte'
|
||||
import {
|
||||
CornerDownLeft,
|
||||
Pen,
|
||||
ChevronRight,
|
||||
ChevronDown,
|
||||
Plus,
|
||||
History,
|
||||
Braces,
|
||||
Code,
|
||||
Save,
|
||||
X
|
||||
} from 'lucide-svelte'
|
||||
import CaptureIcon from '$lib/components/triggers/CaptureIcon.svelte'
|
||||
import FlowPreviewContent from '$lib/components/FlowPreviewContent.svelte'
|
||||
import FlowInputEditor from './FlowInputEditor.svelte'
|
||||
import CapturesInputs from '$lib/components/CapturesInputs.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import ButtonDropDown from '$lib/components/meltComponents/ButtonDropDown.svelte'
|
||||
import CaptureButton from '$lib/components/triggers/CaptureButton.svelte'
|
||||
|
||||
export let noEditor: boolean
|
||||
export let disabled: boolean
|
||||
|
||||
const { flowStore, flowStateStore, previewArgs, initialPath } =
|
||||
const { flowStore, previewArgs, pathStore, initialPath, flowInputEditorState } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
let inputLibraryDrawer: Drawer
|
||||
let jsonPayload: Drawer
|
||||
let pendingJson: string
|
||||
let addProperty: AddProperty | undefined = undefined
|
||||
let addProperty: AddPropertyV2 | undefined = undefined
|
||||
let previewSchema: Record<string, any> | undefined = undefined
|
||||
let payloadData: Record<string, any> | undefined = undefined
|
||||
let previewArguments: Record<string, any> | undefined = $previewArgs
|
||||
let editOptionsOpen = false
|
||||
let dropdownItems: Array<{
|
||||
label: string
|
||||
onClick: () => void
|
||||
disabled?: boolean
|
||||
}> = []
|
||||
|
||||
function importJson() {
|
||||
const parsed = JSON.parse(pendingJson)
|
||||
let editPanelSize = $flowInputEditorState?.editPanelSize ?? 0
|
||||
function updateEditPanelSize(size: number | undefined) {
|
||||
if (!$flowInputEditorState) return
|
||||
if (!size || size === 0) {
|
||||
$flowInputEditorState.editPanelSize = undefined
|
||||
return
|
||||
}
|
||||
$flowInputEditorState.editPanelSize = size
|
||||
}
|
||||
$: updateEditPanelSize(editPanelSize)
|
||||
|
||||
const getDropdownItems = () => {
|
||||
return [
|
||||
{
|
||||
label: 'Input editor',
|
||||
onClick: () => {
|
||||
handleEditSchema('inputEditor')
|
||||
},
|
||||
disabled:
|
||||
!$flowInputEditorState?.selectedTab ||
|
||||
$flowInputEditorState?.selectedTab === 'inputEditor',
|
||||
icon: Pen
|
||||
},
|
||||
{
|
||||
label: 'Trigger captures',
|
||||
onClick: () => {
|
||||
handleEditSchema('captures')
|
||||
},
|
||||
disabled: $flowInputEditorState?.selectedTab === 'captures',
|
||||
icon: CaptureIcon
|
||||
},
|
||||
{
|
||||
label: 'History',
|
||||
onClick: () => {
|
||||
handleEditSchema('history')
|
||||
},
|
||||
disabled: $flowInputEditorState?.selectedTab === 'history',
|
||||
icon: History
|
||||
},
|
||||
{
|
||||
label: 'Json payload',
|
||||
onClick: () => {
|
||||
handleEditSchema('json')
|
||||
},
|
||||
disabled: $flowInputEditorState?.selectedTab === 'json',
|
||||
icon: Braces
|
||||
},
|
||||
{
|
||||
label: "First step's inputs",
|
||||
onClick: () => {
|
||||
handleEditSchema('firstStepInputs')
|
||||
},
|
||||
icon: Code
|
||||
},
|
||||
{
|
||||
label: 'Saved inputs',
|
||||
onClick: () => {
|
||||
handleEditSchema('savedInputs')
|
||||
},
|
||||
disabled: $flowInputEditorState?.selectedTab === 'savedInputs',
|
||||
icon: Save
|
||||
}
|
||||
].filter((item) => !item.disabled)
|
||||
}
|
||||
|
||||
function handleEditSchema(editTab?: any) {
|
||||
if (!$flowInputEditorState) {
|
||||
return
|
||||
}
|
||||
if (editTab !== undefined) {
|
||||
$flowInputEditorState.selectedTab = editTab
|
||||
} else if ($flowInputEditorState.selectedTab === undefined) {
|
||||
$flowInputEditorState.selectedTab = 'inputEditor'
|
||||
} else {
|
||||
$flowInputEditorState.selectedTab = undefined
|
||||
}
|
||||
}
|
||||
|
||||
function schemaFromPayload(payload: any) {
|
||||
const parsed = JSON.parse(JSON.stringify(payload))
|
||||
|
||||
if (!parsed) {
|
||||
sendUserToast('Invalid JSON', true)
|
||||
sendUserToast('Invalid Schema', true)
|
||||
return
|
||||
}
|
||||
|
||||
$flowStore.schema = { required: [], properties: {}, ...convert(parsed) }
|
||||
if (Object.keys(parsed).length === 0) {
|
||||
return {
|
||||
required: [],
|
||||
properties: {},
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
order: []
|
||||
}
|
||||
}
|
||||
|
||||
jsonPayload.closeDrawer()
|
||||
return { required: [], properties: {}, ...convert(parsed) }
|
||||
}
|
||||
const yOffset = 191
|
||||
|
||||
let flowPreviewContent: FlowPreviewContent
|
||||
let previewOpen = false
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
||||
runPreview()
|
||||
}
|
||||
}
|
||||
|
||||
function runPreview() {
|
||||
if (previewArguments) {
|
||||
$previewArgs = previewArguments
|
||||
}
|
||||
previewOpen = true
|
||||
flowPreviewContent?.test()
|
||||
}
|
||||
|
||||
function updatePreviewSchemaAndArgs(payload: any) {
|
||||
payloadData = payload
|
||||
if (!payload) {
|
||||
updatePreviewArguments(undefined)
|
||||
previewSchema = undefined
|
||||
return
|
||||
}
|
||||
previewSchema = schemaFromPayload(payload)
|
||||
updatePreviewArguments(payload)
|
||||
}
|
||||
|
||||
function applySchemaAndArgs() {
|
||||
if (!previewSchema) {
|
||||
return
|
||||
}
|
||||
$flowStore.schema = previewSchema
|
||||
if (previewArguments) {
|
||||
$previewArgs = previewArguments
|
||||
}
|
||||
if ($flowInputEditorState) {
|
||||
$flowInputEditorState.selectedTab = undefined
|
||||
}
|
||||
}
|
||||
|
||||
function applySchema() {
|
||||
$flowStore.schema = previewSchema
|
||||
if ($flowInputEditorState) {
|
||||
$flowInputEditorState.selectedTab = undefined
|
||||
}
|
||||
}
|
||||
|
||||
function updatePreviewArguments(payloadData: Record<string, any> | undefined) {
|
||||
if (!payloadData) {
|
||||
previewArguments = $previewArgs
|
||||
return
|
||||
}
|
||||
previewArguments = payloadData
|
||||
}
|
||||
|
||||
let jsonValid = false
|
||||
function updatePayloadFromJson(jsonInput: string) {
|
||||
if (jsonInput === undefined || jsonInput === null || jsonInput.trim() === '') {
|
||||
updatePreviewSchemaAndArgs(undefined)
|
||||
jsonValid = false
|
||||
return
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(jsonInput)
|
||||
updatePreviewSchemaAndArgs(parsed)
|
||||
jsonValid = true
|
||||
} catch (error) {
|
||||
updatePreviewSchemaAndArgs(undefined)
|
||||
jsonValid = false
|
||||
}
|
||||
}
|
||||
|
||||
let tabButtonWidth = 0
|
||||
|
||||
const TAB_TITLES: Record<string, string> = {
|
||||
inputEditor: 'Input editor',
|
||||
captures: 'Captures',
|
||||
history: 'History',
|
||||
savedInputs: 'Saved inputs',
|
||||
json: 'JSON',
|
||||
firstStepInputs: 'First step',
|
||||
undefined: ''
|
||||
}
|
||||
|
||||
let connectFirstNode: () => void = () => {}
|
||||
|
||||
let init = false
|
||||
function initPayloadData() {
|
||||
if ($flowInputEditorState.payloadData && !init) {
|
||||
init = true
|
||||
updatePreviewSchemaAndArgs($flowInputEditorState.payloadData)
|
||||
$flowInputEditorState.payloadData = undefined
|
||||
}
|
||||
}
|
||||
$: $flowInputEditorState && ((dropdownItems = getDropdownItems()), initPayloadData())
|
||||
|
||||
let preventEnter = false
|
||||
</script>
|
||||
|
||||
<!-- Add svelte:window to listen for keyboard events -->
|
||||
<svelte:window on:keydown={handleKeydown} />
|
||||
|
||||
<Drawer bind:open={previewOpen} alwaysOpen size="75%">
|
||||
<FlowPreviewContent
|
||||
bind:this={flowPreviewContent}
|
||||
open={previewOpen}
|
||||
previewMode="whole"
|
||||
on:close={() => {
|
||||
previewOpen = false
|
||||
}}
|
||||
/>
|
||||
</Drawer>
|
||||
|
||||
<FlowCard {noEditor} title="Flow Input">
|
||||
{#if !disabled}
|
||||
<div class="flex flex-row items-center gap-2 px-4 py-2 border-b">
|
||||
<div class="text-sm">Copy input's schema from</div>
|
||||
<Button
|
||||
color="dark"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
jsonPayload.openDrawer()
|
||||
}}
|
||||
>
|
||||
A JSON
|
||||
</Button>
|
||||
<Button
|
||||
color="dark"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
inputLibraryDrawer.openDrawer()
|
||||
}}
|
||||
>
|
||||
Past runs/Input library
|
||||
</Button>
|
||||
<Button
|
||||
color="dark"
|
||||
size="xs"
|
||||
disabled={$flowStore.value.modules.length === 0 ||
|
||||
$flowStore.value.modules[0].value.type == 'identity'}
|
||||
on:click={() => copyFirstStepSchema($flowStateStore, flowStore)}
|
||||
>
|
||||
First step's inputs
|
||||
</Button>
|
||||
</div>
|
||||
<div class="p-4 border-b">
|
||||
<AddProperty
|
||||
<div class="py-2 px-4 h-full">
|
||||
<EditableSchemaForm
|
||||
bind:schema={$flowStore.schema}
|
||||
bind:this={addProperty}
|
||||
on:change={() => {
|
||||
$flowStore = $flowStore
|
||||
isFlowInput
|
||||
on:edit={(e) => {
|
||||
addProperty?.openDrawer(e.detail)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
on:delete={(e) => {
|
||||
addProperty?.handleDeleteArgument([e.detail])
|
||||
}}
|
||||
displayWebhookWarning
|
||||
editTab={$flowInputEditorState?.selectedTab}
|
||||
{previewSchema}
|
||||
bind:args={previewArguments}
|
||||
bind:editPanelSize
|
||||
editPanelInitialSize={$flowInputEditorState?.editPanelSize}
|
||||
pannelExtraButtonWidth={$flowInputEditorState?.editPanelSize ? tabButtonWidth : 0}
|
||||
>
|
||||
<svelte:fragment slot="openEditTab">
|
||||
<div
|
||||
class={twMerge(
|
||||
'flex flex-row divide-x rounded-md bg-surface overflow-hidden',
|
||||
!!$flowInputEditorState?.selectedTab ? 'rounded-r-none' : '',
|
||||
ButtonType.ColorVariants.blue.divider
|
||||
)}
|
||||
>
|
||||
<button
|
||||
on:click={() => {
|
||||
handleEditSchema()
|
||||
}}
|
||||
title={!!$flowInputEditorState?.selectedTab
|
||||
? 'Close input editor'
|
||||
: 'Open input editor'}
|
||||
class={ButtonType.ColorVariants.blue.contained}
|
||||
>
|
||||
<div class="p-2 center-center">
|
||||
<svelte:component
|
||||
this={!!$flowInputEditorState?.selectedTab ? ChevronRight : Pen}
|
||||
size={14}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<EditableSchemaForm
|
||||
bind:schema={$flowStore.schema}
|
||||
isFlowInput
|
||||
on:edit={(e) => {
|
||||
addProperty?.openDrawer(e.detail)
|
||||
}}
|
||||
on:delete={(e) => {
|
||||
addProperty?.handleDeleteArgument([e.detail])
|
||||
}}
|
||||
offset={yOffset}
|
||||
displayWebhookWarning
|
||||
/>
|
||||
<ButtonDropDown
|
||||
{dropdownItems}
|
||||
closeOnClick={true}
|
||||
bind:open={editOptionsOpen}
|
||||
placement="bottom-end"
|
||||
>
|
||||
<div
|
||||
class={twMerge(
|
||||
'p-2 center-center hover:bg-surface-hover',
|
||||
ButtonType.ColorVariants.blue.contained,
|
||||
'flex flex-row items-center rounded-br-md',
|
||||
'transition-all duration-150 ease-in-out overflow-hidden whitespace-nowrap',
|
||||
!!$flowInputEditorState?.selectedTab ? 'w-[122px] px-3' : 'w-[30px]'
|
||||
)}
|
||||
bind:clientWidth={tabButtonWidth}
|
||||
>
|
||||
<div class="flex flex-row items-center gap-1 justify-between w-full">
|
||||
{#if !!$flowInputEditorState?.selectedTab}
|
||||
<h2 class="text-xs">{TAB_TITLES[$flowInputEditorState?.selectedTab]}</h2>
|
||||
{/if}
|
||||
<ChevronDown size={14} />
|
||||
</div>
|
||||
</div>
|
||||
</ButtonDropDown>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="addProperty">
|
||||
{#if !!previewSchema}
|
||||
<div
|
||||
class={twMerge(
|
||||
'bg-blue-50 border-blue-200 border dark:bg-blue-900/40 dark:border-blue-700/40 text-xs p-2 w-full flex flex-row gap-2 items-center justify-left rounded-md',
|
||||
'text-blue-700 dark:text-blue-100',
|
||||
'relative'
|
||||
)}
|
||||
>
|
||||
<span> Preview only, update schema to save.</span>
|
||||
<div class="flex flex-row items-center gap-2 absolute right-2">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="light"
|
||||
size="xs2"
|
||||
startIcon={{ icon: X }}
|
||||
shortCut={{ key: 'esc', withoutModifier: true }}
|
||||
nonCaptureEvent
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<AddPropertyV2
|
||||
bind:schema={$flowStore.schema}
|
||||
bind:this={addProperty}
|
||||
on:change={() => {
|
||||
$flowStore = $flowStore
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="trigger">
|
||||
<div
|
||||
class="w-full py-2 flex justify-center items-center border border-dashed rounded-md hover:bg-surface-hover"
|
||||
>
|
||||
<Plus size={14} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</AddPropertyV2>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="extraTab">
|
||||
{#if $flowInputEditorState?.selectedTab === 'history'}
|
||||
<FlowInputEditor
|
||||
disabled={!payloadData}
|
||||
on:applySchemaAndArgs={applySchemaAndArgs}
|
||||
on:applySchema={applySchema}
|
||||
on:destroy={() => {
|
||||
updatePreviewSchemaAndArgs(undefined)
|
||||
}}
|
||||
>
|
||||
<HistoricInputs
|
||||
scriptHash={null}
|
||||
scriptPath={null}
|
||||
flowPath={$pathStore}
|
||||
on:select={(e) => {
|
||||
updatePreviewSchemaAndArgs(e.detail ?? undefined)
|
||||
}}
|
||||
/>
|
||||
</FlowInputEditor>
|
||||
{:else if $flowInputEditorState?.selectedTab === 'captures'}
|
||||
<FlowInputEditor
|
||||
disabled={!payloadData}
|
||||
on:applySchemaAndArgs={applySchemaAndArgs}
|
||||
on:applySchema={applySchema}
|
||||
on:destroy={() => {
|
||||
updatePreviewSchemaAndArgs(undefined)
|
||||
}}
|
||||
>
|
||||
<svelete:fragment slot="action">
|
||||
<div class="center-center">
|
||||
<CaptureButton on:openTriggers small={true} />
|
||||
</div>
|
||||
</svelete:fragment>
|
||||
<CapturesInputs
|
||||
on:select={(e) => {
|
||||
updatePreviewSchemaAndArgs(e.detail ?? undefined)
|
||||
}}
|
||||
flowPath={$pathStore}
|
||||
/>
|
||||
</FlowInputEditor>
|
||||
{:else if $flowInputEditorState?.selectedTab === 'savedInputs'}
|
||||
<FlowInputEditor
|
||||
{preventEnter}
|
||||
disabled={!payloadData}
|
||||
on:applySchemaAndArgs={applySchemaAndArgs}
|
||||
on:applySchema={applySchema}
|
||||
on:destroy={() => {
|
||||
updatePreviewSchemaAndArgs(undefined)
|
||||
}}
|
||||
>
|
||||
<SavedInputsPicker
|
||||
flowPath={initialPath}
|
||||
on:select={(e) => {
|
||||
updatePreviewSchemaAndArgs(e.detail ?? undefined)
|
||||
}}
|
||||
on:isEditing={(e) => {
|
||||
preventEnter = e.detail
|
||||
}}
|
||||
previewArgs={previewArguments}
|
||||
/>
|
||||
</FlowInputEditor>
|
||||
{:else if $flowInputEditorState?.selectedTab === 'json'}
|
||||
<FlowInputEditor
|
||||
{preventEnter}
|
||||
disabled={!jsonValid}
|
||||
on:applySchemaAndArgs={applySchemaAndArgs}
|
||||
on:applySchema={applySchema}
|
||||
on:destroy={() => {
|
||||
updatePreviewSchemaAndArgs(undefined)
|
||||
}}
|
||||
>
|
||||
<SimpleEditor
|
||||
on:focus={() => {
|
||||
preventEnter = true
|
||||
updatePayloadFromJson(pendingJson)
|
||||
}}
|
||||
on:blur={async () => {
|
||||
preventEnter = false
|
||||
setTimeout(() => {
|
||||
if (payloadData) {
|
||||
updatePayloadFromJson('')
|
||||
}
|
||||
}, 100)
|
||||
}}
|
||||
on:change={(e) => {
|
||||
updatePayloadFromJson(e.detail.code)
|
||||
}}
|
||||
bind:code={pendingJson}
|
||||
lang="json"
|
||||
class="h-full"
|
||||
placeholder={'Write a JSON payload. The input schema will be inferred.<br/><br/>Example:<br/><br/>{<br/> "foo": "12"<br/>}'}
|
||||
/>
|
||||
</FlowInputEditor>
|
||||
{:else if $flowInputEditorState?.selectedTab === 'firstStepInputs'}
|
||||
<FlowInputEditor
|
||||
disabled={!previewSchema}
|
||||
on:applySchemaAndArgs={() => {
|
||||
applySchemaAndArgs()
|
||||
connectFirstNode()
|
||||
}}
|
||||
on:applySchema={applySchema}
|
||||
on:destroy={() => {
|
||||
updatePreviewSchemaAndArgs(undefined)
|
||||
}}
|
||||
>
|
||||
<FirstStepInputs
|
||||
on:connectFirstNode={({ detail }) => {
|
||||
connectFirstNode = detail.connectFirstNode
|
||||
}}
|
||||
on:select={(e) => {
|
||||
previewSchema = e.detail ?? undefined
|
||||
}}
|
||||
/>
|
||||
</FlowInputEditor>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="runButton">
|
||||
<div class="w-full flex justify-end pr-5">
|
||||
<Button
|
||||
color="dark"
|
||||
btnClasses="w-fit"
|
||||
disabled={!!previewSchema}
|
||||
size="xs"
|
||||
shortCut={{ Icon: CornerDownLeft, hide: false }}
|
||||
on:click={() => {
|
||||
runPreview()
|
||||
}}
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</EditableSchemaForm>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="p-4 border-b">
|
||||
<FlowInputViewer schema={$flowStore.schema} />
|
||||
</div>
|
||||
{/if}
|
||||
</FlowCard>
|
||||
|
||||
<Drawer bind:this={jsonPayload} size="800px">
|
||||
<DrawerContent
|
||||
title="Input schema from JSON"
|
||||
on:close={() => {
|
||||
jsonPayload.closeDrawer()
|
||||
}}
|
||||
noPadding
|
||||
>
|
||||
<SimpleEditor bind:code={pendingJson} lang="json" class="h-full" />
|
||||
<svelte:fragment slot="actions">
|
||||
<Button size="sm" on:click={importJson}>Import</Button>
|
||||
</svelte:fragment>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<Drawer bind:this={inputLibraryDrawer}>
|
||||
<DrawerContent title="Input library {initialPath}" on:close={inputLibraryDrawer?.toggleDrawer}>
|
||||
<SavedInputs
|
||||
flowPath={initialPath}
|
||||
isValid={true}
|
||||
args={$previewArgs}
|
||||
canSaveInputs={false}
|
||||
on:selected_args={(e) => {
|
||||
const parsed = JSON.parse(JSON.stringify(e.detail))
|
||||
|
||||
if (!parsed) {
|
||||
sendUserToast('Invalid JSON', true)
|
||||
return
|
||||
}
|
||||
|
||||
$flowStore.schema = { required: [], properties: {}, ...convert(parsed) }
|
||||
inputLibraryDrawer?.closeDrawer()
|
||||
}}
|
||||
/>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { CornerDownLeft, Save } from 'lucide-svelte'
|
||||
|
||||
export let name: string = ''
|
||||
export let disabled = false
|
||||
export let preventEnter = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function applySchemaAndArgs() {
|
||||
dispatch('applySchemaAndArgs')
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
dispatch('destroy')
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Enter' && !preventEnter) {
|
||||
applySchemaAndArgs()
|
||||
e.preventDefault()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="h-full p-2">
|
||||
<Section label={name} class="h-full" small={true}>
|
||||
<svelte:fragment slot="header">
|
||||
<slot name="header" />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="action">
|
||||
<div class="flex flex-row gap-2 data-schema-picker">
|
||||
<slot name="action" />
|
||||
<Button
|
||||
size="xs2"
|
||||
color="dark"
|
||||
{disabled}
|
||||
shortCut={{ Icon: CornerDownLeft, hide: false, withoutModifier: true }}
|
||||
startIcon={{ icon: Save }}
|
||||
on:click={applySchemaAndArgs}>Update schema</Button
|
||||
>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<slot />
|
||||
</Section>
|
||||
</div>
|
||||
@@ -13,7 +13,7 @@
|
||||
import FlowModuleHeader from './FlowModuleHeader.svelte'
|
||||
import { getLatestHashForScript, scriptLangToEditorLang } from '$lib/scripts'
|
||||
import PropPickerWrapper from '../propPicker/PropPickerWrapper.svelte'
|
||||
import { afterUpdate, getContext, tick } from 'svelte'
|
||||
import { getContext, tick } from 'svelte'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
import FlowModuleScript from './FlowModuleScript.svelte'
|
||||
import FlowModuleEarlyStop from './FlowModuleEarlyStop.svelte'
|
||||
@@ -87,9 +87,6 @@
|
||||
let advancedSelected = 'retries'
|
||||
let advancedRuntimeSelected = 'concurrency'
|
||||
let s3Kind = 's3_client'
|
||||
let wrapper: HTMLDivElement
|
||||
let panes: HTMLElement
|
||||
let totalTopGap = 0
|
||||
let validCode = true
|
||||
let width = 1200
|
||||
|
||||
@@ -176,15 +173,6 @@
|
||||
advancedSelected = subtab
|
||||
}
|
||||
|
||||
afterUpdate(() => {
|
||||
totalTopGap = 0
|
||||
if (!(wrapper && panes)) return
|
||||
|
||||
const wrapperTop = wrapper.getBoundingClientRect().top
|
||||
const panesTop = panes.getBoundingClientRect().top
|
||||
totalTopGap = panesTop - wrapperTop
|
||||
})
|
||||
|
||||
let forceReload = 0
|
||||
let editorPanelSize = noEditor ? 0 : flowModule.value.type == 'script' ? 30 : 50
|
||||
let editorSettingsPanelSize = 100 - editorPanelSize
|
||||
@@ -222,7 +210,7 @@
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
|
||||
{#if flowModule.value}
|
||||
<div class="h-full" bind:this={wrapper} bind:clientWidth={width}>
|
||||
<div class="h-full" bind:clientWidth={width}>
|
||||
<FlowCard
|
||||
on:reload={() => {
|
||||
forceReload++
|
||||
@@ -276,400 +264,401 @@
|
||||
/>
|
||||
</svelte:fragment>
|
||||
|
||||
{#if flowModule.value.type === 'rawscript' && !noEditor}
|
||||
<div class="border-b-2 shadow-sm px-1">
|
||||
<EditorBar
|
||||
customUi={customUi?.editorBar}
|
||||
{validCode}
|
||||
{editor}
|
||||
{diffEditor}
|
||||
lang={flowModule.value['language'] ?? 'deno'}
|
||||
{websocketAlive}
|
||||
iconOnly={width < 950}
|
||||
kind={scriptKind}
|
||||
template={scriptTemplate}
|
||||
args={Object.entries(flowModule.value.input_transforms).reduce((acc, [key, obj]) => {
|
||||
acc[key] = obj.type === 'static' ? obj.value : undefined
|
||||
return acc
|
||||
}, {})}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="h-full flex flex-col">
|
||||
{#if flowModule.value.type === 'rawscript' && !noEditor}
|
||||
<div class="border-b-2 shadow-sm px-1">
|
||||
<EditorBar
|
||||
customUi={customUi?.editorBar}
|
||||
{validCode}
|
||||
{editor}
|
||||
{diffEditor}
|
||||
lang={flowModule.value['language'] ?? 'deno'}
|
||||
{websocketAlive}
|
||||
iconOnly={width < 950}
|
||||
kind={scriptKind}
|
||||
template={scriptTemplate}
|
||||
args={Object.entries(flowModule.value.input_transforms).reduce((acc, [key, obj]) => {
|
||||
acc[key] = obj.type === 'static' ? obj.value : undefined
|
||||
return acc
|
||||
}, {})}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
bind:this={panes}
|
||||
class="h-full"
|
||||
style="max-height: calc(100% - {totalTopGap}px) !important;"
|
||||
id="flow-editor-editor"
|
||||
>
|
||||
<Splitpanes horizontal>
|
||||
<Pane bind:size={editorPanelSize} minSize={20}>
|
||||
{#if flowModule.value.type === 'rawscript'}
|
||||
{#if !noEditor}
|
||||
{#key flowModule.id}
|
||||
<Editor
|
||||
folding
|
||||
path={$pathStore + '/' + flowModule.id}
|
||||
bind:websocketAlive
|
||||
bind:this={editor}
|
||||
class="h-full relative"
|
||||
bind:code={flowModule.value.content}
|
||||
lang={scriptLangToEditorLang(flowModule.value.language)}
|
||||
scriptLang={flowModule.value.language}
|
||||
automaticLayout={true}
|
||||
cmdEnterAction={async () => {
|
||||
selected = 'test'
|
||||
if ($selectedId == flowModule.id) {
|
||||
<div class="min-h-0 flex-grow" id="flow-editor-editor">
|
||||
<Splitpanes horizontal>
|
||||
<Pane bind:size={editorPanelSize} minSize={20}>
|
||||
{#if flowModule.value.type === 'rawscript'}
|
||||
{#if !noEditor}
|
||||
{#key flowModule.id}
|
||||
<Editor
|
||||
folding
|
||||
path={$pathStore + '/' + flowModule.id}
|
||||
bind:websocketAlive
|
||||
bind:this={editor}
|
||||
class="h-full relative"
|
||||
bind:code={flowModule.value.content}
|
||||
lang={scriptLangToEditorLang(flowModule.value.language)}
|
||||
scriptLang={flowModule.value.language}
|
||||
automaticLayout={true}
|
||||
cmdEnterAction={async () => {
|
||||
selected = 'test'
|
||||
if ($selectedId == flowModule.id) {
|
||||
if (flowModule.value.type === 'rawscript') {
|
||||
flowModule.value.content = editor.getCode()
|
||||
}
|
||||
await reload(flowModule)
|
||||
modulePreview?.runTestWithStepArgs()
|
||||
}
|
||||
}}
|
||||
on:change={async (event) => {
|
||||
if (flowModule.value.type === 'rawscript') {
|
||||
flowModule.value.content = editor.getCode()
|
||||
flowModule.value.content = event.detail
|
||||
}
|
||||
await reload(flowModule)
|
||||
modulePreview?.runTestWithStepArgs()
|
||||
}
|
||||
}}
|
||||
on:change={async (event) => {
|
||||
if (flowModule.value.type === 'rawscript') {
|
||||
flowModule.value.content = event.detail
|
||||
}
|
||||
await reload(flowModule)
|
||||
}}
|
||||
formatAction={() => {
|
||||
reload(flowModule)
|
||||
saveDraft()
|
||||
}}
|
||||
fixedOverflowWidgets={true}
|
||||
args={Object.entries(flowModule.value.input_transforms).reduce(
|
||||
(acc, [key, obj]) => {
|
||||
acc[key] = obj.type === 'static' ? obj.value : undefined
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)}
|
||||
/>
|
||||
<DiffEditor
|
||||
open={false}
|
||||
bind:this={diffEditor}
|
||||
automaticLayout
|
||||
fixedOverflowWidgets
|
||||
defaultLang={scriptLangToEditorLang(flowModule.value.language)}
|
||||
class="h-full"
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
{:else if flowModule.value.type === 'script'}
|
||||
{#if !noEditor}
|
||||
<div class="border-t">
|
||||
{#key forceReload}
|
||||
<FlowModuleScript
|
||||
bind:tag
|
||||
showAllCode={false}
|
||||
path={flowModule.value.path}
|
||||
hash={flowModule.value.hash}
|
||||
}}
|
||||
formatAction={() => {
|
||||
reload(flowModule)
|
||||
saveDraft()
|
||||
}}
|
||||
fixedOverflowWidgets={true}
|
||||
args={Object.entries(flowModule.value.input_transforms).reduce(
|
||||
(acc, [key, obj]) => {
|
||||
acc[key] = obj.type === 'static' ? obj.value : undefined
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)}
|
||||
/>
|
||||
<DiffEditor
|
||||
open={false}
|
||||
bind:this={diffEditor}
|
||||
automaticLayout
|
||||
fixedOverflowWidgets
|
||||
defaultLang={scriptLangToEditorLang(flowModule.value.language)}
|
||||
class="h-full"
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
{/if}
|
||||
{:else if flowModule.value.type === 'flow'}
|
||||
{#key forceReload}
|
||||
<FlowPathViewer path={flowModule.value.path} />
|
||||
{/key}
|
||||
{/if}
|
||||
</Pane>
|
||||
<Pane bind:size={editorSettingsPanelSize} minSize={20}>
|
||||
<Tabs bind:selected>
|
||||
{#if !preprocessorModule}
|
||||
<Tab value="inputs">Step Input</Tab>
|
||||
{/if}
|
||||
<Tab value="test">Test this step</Tab>
|
||||
{#if !preprocessorModule}
|
||||
<Tab value="advanced">Advanced</Tab>
|
||||
{/if}
|
||||
</Tabs>
|
||||
<div
|
||||
class={advancedSelected === 'runtime' ? 'h-[calc(100%-68px)]' : 'h-[calc(100%-34px)]'}
|
||||
>
|
||||
{#if selected === 'inputs' && (flowModule.value.type == 'rawscript' || flowModule.value.type == 'script' || flowModule.value.type == 'flow')}
|
||||
<div class="h-full overflow-auto bg-surface" id="flow-editor-step-input">
|
||||
<PropPickerWrapper
|
||||
pickableProperties={stepPropPicker.pickableProperties}
|
||||
error={failureModule}
|
||||
noPadding
|
||||
>
|
||||
<InputTransformSchemaForm
|
||||
class="px-1 xl:px-2"
|
||||
bind:this={inputTransformSchemaForm}
|
||||
pickableProperties={stepPropPicker.pickableProperties}
|
||||
schema={$flowStateStore[$selectedId]?.schema ?? {}}
|
||||
previousModuleId={previousModule?.id}
|
||||
bind:args={flowModule.value.input_transforms}
|
||||
extraLib={stepPropPicker.extraLib}
|
||||
{enableAi}
|
||||
on:changeArg={(e) => {
|
||||
const { argName } = e.detail
|
||||
setFlowInput(argName)
|
||||
}}
|
||||
/>
|
||||
</PropPickerWrapper>
|
||||
</div>
|
||||
{:else if selected === 'test'}
|
||||
<ModulePreview
|
||||
pickableProperties={stepPropPicker.pickableProperties}
|
||||
bind:this={modulePreview}
|
||||
mod={flowModule}
|
||||
{editor}
|
||||
{diffEditor}
|
||||
{noEditor}
|
||||
lang={flowModule.value['language'] ?? 'deno'}
|
||||
schema={$flowStateStore[$selectedId]?.schema ?? {}}
|
||||
/>
|
||||
{:else if selected === 'advanced'}
|
||||
<Tabs bind:selected={advancedSelected}>
|
||||
<Tab value="retries" active={flowModule.retry !== undefined}>Retries</Tab>
|
||||
{#if !$selectedId.includes('failure')}
|
||||
<Tab value="runtime">Runtime</Tab>
|
||||
<Tab value="cache" active={Boolean(flowModule.cache_ttl)}>Cache</Tab>
|
||||
<Tab
|
||||
value="early-stop"
|
||||
active={Boolean(
|
||||
flowModule.stop_after_if || flowModule.stop_after_all_iters_if
|
||||
)}
|
||||
>
|
||||
Early Stop
|
||||
</Tab>
|
||||
<Tab value="skip" active={Boolean(flowModule.skip_if)}>Skip</Tab>
|
||||
<Tab value="suspend" active={Boolean(flowModule.suspend)}>Suspend</Tab>
|
||||
<Tab value="sleep" active={Boolean(flowModule.sleep)}>Sleep</Tab>
|
||||
<Tab value="mock" active={Boolean(flowModule.mock?.enabled)}>Mock</Tab>
|
||||
<Tab value="same_worker">Shared Directory</Tab>
|
||||
{#if flowModule.value['language'] === 'python3' || flowModule.value['language'] === 'deno'}
|
||||
<Tab value="s3">S3</Tab>
|
||||
{/if}
|
||||
{/if}
|
||||
</Tabs>
|
||||
{#if advancedSelected === 'runtime'}
|
||||
<Tabs bind:selected={advancedRuntimeSelected}>
|
||||
<Tab value="concurrency">Concurrency</Tab>
|
||||
<Tab value="timeout">Timeout</Tab>
|
||||
<Tab value="priority">Priority</Tab>
|
||||
<Tab value="lifetime">Lifetime</Tab>
|
||||
</Tabs>
|
||||
{/if}
|
||||
<div class="h-[calc(100%-32px)] overflow-auto p-4">
|
||||
{#if advancedSelected === 'retries'}
|
||||
<Section label="Retries">
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip documentationLink="https://www.windmill.dev/docs/flows/retries">
|
||||
If defined, upon error this step will be retried with a delay and a
|
||||
maximum number of attempts as defined below.
|
||||
</Tooltip>
|
||||
</svelte:fragment>
|
||||
<span class="text-2xs">After all retries attempts have been exhausted:</span>
|
||||
<div class="flex gap-2 mb-4">
|
||||
<Toggle
|
||||
size="xs"
|
||||
bind:checked={flowModule.continue_on_error}
|
||||
options={{
|
||||
left: 'Stop on error and propagate error up',
|
||||
right: "Continue on error with error as step's return"
|
||||
}}
|
||||
/>
|
||||
<Tooltip>
|
||||
When enabled, the flow will continue to the next step after going through
|
||||
all the retries (if any) even if this step fails. This enables to process
|
||||
the error in a branch one for instance.
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="my-8" />
|
||||
<FlowRetries bind:flowModuleRetry={flowModule.retry} />
|
||||
</Section>
|
||||
{:else if advancedSelected === 'runtime' && advancedRuntimeSelected === 'concurrency'}
|
||||
<Section label="Concurrency limits" class="flex flex-col gap-4" eeOnly>
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip>Allowed concurrency within a given timeframe</Tooltip>
|
||||
</svelte:fragment>
|
||||
{#if flowModule.value.type == 'rawscript'}
|
||||
<Label label="Max number of executions within the time window">
|
||||
<div class="flex flex-row gap-2 max-w-sm">
|
||||
<input
|
||||
disabled={!$enterpriseLicense}
|
||||
bind:value={flowModule.value.concurrent_limit}
|
||||
type="number"
|
||||
/>
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
variant="border"
|
||||
on:click={() => {
|
||||
if (flowModule.value.type == 'rawscript') {
|
||||
flowModule.value.concurrent_limit = undefined
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-row gap-2"> Remove Limits </div>
|
||||
</Button>
|
||||
</div>
|
||||
</Label>
|
||||
<Label label="Time window in seconds">
|
||||
<SecondsInput
|
||||
disabled={!$enterpriseLicense}
|
||||
bind:seconds={flowModule.value.concurrency_time_window_s}
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Custom concurrency key (optional)">
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip>
|
||||
Concurrency keys are global, you can have them be workspace specific
|
||||
using the variable `$workspace`. You can also use an argument's value
|
||||
using `$args[name_of_arg]`</Tooltip
|
||||
>
|
||||
</svelte:fragment>
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
type="text"
|
||||
autofocus
|
||||
disabled={!$enterpriseLicense}
|
||||
bind:value={flowModule.value.custom_concurrency_key}
|
||||
placeholder={`$workspace/script/${$pathStore}-$args[foo]`}
|
||||
/>
|
||||
</Label>
|
||||
{:else}
|
||||
<Alert type="warning" title="Limitation" size="xs">
|
||||
The concurrency limit of a workspace script is only settable in the script
|
||||
metadata itself. For hub scripts, this feature is non available yet.
|
||||
</Alert>
|
||||
{/if}
|
||||
</Section>
|
||||
{:else if advancedSelected === 'runtime' && advancedRuntimeSelected === 'timeout'}
|
||||
<div>
|
||||
<FlowModuleTimeout bind:flowModule />
|
||||
</div>
|
||||
{:else if advancedSelected === 'runtime' && advancedRuntimeSelected === 'priority'}
|
||||
<Section label="Priority" class="flex flex-col gap-4">
|
||||
<!-- TODO: Add EE-only badge when we have it -->
|
||||
<Toggle
|
||||
disabled={!$enterpriseLicense || isCloudHosted()}
|
||||
checked={flowModule.priority !== undefined && flowModule.priority > 0}
|
||||
on:change={() => {
|
||||
if (flowModule.priority) {
|
||||
flowModule.priority = undefined
|
||||
} else {
|
||||
flowModule.priority = 100
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: 'Enabled high priority flow step',
|
||||
rightTooltip: `Jobs scheduled from this step when the flow is executed are labeled as high priority and take precedence over the other jobs in the jobs queue. ${
|
||||
!$enterpriseLicense
|
||||
? 'This is a feature only available on enterprise edition.'
|
||||
: ''
|
||||
}`
|
||||
{:else if flowModule.value.type === 'script'}
|
||||
{#if !noEditor}
|
||||
<div class="border-t">
|
||||
{#key forceReload}
|
||||
<FlowModuleScript
|
||||
bind:tag
|
||||
showAllCode={false}
|
||||
path={flowModule.value.path}
|
||||
hash={flowModule.value.hash}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
{/if}
|
||||
{:else if flowModule.value.type === 'flow'}
|
||||
{#key forceReload}
|
||||
<FlowPathViewer path={flowModule.value.path} />
|
||||
{/key}
|
||||
{/if}
|
||||
</Pane>
|
||||
<Pane bind:size={editorSettingsPanelSize} minSize={20}>
|
||||
<Tabs bind:selected>
|
||||
{#if !preprocessorModule}
|
||||
<Tab value="inputs">Step Input</Tab>
|
||||
{/if}
|
||||
<Tab value="test">Test this step</Tab>
|
||||
{#if !preprocessorModule}
|
||||
<Tab value="advanced">Advanced</Tab>
|
||||
{/if}
|
||||
</Tabs>
|
||||
<div
|
||||
class={advancedSelected === 'runtime'
|
||||
? 'h-[calc(100%-68px)]'
|
||||
: 'h-[calc(100%-34px)]'}
|
||||
>
|
||||
{#if selected === 'inputs' && (flowModule.value.type == 'rawscript' || flowModule.value.type == 'script' || flowModule.value.type == 'flow')}
|
||||
<div class="h-full overflow-auto bg-surface" id="flow-editor-step-input">
|
||||
<PropPickerWrapper
|
||||
pickableProperties={stepPropPicker.pickableProperties}
|
||||
error={failureModule}
|
||||
noPadding
|
||||
>
|
||||
<InputTransformSchemaForm
|
||||
class="px-1 xl:px-2"
|
||||
bind:this={inputTransformSchemaForm}
|
||||
pickableProperties={stepPropPicker.pickableProperties}
|
||||
schema={$flowStateStore[$selectedId]?.schema ?? {}}
|
||||
previousModuleId={previousModule?.id}
|
||||
bind:args={flowModule.value.input_transforms}
|
||||
extraLib={stepPropPicker.extraLib}
|
||||
{enableAi}
|
||||
on:changeArg={(e) => {
|
||||
const { argName } = e.detail
|
||||
setFlowInput(argName)
|
||||
}}
|
||||
/>
|
||||
<Label label="Priority number">
|
||||
</PropPickerWrapper>
|
||||
</div>
|
||||
{:else if selected === 'test'}
|
||||
<ModulePreview
|
||||
pickableProperties={stepPropPicker.pickableProperties}
|
||||
bind:this={modulePreview}
|
||||
mod={flowModule}
|
||||
{editor}
|
||||
{diffEditor}
|
||||
{noEditor}
|
||||
lang={flowModule.value['language'] ?? 'deno'}
|
||||
schema={$flowStateStore[$selectedId]?.schema ?? {}}
|
||||
/>
|
||||
{:else if selected === 'advanced'}
|
||||
<Tabs bind:selected={advancedSelected}>
|
||||
<Tab value="retries" active={flowModule.retry !== undefined}>Retries</Tab>
|
||||
{#if !$selectedId.includes('failure')}
|
||||
<Tab value="runtime">Runtime</Tab>
|
||||
<Tab value="cache" active={Boolean(flowModule.cache_ttl)}>Cache</Tab>
|
||||
<Tab
|
||||
value="early-stop"
|
||||
active={Boolean(
|
||||
flowModule.stop_after_if || flowModule.stop_after_all_iters_if
|
||||
)}
|
||||
>
|
||||
Early Stop
|
||||
</Tab>
|
||||
<Tab value="skip" active={Boolean(flowModule.skip_if)}>Skip</Tab>
|
||||
<Tab value="suspend" active={Boolean(flowModule.suspend)}>Suspend</Tab>
|
||||
<Tab value="sleep" active={Boolean(flowModule.sleep)}>Sleep</Tab>
|
||||
<Tab value="mock" active={Boolean(flowModule.mock?.enabled)}>Mock</Tab>
|
||||
<Tab value="same_worker">Shared Directory</Tab>
|
||||
{#if flowModule.value['language'] === 'python3' || flowModule.value['language'] === 'deno'}
|
||||
<Tab value="s3">S3</Tab>
|
||||
{/if}
|
||||
{/if}
|
||||
</Tabs>
|
||||
{#if advancedSelected === 'runtime'}
|
||||
<Tabs bind:selected={advancedRuntimeSelected}>
|
||||
<Tab value="concurrency">Concurrency</Tab>
|
||||
<Tab value="timeout">Timeout</Tab>
|
||||
<Tab value="priority">Priority</Tab>
|
||||
<Tab value="lifetime">Lifetime</Tab>
|
||||
</Tabs>
|
||||
{/if}
|
||||
<div class="h-[calc(100%-32px)] overflow-auto p-4">
|
||||
{#if advancedSelected === 'retries'}
|
||||
<Section label="Retries">
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip>The higher the number, the higher the priority.</Tooltip>
|
||||
<Tooltip documentationLink="https://www.windmill.dev/docs/flows/retries">
|
||||
If defined, upon error this step will be retried with a delay and a
|
||||
maximum number of attempts as defined below.
|
||||
</Tooltip>
|
||||
</svelte:fragment>
|
||||
<input
|
||||
type="number"
|
||||
class="!w-24"
|
||||
disabled={flowModule.priority === undefined}
|
||||
bind:value={flowModule.priority}
|
||||
on:focus
|
||||
<span class="text-2xs">After all retries attempts have been exhausted:</span
|
||||
>
|
||||
<div class="flex gap-2 mb-4">
|
||||
<Toggle
|
||||
size="xs"
|
||||
bind:checked={flowModule.continue_on_error}
|
||||
options={{
|
||||
left: 'Stop on error and propagate error up',
|
||||
right: "Continue on error with error as step's return"
|
||||
}}
|
||||
/>
|
||||
<Tooltip>
|
||||
When enabled, the flow will continue to the next step after going
|
||||
through all the retries (if any) even if this step fails. This enables
|
||||
to process the error in a branch one for instance.
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="my-8" />
|
||||
<FlowRetries bind:flowModuleRetry={flowModule.retry} />
|
||||
</Section>
|
||||
{:else if advancedSelected === 'runtime' && advancedRuntimeSelected === 'concurrency'}
|
||||
<Section label="Concurrency limits" class="flex flex-col gap-4" eeOnly>
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip>Allowed concurrency within a given timeframe</Tooltip>
|
||||
</svelte:fragment>
|
||||
{#if flowModule.value.type == 'rawscript'}
|
||||
<Label label="Max number of executions within the time window">
|
||||
<div class="flex flex-row gap-2 max-w-sm">
|
||||
<input
|
||||
disabled={!$enterpriseLicense}
|
||||
bind:value={flowModule.value.concurrent_limit}
|
||||
type="number"
|
||||
/>
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
variant="border"
|
||||
on:click={() => {
|
||||
if (flowModule.value.type == 'rawscript') {
|
||||
flowModule.value.concurrent_limit = undefined
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-row gap-2"> Remove Limits </div>
|
||||
</Button>
|
||||
</div>
|
||||
</Label>
|
||||
<Label label="Time window in seconds">
|
||||
<SecondsInput
|
||||
disabled={!$enterpriseLicense}
|
||||
bind:seconds={flowModule.value.concurrency_time_window_s}
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Custom concurrency key (optional)">
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip>
|
||||
Concurrency keys are global, you can have them be workspace specific
|
||||
using the variable `$workspace`. You can also use an argument's
|
||||
value using `$args[name_of_arg]`</Tooltip
|
||||
>
|
||||
</svelte:fragment>
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
type="text"
|
||||
autofocus
|
||||
disabled={!$enterpriseLicense}
|
||||
bind:value={flowModule.value.custom_concurrency_key}
|
||||
placeholder={`$workspace/script/${$pathStore}-$args[foo]`}
|
||||
/>
|
||||
</Label>
|
||||
{:else}
|
||||
<Alert type="warning" title="Limitation" size="xs">
|
||||
The concurrency limit of a workspace script is only settable in the
|
||||
script metadata itself. For hub scripts, this feature is non available
|
||||
yet.
|
||||
</Alert>
|
||||
{/if}
|
||||
</Section>
|
||||
{:else if advancedSelected === 'runtime' && advancedRuntimeSelected === 'timeout'}
|
||||
<div>
|
||||
<FlowModuleTimeout bind:flowModule />
|
||||
</div>
|
||||
{:else if advancedSelected === 'runtime' && advancedRuntimeSelected === 'priority'}
|
||||
<Section label="Priority" class="flex flex-col gap-4">
|
||||
<!-- TODO: Add EE-only badge when we have it -->
|
||||
<Toggle
|
||||
disabled={!$enterpriseLicense || isCloudHosted()}
|
||||
checked={flowModule.priority !== undefined && flowModule.priority > 0}
|
||||
on:change={() => {
|
||||
if (flowModule.priority && flowModule.priority > 100) {
|
||||
if (flowModule.priority) {
|
||||
flowModule.priority = undefined
|
||||
} else {
|
||||
flowModule.priority = 100
|
||||
} else if (flowModule.priority && flowModule.priority < 0) {
|
||||
flowModule.priority = 0
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
right: 'Enabled high priority flow step',
|
||||
rightTooltip: `Jobs scheduled from this step when the flow is executed are labeled as high priority and take precedence over the other jobs in the jobs queue. ${
|
||||
!$enterpriseLicense
|
||||
? 'This is a feature only available on enterprise edition.'
|
||||
: ''
|
||||
}`
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
<Label label="Priority number">
|
||||
<svelte:fragment slot="header">
|
||||
<Tooltip>The higher the number, the higher the priority.</Tooltip>
|
||||
</svelte:fragment>
|
||||
<input
|
||||
type="number"
|
||||
class="!w-24"
|
||||
disabled={flowModule.priority === undefined}
|
||||
bind:value={flowModule.priority}
|
||||
on:focus
|
||||
on:change={() => {
|
||||
if (flowModule.priority && flowModule.priority > 100) {
|
||||
flowModule.priority = 100
|
||||
} else if (flowModule.priority && flowModule.priority < 0) {
|
||||
flowModule.priority = 0
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Alert type="warning" title="Limitation" size="xs">
|
||||
Setting priority is only available for enterprise edition and not available
|
||||
on the cloud.
|
||||
</Alert>
|
||||
</Section>
|
||||
{:else if advancedSelected === 'runtime' && advancedRuntimeSelected === 'lifetime'}
|
||||
<div>
|
||||
<FlowModuleDeleteAfterUse bind:flowModule disabled={!$enterpriseLicense} />
|
||||
</div>
|
||||
{:else if advancedSelected === 'cache'}
|
||||
<div>
|
||||
<FlowModuleCache bind:flowModule />
|
||||
</div>
|
||||
{:else if advancedSelected === 'early-stop'}
|
||||
<FlowModuleEarlyStop bind:flowModule />
|
||||
{:else if advancedSelected === 'skip'}
|
||||
<FlowModuleSkip bind:flowModule {parentModule} {previousModule} />
|
||||
{:else if advancedSelected === 'suspend'}
|
||||
<div>
|
||||
<FlowModuleSuspend previousModuleId={previousModule?.id} bind:flowModule />
|
||||
</div>
|
||||
{:else if advancedSelected === 'sleep'}
|
||||
<div>
|
||||
<FlowModuleSleep previousModuleId={previousModule?.id} bind:flowModule />
|
||||
</div>
|
||||
{:else if advancedSelected === 'mock'}
|
||||
<div>
|
||||
<FlowModuleMock bind:flowModule />
|
||||
</div>
|
||||
{:else if advancedSelected === 'same_worker'}
|
||||
<div>
|
||||
<Alert type="info" title="Share a directory between steps">
|
||||
If shared directory is set, will share a folder that will be mounted on
|
||||
`./shared` for each of them to pass data between each other.
|
||||
</Alert>
|
||||
<Button
|
||||
btnClasses="mt-4"
|
||||
on:click={() => {
|
||||
$selectedId = 'settings-same-worker'
|
||||
}}
|
||||
>
|
||||
Set shared directory in the flow settings
|
||||
</Button>
|
||||
</div>
|
||||
{:else if advancedSelected === 's3'}
|
||||
<div>
|
||||
<h2 class="pb-4">
|
||||
S3 snippets
|
||||
<Tooltip>
|
||||
Read/Write object from/to S3 and leverage Polars and DuckDB to run
|
||||
efficient ETL processes.
|
||||
</Tooltip>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="flex gap-2 justify-between mb-4 items-center">
|
||||
<div class="flex gap-2">
|
||||
<ToggleButtonGroup bind:selected={s3Kind} class="w-auto">
|
||||
{#if flowModule.value['language'] === 'deno'}
|
||||
<ToggleButton value="s3_client" size="sm" label="S3 lite client" />
|
||||
{:else}
|
||||
<ToggleButton value="s3_client" size="sm" label="Boto3" />
|
||||
<ToggleButton value="polars" size="sm" label="Polars" />
|
||||
<ToggleButton value="duckdb" size="sm" label="DuckDB" />
|
||||
{/if}
|
||||
</ToggleButtonGroup>
|
||||
<Alert type="warning" title="Limitation" size="xs">
|
||||
Setting priority is only available for enterprise edition and not
|
||||
available on the cloud.
|
||||
</Alert>
|
||||
</Section>
|
||||
{:else if advancedSelected === 'runtime' && advancedRuntimeSelected === 'lifetime'}
|
||||
<div>
|
||||
<FlowModuleDeleteAfterUse bind:flowModule disabled={!$enterpriseLicense} />
|
||||
</div>
|
||||
{:else if advancedSelected === 'cache'}
|
||||
<div>
|
||||
<FlowModuleCache bind:flowModule />
|
||||
</div>
|
||||
{:else if advancedSelected === 'early-stop'}
|
||||
<FlowModuleEarlyStop bind:flowModule />
|
||||
{:else if advancedSelected === 'skip'}
|
||||
<FlowModuleSkip bind:flowModule {parentModule} {previousModule} />
|
||||
{:else if advancedSelected === 'suspend'}
|
||||
<div>
|
||||
<FlowModuleSuspend previousModuleId={previousModule?.id} bind:flowModule />
|
||||
</div>
|
||||
{:else if advancedSelected === 'sleep'}
|
||||
<div>
|
||||
<FlowModuleSleep previousModuleId={previousModule?.id} bind:flowModule />
|
||||
</div>
|
||||
{:else if advancedSelected === 'mock'}
|
||||
<div>
|
||||
<FlowModuleMock bind:flowModule />
|
||||
</div>
|
||||
{:else if advancedSelected === 'same_worker'}
|
||||
<div>
|
||||
<Alert type="info" title="Share a directory between steps">
|
||||
If shared directory is set, will share a folder that will be mounted on
|
||||
`./shared` for each of them to pass data between each other.
|
||||
</Alert>
|
||||
<Button
|
||||
btnClasses="mt-4"
|
||||
on:click={() => {
|
||||
$selectedId = 'settings-same-worker'
|
||||
}}
|
||||
>
|
||||
Set shared directory in the flow settings
|
||||
</Button>
|
||||
</div>
|
||||
{:else if advancedSelected === 's3'}
|
||||
<div>
|
||||
<h2 class="pb-4">
|
||||
S3 snippets
|
||||
<Tooltip>
|
||||
Read/Write object from/to S3 and leverage Polars and DuckDB to run
|
||||
efficient ETL processes.
|
||||
</Tooltip>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="flex gap-2 justify-between mb-4 items-center">
|
||||
<div class="flex gap-2">
|
||||
<ToggleButtonGroup bind:selected={s3Kind} class="w-auto">
|
||||
{#if flowModule.value['language'] === 'deno'}
|
||||
<ToggleButton value="s3_client" size="sm" label="S3 lite client" />
|
||||
{:else}
|
||||
<ToggleButton value="s3_client" size="sm" label="Boto3" />
|
||||
<ToggleButton value="polars" size="sm" label="Polars" />
|
||||
<ToggleButton value="duckdb" size="sm" label="DuckDB" />
|
||||
{/if}
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="xs"
|
||||
on:click={() =>
|
||||
editor.setCode(s3Scripts[flowModule.value['language']][s3Kind])}
|
||||
>
|
||||
Apply snippet
|
||||
</Button>
|
||||
</div>
|
||||
<HighlightCode
|
||||
language={flowModule.value['language']}
|
||||
code={s3Scripts[flowModule.value['language']][s3Kind]}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
<Button
|
||||
size="xs"
|
||||
on:click={() =>
|
||||
editor.setCode(s3Scripts[flowModule.value['language']][s3Kind])}
|
||||
>
|
||||
Apply snippet
|
||||
</Button>
|
||||
</div>
|
||||
<HighlightCode
|
||||
language={flowModule.value['language']}
|
||||
code={s3Scripts[flowModule.value['language']][s3Kind]}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</div>
|
||||
</div>
|
||||
</FlowCard>
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
triggersCount: triggersCount,
|
||||
simplifiedPoll: writable(false),
|
||||
defaultValues: writable(undefined),
|
||||
captureOn: writable(undefined)
|
||||
captureOn: writable(undefined),
|
||||
showCaptureHint: writable(undefined)
|
||||
})
|
||||
|
||||
async function loadFlow(path: string) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { Flow, OpenFlow } from '$lib/gen'
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import { initFlowState, type FlowState } from './flowState'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { get } from 'svelte/store'
|
||||
|
||||
export type FlowMode = 'push' | 'pull'
|
||||
|
||||
@@ -29,12 +31,57 @@ export async function copyFirstStepSchema(flowState: FlowState, flowStore: Writa
|
||||
expr: `flow_input.${key}`
|
||||
}
|
||||
})
|
||||
return flow
|
||||
}
|
||||
sendUserToast('Only scripts can be used as a input schema', true)
|
||||
return flow
|
||||
}
|
||||
sendUserToast('No first step found', true)
|
||||
return flow
|
||||
})
|
||||
}
|
||||
|
||||
export async function getFirstStepSchema(flowState: FlowState, flowStore: Writable<OpenFlow>) {
|
||||
const flow = get(flowStore)
|
||||
const firstModuleId = flow.value.modules[0]?.id
|
||||
|
||||
if (!firstModuleId || !flowState[firstModuleId]) {
|
||||
throw new Error('no first step found')
|
||||
}
|
||||
|
||||
const schema = structuredClone(flowState[firstModuleId].schema)
|
||||
const v = flow.value.modules[0].value
|
||||
|
||||
if (v.type !== 'rawscript' && v.type !== 'script') {
|
||||
throw new Error('only scripts can be used as a input schema')
|
||||
}
|
||||
|
||||
const simplifiedModule = {
|
||||
id: flow.value.modules[0].id,
|
||||
summary: flow.value.modules[0].summary,
|
||||
value: {
|
||||
type: flow.value.modules[0].value.type,
|
||||
...('path' in flow.value.modules[0].value ? { path: flow.value.modules[0].value.path } : {}),
|
||||
...('language' in flow.value.modules[0].value
|
||||
? { language: flow.value.modules[0].value.language }
|
||||
: {})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
schema,
|
||||
mod: simplifiedModule,
|
||||
connectFirstNode: () => {
|
||||
Object.keys(v.input_transforms ?? {}).forEach((key) => {
|
||||
v.input_transforms[key] = {
|
||||
type: 'javascript',
|
||||
expr: `flow_input.${key}`
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function replaceId(expr: string, id: string, newId: string): string {
|
||||
return expr
|
||||
.replaceAll(`results.${id}`, `results.${newId}`)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
|
||||
import FlowPreviewContent from '$lib/components/FlowPreviewContent.svelte'
|
||||
import type { Job } from '$lib/gen'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
import { getContext } from 'svelte'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
@@ -13,14 +14,16 @@
|
||||
let previewOpen = false
|
||||
let previewMode: 'upTo' | 'whole' = 'whole'
|
||||
|
||||
export async function openPreview() {
|
||||
export async function openPreview(test: boolean = false) {
|
||||
if (!previewOpen) {
|
||||
previewOpen = true
|
||||
} else {
|
||||
flowPreviewContent?.test()
|
||||
if (!test) return
|
||||
}
|
||||
flowPreviewContent?.test()
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let flowPreviewContent: FlowPreviewContent
|
||||
let jobId: string | undefined = undefined
|
||||
let job: Job | undefined = undefined
|
||||
@@ -93,5 +96,9 @@
|
||||
on:close={() => {
|
||||
previewOpen = false
|
||||
}}
|
||||
on:openTriggers={(e) => {
|
||||
previewOpen = false
|
||||
dispatch('openTriggers', e.detail)
|
||||
}}
|
||||
/>
|
||||
</Drawer>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import Badge from '$lib/components/common/badge/Badge.svelte'
|
||||
import type { FlowCopilotContext } from '$lib/components/copilot/flow'
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
import { classNames } from '$lib/utils'
|
||||
@@ -9,7 +8,6 @@
|
||||
Database,
|
||||
Gauge,
|
||||
Move,
|
||||
Pencil,
|
||||
PhoneIncoming,
|
||||
Repeat,
|
||||
Square,
|
||||
@@ -28,7 +26,7 @@
|
||||
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
|
||||
import { getDependeeAndDependentComponents } from '../flowExplorer'
|
||||
import { replaceId } from '../flowStore'
|
||||
|
||||
import FlowModuleSchemaItemViewer from './FlowModuleSchemaItemViewer.svelte'
|
||||
import FlowPropPicker from '$lib/components/flows/propPicker/FlowPropPicker.svelte'
|
||||
import type { PropPickerContext } from '$lib/components/prop_picker'
|
||||
export let selected: boolean = false
|
||||
@@ -74,11 +72,6 @@
|
||||
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}
|
||||
@@ -250,58 +243,11 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
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}
|
||||
<div class="flex-none" bind:clientWidth={iconWidth}>
|
||||
<slot name="icon" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Popover
|
||||
class="absolute left-1/2 transform -translate-x-1/2 center-center"
|
||||
style="max-width: calc(100% - {marginLeft}px)"
|
||||
>
|
||||
<div class="text-center truncate {bold ? '!font-bold' : 'font-normal'}">
|
||||
{label}
|
||||
</div>
|
||||
<svelte:fragment slot="text">
|
||||
<div>
|
||||
<div>{label}</div>
|
||||
{#if path != ''}<div>{path}</div>{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
|
||||
<div class="flex items-center space-x-2 relative max-w-[25%]" bind:clientWidth={idBadgeWidth}>
|
||||
{#if id && id !== 'preprocessor' && !id.startsWith('failure') && !id.startsWith('subflow:')}
|
||||
<Badge
|
||||
color="indigo"
|
||||
wrapperClass="max-w-full"
|
||||
baseClass="max-w-full truncate !px-1"
|
||||
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
|
||||
? '!bg-blue-400'
|
||||
: ''} hover:text-white
|
||||
hover:border-blue-700 hover:!visible {hover ? '' : '!hidden'}"
|
||||
on:click|preventDefault|stopPropagation={(event) => (editId = !editId)}
|
||||
title="Edit Id"><Pencil size={14} /></button
|
||||
>
|
||||
{/if}
|
||||
{:else if id?.startsWith('subflow:')}
|
||||
<Badge color="blue" wrapperClass="max-w-full" baseClass="!px-1" title={id}>
|
||||
<span class="max-w-full text-2xs truncate">{id.substring('subflow:'.length)}</span></Badge
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<FlowModuleSchemaItemViewer {label} {path} {id} {deletable} {bold} bind:editId {hover}>
|
||||
<svelte:fragment slot="icon">
|
||||
<slot name="icon" />
|
||||
</svelte:fragment>
|
||||
</FlowModuleSchemaItemViewer>
|
||||
|
||||
{#if id && $flowPropPickerConfig && pickableIds && Object.keys(pickableIds).includes(id)}
|
||||
<div class="absolute -bottom-[14px] right-[21px] translate-x-[50%] center-center">
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<script lang="ts">
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
import Badge from '$lib/components/common/badge/Badge.svelte'
|
||||
import { Pencil } from 'lucide-svelte'
|
||||
|
||||
let iconWidth: number = 0
|
||||
let idBadgeWidth: number | undefined = undefined
|
||||
export let label: string = ''
|
||||
export let path: string = ''
|
||||
export let id: string = ''
|
||||
export let deletable: boolean = false
|
||||
export let bold: boolean = false
|
||||
export let editId: boolean = false
|
||||
export let hover: boolean = false
|
||||
|
||||
$: marginLeft = Math.max(iconWidth ?? 0, idBadgeWidth ?? 0) * 2 + 32
|
||||
</script>
|
||||
|
||||
<div
|
||||
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 && true}
|
||||
<div class="flex-none" bind:clientWidth={iconWidth}>
|
||||
<slot name="icon" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Popover
|
||||
class="absolute left-1/2 transform -translate-x-1/2 center-center"
|
||||
style="max-width: calc(100% - {marginLeft}px)"
|
||||
>
|
||||
<div class="text-center truncate {bold ? '!font-bold' : 'font-normal'}">
|
||||
{label}
|
||||
</div>
|
||||
<svelte:fragment slot="text">
|
||||
<div>
|
||||
<div>{label}</div>
|
||||
{#if path != ''}<div>{path}</div>{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
|
||||
<div class="flex items-center space-x-2 relative max-w-[25%]" bind:clientWidth={idBadgeWidth}>
|
||||
{#if id && id !== 'preprocessor' && !id.startsWith('failure') && !id.startsWith('subflow:')}
|
||||
<Badge
|
||||
color="indigo"
|
||||
wrapperClass="max-w-full"
|
||||
baseClass="max-w-full truncate !px-1"
|
||||
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
|
||||
? '!bg-blue-400'
|
||||
: ''} hover:text-white
|
||||
hover:border-blue-700 hover:!visible {hover ? '' : '!hidden'}"
|
||||
on:click|preventDefault|stopPropagation={(event) => (editId = !editId)}
|
||||
title="Edit Id"><Pencil size={14} /></button
|
||||
>
|
||||
{/if}
|
||||
{:else if id?.startsWith('subflow:')}
|
||||
<Badge color="blue" wrapperClass="max-w-full" baseClass="!px-1" title={id}>
|
||||
<span class="max-w-full text-2xs truncate">{id.substring('subflow:'.length)}</span></Badge
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -22,10 +22,14 @@
|
||||
|
||||
export let retries: number | undefined = undefined
|
||||
export let flowJobs:
|
||||
| { flowJobs: string[]; selected: number; flowJobsSuccess: (boolean | undefined)[], selectedManually: boolean | undefined }
|
||||
| {
|
||||
flowJobs: string[]
|
||||
selected: number
|
||||
flowJobsSuccess: (boolean | undefined)[]
|
||||
selectedManually: boolean | undefined
|
||||
}
|
||||
| undefined
|
||||
|
||||
|
||||
const { selectedId } = getContext<{ selectedId: Writable<string> }>('FlowGraphContext')
|
||||
const dispatch = createEventDispatcher<{
|
||||
delete: CustomEvent<MouseEvent>
|
||||
|
||||
@@ -25,6 +25,19 @@ export type ExtendedOpenFlow = OpenFlow & {
|
||||
visible_to_runner_only?: boolean
|
||||
}
|
||||
|
||||
export type FlowInputEditorState = {
|
||||
selectedTab:
|
||||
| 'inputEditor'
|
||||
| 'history'
|
||||
| 'savedInputs'
|
||||
| 'json'
|
||||
| 'captures'
|
||||
| 'firstStepInputs'
|
||||
| undefined
|
||||
editPanelSize: number | undefined
|
||||
payloadData: Record<string, any> | undefined
|
||||
}
|
||||
|
||||
export type FlowEditorContext = {
|
||||
selectedId: Writable<string>
|
||||
moving: Writable<{ module: FlowModule; modules: FlowModule[] } | undefined>
|
||||
@@ -33,6 +46,7 @@ export type FlowEditorContext = {
|
||||
history: History<OpenFlow>
|
||||
pathStore: Writable<string>
|
||||
flowStore: Writable<ExtendedOpenFlow>
|
||||
flowInputEditorState: Writable<FlowInputEditorState>
|
||||
flowStateStore: Writable<FlowState>
|
||||
testStepStore: Writable<Record<string, any>>
|
||||
saveDraft: () => void
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import Popover from '$lib/components/meltComponents/Popover.svelte'
|
||||
|
||||
export let dropdownItems: Array<{
|
||||
label: string
|
||||
onClick: () => void
|
||||
icon?: any
|
||||
}> = []
|
||||
export let closeOnClick: boolean = false
|
||||
export let placement: any = 'bottom'
|
||||
|
||||
export let open: boolean = false
|
||||
</script>
|
||||
|
||||
<Popover bind:open closeButton={false} {placement}>
|
||||
<svelte:fragment slot="trigger">
|
||||
<slot {open} />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
<div class="flex flex-col bg-surface">
|
||||
{#each dropdownItems as item}
|
||||
<button
|
||||
class="hover:bg-surface-hover p-2 transition-colors duration-150"
|
||||
on:click={() => {
|
||||
item.onClick()
|
||||
if (closeOnClick) {
|
||||
open = false
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<svelte:component this={item.icon} size={14} />
|
||||
<p class="text-xs text-secondary">{item.label}</p>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
@@ -6,24 +6,28 @@
|
||||
export let open = false
|
||||
export let closeButton: boolean = true
|
||||
export let displayArrow: boolean = false
|
||||
export let placement: any = 'bottom'
|
||||
|
||||
const {
|
||||
elements: { trigger, content, arrow, close },
|
||||
states
|
||||
} = createPopover({
|
||||
forceVisible: true
|
||||
forceVisible: true,
|
||||
positioning: {
|
||||
placement
|
||||
}
|
||||
})
|
||||
|
||||
const sync = createSync(states)
|
||||
$: sync.open(open, (v) => (open = v))
|
||||
</script>
|
||||
|
||||
<button type="button" use:melt={$trigger} aria-label="Popup button">
|
||||
<button class="w-full h-full" type="button" use:melt={$trigger} aria-label="Popup button">
|
||||
<slot name="trigger" />
|
||||
</button>
|
||||
|
||||
{#if open}
|
||||
<div use:melt={$content} transition:fade={{ duration: 100 }} class="content">
|
||||
<div use:melt={$content} transition:fade={{ duration: 100 }} class="content z-[9999]">
|
||||
{#if displayArrow}
|
||||
<div use:melt={$arrow} />
|
||||
{/if}
|
||||
@@ -45,6 +49,6 @@
|
||||
}
|
||||
|
||||
.content {
|
||||
@apply z-10 w-fit rounded-[4px] bg-surface p-0 overflow-hidden shadow-md;
|
||||
@apply w-fit rounded-[4px] bg-surface p-0 overflow-hidden shadow-md;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Button } from '../common'
|
||||
import { CornerDownLeft } from 'lucide-svelte'
|
||||
import Popover from '$lib/components/meltComponents/Popover.svelte'
|
||||
|
||||
let name: string = ''
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function addField() {
|
||||
dispatch('add', { name })
|
||||
name = ''
|
||||
}
|
||||
|
||||
let open: boolean = false
|
||||
</script>
|
||||
|
||||
<Popover closeButton={false} bind:open>
|
||||
<svelte:fragment slot="trigger">
|
||||
<slot name="trigger" />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
<div class="flex flex-row gap-2 p-2 rounded-md">
|
||||
<input
|
||||
bind:value={name}
|
||||
placeholder="Field name"
|
||||
on:keydown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
addField()
|
||||
open = false
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="dark"
|
||||
size="xs"
|
||||
id="flow-editor-add-property"
|
||||
on:click={() => {
|
||||
addField()
|
||||
open = false
|
||||
}}
|
||||
disabled={!name}
|
||||
shortCut={{ Icon: CornerDownLeft, withoutModifier: true }}
|
||||
>
|
||||
Add field
|
||||
</Button>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
@@ -0,0 +1,174 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type Schema,
|
||||
type SchemaProperty,
|
||||
modalToSchema,
|
||||
type ModalSchemaProperty
|
||||
} from '$lib/common'
|
||||
import { emptySchema, sendUserToast } from '$lib/utils'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import SimpleEditor from '../SimpleEditor.svelte'
|
||||
import AddPropertyFormV2 from './AddPropertyFormV2.svelte'
|
||||
export let schema: Schema | any = emptySchema()
|
||||
|
||||
export const DEFAULT_PROPERTY: ModalSchemaProperty = {
|
||||
selectedType: 'string',
|
||||
description: '',
|
||||
name: '',
|
||||
required: false
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
if (!schema) {
|
||||
schema = emptySchema()
|
||||
}
|
||||
|
||||
let schemaString: string = ''
|
||||
|
||||
// Internal state: bound to args builder modal
|
||||
let argError = ''
|
||||
let editing = false
|
||||
let oldArgName: string | undefined // when editing argument and changing name
|
||||
let jsonEditor: SimpleEditor
|
||||
|
||||
reorder()
|
||||
|
||||
function reorder() {
|
||||
if (schema.order && Array.isArray(schema.order)) {
|
||||
const n = {}
|
||||
|
||||
;(schema.order as string[]).forEach((x) => {
|
||||
if (schema.properties && schema.properties[x] != undefined) {
|
||||
n[x] = schema.properties[x]
|
||||
}
|
||||
})
|
||||
|
||||
Object.keys(schema.properties ?? {})
|
||||
.filter((x) => !schema.order?.includes(x))
|
||||
.forEach((x) => {
|
||||
n[x] = schema.properties[x]
|
||||
})
|
||||
schema.properties = n
|
||||
}
|
||||
}
|
||||
|
||||
function syncOrders() {
|
||||
if (schema) {
|
||||
schema.order = Object.keys(schema.properties ?? {})
|
||||
}
|
||||
}
|
||||
|
||||
function handleAddOrEditArgument(modalProperty: ModalSchemaProperty): void {
|
||||
// If editing the arg's name, oldName containing the old argument name must be provided
|
||||
argError = ''
|
||||
modalProperty.name = modalProperty.name.trim()
|
||||
|
||||
if (modalProperty.name.length === 0) {
|
||||
argError = 'Arguments need to have a name'
|
||||
} else if (
|
||||
Object.keys(schema.properties ?? {}).includes(modalProperty.name) &&
|
||||
(!editing || (editing && oldArgName && oldArgName !== modalProperty.name))
|
||||
) {
|
||||
argError = 'There is already an argument with this name'
|
||||
} else {
|
||||
if (!schema.properties) {
|
||||
schema.properties = {}
|
||||
}
|
||||
if (!schema.required) {
|
||||
schema.required = []
|
||||
}
|
||||
if (!schema.order || !Array.isArray(schema.order)) {
|
||||
syncOrders()
|
||||
}
|
||||
schema.properties[modalProperty.name] = modalToSchema(modalProperty)
|
||||
if (modalProperty.required) {
|
||||
if (!schema.required.includes(modalProperty.name)) {
|
||||
schema.required.push(modalProperty.name)
|
||||
}
|
||||
} else if (schema.required.includes(modalProperty.name)) {
|
||||
const index = schema.required.indexOf(modalProperty.name, 0)
|
||||
if (index > -1) {
|
||||
schema.required.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
if (editing && oldArgName && oldArgName !== modalProperty.name) {
|
||||
let oldPosition = schema.order.indexOf(oldArgName)
|
||||
schema.order[oldPosition] = modalProperty.name
|
||||
reorder()
|
||||
handleDeleteArgument([oldArgName])
|
||||
}
|
||||
|
||||
if (!schema.order?.includes(modalProperty.name)) {
|
||||
schema.order.push(modalProperty.name)
|
||||
}
|
||||
modalProperty = Object.assign({}, DEFAULT_PROPERTY)
|
||||
editing = false
|
||||
oldArgName = undefined
|
||||
}
|
||||
|
||||
schema = schema
|
||||
schemaString = JSON.stringify(schema, null, '\t')
|
||||
jsonEditor?.setCode(schemaString)
|
||||
|
||||
if (argError !== '') {
|
||||
sendUserToast(argError, true)
|
||||
}
|
||||
|
||||
dispatch('change', schema)
|
||||
}
|
||||
|
||||
export function handleDeleteArgument(argPath: string[]): void {
|
||||
try {
|
||||
let modifiedObject: Schema | SchemaProperty = schema
|
||||
let modifiedProperties = modifiedObject.properties as object
|
||||
let argName = argPath.pop() as string
|
||||
|
||||
argPath.forEach((property) => {
|
||||
if (Object.keys(modifiedProperties).includes(property)) {
|
||||
modifiedObject = modifiedProperties[property]
|
||||
modifiedProperties = modifiedObject.properties as object
|
||||
} else {
|
||||
throw Error('Nested argument not found!')
|
||||
}
|
||||
})
|
||||
|
||||
if (Object.keys(modifiedProperties).includes(argName)) {
|
||||
delete modifiedProperties[argName]
|
||||
|
||||
modifiedObject.required = schema.required.filter((arg) => arg !== argName)
|
||||
if (modifiedObject.order) {
|
||||
modifiedObject.order = modifiedObject.order.filter((arg) => arg !== argName)
|
||||
}
|
||||
schema = schema
|
||||
schemaString = JSON.stringify(schema, null, '\t')
|
||||
dispatch('change', schema)
|
||||
} else {
|
||||
throw Error('Argument not found!')
|
||||
}
|
||||
syncOrders()
|
||||
dispatch('change', schema)
|
||||
} catch (err) {
|
||||
sendUserToast(`Could not delete argument: ${err}`, true)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<AddPropertyFormV2
|
||||
on:add={(e) => {
|
||||
try {
|
||||
handleAddOrEditArgument({
|
||||
...DEFAULT_PROPERTY,
|
||||
selectedType: 'string',
|
||||
name: e.detail.name
|
||||
})
|
||||
} catch (err) {
|
||||
sendUserToast(`Could not add argument: ${err}`, true)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="trigger">
|
||||
<slot name="trigger" />
|
||||
</svelte:fragment>
|
||||
</AddPropertyFormV2>
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { Schema } from '$lib/common'
|
||||
import { GripVertical, Pen, Trash } from 'lucide-svelte'
|
||||
import { GripVertical, Pen, Trash, Plus } from 'lucide-svelte'
|
||||
import EditableSchemaForm from '../EditableSchemaForm.svelte'
|
||||
import { Drawer, DrawerContent } from '../common'
|
||||
import AddProperty from './AddProperty.svelte'
|
||||
@@ -12,6 +12,7 @@
|
||||
import Label from '../Label.svelte'
|
||||
import Toggle from '../Toggle.svelte'
|
||||
import SimpleEditor from '../SimpleEditor.svelte'
|
||||
import AddPropertyV2 from '$lib/components/schema/AddPropertyV2.svelte'
|
||||
|
||||
export let schema: Schema | undefined | any
|
||||
export let parentId: string | undefined = undefined
|
||||
@@ -55,7 +56,6 @@
|
||||
dispatch('change', schema)
|
||||
}
|
||||
|
||||
const yOffset = 49
|
||||
export let jsonView: boolean = false
|
||||
let schemaString: string = JSON.stringify(schema, null, '\t')
|
||||
let editor: SimpleEditor | undefined = undefined
|
||||
@@ -170,10 +170,7 @@
|
||||
</div>
|
||||
|
||||
<Drawer bind:this={schemaFormDrawer} size="1200px">
|
||||
<DrawerContent noPadding title="UI Customisation" on:close={schemaFormDrawer.closeDrawer}>
|
||||
<svelte:fragment slot="actions">
|
||||
<AddProperty on:change bind:schema />
|
||||
</svelte:fragment>
|
||||
<DrawerContent title="UI Customisation" on:close={schemaFormDrawer.closeDrawer}>
|
||||
<EditableSchemaForm
|
||||
on:change
|
||||
bind:this={editableSchemaForm}
|
||||
@@ -185,10 +182,22 @@
|
||||
on:delete={(e) => {
|
||||
addProperty?.handleDeleteArgument([e.detail])
|
||||
}}
|
||||
offset={yOffset}
|
||||
lightweightMode
|
||||
dndType="drawer"
|
||||
/>
|
||||
editTab="inputEditor"
|
||||
>
|
||||
<svelte:fragment slot="addProperty">
|
||||
<AddPropertyV2 bind:schema on:change>
|
||||
<svelte:fragment slot="trigger">
|
||||
<div
|
||||
class="w-full py-2 flex justify-center items-center border border-dashed rounded-md hover:bg-surface-hover"
|
||||
>
|
||||
<Plus size={14} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</AddPropertyV2>
|
||||
</svelte:fragment>
|
||||
</EditableSchemaForm>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
{:else}
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
import type { Schema } from '$lib/common'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import EditableSchemaForm from '../EditableSchemaForm.svelte'
|
||||
import AddProperty from './AddProperty.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Toggle from '../Toggle.svelte'
|
||||
import { emptySchema, validateFileExtension } from '$lib/utils'
|
||||
import AutoComplete from 'simple-svelte-autocomplete'
|
||||
import { Alert } from '../common'
|
||||
import AddPropertyV2 from '$lib/components/schema/AddPropertyV2.svelte'
|
||||
import { Plus } from 'lucide-svelte'
|
||||
|
||||
export let schema: Schema | undefined | any
|
||||
export let offset: number = 0
|
||||
export let uiOnly: boolean = false
|
||||
export let noPreview: boolean = false
|
||||
export let fullHeight: boolean = true
|
||||
@@ -18,7 +18,7 @@
|
||||
export let formatExtension: string | undefined = undefined
|
||||
|
||||
let resourceIsTextFile: boolean = false
|
||||
let addProperty: AddProperty | undefined = undefined
|
||||
let addProperty: AddPropertyV2 | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
schema = emptySchema()
|
||||
formatExtension = undefined
|
||||
} else {
|
||||
formatExtension = ""
|
||||
formatExtension = ''
|
||||
schema = emptySchema()
|
||||
schema.order = ['content']
|
||||
schema.properties = {
|
||||
@@ -62,19 +62,44 @@
|
||||
return matches
|
||||
}
|
||||
|
||||
let suggestedFileExtensions = ['json', 'yaml', 'jinja', 'j2', 'ini', 'cfg', 'toml', 'html', 'xml', 'yml']
|
||||
let suggestedFileExtensions = [
|
||||
'json',
|
||||
'yaml',
|
||||
'jinja',
|
||||
'j2',
|
||||
'ini',
|
||||
'cfg',
|
||||
'toml',
|
||||
'html',
|
||||
'xml',
|
||||
'yml'
|
||||
]
|
||||
let autocompleteExtension = true
|
||||
</script>
|
||||
|
||||
{#if !resourceIsTextFile}
|
||||
<div class={twMerge(fullHeight ? 'h-full' : 'h-80', 'border overflow-y-auto rounded-md')}>
|
||||
<div class="p-4 border-b">
|
||||
<AddProperty
|
||||
on:change={() => dispatch('change', schema)}
|
||||
<div
|
||||
class={twMerge(
|
||||
fullHeight ? 'h-full' : 'h-80',
|
||||
noPreview ? '' : 'border rounded-md p-2',
|
||||
'overflow-y-auto flex flex-col gap-2'
|
||||
)}
|
||||
>
|
||||
{#if noPreview}
|
||||
<AddPropertyV2
|
||||
bind:schema
|
||||
bind:this={addProperty}
|
||||
/>
|
||||
</div>
|
||||
on:change={() => dispatch('change', schema)}
|
||||
>
|
||||
<svelte:fragment slot="trigger">
|
||||
<div
|
||||
class="w-full py-2 flex justify-center items-center border border-dashed rounded-md hover:bg-surface-hover"
|
||||
>
|
||||
<Plus size={14} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</AddPropertyV2>
|
||||
{/if}
|
||||
<EditableSchemaForm
|
||||
bind:schema
|
||||
on:change={() => dispatch('change', schema)}
|
||||
@@ -85,17 +110,35 @@
|
||||
on:delete={(e) => {
|
||||
addProperty?.handleDeleteArgument([e.detail])
|
||||
}}
|
||||
{offset}
|
||||
{uiOnly}
|
||||
{noPreview}
|
||||
{lightweightMode}
|
||||
/>
|
||||
editTab="inputEditor"
|
||||
>
|
||||
<svelte:fragment slot="addProperty">
|
||||
{#if !noPreview}
|
||||
<AddPropertyV2
|
||||
bind:schema
|
||||
bind:this={addProperty}
|
||||
on:change={() => dispatch('change', schema)}
|
||||
>
|
||||
<svelte:fragment slot="trigger">
|
||||
<div
|
||||
class="w-full py-2 flex justify-center items-center border border-dashed rounded-md hover:bg-surface-hover"
|
||||
>
|
||||
<Plus size={14} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</AddPropertyV2>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</EditableSchemaForm>
|
||||
</div>
|
||||
{/if}
|
||||
{#if resourceIsTextFile}
|
||||
<div class="flex items-center space-x-2 w-5/12">
|
||||
<label for="format-extension" class="text-base font-medium whitespace-nowrap">
|
||||
File extension{autocompleteExtension ? "" : " (free text)"}:
|
||||
File extension{autocompleteExtension ? '' : ' (free text)'}:
|
||||
</label>
|
||||
{#if autocompleteExtension}
|
||||
<AutoComplete
|
||||
@@ -145,7 +188,8 @@
|
||||
{#if invalidExtension}
|
||||
<Alert title="Invalid file extension" type="error">
|
||||
The provided extension (<span class="font-bold font-mono">.{formatExtension}</span>) contains
|
||||
invalid characters. Note that you shouldn't add the leading dot, (i.e. enter `json` instead of `.json`)
|
||||
invalid characters. Note that you shouldn't add the leading dot, (i.e. enter `json` instead of
|
||||
`.json`)
|
||||
</Alert>
|
||||
{:else if formatExtension && formatExtension !== ''}
|
||||
<Alert title={`Example: my_file.${formatExtension}`} type="info">
|
||||
|
||||
@@ -363,7 +363,7 @@
|
||||
</Label>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-row gap-2">
|
||||
<div class="flex flex-row gap-2 flex-wrap h-auto">
|
||||
<Toggle
|
||||
options={{ right: 'Required' }}
|
||||
size="xs"
|
||||
@@ -399,7 +399,7 @@
|
||||
{/if}
|
||||
<Toggle
|
||||
options={{
|
||||
right: 'Disabled',
|
||||
right: 'Disableds',
|
||||
rightTooltip: 'Do not let user modify this field'
|
||||
}}
|
||||
lightMode
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import SchemaPickerRow from './SchemaPickerRow.svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { ExternalLink } from 'lucide-svelte'
|
||||
import { base } from '$lib/base'
|
||||
|
||||
export let job: any
|
||||
export let selected = false
|
||||
export let payloadData: any | undefined = undefined
|
||||
export let hovering = false
|
||||
</script>
|
||||
|
||||
<SchemaPickerRow {payloadData} date={job.created_at} {selected} {hovering}>
|
||||
<svelte:fragment slot="start">
|
||||
<div class="center-center">
|
||||
<div
|
||||
class="rounded-full w-2 h-2 {job.success ? 'bg-green-400' : 'bg-red-400'}"
|
||||
title={job.success ? 'Success' : 'Failed'}
|
||||
/>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="extra">
|
||||
<div class="center-center {hovering ? '' : '!hidden'}">
|
||||
<a
|
||||
target="_blank"
|
||||
href="{base}/run/{job.id}?workspace={$workspaceStore}"
|
||||
class="text-right float-right text-secondary"
|
||||
title="See run detail in a new tab"
|
||||
>
|
||||
<ExternalLink size={16} />
|
||||
</a>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</SchemaPickerRow>
|
||||
@@ -203,6 +203,7 @@
|
||||
bind:schema={oneOfSchemas[idx]}
|
||||
uiOnly
|
||||
jsonEnabled={false}
|
||||
editTab="inputEditor"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -210,7 +211,14 @@
|
||||
<div class="text-tertiary text-xs">No settings available for Dynamic Select</div>
|
||||
{:else if type == 'object' && !format?.startsWith('resource-') && !isFlowInput && !isAppInput}
|
||||
<div class="border">
|
||||
<EditableSchemaForm on:change noPreview bind:schema uiOnly jsonEnabled={false} />
|
||||
<EditableSchemaForm
|
||||
on:change
|
||||
noPreview
|
||||
bind:schema
|
||||
uiOnly
|
||||
jsonEnabled={false}
|
||||
editTab="inputEditor"
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-tertiary text-xs">No settings available for this field type</div>
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<script lang="ts">
|
||||
import SchemaPickerRow from './SchemaPickerRow.svelte'
|
||||
import Skeleton from '$lib/components/common/skeleton/Skeleton.svelte'
|
||||
import { JobService } from '$lib/gen/index.js'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { ExternalLink } from 'lucide-svelte'
|
||||
import { base } from '$lib/base'
|
||||
import { Cell, Row } from '$lib/components/table'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let job: any
|
||||
export let selected = false
|
||||
export let payloadData: any | undefined = undefined
|
||||
const dispatch = createEventDispatcher()
|
||||
export let hovering = false
|
||||
|
||||
let loadingArgs = true
|
||||
loadArgsFromRunningJob(job.id)
|
||||
|
||||
async function loadArgsFromRunningJob(id: string | undefined) {
|
||||
if (!id) return
|
||||
payloadData = await JobService.getJobArgs({
|
||||
workspace: $workspaceStore!,
|
||||
id
|
||||
})
|
||||
loadingArgs = false
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if loadingArgs}
|
||||
<Cell>
|
||||
<Skeleton layout={[[1]]} />
|
||||
</Cell>
|
||||
<Cell>
|
||||
<Skeleton layout={[[1]]} />
|
||||
</Cell>
|
||||
<Cell>
|
||||
<Skeleton layout={[[1]]} />
|
||||
</Cell>
|
||||
{:else}
|
||||
<Row
|
||||
on:click={() => dispatch('select', { id: job.id, payloadData })}
|
||||
class={twMerge(
|
||||
selected === job.id ? 'bg-surface-selected' : 'hover:bg-surface-hover',
|
||||
'cursor-pointer rounded-md'
|
||||
)}
|
||||
on:hover={(e) => (hovering = e.detail ? true : false)}
|
||||
>
|
||||
<SchemaPickerRow {payloadData} date={job.created_at} {selected} {hovering}>
|
||||
<svelte:fragment slot="start">
|
||||
<div class="center-center">
|
||||
<div class="rounded-full w-2 h-2 bg-orange-400 animate-pulse" title="Running" />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="extra">
|
||||
<div class="center-center {hovering ? '' : '!hidden'}">
|
||||
<a
|
||||
target="_blank"
|
||||
href="{base}/run/{job.id}?workspace={$workspaceStore}"
|
||||
class="text-right float-right text-secondary"
|
||||
title="See run detail in a new tab"
|
||||
>
|
||||
<ExternalLink size={16} />
|
||||
</a>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</SchemaPickerRow>
|
||||
</Row>
|
||||
{/if}
|
||||
@@ -73,7 +73,7 @@
|
||||
on:consider={handleConsider}
|
||||
on:finalize={handleFinalize}
|
||||
{lightweightMode}
|
||||
{args}
|
||||
bind:args
|
||||
{prettifyHeader}
|
||||
{onlyMaskPassword}
|
||||
{disablePortal}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<script lang="ts">
|
||||
import CustomPopover from '$lib/components/CustomPopover.svelte'
|
||||
import { copyToClipboard } from '$lib/utils'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import ObjectViewer from '$lib/components/propertyPicker/ObjectViewer.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let payloadData: Record<string, any>
|
||||
export let date: string | undefined
|
||||
export let selected = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let hovering = false
|
||||
function formatDate(dateString: string | undefined): string {
|
||||
if (!dateString) return ''
|
||||
const date = new Date(dateString)
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}).format(date)
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
class={twMerge(
|
||||
'w-full rounded-sm px-2',
|
||||
hovering ? 'bg-surface-hover' : '',
|
||||
selected ? 'bg-surface-selected' : ''
|
||||
)}
|
||||
on:click={() => {
|
||||
dispatch('select')
|
||||
}}
|
||||
on:mouseenter={() => {
|
||||
hovering = true
|
||||
}}
|
||||
on:mouseleave={() => {
|
||||
hovering = false
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-row gap-2">
|
||||
<slot name="start" />
|
||||
|
||||
<div
|
||||
class="text-2xs font-normal text-left p-2 rounded-md overflow-auto grow-0 text-ellipsis whitespace-nowrap scrollbar-none"
|
||||
title={formatDate(date)}
|
||||
>
|
||||
{formatDate(date)}
|
||||
</div>
|
||||
|
||||
<CustomPopover class="grow min-w-12 ">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class={twMerge(
|
||||
'text-xs border font-normal text-left p-1 rounded-md overflow-auto grow whitespace-nowrap scrollbar-none',
|
||||
hovering && 'border-surface'
|
||||
)}
|
||||
on:click={() => {
|
||||
if (selected) {
|
||||
copyToClipboard(JSON.stringify(payloadData))
|
||||
}
|
||||
}}
|
||||
on:mouseenter={() => {
|
||||
hovering = true
|
||||
}}
|
||||
on:mouseleave={() => {
|
||||
hovering = false
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(payloadData)}
|
||||
</div>
|
||||
<svelte:fragment slot="overlay">
|
||||
<ObjectViewer json={payloadData} />
|
||||
</svelte:fragment>
|
||||
</CustomPopover>
|
||||
|
||||
{#if hovering}
|
||||
<slot name="extra" />
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<style>
|
||||
.scrollbar-none {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
.scrollbar-none::-webkit-scrollbar {
|
||||
display: none; /* Chrome, Safari and Opera */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,101 @@
|
||||
<script lang="ts">
|
||||
import CustomPopover from '$lib/components/CustomPopover.svelte'
|
||||
import { copyToClipboard } from '$lib/utils'
|
||||
import ObjectViewer from '$lib/components/propertyPicker/ObjectViewer.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import Cell from '$lib/components/table/Cell.svelte'
|
||||
|
||||
export let payloadData: Record<string, any>
|
||||
export let date: string | undefined
|
||||
export let selected = false
|
||||
export let hovering = false
|
||||
|
||||
function formatDate(dateString: string | undefined): string {
|
||||
if (!dateString) return ''
|
||||
const date = new Date(dateString)
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}).format(date)
|
||||
}
|
||||
|
||||
function formatDateShort(dateString: string | undefined): string {
|
||||
if (!dateString) return ''
|
||||
const date = new Date(dateString)
|
||||
const now = new Date()
|
||||
|
||||
// If date is today, only show time
|
||||
if (date.toDateString() === now.toDateString()) {
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}).format(date)
|
||||
}
|
||||
|
||||
// If date is this year, show only month and day
|
||||
if (date.getFullYear() === now.getFullYear()) {
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
}).format(date)
|
||||
}
|
||||
|
||||
// If date is from another year, only show the date with year
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
}).format(date)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Cell>
|
||||
<slot name="start" />
|
||||
</Cell>
|
||||
|
||||
<Cell
|
||||
wrap
|
||||
class="text-2xs font-normal text-left p-2 rounded-md overflow-auto text-ellipsis scrollbar-none"
|
||||
title={formatDate(date)}
|
||||
>
|
||||
{formatDateShort(date)}
|
||||
</Cell>
|
||||
|
||||
<Cell class="items-center flex flex-row gap-2">
|
||||
<CustomPopover class="w-full overflow-auto flex items-center justify-center">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class={twMerge(
|
||||
'text-xs border w-full font-normal text-left p-1 rounded-md whitespace-nowrap overflow-hidden text-ellipsis',
|
||||
hovering && 'border-surface'
|
||||
)}
|
||||
on:click={() => {
|
||||
if (selected) {
|
||||
copyToClipboard(JSON.stringify(payloadData))
|
||||
}
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(payloadData)}
|
||||
</div>
|
||||
<svelte:fragment slot="overlay">
|
||||
<ObjectViewer json={payloadData} />
|
||||
</svelte:fragment>
|
||||
</CustomPopover>
|
||||
|
||||
<slot name="extra" />
|
||||
</Cell>
|
||||
|
||||
<style>
|
||||
.scrollbar-none {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
.scrollbar-none::-webkit-scrollbar {
|
||||
display: none; /* Chrome, Safari and Opera */
|
||||
}
|
||||
</style>
|
||||
@@ -93,7 +93,7 @@
|
||||
<Tab value="logs" size="xs">Logs & Result</Tab>
|
||||
<Tab value="history" size="xs">History</Tab>
|
||||
{#if showCaptures}
|
||||
<Tab value="captures" size="xs">Captures</Tab>
|
||||
<Tab value="captures" size="xs">Trigger captures</Tab>
|
||||
{/if}
|
||||
|
||||
<svelte:fragment slot="content">
|
||||
|
||||
@@ -24,25 +24,69 @@
|
||||
export let rowCount: number | undefined = undefined
|
||||
export let hasMore: boolean = true
|
||||
export let contentHeight: number = 0
|
||||
export let tableFixed: boolean = false
|
||||
export let infiniteScroll: boolean | undefined = undefined
|
||||
|
||||
let footerHeight: number = 0
|
||||
let tableHeight: number = 0
|
||||
const dispatch = createEventDispatcher()
|
||||
let tableContainer: HTMLDivElement
|
||||
let isLoading = false
|
||||
|
||||
setContext<DatatableContext>('datatable', {
|
||||
size
|
||||
})
|
||||
|
||||
$: contentHeight = tableHeight - footerHeight
|
||||
|
||||
function checkScrollStatus() {
|
||||
if (!infiniteScroll || isLoading) return
|
||||
|
||||
const hasScrollbar = tableContainer.scrollHeight > tableContainer.clientHeight
|
||||
if (!hasScrollbar && hasMore) {
|
||||
isLoading = true
|
||||
dispatch('loadMore')
|
||||
setTimeout(() => {
|
||||
isLoading = false
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
if (!infiniteScroll || isLoading) {
|
||||
if (isLoading) {
|
||||
const checkAgain = () => {
|
||||
if (!isLoading) {
|
||||
handleScroll()
|
||||
}
|
||||
}
|
||||
setTimeout(checkAgain, 200)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = tableContainer
|
||||
if (scrollHeight - (scrollTop + clientHeight) < 50) {
|
||||
isLoading = true
|
||||
dispatch('loadMore')
|
||||
setTimeout(() => {
|
||||
isLoading = false
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
$: if (tableContainer && hasMore && !isLoading) {
|
||||
checkScrollStatus()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={twMerge('h-full', rounded ? 'rounded-md' : '', noBorder ? 'border-0' : 'border')}
|
||||
bind:clientHeight={tableHeight}
|
||||
>
|
||||
<List justify="between" gap="none">
|
||||
<div class="w-full overflow-auto min-h-0 grow">
|
||||
<table class={twMerge('min-w-full divide-y')}>
|
||||
<List justify="between" gap="none" hFull={true}>
|
||||
<div class="w-full overflow-auto h-fit" bind:this={tableContainer} on:scroll={handleScroll}>
|
||||
<table class={tableFixed ? 'table-fixed w-full' : 'min-w-full'}>
|
||||
<slot />
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<thead class="bg-surface-secondary sticky top-0 z-10">
|
||||
<thead class="bg-surface-secondary sticky top-0 z-10 border-b">
|
||||
<slot />
|
||||
<div class="absolute top-2 right-2">
|
||||
<slot name="headerAction" />
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
export let selected: boolean = false
|
||||
export let dividable: boolean = false
|
||||
export let disabled: boolean = false
|
||||
export let hovering: boolean = false
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
@@ -15,11 +16,20 @@
|
||||
selected ? 'bg-blue-50 dark:bg-blue-900/50' : '',
|
||||
'transition-all',
|
||||
dividable ? 'divide-x' : '',
|
||||
disabled ? 'opacity-60' : ''
|
||||
disabled ? 'opacity-60' : '',
|
||||
$$props.class
|
||||
)}
|
||||
on:click={() => {
|
||||
dispatch('click')
|
||||
}}
|
||||
on:mouseenter={() => {
|
||||
hovering = true
|
||||
dispatch('hover', true)
|
||||
}}
|
||||
on:mouseleave={() => {
|
||||
hovering = false
|
||||
dispatch('hover', false)
|
||||
}}
|
||||
>
|
||||
<slot />
|
||||
</tr>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as Row } from './Row.svelte'
|
||||
export { default as Cell } from './Cell.svelte'
|
||||
export { default as DataTable } from './DataTable.svelte'
|
||||
@@ -16,6 +16,7 @@ export type TriggerContext = {
|
||||
simplifiedPoll: Writable<boolean | undefined>
|
||||
defaultValues: Writable<Record<string, any> | undefined>
|
||||
captureOn: Writable<boolean | undefined>
|
||||
showCaptureHint: Writable<boolean | undefined>
|
||||
}
|
||||
|
||||
export function setScheduledPollSchedule(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Popover from '$lib/components/meltComponents/Popover.svelte'
|
||||
import { Button } from '$lib/components/common'
|
||||
import { Webhook, Route, Unplug, Mail } from 'lucide-svelte'
|
||||
import { Webhook, Route, Unplug, Mail, Plus } from 'lucide-svelte'
|
||||
import KafkaIcon from '$lib/components/icons/KafkaIcon.svelte'
|
||||
import { enterpriseLicense } from '$lib/stores'
|
||||
import { type CaptureTriggerKind } from '$lib/gen'
|
||||
@@ -10,6 +10,8 @@
|
||||
import CaptureIcon from './CaptureIcon.svelte'
|
||||
import NatsIcon from '../icons/NatsIcon.svelte'
|
||||
|
||||
export let small = false
|
||||
|
||||
let isOpen = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
@@ -25,9 +27,31 @@
|
||||
|
||||
<Popover closeButton={false} bind:open={isOpen}>
|
||||
<svelte:fragment slot="trigger">
|
||||
<Button color="dark" btnClasses="rounded-l-none" wrapperClasses="h-full" nonCaptureEvent>
|
||||
<CaptureIcon />
|
||||
</Button>
|
||||
{#if small}
|
||||
<Button
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="border"
|
||||
wrapperClasses="h-full"
|
||||
nonCaptureEvent
|
||||
title="Test trigger"
|
||||
>
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
<CaptureIcon variant="redDot" />
|
||||
<Plus size={10} class="text-red" />
|
||||
</div>
|
||||
</Button>
|
||||
{:else}
|
||||
<Button
|
||||
color="dark"
|
||||
btnClasses="rounded-l-none"
|
||||
wrapperClasses="h-full"
|
||||
nonCaptureEvent
|
||||
title="Test trigger"
|
||||
>
|
||||
<CaptureIcon variant="redDot" />
|
||||
</Button>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
<div class="flex flex-col bg-surface">
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { ScanIcon } from 'lucide-svelte'
|
||||
|
||||
export let size = 14
|
||||
export let variant: 'redDot' | undefined = undefined
|
||||
</script>
|
||||
|
||||
<div class="relative">
|
||||
<ScanIcon size="14" />
|
||||
<ScanIcon {size} />
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<svg
|
||||
class="text-red-500"
|
||||
class={variant === 'redDot' ? 'text-red-500' : ''}
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -12,43 +12,81 @@
|
||||
<script lang="ts">
|
||||
import { slide } from 'svelte/transition'
|
||||
import AnimatedButton from '../common/button/AnimatedButton.svelte'
|
||||
import PulseButton from '../common/button/PulseButton.svelte'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import { CircleStop } from 'lucide-svelte'
|
||||
import ConnectionIndicator, {
|
||||
type ConnectionInfo
|
||||
} from '../common/alert/ConnectionIndicator.svelte'
|
||||
import CaptureTable from './CaptureTable.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { createEventDispatcher, onDestroy, getContext } from 'svelte'
|
||||
import type { CaptureTriggerKind } from '$lib/gen'
|
||||
import CaptureIcon from './CaptureIcon.svelte'
|
||||
import Tooltip from '../Tooltip.svelte'
|
||||
|
||||
import type { TriggerContext } from '$lib/components/triggers'
|
||||
export let disabled: boolean
|
||||
export let captureType: CaptureTriggerKind
|
||||
export let captureInfo: CaptureInfo
|
||||
export let captureTable: CaptureTable | undefined
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
captureToggle: undefined
|
||||
updateSchema: { payloadData: Record<string, any>; redirect: boolean }
|
||||
}>()
|
||||
|
||||
const { showCaptureHint } = getContext<TriggerContext>('TriggerContext')
|
||||
|
||||
onDestroy(() => {
|
||||
if (captureInfo.active) {
|
||||
dispatch('captureToggle')
|
||||
}
|
||||
})
|
||||
|
||||
function handleUpdateSchema(e: any) {
|
||||
dispatch('updateSchema', {
|
||||
payloadData: e.detail.payloadData,
|
||||
redirect: e.detail.redirect
|
||||
})
|
||||
}
|
||||
|
||||
let openingDuration = 400
|
||||
let pulseButton: PulseButton | undefined
|
||||
function updateShowCaptureHint(show: boolean | undefined) {
|
||||
if (show) {
|
||||
$showCaptureHint = false
|
||||
setTimeout(() => {
|
||||
pulseButton?.triggerPulse(1)
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
$: updateShowCaptureHint($showCaptureHint)
|
||||
</script>
|
||||
|
||||
<div transition:slide class="pb-4">
|
||||
<div transition:slide={{ duration: openingDuration }} class="pb-12 overflow-hidden">
|
||||
<div class="border p-4 rounded-lg">
|
||||
<div class="flex flex-col gap-1 mb-4">
|
||||
<div class="flex flex-row items-center justify-start gap-1">
|
||||
<AnimatedButton animate={captureInfo.active} baseRadius="6px" wrapperClasses="ml-[-2px]">
|
||||
<Button
|
||||
size="xs2"
|
||||
on:click={() => dispatch('captureToggle')}
|
||||
variant="border"
|
||||
{disabled}
|
||||
color="light"
|
||||
startIcon={{ icon: captureInfo.active ? CircleStop : CaptureIcon }}
|
||||
btnClasses={captureInfo.active ? 'text-blue-500 hover:text-blue-500' : ''}
|
||||
>
|
||||
{captureInfo.active ? 'Stop' : 'Start capturing'}
|
||||
</Button>
|
||||
</AnimatedButton>
|
||||
<PulseButton bind:this={pulseButton} numberOfPulses={1} pulseDuration={1}>
|
||||
<AnimatedButton animate={captureInfo.active} baseRadius="6px" wrapperClasses="ml-[-2px]">
|
||||
<Button
|
||||
size="xs2"
|
||||
on:click={() => dispatch('captureToggle')}
|
||||
variant="border"
|
||||
{disabled}
|
||||
color="light"
|
||||
btnClasses={captureInfo.active ? 'text-blue-500 hover:text-blue-500' : ''}
|
||||
>
|
||||
<div class="flex flex-row items-center gap-1 w-28 justify-center">
|
||||
{#if captureInfo.active}
|
||||
<CircleStop size={14} />
|
||||
{:else}
|
||||
<CaptureIcon variant="redDot" size={14} />
|
||||
{/if}
|
||||
{captureInfo.active ? 'Stop' : 'Start capturing'}
|
||||
</div>
|
||||
</Button>
|
||||
</AnimatedButton>
|
||||
</PulseButton>
|
||||
|
||||
{#if captureInfo.active}
|
||||
<ConnectionIndicator connectionInfo={captureInfo.connectionInfo} />
|
||||
@@ -83,9 +121,10 @@
|
||||
path={captureInfo.path}
|
||||
canEdit={true}
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:updateSchema={handleUpdateSchema}
|
||||
on:addPreprocessor
|
||||
maxHeight={300}
|
||||
on:testWithArgs
|
||||
fullHeight={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,54 +1,43 @@
|
||||
<script lang="ts">
|
||||
import Label from '../Label.svelte'
|
||||
import { Clipboard, Info, Trash2, Plus } from 'lucide-svelte'
|
||||
import { Info, Trash2 } from 'lucide-svelte'
|
||||
import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte'
|
||||
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import { copyToClipboard } from '$lib/utils'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import CustomPopover from '../CustomPopover.svelte'
|
||||
import { convert } from '@redocly/json-to-json-schema'
|
||||
import SchemaViewer from '../SchemaViewer.svelte'
|
||||
import { Webhook, Route, Unplug, Mail, Play } from 'lucide-svelte'
|
||||
import KafkaIcon from '$lib/components/icons/KafkaIcon.svelte'
|
||||
import { isObject } from '$lib/utils'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
import { type TriggerKind } from '../triggers'
|
||||
import { CaptureService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { type CaptureTriggerKind } from '$lib/gen'
|
||||
import type { Capture } from '$lib/gen'
|
||||
import { captureTriggerKindToTriggerKind } from '../triggers'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { slide } from 'svelte/transition'
|
||||
import CaptureButton from '$lib/components/triggers/CaptureButton.svelte'
|
||||
import InfiniteList from '../InfiniteList.svelte'
|
||||
import { sendUserToast } from '$lib/utils'
|
||||
import SchemaPickerRow from '$lib/components/schema/SchemaPickerRow.svelte'
|
||||
import { clickOutside } from '$lib/utils'
|
||||
|
||||
export let path: string
|
||||
export let hasPreprocessor = false
|
||||
export let canHavePreprocessor = false
|
||||
export let isFlow = false
|
||||
export let captureType: CaptureTriggerKind | 'all' = 'webhook'
|
||||
export let captureType: CaptureTriggerKind | undefined = undefined
|
||||
export let headless = false
|
||||
export let addButton = false
|
||||
export let hideCapturesWhenEmpty = false
|
||||
export let canEdit = false
|
||||
export let maxHeight: number | undefined = undefined
|
||||
export let fullHeight = true
|
||||
|
||||
let captures: Capture[] = []
|
||||
let selected: number | undefined = undefined
|
||||
let testKind: 'preprocessor' | 'main' = 'main'
|
||||
let isEmpty: boolean = true
|
||||
let infiniteList: InfiniteList | null = null
|
||||
let firstClick = true
|
||||
let capturesLength = 0
|
||||
|
||||
$: hasPreprocessor && (testKind = 'preprocessor')
|
||||
|
||||
let deleteLoading: number | null = null
|
||||
async function deleteCapture(id: number) {
|
||||
deleteLoading = id
|
||||
try {
|
||||
await CaptureService.deleteCapture({
|
||||
workspace: $workspaceStore!,
|
||||
id
|
||||
})
|
||||
refreshCaptures()
|
||||
} finally {
|
||||
deleteLoading = null
|
||||
}
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
openTriggers: {
|
||||
kind: TriggerKind
|
||||
@@ -60,186 +49,263 @@
|
||||
}
|
||||
addPreprocessor: null
|
||||
updateSchema: {
|
||||
schema: any
|
||||
payloadData: any
|
||||
redirect: boolean
|
||||
args?: boolean
|
||||
}
|
||||
select: any
|
||||
testWithArgs: any
|
||||
}>()
|
||||
|
||||
export async function refreshCaptures() {
|
||||
captures = await CaptureService.listCaptures({
|
||||
workspace: $workspaceStore!,
|
||||
runnableKind: isFlow ? 'flow' : 'script',
|
||||
path,
|
||||
triggerKind: captureType !== 'all' ? captureType : undefined
|
||||
})
|
||||
export async function loadCaptures(refresh: boolean = false) {
|
||||
await infiniteList?.loadData(refresh ? 'refresh' : 'loadMore')
|
||||
}
|
||||
refreshCaptures()
|
||||
</script>
|
||||
|
||||
{#if captures.length > 0 || !hideCapturesWhenEmpty}
|
||||
<Label
|
||||
label="Captures"
|
||||
{headless}
|
||||
class={twMerge(
|
||||
'flex flex-col h-full divide-y gap-1',
|
||||
maxHeight ? `max-h-[${maxHeight}px]` : ''
|
||||
)}
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
{#if addButton && captureType !== 'all'}
|
||||
<Button
|
||||
size="xs2"
|
||||
color="light"
|
||||
variant="contained"
|
||||
iconOnly
|
||||
startIcon={{ icon: Plus }}
|
||||
on:click={() => {
|
||||
if (captureType !== 'all') {
|
||||
dispatch('openTriggers', {
|
||||
kind: captureTriggerKindToTriggerKind(captureType),
|
||||
config: {}
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="action">
|
||||
{#if canHavePreprocessor}
|
||||
<div>
|
||||
<ToggleButtonGroup bind:selected={testKind} class="h-full">
|
||||
<ToggleButton value="main" label={isFlow ? 'Flow' : 'Main'} small />
|
||||
<ToggleButton
|
||||
value="preprocessor"
|
||||
label="Preprocessor"
|
||||
small
|
||||
tooltip="When the runnable has a preprocessor, it receives additional information about the request"
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<div class="flex flex-col gap-1 pt-2 grow overflow-y-auto">
|
||||
{#if captures.length === 0}
|
||||
<div class="text-xs text-secondary">
|
||||
{captureType === 'all' ? 'No captures yet' : `No ${captureType} captures yet`}
|
||||
</div>
|
||||
{:else}
|
||||
{#each captures as capture}
|
||||
{@const payload = isObject(capture.payload) ? capture.payload : {}}
|
||||
{@const triggerExtra = isObject(capture.trigger_extra) ? capture.trigger_extra : {}}
|
||||
{@const payloadData =
|
||||
function initLoadCaptures(testKind: 'preprocessor' | 'main' = 'main') {
|
||||
const loadInputsPageFn = async (page: number, perPage: number) => {
|
||||
const captures = await CaptureService.listCaptures({
|
||||
workspace: $workspaceStore!,
|
||||
runnableKind: isFlow ? 'flow' : 'script',
|
||||
path: path ?? '',
|
||||
triggerKind: captureType,
|
||||
page,
|
||||
perPage
|
||||
})
|
||||
let capturesWithPayload = captures.map((capture) => {
|
||||
const payload = isObject(capture.payload) ? capture.payload : {}
|
||||
const triggerExtra = isObject(capture.trigger_extra) ? capture.trigger_extra : {}
|
||||
return {
|
||||
...capture,
|
||||
payloadData:
|
||||
testKind === 'preprocessor'
|
||||
? {
|
||||
...payload,
|
||||
...triggerExtra
|
||||
}
|
||||
: payload}
|
||||
{@const schema =
|
||||
isFlow && testKind === 'main'
|
||||
? { required: [], properties: {}, ...convert(payloadData) }
|
||||
: {}}
|
||||
<div class="flex flex-row gap-1" transition:slide>
|
||||
<div class="text-xs border p-2 rounded-md overflow-auto grow whitespace-nowrap">
|
||||
{JSON.stringify(payloadData)}
|
||||
: { ...payload }
|
||||
}
|
||||
})
|
||||
return capturesWithPayload
|
||||
}
|
||||
infiniteList?.setLoader(loadInputsPageFn)
|
||||
|
||||
const deleteInputFn = async (id: any) => {
|
||||
await CaptureService.deleteCapture({
|
||||
workspace: $workspaceStore!,
|
||||
id
|
||||
})
|
||||
}
|
||||
infiniteList?.setDeleteItemFn(deleteInputFn)
|
||||
}
|
||||
|
||||
function handleSelect(capture: any) {
|
||||
if (selected === capture.id) {
|
||||
deselect()
|
||||
} else {
|
||||
selected = capture.id
|
||||
dispatch('select', capture.payloadData)
|
||||
}
|
||||
}
|
||||
|
||||
function deselect() {
|
||||
selected = undefined
|
||||
dispatch('select', undefined)
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
deselect()
|
||||
})
|
||||
|
||||
const captureKindToIcon: Record<string, any> = {
|
||||
webhook: Webhook,
|
||||
http: Route,
|
||||
email: Mail,
|
||||
websocket: Unplug,
|
||||
kafka: KafkaIcon
|
||||
}
|
||||
|
||||
async function getPropPickerElements(): Promise<HTMLElement[]> {
|
||||
return Array.from(
|
||||
document.querySelectorAll('[data-schema-picker], [data-schema-picker] *')
|
||||
) as HTMLElement[]
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape' && selected) {
|
||||
deselect()
|
||||
}
|
||||
}
|
||||
|
||||
function handleError(error: { type: string; error: any }) {
|
||||
if (error.type === 'delete') {
|
||||
sendUserToast(`Failed to delete capture: ${error.error}`, true)
|
||||
} else if (error.type === 'load') {
|
||||
sendUserToast(`Failed to load captures: ${error.error}`, true)
|
||||
}
|
||||
}
|
||||
|
||||
$: path && infiniteList && initLoadCaptures()
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeydown} />
|
||||
|
||||
<Label label="Trigger captures" {headless} class="h-full flex flex-col gap-1">
|
||||
<svelte:fragment slot="header">
|
||||
{#if addButton}
|
||||
<div class="inline-block">
|
||||
<CaptureButton small={true} on:openTriggers />
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="action">
|
||||
{#if canHavePreprocessor && !isEmpty}
|
||||
<div>
|
||||
<ToggleButtonGroup
|
||||
bind:selected={testKind}
|
||||
class="h-full"
|
||||
on:selected={(e) => {
|
||||
initLoadCaptures(e.detail)
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="main" label={isFlow ? 'Flow' : 'Main'} small />
|
||||
<ToggleButton
|
||||
value="preprocessor"
|
||||
label="Preprocessor"
|
||||
small
|
||||
tooltip="When the runnable has a preprocessor, it receives additional information about the request"
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<div
|
||||
class={fullHeight ? 'h-full' : capturesLength > 7 ? 'h-[300px]' : 'h-fit'}
|
||||
use:clickOutside={{ capture: false, exclude: getPropPickerElements }}
|
||||
on:click_outside={() => {
|
||||
if (firstClick) {
|
||||
firstClick = false
|
||||
return
|
||||
}
|
||||
deselect()
|
||||
}}
|
||||
>
|
||||
<InfiniteList
|
||||
bind:this={infiniteList}
|
||||
selectedItemId={selected}
|
||||
bind:isEmpty
|
||||
on:error={(e) => handleError(e.detail)}
|
||||
on:select={(e) => handleSelect(e.detail)}
|
||||
bind:length={capturesLength}
|
||||
>
|
||||
<svelte:fragment slot="columns">
|
||||
<colgroup>
|
||||
<col class="w-8" />
|
||||
<col class="w-20" />
|
||||
<col />
|
||||
</colgroup>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment let:item let:hover>
|
||||
{@const captureIcon = captureKindToIcon[item.trigger_kind]}
|
||||
<SchemaPickerRow
|
||||
date={item.created_at}
|
||||
payloadData={item.payloadData}
|
||||
selected={selected === item.id}
|
||||
hovering={hover}
|
||||
>
|
||||
<svelte:fragment slot="start">
|
||||
<div class="center-center">
|
||||
<svelte:component this={captureIcon} size={12} />
|
||||
</div>
|
||||
<Button
|
||||
size="xs2"
|
||||
color="light"
|
||||
variant="border"
|
||||
on:click={() => {
|
||||
copyToClipboard(JSON.stringify(payloadData))
|
||||
}}
|
||||
iconOnly
|
||||
startIcon={{ icon: Clipboard }}
|
||||
/>
|
||||
|
||||
{#if isFlow && testKind === 'main'}
|
||||
<CustomPopover>
|
||||
<Button
|
||||
size="xs"
|
||||
color="light"
|
||||
variant="border"
|
||||
on:click={() => {
|
||||
dispatch('updateSchema', { schema, redirect: true })
|
||||
}}
|
||||
wrapperClasses="h-full"
|
||||
>
|
||||
Apply schema
|
||||
</Button>
|
||||
|
||||
<svelte:fragment slot="overlay">
|
||||
{#if schema}
|
||||
<div class="min-w-[400px]">
|
||||
<SchemaViewer {schema} />
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</CustomPopover>
|
||||
{/if}
|
||||
|
||||
{#if testKind === 'preprocessor' && !hasPreprocessor}
|
||||
<CustomPopover noPadding>
|
||||
<Button
|
||||
size="xs"
|
||||
color="dark"
|
||||
disabled
|
||||
endIcon={{
|
||||
icon: Info
|
||||
}}
|
||||
wrapperClasses="h-full"
|
||||
>
|
||||
Apply args
|
||||
</Button>
|
||||
<svelte:fragment slot="overlay">
|
||||
<div class="text-sm p-2 flex flex-col gap-1 items-start">
|
||||
<p>You need to add a preprocessor to use preprocessor captures as args</p>
|
||||
<Button
|
||||
size="xs"
|
||||
color="dark"
|
||||
on:click={() => {
|
||||
dispatch('addPreprocessor')
|
||||
}}
|
||||
>
|
||||
Add preprocessor
|
||||
</Button>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</CustomPopover>
|
||||
{:else}
|
||||
<Button
|
||||
size="xs"
|
||||
color="dark"
|
||||
on:click={() => {
|
||||
if (isFlow && testKind === 'main') {
|
||||
dispatch('updateSchema', { schema, redirect: false })
|
||||
}
|
||||
dispatch('applyArgs', {
|
||||
kind: testKind,
|
||||
args: payloadData
|
||||
})
|
||||
}}
|
||||
disabled={testKind === 'preprocessor' && !hasPreprocessor}
|
||||
>
|
||||
{isFlow && testKind === 'main' ? 'Apply schema and args' : 'Apply args'}
|
||||
</Button>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="extra">
|
||||
{#if canEdit}
|
||||
<Button
|
||||
size="xs2"
|
||||
color="red"
|
||||
iconOnly
|
||||
startIcon={{ icon: Trash2 }}
|
||||
loading={deleteLoading === capture.id}
|
||||
on:click={() => {
|
||||
deleteCapture(capture.id)
|
||||
}}
|
||||
/>
|
||||
<div class="flex flex-row items-center gap-2 px-2">
|
||||
{#if testKind === 'preprocessor' && !hasPreprocessor}
|
||||
<CustomPopover noPadding>
|
||||
<Button
|
||||
size="xs2"
|
||||
color="dark"
|
||||
disabled
|
||||
endIcon={{
|
||||
icon: Info
|
||||
}}
|
||||
wrapperClasses="h-full"
|
||||
>
|
||||
Apply args
|
||||
</Button>
|
||||
<svelte:fragment slot="overlay">
|
||||
<div class="text-sm p-2 flex flex-col gap-1 items-start">
|
||||
<p> You need to add a preprocessor to use preprocessor captures as args </p>
|
||||
<Button
|
||||
size="xs2"
|
||||
color="dark"
|
||||
on:click={() => {
|
||||
dispatch('addPreprocessor')
|
||||
}}
|
||||
>
|
||||
Add preprocessor
|
||||
</Button>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</CustomPopover>
|
||||
{:else}
|
||||
<Button
|
||||
size="xs2"
|
||||
color={hover || selected === item.id ? 'dark' : 'light'}
|
||||
dropdownItems={[
|
||||
{
|
||||
label: 'Use as input schema',
|
||||
onClick: () => {
|
||||
dispatch('updateSchema', {
|
||||
payloadData: item.payloadData,
|
||||
redirect: true,
|
||||
args: true
|
||||
})
|
||||
},
|
||||
disabled: !isFlow || testKind !== 'main'
|
||||
}
|
||||
].filter((item) => !item.disabled)}
|
||||
on:click={() => {
|
||||
if (isFlow && testKind === 'main') {
|
||||
dispatch('testWithArgs', item.payloadData)
|
||||
} else {
|
||||
dispatch('applyArgs', {
|
||||
kind: testKind,
|
||||
args: item.payloadData
|
||||
})
|
||||
}
|
||||
}}
|
||||
disabled={testKind === 'preprocessor' && !hasPreprocessor}
|
||||
title={isFlow && testKind === 'main'
|
||||
? 'Test flow with args'
|
||||
: 'Apply args to preprocessor'}
|
||||
startIcon={isFlow && testKind === 'main' ? { icon: Play } : {}}
|
||||
>
|
||||
{isFlow && testKind === 'main' ? 'Test' : 'Apply args'}
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
size="xs2"
|
||||
color="light"
|
||||
variant="contained"
|
||||
iconOnly
|
||||
startIcon={{ icon: Trash2 }}
|
||||
loading={item.isDeleting}
|
||||
on:click={() => {
|
||||
infiniteList?.deleteItem(item.id)
|
||||
}}
|
||||
btnClasses="hover:text-white hover:bg-red-500 text-red-500"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</Label>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</SchemaPickerRow>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="empty">
|
||||
<div class="text-center text-xs text-tertiary">No captures yet</div>
|
||||
</svelte:fragment>
|
||||
</InfiniteList>
|
||||
</div>
|
||||
</Label>
|
||||
|
||||
@@ -82,9 +82,9 @@
|
||||
})
|
||||
await getCaptureConfigs()
|
||||
}
|
||||
captureTable?.refreshCaptures()
|
||||
i++
|
||||
await sleep(1000)
|
||||
captureTable?.loadCaptures(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,11 +99,6 @@
|
||||
topics: [''],
|
||||
group_id: `windmill_consumer-${$workspaceStore}-${path.replaceAll('/', '__')}`
|
||||
}
|
||||
} else if (captureType === 'http') {
|
||||
args = {
|
||||
route_path: '',
|
||||
http_method: 'post'
|
||||
}
|
||||
} else {
|
||||
args = {}
|
||||
}
|
||||
@@ -190,6 +185,7 @@
|
||||
on:captureToggle={() => {
|
||||
handleCapture()
|
||||
}}
|
||||
on:testWithArgs
|
||||
/>
|
||||
{:else if captureType === 'webhook'}
|
||||
<WebhooksConfigSection
|
||||
@@ -208,13 +204,13 @@
|
||||
on:captureToggle={() => {
|
||||
handleCapture()
|
||||
}}
|
||||
on:testWithArgs
|
||||
/>
|
||||
{:else if captureType === 'http'}
|
||||
<RouteEditorConfigSection
|
||||
{showCapture}
|
||||
can_write={true}
|
||||
bind:route_path={args.route_path}
|
||||
bind:http_method={args.http_method}
|
||||
bind:args
|
||||
headless
|
||||
{captureInfo}
|
||||
bind:captureTable
|
||||
@@ -224,6 +220,7 @@
|
||||
on:captureToggle={() => {
|
||||
handleCapture()
|
||||
}}
|
||||
on:testWithArgs
|
||||
/>
|
||||
{:else if captureType === 'email'}
|
||||
<EmailTriggerConfigSection
|
||||
@@ -242,6 +239,7 @@
|
||||
on:captureToggle={() => {
|
||||
handleCapture()
|
||||
}}
|
||||
on:testWithArgs
|
||||
/>
|
||||
{:else if captureType === 'kafka'}
|
||||
<KafkaTriggersConfigSection
|
||||
@@ -257,6 +255,7 @@
|
||||
on:captureToggle={() => {
|
||||
handleCapture()
|
||||
}}
|
||||
on:testWithArgs
|
||||
/>
|
||||
{:else if captureType === 'nats'}
|
||||
<NatsTriggersConfigSection
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import SchemaForm from '../SchemaForm.svelte'
|
||||
import CaptureSection, { type CaptureInfo } from './CaptureSection.svelte'
|
||||
import CaptureTable from './CaptureTable.svelte'
|
||||
|
||||
export let defaultValues: Record<string, any> | undefined = undefined
|
||||
export let headless: boolean = false
|
||||
export let args: Record<string, any> = {}
|
||||
@@ -159,6 +160,7 @@
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
on:testWithArgs
|
||||
bind:captureTable
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -91,6 +91,8 @@
|
||||
}}
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
on:updateSchema
|
||||
on:testWithArgs
|
||||
cloudDisabled={false}
|
||||
triggerType="kafka"
|
||||
{isFlow}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import CaptureTable from './CaptureTable.svelte'
|
||||
import ClipboardPanel from '../details/ClipboardPanel.svelte'
|
||||
|
||||
export let args: Record<string, any> = { route_path: '', http_method: 'get' }
|
||||
export let dirtyRoutePath: boolean = false
|
||||
export let route_path = ''
|
||||
export let http_method: 'get' | 'post' | 'put' | 'patch' | 'delete' = 'post'
|
||||
@@ -69,6 +70,14 @@
|
||||
$: isValid = routeError === ''
|
||||
|
||||
$: fullRoute = getHttpRoute(route_path)
|
||||
|
||||
$: showCapture && (http_method = 'post')
|
||||
|
||||
function updateArgs(route_path: string, http_method: string) {
|
||||
args && ((args.route_path = route_path), (args.http_method = http_method))
|
||||
}
|
||||
|
||||
$: updateArgs(route_path, http_method)
|
||||
</script>
|
||||
|
||||
<div>
|
||||
@@ -81,6 +90,7 @@
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
on:testWithArgs
|
||||
bind:captureTable
|
||||
>
|
||||
<Label label="URL">
|
||||
|
||||
@@ -166,6 +166,10 @@
|
||||
let drawer: Drawer
|
||||
|
||||
let dirtyPath = false
|
||||
|
||||
let args: Record<string, any> = { route_path: '' }
|
||||
|
||||
$: args && (route_path = args.route_path)
|
||||
</script>
|
||||
|
||||
{#if static_asset_config}
|
||||
@@ -221,6 +225,7 @@
|
||||
|
||||
<RouteEditorConfigSection
|
||||
bind:route_path
|
||||
bind:args
|
||||
bind:isValid
|
||||
bind:dirtyRoutePath
|
||||
bind:http_method
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
export let newItem: boolean = false
|
||||
export let isEditor: boolean = false
|
||||
export let canHavePreprocessor: boolean = false
|
||||
export let hasPreprocessor: boolean = false
|
||||
|
||||
let routeEditor: RouteEditor
|
||||
|
||||
@@ -71,12 +72,15 @@
|
||||
}}
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
on:updateSchema
|
||||
on:testWithArgs
|
||||
cloudDisabled={false}
|
||||
triggerType="http"
|
||||
{isFlow}
|
||||
{path}
|
||||
{isEditor}
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
{newItem}
|
||||
/>
|
||||
{#if !newItem}
|
||||
|
||||
@@ -47,125 +47,145 @@
|
||||
|
||||
<FlowCard {noEditor} title="Triggers">
|
||||
{#if !$simplifiedPoll}
|
||||
<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">HTTP</Tab>
|
||||
<Tab value="websockets" selectedClass="text-primary text-sm font-semibold">Websockets</Tab>
|
||||
<Tab value="kafka" otherValues={['nats']} selectedClass="text-primary text-sm font-semibold"
|
||||
>Event streams</Tab
|
||||
>
|
||||
<Tab value="emails" selectedClass="text-primary text-sm font-semibold">Email</Tab>
|
||||
{#if isFlow}
|
||||
<Tab value="scheduledPoll" selectedClass="text-primary text-sm font-semibold"
|
||||
>Scheduled Poll</Tab
|
||||
<div class="h-full flex flex-col">
|
||||
<Tabs bind:selected={$selectedTrigger} wrapperClass="overflow-hidden shrink-0">
|
||||
<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">HTTP</Tab>
|
||||
<Tab value="websockets" selectedClass="text-primary text-sm font-semibold">Websockets</Tab>
|
||||
<Tab
|
||||
value="kafka"
|
||||
otherValues={['nats']}
|
||||
selectedClass="text-primary text-sm font-semibold"
|
||||
>
|
||||
{/if}
|
||||
Event streams
|
||||
</Tab>
|
||||
<Tab value="emails" selectedClass="text-primary text-sm font-semibold">Email</Tab>
|
||||
{#if isFlow}
|
||||
<Tab value="scheduledPoll" selectedClass="text-primary text-sm font-semibold"
|
||||
>Scheduled Poll</Tab
|
||||
>
|
||||
{/if}
|
||||
|
||||
<svelte:fragment slot="content">
|
||||
{#if $selectedTrigger === 'webhooks'}
|
||||
<div class="p-4">
|
||||
<WebhooksPanel
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
scopes={isFlow ? [`run:flow/${currentPath}`] : [`run:script/${currentPath}`]}
|
||||
path={currentPath}
|
||||
{hash}
|
||||
{isFlow}
|
||||
args={{}}
|
||||
token=""
|
||||
{newItem}
|
||||
isEditor={true}
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
/>
|
||||
</div>
|
||||
{:else if $selectedTrigger === 'emails'}
|
||||
<div class="p-4">
|
||||
<EmailTriggerPanel
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
token=""
|
||||
scopes={isFlow ? [`run:flow/${currentPath}`] : [`run:script/${currentPath}`]}
|
||||
path={currentPath}
|
||||
{isFlow}
|
||||
isEditor={true}
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
{newItem}
|
||||
/>
|
||||
</div>
|
||||
{:else if $selectedTrigger === 'routes'}
|
||||
<div class="p-4">
|
||||
<RoutesPanel
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
{newItem}
|
||||
path={currentPath}
|
||||
{isFlow}
|
||||
isEditor={true}
|
||||
/>
|
||||
</div>
|
||||
{:else if $selectedTrigger === 'websockets'}
|
||||
<div class="p-4">
|
||||
<WebsocketTriggersPanel
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
{newItem}
|
||||
path={currentPath}
|
||||
{isFlow}
|
||||
isEditor={true}
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
/>
|
||||
</div>
|
||||
{:else if $selectedTrigger === 'kafka' || $selectedTrigger === 'nats'}
|
||||
<div class="p-4 flex flex-col gap-2">
|
||||
<ToggleButtonGroup bind:selected={eventStreamType}>
|
||||
<ToggleButton value="kafka" label="Kafka" icon={KafkaIcon} />
|
||||
<ToggleButton value="nats" label="NATS" icon={NatsIcon} />
|
||||
</ToggleButtonGroup>
|
||||
{#if eventStreamType === 'kafka'}
|
||||
<KafkaTriggersPanel
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
{newItem}
|
||||
path={currentPath}
|
||||
{isFlow}
|
||||
isEditor={true}
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
/>
|
||||
{:else if eventStreamType === 'nats'}
|
||||
<NatsTriggersPanel
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
{newItem}
|
||||
path={currentPath}
|
||||
{isFlow}
|
||||
isEditor={true}
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
/>
|
||||
<svelte:fragment slot="content">
|
||||
<div class="min-h-0 grow overflow-y-auto">
|
||||
{#if $selectedTrigger === 'webhooks'}
|
||||
<div class="p-4">
|
||||
<WebhooksPanel
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
on:updateSchema
|
||||
on:testWithArgs
|
||||
scopes={isFlow ? [`run:flow/${currentPath}`] : [`run:script/${currentPath}`]}
|
||||
path={currentPath}
|
||||
{hash}
|
||||
{isFlow}
|
||||
args={{}}
|
||||
token=""
|
||||
{newItem}
|
||||
isEditor={true}
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
/>
|
||||
</div>
|
||||
{:else if $selectedTrigger === 'emails'}
|
||||
<div class="p-4">
|
||||
<EmailTriggerPanel
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
on:updateSchema
|
||||
on:testWithArgs
|
||||
token=""
|
||||
scopes={isFlow ? [`run:flow/${currentPath}`] : [`run:script/${currentPath}`]}
|
||||
path={currentPath}
|
||||
{isFlow}
|
||||
isEditor={true}
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
{newItem}
|
||||
/>
|
||||
</div>
|
||||
{:else if $selectedTrigger === 'routes'}
|
||||
<div class="p-4">
|
||||
<RoutesPanel
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
on:updateSchema
|
||||
on:testWithArgs
|
||||
{newItem}
|
||||
path={currentPath}
|
||||
{isFlow}
|
||||
isEditor={true}
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
/>
|
||||
</div>
|
||||
{:else if $selectedTrigger === 'websockets'}
|
||||
<div class="p-4">
|
||||
<WebsocketTriggersPanel
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
on:updateSchema
|
||||
on:testWithArgs
|
||||
{newItem}
|
||||
path={currentPath}
|
||||
{isFlow}
|
||||
isEditor={true}
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
/>
|
||||
</div>
|
||||
{:else if $selectedTrigger === 'kafka' || $selectedTrigger === 'nats'}
|
||||
<div class="p-4 flex flex-col gap-2">
|
||||
<ToggleButtonGroup bind:selected={eventStreamType}>
|
||||
<ToggleButton value="kafka" label="Kafka" icon={KafkaIcon} />
|
||||
<ToggleButton value="nats" label="NATS" icon={NatsIcon} />
|
||||
</ToggleButtonGroup>
|
||||
{#if eventStreamType === 'kafka'}
|
||||
<KafkaTriggersPanel
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
on:updateSchema
|
||||
on:testWithArgs
|
||||
{newItem}
|
||||
path={currentPath}
|
||||
{isFlow}
|
||||
isEditor={true}
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
/>
|
||||
{:else if eventStreamType === 'nats'}
|
||||
<NatsTriggersPanel
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
{newItem}
|
||||
path={currentPath}
|
||||
{isFlow}
|
||||
isEditor={true}
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if $selectedTrigger === 'schedules'}
|
||||
<div class="p-4">
|
||||
<RunPageSchedules
|
||||
{schema}
|
||||
{isFlow}
|
||||
path={initialPath}
|
||||
{newItem}
|
||||
can_write={canWrite(currentPath, {}, $userStore)}
|
||||
/>
|
||||
</div>
|
||||
{:else if $selectedTrigger === 'scheduledPoll'}
|
||||
<div class="p-4">
|
||||
<ScheduledPollPanel />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if $selectedTrigger === 'schedules'}
|
||||
<div class="p-4">
|
||||
<RunPageSchedules
|
||||
{schema}
|
||||
{isFlow}
|
||||
path={initialPath}
|
||||
{newItem}
|
||||
can_write={canWrite(currentPath, {}, $userStore)}
|
||||
/>
|
||||
</div>
|
||||
{:else if $selectedTrigger === 'scheduledPoll'}
|
||||
<div class="p-4">
|
||||
<ScheduledPollPanel />
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="px-4 pb-2">
|
||||
<RunPageSchedules
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
import Section from '../Section.svelte'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import { type CaptureTriggerKind } from '$lib/gen'
|
||||
import type { FlowEditorContext } from '../flows/types'
|
||||
import type { TriggerContext } from '$lib/components/triggers'
|
||||
import TriggersWrapper from './TriggersWrapper.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
@@ -38,7 +37,16 @@
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const { flowStore, selectedId } = getContext<FlowEditorContext>('FlowEditorContext') || {}
|
||||
let showCapture = false
|
||||
let init = false
|
||||
$: updateShowCapture(!!$captureOn)
|
||||
function updateShowCapture(show: boolean) {
|
||||
if (show && !init) {
|
||||
$captureOn = undefined
|
||||
showCapture = true
|
||||
init = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Section label={captureTypeLabels[triggerType]}>
|
||||
@@ -48,16 +56,16 @@
|
||||
<Button
|
||||
size="xs2"
|
||||
on:click={() => {
|
||||
$captureOn = !$captureOn
|
||||
showCapture = !showCapture
|
||||
}}
|
||||
variant="border"
|
||||
color="light"
|
||||
endIcon={{
|
||||
icon: ChevronDown,
|
||||
classes: twMerge('transition', $captureOn ? 'rotate-180' : '')
|
||||
classes: twMerge('transition', showCapture ? 'rotate-180' : '')
|
||||
}}
|
||||
>
|
||||
Capture
|
||||
Test trigger
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
@@ -69,6 +77,7 @@
|
||||
{disabled}
|
||||
startIcon={{ icon: Save }}
|
||||
on:click={() => {
|
||||
console.log('saveTrigger', args)
|
||||
dispatch('saveTrigger', {
|
||||
config: args
|
||||
})
|
||||
@@ -101,17 +110,12 @@
|
||||
{canHavePreprocessor}
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
on:updateSchema={(e) => {
|
||||
const { schema, redirect } = e.detail
|
||||
$flowStore.schema = schema
|
||||
if (redirect) {
|
||||
$selectedId = 'Input'
|
||||
}
|
||||
}}
|
||||
on:updateSchema
|
||||
on:saveTrigger
|
||||
on:testWithArgs
|
||||
bind:args
|
||||
{data}
|
||||
showCapture={$captureOn}
|
||||
{showCapture}
|
||||
/>
|
||||
{:else}
|
||||
<TriggersWrapper {path} {isFlow} {triggerType} {cloudDisabled} {args} {data} />
|
||||
|
||||
@@ -41,13 +41,7 @@
|
||||
showCapture={false}
|
||||
/>
|
||||
{:else if triggerType === 'http'}
|
||||
<RouteEditorConfigSection
|
||||
showCapture={false}
|
||||
can_write={true}
|
||||
bind:route_path={args.route_path}
|
||||
bind:http_method={args.http_method}
|
||||
headless
|
||||
/>
|
||||
<RouteEditorConfigSection showCapture={false} can_write={true} bind:args headless />
|
||||
{:else if triggerType === 'email'}
|
||||
<EmailTriggerConfigSection
|
||||
hash={data?.hash}
|
||||
|
||||
@@ -262,6 +262,7 @@ done`
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
on:testWithArgs
|
||||
>
|
||||
<Label label="URL">
|
||||
<ClipboardPanel content={captureUrl} disabled={!captureInfo.active} />
|
||||
|
||||
@@ -41,6 +41,8 @@
|
||||
on:applyArgs
|
||||
on:addPreprocessor
|
||||
on:refreshCaptures
|
||||
on:updateSchema
|
||||
on:testWithArgs
|
||||
cloudDisabled={false}
|
||||
triggerType="webhook"
|
||||
{isFlow}
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
on:testWithArgs
|
||||
bind:captureTable
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -79,6 +79,8 @@
|
||||
wsTriggerEditor?.openNew(isFlow, path, e.detail.config)
|
||||
}}
|
||||
on:addPreprocessor
|
||||
on:updateSchema
|
||||
on:testWithArgs
|
||||
cloudDisabled={false}
|
||||
triggerType="websocket"
|
||||
{isFlow}
|
||||
|
||||
@@ -278,7 +278,7 @@
|
||||
name: rt.name,
|
||||
schema: rt.schema as any,
|
||||
description: rt.description ?? '',
|
||||
formatExtension: rt.format_extension,
|
||||
formatExtension: rt.format_extension
|
||||
}
|
||||
editResourceTypeDrawer.openDrawer?.()
|
||||
}
|
||||
@@ -436,14 +436,25 @@
|
||||
<Drawer bind:this={resourceTypeViewer} size="800px">
|
||||
<DrawerContent title={resourceTypeViewerObj.rt} on:close={resourceTypeViewer.closeDrawer}>
|
||||
<div>
|
||||
<h1 class="mb-8 mt-4"><IconedResourceType name={resourceTypeViewerObj.rt} formatExtension={resourceTypeViewerObj.formatExtension} /></h1>
|
||||
<h1 class="mb-8 mt-4"
|
||||
><IconedResourceType
|
||||
name={resourceTypeViewerObj.rt}
|
||||
formatExtension={resourceTypeViewerObj.formatExtension}
|
||||
/></h1
|
||||
>
|
||||
<div class="py-2 box prose mb-8 text-secondary">
|
||||
<GfmMarkdown md={resourceTypeViewerObj.description ?? ''} />
|
||||
</div>
|
||||
{#if resourceTypeViewerObj.formatExtension}
|
||||
<Alert type="info" title="Plain text file resource (.{resourceTypeViewerObj.formatExtension})">
|
||||
This resource type represents a plain text file with a <b>.{resourceTypeViewerObj.formatExtension}</b> extension. (e.g. <b>my_file.{resourceTypeViewerObj.formatExtension}</b>)
|
||||
</Alert>
|
||||
<Alert
|
||||
type="info"
|
||||
title="Plain text file resource (.{resourceTypeViewerObj.formatExtension})"
|
||||
>
|
||||
This resource type represents a plain text file with a <b
|
||||
>.{resourceTypeViewerObj.formatExtension}</b
|
||||
>
|
||||
extension. (e.g. <b>my_file.{resourceTypeViewerObj.formatExtension}</b>)
|
||||
</Alert>
|
||||
{:else}
|
||||
<SchemaViewer schema={resourceTypeViewerObj.schema} />
|
||||
{/if}
|
||||
@@ -486,11 +497,17 @@
|
||||
<div>
|
||||
{#if editResourceType.formatExtension}
|
||||
<Alert type="info" title="Plain text file resource (.{editResourceType.formatExtension})">
|
||||
This resource type represents a plain text file with a <b>.{editResourceType.formatExtension}</b> extension. (e.g. <b>my_file.{editResourceType.formatExtension}</b>). The schema only contains a `content` field and thus cannot be edited.
|
||||
This resource type represents a plain text file with a <b
|
||||
>.{editResourceType.formatExtension}</b
|
||||
>
|
||||
extension. (e.g. <b>my_file.{editResourceType.formatExtension}</b>). The schema only
|
||||
contains a `content` field and thus cannot be edited.
|
||||
</Alert>
|
||||
{:else}
|
||||
<div class="mb-1 font-semibold text-secondary">Schema</div>
|
||||
<EditableSchemaWrapper bind:schema={editResourceType.schema} noPreview />
|
||||
<div class="flex flex-col gap-2">
|
||||
<EditableSchemaWrapper bind:schema={editResourceType.schema} noPreview />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -751,7 +768,11 @@
|
||||
}
|
||||
}}
|
||||
>
|
||||
<IconedResourceType name={resource_type} after={true} formatExtension={resourceNameToFileExt(resource_type)}/>
|
||||
<IconedResourceType
|
||||
name={resource_type}
|
||||
after={true}
|
||||
formatExtension={resourceNameToFileExt(resource_type)}
|
||||
/>
|
||||
</a>
|
||||
</Cell>
|
||||
<Cell>
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
import { onDestroy, onMount, setContext } from 'svelte'
|
||||
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
|
||||
import FlowPreviewButtons from '$lib/components/flows/header/FlowPreviewButtons.svelte'
|
||||
import type { FlowEditorContext, FlowInput } from '$lib/components/flows/types'
|
||||
import type {
|
||||
FlowEditorContext,
|
||||
FlowInput,
|
||||
FlowInputEditorState
|
||||
} from '$lib/components/flows/types'
|
||||
import { writable } from 'svelte/store'
|
||||
import { OpenAPI, type FlowModule, type OpenFlow, type TriggersCount } from '$lib/gen'
|
||||
import { initHistory } from '$lib/history'
|
||||
@@ -84,7 +88,8 @@
|
||||
triggersCount: triggersCount,
|
||||
simplifiedPoll: writable(false),
|
||||
defaultValues: writable(undefined),
|
||||
captureOn: writable(undefined)
|
||||
captureOn: writable(undefined),
|
||||
showCaptureHint: writable(undefined)
|
||||
})
|
||||
|
||||
setContext<FlowEditorContext>('FlowEditorContext', {
|
||||
@@ -102,7 +107,12 @@
|
||||
flowInputsStore: writable<FlowInput>({}),
|
||||
customUi: {},
|
||||
insertButtonOpen: writable(false),
|
||||
executionCount: writable(0)
|
||||
executionCount: writable(0),
|
||||
flowInputEditorState: writable<FlowInputEditorState>({
|
||||
selectedTab: undefined,
|
||||
editPanelSize: undefined,
|
||||
payloadData: undefined
|
||||
})
|
||||
})
|
||||
setContext<PropPickerContext>('PropPickerContext', {
|
||||
flowPropPickerConfig: writable<FlowPropPickerConfig | undefined>(undefined),
|
||||
|
||||
Reference in New Issue
Block a user