diff --git a/frontend/src/lib/components/ArgInput.svelte b/frontend/src/lib/components/ArgInput.svelte index 6363370ed9..1d513cbb2e 100644 --- a/frontend/src/lib/components/ArgInput.svelte +++ b/frontend/src/lib/components/ArgInput.svelte @@ -14,7 +14,7 @@ import Icon from 'svelte-awesome' import { fade } from 'svelte/transition' import JsonEditor from './apps/editor/settingsPanel/inputEditor/JsonEditor.svelte' - import { Badge, Button } from './common' + import { Badge, Button, SecondsInput } from './common' import FieldHeader from './FieldHeader.svelte' import type ItemPicker from './ItemPicker.svelte' import NumberTypeNarrowing from './NumberTypeNarrowing.svelte' @@ -236,12 +236,12 @@ {extra['max']} {value} + {:else if extra['seconds'] !== undefined} + {:else} { - dispatch('focus') - }} + on:focus {disabled} type="number" class={valid diff --git a/frontend/src/lib/components/InputTransformForm.svelte b/frontend/src/lib/components/InputTransformForm.svelte index 6d60f3107f..0efae39b79 100644 --- a/frontend/src/lib/components/InputTransformForm.svelte +++ b/frontend/src/lib/components/InputTransformForm.svelte @@ -27,6 +27,7 @@ export let variableEditor: VariableEditor | undefined = undefined export let itemPicker: ItemPicker | undefined = undefined export let noDynamicToggle = false + export let argExtra: Record = {} let monaco: SimpleEditor | undefined = undefined let monacoTemplate: TemplateEditor | undefined = undefined @@ -236,7 +237,7 @@
@@ -280,6 +281,7 @@ bind:itemsType={schema.properties[argName].items} properties={schema.properties[argName].properties} displayHeader={false} + extra={argExtra} {variableEditor} {itemPicker} bind:pickForField diff --git a/frontend/src/lib/components/common/index.ts b/frontend/src/lib/components/common/index.ts index 25dbac40f4..f19d8729e1 100644 --- a/frontend/src/lib/components/common/index.ts +++ b/frontend/src/lib/components/common/index.ts @@ -10,6 +10,7 @@ export { default as Kbd } from './kbd/Kbd.svelte' export { default as Menu } from './menu/Menu.svelte' export { default as MenuItem } from './menu/MenuItem.svelte' export { default as Popup } from './popup/Popup.svelte' +export { default as SecondsInput } from './seconds/SecondsInput.svelte' export { default as Skeleton } from './skeleton/Skeleton.svelte' export { default as Tab } from './tabs/Tab.svelte' export { default as TabContent } from './tabs/TabContent.svelte' diff --git a/frontend/src/lib/components/common/seconds/SecondsInput.svelte b/frontend/src/lib/components/common/seconds/SecondsInput.svelte new file mode 100644 index 0000000000..156e3b278a --- /dev/null +++ b/frontend/src/lib/components/common/seconds/SecondsInput.svelte @@ -0,0 +1,104 @@ + + +
+ {#if !hideDisplay} + + {/if} +
+
+ + +
+
+ + +
+
+
diff --git a/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte b/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte index dc75ad7e5e..8b112bfc43 100644 --- a/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte +++ b/frontend/src/lib/components/flows/content/FlowModuleComponent.svelte @@ -44,6 +44,7 @@ let modulePreview: ModulePreview let websocketAlive = { pyright: false, black: false, deno: false, go: false } let selected = 'inputs' + let advancedSelected = 'retries' let wrapper: HTMLDivElement let panes: HTMLElement let totalTopGap = 0 @@ -104,6 +105,11 @@ } } + function selectAdvanced(subtab: string) { + selected = 'advanced' + advancedSelected = subtab + } + afterUpdate(() => { totalTopGap = 0 if (!(wrapper && panes)) return @@ -127,10 +133,10 @@ (selected = 'advanced-suspend')} - on:toggleSleep={() => (selected = 'advanced-sleep')} - on:toggleRetry={() => (selected = 'advanced-retries')} - on:toggleStopAfterIf={() => (selected = 'advanced-early-stop')} + on:toggleSuspend={() => selectAdvanced('suspend')} + on:toggleSleep={() => selectAdvanced('sleep')} + on:toggleRetry={() => selectAdvanced('retries')} + on:toggleStopAfterIf={() => selectAdvanced('early-stop')} on:fork={async () => { const [module, state] = await fork(flowModule) flowModule = module @@ -217,7 +223,7 @@ Step Input Test this step - Advanced + Advanced
{#if selected === 'inputs'} @@ -240,42 +246,44 @@ mod={flowModule} schema={$flowStateStore[$selectedId]?.schema ?? {}} /> - {:else if selected.startsWith('advanced')} - - Retries + {:else if selected === 'advanced'} + + Retries {#if !$selectedId.includes('failure')} - Early Stop/Break - Suspend - Sleep - Shared Directory + Early Stop/Break + Suspend + Sleep + Shared Directory {/if} - {#if selected === 'advanced-retries'} - - {:else if selected === 'advanced-early-stop'} - - {:else if selected === 'advanced-suspend'} -
- -
- {:else if selected === 'advanced-sleep'} -
- -
- {:else if selected === 'advanced-same_worker'} -
- - 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. - - -
- {/if} +
+ {#if advancedSelected === 'retries'} + + {:else if advancedSelected === 'early-stop'} + + {:else if advancedSelected === 'suspend'} +
+ +
+ {:else if advancedSelected === 'sleep'} +
+ +
+ {:else if advancedSelected === 'same_worker'} +
+ + 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. + + +
+ {/if} +
{/if}
diff --git a/frontend/src/lib/components/flows/content/FlowModuleEarlyStop.svelte b/frontend/src/lib/components/flows/content/FlowModuleEarlyStop.svelte index 4fa0b3ae03..2ea4210d9c 100644 --- a/frontend/src/lib/components/flows/content/FlowModuleEarlyStop.svelte +++ b/frontend/src/lib/components/flows/content/FlowModuleEarlyStop.svelte @@ -20,14 +20,15 @@
-

Early stop/Break +

+ Early stop/Break + If defined, at the end of the step, the predicate expression will be evaluated to decide if the flow should stop early. Skipped flows are just a label useful to not see them in the runs page. If stop early is run within a forloop, it will just break the for-loop and have it stop - at that iteration instead of stopping the whole flow.

+ at that iteration instead of stopping the whole flow. +
+

{ @@ -45,7 +46,7 @@ }} /> -
+
{#if flowModule.stop_after_if} -

Sleep +

+ Sleep + If defined, at the end of the step, the flow will sleep for a number of seconds before scheduling the next job (if any, no effect if the step is the last one). Sleeping is passive and - does not consume any resources.

+ does not consume any resources. +
+

{ @@ -52,8 +52,7 @@ }} />
- Sleep for duration (seconds) - + Sleep for duration {#if flowModule.sleep && schema.properties['sleep']}
{:else} - + {/if}
diff --git a/frontend/src/lib/components/flows/content/FlowModuleSuspend.svelte b/frontend/src/lib/components/flows/content/FlowModuleSuspend.svelte index 03e94cd4dc..61801fe7b6 100644 --- a/frontend/src/lib/components/flows/content/FlowModuleSuspend.svelte +++ b/frontend/src/lib/components/flows/content/FlowModuleSuspend.svelte @@ -4,6 +4,7 @@ import type { FlowModule } from '$lib/gen' import { emptySchema } from '$lib/utils' + import { SecondsInput } from '../../common' export let flowModule: FlowModule @@ -16,14 +17,15 @@ $: isSuspendEnabled = Boolean(flowModule.suspend) -

Suspend +

+ Suspend + If defined, at the end of the step, the flow will be suspended until it receives external requests to be resumed or canceled. This is most useful to implement approval steps but can be used flexibly for other purpose. To get the resume urls, use `wmill.getResumeUrls()` in - Typescript, or `wmill.get_resume_urls()` in Python.

+ Typescript, or `wmill.get_resume_urls()` in Python. +
+

{ @@ -49,11 +51,11 @@ {/if} - Timeout (in seconds) + Timeout {#if flowModule.suspend} - + {:else} - + {/if}
diff --git a/frontend/src/lib/components/flows/content/FlowRetries.svelte b/frontend/src/lib/components/flows/content/FlowRetries.svelte index fcfec8e129..9f7c21a83f 100644 --- a/frontend/src/lib/components/flows/content/FlowRetries.svelte +++ b/frontend/src/lib/components/flows/content/FlowRetries.svelte @@ -9,8 +9,8 @@ flowModule.retry = { ...flowModule.retry, constant: { - attempts: 0, - seconds: 0 + attempts: 1, + seconds: 5 } } } @@ -19,9 +19,9 @@ flowModule.retry = { ...flowModule.retry, exponential: { - attempts: 0, + attempts: 1, multiplier: 1, - seconds: 0 + seconds: 5 } } } @@ -30,69 +30,121 @@ $: isExponentialRetryEnabled = Boolean(flowModule.retry?.exponential) -
-

Retries +
+

+ Retries + If defined, upon error this step will be retried with a delay and a maximum number of attempts as defined below. If both static and exponential delay is defined, the static delay attempts - are tried before the exponential ones.

+ are tried before the exponential ones. + +

- { - if (isConstantRetryEnabled && flowModule.retry?.constant) { - flowModule.retry.constant = undefined - } else { - setConstantRetries() - } - }} - options={{ - right: 'Constant retry enabled' - }} - /> - {#if flowModule.retry?.constant} - Attempts - - Delay (seconds) - - {:else} - Attempts - - Delay (seconds) - - {/if} - - { - if (isExponentialRetryEnabled && flowModule.retry?.exponential) { - flowModule.retry.exponential = undefined - } else { - setExpoentialRetries() - } - }} - options={{ - right: 'Exponential backoff enabled' - }} - /> - {#if flowModule.retry?.exponential} - Attempts - - Mulitplier - additional delay = multiplier * base ^ (# attempts) - - Base - - {:else} - Attempts - - Mulitplier - additional delay = multiplier * seconds ^ (# attempts) - - Base - - {/if} +
+
+
+ { + if (isConstantRetryEnabled && flowModule.retry?.constant) { + flowModule.retry.constant = undefined + } else { + setConstantRetries() + } + }} + options={{ + right: 'Constant retry enabled' + }} + /> +
+ {#if flowModule.retry?.constant} +
Attempts
+ +
Delay (in seconds)
+ + {:else} +
Attempts
+ +
Delay (in seconds)
+ + {/if} +
+ { + if (isExponentialRetryEnabled && flowModule.retry?.exponential) { + flowModule.retry.exponential = undefined + } else { + setExpoentialRetries() + } + }} + options={{ + right: 'Exponential backoff enabled' + }} + /> +
+ {#if flowModule.retry?.exponential} +
Attempts
+ +
Mulitplier
+ delay = multiplier * base ^ (number of attempt) + +
Base (in seconds)
+ + {:else} +
Attempts
+ +
Mulitplier
+ delay = multiplier * base ^ (number of attempt) + +
Base (in seconds)
+ + {/if} +
+
+ {#if true} + {@const { attempts: cAttempts, seconds: cSeconds } = flowModule.retry?.constant || {}} + {@const { + attempts: eAttempts, + seconds: eSeconds, + multiplier + } = flowModule.retry?.exponential || {}} + {@const cArray = Array.from({ length: cAttempts || 0 }, () => cSeconds)} + {@const eArray = Array.from( + { length: eAttempts || 0 }, + (_, i) => (multiplier || 0) * (eSeconds || 0) ** (i + cArray.length + 1) + )} + {@const array = [...cArray, ...eArray]} +
+
Retry attempts
+ {#if array.length > 0} + + + + + + {#each array.slice(1) as delay, i} + {@const index = i + 2} + + + + + {/each} +
1:After {array[0]} second{array[0] === 1 ? '' : 's'}
{index}: + {delay} second{delay === 1 ? '' : 's'} after attempt #{index - 1} + {#if i > cArray.length - 2} + + ({multiplier} * {eSeconds}{index}) + + {/if} +
+ {:else} +
No retries
+ {/if} +
+ {/if} +
+