Files
windmill/frontend/src/lib/components/FlowStatusWaitingForEvents.svelte
T
Guilhem 6c17a6963e feat(frontend): run test flow from graph (#6122)
* migrate FlowPreviewContent to svelte 5

* run background preview from input panel

* share local run test

* Show approval in graph is testing in graph

* use component and props instead of portal for approval in graph

* Add a toggle to show module status in graph

* open module result after each run

* Fix module reactivity issue

* Add test flow button

* Extract preview run logic from flowPreviewContent

* Revert "Extract preview run logic from flowPreviewContent"

This reverts commit a39c70a920.

* nit

* lazy load preview content

* create component for flow preview button

* open preview v0

* open preview v1

* connect open preview button

* improve graph run display

* enable cancel preview

* Run test flow from input panel

* nit

* wip

* Use global context instead of module context for moduleTestState

* nit

* fix flow preview rendering

* Add testJob to modulesTest context

* update module status based on individual test data

* fix: clear job status on run preview

* detatch run buttons from input node

* move preview job in FlowEditorContext

* move outputPickerOpenFns to FlowEditorContext

* add result panel

* Add result output picker

* add status to loops and branch

* add open detail button to result panel

* fix test up to

* clean unnecessary binding

* clean

* Make iteration annotation smaller in editmode

* detatch test button to and aproval from node

* prevent flow edition during execution

* Prevent step test run during flow run

* Show approval in graph edges

* prevent opening output popover if node is outside the graph

* fix pointerdownOutside action

* fix test up to dropdown not closing

* fix test up to

* nit

* change job status badge display

* fix running status

* Enable test flow in Dev

* fix darkmode

* fix node panel display in Dev

* fix test flow button positionning

* fix suspend in subflows

* improve lazy load of preview

* prevent preview data unmount on close drawer

* clean code

* move flowjob into flow context

* Revert "move flowjob into flow context"

This reverts commit 939e9dbaaf.

* clean context

* nit

* fix dark mode status view

* fix test button alignment

* clean job status on deleted step

* fix retry bad status display

* Detect flow change

* Update frontend/src/lib/components/flows/header/FlowPreviewButtons.svelte

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-09 16:24:03 +00:00

164 lines
4.6 KiB
Svelte

<script lang="ts">
import { mergeSchema } from '$lib/common'
import { type Job, JobService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { sendUserToast } from '$lib/toast'
import { X } from 'lucide-svelte'
import DisplayResult from './DisplayResult.svelte'
import Tooltip from './Tooltip.svelte'
import { Button } from './common'
import SchemaForm from './SchemaForm.svelte'
export let isOwner: boolean
export let workspaceId: string | undefined
export let job: Job
export let light: boolean = false
let default_payload: object = {}
let resumeUrl: string | undefined = undefined
let cancelUrl: string | undefined = undefined
let description: any = undefined
let hide_cancel = false
$: approvalStep = (job?.flow_status?.step ?? 1) - 1
let defaultValues = {}
$: job && getDefaultArgs()
let schema = {}
let lastJobId: string | undefined = undefined
async function getDefaultArgs() {
let jobId = job?.flow_status?.modules?.[approvalStep]?.job
if (jobId === lastJobId) {
return
}
if (!jobId) {
return {}
}
lastJobId = jobId
let job_result = (await JobService.getCompletedJobResult({
workspace: workspaceId ?? $workspaceStore ?? '',
id: jobId
})) as any
const args = job_result?.default_args ?? {}
description = job_result?.description
defaultValues = JSON.parse(JSON.stringify(args))
default_payload = args
resumeUrl = job_result?.['resume']
cancelUrl = job_result?.['cancel']
hide_cancel = job?.raw_flow?.modules?.[approvalStep]?.suspend?.hide_cancel ?? false
schema = mergeSchema(
job?.raw_flow?.modules?.[approvalStep]?.suspend?.resume_form?.schema ?? {},
job_result?.enums ?? {}
)
}
async function continu(approve: boolean) {
if ((resumeUrl && approve) || (cancelUrl && !approve)) {
let split = (approve ? resumeUrl : cancelUrl)!.split('/')
let signatureUrl = split.pop() ?? ''
const regex = /([^?]+)(?:\?[^=]+=(\w+))?/
const matches = signatureUrl.match(regex)
const signature = matches?.[1]
if (!signature) {
sendUserToast(`Could not parse signature: ${signatureUrl}`, true)
return
}
const approver = matches?.[2] || undefined
let resumeId = -1
let parsedResumeId = split.pop() ?? ''
try {
resumeId = new Number(parsedResumeId).valueOf()
} catch (e) {
console.error(`Could not parse resume id: ${parsedResumeId}`)
}
let jobId = split.pop() ?? ''
if (approve) {
await JobService.resumeSuspendedJobPost({
workspace: workspaceId ?? $workspaceStore ?? '',
id: jobId,
requestBody: default_payload as any,
resumeId,
signature,
approver
})
} else {
await JobService.cancelSuspendedJobPost({
workspace: workspaceId ?? $workspaceStore ?? '',
id: jobId,
resumeId,
signature,
approver,
requestBody: {}
})
}
} else {
if (approve) {
await JobService.resumeSuspendedFlowAsOwner({
workspace: workspaceId ?? $workspaceStore ?? '',
id: job?.id ?? '',
requestBody: default_payload as any
})
} else {
await JobService.cancelQueuedJob({
workspace: workspaceId ?? $workspaceStore ?? '',
id: job?.id ?? '',
requestBody: {}
})
}
}
}
</script>
<div class="w-full h-full mt-2 text-sm text-tertiary">
{#if !light}
<p>Waiting to be resumed</p>
{/if}
{#if description != undefined}
<DisplayResult {workspaceId} noControls result={description} language={job?.language} />
{/if}
<div>
{#if isOwner || resumeUrl}
<div class="flex flex-row gap-2 mt-2">
{#if cancelUrl && !hide_cancel}
<div>
<Button
color="red"
title="Cancel the flow"
iconOnly
startIcon={{ icon: X }}
variant="border"
on:click={() => continu(false)}
/>
</div>
{/if}
<div>
<Button color="green" variant="border" on:click={() => continu(true)}
>Resume <Tooltip
>Since you are an owner of this flow, you can send resume events without necessarily
knowing the resume id sent by the approval step</Tooltip
></Button
>
</div>
{#if job?.raw_flow?.modules?.[approvalStep]?.suspend?.resume_form?.schema}
<div class="w-full border rounded-lg p-2">
<SchemaForm onlyMaskPassword bind:args={default_payload} {defaultValues} {schema} />
</div>
{/if}
<Tooltip
>The payload is optional, it is passed to the following step through the `resume` variable</Tooltip
>
</div>
{:else}
You cannot resume the flow yourself without receiving the resume secret since you are not an
owner of {job.script_path} and the approval step did not contain the resume url at key `resume`
{/if}
</div>
</div>