Flow UX improvements wrt to triggers

This commit is contained in:
Ruben Fiszel
2022-12-15 23:09:23 +01:00
parent 2e7b2a9e20
commit 3735171a0d
23 changed files with 303 additions and 208 deletions
+20 -14
View File
@@ -22,7 +22,7 @@
"date-fns": "^2.29.3",
"highlight.js": "^11.7.0",
"monaco-editor": "0.33.0",
"monaco-editor-workers": "^0.34.2",
"monaco-editor-workers": "0.33.0",
"monaco-languageclient": "2.1.0",
"monaco-yaml": "^4.0.2",
"set-interval-async": "^3.0.2",
@@ -37,7 +37,7 @@
"@sveltejs/kit": "next",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.8",
"@types/vscode": "~1.73.1",
"@types/vscode": "~1.68.0",
"@typescript-eslint/eslint-plugin": "^5.46.1",
"@typescript-eslint/parser": "^5.46.1",
"@windmill-labs/svelvet": "^4.0.6",
@@ -642,9 +642,9 @@
"dev": true
},
"node_modules/@types/vscode": {
"version": "1.73.1",
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.73.1.tgz",
"integrity": "sha512-eArfOrAoZVV+Ao9zQOCaFNaeXj4kTCD+bGS2gyNgIFZH9xVMuLMlRrEkhb22NyxycFWKV1UyTh03vhaVHmqVMg==",
"version": "1.68.1",
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.68.1.tgz",
"integrity": "sha512-fXlaq13NT5yHh6yZ3c+UxXloTSk34mIvsNFYyQCeO5Po2BLFAwz7EZT4kQ43B64/aPcnAenyWy3QasrTofBOnQ==",
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
@@ -3979,9 +3979,12 @@
"integrity": "sha512-VcRWPSLIUEgQJQIE0pVT8FcGBIgFoxz7jtqctE+IiCxWugD0DwgyQBcZBhdSrdMC84eumoqMZsGl2GTreOzwqw=="
},
"node_modules/monaco-editor-workers": {
"version": "0.34.2",
"resolved": "https://registry.npmjs.org/monaco-editor-workers/-/monaco-editor-workers-0.34.2.tgz",
"integrity": "sha512-MdO1nvAbGNzc7ygyzwACvs5DOPaTOUNDjU/qTQx0Rsw00BrhL88eMi9sWBm+LYfQbPaZf+YBqwJaJUovl3M6bQ=="
"version": "0.33.0",
"resolved": "https://registry.npmjs.org/monaco-editor-workers/-/monaco-editor-workers-0.33.0.tgz",
"integrity": "sha512-KrIju+r4mKCzGi9kDVqUIze78EilSf8VbvNOQD7p14LSVah5fy1Z3z5QRYDDgHcHFEkzT7vbVMRnBRy6CI/aHQ==",
"dependencies": {
"monaco-editor": "0.33.0"
}
},
"node_modules/monaco-languageclient": {
"version": "2.1.0",
@@ -7390,9 +7393,9 @@
"dev": true
},
"@types/vscode": {
"version": "1.73.1",
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.73.1.tgz",
"integrity": "sha512-eArfOrAoZVV+Ao9zQOCaFNaeXj4kTCD+bGS2gyNgIFZH9xVMuLMlRrEkhb22NyxycFWKV1UyTh03vhaVHmqVMg==",
"version": "1.68.1",
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.68.1.tgz",
"integrity": "sha512-fXlaq13NT5yHh6yZ3c+UxXloTSk34mIvsNFYyQCeO5Po2BLFAwz7EZT4kQ43B64/aPcnAenyWy3QasrTofBOnQ==",
"dev": true
},
"@typescript-eslint/eslint-plugin": {
@@ -9742,9 +9745,12 @@
"integrity": "sha512-VcRWPSLIUEgQJQIE0pVT8FcGBIgFoxz7jtqctE+IiCxWugD0DwgyQBcZBhdSrdMC84eumoqMZsGl2GTreOzwqw=="
},
"monaco-editor-workers": {
"version": "0.34.2",
"resolved": "https://registry.npmjs.org/monaco-editor-workers/-/monaco-editor-workers-0.34.2.tgz",
"integrity": "sha512-MdO1nvAbGNzc7ygyzwACvs5DOPaTOUNDjU/qTQx0Rsw00BrhL88eMi9sWBm+LYfQbPaZf+YBqwJaJUovl3M6bQ=="
"version": "0.33.0",
"resolved": "https://registry.npmjs.org/monaco-editor-workers/-/monaco-editor-workers-0.33.0.tgz",
"integrity": "sha512-KrIju+r4mKCzGi9kDVqUIze78EilSf8VbvNOQD7p14LSVah5fy1Z3z5QRYDDgHcHFEkzT7vbVMRnBRy6CI/aHQ==",
"requires": {
"monaco-editor": "0.33.0"
}
},
"monaco-languageclient": {
"version": "2.1.0",
+2 -2
View File
@@ -20,7 +20,7 @@
"@sveltejs/kit": "next",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.8",
"@types/vscode": "~1.73.1",
"@types/vscode": "~1.68.0",
"@typescript-eslint/eslint-plugin": "^5.46.1",
"@typescript-eslint/parser": "^5.46.1",
"@windmill-labs/svelvet": "^4.0.6",
@@ -73,7 +73,7 @@
"date-fns": "^2.29.3",
"highlight.js": "^11.7.0",
"monaco-editor": "0.33.0",
"monaco-editor-workers": "^0.34.2",
"monaco-editor-workers": "0.33.0",
"monaco-languageclient": "2.1.0",
"monaco-yaml": "^4.0.2",
"set-interval-async": "^3.0.2",
+3 -4
View File
@@ -4,7 +4,7 @@
<script lang="ts">
import { ScheduleService } from '$lib/gen'
import { displayDate, formatCron, sendUserToast } from '$lib/utils'
import { displayDate, emptyString, formatCron, sendUserToast } from '$lib/utils'
import CollapseLink from './CollapseLink.svelte'
export let validCRON = true
@@ -15,7 +15,7 @@
export let disabled = false
let limit = 3
$: handleScheduleInput(schedule)
$: !emptyString(schedule) && handleScheduleInput(schedule)
async function handleScheduleInput(input: string): Promise<void> {
try {
@@ -29,14 +29,13 @@
cronError = `Invalid cron expression`
validCRON = false
} else {
sendUserToast(`Cannot preview: ${err}`, true)
validCRON = false
}
}
}
</script>
<div class="text-red-600 text-2xs grow">{cronError}</div>
<div class="w-full text-right text-red-600 text-2xs grow">{cronError}</div>
<div class="flex flex-row items-end max-w-5xl">
<label class="text-xs min-w-max mr-2 self-center" for="cron-schedule">CRON expression</label>
<input
-10
View File
@@ -40,8 +40,6 @@
} from 'monaco-languageclient'
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
import getMessageServiceOverride from 'vscode/service-override/messages'
import { StandaloneServices } from 'vscode/services'
import {
BASH_INIT_CODE,
DENO_INIT_CODE_CLEAR,
@@ -56,14 +54,6 @@
} from '$lib/editorUtils'
import { dirtyStore } from './common/confirmationModal/dirtyStore'
try {
StandaloneServices.initialize({
...getMessageServiceOverride(document.body)
})
} catch (e) {
console.error(e)
}
let divEl: HTMLDivElement | null = null
let editor: monaco.editor.IStandaloneCodeEditor
+36 -17
View File
@@ -10,11 +10,12 @@
sendUserToast,
setQueryWithoutLoad
} from '$lib/utils'
import { faEye, faPen, faSave } from '@fortawesome/free-solid-svg-icons'
import { faCalendarAlt, faEye, faPen, faSave } 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, Drawer, DrawerContent } from './common'
import { Badge, Button, Drawer, DrawerContent } from './common'
import { dirtyStore } from './common/confirmationModal/dirtyStore'
import UnsavedConfirmationModal from './common/confirmationModal/UnsavedConfirmationModal.svelte'
import { OFFSET } from './CronInput.svelte'
@@ -38,18 +39,22 @@
async function createSchedule(path: string) {
const { cron, args, enabled } = $scheduleStore
await ScheduleService.createSchedule({
workspace: $workspaceStore!,
requestBody: {
path: path,
schedule: formatCron(cron),
offset: OFFSET,
script_path: path,
is_flow: true,
args,
enabled
}
})
try {
await ScheduleService.createSchedule({
workspace: $workspaceStore!,
requestBody: {
path: path,
schedule: formatCron(cron),
offset: OFFSET,
script_path: path,
is_flow: true,
args,
enabled
}
})
} catch (err) {
sendUserToast(`The primary schedule could not be created: ${err}`, true)
}
}
async function saveFlow(): Promise<void> {
@@ -212,10 +217,24 @@
size="sm"
on:click={flowViewer.openDrawer}
>
View Graph
Graph
</Button>
</div>
<div class="gap-1 flex-row hidden md:flex shrink overflow-hidden">
{#if $scheduleStore.enabled}
<Button
btnClasses="hidden lg:inline-flex"
startIcon={{ icon: faCalendarAlt }}
variant="contained"
color="light"
size="xs"
on:click={async () => {
select('settings-schedule')
}}
>
{$scheduleStore.cron ?? ''}
</Button>
{/if}
<Button
btnClasses="hidden lg:inline-flex"
startIcon={{ icon: faPen }}
@@ -223,7 +242,7 @@
color="light"
size="xs"
on:click={async () => {
select('settings')
select('settings-metadata')
document.getElementById('path')?.focus()
}}
>
@@ -235,7 +254,7 @@
color="light"
size="xs"
on:click={async () => {
select('settings')
select('settings-metadata')
document.getElementById('flow-summary')?.focus()
}}
>
+85 -84
View File
@@ -17,11 +17,7 @@
import Popover from './Popover.svelte'
type PathKind = 'resource' | 'script' | 'variable' | 'flow' | 'schedule' | 'app'
export let meta: Meta = {
ownerKind: 'user',
owner: '',
name: ''
}
let meta: Meta | undefined = undefined
export let namePlaceholder = ''
export let initialPath: string
export let path = ''
@@ -36,16 +32,19 @@
let groups: Group[] = []
$: path = metaToPath(meta)
$: meta && onMetaChange()
function onMetaChange() {
if (meta) {
path = metaToPath(meta)
validate(meta, path, kind)
}
}
function metaToPath(meta: Meta): string {
return [meta.ownerKind === 'group' ? 'g' : 'u', meta.owner, meta.name].join('/')
}
export function getPath() {
return path
}
export function focus() {
inputP?.focus()
}
@@ -61,31 +60,25 @@
export async function reset() {
if (path == '' || path == 'u//') {
meta.ownerKind = 'user'
meta = { ownerKind: 'user', name: namePlaceholder, owner: '' }
while ($userStore == undefined) {
await sleep(500)
}
meta.owner = $userStore!.username
meta.name = namePlaceholder
let i = 1
while (await pathExists(metaToPath(meta), kind)) {
meta.name = `${namePlaceholder}_${i}`
i += 1
if (initialPath && initialPath != '') {
meta = pathToMeta(initialPath)
}
}
} else {
meta = pathToMeta(path)
}
}
$: validate(meta, path, kind)
async function loadGroups(): Promise<void> {
groups = await GroupService.listGroups({ workspace: $workspaceStore! })
meta.owner = meta.owner
}
async function validate(meta: Meta, path: string, kind: PathKind) {
@@ -102,7 +95,7 @@
validateTimeout = setTimeout(async () => {
if ((path == '' || path != initialPath) && (await pathExists(path, kind))) {
error = 'path already used'
} else if (validateName(meta)) {
} else if (meta && validateName(meta)) {
error = ''
}
validateTimeout = undefined
@@ -149,89 +142,97 @@
$: {
if ($workspaceStore) {
loadGroups()
initPath()
}
}
$: {
function initPath() {
if (initialPath == undefined || initialPath == '') {
reset()
} else {
meta = pathToMeta(initialPath)
onMetaChange()
path = initialPath
}
}
</script>
<div>
<div class="flex flex-col sm:grid sm:grid-cols-4 sm:gap-4 pb-0 mb-1">
<label class="block">
<span class="text-gray-700 text-sm whitespace-nowrap">
<Popover
>Owner Kind
<span slot="text"
>Select the group <span class="font-mono">all</span>
to share it with all workspace users, and <span class="font-mono">user</span> to keep it
private.
<a href="https://docs.windmill.dev/docs/reference/namespaces">docs</a>
</span>
</Popover>
</span>
{#if meta != undefined}
<label class="block">
<span class="text-gray-700 text-sm whitespace-nowrap">
<Popover
>Owner Kind
<span slot="text"
>Select the group <span class="font-mono">all</span>
to share it with all workspace users, and <span class="font-mono">user</span> to keep
it private.
<a href="https://docs.windmill.dev/docs/reference/namespaces">docs</a>
</span>
</Popover>
</span>
<select
{disabled}
bind:value={meta.ownerKind}
on:change={() => {
if (meta.ownerKind === 'group') {
meta.owner = 'all'
} else {
meta.owner = $userStore?.username ?? ''
}
}}
>
<option>user</option>
<option>group</option>
</select>
</label>
{#if meta.ownerKind === 'user'}
<label class="block">
<span class="text-gray-700 text-sm">Owner</span>
<input
type="text"
bind:value={meta.owner}
placeholder={$userStore?.username ?? ''}
disabled={!($superadmin || ($userStore?.is_admin ?? false))}
/>
</label>
{:else}
<label class="block">
<span class="text-gray-700 text-sm">Owner</span>
<select {disabled} bind:value={meta.owner}>
{#each groups as g}
<option>{g.name}</option>
{/each}
<select
{disabled}
bind:value={meta.ownerKind}
on:change={() => {
if (meta) {
if (meta.ownerKind === 'group') {
meta.owner = 'all'
} else {
meta.owner = $userStore?.username ?? ''
}
}
}}
>
<option>user</option>
<option>group</option>
</select>
</label>
{#if meta.ownerKind === 'user'}
<label class="block">
<span class="text-gray-700 text-sm">Owner</span>
<input
type="text"
bind:value={meta.owner}
placeholder={$userStore?.username ?? ''}
disabled={!($superadmin || ($userStore?.is_admin ?? false))}
/>
</label>
{:else}
<label class="block">
<span class="text-gray-700 text-sm">Owner</span>
<select {disabled} bind:value={meta.owner}>
{#each groups as g}
<option>{g.name}</option>
{/each}
</select>
</label>
{/if}
<label class="block col-span-2">
<span class="text-gray-700 text-sm">
Name
<Required required={true} />
</span>
<input
{disabled}
type="text"
id="path"
autofocus
bind:this={inputP}
autocomplete="off"
on:keyup={handleKeyUp}
bind:value={meta.name}
placeholder={namePlaceholder}
class={error === ''
? ''
: 'border border-red-700 bg-red-100 border-opacity-30 focus:border-red-700 focus:border-opacity-30 focus-visible:ring-red-700 focus-visible:ring-opacity-25 focus-visible:border-red-700'}
/>
</label>
{/if}
<label class="block col-span-2">
<span class="text-gray-700 text-sm">
Name
<Required required={true} />
</span>
<input
{disabled}
type="text"
id="path"
autofocus
bind:this={inputP}
autocomplete="off"
on:keyup={handleKeyUp}
bind:value={meta.name}
placeholder={namePlaceholder}
class={error === ''
? ''
: 'border border-red-700 bg-red-100 border-opacity-30 focus:border-red-700 focus:border-opacity-30 focus-visible:ring-red-700 focus-visible:ring-opacity-25 focus-visible:border-red-700'}
/>
</label>
</div>
<div class="pt-0 text-xs px-1 flex flex-col-reverse sm:grid sm:grid-cols-4 sm:gap-4 w-full">
<div class="col-span-2"><span class="font-mono">{path}</span></div>
<div class="text-red-600 text-2xs col-span-2">{error}</div>
@@ -255,7 +255,7 @@
<RadioButton
label="Script Type"
options={[
['Common Script', Script.kind.SCRIPT],
['Action', Script.kind.SCRIPT],
[
{
title: 'Trigger Script',
@@ -13,22 +13,16 @@
const { selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
</script>
{#key $selectedId}
{#if $selectedId === 'settings'}
<FlowSettings {initialPath} />
{:else if $selectedId === 'inputs'}
<FlowInput />
{:else if $selectedId === 'settings-schedule'}
<FlowSettings {initialPath} defaultTab="schedule" />
{:else if $selectedId === 'settings-same-worker'}
<FlowSettings {initialPath} defaultTab="same-worker" />
{:else if $selectedId === 'settings-graph'}
<FlowSettings {initialPath} defaultTab="graph" />
{:else if $selectedId === 'failure'}
<FlowFailureModule />
{:else}
{#if $selectedId?.startsWith('settings')}
<FlowSettings {initialPath} />
{:else if $selectedId === 'inputs'}
<FlowInput />
{:else if $selectedId === 'failure'}
<FlowFailureModule />
{:else}
{#key $selectedId}
{#each $flowStore.value.modules as flowModule, index (flowModule.id ?? index)}
<FlowModuleWrapper bind:flowModule previousModule={$flowStore.value.modules[index - 1]} />
{/each}
{/if}
{/key}
{/key}
{/if}
@@ -12,8 +12,14 @@
export let failureModule: boolean
export let shouldDisableTriggerScripts: boolean = false
export let summary: string | undefined = undefined
const dispatch = createEventDispatcher()
let kind: 'script' | 'failure' | 'approval' | 'trigger' = failureModule ? 'failure' : 'script'
let kind: 'script' | 'failure' | 'approval' | 'trigger' = failureModule
? 'failure'
: summary == 'Trigger'
? 'trigger'
: 'script'
let pick_existing: 'workspace' | 'hub' = 'hub'
let filter = ''
</script>
@@ -24,8 +30,8 @@
<div class="max-w-min">
<ToggleButtonGroup bind:selected={kind}>
<ToggleButton position="left" value="script" size="sm" startIcon={{ icon: faCode }}>
Common &nbsp;<Tooltip>
A common script is simply a script that is neither a trigger nor an approval script.
Action &nbsp;<Tooltip>
An action script is simply a script that is neither a trigger nor an approval script.
Those are the majority of the scripts.
</Tooltip>
</ToggleButton>
@@ -50,6 +56,12 @@
</div>
</div>
{/if}
{#if kind == 'trigger'}
<p class="text-sm text-gray-600 my-4"
>Once you pick a trigger script, a schedule will be automatically attached to this flow to run
every 15 minutes. Adjust frequency in 'Flow settings -> Schedule'</p
>
{/if}
<h3 class="pb-2 pt-4">
Inline new <span class="text-blue-500">{kind == 'script' ? 'common' : kind}</span> script
<Tooltip>
@@ -196,7 +196,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">Advanced</Tab>
<Tab value="advanced-retries">Advanced</Tab>
</Tabs>
<div class="h-[calc(100%-32px)]">
{#if selected === 'inputs'}
@@ -19,7 +19,7 @@
import FlowBranchesAllWrapper from './FlowBranchesAllWrapper.svelte'
import FlowBranchesOneWrapper from './FlowBranchesOneWrapper.svelte'
const { selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
const { selectedId, schedule } = getContext<FlowEditorContext>('FlowEditorContext')
export let flowModule: FlowModule
@@ -62,6 +62,7 @@
/>
{:else}
<FlowInputs
summary={flowModule.summary}
shouldDisableTriggerScripts={parentModule !== undefined ||
previousModule !== undefined ||
$selectedId == 'failure'}
@@ -73,6 +74,18 @@
module.suspend = { required_events: 1, timeout: 1800 }
}
if (kind == Script.kind.TRIGGER) {
if (!$schedule.cron) {
$schedule.cron = '0 */15 * * *'
}
$schedule.enabled = true
module.stop_after_if = {
expr: 'result == undefined || Array.isArray(result) && result.length == 0',
skip_if_stopped: true
}
}
flowModule = module
$flowStateStore[module.id] = state
}}
@@ -86,6 +99,18 @@
flowModule.id
)
if (kind == Script.kind.TRIGGER) {
if (!$schedule.cron) {
$schedule.cron = '0 */15 * * *'
}
$schedule.enabled = true
module.stop_after_if = {
expr: 'result == undefined || Array.isArray(result) && result.length == 0',
skip_if_stopped: true
}
}
if (kind == Script.kind.APPROVAL) {
module.suspend = { required_events: 1, timeout: 1800 }
}
@@ -2,6 +2,7 @@
import CronInput from '$lib/components/CronInput.svelte'
import SchemaForm from '$lib/components/SchemaForm.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import { emptyString } from '$lib/utils'
import { getContext } from 'svelte'
import { flowStore } from '../flowStore'
import type { FlowEditorContext } from '../types'
@@ -10,7 +11,11 @@
<CronInput bind:schedule={$schedule.cron} />
<SchemaForm schema={$flowStore.schema} bind:args={$schedule.args} />
{#if emptyString($schedule.cron)}
<p class="text-xs text-gray-600 mt-10">Define a schedule frequency first</p>
{/if}
<Toggle
disabled={emptyString($schedule.cron)}
bind:checked={$schedule.enabled}
options={{
right: 'Schedule enabled'
@@ -13,26 +13,26 @@
import { FlowGraph } from '$lib/components/graph'
import { getContext } from 'svelte'
import type { FlowEditorContext } from '../types'
import autosize from 'svelte-autosize'
const { selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
export let initialPath: string
export let defaultTab = 'metadata'
let topHeight = 0
</script>
<div class="h-full overflow-hidden">
<FlowCard title="Settings">
<div class="h-full flex-1">
<Tabs selected={defaultTab}>
<Tab value="metadata">Metadata</Tab>
<Tab value="schedule">Schedule</Tab>
<Tab value="same-worker">Shared Directory</Tab>
<Tab value="graph">Graph</Tab>
<Tabs bind:selected={$selectedId}>
<Tab value="settings-metadata">Metadata</Tab>
<Tab value="settings-schedule">Schedule</Tab>
<Tab value="settings-same-worker">Shared Directory</Tab>
<Tab value="settings-graph">Graph</Tab>
<svelte:fragment slot="content">
<TabContent value="metadata" class="p-4">
<TabContent value="settings-metadata" class="p-4">
<Path bind:path={$flowStore.path} {initialPath} namePlaceholder="my_flow" kind="flow" />
<label class="block my-4">
@@ -49,8 +49,9 @@
<label class="block my-4" for="inp">
<span class="text-gray-700 text-sm">
Description
<Required required={false} detail="markdown" />
<Required required={false} />
<textarea
use:autosize
type="text"
class="text-sm"
id="inp"
@@ -61,7 +62,7 @@
</span>
</label>
</TabContent>
<TabContent value="schedule" class="p-4">
<TabContent value="settings-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
@@ -71,7 +72,7 @@
<FlowSchedules />
</TabContent>
<TabContent value="same-worker" class="p-4 flex flex-col">
<TabContent value="settings-same-worker" class="p-4 flex flex-col">
<Alert type="info" title="Shared Directory">
Steps 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
@@ -85,7 +86,7 @@
}}
/>
</TabContent>
<TabContent value="graph">
<TabContent value="settings-graph">
<div
bind:clientHeight={topHeight}
class="max-w-full w-full overflow-hidden h-screen bg-gray-50"
@@ -20,6 +20,7 @@
$selectedId == undefined ||
[
'settings',
'settings-metadata',
'settings-schedule',
'settings-retries',
'settings-same-worker',
@@ -15,7 +15,7 @@
if ($flowStore?.value?.failure_module) {
$flowStore.value.failure_module = undefined
// By default, we return to settings when disabling the failure module
select('settings')
select('settings-metadata')
} else {
const failureModule: FlowModuleState = {
schema: emptySchema(),
@@ -28,7 +28,7 @@
async function insertNewModuleAtIndex(
index: number,
kind: 'script' | 'forloop' | 'branchone' | 'branchall' | 'flow'
kind: 'script' | 'forloop' | 'branchone' | 'branchall' | 'flow' | 'trigger'
): Promise<void> {
await idMutex.runExclusive(async () => {
var module = emptyModule(kind == 'flow')
@@ -45,12 +45,15 @@
modules.splice(index, 0, flowModule)
modules = modules
$flowStateStore[flowModule.id] = state
if (kind == 'trigger') {
flowModule.summary = 'Trigger'
}
select(flowModule.id)
})
}
function removeAtIndex(index: number): void {
select('settings')
select('settings-graph')
if (!modules) return
const [removedModule] = modules.splice(index, 1)
modules = modules
@@ -113,6 +116,7 @@
{#each modules as mod, index (mod.id ?? index)}
<div transition:slide|local>
<MapItem
{index}
bind:mod
on:delete={(event) => {
if (event.detail.detail.shiftKey || mod.value.type === 'identity') {
@@ -11,27 +11,20 @@
$: settingsClass = classNames(
'border w-full rounded-sm p-2 bg-white border-gray-400 text-sm cursor-pointer flex items-center',
$selectedId === 'settings' ? 'outline outline-offset-1 outline-2 outline-slate-900' : ''
$selectedId?.startsWith('settings')
? 'outline outline-offset-1 outline-2 outline-slate-900'
: ''
)
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div on:click={() => select('settings')} class={settingsClass}>
<div on:click={() => select('settings-metadata')} class={settingsClass}>
<Icon data={faSliders} class="mr-2" />
<span class="text-xs font-bold flex flex-row justify-between w-full gap-2 items-center truncate">
Settings
<span
class={classNames('badge', $schedule?.enabled ? 'badge-on' : 'badge-off')}
on:click|stopPropagation={() => select('settings-schedule')}
>
{#if $schedule?.enabled}
<Badge color="gray" baseClass="truncate">
<Icon data={faCalendarAlt} scale={0.8} />
</Badge>
{/if}
{#if $flowStore.value.same_worker}
<Badge color="blue" baseClass="truncate">./shared</Badge>
{/if}
</span>
{#if $flowStore.value.same_worker}
<Badge color="blue" baseClass="truncate">./shared</Badge>
{/if}
</span>
</div>
@@ -1,11 +1,18 @@
<script lang="ts">
import { Menu } from '$lib/components/common'
import { faCode, faCodeBranch, faPlus, faBarsStaggered } from '@fortawesome/free-solid-svg-icons'
import {
faCode,
faCodeBranch,
faPlus,
faBarsStaggered,
faBolt
} from '@fortawesome/free-solid-svg-icons'
import { createEventDispatcher } from 'svelte'
import Icon from 'svelte-awesome'
import { Repeat } from 'lucide-svelte'
const dispatch = createEventDispatcher()
export let trigger = false
</script>
<Menu noMinW placement="bottom-start" let:close>
@@ -17,6 +24,20 @@
<Icon data={faPlus} scale={0.8} />
</button>
<div class="divide-y divide-gray-100 text-xs w-40">
{#if trigger}
<button
class="w-full text-left p-2 hover:bg-gray-100"
on:click={() => {
close()
dispatch('new', 'trigger')
}}
role="menuitem"
tabindex="-1"
>
<Icon data={faBolt} scale={0.8} class="mr-1" />
Trigger
</button>
{/if}
<button
class="w-full text-left p-2 hover:bg-gray-100"
on:click={() => {
@@ -14,6 +14,7 @@
import { Building, Repeat } from 'lucide-svelte'
export let mod: FlowModule
export let index: number
const { select, selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
const dispatch = createEventDispatcher<{
@@ -35,7 +36,7 @@
</script>
{#if mod}
<InsertModuleButton on:new={(e) => dispatch('insert', e.detail)} />
<InsertModuleButton trigger={index == 0} on:new={(e) => dispatch('insert', e.detail)} />
{#if mod.value.type === 'forloopflow'}
<li class="w-full">
<FlowModuleSchemaItem
@@ -85,7 +85,8 @@
function getConvertedFlowModule(
module: FlowModule,
parent: NestedNodes | string | undefined = undefined,
edgeLabel: string | undefined = undefined
edgeLabel: string | undefined = undefined,
insideLoop: boolean = false
): GraphItem | undefined {
const type = module.value.type
const parentIds = getParentIds(parent)
@@ -99,7 +100,9 @@
'inline',
module,
lang,
edgeLabel
edgeLabel,
undefined,
insideLoop
)
} else if (type === 'script') {
const isHub = module.value.path.startsWith('hub/')
@@ -110,7 +113,9 @@
isHub ? 'hub' : 'workspace',
module,
undefined,
edgeLabel
edgeLabel,
undefined,
insideLoop
)
} else if (type === 'forloopflow') {
return flowModuleToLoop(module.value.modules, module, parent)
@@ -120,11 +125,12 @@
module,
branches,
['Default', ...module.value.branches.map((x) => `If ${truncateRev(x.expr, 20)}`)],
parent
parent,
insideLoop
)
} else if (type === 'branchall') {
const branches = module.value.branches.map((b) => b.modules)
return flowModuleToBranch(module, branches, [], parent)
return flowModuleToBranch(module, branches, [], parent, insideLoop)
} else if (type === 'flow') {
return flowModuleToNode(
parentIds,
@@ -133,7 +139,9 @@
'inline',
module,
undefined,
edgeLabel
edgeLabel,
undefined,
insideLoop
)
}
return flowModuleToNode(
@@ -143,7 +151,9 @@
'inline',
module,
undefined,
edgeLabel
edgeLabel,
undefined,
insideLoop
)
}
@@ -189,7 +199,8 @@
onClickDetail: any,
lang?: RawScript.language,
edgeLabel?: string,
header?: string
header?: string,
insideLoop: boolean = false
): Node {
const langImg: Record<RawScript.language, string> = {
deno: '/icons/ts-lang.svg',
@@ -228,7 +239,7 @@
`
},
host,
width: NODE.width,
width: insideLoop ? NODE.width * 0.8 : NODE.width,
height: NODE.height,
borderColor: selectedNode == nodeId ? 'black' : '#999',
bgColor: selectedNode == nodeId ? '#f5f5f5' : getStateColor(flowModuleStates?.[nodeId]?.type),
@@ -271,7 +282,7 @@
]
}
modules.forEach((module) => {
const item = getConvertedFlowModule(module, loop.items)
const item = getConvertedFlowModule(module, loop.items, undefined, true)
item && loop.items.push(item)
})
loop.items.push(
@@ -289,7 +300,8 @@
module: FlowModule,
branches: FlowModule[][],
edgesLabel: string[],
parent: string | NestedNodes | undefined = undefined
parent: string | NestedNodes | undefined = undefined,
insideLoop: boolean = false
): Branch {
const branch: Branch = {
type: 'branch',
@@ -301,7 +313,10 @@
: 'Run one branch given predicate',
'inline',
module,
undefined
undefined,
undefined,
undefined,
insideLoop
),
items: []
}
@@ -318,7 +333,8 @@
const item = getConvertedFlowModule(
module,
items.length ? items : numberToChars(branch.node.id),
edgesLabel[i]
edgesLabel[i],
insideLoop
)
item && items.push(item)
})
+1 -1
View File
@@ -20,7 +20,7 @@
const hubId = $page.url.searchParams.get('hub')
const templatePath = $page.url.searchParams.get('template')
let selectedId: string = 'settings'
let selectedId: string = 'settings-metadata'
let loading = false
async function loadFlow() {
+15 -9
View File
@@ -34,7 +34,6 @@
import ShareModal from '$lib/components/ShareModal.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import { userStore, workspaceStore } from '$lib/stores'
import SvelteMarkdown from 'svelte-markdown'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import FlowViewer from '$lib/components/FlowViewer.svelte'
import { Button, ActionRow, Skeleton, Badge } from '$lib/components/common'
@@ -173,7 +172,7 @@
href="/flows/run/{path}"
variant="contained"
color="blue"
size="xs"
size="md"
startIcon={{ icon: faPlay }}
>
Run
@@ -183,7 +182,7 @@
href="/flows/edit/{path}"
variant="contained"
color="blue"
size="xs"
size="md"
startIcon={{ icon: faEdit }}
disabled={!can_write}
>
@@ -193,13 +192,13 @@
href="/flows/add?template={flow.path}"
variant="contained"
color="blue"
size="xs"
size="md"
startIcon={{ icon: faCodeFork }}
>
Use as template/Fork
Fork
</Button>
{/if}
<Button href="/runs/{flow.path}" color="blue" size="xs" startIcon={{ icon: faList }}>
<Button href="/runs/{flow.path}" color="blue" size="md" startIcon={{ icon: faList }}>
View runs
</Button>
</svelte:fragment>
@@ -227,7 +226,11 @@
>Edited {displayDaysAgo(flow.edited_at ?? '')} by {flow.edited_by}
<a href="#webhook" class="ml-2">
<Badge color="dark-blue">Webhook</Badge>
</a></p
</a>
{#if schedule}
<a href="#primary-schedule" class="ml-2">
<Badge color="dark-blue">Primary schedule</Badge>
</a>{/if}</p
>
{#if flow.archived}
@@ -239,6 +242,7 @@
<div class="mt-6 grid grid-cols-1 sm:grid-cols-3 gap-6">
<div class="col-span-2">
<h2 class="mb-2">Preview</h2>
<RunForm
autofocus
detailed={false}
@@ -248,7 +252,7 @@
runAction={runFlow}
/>
</div>
<div class="mt-6 box">
<div class="box">
{defaultIfEmptyString(flow.description, 'No description')}
</div>
</div>
@@ -286,7 +290,9 @@
</div>
{#if schedule}
<div class="mt-10">
<h2 class="text-gray-700 pb-1 mb-3 border-b inline-flex flex-row items-center gap-x-4"
<h2
id="primary-schedule"
class="text-gray-700 pb-1 mb-3 border-b inline-flex flex-row items-center gap-x-4"
><div>Primary Schedule </div>
<Badge color="gray">{schedule.schedule}</Badge>
<Toggle
@@ -336,6 +336,7 @@
<div class="mt-6 grid grid-cols-1 sm:grid-cols-3 gap-6">
<div class="col-span-2">
<h2 class="mb-2">Preview</h2>
<RunForm
autofocus
detailed={false}
@@ -345,7 +346,7 @@
runAction={runScript}
/>
</div>
<div class="mt-6 box">
<div class="box">
{defaultIfEmptyString(script.description, 'No description')}
</div>
</div>