Files
windmill/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte
T
Diego Imbert 433341b295 feat: assets as a primary concept (#6125)
* assets migration

* parse assets (duckdb)

* iterate on assets

* S3 object Preview

* remove pagination

* filterText

* better occurence list

* tweak

* assets in JobPreview

* clone impl

* AssetsDetectedBadge

* improve DbManagerButton + asset dropdown button

* edit resource btn

* warning when incorrect resource

* +Resource in DuckDB

* +S3 Object editor bar

* nit fix rename

* flow asset badge

* More Generic OnChange

* Highlight assets used in modules

* Show occurence count in flow

* Better UX, avoid moving parts

* nit

* Asset nodes

* move to dedicated Asset ctx

* fix layoutNodes not handling first assetsMap

* explore asset btn in flow asset node

* correct offset

* single computeAssetNodes function

* Fix y positioning of nodes with assets

* resource editor

* write mode node (ui)

* accessType in ctx + fix insert button positioning

* right positioning when mixing read and write nodes

* right positioning when mixing R and W assets

* Better layout fix algorithm

* listAssetsByUsage and asset nodes on transitive usages

* refactor + remove linkAssets

* Refactor to allow for custom R/W modes

* AssetsDropdownButton in flow script editor

* R/W/RW selection and changes node pos in flow

* layoutNodes doesnt need recompute now

* fix wrong assumption that nodes recompute when assets change

* r/w/rw multi toggle

* MultiToggle cool animation + clearable

* rename + 1px nit

* remove mini toggle button group, use ToggleButtonGroup

* Combinator parser that detects R / W asset context

* nit fix missing flex-1

* missing order by

* better ui indication for access type

* special x offset case when only one asset node for clarity

* parse getResource in TS with swc ecma parser

* support load and write s3 detection in TS

* Python asset parser

* support wmill api calls without special $res: or s3:// syntax

* detect out of context asset uris python

* do not use access type override when not ambiguous in flow graph

* parse_assets match case in rust

* AsRef<str> refactor

* From impl

* Save flow assets

* Save script asset usages + fixes + save fallback access types

* asset sub icon

* max total asset node width to avoid overlap

* small refactor

* don't parse comments in duckdb assets

* fix assets clearing on parse error

* fix script asset save in wrong place

* load initial asset fallback access types

* support variables

* ui fixes

* Support S3Object as URI in TS client

* support new syntax in python client

* Support +S3Object in EditorBar for TS and python

* Reduce resource requests in assets page

* import windmill client when necessary

* update s3Types.d.ts

* nit fix

* Show input resources and s3 objects as assets

* improve asset icons

* DarkModeObserver refactor

* asset page tabs

* Moved resource variables and s3object pages to assets tabs

* fetch resource usages

* Get variables usages

* move assets usage dropdown to component

* Revert "move assets usage dropdown to component"

This reverts commit 622ea4ab12.

* Revert "Get variables usages"

This reverts commit b11ced4e29.

* Revert "fetch resource usages"

This reverts commit aa5187ad4b.

* Revert "Moved resource variables and s3object pages to assets tabs"

This reverts commit 4430487be4.

* Revert "asset page tabs"

This reverts commit dacc2f0da5.

* move assets usage dropdown to component

* asset icon in asset pages

* tooltip

* details

* Storage selector in S3 File Picker

* make edge less opaque

* Refactor computeAssetNodes to separate in and out nodes

* AssetsOverflowedNode

* nits

* fix assets not being parsed in flows sometimes

* show asset kind and resource_type

* ui nits

* support res:// in duckdb

* add banner for old deployments

* Fix permissionning

* fix broken disable /enable all

* assets page view permission for operators

* Disable ExploreAssetButton for operators

* asset kind as subtitle

* do not spam getResource in assets page. prob. revert fail

* update assets page on workspace change

* reload storage names on ws change

* delete assets on archive / deletion

* sqlx prepare

* missing update when updating user

* add indexes on asset

* better message

* missing loadInit: false

* dead code

* use transaction

* typo

* update package.json

* update package.json

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2025-07-11 09:56:53 +00:00

898 lines
32 KiB
Svelte

<script lang="ts">
import { createBubbler } from 'svelte/legacy'
const bubble = createBubbler()
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.svelte'
import type { FlowModule, RawScript, ScriptLang } 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 { getContext, onDestroy, tick, untrack } 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 { getFailureStepPropPicker, 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 FlowModuleMockTransitionMessage from './FlowModuleMockTransitionMessage.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 Label from '$lib/components/Label.svelte'
import { enterpriseLicense } from '$lib/stores'
import { isCloudHosted } from '$lib/cloud'
import { loadSchemaFromModule } from '../flowInfers'
import FlowModuleSkip from './FlowModuleSkip.svelte'
import { type Job, JobService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { checkIfParentLoop } from '../utils'
import ModulePreviewResultViewer from '$lib/components/ModulePreviewResultViewer.svelte'
import { refreshStateStore, usePromise } from '$lib/svelte5Utils.svelte'
import { getStepHistoryLoaderContext } from '$lib/components/stepHistoryLoader.svelte'
import AssetsDropdownButton from '$lib/components/assets/AssetsDropdownButton.svelte'
import { inferAssets } from '$lib/infer'
const {
selectedId,
currentEditor,
previewArgs,
flowStateStore,
flowStore,
pathStore,
saveDraft,
customUi,
executionCount
} = getContext<FlowEditorContext>('FlowEditorContext')
interface Props {
flowModule: FlowModule
failureModule?: boolean
preprocessorModule?: boolean
parentModule?: FlowModule | undefined
previousModule: FlowModule | undefined
scriptKind?: 'script' | 'trigger' | 'approval'
scriptTemplate?: 'pgsql' | 'mysql' | 'script' | 'docker' | 'powershell'
noEditor: boolean
enableAi: boolean
savedModule?: FlowModule | undefined
forceTestTab?: boolean
highlightArg?: string
}
let {
flowModule = $bindable(),
failureModule = false,
preprocessorModule = false,
parentModule = $bindable(),
previousModule,
scriptKind = 'script',
scriptTemplate = 'script',
noEditor,
enableAi,
savedModule = undefined,
forceTestTab = false,
highlightArg = undefined
}: Props = $props()
let workspaceScriptTag: string | undefined = $state(undefined)
let workspaceScriptLang: ScriptLang | undefined = $state(undefined)
let diffMode = $state(false)
let editor: Editor | undefined = $state()
let diffEditor: DiffEditor | undefined = $state()
let modulePreview: ModulePreview | undefined = $state()
let websocketAlive = $state({
pyright: false,
deno: false,
go: false,
ruff: false,
shellcheck: false
})
let selected = $state(preprocessorModule ? 'test' : 'inputs')
let advancedSelected = $state('retries')
let advancedRuntimeSelected = $state('concurrency')
let s3Kind = $state('s3_client')
let validCode = $state(true)
let width = $state(1200)
let lastJob: Job | undefined = $state(undefined)
let testJob: Job | undefined = $state(undefined)
let testIsLoading = $state(false)
let scriptProgress = $state(undefined)
function onModulesChange(savedModule: FlowModule | undefined, flowModule: FlowModule) {
// console.log('onModulesChange', savedModule, flowModule)
return savedModule?.value?.type === 'rawscript' &&
flowModule.value.type === 'rawscript' &&
savedModule.value.content !== flowModule.value.content
? savedModule.value.content
: undefined
}
function onKeyDown(event: KeyboardEvent) {
if ((event.ctrlKey || event.metaKey) && event.key == 'Enter') {
event.preventDefault()
selected = 'test'
modulePreview?.runTestWithStepArgs()
}
}
let inputTransformSchemaForm: InputTransformSchemaForm | undefined = $state(undefined)
let reloadError: string | undefined = $state(undefined)
async function reload(flowModule: FlowModule) {
reloadError = undefined
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'
) {
if (!deepEqual(flowModule.value.input_transforms, input_transforms)) {
flowModule.value.input_transforms = input_transforms
}
}
}
if (flowModule.value.type == 'rawscript' && flowModule.value.lock != undefined) {
if (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
reloadError = e?.message
}
}
function selectAdvanced(subtab: string) {
selected = 'advanced'
advancedSelected = subtab
}
let forceReload = $state(0)
let editorPanelSize = $state(noEditor ? 0 : flowModule.value.type == 'script' ? 30 : 50)
let editorSettingsPanelSize = $state(100 - untrack(() => editorPanelSize))
let stepHistoryLoader = getStepHistoryLoaderContext()
function onSelectedIdChange() {
if (!$flowStateStore?.[$selectedId]?.schema && flowModule) {
reload(flowModule)
}
}
async function getLastJob() {
if (
!$flowStateStore ||
!flowModule.id ||
$flowStateStore[flowModule.id]?.previewResult === 'never tested this far' ||
!$flowStateStore[flowModule.id]?.previewJobId ||
!$flowStateStore[flowModule.id]?.previewWorkspaceId
) {
return
}
const job = await JobService.getJob({
workspace: $flowStateStore[flowModule.id]?.previewWorkspaceId ?? '',
id: $flowStateStore[flowModule.id]?.previewJobId ?? ''
})
if (job) {
lastJob = job
}
}
let leftPanelSize = $state(0)
function showDiffMode() {
diffMode = true
diffEditor?.setOriginal((savedModule?.value as RawScript).content ?? '')
diffEditor?.setModified(editor?.getCode() ?? '')
diffEditor?.show()
editor?.hide()
}
function hideDiffMode() {
diffMode = false
diffEditor?.hide()
editor?.show()
}
let lastDeployedCode = $derived(onModulesChange(savedModule, flowModule))
let stepPropPicker = $derived(
$executionCount != undefined && failureModule
? getFailureStepPropPicker($flowStateStore, flowStore.val, previewArgs.val)
: getStepPropPicker(
$flowStateStore,
parentModule,
previousModule,
flowModule.id,
flowStore.val,
previewArgs.val,
false
)
)
$effect.pre(() => {
$selectedId && untrack(() => onSelectedIdChange())
})
$effect(() => {
if (testJob && testJob.type === 'CompletedJob') {
lastJob = $state.snapshot(testJob)
} else if ($workspaceStore && $pathStore && flowModule?.id && $flowStateStore) {
untrack(() => getLastJob())
}
})
let parentLoop = $derived(
flowStore.val && flowModule ? checkIfParentLoop(flowStore.val, flowModule.id) : undefined
)
$effect(() => {
if (selected === 'test') {
leftPanelSize = 50
} else {
leftPanelSize = 100
}
})
$effect(() => {
editor &&
($currentEditor = {
type: 'script',
editor,
stepId: flowModule.id,
showDiffMode,
hideDiffMode,
diffMode,
lastDeployedCode
})
})
onDestroy(() => {
$currentEditor = undefined
})
// Handle force test tab prop with animation
$effect(() => {
if (forceTestTab) {
selected = 'test'
// Add a smooth transition to the test tab
setTimeout(() => {
const testTab = document.querySelector('[value="test"]')
if (testTab) {
testTab.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
}
}, 100)
}
})
let assets = usePromise(
async () =>
flowModule.value.type === 'rawscript'
? await inferAssets(flowModule.value.language, flowModule.value.content)
: undefined,
{ clearValueOnRefresh: false, loadInit: false }
)
$effect(() => {
if (flowModule.value.type !== 'rawscript') return
;[flowModule.value.content, flowModule.value.language]
untrack(() => assets.refresh())
})
let rawScriptLang = $derived(
flowModule.value.type == 'rawscript' ? flowModule.value.language : undefined
)
</script>
<svelte:window onkeydown={onKeyDown} />
{#if flowModule.value}
<div class="h-full" bind:clientWidth={width}>
<FlowCard
flowModuleValue={flowModule?.value}
on:reload={() => {
forceReload++
reload(flowModule)
}}
{noEditor}
on:setHash={(e) => {
if (flowModule.value.type == 'script') {
flowModule.value.hash = e.detail
}
}}
bind:summary={flowModule.summary}
>
{#snippet header()}
<FlowModuleHeader
tag={workspaceScriptTag ?? rawScriptLang ?? workspaceScriptLang}
module={flowModule}
on:tagChange={(e) => {
console.log('tagChange', e.detail)
if (flowModule.value.type == 'script') {
flowModule.value.tag_override = e.detail
} else if (flowModule.value.type == 'rawscript') {
flowModule.value.tag = e.detail
}
}}
on:toggleSuspend={() => selectAdvanced('suspend')}
on:toggleSleep={() => selectAdvanced('sleep')}
on:toggleMock={() => selectAdvanced('mock')}
on:toggleRetry={() => selectAdvanced('retries')}
on:togglePin={() => (selected = 'test')}
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)
}
if (flowModule.value.type == 'flow') {
forceReload++
await reload(flowModule)
}
}}
on:createScriptFromInlineScript={async () => {
const [module, state] = await createScriptFromInlineScript(
flowModule,
$selectedId,
$flowStateStore[flowModule.id].schema,
$pathStore
)
if (flowModule.value.type == 'rawscript') {
module.value.input_transforms = flowModule.value.input_transforms
}
flowModule = module
$flowStateStore[module.id] = state
}}
/>
{/snippet}
<div class="h-full flex flex-col">
{#if flowModule.value.type === 'rawscript' && !noEditor}
<div class="shadow-sm px-1 border-b-1 border-gray-200 dark:border-gray-700">
<EditorBar
customUi={customUi?.editorBar}
{validCode}
{editor}
{diffEditor}
lang={flowModule.value['language'] ?? 'deno'}
{websocketAlive}
iconOnly={width < 950}
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
}, {})}
on:showDiffMode={showDiffMode}
on:hideDiffMode={hideDiffMode}
{lastDeployedCode}
{diffMode}
openAiChat
/>
</div>
{/if}
<div class="min-h-0 flex-grow" id="flow-editor-editor">
<Splitpanes horizontal>
<Pane bind:size={editorPanelSize} minSize={10} class="relative">
{#if flowModule.value.type === 'rawscript'}
{#if !noEditor}
{#key flowModule.id}
<div class="absolute top-2 right-4 z-10 flex flex-row gap-2">
{#if assets.value?.length}
<AssetsDropdownButton
assets={assets.value}
bind:fallbackAccessTypes={flowModule.value.asset_fallback_access_types}
/>
{/if}
</div>
<Editor
loadAsync
folding
path={$pathStore + '/' + flowModule.id}
bind:websocketAlive
bind:this={editor}
class="h-full relative"
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' && editor) {
flowModule.value.content = editor.getCode()
}
await reload(flowModule)
modulePreview?.runTestWithStepArgs()
}
}}
on:change={async (event) => {
const content = event.detail
if (flowModule.value.type === 'rawscript') {
if (flowModule.value.content !== content) {
flowModule.value.content = content
}
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
},
{}
)}
key={`flow-inline-${$workspaceStore}-${$pathStore}-${flowModule.id}`}
/>
<DiffEditor
open={false}
bind:this={diffEditor}
automaticLayout
fixedOverflowWidgets
defaultLang={scriptLangToEditorLang(flowModule.value.language)}
class="h-full"
/>
{/key}
{/if}
{:else if flowModule.value.type === 'script'}
{#if !noEditor && (customUi?.hubCode != false || !flowModule?.value?.path?.startsWith('hub/'))}
<div class="border-t">
{#key forceReload}
<FlowModuleScript
bind:tag={workspaceScriptTag}
bind:language={workspaceScriptLang}
showAllCode={false}
path={flowModule.value.path}
hash={flowModule.value.hash}
/>
{/key}
</div>
{/if}
{:else if flowModule.value.type === 'flow'}
{#key forceReload}
<FlowPathViewer path={flowModule.value.path} />
{/key}
{/if}
</Pane>
<Pane bind:size={editorSettingsPanelSize} minSize={20}>
<Splitpanes>
<Pane minSize={36} bind:size={leftPanelSize}>
<Tabs bind:selected>
{#if !preprocessorModule}
<Tab value="inputs">Step Input</Tab>
{/if}
<Tab value="test">Test this step</Tab>
{#if !preprocessorModule}
<Tab value="advanced">Advanced</Tab>
{/if}
</Tabs>
<div
class={advancedSelected === 'runtime'
? 'h-[calc(100%-68px)]'
: 'h-[calc(100%-34px)]'}
>
{#if selected === 'inputs' && (flowModule.value.type == 'rawscript' || flowModule.value.type == 'script' || flowModule.value.type == 'flow')}
<div class="h-full overflow-auto bg-surface" id="flow-editor-step-input">
<PropPickerWrapper
pickableProperties={stepPropPicker.pickableProperties}
error={failureModule}
noPadding
>
{#if reloadError}
<div
title={reloadError}
class="absolute left-2 top-2 rounded-full w-2 h-2 bg-red-300"
></div>
{/if}
<InputTransformSchemaForm
class="px-1 xl:px-2"
bind:this={inputTransformSchemaForm}
pickableProperties={stepPropPicker.pickableProperties}
schema={$flowStateStore[$selectedId]?.schema ?? {}}
previousModuleId={previousModule?.id}
bind:args={
() => {
// @ts-ignore
return flowModule?.value?.input_transforms
},
(v) => {
if (
typeof flowModule?.value === 'object' &&
flowModule?.value !== null
) {
// @ts-ignore
flowModule.value.input_transforms = v
}
}
}
extraLib={stepPropPicker.extraLib}
{enableAi}
/>
</PropPickerWrapper>
</div>
{:else if selected === 'test'}
<ModulePreview
pickableProperties={stepPropPicker.pickableProperties}
bind:this={modulePreview}
mod={flowModule}
{noEditor}
schema={$flowStateStore[$selectedId]?.schema ?? {}}
bind:testJob
bind:testIsLoading
bind:scriptProgress
focusArg={highlightArg}
/>
{: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 || flowModule.stop_after_all_iters_if
)}
>
Early Stop
</Tab>
<Tab value="skip" active={Boolean(flowModule.skip_if)}>Skip</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">
{#snippet 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>
{/snippet}
<span class="text-2xs"
>After all retries attempts have been exhausted:</span
>
<div class="flex gap-2 mb-4">
<Toggle
size="xs"
bind:checked={flowModule.continue_on_error}
options={{
left: 'Stop on error and propagate error up',
right: "Continue on error with error as step's return"
}}
/>
<Tooltip>
When enabled, the flow will continue to the next step after going
through all the retries (if any) even if this step fails. This
enables to process the error in a branch one for instance.
</Tooltip>
</div>
<div class="my-8"></div>
<FlowRetries bind:flowModuleRetry={flowModule.retry} />
</Section>
{:else if advancedSelected === 'runtime' && advancedRuntimeSelected === 'concurrency'}
<Section label="Concurrency limits" class="flex flex-col gap-4" eeOnly>
{#snippet header()}
<Tooltip>Allowed concurrency within a given timeframe</Tooltip>
{/snippet}
{#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>
<Label label="Custom concurrency key (optional)">
{#snippet header()}
<Tooltip>
Concurrency keys are global, you can have them be workspace
specific using the variable `$workspace`. You can also use an
argument's value using `$args[name_of_arg]`</Tooltip
>
{/snippet}
<!-- svelte-ignore a11y_autofocus -->
<input
type="text"
autofocus
disabled={!$enterpriseLicense}
bind:value={flowModule.value.custom_concurrency_key}
placeholder={`$workspace/script/${$pathStore}-$args[foo]`}
/>
</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">
{#snippet header()}
<Tooltip>The higher the number, the higher the priority.</Tooltip>
{/snippet}
<input
type="number"
class="!w-24"
disabled={flowModule.priority === undefined}
bind:value={flowModule.priority}
onfocus={bubble('focus')}
onchange={() => {
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 === 'skip'}
<FlowModuleSkip bind:flowModule {parentModule} {previousModule} />
{: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>
<FlowModuleMockTransitionMessage />
</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">
{#snippet children({ item })}
{#if flowModule.value['language'] === 'deno'}
<ToggleButton
value="s3_client"
size="sm"
label="S3 lite client"
{item}
/>
{:else}
<ToggleButton
value="s3_client"
size="sm"
label="Boto3"
{item}
/>
<ToggleButton value="polars" size="sm" label="Polars" {item} />
<ToggleButton value="duckdb" size="sm" label="DuckDB" {item} />
{/if}
{/snippet}
</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>
{#if selected === 'test'}
<Pane minSize={20} class="relative">
{#if stepHistoryLoader?.stepStates[flowModule.id]?.initial && !flowModule.mock?.enabled}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
onclick={() => {
stepHistoryLoader?.resetInitial(flowModule.id)
}}
class="cursor-pointer h-full hover:bg-gray-500/20 dark:hover:bg-gray-500/20 dark:bg-gray-500/80 bg-gray-500/40 absolute top-0 left-0 w-full z-50"
>
<div class="text-center text-primary text-sm py-2 pt-20"
><span class="font-bold border p-2 bg-surface-secondary rounded-md"
>Run loaded from history</span
></div
>
</div>
{/if}
<ModulePreviewResultViewer
lang={flowModule.value['language'] ?? 'deno'}
{editor}
{diffEditor}
loopStatus={parentLoop
? { type: 'inside', flow: parentLoop.type }
: undefined}
onUpdateMock={(detail) => {
flowModule.mock = detail
flowModule = flowModule
refreshStateStore(flowStore)
}}
{lastJob}
{scriptProgress}
{testJob}
mod={flowModule}
{testIsLoading}
disableMock={preprocessorModule || failureModule}
disableHistory={failureModule}
loadingJob={stepHistoryLoader?.stepStates[flowModule.id]?.loadingJobs}
/>
</Pane>
{/if}
</Splitpanes>
</Pane>
</Splitpanes>
</div>
</div>
</FlowCard>
</div>
{:else}
Incorrect flow module type
{/if}