Files
windmill/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte

269 lines
9.0 KiB
Svelte

<script lang="ts">
import { Pane, Splitpanes } from 'svelte-splitpanes'
import Tab from '$lib/components/common/tabs/Tab.svelte'
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
import Editor from '$lib/components/Editor.svelte'
import EditorBar from '$lib/components/EditorBar.svelte'
import ModulePreview from '$lib/components/ModulePreview.svelte'
import { createScriptFromInlineScript, fork } from '$lib/components/flows/flowStateUtils'
import { flowStore } from '$lib/components/flows/flowStore'
import SchemaForm from '$lib/components/SchemaForm.svelte'
import { RawScript, type FlowModule, type PathFlow, type PathScript } from '$lib/gen'
import FlowCard from '../common/FlowCard.svelte'
import FlowModuleHeader from './FlowModuleHeader.svelte'
import { flowStateStore } from '../flowState'
import { schemaToObject, scriptLangToEditorLang } from '$lib/utils'
import PropPickerWrapper from '../propPicker/PropPickerWrapper.svelte'
import { afterUpdate, getContext, setContext } from 'svelte'
import type { FlowEditorContext } from '../types'
import { loadSchemaFromModule } from '../utils'
import FlowModuleScript from './FlowModuleScript.svelte'
import FlowModuleEarlyStop from './FlowModuleEarlyStop.svelte'
import FlowModuleSuspend from './FlowModuleSuspend.svelte'
import FlowRetries from './FlowRetries.svelte'
import { getStepPropPicker } from '../previousResults'
import Button from '$lib/components/common/button/Button.svelte'
import Alert from '$lib/components/common/alert/Alert.svelte'
import FlowModuleSleep from './FlowModuleSleep.svelte'
import FlowPathViewer from './FlowPathViewer.svelte'
const { selectedId, previewArgs } = getContext<FlowEditorContext>('FlowEditorContext')
export let flowModule: FlowModule
export let failureModule: boolean = false
export let parentModule: FlowModule | undefined = undefined
export let previousModule: FlowModule | undefined
let value = flowModule.value as PathFlow | RawScript | PathScript
$: value = flowModule.value as PathFlow | RawScript | PathScript
let editor: Editor
let modulePreview: ModulePreview
let websocketAlive = { pyright: false, black: false, deno: false, go: false }
let selected = 'inputs'
let wrapper: HTMLDivElement
let panes: HTMLElement
let totalTopGap = 0
let validCode = true
let width = 1200
let inputTransforms: Record<string, any> = value.input_transforms
$: value.input_transforms = inputTransforms
$: stepPropPicker = failureModule
? {
pickableProperties: {
flow_input: schemaToObject($flowStore.schema, $previewArgs),
priorIds: {},
previousId: undefined,
hasResume: false
},
extraLib: ''
}
: getStepPropPicker(
$flowStateStore,
parentModule,
previousModule,
flowModule.id,
$flowStore,
$previewArgs,
false,
true
)
function onKeyDown(event: KeyboardEvent) {
if ((event.ctrlKey || event.metaKey) && event.key == 'Enter') {
event.preventDefault()
selected = 'test'
modulePreview?.runTestWithStepArgs()
}
}
async function reload(flowModule: FlowModule) {
try {
const { input_transforms, schema } = await loadSchemaFromModule(flowModule)
validCode = true
setTimeout(() => {
if (JSON.stringify(value.input_transforms) !== JSON.stringify(input_transforms)) {
inputTransforms = input_transforms
}
})
if (JSON.stringify(schema) !== JSON.stringify($flowStateStore[flowModule.id]?.schema)) {
if (!$flowStateStore[flowModule.id]) {
$flowStateStore[flowModule.id] = { schema }
} else {
$flowStateStore[flowModule.id].schema = schema
}
}
} catch (e) {
validCode = false
}
}
afterUpdate(() => {
totalTopGap = 0
if (!(wrapper && panes)) return
const wrapperTop = wrapper.getBoundingClientRect().top
const panesTop = panes.getBoundingClientRect().top
totalTopGap = panesTop - wrapperTop
})
</script>
<svelte:window on:keydown={onKeyDown} />
{#if value}
<div class="h-full" bind:this={wrapper} bind:clientWidth={width}>
<FlowCard bind:flowModule>
<svelte:fragment slot="header">
<FlowModuleHeader
bind:module={flowModule}
on:toggleSuspend={() => (selected = 'advanced-suspend')}
on:toggleSleep={() => (selected = 'advanced-sleep')}
on:toggleRetry={() => (selected = 'advanced-retries')}
on:toggleStopAfterIf={() => (selected = 'advanced-early-stop')}
on:fork={async () => {
const [module, state] = await fork(flowModule)
flowModule = module
$flowStateStore[module.id] = state
}}
on:createScriptFromInlineScript={async () => {
const [module, state] = await createScriptFromInlineScript(
flowModule,
$selectedId,
$flowStateStore[flowModule.id].schema
)
flowModule = module
$flowStateStore[module.id] = state
}}
/>
</svelte:fragment>
{#if value.type === 'rawscript'}
<div class="border-b-2 shadow-sm px-1">
<EditorBar
{validCode}
{editor}
lang={value['language'] ?? 'deno'}
{websocketAlive}
iconOnly={width < 768}
/>
</div>
{/if}
<div
bind:this={panes}
class="h-full"
style="max-height: calc(100% - {totalTopGap}px) !important;"
>
<Splitpanes horizontal>
<Pane size={value.type === 'script' ? 30 : 50} minSize={20}>
{#if value.type === 'rawscript'}
<div class="h-full">
<Editor
bind:websocketAlive
bind:this={editor}
class="h-full px-2"
bind:code={value.content}
deno={value.language === RawScript.language.DENO}
lang={scriptLangToEditorLang(value.language)}
automaticLayout={true}
cmdEnterAction={async () => {
selected = 'test'
if (value.type === 'rawscript') {
value.content = editor.getCode()
}
await reload(flowModule)
modulePreview?.runTestWithStepArgs()
}}
on:change={async (event) => {
await reload(flowModule)
}}
formatAction={() => reload(flowModule)}
/>
</div>
{:else if value.type === 'script'}
<FlowModuleScript path={value.path} hash={value.hash} />
{:else if value.type === 'flow'}
<FlowPathViewer path={value.path} />
{/if}
</Pane>
<Pane size={value.type === 'script' ? 70 : 50} minSize={20}>
<Tabs bind:selected>
<Tab value="inputs"><span class="font-semibold">Step Input</span></Tab>
<Tab value="test"><span class="font-semibold text-md">Test this step</span></Tab>
<Tab value="advanced-retries">Advanced</Tab>
</Tabs>
<div class="h-[calc(100%-32px)]">
{#if selected === 'inputs'}
<div class="h-full overflow-auto">
<PropPickerWrapper
pickableProperties={stepPropPicker.pickableProperties}
error={failureModule}
>
<SchemaForm
schema={$flowStateStore[$selectedId]?.schema ?? {}}
inputTransform={true}
previousModuleId={previousModule?.id}
bind:args={value.input_transforms}
bind:extraLib={stepPropPicker.extraLib}
/>
</PropPickerWrapper>
</div>
{:else if selected === 'test'}
<ModulePreview
bind:this={modulePreview}
mod={flowModule}
schema={$flowStateStore[$selectedId]?.schema ?? {}}
/>
{:else if selected.startsWith('advanced')}
<Tabs bind:selected>
<Tab value="advanced-retries">Retries</Tab>
{#if !$selectedId.includes('failure')}
<Tab value="advanced-early-stop">Early Stop/Break</Tab>
<Tab value="advanced-suspend">Suspend</Tab>
<Tab value="advanced-sleep">Sleep</Tab>
<Tab value="advanced-same_worker">Shared Directory</Tab>
{/if}
</Tabs>
{#if selected === 'advanced-retries'}
<FlowRetries bind:flowModule class="px-4 pb-4 h-full overflow-auto" />
{:else if selected === 'advanced-early-stop'}
<FlowModuleEarlyStop bind:flowModule class="px-4 pb-4 h-full overflow-auto" />
{:else if selected === 'advanced-suspend'}
<div class="px-4 pb-4 h-full overflow-auto">
<FlowModuleSuspend bind:flowModule />
</div>
{:else if selected === 'advanced-sleep'}
<div class="px-4 pb-4 h-full overflow-auto">
<FlowModuleSleep previousModuleId={previousModule?.id} bind:flowModule />
</div>
{:else if selected === 'advanced-same_worker'}
<div class="p-4 h-full overflow-auto">
<Alert type="info" title="Share a directory between steps">
If shared directory is set, will share a folder that will be mounted on
`./shared` for each of them to pass data between each other.
</Alert>
<Button
btnClasses="mt-4"
on:click={() => {
$selectedId = 'settings-same-worker'
}}>Set shared directory in the flow settings</Button
>
</div>
{/if}
{/if}
</div>
</Pane>
</Splitpanes>
</div>
</FlowCard>
</div>
{:else}
Incorrect flow module type
{/if}