mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-17 16:03:06 +00:00
* 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>
306 lines
11 KiB
Svelte
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>
|