Files
windmill/frontend/src/lib/components/FlowGraphViewerStep.svelte
T
Guilhem 3b5c1657c7 fix(frontend): improve runs detail page (#7694)
* separate flow and status with splitpane

* fix autoscroll behavior

* Flow log viewer nit

* not graph viewer

* Improve flow job result

* Create job detail header to replace metadata

* Remove FlowPreviewResult

* Create compact job header

* Use job header in runs page

* Improve runs page run preview

* Use flow header in detail section

* Show logs for script steps

* Clean old schedule status

* Limit result height

* Script run detail improvement

* Script run preview improvement

* Fix csv table overflow

* surface tertiary as background for Inputs

* nit

* Improve runs detail skeleton

* fix check

* nit

* Improve node definition

* fix flow module component overflow

* Use component DataTable for flow schema viewer

* Add language icon to step detail

* improve run header

* Improve Job detail header

* nit

* restore isOwner logic

* Handle resume flows

* restore execution status in run preview

* restore flow execution status in the preview

* flow preview, add status bar

* nit module status

* nit

* Remove flor preview result

* nit

* nit

* fix flow result card

* nit

* nit

* nit

* improve field selection on runs detail based on job type

* Improve column layout

* create JobStatusIcon component

* remove job status badge icons

* improve compact version

* use shared job field display

* improve job detail field display

* fix badge alignment

* increase padding

* nit

* nit

* improve compact display

* make background darker for metadata

* use auto layout

* fix auto layout

* improve display

* fix truncate logic

* fix compact

* improve flex adaptibility

* improve responsive layout

* improve extra compact header

* nit

* remove unused icons

* nit

* Improve flow result display

* nit

* merge progressbar and execution status

* handle canceled flow better
2026-02-03 17:20:35 +00:00

330 lines
11 KiB
Svelte

<script lang="ts">
import HighlightCode from './HighlightCode.svelte'
import InputTransformsViewer from './InputTransformsViewer.svelte'
import IconedPath from './IconedPath.svelte'
import type { FlowModule } from '$lib/gen'
import { Badge, Button, Drawer, DrawerContent } from './common'
import { Highlight } from 'svelte-highlight'
import ObjectViewer from './propertyPicker/ObjectViewer.svelte'
import typescript from 'svelte-highlight/languages/typescript'
import FlowPathViewer from './flows/content/FlowPathViewer.svelte'
import SchemaViewer from './SchemaViewer.svelte'
import { scriptPathToHref } from '$lib/scripts'
import { cleanExpr, copyToClipboard } from '$lib/utils'
import { hubBaseUrlStore } from '$lib/stores'
import { twMerge } from 'tailwind-merge'
import FlowModuleScript from './flows/content/FlowModuleScript.svelte'
import { Copy, Expand } from 'lucide-svelte'
import HighlightTheme from './HighlightTheme.svelte'
import LanguageIcon from './common/languageIcons/LanguageIcon.svelte'
export let schema: any | undefined = undefined
export let stepDetail: FlowModule | string | undefined = undefined
export let jobScriptHash: string | undefined = undefined
let codeViewer: Drawer
</script>
<HighlightTheme />
<Drawer bind:this={codeViewer} size="900px">
<DrawerContent title={'Expanded Code'} on:close={codeViewer.closeDrawer}>
{#if stepDetail && typeof stepDetail != 'string'}
{#if stepDetail.value.type == 'script'}
<div class="mb-4">
<a
rel="noreferrer"
target="_blank"
href={scriptPathToHref(stepDetail?.value?.path ?? '', $hubBaseUrlStore)}
class=""
>
<IconedPath path={stepDetail?.value?.path ?? ''} />
</a>
</div>
<div class="text-xs mb-4 mt-2">
<h3 class="mb-1 text-xs font-semibold text-emphasis">Step inputs</h3>
<InputTransformsViewer inputTransforms={stepDetail?.value?.input_transforms ?? {}} />
</div>
{#if stepDetail.value.path.startsWith('hub/')}
<div class="mt-6">
<h3 class="mb-1 mt-6 text-xs font-semibold text-emphasis">Code</h3>
<iframe
class="w-full h-full text-sm"
title="embedded script from hub"
frameborder="0"
src="{$hubBaseUrlStore}/embed/script/{stepDetail.value?.path?.substring(4)}"
></iframe>
</div>
{/if}
{:else if stepDetail.value.type == 'rawscript'}
<div class="text-2xs mb-4 mt-2">
<h3 class="mb-1 text-xs font-semibold text-emphasis">Step inputs</h3>
<InputTransformsViewer inputTransforms={stepDetail?.value?.input_transforms ?? {}} />
</div>
<div class="mb-1 mt-6 flex items-center gap-1">
<h3 class="text-xs font-semibold text-emphasis">Code</h3>
<LanguageIcon lang={stepDetail.value.language} size={14} />
</div>
<div class="!text-xs rounded-md p-2 border bg-surface-input">
<HighlightCode language={stepDetail.value.language} code={stepDetail.value.content} />
</div>
<h3 class="mt-8 mb-1 text-xs font-normal text-primary">Lockfile</h3>
<div>
{#if stepDetail.value.lock}
<pre class="bg-surface-secondary text-sm p-2 h-full overflow-auto w-full"
>{stepDetail.value.lock}</pre
>
{:else}
<p class="bg-surface-secondary text-sm p-2"> There is no lock file for this script </p>
{/if}
</div>
{/if}
{/if}
</DrawerContent>
</Drawer>
<div class={twMerge('p-2 overflow-y-scroll')}>
{#if stepDetail == undefined}
<div>
<p class="font-medium text-secondary text-center pt-4 pb-8">
Click on a step to see its details
</p>
{#if schema}
<h3 class="mb-2 font-semibold">Flow Inputs</h3>
<SchemaViewer {schema} />
{/if}
</div>
{:else if stepDetail == 'Input'}
{#if schema}
<SchemaViewer {schema} />
{:else}
<p class="font-medium text-secondary text-center pt-4 pb-8"> No input schema </p>
{/if}
{:else if stepDetail == 'Result'}
<p class="font-medium text-secondary text-center pt-4 pb-8"> End of the flow </p>
{:else if typeof stepDetail != 'string' && stepDetail.value}
<div class="">
<div class="sticky top-0 bg-surface w-full flex items-center py-2">
{#if stepDetail.id && stepDetail.id != 'failure' && stepDetail.id != 'preprocessor'}
<Badge color="indigo">
{stepDetail.id}
</Badge>
{/if}
<span
class={twMerge(
'font-semibold text-emphasis text-sm',
stepDetail.id !== 'failure' && stepDetail.id !== 'preprocessor' ? 'ml-2' : ''
)}
>
{#if stepDetail.summary}
{stepDetail.summary}
{:else if stepDetail.value.type == 'identity'}
Identity
{:else if stepDetail.value.type == 'forloopflow'}
For loop {#if stepDetail.value.parallel}(parallel){/if}
{#if stepDetail.value.skip_failures}(skip failures){/if}
{#if stepDetail.value.squash}(squash){/if}
{:else if stepDetail.value.type == 'branchall'}
Run all branches {#if stepDetail.value.parallel}(parallel){/if}
{:else if stepDetail.value.type == 'branchone'}
Run one branch
{:else if stepDetail.value.type == 'flow'}
Inner flow
{:else if stepDetail.value.type == 'whileloopflow'}
While loop {#if stepDetail.value.skip_failures}(skip failures){/if}
{#if stepDetail.value.squash}(squash){/if}
{:else if stepDetail.id === 'failure'}
Error handler
{:else if stepDetail.id === 'preprocessor'}
Preprocessor
{:else if stepDetail.value.type == 'rawscript'}
Inline {stepDetail.value.language} script
{:else if stepDetail.value.type == 'script'}
Workspace script
{:else if stepDetail.value.type == 'aiagent'}
AI Agent
{/if}
</span>
</div>
{#if stepDetail.value.type == 'script'}
<div class="pb-2">
<a
rel="noreferrer"
target="_blank"
href={scriptPathToHref(stepDetail?.value?.path ?? '', $hubBaseUrlStore)}
class=""
>
<IconedPath path={stepDetail?.value?.path ?? ''} />
</a>
</div>
{/if}
</div>
{#if stepDetail.value.type == 'identity'}
<p class="font-medium text-secondary text-center pt-4 pb-8">
An identity step returns its inputs as outputs
</p>
{:else if stepDetail.value.type == 'rawscript'}
{#if stepDetail.id !== 'preprocessor'}
<div class="text-xs">
<h3 class="mb-1 font-semibold mt-2 text-xs text-emphasis">Step inputs</h3>
<InputTransformsViewer inputTransforms={stepDetail?.value?.input_transforms ?? {}} />
</div>
{/if}
<div>
<div class="mb-1 mt-6 flex justify-between items-center">
<div class="flex items-center gap-2">
<h3 class="font-semibold text-xs text-emphasis">Code</h3>
<LanguageIcon lang={stepDetail.value.language} width={14} height={14} />
</div>
<Button
unifiedSize="sm"
variant="subtle"
onClick={codeViewer.openDrawer}
startIcon={{ icon: Expand }}>Expand</Button
>
</div>
<div class="border p-2 rounded-md bg-surface-input">
<HighlightCode
language={stepDetail.value.language}
code={stepDetail.value.content}
className="whitespace-pre-wrap"
/>
</div>
<h3 class="mb-1 mt-6 text-xs font-semibold text-emphasis">Lockfile</h3>
<div>
{#if stepDetail.value.lock}
<pre class="bg-surface-input rounded-md border text-xs p-2 h-full overflow-auto w-full"
>{stepDetail.value.lock}</pre
>
{:else}
<p class="bg-surface-secondary text-xs italic p-2 rounded">
There is no lockfile for this inline script
</p>
{/if}
</div>
</div>
{:else if stepDetail.value.type == 'script'}
{#if stepDetail.id !== 'preprocessor'}
<div class="text-xs">
<h3 class="mb-1 font-semibold mt-2 text-xs text-emphasis">Step inputs</h3>
<InputTransformsViewer inputTransforms={stepDetail?.value?.input_transforms ?? {}} />
</div>
{/if}
{#if stepDetail.value.path.startsWith('hub/')}
<div class="flex flex-col grow">
<div class="mb-1 mt-6 flex justify-between items-center">
<h3 class="font-semibold text-xs text-emphasis">Code</h3>
<Button
unifiedSize="sm"
variant="subtle"
onClick={codeViewer.openDrawer}
startIcon={{ icon: Expand }}>Expand</Button
>
</div>
<iframe
class="w-full grow text-sm h-full"
title="embedded script from hub"
frameborder="0"
src="{$hubBaseUrlStore}/embed/script/{stepDetail.value?.path?.substring(4)}"
></iframe>
</div>
{:else}
<FlowModuleScript path={stepDetail.value.path} hash={jobScriptHash} />
{/if}
{:else if stepDetail.value.type == 'aiagent'}
<div class="text-xs">
<h3 class="mb-1 font-semibold mt-2 text-xs text-emphasis">Step inputs</h3>
<InputTransformsViewer inputTransforms={stepDetail?.value?.input_transforms ?? {}} />
</div>
{:else if stepDetail.value.type == 'forloopflow'}
<div>
<p class="font-medium text-secondary pb-2"> Iterator expression: </p>
{#if stepDetail.value.iterator.type == 'static'}
<ObjectViewer json={stepDetail.value.iterator.value} />
{:else}
<span class="text-xs">
<Highlight language={typescript} code={cleanExpr(stepDetail.value.iterator.expr)} />
</span>
{/if}
</div>
{:else if stepDetail.value.type == 'whileloopflow'}
<div>
{#if stepDetail.stop_after_if}
<p class="font-medium text-secondary pb-2 pt-4">Stop after if expr:: </p>
<span class="text-xs">
<Highlight language={typescript} code={cleanExpr(stepDetail.stop_after_if?.expr)} />
</span>
{/if}
</div>
<div>
{#if stepDetail.stop_after_all_iters_if}
<p class="font-medium text-secondary pb-2 pt-4">Stop after all iters if expr:: </p>
<span class="text-xs">
<Highlight
language={typescript}
code={cleanExpr(stepDetail.stop_after_all_iters_if?.expr)}
/>
</span>
{/if}
</div>
{:else if stepDetail.value.type == 'branchall'}
<p class="font-medium text-secondary text-center pt-4 pb-8">
All branches will run, regardless of the inputs
</p>
{:else if stepDetail.value.type == 'branchone'}
<p class="font-medium text-secondary text-center pt-4 pb-8">
Only one branch will run based on a predicate
</p>
<div class="flex-col flex gap-2">
<div class="flex flex-row gap-4 text-sm p-2">
<Badge large={true} color="blue">Default branch</Badge>
<p class="italic text-primary"
>If none of the predicates' expressions evaluated in-order match, this branch is chosen</p
>
</div>
{#each stepDetail.value.branches as v, i}
<div class="flex flex-col gap-4-2 items-center">
<div class="w-full flex gap-2 px-2 pt-4 pb-2">
<Badge large={true} color="blue">Branch {i + 1}</Badge>
<span>{v.summary}</span>
</div>
<div class="w-full border p-2">
<HighlightCode language="frontend" code={v.expr} />
</div>
</div>
{/each}
</div>
{:else if stepDetail.value.type == 'flow'}
<div class="text-sm mb-1 flex justify-between flex-row items-center">
<span class="text-xs font-normal text-secondary"
>Flow path: <span class="text-emphasis">{stepDetail.value.path}</span>
</span>
<Button
unifiedSize="sm"
startIcon={{ icon: Copy }}
variant="subtle"
on:click={() => {
if (typeof stepDetail !== 'string') {
const val = stepDetail?.value
if (val?.type == 'flow') {
copyToClipboard(val.path)
}
}
}}
>
Copy path
</Button>
</div>
<FlowPathViewer noSide path={stepDetail.value.path} />
{/if}
{:else}
<p class="font-medium text-secondary text-center pt-4 pb-8">
Step {stepDetail} selected
</p>
{/if}
</div>