feat: history for flow inputs (#5117)

* feat (frontend): improve capture UI

* show schema diff

* add accept reject form

* add diff for first node input

* move update schema button

* change modified properties display

* propagate change through nested components

* allow modification of nested object in schema

* reject nested changes

* clean logic

* clean code

* clean

* nit

* fix first strep input

* fix argument preview

* clean

* restore indentation for nested objects

* ignore undefined field in diff computation

* fix run button disabled update

* nit

* fix update JSON

* fix deletion of nested components

* dark mode

* clean

* replace dropdown with sidebar

* only check type for compatibility

* add shadow

* disbale dnd on edit

* auto-scroll within schema

* fix nested not dnd object diff viewer

* fix captures drawer

* open edit tab on add new arg

* change button label when schema is the same

* ajust padding

* fix arg update

* fix oneof display

* propagate change event through nested schema

* handle oneOf

* fix preview arg sync

* handle s3 object

* fix arg sync

* update schema input compatibility

* clean compatible

* accept empty array items

* clean

* increase of schema args gap

* fix nested schema update

* allow number and int compatibility

* reset args when modifying schema

* open fields on add with addPropertyV2

* feat(frontend): use new saved inputs picker

* add object viewer to saved inputs row

* Display alert info on select saved or historic input

* Allow args edit when using saved/historic inputs

* replace input library drawers with panels

* add captures and saved inputs to arg picker

* polish ui

* nit

* clean

* add json arg editor

* fix escape for deselect input

* bind schema form valid

* nit

* handle big payloads

* nit

* nit

* validity check disabled in json view

* improve infinit list request

* fix json schema inferrence

* nit

* nit

* nit

* nit

* fix tipo

* handle large captures

* fix cancel button shift

* use sideTabBar has a trigger

* nit

* fix everything

* fix

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
This commit is contained in:
Guilhem
2025-01-30 15:30:56 +00:00
committed by GitHub
parent 343ff94f3c
commit cd440564d2
29 changed files with 957 additions and 428 deletions
+1 -1
View File
@@ -381,11 +381,11 @@
? 'border-2 border-green-500 bg-surface'
: ''
)}
data-schema-picker
>
{#if diffStatus && typeof diffStatus === 'object' && diffStatus.diff !== 'same'}
<div
class="absolute top-0 right-2 rounded-md rounded-t-none flex flex-row overflow-hidden bg-surface"
data-schema-picker
>
<button
class="p-1 bg-green-500 text-white hover:bg-green-600"
@@ -1,18 +0,0 @@
<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>
@@ -51,6 +51,7 @@
export let diff: Record<string, SchemaDiff> = {}
export let disableDnd: boolean = false
export let shouldDispatchChanges: boolean = false
export let isValid: boolean = true
const dispatch = createEventDispatcher()
@@ -303,6 +304,7 @@
dispatch('change', schema)
}}
{shouldDispatchChanges}
bind:isValid
/>
<slot name="runButton" />
@@ -1,32 +1,25 @@
<script lang="ts">
import { type Job, JobService, type Flow, type RestartedFrom, type OpenFlow } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { Badge, Button, Drawer, Popup } from './common'
import { Badge, Button, Popup } from './common'
import { createEventDispatcher, getContext } from 'svelte'
import type { FlowEditorContext } from './flows/types'
import { runFlowPreview } from './flows/utils'
import SchemaForm from './SchemaForm.svelte'
import SchemaFormWithArgPicker from './SchemaFormWithArgPicker.svelte'
import FlowStatusViewer from '../components/FlowStatusViewer.svelte'
import FlowProgressBar from './flows/FlowProgressBar.svelte'
import {
AlertTriangle,
ArrowRight,
CornerDownLeft,
Play,
RefreshCw,
X,
ArrowLeftIcon
} from 'lucide-svelte'
import { AlertTriangle, ArrowRight, CornerDownLeft, Play, RefreshCw, X } 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'
import InputSelectedBadge from './schema/InputSelectedBadge.svelte'
import Toggle from './Toggle.svelte'
import JsonInputs from './JsonInputs.svelte'
export let previewMode: 'upTo' | 'whole'
export let open: boolean
export let preventEscape = false
export let jobId: string | undefined = undefined
export let job: Job | undefined = undefined
@@ -39,6 +32,10 @@
let isRunning: boolean = false
let jobProgressReset: () => void
let jsonView: boolean = false
let jsonEditor: JsonInputs
let schemaHeight = 0
let isValid: boolean = true
export function test() {
renderCount++
@@ -83,6 +80,10 @@
const newFlow = extractFlow(previewMode)
jobId = await runFlowPreview(args, newFlow, $pathStore, restartedFrom)
isRunning = true
if (inputSelected) {
savedArgs = $previewArgs
inputSelected = undefined
}
} catch (e) {
sendUserToast('Could not run preview', true, undefined, e.toString())
isRunning = false
@@ -99,6 +100,14 @@
runPreview($previewArgs, undefined)
}
break
case 'Escape':
if (preventEscape) {
selectInput(undefined)
event.preventDefault()
event.stopPropagation
}
break
}
}
}
@@ -124,97 +133,40 @@
}
}
let savedArgs = $previewArgs
let inputSelected: 'captures' | 'history' | 'saved' | undefined = undefined
async function selectInput(input, type?: 'captures' | 'history' | 'saved' | undefined) {
if (!input) {
$previewArgs = savedArgs
inputSelected = undefined
setTimeout(() => {
preventEscape = false
}, 100)
} else {
$previewArgs = input
inputSelected = type
preventEscape = true
jsonEditor?.setCode(JSON.stringify($previewArgs ?? {}, null, '\t'))
}
}
export function refresh() {
renderCount++
}
$: if (job?.type === 'CompletedJob') {
isRunning = false
}
$: 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} />
<Drawer bind:this={inputLibraryDrawer}>
<DrawerContent title="Input library {initialPath}" on:close={inputLibraryDrawer?.toggleDrawer}>
<SavedInputs
flowPath={initialPath}
isValid={true}
args={$previewArgs}
on:selected_args={(e) => {
$previewArgs = JSON.parse(JSON.stringify(e.detail))
inputLibraryDrawer?.closeDrawer()
renderCount++
}}
/>
</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">
<div class="min-h-0 grow h-full">
<CapturesInputs
flowPath={$pathStore}
headless={false}
addButton={true}
on:select={(e) => {
selectedCapture = e.detail
}}
on:openTriggers={(e) => {
dispatch('openTriggers', e.detail)
captureLibraryDrawer?.closeDrawer()
}}
/>
</div>
<div class="w-full flex flex-col gap-2 px-2 py-2 h-[50%]">
<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>
</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="flex flex-col space-y-2 h-screen bg-surface px-4 py-2 w-full" id="flow-preview-content">
<div class="flex flex-row w-full items-center gap-x-2">
<div class="w-8">
<Button
on:click={() => dispatch('close')}
@@ -227,28 +179,30 @@
</div>
{#if isRunning}
<Button
color="red"
on:click={async () => {
isRunning = false
try {
jobId &&
(await JobService.cancelQueuedJob({
workspace: $workspaceStore ?? '',
id: jobId,
requestBody: {}
}))
} catch {}
}}
size="sm"
btnClasses="w-full max-w-lg"
loading={true}
clickableWhileLoading
>
Cancel
</Button>
<div class="mx-auto">
<Button
color="red"
on:click={async () => {
isRunning = false
try {
jobId &&
(await JobService.cancelQueuedJob({
workspace: $workspaceStore ?? '',
id: jobId,
requestBody: {}
}))
} catch {}
}}
size="sm"
btnClasses="w-full max-w-lg"
loading={true}
clickableWhileLoading
>
Cancel
</Button>
</div>
{:else}
<div class="flex flex-row gap-4">
<div class="grow justify-center flex flex-row gap-4">
{#if jobId !== undefined && selectedJobStep !== undefined && selectedJobStepIsTopLevel}
{#if selectedJobStepType == 'single'}
<Button
@@ -352,26 +306,6 @@
</Button>
</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"
size="sm"
variant="border"
on:click={() => {
inputLibraryDrawer?.openDrawer()
}}>Past runs/Input library</Button
>
{/if}
</div>
</div>
<div class="w-full flex flex-col gap-y-1">
{#if lastPreviewFlow && JSON.stringify($flowStore) != lastPreviewFlow}
@@ -387,17 +321,68 @@
<FlowProgressBar {job} bind:reset={jobProgressReset} />
</div>
<div class="overflow-y-auto grow flex flex-col pr-4">
<div class="overflow-y-auto grow flex flex-col pt-4">
<div class="border-b">
{#key renderCount}
<SchemaForm
noVariablePicker
compact
class="py-4 max-w-3xl"
schema={$flowStore.schema}
bind:args={$previewArgs}
/>
{/key}
<SchemaFormWithArgPicker
runnableId={initialPath == '' ? $pathStore : initialPath}
runnableType={'FlowPath'}
previewArgs={$previewArgs}
on:openTriggers
on:select={(e) => {
selectInput(e.detail.payload, e.detail?.type)
}}
{isValid}
{jsonView}
>
<div class="w-full flex flex-row justify-between">
<InputSelectedBadge {inputSelected} />
<div class="flex flex-row gap-2">
<Toggle
bind:checked={jsonView}
label="JSON View"
size="xs"
options={{
right: 'JSON',
rightTooltip: 'Fill args from JSON'
}}
lightMode
on:change={(e) => {
jsonEditor?.setCode(JSON.stringify($previewArgs ?? {}, null, '\t'))
refresh()
}}
/>
</div>
</div>
{#if jsonView}
<div class="py-2" style="height: {Math.max(schemaHeight, 100)}px" data-schema-picker>
<JsonInputs
bind:this={jsonEditor}
on:select={(e) => {
if (e.detail) {
$previewArgs = e.detail
}
}}
updateOnBlur={false}
placeholder={`Write args as JSON.<br/><br/>Example:<br/><br/>{<br/>&nbsp;&nbsp;"foo": "12"<br/>}`}
/>
</div>
{:else}
{#key renderCount}
<div bind:clientHeight={schemaHeight} class="min-h-[40vh]">
<SchemaForm
noVariablePicker
compact
schema={$flowStore.schema}
bind:args={$previewArgs}
on:change={() => {
savedArgs = $previewArgs
}}
bind:isValid
/>
</div>
{/key}
{/if}
</SchemaFormWithArgPicker>
</div>
<div class="pt-4 flex flex-col grow">
{#if jobId}
@@ -821,7 +821,7 @@
As a non logged in user, you can only see jobs ran by anonymous users like you
</Alert>
{:else if job}
<div class="flow-root w-full space-y-4 {wideResults ? '' : 'max-w-7xl'} mx-auto px-4">
<div class="flow-root w-full space-y-4 {wideResults ? '' : 'max-w-7xl px-4'} mx-auto">
<!-- {#if innerModules.length > 0 && true}
<h3 class="text-md leading-6 font-bold text-primay border-b pb-2">Flow result</h3>
{:else}
@@ -10,32 +10,19 @@
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
export let runnableId: string | undefined = undefined
export let runnableType: RunnableType | undefined = undefined
export let loading: boolean = false
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!,
@@ -49,6 +36,13 @@
const inputsWithPayload = await Promise.all(
inputs.map(async (input) => {
const payloadData = await loadArgsFromHistory(input.id, undefined, false)
if (payloadData === 'WINDMILL_TOO_BIG') {
return {
...input,
payloadData: 'WINDMILL_TOO_BIG',
getFullPayload: () => loadArgsFromHistory(input.id, undefined, true)
}
}
return {
...input,
payloadData
@@ -60,21 +54,24 @@
infiniteList?.setLoader(loadInputsPageFn)
}
function handleSelected(data: any) {
async function handleSelected(data: any) {
if (selected === data.id) {
selected = undefined
dispatch('select', undefined)
resetSelected(true)
return
}
selected = data.id
dispatch('select', data.payloadData)
if (data.payloadData === 'WINDMILL_TOO_BIG') {
const fullPayload = await data.getFullPayload?.()
dispatch('select', fullPayload)
} else {
dispatch('select', structuredClone(data.payloadData))
}
}
let selected: string | undefined = undefined
onDestroy(() => {
selected = undefined
dispatch('select', undefined)
resetSelected(true)
})
async function getPropPickerElements(): Promise<HTMLElement[]> {
@@ -100,8 +97,7 @@
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape' && selected) {
selected = undefined
dispatch('select', undefined)
resetSelected(true)
event.stopPropagation()
event.preventDefault()
}
@@ -115,14 +111,21 @@
let jobHovered: string | undefined = undefined
function refresh() {
export function refresh() {
if (infiniteList) {
infiniteList.loadData('refresh')
}
}
export function resetSelected(dispatchEvent?: boolean) {
selected = undefined
if (dispatchEvent) {
dispatch('select', undefined)
}
}
$: !loading && refresh()
$: $workspaceStore && (scriptHash || scriptPath || flowPath) && infiniteList && initLoadInputs()
$: $workspaceStore && runnableId && runnableType && infiniteList && initLoadInputs()
</script>
<svelte:window on:keydown={handleKeydown} />
@@ -153,19 +156,18 @@
use:clickOutside={{ capture: false, exclude: getPropPickerElements }}
on:click_outside={() => {
if (selected) {
selected = undefined
dispatch('select', undefined)
resetSelected(true)
}
}}
>
<div class="grow-0">
<div class="grow-0" data-schema-picker>
<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 class="w-16" />
<col />
</colgroup>
@@ -190,7 +192,7 @@
</DataTable>
</div>
<div class="min-h-0 grow">
<div class="min-h-0 grow" data-schema-picker>
<InfiniteList
bind:this={infiniteList}
selectedItemId={selected}
@@ -200,7 +202,7 @@
<svelte:fragment slot="columns">
<colgroup>
<col class="w-8" />
<col class="w-20" />
<col class="w-16" />
<col />
</colgroup>
</svelte:fragment>
@@ -213,7 +215,9 @@
/>
</svelte:fragment>
<svelte:fragment slot="empty">
<div class="text-center text-tertiary text-xs py-2">No previous Runs</div>
<div class="text-center text-tertiary text-xs py-2">
{runnableId ? 'No previous inputs' : 'Save draft to see previous runs'}
</div>
</svelte:fragment>
</InfiniteList>
</div>
@@ -56,10 +56,14 @@
isNew: false
}))
}
}, 1000)
}, 2000)
page = Math.floor(items.length / perPage) + 1
hasMore = items.length === perPage * (page - 1)
page = Math.ceil(items.length / perPage)
hasMore = items.length === perPage * page
if (hasMore) {
const potentialNewItems = await loadInputs(page + 1, perPage)
hasMore = potentialNewItems.length > 0
}
initLoad = true
isEmpty = items.length === 0
length = items.length
@@ -112,6 +116,7 @@
on:loadMore={() => {
loadData()
}}
{loading}
>
<slot name="columns" />
@@ -0,0 +1,70 @@
<script lang="ts">
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
import { createEventDispatcher } from 'svelte'
import { clickOutside } from '$lib/utils'
const dispatch = createEventDispatcher()
export let updateOnBlur = true
export let placeholder =
'Write a JSON payload. The input schema will be inferred.<br/><br/>Example:<br/><br/>{<br/>&nbsp;&nbsp;"foo": "12"<br/>}'
let pendingJson = ''
let simpleEditor: SimpleEditor | undefined = undefined
function updatePayloadFromJson(jsonInput: string) {
if (jsonInput === undefined || jsonInput === null || jsonInput.trim() === '') {
dispatch('select', undefined)
return
}
try {
const parsed = JSON.parse(jsonInput)
dispatch('select', parsed)
} catch (error) {
dispatch('select', undefined)
}
}
export function setCode(code: string) {
simpleEditor?.setCode(code)
}
async function getPropPickerElements(): Promise<HTMLElement[]> {
const elements = document.querySelectorAll('[data-schema-picker], [data-schema-picker] *')
return elements ? (Array.from(elements) as HTMLElement[]) : []
}
</script>
<div
class="h-full"
use:clickOutside={{ capture: false, exclude: getPropPickerElements }}
on:click_outside={() => {
if (updateOnBlur) {
updatePayloadFromJson('')
}
}}
>
<SimpleEditor
bind:this={simpleEditor}
on:focus={() => {
if (updateOnBlur) {
dispatch('focus')
updatePayloadFromJson(pendingJson)
}
}}
on:blur={async () => {
if (updateOnBlur) {
dispatch('blur')
}
}}
on:change={(e) => {
if (e.detail?.code !== undefined) {
updatePayloadFromJson(e.detail.code)
}
}}
bind:code={pendingJson}
lang="json"
class="h-full"
{placeholder}
/>
</div>
+40 -17
View File
@@ -18,6 +18,7 @@
import RunFormAdvancedPopup from './RunFormAdvancedPopup.svelte'
import { page } from '$app/stores'
import { replaceState } from '$app/navigation'
import JsonInputs from '$lib/components/JsonInputs.svelte'
export let runnable:
| {
@@ -54,9 +55,12 @@
export let overrideTag: string | undefined
export let args: Record<string, any> = {}
export let jsonView = false
let reloadArgs = 0
let blockPopupOpen = false
let jsonEditor: JsonInputs | undefined = undefined
let schemaHeight = 0
export async function setArgs(nargs: Record<string, any>) {
args = nargs
@@ -89,6 +93,10 @@
console.error('Impossible to set hash in args', e)
}
}
export function setCode(code: string) {
jsonEditor?.setCode(code)
}
</script>
<div class="max-w-3xl">
@@ -137,7 +145,7 @@
{#if topButton}
<Button
btnClasses="!px-6 !py-1 w-full"
disabled={!isValid}
disabled={!isValid || jsonView}
on:click={() => runAction(undefined, args, invisible_to_owner, overrideTag)}
>
{buttonText}
@@ -147,22 +155,37 @@
<div class="my-2" />
{#if !runnable.schema.properties || Object.keys(runnable.schema.properties).length === 0}
<div class="text-sm py-4 italic">No arguments</div>
{:else if jsonView}
<div class="py-2" style="height: {schemaHeight}px" data-schema-picker>
<JsonInputs
bind:this={jsonEditor}
on:select={(e) => {
if (e.detail) {
args = e.detail
}
}}
updateOnBlur={false}
placeholder={`Write args as JSON.<br/><br/>Example:<br/><br/>{<br/>&nbsp;&nbsp;"foo": "12"<br/>}`}
/>
</div>
{:else}
{#key reloadArgs}
<SchemaForm
helperScript={runnable.hash
? {
type: 'hash',
hash: runnable.hash
}
: undefined}
prettifyHeader
{noVariablePicker}
{autofocus}
schema={runnable.schema}
bind:isValid
bind:args
/>
<div bind:clientHeight={schemaHeight}>
<SchemaForm
helperScript={runnable.hash
? {
type: 'hash',
hash: runnable.hash
}
: undefined}
prettifyHeader
{noVariablePicker}
{autofocus}
schema={runnable.schema}
bind:isValid
bind:args
/>
</div>
{/key}
{/if}
{:else}
@@ -176,7 +199,7 @@
{loading}
color="dark"
btnClasses="!px-6 !py-1 !h-8 inline-flex gap-2"
disabled={!isValid}
disabled={!isValid || jsonView}
on:click={() => runAction(scheduledForStr, args, invisible_to_owner, overrideTag)}
shortCut={{ Icon: CornerDownLeft, hide: !viewKeybinding }}
>
@@ -230,7 +253,7 @@
{:else if !topButton}
<Button
btnClasses="!px-6 !py-1 w-full"
disabled={!isValid}
disabled={!isValid || jsonView}
on:click={() => runAction(undefined, args, invisible_to_owner, overrideTag)}
shortCut={{ Icon: CornerDownLeft, hide: !viewKeybinding }}
>
@@ -4,7 +4,7 @@
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 { Edit, Trash2, Save, Eye } from 'lucide-svelte'
import Toggle from './Toggle.svelte'
import { Cell } from './table/index'
import { clickOutside } from '$lib/utils'
@@ -12,15 +12,23 @@
import Popover from '$lib/components/Popover.svelte'
import InfiniteList from './InfiniteList.svelte'
import { twMerge } from 'tailwind-merge'
import PopoverV2 from '$lib/components/meltComponents/Popover.svelte'
import ObjectViewer from '$lib/components/propertyPicker/ObjectViewer.svelte'
export let flowPath: string | null = null
export let previewArgs: any = undefined
export let runnableId: string | undefined = undefined
export let runnableType: RunnableType | undefined = undefined
export let isValid: boolean = false
export let noButton: boolean = false
export let jsonView: boolean = false
interface EditableInput extends Input {
isEditing?: boolean
isSaving?: boolean
isNew?: boolean
isDeleting?: boolean
payloadData?: any
getFullPayload?: () => Promise<any>
}
let infiniteList: InfiniteList | null = null
@@ -30,10 +38,6 @@
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
@@ -57,13 +61,30 @@
function initLoadInputs() {
const loadInputsPageFn = async (page: number, perPage: number) => {
return await InputService.listInputs({
const inputs = await InputService.listInputs({
workspace: $workspaceStore!,
runnableId,
runnableType,
page,
perPage
})
const inputsWithPayload = await Promise.all(
inputs.map(async (input) => {
const payloadData = await loadLargeArgs(input.id, undefined, false)
if (payloadData === 'WINDMILL_TOO_BIG') {
return {
...input,
payloadData: 'WINDMILL_TOO_BIG',
getFullPayload: () => loadLargeArgs(input.id, undefined, true)
}
}
return {
...input,
payloadData
}
})
)
return inputsWithPayload
}
infiniteList?.setLoader(loadInputsPageFn)
@@ -95,20 +116,21 @@
if (input.isEditing) return
if (selectedInput === input.id) {
selectedInput = null
selectedArgs = undefined
dispatch('select', undefined)
resetSelected(true)
} else {
selectedInput = input.id
selectedArgs = await loadLargeArgs(input.id, true, false)
dispatch('select', selectedArgs)
if (input.payloadData === 'WINDMILL_TOO_BIG') {
const fullPayload = await input.getFullPayload?.()
dispatch('select', fullPayload)
} else {
selectedArgs = structuredClone(input.payloadData ?? {})
dispatch('select', selectedArgs)
}
}
}
onDestroy(() => {
selectedInput = null
selectedArgs = undefined
dispatch('select', undefined)
resetSelected(true)
})
async function getPropPickerElements(): Promise<HTMLElement[]> {
@@ -122,9 +144,7 @@
event.stopPropagation()
event.preventDefault()
} else if (event.key === 'Escape' && selectedInput) {
selectedInput = null
selectedArgs = undefined
dispatch('select', undefined)
resetSelected(true)
event.stopPropagation()
event.preventDefault()
}
@@ -151,42 +171,57 @@
}
}
$: $workspaceStore && flowPath && (infiniteList && initLoadInputs(), (draft = false))
export function refresh() {
infiniteList?.loadData('refresh')
}
export function resetSelected(dispatchEvent?: boolean) {
selectedInput = null
selectedArgs = undefined
if (dispatchEvent) {
dispatch('select', undefined)
}
}
$: $workspaceStore &&
runnableId &&
runnableType &&
(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"
class="w-full flex flex-col gap-1 h-full overflow-y-auto"
use:clickOutside={{ capture: false, exclude: getPropPickerElements }}
on:click_outside={() => {
selectedInput = null
selectedArgs = undefined
dispatch('select', undefined)
resetSelected(true)
}}
>
<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 !noButton}
<div>
<Popover class="w-full" placement="bottom" disablePopup={runnableId && previewArgs}>
<svelte:fragment slot="text">
{#if !runnableId}
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 || !runnableId || !isValid || jsonView}
on:update={() => {
refresh()
}}
showTooltip={true}
/>
</Popover>
</div>
{/if}
<div class="grow min-h-0" data-schema-picker>
{#if !draft}
<InfiniteList
bind:this={infiniteList}
@@ -208,9 +243,9 @@
</Cell>
<Cell>
<div
class="w-full flex items-center text-sm justify-between gap-4 px-4 text-left transition-all"
class="w-full flex items-center text-sm justify-between gap-4 py-1 text-left transition-all"
>
<div class="w-full h-full items-center justify-between flex gap-1 min-w-0 p-1">
<div class="w-full h-full items-center justify-between flex gap-1 min-w-0">
{#if isEditing && isEditing.id === item.id}
<form
on:submit={() => {
@@ -230,6 +265,25 @@
{/if}
{#if item.created_by == $userStore?.username || $userStore?.is_admin || $userStore?.is_super_admin}
<div class="items-center flex gap-2">
<PopoverV2 displayArrow={false} closeButton={false} openOnHover={true}>
<svelte:fragment slot="trigger">
<Eye class="w-4 h-4 group-hover:block hidden" />
</svelte:fragment>
<svelte:fragment slot="content">
<div class="p-2">
{#if item.payloadData === 'WINDMILL_TOO_BIG'}
<div class="text-center text-tertiary text-xs">
Payload too big to preview but can still be loaded
</div>
{:else}
<div class="max-w-60 overflow-auto">
<ObjectViewer json={item.payloadData} />
</div>
{/if}
</div>
</svelte:fragment>
</PopoverV2>
{#if !isEditing || isEditing?.id !== item.id}
<div class="group-hover:block hidden -my-2">
<Toggle
@@ -0,0 +1,99 @@
<script lang="ts">
import SavedInputsPicker from '$lib/components/SavedInputsPicker.svelte'
import { createEventDispatcher } from 'svelte'
import HistoricInputs from '$lib/components/HistoricInputs.svelte'
import { type RunnableType } from '$lib/gen/types.gen.js'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import Section from '$lib/components/Section.svelte'
import SaveInputsButton from '$lib/components/SaveInputsButton.svelte'
const dispatch = createEventDispatcher()
export let scriptHash: string | null = null
export let scriptPath: string | null = null
export let flowPath: string | null = null
export let inputSelected: 'saved' | 'history' | undefined = undefined
export let jsonView: boolean = false
// Are the current Inputs valid and able to be saved?
export let isValid: boolean
export let args: object
let savedArgs: any = undefined
let runnableType: RunnableType | undefined = undefined
let savedInputsPicker: SavedInputsPicker | undefined = undefined
let historicInputs: HistoricInputs | undefined = undefined
$: runnableId = scriptHash || scriptPath || flowPath || undefined
$: runnableType = scriptHash
? 'ScriptHash'
: scriptPath
? 'ScriptPath'
: flowPath
? 'FlowPath'
: undefined
function selectArgs(selected_args: any, type: 'saved' | 'history' | undefined) {
if (selected_args) {
if (!inputSelected) {
savedArgs = args
}
inputSelected = type
dispatch('selected_args', selected_args)
} else if (savedArgs) {
inputSelected = type
dispatch('selected_args', savedArgs)
}
}
</script>
<div class="min-w-[300px] h-full flex flex-col">
<Splitpanes horizontal={true}>
<Pane class="px-4 py-2 h-full">
<Section
label="Saved Inputs"
tooltip="Shared inputs are available to anyone with access to the script"
wrapperClass="h-full"
small={true}
>
<svelte:fragment slot="action">
<SaveInputsButton
{args}
disabled={!isValid || jsonView}
{runnableId}
{runnableType}
on:update={() => {
savedInputsPicker?.refresh()
}}
/>
</svelte:fragment>
<SavedInputsPicker
bind:this={savedInputsPicker}
previewArgs={args}
{runnableId}
{runnableType}
{isValid}
on:select={(e) => {
if (e.detail) historicInputs?.resetSelected()
selectArgs(e.detail, e.detail ? 'saved' : undefined)
}}
noButton={true}
/>
</Section>
</Pane>
<Pane class="px-4 py-4 h-full">
<Section label="History" wrapperClass="h-full" small={true}>
<HistoricInputs
bind:this={historicInputs}
{runnableId}
{runnableType}
on:select={(e) => {
if (e.detail) savedInputsPicker?.resetSelected()
selectArgs(e.detail, e.detail ? 'history' : undefined)
}}
/>
</Section>
</Pane>
</Splitpanes>
</div>
@@ -0,0 +1,160 @@
<script lang="ts">
import { Pane, Splitpanes } from 'svelte-splitpanes'
import FlowInputEditor from '$lib/components/flows/content/FlowInputEditor.svelte'
import HistoricInputs from '$lib/components/HistoricInputs.svelte'
import SideBarTab from '$lib/components/meltComponents/SideBarTab.svelte'
import { History, Save } from 'lucide-svelte'
import CaptureIcon from '$lib/components/triggers/CaptureIcon.svelte'
import CaptureButton from './triggers/CaptureButton.svelte'
import SavedInputsPicker from './SavedInputsPicker.svelte'
import { createEventDispatcher } from 'svelte'
import CaptureTable from './triggers/CaptureTable.svelte'
import RefreshButton from './common/button/RefreshButton.svelte'
export let runnableId: string = ''
export let runnableType: any
export let previewArgs: any
export let isValid: boolean = true
export let jsonView: boolean = false
const dispatch = createEventDispatcher()
const getDropdownItems = () => {
return [
{
label: 'History',
onClick: () => {
if (selectedTab === 'history') {
selectedTab = undefined
rightPanelOpen = false
} else {
selectedTab = 'history'
rightPanelOpen = true
}
},
icon: History,
selected: selectedTab === 'history'
},
{
label: 'Saved inputs',
onClick: () => {
if (selectedTab === 'saved_inputs') {
selectedTab = undefined
rightPanelOpen = false
} else {
selectedTab = 'saved_inputs'
rightPanelOpen = true
}
},
icon: Save,
selected: selectedTab === 'saved_inputs'
},
{
label: 'Trigger captures',
onClick: () => {
if (selectedTab === 'captures') {
selectedTab = undefined
rightPanelOpen = false
} else {
selectedTab = 'captures'
rightPanelOpen = true
}
},
icon: CaptureIcon,
selected: selectedTab === 'captures'
}
]
}
let rightHeight = 0
let selectedTab: 'history' | 'saved_inputs' | 'captures' | undefined = undefined
let dropdownItems: any
let rightPanelOpen = false
let loading = false
let historicInputs: HistoricInputs | undefined = undefined
$: selectedTab, (dropdownItems = getDropdownItems())
</script>
<div class="h-fit">
<Splitpanes class={!rightPanelOpen ? 'splitter-hidden' : ''}>
<Pane class="!overflow-visible" size={70} minSize={30}>
<div class="relative w-full h-fit pr-12 pb-4 overflow-hidden" bind:clientHeight={rightHeight}>
<div class="absolute right-[1px] -top-[1px] z-50">
<SideBarTab
expandRight={rightPanelOpen}
{dropdownItems}
fullMenu={true}
noTrigger={true}
/>
</div>
<slot />
</div>
</Pane>
<Pane minSize={rightPanelOpen ? 30 : 0}>
{#if rightPanelOpen}
<div style="height: {rightHeight}px" class="border-t border-r pb-2">
{#if selectedTab === 'history'}
<FlowInputEditor title="History">
<svelete:fragment slot="action">
<div class="center-center">
<RefreshButton {loading} on:click={() => historicInputs?.refresh()} />
</div>
</svelete:fragment>
<HistoricInputs
bind:loading
bind:this={historicInputs}
{runnableId}
{runnableType}
on:select={(e) => {
dispatch('select', { payload: e.detail, type: 'history' })
}}
/>
</FlowInputEditor>
{:else if selectedTab === 'saved_inputs'}
<FlowInputEditor title="Saved inputs">
<SavedInputsPicker
{isValid}
{runnableId}
{runnableType}
{previewArgs}
on:select={(e) => {
dispatch('select', { payload: e.detail, type: 'saved' })
}}
{jsonView}
/>
</FlowInputEditor>
{:else if selectedTab === 'captures'}
<FlowInputEditor title="Trigger captures">
<svelete:fragment slot="action">
<div class="center-center">
<CaptureButton on:openTriggers small={true} />
</div>
</svelete:fragment>
<div class="h-full">
<CaptureTable
path={runnableId}
on:select={(e) => {
dispatch('select', { payload: e.detail, type: 'captures' })
}}
isFlow={true}
headless={true}
addButton={false}
/>
</div>
</FlowInputEditor>
{/if}
</div>
{/if}
</Pane>
</Splitpanes>
</div>
<style>
:global(.splitter-hidden .splitpanes__splitter) {
background-color: transparent !important;
border: none !important;
opacity: 0 !important;
}
</style>
+2 -1
View File
@@ -9,13 +9,14 @@
export let documentationLink: string | undefined = undefined
export let eeOnly = false
export let small: boolean = false
export let wrapperClass: string = ''
export let collapsable: boolean = false
export let collapsed: boolean = true
export let headless: boolean = false
</script>
<div class="w-full flex flex-col">
<div class={twMerge('w-full flex flex-col', wrapperClass)}>
{#if !headless}
<div class="flex flex-row justify-between items-center mb-2">
<h2
@@ -1,5 +1,5 @@
<script lang="ts">
import type { AlertType } from './model'
import { type AlertType, classes } from './model'
import Tooltip from '$lib/components/Tooltip.svelte'
import {
AlertCircle,
@@ -38,34 +38,6 @@
success: CheckCircle2
}
const classes: Record<AlertType, Record<string, string>> = {
info: {
bgClass: 'bg-blue-50 border-blue-200 border dark:bg-blue-900/40 dark:border-blue-700/40',
iconClass: 'text-blue-500 dark:text-blue-300',
titleClass: 'text-blue-800 dark:text-blue-100',
descriptionClass: 'text-blue-700 dark:text-blue-100'
},
warning: {
bgClass:
'bg-yellow-50 border-yellow-200 border dark:bg-yellow-900/40 dark:border-yellow-700/40',
iconClass: 'text-yellow-500 dark:text-yellow-300/90',
titleClass: 'text-yellow-800 dark:text-yellow-100/90',
descriptionClass: 'text-yellow-700 dark:text-yellow-100/90'
},
error: {
bgClass: 'bg-red-50 border-red-200 border dark:bg-red-900/40 dark:border-red-700/40',
iconClass: 'text-red-500 dark:text-red-300/90',
titleClass: 'text-red-800 dark:text-red-100/90',
descriptionClass: 'text-red-700 dark:text-red-100/90'
},
success: {
bgClass: 'bg-green-50 border-green-200 border dark:bg-green-900/40 dark:border-green-700/40',
iconClass: 'text-green-500 dark:text-green-300/90',
titleClass: 'text-green-800 dark:text-green-100/90',
descriptionClass: 'text-green-700 dark:text-green-100/90'
}
}
function toggleCollapse() {
if (collapsible) {
isCollapsed = !isCollapsed
@@ -1 +1,29 @@
export type AlertType = 'success' | 'error' | 'warning' | 'info'
export const classes: Record<AlertType, Record<string, string>> = {
info: {
bgClass: 'bg-blue-50 border-blue-200 border dark:bg-blue-900/40 dark:border-blue-700/40',
iconClass: 'text-blue-500 dark:text-blue-300',
titleClass: 'text-blue-800 dark:text-blue-100',
descriptionClass: 'text-blue-700 dark:text-blue-100'
},
warning: {
bgClass:
'bg-yellow-50 border-yellow-200 border dark:bg-yellow-900/40 dark:border-yellow-700/40',
iconClass: 'text-yellow-500 dark:text-yellow-300/90',
titleClass: 'text-yellow-800 dark:text-yellow-100/90',
descriptionClass: 'text-yellow-700 dark:text-yellow-100/90'
},
error: {
bgClass: 'bg-red-50 border-red-200 border dark:bg-red-900/40 dark:border-red-700/40',
iconClass: 'text-red-500 dark:text-red-300/90',
titleClass: 'text-red-800 dark:text-red-100/90',
descriptionClass: 'text-red-700 dark:text-red-100/90'
},
success: {
bgClass: 'bg-green-50 border-green-200 border dark:bg-green-900/40 dark:border-green-700/40',
iconClass: 'text-green-500 dark:text-green-300/90',
titleClass: 'text-green-800 dark:text-green-100/90',
descriptionClass: 'text-green-700 dark:text-green-100/90'
}
}
@@ -9,6 +9,7 @@
export let open = false
export let id = (Math.random() + 1).toString(36).substring(10)
export let preventEscape = false
if (open) {
openedDrawers.push(id)
@@ -63,7 +64,10 @@
if (open) {
switch (event.key) {
case 'Escape':
if (id == openedDrawers[openedDrawers.length - 1] || openedDrawers.length == 0) {
if (
(id == openedDrawers[openedDrawers.length - 1] || openedDrawers.length == 0) &&
!preventEscape
) {
openedDrawers.pop()
event.preventDefault()
event.stopPropagation()
@@ -11,6 +11,7 @@
export let alwaysOpen = false
export let shouldUsePortal: boolean = true
export let offset: number = 0
export let preventEscape = false
let disposable: Disposable | undefined = undefined
@@ -71,6 +72,7 @@
bind:this={disposable}
on:open
on:close
{preventEscape}
>
<aside
class="drawer windmill-app windmill-drawer {$$props.class ?? ''} {$$props.positionClass ??
@@ -28,7 +28,7 @@
<div class="flex flex-col h-full">
<Tabs bind:selected wrapperClass="flex-none w-full">
<Tab value="saved_inputs">Saved Inputs</Tab>
<Tab value="saved_inputs">Inputs library</Tab>
{#if !isOperator}
<Tab value="triggers">Triggers</Tab>
{/if}
@@ -5,7 +5,7 @@
import FlowCard from '../common/FlowCard.svelte'
import type { FlowEditorContext } from '../types'
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
import JsonInputs from '$lib/components/JsonInputs.svelte'
import { convert } from '@redocly/json-to-json-schema'
import { sendUserToast } from '$lib/toast'
import EditableSchemaForm from '$lib/components/EditableSchemaForm.svelte'
@@ -29,7 +29,6 @@
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 CaptureButton from '$lib/components/triggers/CaptureButton.svelte'
import {
@@ -42,6 +41,7 @@
applyDiff
} from '$lib/components/schema/schemaUtils'
import SideBarTab from '$lib/components/meltComponents/SideBarTab.svelte'
import CaptureTable from '$lib/components/triggers/CaptureTable.svelte'
export let noEditor: boolean
export let disabled: boolean
@@ -49,7 +49,6 @@
const { flowStore, previewArgs, pathStore, initialPath, flowInputEditorState } =
getContext<FlowEditorContext>('FlowEditorContext')
let pendingJson: string
let addProperty: AddPropertyV2 | undefined = undefined
let previewSchema: Record<string, any> | undefined = undefined
let payloadData: Record<string, any> | undefined = undefined
@@ -65,6 +64,8 @@
let runDisabled: boolean = false
let editableSchemaForm: EditableSchemaForm | undefined = undefined
let savedPreviewArgs: Record<string, any> | undefined = undefined
let preventEscape = false
let isValid = true
function updateEditPanelSize(size: number | undefined) {
if (!$flowInputEditorState) return
@@ -244,19 +245,6 @@
previewArguments = structuredClone(payloadData)
}
function updatePayloadFromJson(jsonInput: string) {
if (jsonInput === undefined || jsonInput === null || jsonInput.trim() === '') {
updatePreviewSchemaAndArgs(undefined)
return
}
try {
const parsed = JSON.parse(jsonInput)
updatePreviewSchemaAndArgs(parsed)
} catch (error) {
updatePreviewSchemaAndArgs(undefined)
}
}
let tabButtonWidth = 0
let connectFirstNode: () => void = () => {}
@@ -325,7 +313,7 @@
<!-- Add svelte:window to listen for keyboard events -->
<svelte:window on:keydown={handleKeydown} />
<Drawer bind:open={previewOpen} alwaysOpen size="75%">
<Drawer bind:open={previewOpen} alwaysOpen size="75%" {preventEscape}>
<FlowPreviewContent
bind:this={flowPreviewContent}
open={previewOpen}
@@ -333,6 +321,7 @@
on:close={() => {
previewOpen = false
}}
bind:preventEscape
/>
</Drawer>
@@ -378,6 +367,7 @@
on:schemaChange={() => {
resetArgs()
}}
bind:isValid
>
<svelte:fragment slot="openEditTab">
<div class={twMerge('flex flex-row divide-x', ButtonType.ColorVariants.blue.divider)}>
@@ -470,9 +460,8 @@
}}
>
<HistoricInputs
scriptHash={null}
scriptPath={null}
flowPath={$pathStore}
runnableId={initialPath ?? undefined}
runnableType={$pathStore ? 'FlowPath' : undefined}
on:select={(e) => {
updatePreviewSchemaAndArgs(e.detail ?? undefined)
}}
@@ -490,12 +479,17 @@
<CaptureButton on:openTriggers small={true} />
</div>
</svelete:fragment>
<CapturesInputs
on:select={(e) => {
updatePreviewSchemaAndArgs(e.detail ?? undefined)
}}
flowPath={$pathStore}
/>
<div class="h-full">
<CaptureTable
path={$pathStore}
on:select={(e) => {
updatePreviewSchemaAndArgs(e.detail ?? undefined)
}}
isFlow={true}
headless={true}
addButton={false}
/>
</div>
</FlowInputEditor>
{:else if $flowInputEditorState?.selectedTab === 'savedInputs'}
<FlowInputEditor
@@ -505,7 +499,8 @@
title="Saved inputs"
>
<SavedInputsPicker
flowPath={initialPath}
runnableId={initialPath ?? undefined}
runnableType={$pathStore ? 'FlowPath' : undefined}
on:select={(e) => {
updatePreviewSchemaAndArgs(e.detail ?? undefined)
}}
@@ -513,6 +508,7 @@
preventEnter = e.detail
}}
previewArgs={previewArguments}
{isValid}
/>
</FlowInputEditor>
{:else if $flowInputEditorState?.selectedTab === 'json'}
@@ -522,26 +518,16 @@
}}
title="Json payload"
>
<SimpleEditor
<JsonInputs
on:focus={() => {
preventEnter = true
updatePayloadFromJson(pendingJson)
}}
on:blur={async () => {
preventEnter = false
setTimeout(() => {
if (payloadData) {
updatePayloadFromJson('')
}
}, 100)
}}
on:change={(e) => {
updatePayloadFromJson(e.detail.code)
on:select={(e) => {
updatePreviewSchemaAndArgs(e.detail ?? undefined)
}}
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'}
@@ -575,7 +561,7 @@
<Button
color="dark"
btnClasses="w-fit"
disabled={runDisabled}
disabled={runDisabled || !isValid}
size="xs"
shortCut={{ Icon: CornerDownLeft, hide: false }}
on:click={() => {
@@ -17,6 +17,7 @@
export async function openPreview(test: boolean = false) {
if (!previewOpen) {
previewOpen = true
flowPreviewContent?.refresh()
if (!test) return
}
flowPreviewContent?.test()
@@ -27,6 +28,7 @@
let flowPreviewContent: FlowPreviewContent
let jobId: string | undefined = undefined
let job: Job | undefined = undefined
let preventEscape = false
$: upToDisabled =
$selectedId == undefined ||
@@ -79,6 +81,9 @@
on:click={() => {
previewMode = 'whole'
previewOpen = !previewOpen
if (previewOpen) {
flowPreviewContent?.refresh()
}
}}
startIcon={{ icon: Play }}
id="flow-editor-test-flow"
@@ -86,7 +91,7 @@
Test flow
</Button>
<Drawer bind:open={previewOpen} alwaysOpen size="75%">
<Drawer bind:open={previewOpen} alwaysOpen size="75%" {preventEscape}>
<FlowPreviewContent
bind:this={flowPreviewContent}
open={previewOpen}
@@ -100,5 +105,6 @@
previewOpen = false
dispatch('openTriggers', e.detail)
}}
bind:preventEscape
/>
</Drawer>
@@ -7,6 +7,8 @@
export let closeButton: boolean = true
export let displayArrow: boolean = false
export let placement: any = 'bottom'
export let disablePopup: boolean = false
export let openOnHover: boolean = false
const {
elements: { trigger, content, arrow, close },
@@ -22,7 +24,14 @@
$: sync.open(open, (v) => (open = v))
</script>
<button class="w-full h-full" type="button" use:melt={$trigger} aria-label="Popup button">
<button
class="w-full h-full"
type="button"
use:melt={$trigger}
aria-label="Popup button"
on:mouseenter={() => (openOnHover ? (open = true) : null)}
on:mouseleave={() => (openOnHover ? (open = false) : null)}
>
<slot name="trigger" />
</button>
@@ -1,4 +1,6 @@
<script lang="ts">
import { twMerge } from 'tailwind-merge'
export let dropdownItems: Array<{
label: string
onClick: () => void
@@ -7,6 +9,8 @@
}> = []
export let fullMenu: boolean = false
export let noTrigger: boolean = false
export let expandRight = false
let open = false
let timeout: NodeJS.Timeout | null = null
@@ -25,21 +29,31 @@
}
open = false
}
let hasCloseButton = $$slots['close button']
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="flex flex-col relative !overflow-visible"
class="flex flex-col relative !overflow-visible {expandRight ? 'pr-2 -mr-2' : ''}"
on:mouseenter={handleMouseEnter}
on:mouseleave={handleMouseLeave}
>
<slot name="close button" />
{#if hasCloseButton}
<slot name="close button" />
{:else}
<div class="w-[31px]" />
{/if}
{#if fullMenu}
<div
class="absolute flex-col top-[30px] left-0 z-50 bg-surface border-l border-b {open
? 'rounded-md rounded-tl-none overflow-hidden shadow-md'
: 'rounded-bl-md'}"
class={twMerge(
'absolute flex-col left-0 z-50 bg-surface border-l border-b overflow-hidden',
hasCloseButton ? 'top-[30px]' : 'top-[1px]',
open ? 'rounded-md rounded-tl-none shadow-md' : 'rounded-bl-md',
hasCloseButton ? '' : 'rounded-tl-md border-t',
noTrigger && !dropdownItems.some((item) => item.selected) ? 'rounded-md border' : ''
)}
>
{#each dropdownItems as item}
<button
@@ -0,0 +1,30 @@
<script lang="ts">
import { classes } from '$lib/components/common/alert/model'
import { twMerge } from 'tailwind-merge'
import Button from '$lib/components/common/button/Button.svelte'
import { X } from 'lucide-svelte'
export let inputSelected: 'history' | 'captures' | 'saved' | undefined = undefined
</script>
<div class="min-h-[34px]">
<div
class="rounded-md flex flex-row gap-2 items-center py-1 px-2 w-fit {classes['info']
.bgClass} {inputSelected ? '' : 'hidden'}"
>
<p class={twMerge(classes['info'].descriptionClass, 'text-xs px-2')}>
Using {inputSelected === 'history'
? 'historic'
: inputSelected === 'captures'
? 'captures'
: 'saved'} input arguments
</p>
<Button
color="light"
size="xs2"
startIcon={{ icon: X }}
shortCut={{ key: 'esc', withoutModifier: true }}
nonCaptureEvent
/>
</div>
</div>
@@ -19,6 +19,7 @@
export let shouldDispatchChanges: boolean = false
export let diff: Record<string, SchemaDiff> = {}
export let nestedClasses = ''
export let isValid: boolean = true
const dispatch = createEventDispatcher()
const flipDurationMs = 200
@@ -98,6 +99,7 @@
{diff}
{nestedParent}
{shouldDispatchChanges}
bind:isValid
>
<svelte:fragment slot="actions">
{#if !disableDnd}
@@ -5,7 +5,7 @@
import { twMerge } from 'tailwind-merge'
import Cell from '$lib/components/table/Cell.svelte'
export let payloadData: Record<string, any>
export let payloadData: Record<string, any> | string
export let date: string | undefined
export let selected = false
export let hovering = false
@@ -58,7 +58,7 @@
<Cell
wrap
class="text-2xs font-normal text-left p-2 rounded-md overflow-auto text-ellipsis scrollbar-none"
class="text-2xs font-normal text-left py-2 rounded-md overflow-auto text-ellipsis scrollbar-none"
title={formatDate(date)}
>
{formatDateShort(date)}
@@ -82,7 +82,15 @@
{JSON.stringify(payloadData)}
</div>
<svelte:fragment slot="overlay">
<ObjectViewer json={payloadData} />
{#if payloadData === 'WINDMILL_TOO_BIG'}
<div class="text-center text-tertiary text-xs">
Payload too big to preview but can still be loaded
</div>
{:else}
<div class="max-w-60 overflow-auto">
<ObjectViewer json={payloadData} />
</div>
{/if}
</svelte:fragment>
</CustomPopover>
@@ -31,7 +31,7 @@
let tableHeight: number = 0
const dispatch = createEventDispatcher()
let tableContainer: HTMLDivElement
let isLoading = false
export let loading = false
setContext<DatatableContext>('datatable', {
size
@@ -40,23 +40,19 @@
$: contentHeight = tableHeight - footerHeight
function checkScrollStatus() {
if (!infiniteScroll || isLoading) return
if (!infiniteScroll || loading) 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) {
if (!infiniteScroll || loading) {
if (loading) {
const checkAgain = () => {
if (!isLoading) {
if (!loading) {
handleScroll()
}
}
@@ -67,15 +63,11 @@
const { scrollTop, scrollHeight, clientHeight } = tableContainer
if (scrollHeight - (scrollTop + clientHeight) < 50) {
isLoading = true
dispatch('loadMore')
setTimeout(() => {
isLoading = false
}, 1000)
}
}
$: if (tableContainer && hasMore && !isLoading) {
$: if (tableContainer && hasMore && !loading) {
checkScrollStatus()
}
</script>
@@ -7,7 +7,6 @@
import CustomPopover from '../CustomPopover.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, onDestroy } from 'svelte'
import { type TriggerKind } from '../triggers'
import { CaptureService } from '$lib/gen'
@@ -15,9 +14,10 @@
import { type CaptureTriggerKind } from '$lib/gen'
import CaptureButton from '$lib/components/triggers/CaptureButton.svelte'
import InfiniteList from '../InfiniteList.svelte'
import { sendUserToast } from '$lib/utils'
import { isObject, sendUserToast } from '$lib/utils'
import SchemaPickerRow from '$lib/components/schema/SchemaPickerRow.svelte'
import { clickOutside } from '$lib/utils'
import type { Capture } from '$lib/gen'
export let path: string
export let hasPreprocessor = false
@@ -57,6 +57,11 @@
testWithArgs: any
}>()
interface CaptureWithPayload extends Capture {
getFullCapture?: () => Promise<any>
payloadData?: any
}
export async function loadCaptures(refresh: boolean = false) {
await infiniteList?.loadData(refresh ? 'refresh' : 'loadMore')
}
@@ -71,19 +76,36 @@
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 }
let capturesWithPayload: CaptureWithPayload[] = captures.map((capture) => {
let newCapture: CaptureWithPayload = { ...capture }
if (capture.payload === 'WINDMILL_TOO_BIG') {
newCapture = {
...capture,
getFullCapture: () =>
CaptureService.getCapture({
workspace: $workspaceStore!,
id: capture.id
})
}
}
const trigger_extra = isObject(capture.trigger_extra) ? capture.trigger_extra : {}
newCapture.payloadData =
testKind === 'preprocessor'
? capture.payload === 'WINDMILL_TOO_BIG'
? {
payload: capture.payload,
...trigger_extra
}
: typeof capture.payload === 'object'
? {
...capture.payload,
...trigger_extra
}
: trigger_extra
: capture.payload
return newCapture
})
return capturesWithPayload
}
@@ -98,15 +120,33 @@
infiniteList?.setDeleteItemFn(deleteInputFn)
}
function handleSelect(capture: any) {
async function handleSelect(capture: any) {
if (selected === capture.id) {
deselect()
} else {
const payloadData = await getPayload(capture)
selected = capture.id
dispatch('select', capture.payloadData)
dispatch('select', structuredClone(payloadData))
}
}
async function getPayload(capture: any) {
let payloadData = {}
if (capture.getFullCapture) {
const fullCapture = await capture.getFullCapture()
payloadData =
testKind === 'preprocessor'
? {
...(typeof fullCapture.payload === 'object' ? fullCapture.payload : {}),
...(typeof fullCapture.trigger_extra === 'object' ? fullCapture.trigger_extra : {})
}
: fullCapture.payload
} else {
payloadData = structuredClone(capture.payloadData)
}
return payloadData
}
function deselect() {
selected = undefined
dispatch('select', undefined)
@@ -209,7 +249,7 @@
<svelte:fragment slot="columns">
<colgroup>
<col class="w-8" />
<col class="w-20" />
<col class="w-16" />
<col />
</colgroup>
</svelte:fragment>
@@ -265,9 +305,10 @@
dropdownItems={[
{
label: 'Use as input schema',
onClick: () => {
onClick: async () => {
const payloadData = await getPayload(item)
dispatch('updateSchema', {
payloadData: item.payloadData,
payloadData,
redirect: true,
args: true
})
@@ -275,20 +316,23 @@
disabled: !isFlow || testKind !== 'main'
}
].filter((item) => !item.disabled)}
on:click={() => {
on:click={async () => {
const payloadData = await getPayload(item)
if (isFlow && testKind === 'main') {
dispatch('testWithArgs', item.payloadData)
dispatch('testWithArgs', payloadData)
} else {
dispatch('applyArgs', {
kind: testKind,
args: item.payloadData
args: payloadData
})
}
}}
disabled={testKind === 'preprocessor' && !hasPreprocessor}
title={isFlow && testKind === 'main'
? 'Test flow with args'
: 'Apply args to preprocessor'}
: testKind === 'preprocessor'
? 'Apply args to preprocessor'
: 'Apply args to inputs'}
startIcon={isFlow && testKind === 'main' ? { icon: Play } : {}}
>
{isFlow && testKind === 'main' ? 'Test' : 'Apply args'}
@@ -22,7 +22,7 @@
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
import { sendUserToast } from '$lib/toast'
import DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte'
import SavedInputs from '$lib/components/SavedInputs.svelte'
import SavedInputsV2 from '$lib/components/SavedInputsV2.svelte'
import {
FolderOpen,
Archive,
@@ -60,10 +60,12 @@
import json from 'svelte-highlight/languages/json'
import { writable } from 'svelte/store'
import TriggersBadge from '$lib/components/graph/renderers/triggers/TriggersBadge.svelte'
import InputSelectedBadge from '$lib/components/schema/InputSelectedBadge.svelte'
import WebsocketTriggersPanel from '$lib/components/triggers/websocket/WebsocketTriggersPanel.svelte'
import KafkaTriggersPanel from '$lib/components/triggers/kafka/KafkaTriggersPanel.svelte'
import NatsTriggersPanel from '$lib/components/triggers/nats/NatsTriggersPanel.svelte'
import PostgresTriggersPanel from '$lib/components/triggers/postgres/PostgresTriggersPanel.svelte'
import Toggle from '$lib/components/Toggle.svelte'
let flow: Flow | undefined
let can_write = false
@@ -74,6 +76,8 @@
let scheduledForStr: string | undefined = undefined
let invisible_to_owner: boolean | undefined = undefined
let overrideTag: string | undefined = undefined
let inputSelected: 'saved' | 'history' | undefined = undefined
let jsonView = false
const triggersCount = writable<TriggersCount | undefined>(undefined)
@@ -430,7 +434,7 @@
<svelte:fragment slot="form">
<div class="flex-col flex h-full justify-between">
<div class="p-8 w-full max-w-3xl mx-auto gap-2 bg-surface">
<div class="flex flex-col gap-2 mb-8">
<div class="mb-1">
{#if !emptyString(flow?.description)}
<GfmMarkdown md={defaultIfEmptyString(flow?.description, 'No description')} />
{/if}
@@ -443,20 +447,41 @@
</Badge>
{/if}
<RunForm
bind:scheduledForStr
bind:invisible_to_owner
bind:overrideTag
viewKeybinding
{loading}
autofocus
detailed={false}
bind:isValid
runnable={flow}
runAction={runFlow}
bind:args
bind:this={runForm}
/>
<div class="flex flex-col align-left">
<div class="flex flex-row justify-between">
<InputSelectedBadge {inputSelected} />
<Toggle
bind:checked={jsonView}
label="JSON View"
size="xs"
options={{
right: 'JSON',
rightTooltip: 'Fill args from JSON'
}}
lightMode
on:change={(e) => {
runForm?.setCode(JSON.stringify(args ?? {}, null, '\t'))
}}
/>
</div>
<RunForm
bind:scheduledForStr
bind:invisible_to_owner
bind:overrideTag
viewKeybinding
{loading}
autofocus
detailed={false}
bind:isValid
runnable={flow}
runAction={runFlow}
bind:args
bind:this={runForm}
{jsonView}
/>
</div>
<div class="py-10" />
{#if !emptyString(flow.summary)}
@@ -499,13 +524,14 @@
</div>
</svelte:fragment>
<svelte:fragment slot="save_inputs">
<SavedInputs
<SavedInputsV2
{jsonView}
flowPath={flow?.path}
{isValid}
args={args ?? {}}
bind:inputSelected
on:selected_args={(e) => {
const nargs = JSON.parse(JSON.stringify(e.detail))
runForm?.setArgs(nargs)
args = nargs
}}
/>
@@ -18,12 +18,7 @@
} from '$lib/utils'
import Tooltip from '$lib/components/Tooltip.svelte'
import ShareModal from '$lib/components/ShareModal.svelte'
import {
enterpriseLicense,
hubBaseUrlStore,
userStore,
workspaceStore
} from '$lib/stores'
import { enterpriseLicense, hubBaseUrlStore, userStore, workspaceStore } from '$lib/stores'
import { isDeployable, ALL_DEPLOYABLE } from '$lib/utils_deployable'
import { onDestroy } from 'svelte'
@@ -46,7 +41,7 @@
import { sendUserToast } from '$lib/toast'
import DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte'
import SavedInputs from '$lib/components/SavedInputs.svelte'
import SavedInputsV2 from '$lib/components/SavedInputsV2.svelte'
import WebhooksPanel from '$lib/components/triggers/webhook/WebhooksPanel.svelte'
import DetailPageLayout from '$lib/components/details/DetailPageLayout.svelte'
import DetailPageHeader from '$lib/components/details/DetailPageHeader.svelte'
@@ -92,6 +87,8 @@
import KafkaTriggersPanel from '$lib/components/triggers/kafka/KafkaTriggersPanel.svelte'
import NatsTriggersPanel from '$lib/components/triggers/nats/NatsTriggersPanel.svelte'
import PostgresTriggersPanel from '$lib/components/triggers/postgres/PostgresTriggersPanel.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import InputSelectedBadge from '$lib/components/schema/InputSelectedBadge.svelte'
let script: Script | undefined
let topHash: string | undefined
@@ -104,6 +101,8 @@
let scheduledForStr: string | undefined = undefined
let invisible_to_owner: boolean | undefined = undefined
let overrideTag: string | undefined = undefined
let inputSelected: 'saved' | 'history' | undefined = undefined
let jsonView = false
$: cliCommand = `wmill script run ${script?.path} -d '${JSON.stringify(args)}'`
@@ -599,7 +598,7 @@
</svelte:fragment>
<svelte:fragment slot="form">
<div class="p-8 w-full max-w-3xl mx-auto">
<div class="flex flex-col gap-0.5 mb-4">
<div class="flex flex-col gap-0.5 mb-1">
{#if script.lock_error_logs || topHash || script.archived || script.deleted}
<div class="flex flex-col gap-2 my-2">
{#if script.lock_error_logs}
@@ -651,21 +650,41 @@
</Badge>
{/if}
<RunForm
bind:scheduledForStr
bind:invisible_to_owner
bind:overrideTag
viewKeybinding
loading={runLoading}
autofocus
detailed={false}
bind:isValid
runnable={script}
runAction={runScript}
bind:args
schedulable={true}
bind:this={runForm}
/>
<div class="flex flex-col align-left">
<div class="flex flex-row justify-between">
<InputSelectedBadge {inputSelected} />
<Toggle
bind:checked={jsonView}
label="JSON View"
size="xs"
options={{
right: 'JSON',
rightTooltip: 'Fill args from JSON'
}}
lightMode
on:change={(e) => {
runForm?.setCode(JSON.stringify(args ?? {}, null, '\t'))
}}
/>
</div>
<RunForm
bind:scheduledForStr
bind:invisible_to_owner
bind:overrideTag
viewKeybinding
loading={runLoading}
autofocus
detailed={false}
bind:isValid
runnable={script}
runAction={runScript}
bind:args
schedulable={true}
bind:this={runForm}
{jsonView}
/>
</div>
<div class="py-10" />
{#if !emptyString(script.summary)}
@@ -695,14 +714,15 @@
</svelte:fragment>
<svelte:fragment slot="save_inputs">
{#if args}
<SavedInputs
<SavedInputsV2
scriptPath={script?.path}
scriptHash={topHash}
{isValid}
{jsonView}
{args}
bind:inputSelected
on:selected_args={(e) => {
const nargs = JSON.parse(JSON.stringify(e.detail))
runForm?.setArgs(nargs)
args = nargs
}}
/>
@@ -737,6 +757,7 @@
<svelte:fragment slot="postgres">
<div class="p-2">
<PostgresTriggersPanel path={script.path ?? ''} isFlow={false} />
</div>
</svelte:fragment>
<svelte:fragment slot="nats">
<div class="p-2">