Files
windmill/frontend/src/lib/components/FlowGraphViewerStep.svelte
T
6186a0645d feat: edit variables, resources and triggers in their own session tab (#11206)
* feat: edit variables, resources and triggers in their own session tab

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: take a trigger's flow kind from its config and name the written trigger

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor: one operating-workspace context for editors acting on a session's workspace

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: reload page item tabs only in sessions acting on the tool's workspace

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: resolve open in workspace for every preview tab kind in one place

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: guard every component under a session editor against the navigation workspace

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: show a page item tab whose item is gone instead of mounting its editor

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: follow a saved page item in the tab's own workspace and restore in-frame rows

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: close a page item tab from its editor header

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: judge trigger permissions by the user acting in the session workspace

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: judge every session-editor permission by the user acting in that workspace

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: keep session permissions reactive to the acting role and restore tabs as last seen

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: default Path's owner controls to the user acting in its workspace

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-09-19 23:29:14 +02:00

306 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 { Link } from 'lucide-svelte'
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'
import FlowGraphViewerStepHeader from './FlowGraphViewerStepHeader.svelte'
import { useOperatingWorkspace } from '$lib/components/operatingWorkspace.svelte'
const operatingWorkspace = useOperatingWorkspace()
interface Props {
schema?: any | undefined
stepDetail?: FlowModule | string | undefined
jobScriptHash?: string | undefined
hideDefaultInputs?: boolean
// The workspace the viewed flow belongs to (differs from the nav workspace in fork/session
// editors); used to qualify resource links.
workspace?: string
/** Given, the step header starts with a back control that calls it. */
onBack?: () => void
}
let {
schema = undefined,
stepDetail = undefined,
jobScriptHash = undefined,
hideDefaultInputs = false,
workspace = undefined,
onBack = undefined
}: Props = $props()
let ws = $derived(workspace ?? $operatingWorkspace)
let codeViewer: Drawer | undefined = $state()
</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>
<div class="mt-6">
<h3 class="mb-1 mt-6 text-xs font-semibold text-emphasis">Code</h3>
<FlowModuleScript path={stepDetail.value.path} hash={jobScriptHash} />
</div>
{: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="text-secondary text-xs italic px-2 pt-2"> Click on a step to see its details </p>
{#if schema && !hideDefaultInputs}
<h3 class="mb-2 font-semibold">Flow Inputs</h3>
<SchemaViewer {schema} />
{/if}
</div>
{:else if stepDetail == 'Input'}
<FlowGraphViewerStepHeader {stepDetail} {onBack} />
{#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'}
<FlowGraphViewerStepHeader {stepDetail} {onBack} />
<p class="font-medium text-secondary text-center pt-4 pb-8"> End of the flow </p>
{:else if typeof stepDetail != 'string' && stepDetail.value}
<!-- A direct child of the scrolling root: a sticky row can only hold within its parent's
box, so wrapped with the path link below it would scroll away with that wrapper. -->
<FlowGraphViewerStepHeader {stepDetail} {onBack} />
<div class="">
{#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}
<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>
<FlowModuleScript path={stepDetail.value.path} hash={jobScriptHash} />
{:else if stepDetail.value.type == 'aiagent'}
<div class="text-xs">
{#if stepDetail.value.agent}
<div class="mb-1 flex items-center gap-1.5 text-2xs text-secondary">
<Link size={12} class="shrink-0" />
<span class="shrink-0">Linked to</span>
<a
class="truncate font-medium"
href={`/resources?path=${stepDetail.value.agent}&workspace=${ws}`}
title={stepDetail.value.agent}>{stepDetail.value.agent}</a
>
</div>
{/if}
<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 if stepDetail.value.iterator.type == 'javascript'}
<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>