mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 00:02:19 +00:00
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:
@@ -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/> "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/> "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>
|
||||
@@ -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/> "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>
|
||||
@@ -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/> "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">
|
||||
|
||||
Reference in New Issue
Block a user