fix(frontend): fix test step behavior (#6427)

* fix flowStateStore val

* handle run preview multiple keyboard actions

* Synchronise input args and prview args

* Fix arg update one step load

* fix input ste manually not reseted after preview

* rename test steps to stepsInputArgs

* simplify job result update

* fix job preview logic

* fix import

* nit

* clean

* fix test job not displaying when data is pinned

* remove job history loader display delay

* nit

* nit

* add error handler to steps input args comparison function

* prevent result node to display connection
This commit is contained in:
Guilhem
2025-08-25 12:49:46 +00:00
committed by GitHub
parent 082312000f
commit fc20b7bd91
18 changed files with 328 additions and 319 deletions
+5 -4
View File
@@ -48,7 +48,7 @@
import type { FlowPropPickerConfig, PropPickerContext } from './prop_picker'
import type { PickableProperties } from './flows/previousResults'
import { Triggers } from './triggers/triggers.svelte'
import { TestSteps } from './flows/testSteps.svelte'
import { StepsInputArgs } from './flows/stepsInputArgs.svelte'
import { ModulesTestStates } from './modulesTest.svelte'
import type { GraphModuleState } from './graph'
@@ -457,7 +457,7 @@
const scriptEditorDrawer = writable(undefined)
const moving = writable<{ id: string } | undefined>(undefined)
const history = initHistory(flowStore.val)
const testSteps = new TestSteps()
const stepsInputArgs = new StepsInputArgs()
const selectedIdStore = writable('settings-metadata')
const triggersCount = writable<TriggersCount | undefined>(undefined)
const modulesTestStates = new ModulesTestStates((moduleId) => {
@@ -481,7 +481,7 @@
pathStore: writable(''),
flowStateStore,
flowStore,
testSteps,
stepsInputArgs,
saveDraft: () => {},
initialPathStore: writable(''),
fakeInitialPath: '',
@@ -806,7 +806,7 @@
noEditor
on:applyArgs={(ev) => {
if (ev.detail.kind === 'preprocessor') {
testSteps.setStepArgs('preprocessor', ev.detail.args ?? {})
stepsInputArgs.setStepArgs('preprocessor', ev.detail.args ?? {})
$selectedIdStore = 'preprocessor'
} else {
previewArgsStore.val = ev.detail.args ?? {}
@@ -818,6 +818,7 @@
isOwner={flowPreviewContent?.getIsOwner()}
{suspendStatus}
onOpenDetails={flowPreviewButtons?.openPreview}
previewOpen={flowPreviewButtons?.getPreviewOpen()}
/>
{/key}
</Pane>
@@ -77,7 +77,7 @@
} from './triggers/utils'
import DraftTriggersConfirmationModal from './common/confirmationModal/DraftTriggersConfirmationModal.svelte'
import { Triggers } from './triggers/triggers.svelte'
import { TestSteps } from './flows/testSteps.svelte'
import { StepsInputArgs } from './flows/stepsInputArgs.svelte'
import { aiChatManager } from './copilot/chat/AIChatManager.svelte'
import type { GraphModuleState } from './graph'
import {
@@ -571,7 +571,7 @@
payloadData: undefined
})
const testSteps = new TestSteps()
const stepsInputArgs = new StepsInputArgs()
function select(selectedId: string) {
selectedIdStore.set(selectedId)
@@ -592,7 +592,7 @@
flowStateStore,
flowStore,
pathStore,
testSteps,
stepsInputArgs,
saveDraft,
initialPathStore,
fakeInitialPath,
@@ -1129,6 +1129,8 @@
bind:this={flowPreviewButtons}
{loading}
onRunPreview={() => {
// Reset manually edited args inputs when running a preview
stepsInputArgs.resetManuallyEditedArgs()
modulesTestStates.hideJobsInGraph()
localModuleStates = {}
showJobStatus = true
@@ -1170,7 +1172,7 @@
{newFlow}
on:applyArgs={(ev) => {
if (ev.detail.kind === 'preprocessor') {
testSteps.setStepArgs('preprocessor', ev.detail.args ?? {})
stepsInputArgs.setStepArgs('preprocessor', ev.detail.args ?? {})
$selectedIdStore = 'preprocessor'
}
}}
@@ -1218,6 +1220,7 @@
delete modulesTestStates.states[id]
}}
{flowHasChanged}
previewOpen={flowPreviewButtons?.getPreviewOpen()}
/>
{:else}
<CenteredPage>Loading...</CenteredPage>
@@ -140,7 +140,7 @@
jobId = await runFlowPreview(args, newFlow, $pathStore, restartedFrom)
isRunning = true
if (inputSelected) {
savedArgs = previewArgs.val
savedArgs = $state.snapshot(previewArgs.val)
inputSelected = undefined
}
onRunPreview?.()
@@ -166,7 +166,7 @@
if (preventEscape) {
selectInput(undefined)
event.preventDefault()
event.stopPropagation
event.stopPropagation()
}
break
}
@@ -506,7 +506,7 @@
schema={flowStore.val.schema}
bind:args={previewArgs.val}
on:change={() => {
savedArgs = previewArgs.val
savedArgs = $state.snapshot(previewArgs.val)
}}
bind:isValid
helperScript={flowStore.val.schema?.['x-windmill-dyn-select-code'] &&
@@ -18,6 +18,7 @@
noEditor?: boolean
scriptProgress?: any
focusArg?: string
onJobDone?: () => void
}
let {
@@ -28,7 +29,8 @@
testIsLoading = $bindable(false),
noEditor = false,
scriptProgress = $bindable(undefined),
focusArg = undefined
focusArg = undefined,
onJobDone
}: Props = $props()
const { flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
@@ -46,6 +48,7 @@
bind:testIsLoading
bind:scriptProgress
bind:this={moduleTest}
{onJobDone}
/>
<div class="p-4">
@@ -32,7 +32,7 @@
focusArg = undefined
}: Props = $props()
const { testSteps, flowStateStore, flowStore, previewArgs } =
const { stepsInputArgs, flowStateStore, flowStore, previewArgs } =
getContext<FlowEditorContext>('FlowEditorContext')
let inputCheck: { [id: string]: boolean } = $state({})
@@ -45,12 +45,12 @@
let lkeys = Object.keys(schema?.properties ?? {})
if (schema?.properties && JSON.stringify(lkeys) != JSON.stringify(keys)) {
keys = lkeys
untrack(() => testSteps?.removeExtraKey(mod.id, keys))
untrack(() => stepsInputArgs?.removeExtraKey(mod.id, keys))
}
})
function plugIt(argName: string) {
testSteps?.setEvaluatedStepArg(
stepsInputArgs?.setEvaluatedStepArg(
mod.id,
argName,
$state.snapshot(evalValue(argName, mod, pickableProperties, true))
@@ -102,8 +102,8 @@
$effect.pre(() => {
if (!initialized) {
if (testSteps) {
testSteps?.updateStepArgs(mod.id, flowStateStore.val, flowStore?.val, previewArgs?.val)
if (stepsInputArgs) {
stepsInputArgs?.updateStepArgs(mod.id, flowStateStore.val, flowStore?.val, previewArgs?.val)
initialized = true
}
}
@@ -130,8 +130,8 @@
label={argName}
description={schema.properties[argName].description}
bind:value={
() => testSteps?.getStepInputArgs(mod.id, argName),
(v) => testSteps?.setStepInputArgs(mod.id, argName, v)
() => stepsInputArgs?.getStepInputArgs(mod.id, argName),
(v) => stepsInputArgs?.setStepInputArgs(mod.id, argName, v)
}
type={schema.properties[argName].type}
oneOf={schema.properties[argName].oneOf}
@@ -152,7 +152,7 @@
placeholder={schema.properties[argName].placeholder}
/>
{/if}
{#if testSteps?.isArgManuallySet(mod.id, argName)}
{#if stepsInputArgs?.isArgManuallySet(mod.id, argName)}
<div class="pt-6 mt-0.5">
<Button
on:click={() => {
@@ -7,7 +7,7 @@
import { type Script, type Job, type FlowModule } from '$lib/gen'
import OutputPickerInner from '$lib/components/flows/propPicker/OutputPickerInner.svelte'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import type { FlowEditorContext } from './flows/types'
import type { FlowEditorContext, OutputViewerJob } from './flows/types'
import { getContext } from 'svelte'
import { getStringError } from './copilot/chat/utils'
import AiAgentLogViewer from './AIAgentLogViewer.svelte'
@@ -17,9 +17,8 @@
editor: Editor | undefined
diffEditor: DiffEditor | undefined
loopStatus?: { type: 'inside' | 'self'; flow: 'forloopflow' | 'whileloopflow' } | undefined
lastJob?: Job | undefined
testJob?: Job & { result_stream?: string }
scriptProgress?: number | undefined
testJob?: Job | undefined
mod: FlowModule
testIsLoading?: boolean
disableMock?: boolean
@@ -34,7 +33,6 @@
editor,
diffEditor,
loopStatus = undefined,
lastJob = undefined,
scriptProgress = $bindable(undefined),
testJob = undefined,
mod,
@@ -46,21 +44,20 @@
tagLabel = undefined
}: Props = $props()
const { testSteps } = getContext<FlowEditorContext>('FlowEditorContext')
const { stepsInputArgs } = getContext<FlowEditorContext>('FlowEditorContext')
let selectedJob: Job | undefined = $state(undefined)
let preview: 'mock' | 'job' | undefined = $state(undefined)
let jobProgressReset: () => void = $state(() => {})
$effect(() => {
if (preview != undefined && testJob) {
preview = undefined
}
})
let forceJson = $state(false)
let outputPickerInner: OutputPickerInner | undefined = $state(undefined)
export function getOutputPickerInner() {
return outputPickerInner
}
const selectedJob: OutputViewerJob = $derived.by(
() => outputPickerInner?.getSelectedJob?.() ?? undefined
)
const logJob = $derived(testJob ?? selectedJob)
const preview = $derived.by(() => outputPickerInner?.getPreview?.())
</script>
<Splitpanes horizontal>
@@ -75,7 +72,6 @@
{/if}
<OutputPickerInner
{lastJob}
{testJob}
fullResult
moduleId={mod.id}
@@ -83,24 +79,22 @@
getLogs
{onUpdateMock}
mock={mod.mock}
bind:forceJson
bind:selectedJob
isLoading={testIsLoading || loadingJob}
bind:preview
path={`path` in mod.value ? mod.value.path : ''}
{loopStatus}
{disableMock}
{disableHistory}
bind:this={outputPickerInner}
>
{#snippet copilot_fix()}
{#if lang && editor && diffEditor && testSteps.getStepArgs(mod.id) && selectedJob?.type === 'CompletedJob' && !selectedJob.success && getStringError(selectedJob.result)}
{#if lang && editor && diffEditor && stepsInputArgs.getStepArgs(mod.id) && selectedJob?.type === 'CompletedJob' && !selectedJob.success && getStringError(selectedJob.result)}
<ScriptFix {lang} />
{/if}
{/snippet}
</OutputPickerInner>
</Pane>
<Pane size={35} minSize={10}>
{#if (mod.mock?.enabled && preview != 'job') || preview == 'mock'}
{#if (mod.mock?.enabled && preview !== 'job' && testJob?.type !== 'QueuedJob') || preview === 'mock'}
<LogViewer
small
content={undefined}
@@ -124,9 +118,9 @@
jobId={logJob?.id}
duration={logJob?.['duration_ms']}
mem={logJob?.['mem_peak']}
content={logJob?.logs}
content={logJob?.['logs']}
isLoading={(testIsLoading && logJob?.['running'] == false) || loadingJob}
tag={logJob?.tag}
tag={logJob?.['tag']}
{tagLabel}
/>
{/if}
@@ -14,6 +14,7 @@
testIsLoading?: boolean
noEditor?: boolean
scriptProgress?: any
onJobDone?: () => void
}
let {
@@ -21,10 +22,11 @@
testJob = $bindable(undefined),
testIsLoading = $bindable(false),
noEditor = false,
scriptProgress = $bindable(undefined)
scriptProgress = $bindable(undefined),
onJobDone
}: Props = $props()
const { flowStore, flowStateStore, pathStore, testSteps, previewArgs, modulesTestStates } =
const { flowStore, flowStateStore, pathStore, stepsInputArgs, previewArgs, modulesTestStates } =
getContext<FlowEditorContext>('FlowEditorContext')
let jobLoader: JobLoader | undefined = $state(undefined)
@@ -32,12 +34,12 @@
let stepHistoryLoader = getStepHistoryLoaderContext()
export function runTestWithStepArgs() {
runTest(testSteps.getStepArgs(mod.id))
runTest(stepsInputArgs.getStepArgs(mod.id))
}
export function loadArgsAndRunTest() {
testSteps?.updateStepArgs(mod.id, flowStateStore.val, flowStore?.val, previewArgs?.val)
runTest(testSteps.getStepArgs(mod.id))
stepsInputArgs?.updateStepArgs(mod.id, flowStateStore.val, flowStore?.val, previewArgs?.val)
runTest(stepsInputArgs.getStepArgs(mod.id))
}
export async function runTest(args: any) {
@@ -138,6 +140,7 @@
if (modulesTestStates.states[mod.id]) {
modulesTestStates.states[mod.id].testJob = testJob
}
onJobDone?.()
}
export function cancelJob() {
@@ -58,6 +58,7 @@
suspendStatus?: StateStore<Record<string, { job: Job; nb: number }>>
onDelete?: (id: string) => void
flowHasChanged?: boolean
previewOpen: boolean
}
let {
@@ -91,7 +92,8 @@
job,
suspendStatus,
onDelete,
flowHasChanged
flowHasChanged,
previewOpen
}: Props = $props()
let flowModuleSchemaMap: FlowModuleSchemaMap | undefined = $state()
@@ -206,6 +208,7 @@
{isOwner}
{suspendStatus}
onOpenDetails={onOpenPreview}
{previewOpen}
/>
{/if}
</Pane>
@@ -34,6 +34,7 @@
isOwner?: boolean
suspendStatus?: StateStore<Record<string, { job: Job; nb: number }>>
onOpenDetails?: () => void
previewOpen?: boolean
}
let {
@@ -49,7 +50,8 @@
job,
isOwner,
suspendStatus,
onOpenDetails
onOpenDetails,
previewOpen = false
}: Props = $props()
const {
@@ -95,6 +97,7 @@
}}
on:applyArgs
{onTestFlow}
{previewOpen}
/>
{:else if $selectedId === 'Result'}
<FlowResult {noEditor} {job} {isOwner} {suspendStatus} {onOpenDetails} />
@@ -48,9 +48,10 @@
noEditor: boolean
disabled: boolean
onTestFlow?: () => void
previewOpen: boolean
}
let { noEditor, disabled, onTestFlow }: Props = $props()
let { noEditor, disabled, onTestFlow, previewOpen }: Props = $props()
const {
flowStore,
previewArgs,
@@ -63,7 +64,6 @@
let addPropertyV2: AddPropertyV2 | undefined = $state(undefined)
let previewSchema: Record<string, any> | undefined = $state(undefined)
let payloadData: Record<string, any> | undefined = undefined
let previewArguments: Record<string, any> | undefined = $state(previewArgs.val)
let dropdownItems: Array<{
label: string
onClick: () => void
@@ -195,7 +195,9 @@
function handleKeydown(event: KeyboardEvent) {
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
runPreview()
if (!previewOpen) {
runPreview()
}
} else if (event.key === 'Enter' && previewSchema && !preventEnter) {
applySchemaAndArgs()
connectFirstNode()
@@ -205,9 +207,6 @@
}
function runPreview() {
if (previewArguments) {
previewArgs.val = structuredClone($state.snapshot(previewArguments))
}
onTestFlow?.()
}
@@ -248,8 +247,8 @@
async function applySchemaAndArgs() {
flowStore.val.schema = applyDiff(flowStore.val.schema, diff)
if (previewArguments) {
savedPreviewArgs = structuredClone($state.snapshot(previewArguments))
if (previewArgs.val) {
savedPreviewArgs = structuredClone($state.snapshot(previewArgs.val))
}
updatePreviewSchemaAndArgs(undefined)
if ($flowInputEditorState) {
@@ -259,11 +258,13 @@
function updatePreviewArguments(payloadData: Record<string, any> | undefined) {
if (!payloadData) {
previewArguments = savedPreviewArgs
if (savedPreviewArgs) {
previewArgs.val = savedPreviewArgs
}
return
}
savedPreviewArgs = structuredClone($state.snapshot(previewArguments))
previewArguments = structuredClone($state.snapshot(payloadData))
savedPreviewArgs = structuredClone($state.snapshot(previewArgs.val))
previewArgs.val = structuredClone($state.snapshot(payloadData))
}
let tabButtonWidth = 0
@@ -372,7 +373,7 @@
displayWebhookWarning
editTab={$flowInputEditorState?.selectedTab}
{previewSchema}
bind:args={previewArguments}
bind:args={previewArgs.val}
bind:editPanelSize={
() => {
return editPanelSize
@@ -399,9 +400,8 @@
}}
shouldDispatchChanges={true}
on:change={() => {
previewArguments = previewArguments
if (!previewSchema) {
savedPreviewArgs = structuredClone($state.snapshot(previewArguments))
savedPreviewArgs = structuredClone($state.snapshot(previewArgs.val))
}
refreshStateStore(flowStore)
}}
@@ -560,7 +560,7 @@
on:isEditing={(e) => {
preventEnter = e.detail
}}
previewArgs={previewArguments}
previewArgs={previewArgs.val}
{isValid}
limitPayloadSize
bind:this={savedInputsPicker}
@@ -583,7 +583,7 @@
on:select={(e) => {
updatePreviewSchemaAndArgs(e.detail ?? undefined)
}}
selected={!!previewArguments}
selected={!!previewArgs.val}
bind:this={jsonInputs}
/>
</FlowInputEditor>
@@ -47,7 +47,7 @@
import { isCloudHosted } from '$lib/cloud'
import { loadSchemaFromModule } from '../flowInfers'
import FlowModuleSkip from './FlowModuleSkip.svelte'
import { type Job, JobService } from '$lib/gen'
import { type Job } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { checkIfParentLoop } from '../utils'
import ModulePreviewResultViewer from '$lib/components/ModulePreviewResultViewer.svelte'
@@ -120,7 +120,6 @@
let s3Kind = $state('s3_client')
let validCode = $state(true)
let width = $state(1200)
let lastJob: Job | undefined = $state(undefined)
let testJob: Job | undefined = $state(undefined)
let testIsLoading = $state(false)
let scriptProgress = $state(undefined)
@@ -197,43 +196,10 @@
let editorSettingsPanelSize = $state(100 - untrack(() => editorPanelSize))
let stepHistoryLoader = getStepHistoryLoaderContext()
let lastJobId: string | undefined = undefined
function onSelectedIdChange() {
if (!flowStateStore?.val?.[$selectedId]?.schema && flowModule) {
reload(flowModule)
}
lastJobId = undefined
}
async function getLastJob() {
if (
!flowStateStore ||
!flowModule.id ||
flowStateStore.val[flowModule.id]?.previewResult === 'never tested this far' ||
!flowStateStore.val[flowModule.id]?.previewJobId
) {
return
}
if (
lastJobId == flowStateStore.val[flowModule.id]?.previewJobId ||
lastJob?.id == flowStateStore.val[flowModule.id]?.previewJobId ||
flowStateStore.val[flowModule.id]?.previewSuccess == undefined
) {
return
}
lastJobId = flowStateStore.val[flowModule.id]?.previewJobId
const job = await JobService.getJob({
workspace: $workspaceStore ?? '',
id: flowStateStore.val[flowModule.id]?.previewJobId ?? '',
noCode: true
})
if (job && job.type === 'CompletedJob') {
lastJobId = flowStateStore.val[flowModule.id]?.previewJobId
lastJob = job
}
}
let leftPanelSize = $state(0)
@@ -270,13 +236,6 @@
$effect.pre(() => {
$selectedId && untrack(() => onSelectedIdChange())
})
$effect(() => {
if (testJob && testJob.type === 'CompletedJob') {
lastJob = $state.snapshot(testJob)
} else if ($workspaceStore && $pathStore && flowModule?.id && flowStateStore) {
untrack(() => getLastJob())
}
})
let parentLoop = $derived(
flowStore.val && flowModule ? checkIfParentLoop(flowStore.val, flowModule.id) : undefined
)
@@ -322,6 +281,12 @@
let rawScriptLang = $derived(
flowModule.value.type == 'rawscript' ? flowModule.value.language : undefined
)
let modulePreviewResultViewer: ModulePreviewResultViewer | undefined = $state(undefined)
function onJobDone() {
modulePreviewResultViewer?.getOutputPickerInner()?.setJobPreview()
}
</script>
<svelte:window onkeydown={onKeyDown} />
@@ -596,6 +561,7 @@
bind:testIsLoading
bind:scriptProgress
focusArg={highlightArg}
{onJobDone}
/>
{:else if selected === 'advanced'}
<Tabs bind:selected={advancedSelected}>
@@ -872,7 +838,7 @@
</Pane>
{#if selected === 'test'}
<Pane minSize={20} class="relative">
{#if stepHistoryLoader?.stepStates[flowModule.id]?.initial && lastJob && !flowModule.mock?.enabled}
{#if stepHistoryLoader?.stepStates[flowModule.id]?.initial && !flowModule.mock?.enabled}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
@@ -900,15 +866,15 @@
flowModule = flowModule
refreshStateStore(flowStore)
}}
{lastJob}
{scriptProgress}
{testJob}
{scriptProgress}
mod={flowModule}
{testIsLoading}
disableMock={preprocessorModule || failureModule}
disableHistory={failureModule}
loadingJob={stepHistoryLoader?.stepStates[flowModule.id]?.loadingJobs}
tagLabel={customUi?.tagLabel}
bind:this={modulePreviewResultViewer}
/>
</Pane>
{/if}
@@ -144,9 +144,7 @@
let testIsLoading = $state(false)
let hover = $state(false)
let connectingData: any | undefined = $state(undefined)
let lastJob: any | undefined = $state(undefined)
let outputPicker: OutputPicker | undefined = $state(undefined)
let historyOpen = $state(false)
let testJob: any | undefined = $state(undefined)
let outputPickerBarOpen = $state(false)
@@ -170,30 +168,6 @@
updateConnectingData(id, pickableIds, $flowPropPickerConfig, flowStateStore)
})
function updateLastJob(flowStateStore: any | undefined) {
if (
!flowStateStore ||
!id ||
flowStateStore.val[id]?.previewResult === 'never tested this far'
) {
return
}
lastJob = {
id: flowStateStore.val[id]?.previewJobId ?? '',
result: flowStateStore.val[id]?.previewResult,
type: 'CompletedJob' as const,
success: flowStateStore.val[id]?.previewSuccess ?? undefined
}
}
$effect(() => {
if (testJob && testJob.type === 'CompletedJob') {
lastJob = $state.snapshot(testJob)
} else if (id) {
updateLastJob(flowStateStore)
}
})
let isConnectingCandidate = $derived(
!!id && !!$flowPropPickerConfig && !!pickableIds && Object.keys(pickableIds).includes(id)
)
@@ -207,6 +181,9 @@
const action = $derived(getAiModuleAction(id))
let testRunDropdownOpen = $state(false)
let outputPickerInner: OutputPickerInner | undefined = $state(undefined)
let historyOpen = $derived.by(() => outputPickerInner?.getHistoryOpen?.() ?? false)
</script>
{#if deletable && id && editId}
@@ -271,7 +248,15 @@
{@const flowStore = flowEditorContext?.flowStore.val}
{@const mod = flowStore?.value ? dfsPreviousResults(id, flowStore, false)[0] : undefined}
{#if mod && flowStateStore?.val?.[id]}
<ModuleTest bind:this={moduleTest} {mod} bind:testIsLoading bind:testJob />
<ModuleTest
bind:this={moduleTest}
{mod}
bind:testIsLoading
bind:testJob
onJobDone={() => {
outputPickerInner?.setJobPreview?.()
}}
/>
{/if}
{/if}
@@ -455,7 +440,6 @@
prefix={'results'}
connectingData={isConnecting ? connectingData : undefined}
{mock}
{lastJob}
{testJob}
moduleId={id}
onSelect={selectConnection}
@@ -463,12 +447,12 @@
{path}
{loopStatus}
rightMargin
bind:derivedHistoryOpen={historyOpen}
historyOffset={{ mainAxis: 12, crossAxis: -9 }}
clazz="p-1"
isLoading={testIsLoading ||
(id ? stepHistoryLoader?.stepStates[id]?.loadingJobs : false)}
initial={id ? stepHistoryLoader?.stepStates[id]?.initial : undefined}
bind:this={outputPickerInner}
/>
{/snippet}
</OutputPicker>
@@ -146,7 +146,7 @@
{selected}
{hover}
id={id ?? ''}
isConnectingCandidate={true}
isConnectingCandidate={nodeKind !== 'result'}
variant="virtual"
type={outputType}
{darkMode}
@@ -13,14 +13,14 @@
let { inputTransform, id, onEditInput }: Props = $props()
const { flowStore, flowStateStore, testSteps, previewArgs } =
const { flowStore, flowStateStore, stepsInputArgs, previewArgs } =
getContext<FlowEditorContext | undefined>('FlowEditorContext') || {}
onMount(() => {
testSteps?.updateStepArgs(id, flowStateStore?.val, flowStore?.val, previewArgs?.val)
stepsInputArgs?.updateStepArgs(id, flowStateStore?.val, flowStore?.val, previewArgs?.val)
})
const input = $derived(testSteps?.getStepArgs(id))
const input = $derived(stepsInputArgs?.getStepArgs(id))
</script>
<div class="p-4 pr-6 h-full overflow-y-auto">
@@ -42,10 +42,10 @@
{:else if inputTransform[key].type === 'static'}
<DollarSign size={12} class="text-tertiary font-mono -my-1" />
{/if}
{#if testSteps?.isArgManuallySet(id, key)}
{#if stepsInputArgs?.isArgManuallySet(id, key)}
<button
onclick={() => {
testSteps?.evalArg(id, key, flowStateStore?.val, flowStore?.val, previewArgs?.val)
stepsInputArgs?.evalArg(id, key, flowStateStore?.val, flowStore?.val, previewArgs?.val)
}}
title="Re-evaluate input"
class="-my-1 ml-0.5 hover:text-primary dark:hover:text-primary dark:text-gray-500 text-gray-300"
@@ -20,7 +20,7 @@
import ObjectViewer from '$lib/components/propertyPicker/ObjectViewer.svelte'
import StepHistory from './StepHistory.svelte'
import { Popover } from '$lib/components/meltComponents'
import { untrack } from 'svelte'
import { getContext, untrack } from 'svelte'
import { Tooltip } from '$lib/components/meltComponents'
import type { Job } from '$lib/gen'
import DisplayResult from '$lib/components/DisplayResult.svelte'
@@ -29,6 +29,7 @@
import DisplayResultControlBar from '$lib/components/DisplayResultControlBar.svelte'
import { base } from '$lib/base'
import { fade } from 'svelte/transition'
import type { FlowEditorContext, OutputViewerJob } from '../types'
interface Props {
prefix?: string
@@ -44,10 +45,7 @@
fullResult?: boolean
closeOnOutsideClick?: boolean
getLogs?: boolean
selectedJob?: SelectedJob
forceJson?: boolean
isLoading?: boolean
preview?: 'mock' | 'job' | undefined
hideHeaderBar?: boolean
simpleViewer?: any | undefined
path?: string
@@ -56,7 +54,6 @@
rightMargin?: boolean
disableMock?: boolean
disableHistory?: boolean
lastJob?: Job
testJob?: Job & { result_stream?: string }
derivedHistoryOpen?: boolean // derived from historyOpen
historyOffset?: any
@@ -72,8 +69,7 @@
}
let {
lastJob = undefined,
testJob = undefined,
testJob,
prefix = '',
allowCopy = false,
connectingData = undefined,
@@ -82,10 +78,7 @@
fullResult = false,
closeOnOutsideClick = false,
getLogs = false,
selectedJob = $bindable(undefined),
forceJson = $bindable(false),
isLoading = $bindable(false),
preview = $bindable(undefined),
hideHeaderBar = false,
simpleViewer = undefined,
path = '',
@@ -94,7 +87,6 @@
rightMargin = false,
disableMock = false,
disableHistory = false,
derivedHistoryOpen = $bindable(false),
historyOffset = { mainAxis: 8, crossAxis: -4.5 },
clazz,
copilot_fix,
@@ -106,19 +98,6 @@
customEmptyJobMessage
}: Props = $props()
type SelectedJob =
| ((
| Job
| {
id: string
result: unknown
type: 'CompletedJob'
workspace_id: string
success: boolean
}
) & { preview?: boolean; result_stream?: string; result?: unknown })
| undefined
let jsonView = $state(false)
let clientHeight: number = $state(0)
let tmpMock: { enabled: boolean; return_value?: unknown } | undefined = $state(undefined)
@@ -127,6 +106,8 @@
let historyOpen = $state(false)
let contentEl: HTMLDivElement | undefined = $state(undefined)
let hasOverflow = $state(false)
let preview: 'mock' | 'job' | undefined = $state(undefined)
let selectedJob: OutputViewerJob = $state(undefined)
function checkOverflow() {
if (contentEl) {
@@ -134,26 +115,21 @@
}
}
function selectJob(nJob: SelectedJob | undefined) {
function selectJob(nJob: OutputViewerJob | undefined) {
if (nJob && (nJob.result_stream || nJob.type == 'CompletedJob')) {
selectedJob = nJob
} else if (job && (job.result_stream || job.type == 'CompletedJob')) {
selectedJob = job
} else if (lastJob && (lastJob.result_stream || lastJob.type == 'CompletedJob')) {
selectedJob = lastJob
} else {
selectedJob = undefined
}
}
$effect(() => {
if (!job || !(job.result_stream || job.type == 'CompletedJob')) {
if (!lastJob || !(lastJob.result_stream || lastJob.type == 'CompletedJob')) {
return
}
selectJob(job)
if (job.preview && mock?.enabled) {
preview = 'job'
job.preview = false
}
selectJob(lastJob)
})
function togglePreview(nPrev: 'mock' | 'job' | undefined) {
@@ -174,6 +150,9 @@
let toolbarLocationJob: 'self' | 'external' | undefined = $state(undefined)
let toolbarLocationMock: 'self' | 'external' | undefined = $state(undefined)
const flowEditorContext = getContext<FlowEditorContext | undefined>('FlowEditorContext')
const flowStateStore = flowEditorContext?.flowStateStore
function updateCanEditWithDblClick(newValue: boolean) {
canEditWithDblClick = newValue
if (debounceTimeout) {
@@ -224,16 +203,41 @@
}
}
let mockUpdateStatus = $derived(
function updateLastJob() {
if (testJob) {
return testJob
}
if (
!flowStateStore ||
!moduleId ||
flowStateStore.val[moduleId]?.previewResult === 'never tested this far'
) {
return
}
return {
id: flowStateStore.val[moduleId]?.previewJobId ?? '',
result: flowStateStore.val[moduleId]?.previewResult,
type: 'CompletedJob' as const,
success: flowStateStore.val[moduleId]?.previewSuccess ?? undefined
} as Job & { result_stream?: string } & { preview?: boolean }
}
const lastJob = $derived.by(updateLastJob)
export function setJobPreview() {
if (mock?.enabled) {
preview = 'job'
}
}
const mockUpdateStatus = $derived(
preview === 'mock' && !mock?.enabled
? 'restore'
: preview === 'job' && mock?.enabled && selectedJob?.type === 'CompletedJob'
: preview === 'job' && mock?.enabled && selectedJob?.['type'] === 'CompletedJob'
? 'override'
: undefined
)
$effect(() => {
derivedHistoryOpen = historyOpen
})
$effect(() => {
if (displayResultJob && typeof displayResultJob.getToolbarLocation === 'function') {
toolbarLocationJob = displayResultJob.getToolbarLocation()
@@ -258,23 +262,67 @@
}
})
let job = $derived.by(() => {
if (testJob) {
return { ...testJob, preview: testJob.type === 'CompletedJob' }
}
if (lastJob) {
return { ...lastJob, preview: false }
}
return undefined
}) as SelectedJob | undefined
let popoverHeight = $derived(customHeight ?? (clientHeight > 0 ? clientHeight : 0))
// If a test job is running, we override the preview to show the test job
const executingTestJob = $derived(testJob && testJob.type === 'QueuedJob')
const isLoadingAndNotMock = $derived(
isLoading && job?.result_stream === undefined && !mock?.enabled
isLoading && lastJob?.result_stream === undefined && (!mock?.enabled || executingTestJob)
)
const copilot_fix_render = $derived(copilot_fix)
export function getPreview() {
return preview
}
async function selectJobFromHistory({
detail
}: {
detail: 'mock' | (Job & { getFullJob: () => Promise<Job> })
}) {
if (!detail) {
togglePreview(undefined)
return
}
if (detail === 'mock') {
if (mock?.enabled) {
togglePreview(undefined)
return
}
togglePreview('mock')
return
}
if (detail.id === lastJob?.id && !mock?.enabled) {
togglePreview(undefined)
return
}
isLoading = true
try {
const fullJob = await detail.getFullJob()
if (fullJob) {
selectJob(fullJob)
togglePreview('job')
}
} finally {
isLoading = false
}
}
export function getHistoryOpen() {
return historyOpen
}
export function getSelectedJob() {
return selectedJob
}
const noHistory = $derived(
loopStatus ? (loopStatus.type === 'self' ? 'isLoop' : 'isInsideLoop') : undefined
)
</script>
<div
@@ -282,6 +330,7 @@
bind:clientHeight
style={canEditWithDblClick ? 'cursor: text;' : ''}
>
<!-- Top toolbar -->
<div
class={twMerge(
'text-xs px-1',
@@ -297,89 +346,24 @@
)}
>
<div class="flex flex-row items-center gap-0.5">
<!-- History picker -->
{#if !disableHistory}
<Popover
bind:this={stepHistoryPopover}
floatingConfig={{
strategy: 'fixed',
placement: 'left-start',
offset: historyOffset,
gutter: 0 // hack to make offset effective, see https://github.com/melt-ui/melt-ui/issues/528
}}
contentClasses="w-[225px] overflow-hidden"
{closeOnOutsideClick}
usePointerDownOutside={closeOnOutsideClick}
disablePopup={!!connectingData || jsonView}
bind:isOpen={historyOpen}
>
{#snippet trigger()}
<Button
color="light"
size="xs2"
variant="contained"
btnClasses="bg-surface h-[27px]"
startIcon={{ icon: History }}
nonCaptureEvent
/>
{/snippet}
{#snippet content()}
<div class="rounded-[inherit]" style={`height: ${popoverHeight}px`}>
<StepHistory
{moduleId}
{getLogs}
on:select={async ({ detail }) => {
if (!detail) {
togglePreview(undefined)
return
}
if (detail === 'mock') {
if (mock?.enabled) {
togglePreview(undefined)
return
}
togglePreview('mock')
return
}
if (detail.id === job?.id && !mock?.enabled) {
togglePreview(undefined)
return
}
// Create a timeout to show loading state after 200ms
const loadingTimeout = setTimeout(() => {
isLoading = true
}, 200)
try {
const fullJob = await detail.getFullJob()
if (fullJob) {
selectJob(fullJob)
togglePreview('job')
}
} finally {
// Clear the timeout if operation completed before 200ms
clearTimeout(loadingTimeout)
isLoading = false
}
}}
mockValue={mock?.return_value}
mockEnabled={mock?.enabled}
{path}
noHistory={loopStatus
? loopStatus.type === 'self'
? 'isLoop'
: 'isInsideLoop'
: undefined}
/>
</div>
{/snippet}
</Popover>
{@render historyPicker()}
{/if}
{#if !isLoadingAndNotMock || mock?.enabled}
<!-- Badge-->
{#if executingTestJob}
<OutputBadge
job={testJob}
class={twMerge(
'min-w-16 min-h-[23px] text-secondary',
preview ? 'bg-surface shadow-sm' : ''
)}
/>
{:else if !isLoadingAndNotMock || mock?.enabled}
<div
class={twMerge(
'w-grow min-w-0 flex gap-1 items-center h-[27px] rounded-md group',
preview || selectedJob?.id !== job?.id ? 'p-[2px] bg-surface-secondary' : ''
preview || selectedJob?.id !== lastJob?.id ? 'p-[2px] bg-surface-secondary' : ''
)}
>
{#if loopStatus?.type === 'self'}
@@ -415,7 +399,7 @@
job={selectedJob}
class={twMerge(
'min-w-16 text-secondary',
preview || selectedJob?.id !== job?.id ? 'bg-surface shadow-sm h-[23px]' : ''
preview || selectedJob?.id !== lastJob?.id ? 'bg-surface shadow-sm h-[23px]' : ''
)}
/>
{/if}
@@ -433,17 +417,9 @@
>
{/if}
</div>
{:else if testJob}
<!-- {JSON.stringify(testJob)} -->
<OutputBadge
job={testJob}
class={twMerge(
'min-w-16 text-secondary',
preview || testJob?.id !== job?.id ? 'bg-surface shadow-sm h-[23px]' : ''
)}
/>
{/if}
<!-- Pin button -->
{#if !disableMock && !isLoadingAndNotMock}
<Tooltip disablePopup={mock?.enabled}>
<Button
@@ -472,6 +448,7 @@
{/if}
{#if jsonView}
<!-- Save button -->
<Button
size="xs2"
color="green"
@@ -492,6 +469,7 @@
}}
disabled={!!error || !tmpMock}
/>
<!-- Cancel button -->
<Button
size="xs2"
color="red"
@@ -503,7 +481,8 @@
tmpMock = undefined
}}
/>
{:else if mock?.enabled && !preview}
{:else if mock?.enabled && !preview && !executingTestJob}
<!-- Edit pin button -->
<Tooltip disablePopup={mock?.enabled}>
<Button
size="xs2"
@@ -614,6 +593,7 @@
<Loader2 class="animate-spin" />
</div>
{:else if connectingData !== undefined || simpleViewer}
<!-- Display data for input connections -->
<ObjectViewer
json={moduleId
? {
@@ -630,6 +610,7 @@
{editKey}
/>
{:else if jsonView}
<!-- Json editor for editing mock data -->
{#await import('$lib/components/JsonEditor.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
@@ -653,12 +634,12 @@
class="h-full"
/>
{/await}
{:else if (mock?.enabled || preview == 'mock') && preview != 'job'}
{:else if (mock?.enabled || preview == 'mock') && preview != 'job' && !executingTestJob}
<!-- Display data for mock data -->
{#if fullResult}
<div class="break-words relative h-full">
<DisplayResult
bind:this={displayResultMock}
bind:forceJson
workspaceId={undefined}
jobId={undefined}
result_stream={undefined}
@@ -682,12 +663,12 @@
/>
{/if}
{:else if selectedJob != undefined && (selectedJob.result_stream || selectedJob.type == 'CompletedJob')}
<!-- Display data for job data -->
{#if fullResult}
<div class="break-words relative h-full">
{#key selectedJob}
<DisplayResult
bind:this={displayResultJob}
bind:forceJson
workspaceId={selectedJob?.workspace_id}
jobId={selectedJob?.id}
result={selectedJob?.result}
@@ -715,7 +696,7 @@
pureViewer={false}
/>
{/if}
{:else if !job}
{:else if !lastJob}
<div class="flex flex-col items-center justify-center h-full">
<p class="text-xs text-secondary">
{customEmptyJobMessage ?? 'Test this step to see results'}{#if !disableMock}
@@ -753,6 +734,47 @@
</button>
{/snippet}
{#snippet historyPicker()}
<Popover
bind:this={stepHistoryPopover}
floatingConfig={{
strategy: 'fixed',
placement: 'left-start',
offset: historyOffset,
gutter: 0 // hack to make offset effective, see https://github.com/melt-ui/melt-ui/issues/528
}}
contentClasses="w-[225px] overflow-hidden"
{closeOnOutsideClick}
usePointerDownOutside={closeOnOutsideClick}
disablePopup={!!connectingData || jsonView}
bind:isOpen={historyOpen}
>
{#snippet trigger()}
<Button
color="light"
size="xs2"
variant="contained"
btnClasses="bg-surface h-[27px]"
startIcon={{ icon: History }}
nonCaptureEvent
/>
{/snippet}
{#snippet content()}
<div class="rounded-[inherit]" style={`height: ${popoverHeight}px`}>
<StepHistory
{moduleId}
{getLogs}
on:select={selectJobFromHistory}
mockValue={mock?.return_value}
mockEnabled={mock?.enabled}
{path}
{noHistory}
/>
</div>
{/snippet}
</Popover>
{/snippet}
<style>
.dbl-click-editable {
cursor: text;
@@ -8,7 +8,7 @@ import {
} from './previousResults'
import { evalValue } from './utils'
export class TestSteps {
export class StepsInputArgs {
#stepsEvaluated = $state<Record<string, Record<string, any>>>({})
#steps = $state<Record<string, Record<string, any>>>({})
@@ -53,10 +53,20 @@ export class TestSteps {
}
isArgManuallySet(moduleId: string, argName: string): boolean {
return (
JSON.stringify(this.#steps[moduleId]?.[argName]) !==
JSON.stringify(this.#stepsEvaluated[moduleId]?.[argName])
)
try {
const stepsValue = this.#steps[moduleId]?.[argName]
const evaluatedValue = this.#stepsEvaluated[moduleId]?.[argName]
const stepsStr = JSON.stringify(stepsValue)
const evaluatedStr = JSON.stringify(evaluatedValue)
return stepsStr !== evaluatedStr
} catch (error) {
console.warn(
`Error in isArgManuallySet for moduleId: ${moduleId}, argName: ${argName}`,
error
)
return this.#steps[moduleId]?.[argName] !== this.#stepsEvaluated[moduleId]?.[argName]
}
}
getManuallyEditedArgs(moduleId: string): string[] {
@@ -185,4 +195,8 @@ export class TestSteps {
})
this.#stepsEvaluated[moduleId] = nargs
}
resetManuallyEditedArgs() {
this.#steps = $state.snapshot(this.#stepsEvaluated)
}
}
+35 -22
View File
@@ -1,4 +1,4 @@
import type { OpenFlow } from '$lib/gen'
import type { Job, OpenFlow } from '$lib/gen'
import type { History } from '$lib/history.svelte'
import type { Writable } from 'svelte/store'
import type ScriptEditorDrawer from './content/ScriptEditorDrawer.svelte'
@@ -7,7 +7,7 @@ import type { FlowBuilderWhitelabelCustomUi } from '../custom_ui'
import type Editor from '../Editor.svelte'
import type SimpleEditor from '../SimpleEditor.svelte'
import type { StateStore } from '$lib/utils'
import type { TestSteps } from './testSteps.svelte'
import type { StepsInputArgs } from './stepsInputArgs.svelte'
import type { Asset, AssetWithAccessType } from '../assets/lib'
import type S3FilePicker from '../S3FilePicker.svelte'
import type DbManagerDrawer from '../DBManagerDrawer.svelte'
@@ -37,31 +37,31 @@ export type ExtendedOpenFlow = OpenFlow & {
export type FlowInputEditorState = {
selectedTab:
| 'inputEditor'
| 'history'
| 'savedInputs'
| 'json'
| 'captures'
| 'firstStepInputs'
| undefined
| 'inputEditor'
| 'history'
| 'savedInputs'
| 'json'
| 'captures'
| 'firstStepInputs'
| undefined
editPanelSize: number | undefined
payloadData: Record<string, any> | undefined
}
export type CurrentEditor =
| ((
| {
type: 'script'
editor: Editor
showDiffMode: () => void
hideDiffMode: () => void
diffMode: boolean
lastDeployedCode: string | undefined
}
| { type: 'iterator'; editor: SimpleEditor }
) & {
stepId: string
})
| {
type: 'script'
editor: Editor
showDiffMode: () => void
hideDiffMode: () => void
diffMode: boolean
lastDeployedCode: string | undefined
}
| { type: 'iterator'; editor: SimpleEditor }
) & {
stepId: string
})
| undefined
export type FlowEditorContext = {
@@ -75,7 +75,7 @@ export type FlowEditorContext = {
flowStore: StateStore<ExtendedOpenFlow>
flowInputEditorState: Writable<FlowInputEditorState>
flowStateStore: StateStore<FlowState>
testSteps: TestSteps
stepsInputArgs: StepsInputArgs
saveDraft: () => void
initialPathStore: Writable<string>
fakeInitialPath: string
@@ -97,3 +97,16 @@ export type FlowGraphAssetContext = StateStore<{
additionalAssetsMap: Record<string, AssetWithAccessType[]>
computeAssetsCount: (asset: Asset) => number
}>
export type OutputViewerJob =
| ((
| Job
| {
id: string
result: unknown
type: 'CompletedJob'
workspace_id: string
success: boolean
}
) & { result_stream?: string; result?: unknown })
| undefined
+4 -4
View File
@@ -24,7 +24,7 @@
import type { FlowPropPickerConfig, PropPickerContext } from '$lib/components/prop_picker'
import type { PickableProperties } from '$lib/components/flows/previousResults'
import { Triggers } from '$lib/components/triggers/triggers.svelte'
import { TestSteps } from '$lib/components/flows/testSteps.svelte'
import { StepsInputArgs } from '$lib/components/flows/stepsInputArgs.svelte'
import { ModulesTestStates } from '$lib/components/modulesTest.svelte'
let token = $page.url.searchParams.get('wm_token') ?? undefined
@@ -75,7 +75,7 @@
const moving = writable<{ id: string } | undefined>(undefined)
const history = initHistory(flowStore.val)
const testSteps = new TestSteps()
const stepsInputArgs = new StepsInputArgs()
const selectedIdStore = writable('settings-metadata')
const triggersCount = writable<TriggersCount | undefined>(undefined)
setContext<TriggerContext>('TriggerContext', {
@@ -94,7 +94,7 @@
pathStore: writable(''),
flowStateStore,
flowStore,
testSteps,
stepsInputArgs,
saveDraft: () => {},
initialPathStore: writable(''),
fakeInitialPath: '',
@@ -292,7 +292,7 @@
noEditor
on:applyArgs={(ev) => {
if (ev.detail.kind === 'preprocessor') {
testSteps.setStepArgs('preprocessor', ev.detail.args ?? {})
stepsInputArgs.setStepArgs('preprocessor', ev.detail.args ?? {})
$selectedIdStore = 'preprocessor'
} else {
previewArgsStore.val = ev.detail.args ?? {}