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:
Guilhem
2025-01-13 14:45:38 +01:00
committed by GitHub
parent 96d31f9577
commit ebfde197fc
84 changed files with 4893 additions and 2179 deletions
@@ -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"
}
+34 -55
View File
@@ -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:
+10 -2
View File
@@ -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?;
+18 -2
View File
@@ -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)
+15 -13
View File
@@ -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>&nbsp; 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>&nbsp; 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>
+8 -2
View File
@@ -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>
+26 -8
View File
@@ -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>&nbsp; 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>&nbsp; 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>
+33 -66
View File
@@ -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" />
+6 -4
View File
@@ -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/>&nbsp;&nbsp;"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" />
+11 -1
View File
@@ -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'
+1
View File
@@ -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>
+13 -3
View File
@@ -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),