flow UX improvements

This commit is contained in:
Ruben Fiszel
2022-10-29 16:04:05 +02:00
parent 12de3e700f
commit a08beefa65
17 changed files with 216 additions and 126 deletions

View File

@@ -12,7 +12,6 @@
} from '$lib/utils'
import { faGlobe, faPen } from '@fortawesome/free-solid-svg-icons'
import { setContext } from 'svelte'
import Icon from 'svelte-awesome'
import { writable } from 'svelte/store'
import CenteredPage from './CenteredPage.svelte'
import { Button } from './common'
@@ -23,6 +22,7 @@
import { flowStateStore } from './flows/flowState'
import { flowStore } from './flows/flowStore'
import FlowImportExportMenu from './flows/header/FlowImportExportMenu.svelte'
import FlowPreviewButtons from './flows/header/FlowPreviewButtons.svelte'
import { loadFlowSchedule, type Schedule } from './flows/scheduleUtils'
import type { FlowEditorContext } from './flows/types'
import { cleanInputs } from './flows/utils'
@@ -162,41 +162,8 @@
<div class="flex flex-col flex-1 h-full">
<!-- Nav between steps-->
<div
class="justify-between flex flex-row w-full my-2 px-4 space-x-4 h-10 overflow-x-auto scrollbar-hidden"
>
<div id="flow_title" class="flex justify-between items-center">
<button class="flex flex-row items-center w-full h-full" on:click={() => select('settings')}>
<span class="font-mono text-sm"> {$flowStore.path}</span>
<Icon
data={faPen}
scale={0.8}
class="text-gray-500 ml-2 flex justify-center items-center mb-0.5"
/>
</button>
</div>
<div class="shrink h-full">
<button
class="flex flex-row items-center w-full h-full"
on:click={() => {
select('settings')
document.getElementById('flow-summary')?.focus()
}}
>
<div class="overflow-x-auto flex items-center h-full text-sm text-left font-semibold">
<div>
{$flowStore.summary == '' || !$flowStore.summary ? 'No summary' : $flowStore.summary}
</div>
</div>
<div>
<Icon data={faPen} scale={0.8} class="text-gray-500 ml-1" />
</div>
</button>
</div>
<div class="flex flex-row-reverse ml-2 space-x-reverse space-x-2">
<Button disabled={pathError != ''} color="blue" size="sm" on:click={saveFlow}>Save</Button>
<FlowImportExportMenu />
<div class="justify-between flex flex-row w-full py-2 px-4 space-x-4">
<div class="flex flex-row space-x-2">
<Button
color="light"
size="sm"
@@ -212,10 +179,43 @@
url.searchParams.append('flow', btoa(JSON.stringify(openFlow)))
window.open(url, '_blank')?.focus()
}}
startIcon={{ icon: faGlobe }}
>
<Icon data={faGlobe} scale={0.8} class="inline mr-2" />
Publish to Hub
</Button>
<FlowImportExportMenu />
</div>
<div class="gap-1 flex-row hidden md:flex shrink overflow-hidden">
<Button
btnClasses="hidden lg:inline-flex"
startIcon={{ icon: faPen }}
variant="contained"
color="light"
size="xs"
on:click={async () => {
select('settings')
}}
>
{$flowStore.path}
</Button>
<Button
startIcon={{ icon: faPen }}
variant="contained"
color="light"
size="xs"
on:click={async () => {
select('settings')
document.getElementById('flow-summary')?.focus()
}}
>
<div class="max-w-[10em] !truncate">
{$flowStore.summary == '' || !$flowStore.summary ? 'No summary' : $flowStore.summary}
</div>
</Button>
</div>
<div class="flex flex-row-reverse ml-2 space-x-reverse space-x-2">
<Button disabled={pathError != ''} size="sm" on:click={saveFlow}>Save</Button>
<FlowPreviewButtons />
</div>
</div>

View File

@@ -98,8 +98,9 @@
</Breadcrumb>
</div>
<div class="gap-1 flex-row hidden md:flex">
<div class="gap-1 flex-row hidden md:flex shrink overflow-hidden">
<Button
btnClasses="hidden lg:inline-flex"
startIcon={{ icon: faPen }}
variant="contained"
color="light"

View File

@@ -9,12 +9,10 @@
const dispatch = createEventDispatcher()
</script>
<div class="flex items-center justify-center space-x-1">
<div class="flex items-center justify-center">
{#each items as item, index}
{#if index > 0}
<div class="mx-1">
<slot name="separator" />
</div>
<slot name="separator" />
{/if}
<Button
size="sm"

View File

@@ -18,6 +18,7 @@
export let endIcon: ButtonType.Icon | undefined = undefined
export let element: ButtonType.Element | undefined = undefined
export let id: string = ''
export let nonCaptureEvent: boolean = false
const dispatch = createEventDispatcher()
// Order of classes: border, border modifier, bg, bg modifier, text, text modifier, everything else
@@ -68,13 +69,15 @@
}
function onClick(event: MouseEvent) {
event.preventDefault()
dispatch('click', event)
if (href) {
if (href.startsWith('http')) {
window.open(href, target)
} else {
goto(href)
if (!nonCaptureEvent) {
event.preventDefault()
dispatch('click', event)
if (href) {
if (href.startsWith('http')) {
window.open(href, target)
} else {
goto(href)
}
}
}
}
@@ -90,7 +93,7 @@
<svelte:element
this={href ? 'a' : 'button'}
bind:this={element}
on:click|stopPropagation={onClick}
on:click={onClick}
on:focus
on:blur
{...buttonProps}

View File

@@ -47,24 +47,20 @@
</script>
<div class="relative" bind:this={menu}>
<div>
<div on:click={() => (show = !show)} on:click>
<slot name="trigger" />
</div>
{#if show}
<div
in:scale={{ duration: 100, start: 0.95 }}
out:scale={{ duration: 75, start: 0.95 }}
class={classNames(
'z-50 absolute mt-2 w-60 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none',
placementsClasses[placement]
)}
role="menu"
tabindex="-1"
>
<slot {close} />
</div>
{/if}
<div on:click={() => (show = !show)} on:click>
<slot name="trigger" />
</div>
{#if show}
<div
class={classNames(
'z-50 absolute mt-2 w-60 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none',
placementsClasses[placement]
)}
role="menu"
tabindex="-1"
>
<slot {close} />
</div>
{/if}
</div>

View File

@@ -3,16 +3,14 @@
import FlowEditorPanel from './content/FlowEditorPanel.svelte'
import FlowModuleSchemaMap from './map/FlowModuleSchemaMap.svelte'
import { flowStore } from './flowStore'
import FlowPreviewButtons from './header/FlowPreviewButtons.svelte'
export let initialPath: string
</script>
<div class="h-full overflow-hidden border-t">
<Splitpanes>
<Pane size={25} minSize={20} class="h-full flex flex-col">
<FlowPreviewButtons />
<div class="grow overflow-auto p-4 bg-gray-50">
<Pane size={25} minSize={20} class="h-full">
<div class="grow overflow-auto p-4 bg-gray h-full bg-gray-50 relative">
{#if $flowStore.value.modules}
<FlowModuleSchemaMap bind:modules={$flowStore.value.modules} root />
{/if}

View File

@@ -1,45 +1,82 @@
<script lang="ts">
import { Tab } from '$lib/components/common'
import { Alert, Tab } from '$lib/components/common'
import TabContent from '$lib/components/common/tabs/TabContent.svelte'
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
import type { FlowModule } from '$lib/gen'
import FlowCard from '../common/FlowCard.svelte'
import FlowModuleEarlyStop from './FlowModuleEarlyStop.svelte'
import FlowModuleSuspend from './FlowModuleSuspend.svelte'
import FlowRetries from './FlowRetries.svelte'
export let flowModule: FlowModule
export let type: 'branchall' | 'branchone'
export let parentModule: FlowModule | undefined
export let previousModuleId: string | undefined
let selected: string = 'retries'
</script>
{#if flowModule}
<Tabs bind:selected>
<Tab value="retries">Retries</Tab>
<Tab value="early-stop">Early Stop</Tab>
<Tab value="suspend">Sleep/Suspend</Tab>
<svelte:fragment slot="content">
<div class="overflow-hidden bg-white" style="height:calc(100% - 32px);">
<TabContent value="retries" class="flex flex-col flex-1 h-full">
<div class="p-4 overflow-y-auto">
<FlowRetries bind:flowModule />
</div>
</TabContent>
<TabContent value="early-stop" class="flex flex-col flex-1 h-full">
<div class="p-4 overflow-y-auto">
<FlowModuleEarlyStop {previousModuleId} bind:flowModule {parentModule} />
</div>
</TabContent>
<TabContent value="suspend" class="flex flex-col flex-1 h-full">
<div class="p-4 overflow-y-auto">
<FlowModuleSuspend {previousModuleId} bind:flowModule />
</div>
</TabContent>
<div class="h-full flex flex-col">
<FlowCard title={type == 'branchall' ? 'Run all branches' : 'Run one branch'}>
<div class="flex flex-col h-full">
<div class="border">
{#if type == 'branchall'}
<Alert type="info" title="All branches will be run in order">
Branches run in sequence, every branch is run to the end one after the other.
<br />
<br />
Configure branches to skip errors and to not result in that step itself failing.
<br />
<br />
The result of this step is the list of the result of each branch.
<br />
<br />
Since this is a step containing all branches as embedded flows, this step can be retried,
stopped early, can be made to sleep or to suspend after execution.
</Alert>
{:else}
<Alert type="info" title="Only one branch will be run">
Only one branch is ran, the first one that match its predicate, if none do, the default
branch is chosen.
<br />
<br />
The result of this step is the result of the branch.
<br />
<br />
Since this is a step containing all branches as embedded flows, this step can be retried,
stopped early, can be made to sleep or to suspend after execution.
</Alert>
{/if}
</div>
</svelte:fragment>
</Tabs>
{/if}
{#if flowModule}
<Tabs bind:selected>
<Tab value="retries">Retries</Tab>
<Tab value="early-stop">Early Stop</Tab>
<Tab value="suspend">Sleep/Suspend</Tab>
<svelte:fragment slot="content">
<div class="overflow-hidden bg-white">
<TabContent value="retries" class="flex flex-col flex-1 h-full">
<div class="p-4 overflow-y-auto">
<FlowRetries bind:flowModule />
</div>
</TabContent>
<TabContent value="early-stop" class="flex flex-col flex-1 h-full">
<div class="p-4 overflow-y-auto">
<FlowModuleEarlyStop {previousModuleId} bind:flowModule {parentModule} />
</div>
</TabContent>
<TabContent value="suspend" class="flex flex-col flex-1 h-full">
<div class="p-4 overflow-y-auto">
<FlowModuleSuspend {previousModuleId} bind:flowModule />
</div>
</TabContent>
</div>
</svelte:fragment>
</Tabs>
{/if}
</div>
</FlowCard>
</div>

View File

@@ -8,6 +8,7 @@
import { getStepPropPicker } from '../previousResults'
import { flowStateStore } from '../flowState'
import { flowStore } from '../flowStore'
import Tooltip from '$lib/components/Tooltip.svelte'
const { previewArgs } = getContext<FlowEditorContext>('FlowEditorContext')
@@ -36,6 +37,13 @@
</script>
<div class="flex flex-col items-start space-y-2 {$$props.class}">
<h2 class="mt-2"
>Early stop <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.</Tooltip
></h2
>
<Toggle
checked={isStopAfterIfEnabled}
on:change={() => {

View File

@@ -2,6 +2,7 @@
import InputTransformForm from '$lib/components/InputTransformForm.svelte'
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'
@@ -31,6 +32,13 @@
$: isSleepEnabled = Boolean(flowModule.sleep)
</script>
<h2 class="mt-2"
>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.getResumeEndpoints`.</Tooltip
></h2
>
<Toggle
checked={isSuspendEnabled}
on:change={() => {
@@ -65,6 +73,12 @@
{/if}
</div>
<h2 class="mt-4"
>Sleep<Tooltip>
If defined, at the end of the step, the flow will sleep for a number of seconds before being
resumed. Sleeping is passive and does not consume any resources.</Tooltip
></h2
>
<Toggle
checked={isSleepEnabled}
on:change={() => {

View File

@@ -18,6 +18,7 @@
import FlowInputs from './FlowInputs.svelte'
import { flowStateStore, type FlowModuleState } from '../flowState'
import Tooltip from '$lib/components/Tooltip.svelte'
import { Alert } from '$lib/components/common'
const { selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
@@ -34,10 +35,30 @@
{#if flowModule.value.type === 'forloopflow'}
<FlowLoop bind:mod={flowModule} {parentModule} {previousModuleId} />
{:else if flowModule.value.type === 'branchone'}
<FlowBranchesWrapper {previousModuleId} bind:flowModule {parentModule} />
<FlowBranchesWrapper
type={flowModule.value.type}
{previousModuleId}
bind:flowModule
{parentModule}
/>
{:else if flowModule.value.type === 'branchall'}
<FlowBranchesWrapper {previousModuleId} bind:flowModule {parentModule} />
<FlowBranchesWrapper
type={flowModule.value.type}
{previousModuleId}
bind:flowModule
{parentModule}
/>
{:else if flowModule.value.type === 'identity'}
{#if $selectedId == 'failure'}
<Alert type="info" title="Error handlers are triggered upon non recovered errors">
If defined, the error handler will take as input, the result of the step that errored (which
has its error in the 'error field').
<br />
<br />
Steps are retried until they succeed, or until the maximum number of retries defined for that
spec is reached, at which point the error handler is called.
</Alert>
{/if}
<h1 class="p-4"
>Select a step kind <Tooltip
>Until being defined, this step acts as an identify function, returning as result its input

View File

@@ -1,5 +1,7 @@
<script lang="ts">
import { Alert } from '$lib/components/common'
import Toggle from '$lib/components/Toggle.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import type { FlowModule } from '$lib/gen'
export let flowModule: FlowModule
@@ -30,6 +32,15 @@
</script>
<div class="flex flex-col items-start space-y-1 {$$props.class}">
<h2 class="mt-2"
>Retries <Tooltip>
If defined, upon error this step will be retried with a delay and a maximum number of attempts
as defined below. If none of the retries succeed, the step job is a failure and the error will
propagate up in the case of a branch, and the error handler will be called ultimately if not
handled prior.</Tooltip
></h2
>
<Toggle
checked={isConstantRetryEnabled}
on:change={() => {

View File

@@ -10,6 +10,7 @@
import FlowSchedules from './FlowSchedules.svelte'
import SvelteMarkdown from 'svelte-markdown'
import Toggle from '$lib/components/Toggle.svelte'
import { Alert } from '$lib/components/common'
export let initialPath: string
@@ -71,12 +72,22 @@
</div>
</TabContent>
<TabContent value="schedule" class="p-4">
<Alert type="info" title="Primary Schedule">
Flows can be triggered by any schedules, their webhooks or their UI but they only have
only one primary schedules with which they share the same path. The primary schedule can
be set here.
</Alert>
<div class="mt-4" />
<FlowSchedules />
</TabContent>
<TabContent value="same-worker" class="p-4 flex flex-col">
<Alert type="info" title="Toggle Same Worker to have all steps be ran on the same worker">
Steps will be run one after the other on the same worker, and will share a folder at
`/shared` in which they can store heavier data and pass them to the next step. <br /><br
/>Beware that the `/shared` folder is not preserved across suspends and sleeps.
</Alert>
<span class="my-2 text-sm font-bold">Same Worker</span>
<Toggle
bind:checked={$flowStore.value.same_worker}
options={{

View File

@@ -7,7 +7,7 @@
import FlowViewer from '$lib/components/FlowViewer.svelte'
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
import { sendUserToast } from '$lib/utils'
import { faFileExport, faFileImport } from '@fortawesome/free-solid-svg-icons'
import { faFileExport, faFileImport, faGlobe } from '@fortawesome/free-solid-svg-icons'
import Icon from 'svelte-awesome'
import { Button } from '../../common'
import { flowStore, initFlow } from '../flowStore'
@@ -27,13 +27,10 @@
</script>
<Menu placement="bottom-end">
<button
slot="trigger"
class="flex items-center text-sm border focus:outline-none focus:ring-4 rounded-md
text-gray-800 bg-white hover:bg-gray-100 focus:ring-gray-300 px-4 py-2"
>
Import/Export
</button>
<div slot="trigger">
<Button nonCaptureEvent={true} color="light" size="sm" variant="border">Import/Export</Button>
</div>
<div class="divide-y divide-gray-200">
<MenuItem on:click={() => jsonSetterDrawer.toggleDrawer()}>
<Icon data={faFileImport} scale={0.6} class="inline mr-2" />

View File

@@ -6,7 +6,6 @@
import { faPlay } from '@fortawesome/free-solid-svg-icons'
import { getContext } from 'svelte'
import Icon from 'svelte-awesome'
import type { FlowEditorContext } from '../types'
const { selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
let previewOpen = false
@@ -22,23 +21,19 @@
].includes($selectedId)
</script>
<div
class="flex flex-row-reverse justify-between items-center flex-wrap gap-2 w-full py-1 px-4 bg-white border-b"
>
<div class="flex flex-row-reverse justify-between items-center gap-x-2">
<Button
btnClasses="grow"
on:click={() => {
previewMode = 'whole'
previewOpen = !previewOpen
}}
size="sm"
endIcon={{ icon: faPlay }}
>
Test flow
<Icon data={faPlay} class="ml-2" scale={0.8} />
</Button>
{#if !upToDisabled}
<Button
btnClasses="grow"
size="sm"
disabled={upToDisabled}
color="light"
@@ -47,12 +42,12 @@
previewMode = 'upTo'
previewOpen = !previewOpen
}}
endIcon={{ icon: faPlay }}
>
Test up to step {$selectedId
.split('-')
.map((x) => (Number.isNaN(Number(x)) ? x : Number(x) + 1))
.join(' ')}
<Icon data={faPlay} class="ml-2" scale={0.8} />
</Button>
{/if}
</div>

View File

@@ -41,7 +41,7 @@
}
}}
class={classNames(
'border rounded-md p-2 bg-white text-sm cursor-pointer mt-4 flex flex-col overflow-x-hidden',
'border rounded-md p-2 bg-white text-sm cursor-pointer mt-4 flex flex-col overflow-x-hidden bottom-0 z-20 sticky',
$selectedId.includes('failure') ? 'outline outline-offset-1 outline-2 outline-slate-900' : ''
)}
>

View File

@@ -80,7 +80,7 @@
{#if deletable}
<Button
on:click={(event) => dispatch('delete', event)}
startIcon={{ icon: faTrashAlt }}
startIcon={{ icon: faTrashAlt, classes: 'text-gray-500' }}
iconOnly={true}
color="light"
variant="border"

View File

@@ -99,10 +99,10 @@
{/each}
<InsertModuleButton on:click={() => insertNewModuleAtIndex(modules.length)} />
{#if root}
<FlowErrorHandlerItem />
{/if}
</ul>
{#if root}
<FlowErrorHandlerItem />
{/if}
</div>
<RemoveStepConfirmationModal