feat(frontend): Add seconds input (#1445)

* feat(frontend): Add seconds input

* add more instances

* fix header toggles

* add retry calculations
This commit is contained in:
Ádám Kovács
2023-04-21 14:35:50 +02:00
committed by GitHub
parent 3d031c7017
commit 30bf7ad3e9
9 changed files with 304 additions and 134 deletions
+4 -4
View File
@@ -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 @@
<span>{extra['max']}</span>
<span class="mx-2"><Badge large color="blue">{value}</Badge></span>
</div>
{:else if extra['seconds'] !== undefined}
<SecondsInput bind:seconds={value} on:focus />
{:else}
<input
{autofocus}
on:focus={(e) => {
dispatch('focus')
}}
on:focus
{disabled}
type="number"
class={valid
@@ -27,6 +27,7 @@
export let variableEditor: VariableEditor | undefined = undefined
export let itemPicker: ItemPicker | undefined = undefined
export let noDynamicToggle = false
export let argExtra: Record<string, any> = {}
let monaco: SimpleEditor | undefined = undefined
let monacoTemplate: TemplateEditor | undefined = undefined
@@ -236,7 +237,7 @@
<div class="max-w-xs" />
<div
class="relative mb-6 {$propPickerConfig?.propName == argName
class="relative {$propPickerConfig?.propName == argName
? 'outline outline-offset-0 outline-2 outline-blue-500 rounded-md'
: ''}"
>
@@ -280,6 +281,7 @@
bind:itemsType={schema.properties[argName].items}
properties={schema.properties[argName].properties}
displayHeader={false}
extra={argExtra}
{variableEditor}
{itemPicker}
bind:pickForField
@@ -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'
@@ -0,0 +1,104 @@
<script lang="ts">
type TimeUnit = number | undefined
const ONE_DAY_IN_SECONDS = 86400 as const
const ONE_HOUR_IN_SECONDS = 3600 as const
const ONE_MINUTE_IN_SECONDS = 60 as const
export let seconds = 0
export let hideDisplay = false
export let disabled = false
let day: TimeUnit = undefined
let hour: TimeUnit = undefined
let min: TimeUnit = undefined
let sec: TimeUnit = undefined
$: convertSecondsToTime(seconds)
function convertSecondsToTime(seconds) {
day = Math.floor(seconds / ONE_DAY_IN_SECONDS)
seconds -= day * ONE_DAY_IN_SECONDS
day = day || undefined
hour = Math.floor(seconds / ONE_HOUR_IN_SECONDS)
seconds -= hour * ONE_HOUR_IN_SECONDS
hour = hour || undefined
min = Math.floor(seconds / ONE_MINUTE_IN_SECONDS)
seconds -= min * ONE_MINUTE_IN_SECONDS
min = min || undefined
sec = seconds || undefined
}
function convertUnitsToSeconds() {
seconds =
(day || 0) * ONE_DAY_IN_SECONDS +
(hour || 0) * ONE_HOUR_IN_SECONDS +
(min || 0) * ONE_MINUTE_IN_SECONDS +
(sec || 0)
if (seconds < 0) seconds = 0
}
</script>
<div>
{#if !hideDisplay}
<input
value={disabled ? '' : seconds + ' second' + (seconds === 1 ? '' : 's')}
{disabled}
readonly
type="text"
class="max-w-[248px] bg-gray-50 mb-2"
/>
{/if}
<div class="flex flex-wrap items-center gap-2 text-xs font-medium">
<div class="flex items-center gap-2">
<label>
Sec
<input
type="number"
class="!w-14"
{disabled}
bind:value={sec}
on:change={convertUnitsToSeconds}
on:focus
/>
</label>
<label>
Min
<input
type="number"
class="!w-14"
{disabled}
bind:value={min}
on:change={convertUnitsToSeconds}
on:focus
/>
</label>
</div>
<div class="flex items-center gap-2">
<label>
Hour
<input
type="number"
class="!w-14"
{disabled}
bind:value={hour}
on:change={convertUnitsToSeconds}
on:focus
/>
</label>
<label>
Day
<input
type="number"
class="!w-14"
{disabled}
bind:value={day}
on:change={convertUnitsToSeconds}
on:focus
/>
</label>
</div>
</div>
</div>
@@ -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 @@
<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: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 @@
<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>
<Tab value="advanced">Advanced</Tab>
</Tabs>
<div class="h-[calc(100%-32px)]">
{#if selected === 'inputs'}
@@ -240,42 +246,44 @@
mod={flowModule}
schema={$flowStateStore[$selectedId]?.schema ?? {}}
/>
{:else if selected.startsWith('advanced')}
<Tabs bind:selected>
<Tab value="advanced-retries">Retries</Tab>
{:else if selected === 'advanced'}
<Tabs bind:selected={advancedSelected}>
<Tab value="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>
<Tab value="early-stop">Early Stop/Break</Tab>
<Tab value="suspend">Suspend</Tab>
<Tab value="sleep">Sleep</Tab>
<Tab value="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}
<div class="h-[calc(100%-32px)] overflow-auto p-4">
{#if advancedSelected === 'retries'}
<FlowRetries bind:flowModule />
{:else if advancedSelected === 'early-stop'}
<FlowModuleEarlyStop bind:flowModule />
{:else if advancedSelected === 'suspend'}
<div>
<FlowModuleSuspend bind:flowModule />
</div>
{:else if advancedSelected === 'sleep'}
<div>
<FlowModuleSleep previousModuleId={previousModule?.id} 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>
{/if}
</div>
{/if}
</div>
</Pane>
@@ -20,14 +20,15 @@
</script>
<div class="flex flex-col items-start space-y-2 {$$props.class}">
<h2 class="mt-2"
>Early stop/Break<Tooltip>
<h2>
Early stop/Break
<Tooltip>
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.</Tooltip
></h2
>
at that iteration instead of stopping the whole flow.
</Tooltip>
</h2>
<Toggle
checked={isStopAfterIfEnabled}
on:change={() => {
@@ -45,7 +46,7 @@
}}
/>
<div class="w-full border p-2 flex flex-col {flowModule.stop_after_if ? '' : 'bg-gray-50'}">
<div class="w-full border p-2 flex flex-col {flowModule.stop_after_if ? '' : 'bg-gray-50'}">
{#if flowModule.stop_after_if}
<Toggle
bind:checked={flowModule.stop_after_if.skip_if_stopped}
@@ -3,12 +3,12 @@
import type SimpleEditor from '$lib/components/SimpleEditor.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import type { FlowModule } from '$lib/gen'
import { emptySchema } from '$lib/utils'
import { getContext } from 'svelte'
import PropPickerWrapper from '../propPicker/PropPickerWrapper.svelte'
import type { FlowEditorContext } from '../types'
import { SecondsInput } from '../../common'
export let flowModule: FlowModule
export let previousModuleId: string | undefined
@@ -17,8 +17,7 @@
let schema = emptySchema()
schema.properties['sleep'] = {
type: 'number',
description: 'Sleep time in seconds'
type: 'number'
}
let editor: SimpleEditor | undefined = undefined
@@ -28,13 +27,14 @@
$: isSleepEnabled = Boolean(flowModule.sleep)
</script>
<h2 class="mt-2"
>Sleep<Tooltip>
<h2>
Sleep
<Tooltip>
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.</Tooltip
></h2
>
does not consume any resources.
</Tooltip>
</h2>
<Toggle
checked={isSleepEnabled}
on:change={() => {
@@ -52,8 +52,7 @@
}}
/>
<div>
<span class="text-xs font-bold">Sleep for duration (seconds)</span>
<span class="text-xs font-bold">Sleep for duration</span>
{#if flowModule.sleep && schema.properties['sleep']}
<div class="border">
<PropPickerWrapper
@@ -71,10 +70,11 @@
argName="sleep"
{schema}
{previousModuleId}
argExtra={{ seconds: true }}
/>
</PropPickerWrapper>
</div>
{:else}
<input type="number" disabled />
<SecondsInput disabled />
{/if}
</div>
@@ -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)
</script>
<h2 class="mt-2"
>Suspend<Tooltip>
<h2>
Suspend
<Tooltip>
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.</Tooltip
></h2
>
Typescript, or `wmill.get_resume_urls()` in Python.
</Tooltip>
</h2>
<Toggle
checked={isSuspendEnabled}
on:change={() => {
@@ -49,11 +51,11 @@
<input type="number" disabled />
{/if}
<span class="text-xs font-bold">Timeout (in seconds)</span>
<span class="text-xs font-bold">Timeout</span>
{#if flowModule.suspend}
<input bind:value={flowModule.suspend.timeout} type="number" min="1" placeholder="1800" />
<SecondsInput bind:seconds={flowModule.suspend.timeout} />
{:else}
<input type="number" disabled />
<SecondsInput disabled />
{/if}
</div>
@@ -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)
</script>
<div class="flex flex-col items-start space-y-1 {$$props.class}">
<h2 class="mt-2"
>Retries <Tooltip>
<div class={$$props.class}>
<h2>
Retries
<Tooltip>
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.</Tooltip
></h2
>
are tried before the exponential ones.
</Tooltip>
</h2>
<Toggle
checked={isConstantRetryEnabled}
on:change={() => {
if (isConstantRetryEnabled && flowModule.retry?.constant) {
flowModule.retry.constant = undefined
} else {
setConstantRetries()
}
}}
options={{
right: 'Constant retry enabled'
}}
/>
{#if flowModule.retry?.constant}
<span class="text-xs font-bold">Attempts</span>
<input bind:value={flowModule.retry.constant.attempts} type="number" />
<span class="text-xs font-bold">Delay (seconds)</span>
<input bind:value={flowModule.retry.constant.seconds} type="number" />
{:else}
<span class="text-xs font-bold">Attempts</span>
<input type="number" disabled />
<span class="text-xs font-bold">Delay (seconds)</span>
<input type="number" disabled />
{/if}
<Toggle
checked={isExponentialRetryEnabled}
on:change={() => {
if (isExponentialRetryEnabled && flowModule.retry?.exponential) {
flowModule.retry.exponential = undefined
} else {
setExpoentialRetries()
}
}}
options={{
right: 'Exponential backoff enabled'
}}
/>
{#if flowModule.retry?.exponential}
<span class="text-xs font-bold">Attempts</span>
<input bind:value={flowModule.retry.exponential.attempts} type="number" />
<span class="text-xs font-bold">Mulitplier</span>
<span class="text-xs text-gray-500">additional delay = multiplier * base ^ (# attempts)</span>
<input bind:value={flowModule.retry.exponential.multiplier} type="number" />
<span class="text-xs font-bold">Base</span>
<input bind:value={flowModule.retry.exponential.seconds} type="number" />
{:else}
<span class="text-xs font-bold">Attempts</span>
<input type="number" disabled />
<span class="text-xs font-bold">Mulitplier</span>
<span class="text-xs text-gray-500">additional delay = multiplier * seconds ^ (# attempts)</span
>
<input type="number" disabled />
<span class="text-xs font-bold">Base</span>
<input type="number" disabled />
{/if}
<div class="flex">
<div class="w-1/2 mr-2">
<div class="pt-4">
<Toggle
checked={isConstantRetryEnabled}
on:change={() => {
if (isConstantRetryEnabled && flowModule.retry?.constant) {
flowModule.retry.constant = undefined
} else {
setConstantRetries()
}
}}
options={{
right: 'Constant retry enabled'
}}
/>
</div>
{#if flowModule.retry?.constant}
<div class="text-xs font-bold !mt-2">Attempts</div>
<input bind:value={flowModule.retry.constant.attempts} type="number" />
<div class="text-xs font-bold !mt-2">Delay (in seconds)</div>
<input bind:value={flowModule.retry.constant.seconds} type="number" />
{:else}
<div class="text-xs font-bold !mt-2">Attempts</div>
<input type="number" disabled />
<div class="text-xs font-bold !mt-2">Delay (in seconds)</div>
<input type="number" disabled />
{/if}
<div class="pt-6">
<Toggle
checked={isExponentialRetryEnabled}
on:change={() => {
if (isExponentialRetryEnabled && flowModule.retry?.exponential) {
flowModule.retry.exponential = undefined
} else {
setExpoentialRetries()
}
}}
options={{
right: 'Exponential backoff enabled'
}}
/>
</div>
{#if flowModule.retry?.exponential}
<div class="text-xs font-bold !mt-2">Attempts</div>
<input bind:value={flowModule.retry.exponential.attempts} type="number" />
<div class="text-xs font-bold !mt-2">Mulitplier</div>
<span class="text-xs text-gray-500">delay = multiplier * base ^ (number of attempt)</span>
<input bind:value={flowModule.retry.exponential.multiplier} type="number" />
<div class="text-xs font-bold !mt-2">Base (in seconds)</div>
<input bind:value={flowModule.retry.exponential.seconds} type="number" />
{:else}
<div class="text-xs font-bold !mt-2">Attempts</div>
<input type="number" disabled />
<div class="text-xs font-bold !mt-2">Mulitplier</div>
<span class="text-xs text-gray-500">delay = multiplier * base ^ (number of attempt)</span>
<input type="number" disabled />
<div class="text-xs font-bold !mt-2">Base (in seconds)</div>
<input type="number" disabled />
{/if}
</div>
<div class="w-1/2 ml-2">
{#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]}
<div
class="bg-gray-50 border border-gray-300 rounded max-h-[431px] overflow-auto px-4 py-2"
>
<div class="text-xs font-medium mb-2">Retry attempts</div>
{#if array.length > 0}
<table class="text-xs">
<tr>
<td class="font-semibold pr-1">1:</td>
<td class="pb-1">After {array[0]} second{array[0] === 1 ? '' : 's'}</td>
</tr>
{#each array.slice(1) as delay, i}
{@const index = i + 2}
<tr>
<td class="font-semibold pr-1 align-top">{index}:</td>
<td class="pb-1 whitespace-nowrap">
{delay} second{delay === 1 ? '' : 's'} after attempt #{index - 1}
{#if i > cArray.length - 2}
<span class="text-gray-400 pl-2">
({multiplier} * {eSeconds}<sup>{index}</sup>)
</span>
{/if}
</td>
</tr>
{/each}
</table>
{:else}
<div class="text-xs">No retries</div>
{/if}
</div>
{/if}
</div>
</div>
</div>