Files
windmill/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte
2024-03-01 01:12:20 +01:00

565 lines
20 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 Toggle from '$lib/components/Toggle.svelte'
import { createScriptFromInlineScript, fork } from '$lib/components/flows/flowStateUtils'
import type { FlowModule } from '$lib/gen'
import FlowCard from '../common/FlowCard.svelte'
import FlowModuleHeader from './FlowModuleHeader.svelte'
import { getLatestHashForScript, scriptLangToEditorLang } from '$lib/scripts'
import PropPickerWrapper from '../propPicker/PropPickerWrapper.svelte'
import { afterUpdate, getContext, tick } from 'svelte'
import type { FlowEditorContext } from '../types'
import FlowModuleScript from './FlowModuleScript.svelte'
import FlowModuleEarlyStop from './FlowModuleEarlyStop.svelte'
import FlowModuleSuspend from './FlowModuleSuspend.svelte'
import FlowModuleCache from './FlowModuleCache.svelte'
import FlowModuleDeleteAfterUse from './FlowModuleDeleteAfterUse.svelte'
import FlowRetries from './FlowRetries.svelte'
import { getStepPropPicker } from '../previousResults'
import { deepEqual } from 'fast-equals'
import Section from '$lib/components/Section.svelte'
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'
import InputTransformSchemaForm from '$lib/components/InputTransformSchemaForm.svelte'
import { schemaToObject } from '$lib/schema'
import FlowModuleMock from './FlowModuleMock.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import { SecondsInput } from '$lib/components/common'
import DiffEditor from '$lib/components/DiffEditor.svelte'
import FlowModuleTimeout from './FlowModuleTimeout.svelte'
import HighlightCode from '$lib/components/HighlightCode.svelte'
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
import s3Scripts from './s3Scripts/lib'
import type { FlowCopilotContext } from '$lib/components/copilot/flow'
import Label from '$lib/components/Label.svelte'
import { enterpriseLicense } from '$lib/stores'
import { isCloudHosted } from '$lib/cloud'
import { loadSchemaFromModule } from '../flowInfers'
const { selectedId, previewArgs, flowStateStore, flowStore, pathStore, saveDraft } =
getContext<FlowEditorContext>('FlowEditorContext')
export let flowModule: FlowModule
export let failureModule: boolean = false
export let parentModule: FlowModule | undefined = undefined
export let previousModule: FlowModule | undefined
export let scriptKind: 'script' | 'trigger' | 'approval' = 'script'
export let scriptTemplate: 'pgsql' | 'mysql' | 'script' | 'docker' | 'powershell' = 'script'
export let noEditor: boolean
export let enableAi: boolean
let editor: Editor
let diffEditor: DiffEditor
let modulePreview: ModulePreview
let websocketAlive = {
pyright: false,
black: false,
deno: false,
go: false,
ruff: false,
shellcheck: false
}
let selected = 'inputs'
let advancedSelected = 'retries'
let advancedRuntimeSelected = 'concurrency'
let s3Kind = 's3_client'
let wrapper: HTMLDivElement
let panes: HTMLElement
let totalTopGap = 0
let validCode = true
let width = 1200
const { modulesStore: copilotModulesStore } =
getContext<FlowCopilotContext | undefined>('FlowCopilotContext') || {}
function setCopilotModuleEditor() {
copilotModulesStore?.update((modules) => {
const module = modules.find((m) => m.id === flowModule.id)
if (module) {
module.editor = editor
}
return modules
})
}
$: editor !== undefined && setCopilotModuleEditor()
$: stepPropPicker = failureModule
? {
pickableProperties: {
flow_input: schemaToObject($flowStore.schema as any, $previewArgs),
priorIds: {},
previousId: undefined,
hasResume: false
},
extraLib: ''
}
: getStepPropPicker(
$flowStateStore,
parentModule,
previousModule,
flowModule.id,
$flowStore,
$previewArgs,
false
)
function onKeyDown(event: KeyboardEvent) {
if ((event.ctrlKey || event.metaKey) && event.key == 'Enter') {
event.preventDefault()
selected = 'test'
modulePreview?.runTestWithStepArgs()
}
}
let inputTransformSchemaForm: InputTransformSchemaForm | undefined = undefined
async function reload(flowModule: FlowModule) {
try {
const { input_transforms, schema } = await loadSchemaFromModule(flowModule)
validCode = true
if (inputTransformSchemaForm) {
inputTransformSchemaForm.setArgs(input_transforms)
} else {
if (
flowModule.value.type == 'rawscript' ||
flowModule.value.type == 'script' ||
flowModule.value.type == 'flow'
) {
flowModule.value.input_transforms = input_transforms
}
}
if (flowModule.value.type == 'rawscript' && flowModule.value.lock != undefined) {
flowModule.value.lock = undefined
}
await tick()
if (!deepEqual(schema, $flowStateStore[flowModule.id]?.schema)) {
if (!$flowStateStore[flowModule.id]) {
$flowStateStore[flowModule.id] = { schema }
} else {
$flowStateStore[flowModule.id].schema = schema
}
}
} catch (e) {
validCode = false
}
}
function selectAdvanced(subtab: string) {
selected = 'advanced'
advancedSelected = subtab
}
afterUpdate(() => {
totalTopGap = 0
if (!(wrapper && panes)) return
const wrapperTop = wrapper.getBoundingClientRect().top
const panesTop = panes.getBoundingClientRect().top
totalTopGap = panesTop - wrapperTop
})
let forceReload = 0
let editorPanelSize = noEditor ? 0 : flowModule.value.type == 'script' ? 30 : 50
let editorSettingsPanelSize = 100 - editorPanelSize
</script>
<svelte:window on:keydown={onKeyDown} />
{#if flowModule.value}
<div class="h-full" bind:this={wrapper} bind:clientWidth={width}>
<FlowCard
on:reload={() => {
forceReload++
reload(flowModule)
}}
{noEditor}
bind:flowModule
>
<svelte:fragment slot="header">
<FlowModuleHeader
bind:module={flowModule}
on:toggleSuspend={() => selectAdvanced('suspend')}
on:toggleSleep={() => selectAdvanced('sleep')}
on:toggleMock={() => selectAdvanced('mock')}
on:toggleRetry={() => selectAdvanced('retries')}
on:toggleConcurrency={() => selectAdvanced('runtime')}
on:toggleCache={() => selectAdvanced('cache')}
on:toggleStopAfterIf={() => selectAdvanced('early-stop')}
on:fork={async () => {
const [module, state] = await fork(flowModule)
flowModule = module
$flowStateStore[module.id] = state
}}
on:reload={async () => {
if (flowModule.value.type == 'script') {
if (flowModule.value.hash != undefined) {
flowModule.value.hash = await getLatestHashForScript(flowModule.value.path)
}
forceReload++
await reload(flowModule)
}
}}
on:createScriptFromInlineScript={async () => {
const [module, state] = await createScriptFromInlineScript(
flowModule,
$selectedId,
$flowStateStore[flowModule.id].schema,
$flowStore,
$pathStore
)
flowModule = module
$flowStateStore[module.id] = state
}}
/>
</svelte:fragment>
{#if flowModule.value.type === 'rawscript' && !noEditor}
<div class="border-b-2 shadow-sm px-1">
<EditorBar
{validCode}
{editor}
{diffEditor}
lang={flowModule.value['language'] ?? 'deno'}
{websocketAlive}
iconOnly={width < 850}
kind={scriptKind}
template={scriptTemplate}
args={Object.entries(flowModule.value.input_transforms).reduce((acc, [key, obj]) => {
acc[key] = obj.type === 'static' ? obj.value : undefined
return acc
}, {})}
/>
</div>
{/if}
<div
bind:this={panes}
class="h-full"
style="max-height: calc(100% - {totalTopGap}px) !important;"
id="flow-editor-editor"
>
<Splitpanes horizontal>
<Pane bind:size={editorPanelSize} minSize={20}>
{#if flowModule.value.type === 'rawscript'}
{#if !noEditor}
{#key flowModule.id}
<Editor
folding
path={flowModule.value.path}
bind:websocketAlive
bind:this={editor}
class="h-full relative"
bind:code={flowModule.value.content}
lang={scriptLangToEditorLang(flowModule.value.language)}
scriptLang={flowModule.value.language}
automaticLayout={true}
cmdEnterAction={async () => {
selected = 'test'
if ($selectedId == flowModule.id) {
if (flowModule.value.type === 'rawscript') {
flowModule.value.content = editor.getCode()
}
await reload(flowModule)
modulePreview?.runTestWithStepArgs()
}
}}
on:change={async (event) => {
if (flowModule.value.type === 'rawscript') {
flowModule.value.content = event.detail
}
await reload(flowModule)
}}
formatAction={() => {
reload(flowModule)
saveDraft()
}}
fixedOverflowWidgets={true}
args={Object.entries(flowModule.value.input_transforms).reduce(
(acc, [key, obj]) => {
acc[key] = obj.type === 'static' ? obj.value : undefined
return acc
},
{}
)}
/>
<DiffEditor
bind:this={diffEditor}
automaticLayout
fixedOverflowWidgets
class="hidden h-full"
/>
{/key}
{/if}
{:else if flowModule.value.type === 'script'}
{#if !noEditor}
<div class="border-t">
{#key forceReload}
<FlowModuleScript path={flowModule.value.path} hash={flowModule.value.hash} />
{/key}
</div>
{/if}
{:else if flowModule.value.type === 'flow'}
<FlowPathViewer path={flowModule.value.path} />
{/if}
</Pane>
<Pane bind:size={editorSettingsPanelSize} minSize={20}>
<Tabs bind:selected>
<Tab value="inputs">Step Input</Tab>
<Tab value="test">Test this step</Tab>
<Tab value="advanced">Advanced</Tab>
</Tabs>
<div
class={advancedSelected === 'runtime' ? 'h-[calc(100%-64px)]' : 'h-[calc(100%-32px)]'}
>
{#if selected === 'inputs' && (flowModule.value.type == 'rawscript' || flowModule.value.type == 'script' || flowModule.value.type == 'flow')}
<div class="h-full overflow-auto" id="flow-editor-step-input">
<PropPickerWrapper
pickableProperties={stepPropPicker.pickableProperties}
error={failureModule}
>
<InputTransformSchemaForm
bind:this={inputTransformSchemaForm}
pickableProperties={stepPropPicker.pickableProperties}
schema={$flowStateStore[$selectedId]?.schema ?? {}}
previousModuleId={previousModule?.id}
bind:args={flowModule.value.input_transforms}
extraLib={stepPropPicker.extraLib}
{enableAi}
/>
</PropPickerWrapper>
</div>
{:else if selected === 'test'}
<ModulePreview
pickableProperties={stepPropPicker.pickableProperties}
bind:this={modulePreview}
mod={flowModule}
{editor}
{diffEditor}
{noEditor}
lang={flowModule.value['language'] ?? 'deno'}
schema={$flowStateStore[$selectedId]?.schema ?? {}}
/>
{:else if selected === 'advanced'}
<Tabs bind:selected={advancedSelected}>
<Tab value="retries" active={flowModule.retry !== undefined}>Retries</Tab>
{#if !$selectedId.includes('failure')}
<Tab value="runtime">Runtime</Tab>
<Tab value="cache" active={Boolean(flowModule.cache_ttl)}>Cache</Tab>
<Tab value="early-stop" active={Boolean(flowModule.stop_after_if)}>
Early Stop
</Tab>
<Tab value="suspend" active={Boolean(flowModule.suspend)}>Suspend</Tab>
<Tab value="sleep" active={Boolean(flowModule.sleep)}>Sleep</Tab>
<Tab value="mock" active={Boolean(flowModule.mock?.enabled)}>Mock</Tab>
<Tab value="same_worker">Shared Directory</Tab>
{#if flowModule.value['language'] === 'python3' || flowModule.value['language'] === 'deno'}
<Tab value="s3">S3</Tab>
{/if}
{/if}
</Tabs>
{#if advancedSelected === 'runtime'}
<Tabs bind:selected={advancedRuntimeSelected}>
<Tab value="concurrency">Concurrency</Tab>
<Tab value="timeout">Timeout</Tab>
<Tab value="priority">Priority</Tab>
<Tab value="lifetime">Lifetime</Tab>
</Tabs>
{/if}
<div class="h-[calc(100%-32px)] overflow-auto p-4">
{#if advancedSelected === 'retries'}
<Section label="Retries">
<svelte:fragment slot="header">
<Tooltip documentationLink="https://www.windmill.dev/docs/flows/retries">
If defined, upon error this step will be retried with a delay and a
maximum number of attempts as defined below.
</Tooltip>
</svelte:fragment>
<FlowRetries bind:flowModuleRetry={flowModule.retry} />
</Section>
{:else if advancedSelected === 'runtime' && advancedRuntimeSelected === 'concurrency'}
<Section label="Concurrency Limits" class="flex flex-col gap-4" eeOnly>
<svelte:fragment slot="header">
<Tooltip>Allowed concurrency within a given timeframe</Tooltip>
</svelte:fragment>
{#if flowModule.value.type == 'rawscript'}
<Label label="Max number of executions within the time window">
<div class="flex flex-row gap-2 max-w-sm">
<input
disabled={!$enterpriseLicense}
bind:value={flowModule.value.concurrent_limit}
type="number"
/>
<Button
size="xs"
color="light"
variant="border"
on:click={() => {
if (flowModule.value.type == 'rawscript') {
flowModule.value.concurrent_limit = undefined
}
}}
>
<div class="flex flex-row gap-2"> Remove Limits </div>
</Button>
</div>
</Label>
<Label label="Time window in seconds">
<SecondsInput
disabled={!$enterpriseLicense}
bind:seconds={flowModule.value.concurrency_time_window_s}
/>
</Label>
{:else}
<Alert type="warning" title="Limitation" size="xs">
The concurrency limit of a workspace script is only settable in the script
metadata itself. For hub scripts, this feature is non available yet.
</Alert>
{/if}
</Section>
{:else if advancedSelected === 'runtime' && advancedRuntimeSelected === 'timeout'}
<div>
<FlowModuleTimeout bind:flowModule />
</div>
{:else if advancedSelected === 'runtime' && advancedRuntimeSelected === 'priority'}
<Section label="Priority" class="flex flex-col gap-4">
<!-- TODO: Add EE-only badge when we have it -->
<Toggle
disabled={!$enterpriseLicense || isCloudHosted()}
checked={flowModule.priority !== undefined && flowModule.priority > 0}
on:change={() => {
if (flowModule.priority) {
flowModule.priority = undefined
} else {
flowModule.priority = 100
}
}}
options={{
right: 'Enabled high priority flow step',
rightTooltip: `Jobs scheduled from this step when the flow is executed are labeled as high priority and take precedence over the other jobs in the jobs queue. ${
!$enterpriseLicense
? 'This is a feature only available on enterprise edition.'
: ''
}`
}}
/>
<Label label="Priority number">
<svelte:fragment slot="header">
<Tooltip>The higher the number, the higher the priority.</Tooltip>
</svelte:fragment>
<input
type="number"
class="!w-24"
disabled={flowModule.priority === undefined}
bind:value={flowModule.priority}
on:focus
on:change={() => {
if (flowModule.priority && flowModule.priority > 100) {
flowModule.priority = 100
} else if (flowModule.priority && flowModule.priority < 0) {
flowModule.priority = 0
}
}}
/>
</Label>
<Alert type="warning" title="Limitation" size="xs">
Setting priority is only available for enterprise edition and not available
on the cloud.
</Alert>
</Section>
{:else if advancedSelected === 'runtime' && advancedRuntimeSelected === 'lifetime'}
<div>
<FlowModuleDeleteAfterUse bind:flowModule disabled={!$enterpriseLicense} />
</div>
{:else if advancedSelected === 'cache'}
<div>
<FlowModuleCache bind:flowModule />
</div>
{:else if advancedSelected === 'early-stop'}
<FlowModuleEarlyStop bind:flowModule />
{:else if advancedSelected === 'suspend'}
<div>
<FlowModuleSuspend previousModuleId={previousModule?.id} bind:flowModule />
</div>
{:else if advancedSelected === 'sleep'}
<div>
<FlowModuleSleep previousModuleId={previousModule?.id} bind:flowModule />
</div>
{:else if advancedSelected === 'mock'}
<div>
<FlowModuleMock bind:flowModule />
</div>
{:else if advancedSelected === 'same_worker'}
<div>
<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>
{:else if advancedSelected === 's3'}
<div>
<h2 class="pb-4">
S3 snippets
<Tooltip>
Read/Write object from/to S3 and leverage Polars and DuckDB to run
efficient ETL processes.
</Tooltip>
</h2>
</div>
<div class="flex gap-2 justify-between mb-4 items-center">
<div class="flex gap-2">
<ToggleButtonGroup bind:selected={s3Kind} class="w-auto">
{#if flowModule.value['language'] === 'deno'}
<ToggleButton value="s3_client" size="sm" label="S3 lite client" />
{:else}
<ToggleButton value="s3_client" size="sm" label="Boto3" />
<ToggleButton value="polars" size="sm" label="Polars" />
<ToggleButton value="duckdb" size="sm" label="DuckDB" />
{/if}
</ToggleButtonGroup>
</div>
<Button
size="xs"
on:click={() =>
editor.setCode(s3Scripts[flowModule.value['language']][s3Kind])}
>
Apply snippet
</Button>
</div>
<HighlightCode
language={flowModule.value['language']}
code={s3Scripts[flowModule.value['language']][s3Kind]}
/>
{/if}
</div>
{/if}
</div>
</Pane>
</Splitpanes>
</div>
</FlowCard>
</div>
{:else}
Incorrect flow module type
{/if}