feat(frontend): flow UX overhaul II + go + python support for trigger scripts (#928)

* all

* improve all

* improve all

* fix frontend

* fix input picker nits

* fix(frontend): Fix Menu display

* all

* fix all

* fix all

Co-authored-by: Faton Ramadani <faton.ramadani14@gmail.com>
This commit is contained in:
Ruben Fiszel
2022-11-22 16:18:14 +01:00
committed by GitHub
parent b935d69aad
commit 802abe7f90
35 changed files with 796 additions and 580 deletions
+3 -1
View File
@@ -1,8 +1,10 @@
import type { Script } from "./gen"
export type OwnerKind = 'group' | 'user'
export type ActionKind = 'Create' | 'Update' | 'Delete' | 'Execute'
export type SupportedLanguage = 'deno' | 'python3' | 'go' | 'bash'
export type SupportedLanguage = Script.language
export interface SchemaProperty {
type: string | undefined
@@ -188,7 +188,7 @@
<UnsavedConfirmationModal />
<Drawer bind:this={flowViewer} size="900px">
<Drawer bind:this={flowViewer} size="1200px">
<DrawerContent title="View Graph" on:close={flowViewer.closeDrawer} noPadding>
<FlowGraphViewer flow={$flowStore} overflowAuto />
</DrawerContent>
@@ -241,9 +241,11 @@
</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>
<div class="flex flex-row space-x-2">
<FlowPreviewButtons />
<div>
<Button disabled={pathError != ''} size="sm" on:click={saveFlow}>Save</Button>
</div>
</div>
</div>
@@ -67,6 +67,7 @@
arg.expr = getDefaultExpr(undefined, previousModuleId, rawValue)
arg.type = 'javascript'
propertyType = 'javascript'
monaco?.setCode(arg.expr)
}
}
@@ -92,8 +93,8 @@
</script>
{#if arg != undefined}
<div class="flex flex-row justify-between gap-4 mb-1">
<div class="flex items-center grow">
<div class="flex flex-row justify-between gap-1 mb-1">
<div class="flex items-center flex-wrap grow">
<FieldHeader
label={argName}
format={schema.properties[argName].format}
@@ -114,55 +115,63 @@
</span>
{/if}
</div>
<div>
<ToggleButtonGroup
bind:selected={propertyType}
on:selected={(e) => {
const staticTemplate = isStaticTemplate(inputCat)
if (e.detail === 'javascript') {
arg.expr = getDefaultExpr(
argName,
previousModuleId,
staticTemplate ? `\`${arg.value ?? ''}\`` : arg.value
)
<div class="flex flex-row gap-1 flex-wrap">
<div>
<ToggleButtonGroup
bind:selected={propertyType}
on:selected={(e) => {
const staticTemplate = isStaticTemplate(inputCat)
if (e.detail === 'javascript') {
arg.expr = getDefaultExpr(
argName,
previousModuleId,
staticTemplate
? `\`${arg.value ?? ''}\``
: arg.value
? JSON.stringify(arg.value, null, 4)
: ''
)
arg.value = undefined
propertyType = 'javascript'
arg.type = 'javascript'
} else {
arg.value = staticTemplate ? codeToStaticTemplate(arg.expr) : undefined
arg.expr = undefined
propertyType = 'static'
if (!isStaticTemplate) {
arg.type = 'static'
arg.value = undefined
propertyType = 'javascript'
arg.type = 'javascript'
} else {
setPropertyType(arg.value)
arg.value = staticTemplate ? codeToStaticTemplate(arg.expr) : undefined
arg.expr = undefined
propertyType = 'static'
if (!isStaticTemplate) {
arg.type = 'static'
} else {
setPropertyType(arg.value)
}
}
}
}}
>
{#if isStaticTemplate(inputCat)}
<ToggleButton position="left" value="static" size="xs">Template</ToggleButton>
{:else}
<ToggleButton position="left" value="static" size="xs">Static</ToggleButton>
{/if}
}}
>
{#if isStaticTemplate(inputCat)}
<ToggleButton position="left" value="static" size="xs">Template</ToggleButton>
{:else}
<ToggleButton position="left" value="static" size="xs">Static</ToggleButton>
{/if}
<ToggleButton position="right" value="javascript" startIcon={{ icon: faCode }} size="xs">
Dynamic (JS)
</ToggleButton>
</ToggleButtonGroup>
<ToggleButton position="right" value="javascript" startIcon={{ icon: faCode }} size="xs">
Dynamic (JS)
</ToggleButton>
</ToggleButtonGroup>
</div>
<div>
<Button
variant="contained"
color="blue"
size="xs"
on:click={() => {
focusProp(argName, 'connect', (path) => {
connectProperty(path)
return false
})
}}>Connect &rightarrow;</Button
>
</div>
</div>
<Button
variant="contained"
color="blue"
size="xs"
on:click={() => {
focusProp(argName, 'connect', (path) => {
connectProperty(path)
return false
})
}}>Connect &rightarrow;</Button
>
</div>
<div class="max-w-xs" />
@@ -135,12 +135,17 @@
}
function changePosition(i: number, up: boolean): any {
isAnimated = true
setTimeout(() => {
isAnimated = false
}, 500)
const entries = Object.entries(schema.properties)
var element = entries[i]
entries.splice(i, 1)
entries.splice(up ? i - 1 : i + 1, 0, element)
schema.properties = Object.fromEntries(entries)
}
let isAnimated = false
</script>
<div class="flex flex-col">
@@ -148,8 +153,8 @@
<div>
<Button
variant="contained"
color="blue"
size="md"
color="dark"
size="sm"
startIcon={{ icon: faPlus }}
on:click={() => {
modalProperty = Object.assign({}, DEFAULT_PROPERTY)
@@ -164,7 +169,7 @@
<Toggle
on:change={() => switchTab()}
options={{
right: 'Json Schema Editor'
right: 'As JSON'
}}
/>
<div class="ml-2">
@@ -192,7 +197,7 @@
</tr>
<tbody slot="body">
{#each Object.entries(schema.properties) as [name, property], i (name)}
<tr animate:flip={{ duration: 50 }}>
<tr animate:flip>
<td class="font-bold">{name}</td>
<td>
<SchemaEditorProperty {property} />
@@ -206,13 +211,17 @@
>
<td class="justify-end flex">
{#if i > 0}
<button on:click={() => changePosition(i, true)} class="text-lg mr-2"
>&uparrow;</button
<button
on:click={() => changePosition(i, true)}
class="text-lg mr-2 {isAnimated ? 'invisible' : ''}"
>
&uparrow;</button
>
{/if}
{#if i < Object.keys(schema.properties).length - 1}
<button on:click={() => changePosition(i, false)} class="text-lg mr-2"
>&downarrow;</button
<button
on:click={() => changePosition(i, false)}
class="text-lg mr-2 {isAnimated ? 'invisible' : ''}">&downarrow;</button
>
{/if}
@@ -19,14 +19,21 @@
import { faChevronDown, faChevronUp, faPen, faSave } from '@fortawesome/free-solid-svg-icons'
import Breadcrumb from './common/breadcrumb/Breadcrumb.svelte'
import Toggle from './Toggle.svelte'
import LanguageIcon from './common/languageIcons/LanguageIcon.svelte'
import type { SupportedLanguage } from '$lib/common'
export let script: Script
export let initialPath: string = ''
export let template: 'pgsql' | 'script' = 'script'
export let initialArgs: Record<string, any> = {}
const langs: [string, SupportedLanguage][] = [
['Typescript (Deno)', Script.language.DENO],
['Python 3.11', Script.language.PYTHON3],
['Go', Script.language.GO],
['Bash', Script.language.BASH]
]
let viewScriptKind = script.kind !== Script.kind.SCRIPT
let viewTemplate = script.kind !== Script.kind.SCRIPT && script.language == Script.language.DENO
let pathError = ''
@@ -185,18 +192,36 @@
/>
</label>
<h2 class="border-b pb-1 mt-4">Language</h2>
<div class="max-w-md">
<RadioButton
label="Language"
options={[
['Typescript (Deno)', 'deno'],
['Python 3.11', 'python3'],
['Go', 'go'],
['Bash', 'bash']
]}
on:change={(e) => initContent(e.detail, script.kind, template)}
bind:value={script.language}
/>
<div class="max-w-md flex flex-row gap-x-2">
{#each langs as [label, lang]}
{@const isPicked = script.language == lang && template == 'script'}
<Button
size="sm"
variant="border"
color={isPicked ? 'blue' : 'dark'}
btnClasses={isPicked ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
on:click={() => {
script.language = lang
template = 'script'
initContent(lang, script.kind, template)
}}
>
<LanguageIcon {lang} /><span class="ml-2">{label}</span>
</Button>
{/each}
<Button
size="sm"
variant="border"
color={template == 'pgsql' ? 'blue' : 'dark'}
btnClasses={template == 'pgsql' ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
on:click={() => {
script.language = Script.language.DENO
template = 'pgsql'
initContent(script.language, script.kind, template)
}}
>
<LanguageIcon lang="pgsql" /><span class="ml-2">PostgreSQL</span>
</Button>
</div>
<h2 class="border-b pb-1 mt-4">Advanced</h2>
<div>
@@ -206,7 +231,7 @@
endIcon={{ icon: viewScriptKind ? faChevronUp : faChevronDown }}
on:click={() => (viewScriptKind = !viewScriptKind)}
>
Specialize the script as a specific module kind for flows
Tag this script as having a specific purpose inside flows
</Button>
</div>
{#if viewScriptKind}
@@ -249,31 +274,6 @@
/>
</div>
{/if}
{#if script.language == 'deno' && script.kind == Script.kind.SCRIPT}
<div
><Button
color="light"
size="sm"
endIcon={{ icon: viewTemplate ? faChevronUp : faChevronDown }}
on:click={() => (viewTemplate = !viewTemplate)}
>
Use a predefined template specific to this language and script kind
</Button>
</div>
{#if viewTemplate}
<div class="max-w-lg">
<RadioButton
label="Template"
options={[
['Standard', 'script'],
['PostgreSQL Prepared Statement', 'pgsql']
]}
on:change={(e) => initContent(script.language, script.kind, e.detail)}
bind:value={template}
/>
</div>
{/if}
{/if}
<div class="ml-3">
<Toggle
bind:checked={script.is_template}
@@ -61,7 +61,7 @@
const handleHide = () => (hidden = !hidden)
</script>
<span class="inline-flex justify-center items-center whitespace-nowrap">
<span on:click on:keydown class="inline-flex justify-center items-center whitespace-nowrap">
<svelte:element
this={href ? 'a' : 'span'}
{href}
@@ -1,17 +1,19 @@
<script lang="ts">
import type { SupportedLanguage } from '$lib/common'
import PostgresIcon from '$lib/components/icons/PostgresIcon.svelte'
import type { SvelteComponent } from 'svelte'
import { BashIcon, GoIcon, PythonIcon, TypeScriptIcon } from './'
export let lang: SupportedLanguage
export let lang: SupportedLanguage | 'pgsql'
export let width = 16
export let height = 16
export let scale = 1
const langToComponent: Record<SupportedLanguage, typeof SvelteComponent> = {
const langToComponent: Record<SupportedLanguage | 'pgsql', typeof SvelteComponent> = {
go: GoIcon,
python3: PythonIcon,
deno: TypeScriptIcon,
bash: BashIcon
bash: BashIcon,
pgsql: PostgresIcon
}
</script>
@@ -1,8 +1,8 @@
<script lang="ts">
import { classNames } from '$lib/utils'
import { onMount } from 'svelte'
import { scale } from 'svelte/transition'
export let noMinW = false
let show = false
let menu: HTMLDivElement
@@ -47,15 +47,17 @@
</script>
<div class="relative" bind:this={menu}>
<div on:click={() => (show = !show)} on:click>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div on:click={() => (show = !show)}>
<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]
'z-50 absolute mt-2 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none',
placementsClasses[placement],
noMinW ? 'min-w-0' : 'w-60'
)}
role="menu"
tabindex="-1"
@@ -22,7 +22,7 @@
class={classNames(
$selected?.startsWith(value)
? 'border-b-2 border-gray-600 text-gray-800 '
: 'hover:border-b-2 hover:border-gray-300 text-gray-500',
: 'hover:border-b-2 hover:border-gray-300 text-gray-600',
'py-1 px-4 cursor-pointer transition-all ease-linear font-medium',
fontSizeClasses[size],
$$props.class
@@ -12,7 +12,7 @@
<div class="h-full overflow-hidden border-t">
<Splitpanes>
<Pane size={25} minSize={20} class="h-full">
<Pane size={20} minSize={15} class="h-full">
<div class="grow overflow-auto bg-gray h-full bg-gray-50 relative">
{#if loading}
<div class="p-2 pt-10">
@@ -25,7 +25,7 @@
{/if}
</div>
</Pane>
<Pane size={75} minSize={40}>
<Pane size={80} minSize={40}>
{#if loading}
<div class="w-full h-full ">
<div class="block m-auto mt-40 w-10">
@@ -80,7 +80,7 @@
{branch.summary && branch.summary != '' ? branch.summary : 'No summary'}</div
>
<div>
<Button size="sm" on:click={() => ($selectedId = branch.id)}>
<Button color="dark" size="sm" on:click={() => ($selectedId = branch.id)}>
Set branch {i} predicate</Button
>
</div>
@@ -94,7 +94,7 @@
{branch.summary && branch.summary != '' ? branch.summary : 'No summary'}
</div>
<div>
<Button size="sm" on:click={() => ($selectedId = branch.id)}>
<Button color="dark" size="sm" on:click={() => ($selectedId = branch.id)}>
Set branch {i} summary</Button
>
</div>
@@ -13,33 +13,29 @@
<CapturePayload bind:this={capturePayload} />
<FlowCard title="Flow Input">
<div slot="header">
<div class="flex flex-row space-x-4">
<div class="p-6">
<div class="flex flex-row items-center space-x-4 pb-2 border-b border-gray-400">
<div>Copy input's schema from</div>
<Button
color="light"
color="dark"
size="sm"
on:click={() => {
capturePayload.openDrawer()
}}
variant="border"
>
Capture from a request to seed inputs
A request
</Button>
<Button
color="light"
color="dark"
size="sm"
disabled={$flowStore.value.modules.length === 0 ||
isEmptyFlowModule($flowStore.value.modules[0])}
on:click={copyFirstStepSchema}
variant="border"
>
Copy from first step schema {#if $flowStore.value.modules.length === 0 || isEmptyFlowModule($flowStore.value.modules[0])}
(no steps to copy from!){/if}
First step's inputs
</Button>
</div>
</div>
<div>
<div class="p-6">
<div class="pt-6">
<SchemaEditor
bind:schema={$flowStore.schema}
on:change={() => {
@@ -49,6 +45,7 @@
</div>
</div>
<div class="p-6">
<h2 class="mb-4">Customize Inputs</h2>
<SchemaForm bind:schema={$flowStore.schema} editableSchema={true} />
</div>
</FlowCard>
@@ -1,163 +1,131 @@
<script lang="ts">
import { ToggleButton, ToggleButtonGroup } from '$lib/components/common'
import WindmillIcon from '$lib/components/icons/WindmillIcon.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import { RawScript, Script } from '$lib/gen'
import { faCode, faCodeBranch, faRepeat } from '@fortawesome/free-solid-svg-icons'
import { faBolt, faBuilding, faCheck, faCode } from '@fortawesome/free-solid-svg-icons'
import { createEventDispatcher } from 'svelte'
import FlowScriptPicker from '../pickers/FlowScriptPicker.svelte'
import PickHubScript from '../pickers/PickHubScript.svelte'
import PickScript from '../pickers/PickScript.svelte'
import WorkspaceScriptPicker from '../pickers/WorkspaceScriptPicker.svelte'
export let failureModule: boolean
export let shouldDisableTriggerScripts: boolean = false
const dispatch = createEventDispatcher()
let kind: 'script' | 'failure' | 'approval' | 'trigger' = failureModule ? 'failure' : 'script'
let pick_existing: 'workspace' | 'hub' = 'hub'
</script>
<div class="space-y-4 p-4">
<div class="text-sm font-bold">{failureModule ? 'Error handler' : 'Common script'}</div>
<div class="grid sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div class="space-y-4 p-4 h-full flex flex-col">
{#if !failureModule}
<div class="center-center">
<div class="max-w-min">
<ToggleButtonGroup bind:selected={kind}>
<ToggleButton position="left" value="script" size="sm" startIcon={{ icon: faCode }}>
Common
</ToggleButton>
{#if !shouldDisableTriggerScripts}
<ToggleButton position="center" value="trigger" size="sm" startIcon={{ icon: faBolt }}>
Trigger <Tooltip>
Used as a first step most commonly with a state and a schedule to watch for changes
on an external system, compute the diff since last time, set the new state. The
diffs are then treated one by one with a for-loop.
</Tooltip>
</ToggleButton>
{/if}
<ToggleButton position="right" value="approval" size="sm" startIcon={{ icon: faCheck }}>
Approval <Tooltip>
An approval step will suspend the execution of a flow until it has been approved
through the resume endpoints or the approval page by and solely by the recipients of
those secret urls. Use getResumeEndpoints from the wmill client to generate those
URLs.
</Tooltip>
</ToggleButton>
</ToggleButtonGroup>
</div>
</div>
{/if}
<h3>Inline new</h3>
<div class="flex flex-row flex-wrap gap-x-4 gap-y-2">
<FlowScriptPicker
label="Inline Python (3.10)"
icon={faCode}
iconColor="text-green-500"
on:click={() => {
dispatch('new', {
language: RawScript.language.PYTHON3,
kind: failureModule ? 'failure' : 'script',
subkind: 'flow'
})
}}
/>
<FlowScriptPicker
label="Inline Typescript (Deno)"
icon={faCode}
iconColor="text-blue-800"
label="Typescript (Deno)"
lang={Script.language.DENO}
on:click={() => {
dispatch('new', {
language: RawScript.language.DENO,
kind: failureModule ? 'failure' : 'script',
kind,
subkind: 'flow'
})
}}
/>
<FlowScriptPicker
label="Inline Go"
icon={faCode}
iconColor="text-blue-700"
label="Python (3.11)"
lang={Script.language.PYTHON3}
on:click={() => {
dispatch('new', {
language: RawScript.language.GO,
kind: failureModule ? 'failure' : 'script',
subkind: 'flow'
language: RawScript.language.PYTHON3,
kind,
subkind: kind
})
}}
/>
<FlowScriptPicker
label="Inline Bash"
icon={faCode}
iconColor="text-green-700"
on:click={() => {
dispatch('new', {
language: RawScript.language.BASH,
kind: failureModule ? 'failure' : 'script',
subkind: 'flow'
})
}}
/>
{#if !failureModule}
{#if kind != 'approval'}
<FlowScriptPicker
label={`Inline PostgreSQL`}
icon={faCode}
iconColor="text-blue-800"
on:click={() =>
dispatch('new', { language: RawScript.language.DENO, kind: 'script', subkind: 'pgsql' })}
label="Go"
lang={Script.language.GO}
on:click={() => {
dispatch('new', {
language: RawScript.language.GO,
kind,
subkind: 'flow'
})
}}
/>
{/if}
<PickScript
customText={failureModule ? 'Error Handler from workspace' : undefined}
kind={failureModule ? Script.kind.FAILURE : Script.kind.SCRIPT}
on:pick
/>
<PickHubScript
customText={failureModule ? 'Error Handler from Hub' : undefined}
kind={failureModule ? Script.kind.FAILURE : Script.kind.SCRIPT}
on:pick
/>
{#if kind == 'script'}
<FlowScriptPicker
label="Bash"
lang={Script.language.PYTHON3}
on:click={() => {
dispatch('new', {
language: RawScript.language.BASH,
kind,
subkind: 'flow'
})
}}
/>
{#if !failureModule}
<FlowScriptPicker
label={`PostgreSQL`}
lang="pgsql"
on:click={() =>
dispatch('new', { language: RawScript.language.DENO, kind, subkind: 'pgsql' })}
/>
{/if}
{/if}
</div>
{#if !shouldDisableTriggerScripts}
<div class="text-sm font-bold pt-8">
Trigger script
<Tooltip>
Used as a first step most commonly with an internal state and a schedule to watch for
changes on an external system, compute the diff since last time, set the new state. The
diffs are then treated one by one with a for-loop.
</Tooltip>
</div>
<div class="grid sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<PickScript customText="Trigger script from workspace" kind={Script.kind.TRIGGER} on:pick />
<PickHubScript customText="Trigger script from Hub" kind={Script.kind.TRIGGER} on:pick />
<FlowScriptPicker
label="Inline Typescript (Deno)"
icon={faCode}
iconColor="text-blue-800"
on:click={() => dispatch('new', { language: RawScript.language.DENO, kind: 'trigger' })}
/>
</div>
{/if}
{#if !failureModule}
<div class="text-sm font-bold pt-8">
Approval step
<Tooltip>
Inlined common scripts can be turned into approval step by changing their suspend settings.
An approval step will suspend the execution of a flow until it has been approved through the
resume endpoints or the approval page by and solely by the recipients of those secret urls.
Use getResumeEndpoints from the wmill client to generate those URLs.
</Tooltip>
</div>
<div class="grid sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<PickScript customText="Approval step from workspace" kind={Script.kind.APPROVAL} on:pick />
<PickHubScript
customText={'Approval step from the Hub'}
kind={Script.kind.APPROVAL}
on:pick
/>
<FlowScriptPicker
label="Inline Typescript (Deno)"
icon={faCode}
iconColor="text-blue-800"
on:click={() => dispatch('new', { language: RawScript.language.DENO, kind: 'approval' })}
/>
</div>
<div class="text-sm font-bold pt-8">Flow primitive</div>
<div class="grid sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<FlowScriptPicker
label={`Branches and switch to one`}
icon={faCodeBranch}
iconColor="text-blue-500"
on:click={() => dispatch('branchone')}
/>
<FlowScriptPicker
label={`Branches and run them all`}
icon={faCodeBranch}
iconColor="text-blue-500"
on:click={() => dispatch('branchall')}
/>
<FlowScriptPicker
label={`For loop`}
icon={faRepeat}
iconColor="text-blue-500"
on:click={() => dispatch('loop')}
/>
</div>
<h3>Use pre-made</h3>
<div class="max-w-min">
<ToggleButtonGroup bind:selected={pick_existing}>
<ToggleButton position="left" value="hub" size="xs">
<WindmillIcon white={pick_existing == 'hub'} height="16px" width="16px" /><div
class="mr-1"
/>Hub
</ToggleButton>
<ToggleButton position="right" value="workspace" size="xs" startIcon={{ icon: faBuilding }}>
Workspace
</ToggleButton>
</ToggleButtonGroup>
</div>
{#if pick_existing == 'hub'}
<PickHubScript {kind} on:pick />
{:else}
<WorkspaceScriptPicker {kind} on:pick />
{/if}
</div>
@@ -165,7 +165,7 @@
<Splitpanes horizontal>
<Pane size={50} minSize={20}>
{#if flowModule.value.type === 'rawscript'}
<div on:mouseleave={() => reload(flowModule)} class="h-full">
<div class="h-full">
<Editor
bind:websocketAlive
bind:this={editor}
@@ -46,34 +46,12 @@
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>
{:else}
<h1 class="p-4"
>Select a step kind <Tooltip
>Until being defined, this step acts as an identify function, returning as result its
input and assigning it a key 'previous_result' if the input is not a json object</Tooltip
></h1
>
{/if}
<FlowInputs
shouldDisableTriggerScripts={parentModule !== undefined ||
previousModule !== undefined ||
$selectedId == 'failure'}
on:loop={async () => {
const [module, state] = await createLoop(flowModule.id)
flowModule = module
$flowStateStore[module.id] = state
}}
on:branchone={async () => {
const [module, state] = await createBranches(flowModule.id)
flowModule = module
$flowStateStore[module.id] = state
}}
on:branchall={async () => {
const [module, state] = await createBranchAll(flowModule.id)
flowModule = module
$flowStateStore[module.id] = state
}}
on:pick={async ({ detail }) => {
const { path, summary, kind } = detail
const [module, state] = await pickScript(path, summary, flowModule.id)
@@ -56,7 +56,7 @@
{/if}
</div>
<Drawer bind:open={previewOpen} size="800px">
<Drawer bind:open={previewOpen} size="1200px">
<FlowPreviewContent
open={previewOpen}
bind:previewMode
@@ -9,12 +9,11 @@
import { classNames } from '$lib/utils'
import Icon from 'svelte-awesome'
import { deleteFlowStateById, emptyModule, idMutex } from '../flowStateUtils'
import { emptyFlowModuleState } from '../utils'
import { flowStateStore } from '../flowState'
let selectedBranch = 0
export let module: FlowModule
let selectedBranch = 0
const { select, selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
async function addBranch() {
@@ -24,11 +23,9 @@
module.value.branches.splice(module.value.branches.length, 0, {
summary: '',
skip_failure: false,
modules: [newModule]
modules: []
})
module = module
$flowStateStore[newModule.id] = emptyFlowModuleState()
}
})
}
@@ -52,43 +49,44 @@
>
{#each module.value.branches ?? [] as branch, branchIndex (branchIndex)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
transition:slide|local
on:click={() => {
selectedBranch = branchIndex
select(`${module.id}-branch-${branchIndex}`)
}}
class={classNames(
`border-b ${
branchIndex > 0 ? 'border-t' : ''
} w-full p-2 bg-white border-gray-500 text-sm cursor-pointer flex items-center relative module`,
$selectedId === `${module.id}-branch-${branchIndex}`
? 'outline outline-2 outline-slate-900'
: ''
)}
>
<Icon data={faCodeBranch} class="mr-2" />
<span
class="text-xs flex flex-row justify-between w-full flex-wrap gap-2 items-center truncate"
<div transition:slide|local>
<div
on:click={() => {
selectedBranch = branchIndex
select(`${module.id}-branch-${branchIndex}`)
}}
class={classNames(
`border-b ${
branchIndex > 0 ? 'border-t' : ''
} w-full p-2 bg-white border-gray-500 text-sm cursor-pointer flex items-center relative module`,
$selectedId === `${module.id}-branch-${branchIndex}`
? 'outline outline-2 outline-slate-900'
: ''
)}
>
{branch.summary || `Branch ${branchIndex}`}
<button
class="absolute -top-2 -right-1 rounded-full h-4 w-4 bg-white center-center {$selectedId ===
`${module.id}-branch-${branchIndex}`
? ''
: '!hidden'} trash"
on:click={() => removeBranch(branchIndex)}
><Icon
data={faTimesCircle}
class="text-gray-600 hover:text-red-600"
scale={0.9}
/></button
<Icon data={faCodeBranch} class="mr-2" />
<span
class="text-xs flex flex-row justify-between w-full flex-wrap gap-2 items-center truncate"
>
</span>
</div>
{branch.summary || `Branch ${branchIndex}`}
<button
class="absolute -top-2 -right-1 rounded-full h-4 w-4 bg-white center-center {$selectedId ===
`${module.id}-branch-${branchIndex}`
? ''
: '!hidden'} trash"
on:click={() => removeBranch(branchIndex)}
><Icon
data={faTimesCircle}
class="text-gray-600 hover:text-red-600"
scale={0.9}
/></button
>
</span>
</div>
<div>
<FlowModuleSchemaMap bind:modules={branch.modules} />
<div>
<FlowModuleSchemaMap bind:modules={branch.modules} />
</div>
</div>
{/each}
<div class="overflow-clip ml-2 mt-2">
@@ -21,15 +21,12 @@
async function addBranch() {
await idMutex.runExclusive(async () => {
if (module.value.type === 'branchone') {
const newModule = emptyModule()
module.value.branches.splice(module.value.branches.length, 0, {
summary: '',
expr: '',
modules: [newModule]
expr: 'false',
modules: []
})
module = module
$flowStateStore[newModule.id] = emptyFlowModuleState()
}
})
}
@@ -77,41 +74,42 @@
{#each module.value.branches ?? [] as branch, branchIndex (branchIndex)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
transition:slide|local
on:click={() => {
selectedBranch = branchIndex + 1
select(`${module.id}-branch-${branchIndex}`)
}}
class={classNames(
`border-b border-t w-full p-2 bg-white border-gray-500 text-sm cursor-pointer flex items-center relative module`,
$selectedId === `${module.id}-branch-${branchIndex}`
? 'outline outline-2 outline-slate-900'
: ''
)}
>
<Icon data={faCodeBranch} class="mr-2" />
<span
class="text-xs flex flex-row justify-between w-full flex-wrap gap-2 items-center truncate"
<div transition:slide|local>
<div
on:click={() => {
selectedBranch = branchIndex + 1
select(`${module.id}-branch-${branchIndex}`)
}}
class={classNames(
`border-b border-t w-full p-2 bg-white border-gray-500 text-sm cursor-pointer flex items-center relative module`,
$selectedId === `${module.id}-branch-${branchIndex}`
? 'outline outline-2 outline-slate-900'
: ''
)}
>
{branch.summary || `Branch ${branchIndex}`}
<button
class="absolute -top-2 -right-2 rounded-full bg-white h-4 w-4 center-center trash {$selectedId ===
`${module.id}-branch-${branchIndex}`
? ''
: '!hidden'}"
on:click={() => removeBranch(branchIndex)}
><Icon
data={faTimesCircle}
class="text-gray-600 hover:text-red-600"
scale={0.9}
/></button
<Icon data={faCodeBranch} class="mr-2" />
<span
class="text-xs flex flex-row justify-between w-full flex-wrap gap-2 items-center truncate"
>
</span>
</div>
{branch.summary || `Branch ${branchIndex}`}
<button
class="absolute -top-2 -right-2 rounded-full bg-white h-4 w-4 center-center trash {$selectedId ===
`${module.id}-branch-${branchIndex}`
? ''
: '!hidden'}"
on:click={() => removeBranch(branchIndex)}
><Icon
data={faTimesCircle}
class="text-gray-600 hover:text-red-600"
scale={0.9}
/></button
>
</span>
</div>
<div>
<FlowModuleSchemaMap bind:modules={branch.modules} />
<div>
<FlowModuleSchemaMap bind:modules={branch.modules} />
</div>
</div>
{/each}
<div class="overflow-clip ml-2">
@@ -41,16 +41,18 @@
}
}}
class={classNames(
'border rounded-md p-2 bg-white text-sm border-gray-400 cursor-pointer flex flex-col overflow-x-hidden ',
'border rounded-sm p-2 bg-white text-sm border-gray-400 cursor-pointer flex flex-col overflow-x-hidden ',
$selectedId.includes('failure') ? 'outline outline-offset-1 outline-2 outline-slate-900' : ''
)}
>
<div class=" flex justify-between items-center flex-wrap">
<div>
<Icon data={faBug} class="mr-2" />
<span class="font-bold">Error handler</span>
<span class="font-bold text-xs">Error handler</span>
</div>
<div class="-my-1">
<Toggle checked={Boolean($flowStore.value.failure_module)} on:change={onToggle} />
</div>
<Toggle checked={Boolean($flowStore.value.failure_module)} on:change={onToggle} />
</div>
<div class="w-full truncate block">
@@ -1,8 +1,15 @@
<script lang="ts">
import type { FlowEditorContext } from '../types'
import { getContext } from 'svelte'
import { deleteFlowStateById, emptyModule, idMutex } from '$lib/components/flows/flowStateUtils'
import { flowStateStore } from '../flowState'
import {
createBranchAll,
createBranches,
createLoop,
deleteFlowStateById,
emptyModule,
idMutex
} from '$lib/components/flows/flowStateUtils'
import { flowStateStore, type FlowModuleState } from '../flowState'
import type { FlowModule } from '$lib/gen'
import FlowErrorHandlerItem from './FlowErrorHandlerItem.svelte'
import RemoveStepConfirmationModal from '../content/RemoveStepConfirmationModal.svelte'
@@ -19,12 +26,24 @@
let indexToRemove: number | undefined = undefined
const { select } = getContext<FlowEditorContext>('FlowEditorContext')
async function insertNewModuleAtIndex(index: number): Promise<void> {
async function insertNewModuleAtIndex(
index: number,
kind: 'script' | 'forloop' | 'branchone' | 'branchall'
): Promise<void> {
await idMutex.runExclusive(async () => {
const flowModule = emptyModule()
var module = emptyModule()
var state = emptyFlowModuleState()
if (kind == 'forloop') {
;[module, state] = await createLoop(module.id)
} else if (kind == 'branchone') {
;[module, state] = await createBranches(module.id)
} else if (kind == 'branchall') {
;[module, state] = await createBranchAll(module.id)
}
const flowModule = module
modules.splice(index, 0, flowModule)
modules = modules
$flowStateStore[flowModule.id] = emptyFlowModuleState()
$flowStateStore[flowModule.id] = state
select(flowModule.id)
})
}
@@ -75,11 +94,15 @@
<div class="flex flex-col h-full">
{#if root}
<div class="flex-initial p-4 border-b">
<div class="flex-initial px-3 py-2 border-b">
<FlowSettingsItem />
</div>
{/if}
<ul class="w-full flex-auto relative overflow-y-auto overflow-x-hidden {root ? 'px-2' : ''} py-1">
<ul
class="w-full flex-auto relative {root
? ' overflow-y-auto overflow-x-hidden px-2 my-2'
: ''} py-1"
>
{#if root}
<li>
<FlowInputsItem />
@@ -97,17 +120,17 @@
indexToRemove = index
}
}}
on:insert={() => {
insertNewModuleAtIndex(index)
on:insert={(e) => {
insertNewModuleAtIndex(index, e.detail)
}}
/>
</div>
{/each}
<InsertModuleButton on:click={() => insertNewModuleAtIndex(modules.length)} />
<InsertModuleButton on:new={(e) => insertNewModuleAtIndex(modules.length, e.detail)} />
</ul>
{#if root}
<div class="flex-none p-4 border-t">
<div class="flex-none px-4 py-2 border-t">
<FlowErrorHandlerItem />
</div>
{/if}
@@ -9,7 +9,7 @@
const { select, selectedId, schedule } = getContext<FlowEditorContext>('FlowEditorContext')
$: settingsClass = classNames(
'border w-full rounded-md p-2 bg-white border-gray-400 text-sm cursor-pointer flex items-center',
'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' : ''
)
</script>
@@ -1,12 +1,67 @@
<script lang="ts">
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import { Menu } from '$lib/components/common'
import { faCode, faCodeBranch, faPlus, faRepeat } from '@fortawesome/free-solid-svg-icons'
import { createEventDispatcher } from 'svelte'
import Icon from 'svelte-awesome'
const dispatch = createEventDispatcher()
</script>
<button
on:click
type="button"
class="text-gray-900 bg-white border mx-0.5 border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 font-medium rounded-full text-sm w-6 h-6 flex items-center justify-center"
>
<Icon data={faPlus} scale={0.8} />
</button>
<Menu noMinW placement="bottom-start" let:close>
<button
slot="trigger"
type="button"
class="text-gray-900 bg-white border mx-0.5 border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 font-medium rounded-full text-sm w-6 h-6 flex items-center justify-center"
>
<Icon data={faPlus} scale={0.8} />
</button>
<div class="divide-y divide-gray-100 text-xs w-40">
<button
class="w-full text-left p-2"
on:click={() => {
close()
dispatch('new', 'script')
}}
role="menuitem"
tabindex="-1"
>
<Icon data={faCode} scale={0.8} class="mr-1" />
Script
</button>
<button
class="w-full text-left p-2"
on:click={() => {
close()
dispatch('new', 'forloop')
}}
role="menuitem"
>
<Icon data={faRepeat} scale={0.8} class="mr-1" />
For Loop
</button>
<button
class="w-full text-left p-2"
on:click={() => {
close()
dispatch('new', 'branchone')
}}
role="menuitem"
>
<Icon data={faCodeBranch} scale={0.8} class="mr-1" />
Branch to one
</button>
<button
class="w-full text-left p-2"
on:click={() => {
close()
dispatch('new', 'branchall')
}}
role="menuitem"
>
<Icon data={faCodeBranch} scale={0.8} class="mr-1" />
Branch to all
</button>
</div>
</Menu>
@@ -12,7 +12,6 @@
faCode,
faCodeBranch,
faLongArrowDown,
faQuestion,
faRepeat
} from '@fortawesome/free-solid-svg-icons'
import Icon from 'svelte-awesome'
@@ -20,7 +19,10 @@
export let mod: FlowModule
const { select, selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
const dispatch = createEventDispatcher<{ delete: CustomEvent<MouseEvent>; insert: void }>()
const dispatch = createEventDispatcher<{
delete: CustomEvent<MouseEvent>
insert: 'script' | 'forloop' | 'branchone' | 'branchall'
}>()
$: itemProps = {
selected: $selectedId === mod.id,
@@ -35,7 +37,7 @@
</script>
{#if mod}
<InsertModuleButton on:click={() => dispatch('insert')} />
<InsertModuleButton on:new={(e) => dispatch('insert', e.detail)} />
{#if mod.value.type === 'forloopflow'}
<li class="w-full">
<FlowModuleSchemaItem
@@ -52,7 +54,7 @@
</FlowModuleSchemaItem>
<div class="flex flex-row w-full">
<div class="w-7 shrink-0 line" />
<div class="grow my-1 overflow-auto">
<div class="grow my-1">
<div class="w-full">
<FlowModuleSchemaMap bind:modules={mod.value.modules} />
</div>
@@ -1,11 +1,14 @@
<script lang="ts">
import type { SupportedLanguage } from '$lib/common'
import Button from '$lib/components/common/button/Button.svelte'
import type { IconDefinition } from '@fortawesome/free-brands-svg-icons'
import LanguageIcon from '$lib/components/common/languageIcons/LanguageIcon.svelte'
import type { IconDefinition } from '@fortawesome/free-solid-svg-icons'
export let disabled: boolean = false
export let icon: IconDefinition
export let label: string
export let iconColor: string
export let lang: SupportedLanguage | 'pgsql' | undefined = undefined
export let icon: IconDefinition | undefined = undefined
export let iconColor: string | undefined = undefined
</script>
<Button
@@ -21,5 +24,7 @@
classes: iconColor
}}
>
{label}
{#if lang}
<LanguageIcon {lang} />
{/if}<span class="ml-2">{label}</span>
</Button>
@@ -1,39 +1,88 @@
<script lang="ts">
import { faUserGroup } from '@fortawesome/free-solid-svg-icons'
import FlowScriptPicker from '$lib/components/flows/pickers/FlowScriptPicker.svelte'
import ItemPicker from '$lib/components/ItemPicker.svelte'
import { hubScripts } from '$lib/stores'
import { createEventDispatcher } from 'svelte'
import { Script } from '$lib/gen'
import type { HubItem } from './model'
import Fuse from 'fuse.js'
import IconedResourceType from '$lib/components/IconedResourceType.svelte'
import { Badge } from '$lib/components/common'
export let kind: Script.kind
export let customText: string | undefined = undefined
export let kind: 'script' | 'trigger' | 'approval' | 'failure' = 'script'
let items: HubItem[]
$: items = $hubScripts?.filter((x) => x.kind == kind) ?? []
let itemPicker: ItemPicker
let items: HubItem[] = []
let filteredItems: Item[] | undefined = []
let itemsFilter = ''
let appFilter: string | undefined = undefined
const fuseOptions = {
includeScore: false,
keys: ['path', 'summay']
}
const fuse: Fuse<Item> = new Fuse(items, fuseOptions)
$: {
items =
$hubScripts?.filter(
(x) => x.kind == kind && (appFilter == undefined || x.app == appFilter)
) ?? []
fuse.setCollection(items)
}
$: filteredItems =
itemsFilter.length > 0 && items ? fuse.search(itemsFilter).map((value) => value.item) : items
$: apps = Array.from(new Set(filteredItems?.map((x) => x.app) ?? []))
const dispatch = createEventDispatcher()
</script>
<ItemPicker
bind:this={itemPicker}
pickCallback={(path, summary) => {
dispatch('pick', { path, summary, kind })
}}
itemName={'Script'}
extraField="summary"
loadItems={async () => {
return items
}}
noItemMessage="Hub not reachable. If your environment is air gapped, contact sales@windmill.dev to setup a local mirror."
/>
<div class="flex flex-col min-h-0">
<div class="w-12/12 pb-4">
<input type="text" placeholder="Search script" bind:value={itemsFilter} class="search-item" />
</div>
<FlowScriptPicker
label={customText ?? `${kind == Script.kind.SCRIPT ? 'Script' : `${kind} script`} from the Hub`}
icon={faUserGroup}
iconColor="text-blue-500"
on:click={() => itemPicker.openDrawer()}
/>
<div class="gap-2 w-full flex flex-wrap pb-4">
{#each apps as app}
<Badge
class="cursor-pointer"
on:click={() => {
appFilter = appFilter == app ? undefined : app
}}
capitalize
color={app === appFilter ? 'blue' : 'gray'}
>
{app}
{#if app === appFilter}&cross;{/if}
</Badge>
{/each}
</div>
{#if filteredItems}
<div class="overflow-auto">
<ul class="divide-y divide-gray-200">
{#each filteredItems as obj}
<li class="flex flex-row w-full">
<button
class="py-4 px-1 gap-1 flex flex-row grow hover:bg-white hover:border text-black"
on:click={() => {
dispatch('pick', obj)
}}
>
<div class="mr-2 text-sm text-left truncate w-24">
<IconedResourceType after={true} silent={false} name={obj['app']} />
</div>
<div class="flex flex-col">
<div class="text-sm font-semibold flex flex-col">
<span class="mr-2 text-left">{obj['summary'] ?? ''}</span>
<span class="font-normal text-xs text-left italic overflow-hidden"
>{obj['path'] ?? ''}</span
>
</div>
<div class="text-xs font-light italic text-left">{obj['description'] ?? ''}</div>
</div>
</button>
</li>
{/each}
</ul>
</div>
{/if}
</div>
@@ -1,6 +1,6 @@
<script lang="ts">
import ItemPicker from '$lib/components/ItemPicker.svelte'
import { faUserGroup } from '@fortawesome/free-solid-svg-icons'
import { faBuilding, faUserGroup } from '@fortawesome/free-solid-svg-icons'
import { Script, ScriptService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { createEventDispatcher } from 'svelte'
@@ -31,7 +31,7 @@
<FlowScriptPicker
label={customText ?? `${kind == Script.kind.SCRIPT ? 'Script' : `${kind} script`} from workspace`}
icon={faUserGroup}
icon={faBuilding}
iconColor="text-blue-500"
on:click={() => itemPicker.openDrawer()}
/>
@@ -0,0 +1,63 @@
<script lang="ts">
import { workspaceStore } from '$lib/stores'
import { createEventDispatcher } from 'svelte'
import type { HubItem } from './model'
import Fuse from 'fuse.js'
import { ScriptService } from '$lib/gen'
export let kind: 'script' | 'trigger' | 'approval' | 'failure' = 'script'
let items: HubItem[] = []
let filteredItems: Item[] | undefined = []
let itemsFilter = ''
const fuseOptions = {
includeScore: false,
keys: ['path', 'summay']
}
const fuse: Fuse<Item> = new Fuse(items, fuseOptions)
$: $workspaceStore && loadItems()
$: filteredItems =
itemsFilter.length > 0 && items ? fuse.search(itemsFilter).map((value) => value.item) : items
async function loadItems(): Promise<void> {
items = await ScriptService.listScripts({ workspace: $workspaceStore!, kind })
fuse.setCollection(items)
}
const dispatch = createEventDispatcher()
</script>
<div class="flex flex-col min-h-0">
<div class="w-12/12 pb-4">
<input type="text" placeholder="Search script" bind:value={itemsFilter} class="search-item" />
</div>
{#if filteredItems}
<ul class="divide-y divide-gray-200 overflow-auto">
{#each filteredItems as obj}
<li class="flex flex-row w-full">
<button
class="py-4 px-1 gap-1 flex flex-row grow hover:bg-white hover:border text-black"
on:click={() => {
dispatch('pick', obj)
}}
>
<div class="flex flex-col">
<div class="text-sm font-semibold flex flex-col">
<span class="mr-2 text-left">{obj['summary'] ?? ''}</span>
<span class="font-normal text-xs text-left italic overflow-hidden"
>{obj['path'] ?? ''}</span
>
</div>
<div class="text-xs font-light italic text-left">{obj['description'] ?? ''}</div>
</div>
</button>
</li>
{/each}
</ul>
{/if}
</div>
@@ -49,10 +49,10 @@
</script>
<Splitpanes>
<Pane minSize={20} size={66} class="relative p-4 !transition-none">
<Pane minSize={20} size={60} class="relative p-4 !transition-none">
<slot />
</Pane>
<Pane minSize={20} size={34} class="px-2 py-2 h-full !transition-none">
<Pane minSize={20} size={40} class="py-2 relative !transition-none">
{#if result}
<PropPickerResult {result} />
{:else if pickableProperties}
+1 -1
View File
@@ -152,7 +152,7 @@ export async function runFlowPreview(args: Record<string, any>, flow: Flow) {
}
export function codeToStaticTemplate(code?: string): string | undefined {
if (!code) return undefined
if (!code || typeof code != 'string') return undefined
const lines = code
.split('\n')
@@ -59,135 +59,139 @@
}
</script>
{#if !notSelectable}
<div class="flex flex-row space-x-1">
{#if $propPickerConfig}
<Badge color="blue">
{`Selected: ${$propPickerConfig?.propName}`}
</Badge>
<Badge color="blue">
{`Mode: ${$propPickerConfig?.insertionMode}`}
</Badge>
{:else}
<Badge color="blue">&leftarrow; Select a step input</Badge>
<div class="flex flex-col h-full">
<div class="px-2">
{#if !notSelectable}
<div class="flex flex-row space-x-1">
{#if $propPickerConfig}
<Badge large color="blue">
{`Selected: ${$propPickerConfig?.propName}`}
</Badge>
<Badge large color="blue">
{`Mode: ${$propPickerConfig?.insertionMode}`}
</Badge>
{:else}
<Badge large color="blue">&leftarrow; Select a step input</Badge>
{/if}
</div>
{/if}
</div>
{/if}
<input
type="text"
bind:value={search}
class="bg-gray-50 mt-1 border border-gray-300 text-gray-900 text-sm rounded-lg block px-2 mb-2 w-full"
placeholder="Search prop..."
/>
<div class:bg-gray-100={!$propPickerConfig && !notSelectable}>
<div class="flex justify-between items-center space-x-1">
<span class="font-bold text-sm">Flow Input</span>
<div class="flex space-x-2 items-center" />
</div>
<div class="overflow-y-auto mb-2">
<ObjectViewer
pureViewer={!$propPickerConfig}
json={flowInputsFiltered}
on:select={(e) => {
dispatch('select', `flow_input.${e.detail}`)
}}
<input
type="text"
bind:value={search}
class="bg-gray-50 mt-2 border border-gray-300 text-gray-900 text-sm rounded-lg block mb-2 w-full"
placeholder="Search prop..."
/>
</div>
{#if error}
<span class="font-bold text-sm">Error</span>
<div class="overflow-y-auto px-2 grow" class:bg-gray-100={!$propPickerConfig && !notSelectable}>
<div class="flex justify-between items-center space-x-1">
<span class="font-bold text-sm">Flow Input</span>
<div class="flex space-x-2 items-center" />
</div>
<div class="overflow-y-auto mb-2">
<ObjectViewer
pureViewer={!$propPickerConfig}
json={{ previous_result: { error: 'The error to handle' } }}
on:select
json={flowInputsFiltered}
on:select={(e) => {
dispatch('select', `flow_input.${e.detail}`)
}}
/>
</div>
{:else}
{#if previousId}
<span class="font-bold text-sm">Previous Result</span>
{#if error}
<span class="font-bold text-sm">Error</span>
<div class="overflow-y-auto mb-2">
<ObjectViewer
topLevelNode
pureViewer={!$propPickerConfig}
json={Object.fromEntries(
Object.entries(resultByIdFiltered).filter(([k, v]) => k == previousId)
)}
on:select={(e) => {
dispatch('select', `results.${e.detail}`)
}}
json={{ previous_result: { error: 'The error to handle' } }}
on:select
/>
</div>
{:else}
{#if previousId}
<span class="font-bold text-sm">Previous Result</span>
<div class="overflow-y-auto mb-2">
<ObjectViewer
topLevelNode
pureViewer={!$propPickerConfig}
json={Object.fromEntries(
Object.entries(resultByIdFiltered).filter(([k, v]) => k == previousId)
)}
on:select={(e) => {
dispatch('select', `results.${e.detail}`)
}}
/>
</div>
{/if}
{#if Object.keys(pickableProperties.priorIds).length > 0}
<span class="font-bold text-sm">All Results</span>
<div class="overflow-y-auto mb-2">
<ObjectViewer
topLevelNode
pureViewer={!$propPickerConfig}
collapsed={true}
json={resultByIdFiltered}
on:select={(e) => {
dispatch('select', `results.${e.detail}`)
}}
/>
</div>
{/if}
{/if}
{#if Object.keys(pickableProperties.priorIds).length > 0}
<span class="font-bold text-sm">All Results</span>
<div class="overflow-y-auto mb-2">
<ObjectViewer
topLevelNode
pureViewer={!$propPickerConfig}
collapsed={true}
json={resultByIdFiltered}
on:select={(e) => {
dispatch('select', `results.${e.detail}`)
}}
/>
</div>
{/if}
{/if}
{#if displayContext}
<span class="font-bold text-sm">Variables </span>
<div class="overflow-y-auto mb-2">
{#if displayVariable}
<Button
color="light"
size="xs"
variant="border"
on:click={() => {
displayVariable = false
}}>-</Button
>
<ObjectViewer
pureViewer={!$propPickerConfig}
rawKey={true}
json={variables}
on:select={(e) => dispatch('select', `variable('${e.detail}')`)}
/>
{:else}
<button
class="border border-blue-600 key font-normal rounded hover:bg-blue-100 px-1"
on:click={async () => {
await loadVariables()
displayVariable = true
}}>{'{...}'}</button
>
{/if}
</div>
<span class="font-bold text-sm">Resources</span>
<div class="overflow-y-auto mb-2">
{#if displayResources}
<Button
color="light"
variant="border"
size="xs"
on:click={() => {
displayResources = false
}}>-</Button
>
<ObjectViewer
pureViewer={!$propPickerConfig}
rawKey={true}
json={resources}
on:select={(e) => dispatch('select', `resource('${e.detail}')`)}
/>
{:else}
<button
class="border border-blue-600 px-1 key font-normal rounded hover:bg-blue-100"
on:click={async () => {
await loadResources()
displayResources = true
}}>{'{...}'}</button
>
{/if}
</div>
{/if}
{#if displayContext}
<span class="font-bold text-sm">Variables </span>
<div class="overflow-y-auto mb-2">
{#if displayVariable}
<Button
color="light"
size="xs"
variant="border"
on:click={() => {
displayVariable = false
}}>-</Button
>
<ObjectViewer
pureViewer={!$propPickerConfig}
rawKey={true}
json={variables}
on:select={(e) => dispatch('select', `variable('${e.detail}')`)}
/>
{:else}
<button
class="border border-blue-600 key font-normal rounded hover:bg-blue-100 px-1"
on:click={async () => {
await loadVariables()
displayVariable = true
}}>{'{...}'}</button
>
{/if}
</div>
<span class="font-bold text-sm">Resources</span>
<div class="overflow-y-auto mb-2">
{#if displayResources}
<Button
color="light"
variant="border"
size="xs"
on:click={() => {
displayResources = false
}}>-</Button
>
<ObjectViewer
pureViewer={!$propPickerConfig}
rawKey={true}
json={resources}
on:select={(e) => dispatch('select', `resource('${e.detail}')`)}
/>
{:else}
<button
class="border border-blue-600 px-1 key font-normal rounded hover:bg-blue-100"
on:click={async () => {
await loadResources()
displayResources = true
}}>{'{...}'}</button
>
{/if}
</div>
{/if}
</div>
</div>
+57 -13
View File
@@ -161,20 +161,60 @@ export const DENO_INIT_CODE_TRIGGER = `import * as wmill from "https://deno.land
export async function main() {
// A common trigger script would follow this pattern:
// 1. Get the last saved state
// const state = await wmill.getState()
// 2. Get the actual state from the external service
// const newState = await (await fetch('https://hacker-news.firebaseio.com/v0/topstories.json')).json()
// 3. Compare the two states and update the internal state
// await wmill.setState(newState)
// 4. Return the new rows
// return range from (state to newState)
// A common trigger script would follow this pattern:
// 1. Get the last saved state
// const state = await wmill.getState()
// 2. Get the actual state from the external service
// const newState = await (await fetch('https://hacker-news.firebaseio.com/v0/topstories.json')).json()
// 3. Compare the two states and update the internal state
// await wmill.setState(newState)
// 4. Return the new rows
// return range from (state to newState)
return [1,2,3]
return [1,2,3]
// In subsequent scripts, you may refer to each row/value returned by the trigger script using
// 'flow_input._value'
// In subsequent scripts, you may refer to each row/value returned by the trigger script using
// 'flow_input.iter.value'
}
`
export const PYTHON_INIT_CODE_TRIGGER = `import wmill
def main():
# A common trigger script would follow this pattern:
# 1. Get the last saved state
# const state = wnmill.get_state()
# 2. Get the actual state from the external service
# const newState = ...
# 3. Compare the two states and update the internal state
# wmill.setState(newState)
# 4. Return the new rows
# return range from (state to newState)
return [1, 2, 3]
`
export const GO_INIT_CODE_TRIGGER = `package inner
import (
wmill "github.com/windmill-labs/windmill-go-client"
)
func main() (interface{}, error) {
// A common trigger script would follow this pattern:
// 1. Get the last saved state
state, _ := wmill.GetState()
// 2. Get the actual state from the external service
// newState := ...
// 3. Compare the two states and update the internal state
wmill.SetState(4)
// 4. Return the new rows
return state, nil
// In subsequent scripts, you may refer to each row/value returned by the trigger script using
// 'flow_input.iter.value'
}
`
@@ -184,7 +224,7 @@ export async function main(approver?: string) {
return wmill.getResumeEndpoints(approver)
}`
const ALL_INITIAL_CODE = [PYTHON_INIT_CODE, DENO_INIT_CODE, POSTGRES_INIT_CODE, DENO_INIT_CODE_TRIGGER, DENO_INIT_CODE_CLEAR, PYTHON_INIT_CODE_CLEAR, DENO_INIT_CODE_APPROVAL, DENO_FAILURE_MODULE_CODE]
const ALL_INITIAL_CODE = [PYTHON_INIT_CODE, PYTHON_INIT_CODE_TRIGGER, DENO_INIT_CODE, POSTGRES_INIT_CODE, DENO_INIT_CODE_TRIGGER, DENO_INIT_CODE_CLEAR, PYTHON_INIT_CODE_CLEAR, DENO_INIT_CODE_APPROVAL, DENO_FAILURE_MODULE_CODE]
export function isInitialCode(content: string): boolean {
for (const code of ALL_INITIAL_CODE) {
@@ -221,6 +261,8 @@ export function initialCode(language: 'deno' | 'python3' | 'go' | 'bash', kind:
return PYTHON_INIT_CODE_CLEAR
} else if (kind === 'failure') {
return PYTHON_FAILURE_MODULE_CODE
} else if (kind === 'trigger') {
return PYTHON_INIT_CODE_TRIGGER
} else {
return PYTHON_INIT_CODE
}
@@ -229,6 +271,8 @@ export function initialCode(language: 'deno' | 'python3' | 'go' | 'bash', kind:
} else {
if (kind === 'failure') {
return GO_FAILURE_MODULE_CODE
} else if (kind === 'trigger') {
return GO_INIT_CODE_TRIGGER
} else {
return GO_INIT_CODE
}
+3 -4
View File
@@ -59,9 +59,8 @@ export function displayDate(dateString: string | undefined): string {
if (date.toString() === 'Invalid Date') {
return ''
} else {
return `${date.getFullYear()}/${
date.getMonth() + 1
}/${date.getDate()} at ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`
return `${date.getFullYear()}/${date.getMonth() + 1
}/${date.getDate()} at ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`
}
}
@@ -441,7 +440,7 @@ export async function getScriptByPath(path: string): Promise<{
return {
content,
language,
language: language as SupportedLanguage,
schema
}
} else {
+2 -1
View File
@@ -44,7 +44,8 @@
path: templatePath
})
Object.assign(flow, template)
flow.path = templatePath
const oldPath = flow.path.split('/')
flow.path = `u/${$userStore?.username}/${oldPath[oldPath.length - 1]}`
flow = flow
$page.url.searchParams.delete('template')
selectedId = 'settings-graph'
+11 -9
View File
@@ -20,7 +20,7 @@
import { userStore, workspaceStore } from '$lib/stores'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import RunForm from '$lib/components/RunForm.svelte'
import { Button } from '$lib/components/common'
import { Button, Skeleton } from '$lib/components/common'
import { faPlay, faScroll } from '@fortawesome/free-solid-svg-icons'
import SharedBadge from '$lib/components/SharedBadge.svelte'
import SvelteMarkdown from 'svelte-markdown'
@@ -131,13 +131,15 @@
<SvelteMarkdown source={defaultIfEmptyString(flow.description, 'No description')} />
</div>
</div>
<RunForm
autofocus
bind:this={runForm}
bind:isValid
detailed={false}
runnable={flow}
runAction={runFlow}
/>
{:else}
<Skeleton layout={[2, [3], 1, [2], 4, [4], 3, [8]]} />
{/if}
<RunForm
autofocus
bind:this={runForm}
bind:isValid
detailed={false}
runnable={flow}
runAction={runFlow}
/>
</CenteredPage>
@@ -23,7 +23,7 @@
import { inferArgs } from '$lib/infer'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import RunForm from '$lib/components/RunForm.svelte'
import { Alert, Badge, Button } from '$lib/components/common'
import { Alert, Badge, Button, Skeleton } from '$lib/components/common'
import SharedBadge from '$lib/components/SharedBadge.svelte'
import SvelteMarkdown from 'svelte-markdown'
import { faPlay, faScroll } from '@fortawesome/free-solid-svg-icons'
@@ -167,30 +167,32 @@
<SvelteMarkdown source={defaultIfEmptyString(script.description, 'No description')} />
</div>
</div>
{/if}
{#if script?.lock_error_logs}
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4" role="alert">
<p class="font-bold">Not deployed properly</p>
<p>
This version of this script is unable to be run because because the deployment had the
following errors:
</p>
<pre class="w-full text-xs mt-2 whitespace-pre-wrap">{script.lock_error_logs}</pre>
</div>
{:else if script && script?.lock == undefined}
<div class="bg-orange-100 border-l-4 border-orange-500 text-orange-700 p-4" role="alert">
<p class="font-bold">Deployment in progress</p>
<p>Refresh this page in a few seconds.</p>
</div>
{#if script?.lock_error_logs}
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4" role="alert">
<p class="font-bold">Not deployed properly</p>
<p>
This version of this script is unable to be run because because the deployment had the
following errors:
</p>
<pre class="w-full text-xs mt-2 whitespace-pre-wrap">{script.lock_error_logs}</pre>
</div>
{:else if script && script?.lock == undefined}
<div class="bg-orange-100 border-l-4 border-orange-500 text-orange-700 p-4" role="alert">
<p class="font-bold">Deployment in progress</p>
<p>Refresh this page in a few seconds.</p>
</div>
{:else}
<RunForm
autofocus
detailed={false}
bind:isValid
bind:this={runForm}
runnable={script}
runAction={runScript}
/>
{/if}
{:else}
<RunForm
autofocus
detailed={false}
bind:isValid
bind:this={runForm}
runnable={script}
runAction={runScript}
/>
<Skeleton layout={[2, [3], 1, [2], 4, [4], 3, [8]]} />
{/if}
</CenteredPage>