mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-08 08:02:27 +00:00
Flow UX improvements wrt to triggers
This commit is contained in:
Generated
+20
-14
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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 <Tooltip>
|
||||
A common script is simply a script that is neither a trigger nor an approval script.
|
||||
Action <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)
|
||||
})
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user