feat(frontend): overhaul the whole flow UX

This commit is contained in:
Ruben Fiszel
2022-11-21 17:28:55 +01:00
parent c6dc07007c
commit d23e218e1f
30 changed files with 382 additions and 336 deletions
@@ -65,6 +65,7 @@
}
$: {
error = ''
if (inputCat === 'object') {
evalValueToRaw()
validateInput(pattern, value)
+7 -6
View File
@@ -10,6 +10,7 @@
faCode,
faCube,
faDollarSign,
faEye,
faRotate,
faRotateLeft,
faWallet
@@ -232,7 +233,7 @@
startIcon={{ icon: faDollarSign }}
{iconOnly}
>
+Contextual Variable
+Context Var
</Button>
</div>
<div>
@@ -245,7 +246,7 @@
startIcon={{ icon: faWallet }}
{iconOnly}
>
+Variable
+Var
</Button>
</div>
<div>
@@ -270,9 +271,9 @@
color="light"
on:click={scriptPicker.openDrawer}
{iconOnly}
startIcon={{ icon: faCode }}
startIcon={{ icon: faEye }}
>
View Script
Script
</Button>
</div>
@@ -286,7 +287,7 @@
{iconOnly}
startIcon={{ icon: faRotateLeft }}
>
Reset content
Reset
</Button>
</div>
</div>
@@ -300,7 +301,7 @@
startIcon={{ icon: faRotate }}
>
{#if !iconOnly}
Reload assistants
Assistant
{/if}
<span class="ml-1 -my-1">
{#if lang == 'deno'}
@@ -11,10 +11,12 @@
| undefined = undefined
</script>
<div>
<div class="inline-flex flex-row items-center">
<span class="font-semibold">
{label}
</span>
<Required {required} class="!ml-0 pr-1" />
<span class="text-sm italic text-indigo-800">
({type ?? 'any'}{contentEncoding && contentEncoding != ''
? `, encoding: ${contentEncoding}`
@@ -22,5 +24,4 @@
? ` of ${itemsType?.type}s`
: ''})</span
>
<Required {required} class="!ml-0" />
</div>
@@ -10,17 +10,15 @@
import type { PropPickerWrapperContext } from './flows/propPicker/PropPickerWrapper.svelte'
import { codeToStaticTemplate, getDefaultExpr, isCodeInjection } from './flows/utils'
import SimpleEditor from './SimpleEditor.svelte'
import Toggle from './Toggle.svelte'
import { Button } from './common'
import Icon from 'svelte-awesome'
import { faChain } from '@fortawesome/free-solid-svg-icons'
import { Button, ToggleButton, ToggleButtonGroup } from './common'
import { faCode } from '@fortawesome/free-solid-svg-icons'
export let schema: Schema
export let arg: InputTransform | any
export let argName: string
export let extraLib: string = 'missing extraLib'
export let inputCheck: boolean = true
export let importPath: string | undefined = undefined
export let previousModuleId: string | undefined
export let monaco: SimpleEditor | undefined = undefined
let argInput: ArgInput | undefined = undefined
@@ -44,7 +42,7 @@
}
if (isCodeInjection(rawValue)) {
arg.expr = getDefaultExpr(importPath, argName, `\`${rawValue}\``)
arg.expr = getDefaultExpr(argName, previousModuleId, `\`${rawValue}\``)
arg.type = 'javascript'
propertyType = 'static'
} else {
@@ -66,14 +64,12 @@
arg.value = `\$\{${rawValue}}`
setPropertyType(arg.value)
} else {
arg.expr = getDefaultExpr(importPath, undefined, rawValue)
arg.expr = getDefaultExpr(undefined, previousModuleId, rawValue)
arg.type = 'javascript'
propertyType = 'javascript'
}
}
$: checked = propertyType == 'javascript'
function onFocus() {
if (isStaticTemplate(inputCat)) {
focusProp(argName, 'append', (path) => {
@@ -96,68 +92,85 @@
</script>
{#if arg != undefined}
<div class="flex justify-between items-center mb-2">
<div class="flex items-center">
<FieldHeader
label={argName}
format={schema.properties[argName].format}
contentEncoding={schema.properties[argName].contentEncoding}
required={schema.required.includes(argName)}
type={schema.properties[argName].type}
itemsType={schema.properties[argName].items}
/>
<div class="flex flex-row justify-between items-center gap-x-1 mb-2">
<div class="flex flex-row gap-4">
<div class="flex items-center w-full">
<FieldHeader
label={argName}
format={schema.properties[argName].format}
contentEncoding={schema.properties[argName].contentEncoding}
required={schema.required.includes(argName)}
type={schema.properties[argName].type}
itemsType={schema.properties[argName].items}
/>
{#if !checked && arg.type === 'javascript'}
<span class="bg-blue-100 text-blue-800 text-sm font-medium mr-2 px-2.5 py-0.5 rounded ml-2">
{'${...}'}
</span>
{/if}
{#if propertyType == 'static' && arg.type === 'javascript'}
<span
class="bg-blue-100 text-blue-800 text-sm font-medium mr-2 px-2.5 py-0.5 rounded ml-2"
>
{'${...}'}
</span>
{/if}
</div>
<div class="flex flex-col w-full gap-2">
<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
)
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'
} 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}
<ToggleButton position="right" value="javascript" startIcon={{ icon: faCode }} size="xs">
Dynamic (JS)
</ToggleButton>
</ToggleButtonGroup>
<!-- <InputsSpecEditor bind:appInputTransform={componentInputSpecs[inputSpecKey]} /> -->
</div>
</div>
<div class="flex flex-row space-x-4 items-center">
<Toggle
bind:checked
options={{
right: 'Raw Javascript Editor'
}}
on:change={(e) => {
const type = e.detail ? 'javascript' : 'static'
const staticTemplate = isStaticTemplate(inputCat)
if (type === 'javascript') {
arg.expr = getDefaultExpr(
importPath,
argName,
staticTemplate ? `\`${arg.value ?? ''}\`` : arg.value
)
arg.value = undefined
propertyType = 'javascript'
} else {
arg.value = staticTemplate ? codeToStaticTemplate(arg.expr) : undefined
arg.expr = undefined
propertyType = 'static'
}
arg.type = type
}}
/>
<Button
variant="contained"
color="blue"
size="sm"
on:click={() => {
focusProp(argName, 'connect', (path) => {
connectProperty(path)
return false
})
}}>Connect &rightarrow;</Button
>
<div>
<Button
variant="contained"
color="blue"
size="xs"
on:click={() => {
focusProp(argName, 'connect', (path) => {
connectProperty(path)
return false
})
}}>Connect &rightarrow;</Button
>
</div>
</div>
</div>
<div class="max-w-xs" />
{#if propertyType === undefined || !checked}
{#if propertyType === undefined || propertyType == 'static'}
<ArgInput
bind:this={argInput}
on:focus={onFocus}
@@ -183,30 +196,27 @@
}
}}
/>
{:else if checked}
{#if arg.expr != undefined}
<div class="border rounded p-2 mt-2 border-gray-300">
<SimpleEditor
bind:this={monaco}
bind:code={arg.expr}
{extraLib}
lang="javascript"
class="few-lines-editor"
extraLibPath="file:///node_modules/@types/windmill@{importPath}/index.d.ts"
shouldBindKey={false}
on:focus={() => {
focusProp(argName, 'insert', (path) => {
monaco?.insertAtCursor(path)
return false
})
}}
/>
</div>
<DynamicInputHelpBox {importPath} />
<div class="mb-2" />
{/if}
{:else if arg.expr != undefined}
<div class="border rounded p-2 mt-2 border-gray-300">
<SimpleEditor
bind:this={monaco}
bind:code={arg.expr}
{extraLib}
lang="javascript"
shouldBindKey={false}
on:focus={() => {
focusProp(argName, 'insert', (path) => {
monaco?.insertAtCursor(path)
return false
})
}}
autoHeight
/>
</div>
<DynamicInputHelpBox />
<div class="mb-2" />
{:else}
<p>Not recognized arg type {arg.type}</p>
Not recognized input type {argName}
{/if}
{:else}
<p class="text-sm text-gray-700">Arg at {argName} is undefined</p>
@@ -14,10 +14,8 @@
export let editableSchema = false
export let isValid: boolean = true
export let extraLib: string = 'missing extraLib'
export let importPath: string | undefined = undefined
export let autofocus = false
export let animateNew = false
export let previousModuleId: string | undefined = undefined
let clazz: string = ''
export { clazz as class }
@@ -46,12 +44,12 @@
<div transition:slide|local>
{#if inputTransform}
<InputTransformForm
{previousModuleId}
bind:arg={args[argName]}
bind:schema
bind:argName
bind:inputCheck={inputCheck[argName]}
bind:extraLib
bind:importPath
/>
{:else}
<ArgInput
@@ -3,7 +3,7 @@
import { CompletedJob, Job, JobService } from '$lib/gen'
import { userStore, workspaceStore } from '$lib/stores'
import { emptySchema, scriptLangToEditorLang } from '$lib/utils'
import { faPlay, faRotateRight } from '@fortawesome/free-solid-svg-icons'
import { faPlay } from '@fortawesome/free-solid-svg-icons'
import Editor from './Editor.svelte'
import { inferArgs } from '$lib/infer'
import type { Preview } from '$lib/gen/models/Preview'
@@ -16,7 +16,6 @@
import { onMount } from 'svelte'
import { Button, Kbd } from './common'
import SplitPanesWrapper from './splitPanes/SplitPanesWrapper.svelte'
import Tooltip from './Tooltip.svelte'
import WindmillIcon from './icons/WindmillIcon.svelte'
// Exported
@@ -165,12 +164,7 @@
<Pane size={40} minSize={10}>
<Splitpanes horizontal>
<Pane size={33}>
<div class="w-full bg-gray-100 px-2 text-sm"
>Preview <Tooltip>
To recompute the input schema press <Kbd>Ctrl/Cmd</Kbd> + <Kbd>S</Kbd> or move the focus
outside of the text editor
</Tooltip></div
>
<div class="w-full bg-gray-100 px-2 text-sm">Preview</div>
<div class="px-2">
<div class="break-words relative font-sans">
<SchemaForm {schema} bind:args bind:isValid />
@@ -38,8 +38,8 @@
export let formatAction: (() => void) | undefined = undefined
export let automaticLayout = true
export let extraLib: string = ''
export let extraLibPath: string = ''
export let shouldBindKey: boolean = true
export let autoHeight = false
const dispatch = createEventDispatcher()
@@ -96,6 +96,7 @@
}
}
let width = 0
async function loadMonaco() {
model = monaco.editor.createModel(code, lang, monaco.Uri.parse(uri))
@@ -115,6 +116,24 @@
dispatch('change')
})
if (autoHeight) {
let ignoreEvent = false
const updateHeight = () => {
const contentHeight = Math.min(1000, editor.getContentHeight())
if (divEl) {
divEl.style.height = `${contentHeight}px`
}
try {
ignoreEvent = true
editor.layout({ width, height: contentHeight })
} finally {
ignoreEvent = false
}
}
editor.onDidContentSizeChange(updateHeight)
updateHeight()
}
editor.onDidFocusEditorText(() => {
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, function () {
code = getCode()
@@ -133,13 +152,15 @@
dispatch('blur')
})
if (lang == 'javascript' && extraLib != '' && extraLibPath != '') {
if (lang == 'javascript' && extraLib != '') {
monaco.languages.typescript.javascriptDefaults.setExtraLibs([
{
content: extraLib,
filePath: extraLibPath
filePath: 'windmill.d.ts'
}
])
} else {
monaco.languages.typescript.javascriptDefaults.setExtraLibs([])
}
}
@@ -157,7 +178,7 @@
})
</script>
<div bind:this={divEl} class={$$props.class} />
<div bind:this={divEl} class={$$props.class} bind:clientWidth={width} />
<style>
.editor {
@@ -20,7 +20,7 @@
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class={classNames(
value === $selected
$selected?.startsWith(value)
? 'border-b-2 border-gray-600 text-gray-800 '
: 'hover:border-b-2 hover:border-gray-300 text-gray-500',
'py-1 px-4 cursor-pointer transition-all ease-linear font-medium',
@@ -6,13 +6,15 @@
</script>
<script lang="ts">
import { setContext } from 'svelte'
import { createEventDispatcher, setContext } from 'svelte'
import { writable, type Writable } from 'svelte/store'
export let selected: any
const dispatch = createEventDispatcher()
const selectedContent = writable(selected)
$: $selectedContent && dispatch('selected', $selectedContent)
setContext<ToggleButtonContext>('ToggleButtonGroup', {
selected: selectedContent,
select: (value: any) => {
@@ -3,8 +3,6 @@
import Icon from 'svelte-awesome'
import { slide } from 'svelte/transition'
export let importPath: string | undefined = undefined
$: opened = false
</script>
@@ -27,37 +25,14 @@
role="alert"
id="dynamic-input-help-box"
>
<p class="font-bold">Dynamic arg help</p>
<p>
When a field is using the "Raw Javascript Editor", its value is computed dynamically as the
evaluation of its corresponding javascript snippet.
</p>
That snippet can be a single line:
<pre><code>last_result.myarg</code></pre>
or a multiline:
<pre
><code
>let x = 5;
x + 2</code
></pre
>
the last line must always be the final expression.
<p>
If it is multiline, the statement before the final expression <b
>MUST END WITH ; and a newline</b
>
</p>
The snippet can also be a string template:
<code
>`Hello $&#123;params.name&#125;, all your base $&#123;previous_result.base_name&#125; belong
to us`</code
>
However, the last line must always be the final expression.
<p>
The snippet can use any javascript primitives, and the following flow specific objects and
functions:
</p>
<ul class="ml-4">
<li><b>result.id</b>: the result of step at id 'id'</li>
<li><b>{'results.<id>'}</b>: the result of step at id 'id'</li>
<li><b>flow_input</b>: the object containing the flow input arguments</li>
<li><b>params</b>: the object containing the current step static values</li>
<li>
@@ -68,11 +43,5 @@ x + 2</code
<b>resource(path)</b>: the function returning the resource at a given path as an object
</li>
</ul>
<p>To re-enable editor assistance, import the helper functions types using:</p>
<code>
{`import { results, flow_input, variable, resource, params } from 'windmill${
importPath ? `@${importPath}` : ''
}'`}
</code>
</div>
{/if}
@@ -22,7 +22,7 @@
const { previewArgs } = getContext<FlowEditorContext>('FlowEditorContext')
let editor: SimpleEditor | undefined = undefined
$: pickableProperties = getStepPropPicker(
$: stepPropPicker = getStepPropPicker(
$flowStateStore,
parentModule,
previousModule,
@@ -31,7 +31,7 @@
$previewArgs,
false,
true
).pickableProperties
)
</script>
<div class="h-full flex flex-col">
@@ -47,7 +47,7 @@
<div class="border w-full">
<PropPickerWrapper
notSelectable
{pickableProperties}
pickableProperties={stepPropPicker.pickableProperties}
on:select={({ detail }) => {
editor?.insertAtCursor(detail)
}}
@@ -58,6 +58,7 @@
bind:code={branch.expr}
class="small-editor"
shouldBindKey={false}
extraLib={stepPropPicker.extraLib}
/>
</PropPickerWrapper>
</div>
@@ -25,7 +25,7 @@
let editor: SimpleEditor | undefined = undefined
let selected: string = 'early-stop'
$: pickableProperties = getStepPropPicker(
$: stepPropPicker = getStepPropPicker(
$flowStateStore,
parentModule,
previousModule,
@@ -34,7 +34,7 @@
$previewArgs,
false,
true
).pickableProperties
)
</script>
<div class="h-full flex flex-col">
@@ -56,7 +56,7 @@
<div class="border w-full">
<PropPickerWrapper
notSelectable
{pickableProperties}
pickableProperties={stepPropPicker.pickableProperties}
on:select={({ detail }) => {
editor?.insertAtCursor(detail)
}}
@@ -67,6 +67,7 @@
bind:code={mod.value.iterator.expr}
class="small-editor"
shouldBindKey={false}
extraLib={stepPropPicker.extraLib}
/>
</PropPickerWrapper>
</div>
@@ -125,9 +125,9 @@
<svelte:fragment slot="header">
<FlowModuleHeader
bind:module={flowModule}
on:toggleSuspend={() => (selected = 'suspend')}
on:toggleRetry={() => (selected = 'retries')}
on:toggleStopAfterIf={() => (selected = 'early-stop')}
on:toggleSuspend={() => (selected = 'advanced-suspend')}
on:toggleRetry={() => (selected = 'advanced-retries')}
on:toggleStopAfterIf={() => (selected = 'advanced-early-stop')}
on:fork={async () => {
const [module, state] = await fork(flowModule)
flowModule = module
@@ -194,19 +194,9 @@
</Pane>
<Pane size={50} minSize={20}>
<Tabs bind:selected>
<Tab value="inputs"
><Tooltip>
Move the focus outside of the text editor to recompute the inputs or press
<Kbd>Ctrl/Cmd</Kbd> + <Kbd>S</Kbd>
</Tooltip><span class="font-semibold">Step Input</span></Tab
>
<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="retries">Retries</Tab>
{#if !$selectedId.includes('failure')}
<Tab value="early-stop">Early Stop</Tab>
<Tab value="suspend">Sleep/Suspend</Tab>
<Tab value="same_worker">Same Worker/Shared dir</Tab>
{/if}
<Tab value="advanced">Advanced</Tab>
</Tabs>
<div class="h-[calc(100%-32px)]">
{#if selected === 'inputs'}
@@ -215,7 +205,7 @@
<SchemaForm
schema={$flowStateStore[$selectedId]?.schema ?? {}}
inputTransform={true}
importPath={$selectedId}
previousModuleId={previousModule?.id}
bind:args={flowModule.value.input_transforms}
bind:extraLib={stepPropPicker.extraLib}
/>
@@ -227,27 +217,37 @@
mod={flowModule}
schema={$flowStateStore[$selectedId]?.schema ?? {}}
/>
{:else if selected === 'retries'}
<FlowRetries bind:flowModule class="px-4 pb-4 h-full overflow-auto" />
{:else if selected === 'early-stop'}
<FlowModuleEarlyStop bind:flowModule class="px-4 pb-4 h-full overflow-auto" />
{:else if selected === 'suspend'}
<div class="px-4 pb-4 h-full overflow-auto">
<FlowModuleSuspend previousModuleId={previousModule?.id} bind:flowModule />
</div>
{:else if selected === 'same_worker'}
<div class="p-4 h-full overflow-auto">
<Alert type="info" title="Share a directory using same worker">
If same worker is set, all steps will be run on the same worker and will share
the folder `/shared` to pass data between each other.
</Alert>
<Button
btnClasses="mt-4"
on:click={() => {
$selectedId = 'settings-same-worker'
}}>Set same worker in the flow settings</Button
>
</div>
{:else if selected.startsWith('advanced')}
<Tabs bind:selected>
<Tab value="advanced-retries">Retries</Tab>
{#if !$selectedId.includes('failure')}
<Tab value="advanced-early-stop">Early Stop</Tab>
<Tab value="advanced-suspend">Sleep/Suspend</Tab>
<Tab value="advanced-same_worker">Same Worker/Shared dir</Tab>
{/if}
</Tabs>
{#if selected === 'advanced-retries'}
<FlowRetries bind:flowModule class="px-4 pb-4 h-full overflow-auto" />
{:else if selected === 'advanced-early-stop'}
<FlowModuleEarlyStop bind:flowModule class="px-4 pb-4 h-full overflow-auto" />
{:else if selected === 'advanced-suspend'}
<div class="px-4 pb-4 h-full overflow-auto">
<FlowModuleSuspend previousModuleId={previousModule?.id} bind:flowModule />
</div>
{:else if selected === 'advanced-same_worker'}
<div class="p-4 h-full overflow-auto">
<Alert type="info" title="Share a directory using same worker">
If same worker is set, all steps will be run on the same worker and will share
the folder `/shared` to pass data between each other.
</Alert>
<Button
btnClasses="mt-4"
on:click={() => {
$selectedId = 'settings-same-worker'
}}>Set same worker in the flow settings</Button
>
</div>
{/if}
{/if}
</div>
</Pane>
@@ -63,6 +63,7 @@
lang="javascript"
bind:code={flowModule.stop_after_if.expr}
class="small-editor"
extraLib={`declare const result = ${JSON.stringify(result)};`}
/>
</PropPickerWrapper>
</div>
@@ -107,7 +107,12 @@
editor?.insertAtCursor(detail)
}}
>
<InputTransformForm bind:arg={flowModule.sleep} argName="sleep" {schema} />
<InputTransformForm
bind:arg={flowModule.sleep}
argName="sleep"
{schema}
{previousModuleId}
/>
</PropPickerWrapper>
</div>
{:else}
@@ -1,4 +1,5 @@
<script lang="ts">
import { Badge } from '$lib/components/common'
import Button from '$lib/components/common/button/Button.svelte'
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
@@ -11,16 +12,17 @@
let previewOpen = false
let previewMode: 'upTo' | 'whole' = 'whole'
$: upToDisabled = [
'settings',
'settings-schedule',
'settings-retries',
'settings-same-worker',
'settings-graph',
'inputs',
'schedules',
'failure'
].includes($selectedId)
$: upToDisabled =
[
'settings',
'settings-schedule',
'settings-retries',
'settings-same-worker',
'settings-graph',
'inputs',
'schedules',
'failure'
].includes($selectedId) || $selectedId.includes('branch')
</script>
<div class="flex flex-row-reverse justify-between items-center gap-x-2">
@@ -46,10 +48,10 @@
}}
endIcon={{ icon: faPlay }}
>
Test up to step {$selectedId
.split('-')
.map((x) => (Number.isNaN(Number(x)) ? x : Number(x) + 1))
.join(' ')}
Test up to
<Badge baseClass="ml-1" color="indigo">
{$selectedId}
</Badge>
</Button>
{/if}
</div>
@@ -46,21 +46,24 @@
</script>
{#if module.value.type === 'branchall'}
<div class="flex text-xs">
<div class="flex text-xs px-2">
<div
class="w-full space-y-2 flex flex-col border p-2 bg-gray-500 border-gray-600 bg-opacity-10 rounded-sm my-2 relative"
class="w-full space-y-2 pb-2 flex flex-col border bg-gray-500 border-gray-600 bg-opacity-10 rounded-sm my-2 relative"
>
{#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 w-full rounded-md p-2 bg-white text-sm cursor-pointer flex items-center relative module',
`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-offset-1 outline-2 outline-slate-900'
? 'outline outline-2 outline-slate-900'
: ''
)}
>
@@ -85,10 +88,10 @@
</div>
<div>
<FlowModuleSchemaMap bind:modules={branch.modules} color="indigo" />
<FlowModuleSchemaMap bind:modules={branch.modules} />
</div>
{/each}
<div class="overflow-clip">
<div class="overflow-clip ml-2 mt-2">
<Button
size="xs"
color="dark"
@@ -10,6 +10,7 @@
import { deleteFlowStateById, emptyModule, idMutex } from '../flowStateUtils'
import { emptyFlowModuleState } from '../utils'
import { flowStateStore } from '../flowState'
import { slide } from 'svelte/transition'
export let module: FlowModule
@@ -46,9 +47,9 @@
</script>
{#if module.value.type === 'branchone'}
<div class="flex text-xs">
<div class="flex text-xs px-2">
<div
class="w-full space-y-2 flex flex-col border p-2 bg-gray-500 border-gray-600 bg-opacity-10 rounded-sm my-2 relative"
class="w-full space-y-2 pb-2 flex flex-col border bg-gray-500 border-gray-600 bg-opacity-10 rounded-sm my-2 relative"
>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
@@ -57,9 +58,9 @@
select(`${module.id}-branch-default`)
}}
class={classNames(
'border w-full rounded-md p-2 bg-white text-sm cursor-pointer flex items-center',
`border-b w-full p-2 bg-white border-gray-500 text-sm cursor-pointer flex items-center relative module`,
$selectedId === `${module.id}-branch-default`
? 'outline outline-offset-1 outline-2 outline-slate-900'
? 'outline outline-2 outline-slate-900'
: ''
)}
>
@@ -71,20 +72,21 @@
</span>
</div>
<div>
<FlowModuleSchemaMap bind:modules={module.value.default} color="indigo" />
<FlowModuleSchemaMap bind:modules={module.value.default} />
</div>
{#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 w-full rounded-md p-2 bg-white text-sm cursor-pointer flex items-center mb-4 module relative',
`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-offset-1 outline-2 outline-slate-900'
? 'outline outline-2 outline-slate-900'
: ''
)}
>
@@ -109,10 +111,10 @@
</div>
<div>
<FlowModuleSchemaMap bind:modules={branch.modules} color="indigo" />
<FlowModuleSchemaMap bind:modules={branch.modules} />
</div>
{/each}
<div class="overflow-clip">
<div class="overflow-clip ml-2">
<Button
btnClasses=""
size="xs"
@@ -41,7 +41,7 @@
}
}}
class={classNames(
'border rounded-md p-2 bg-white text-sm cursor-pointer flex flex-col overflow-x-hidden ',
'border rounded-md 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' : ''
)}
>
@@ -3,7 +3,7 @@
import { getContext } from 'svelte'
import FlowModuleSchemaItem from './FlowModuleSchemaItem.svelte'
import Icon from 'svelte-awesome'
import { faPen } from '@fortawesome/free-solid-svg-icons'
import { faFlagCheckered, faPen } from '@fortawesome/free-solid-svg-icons'
const { select, selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
</script>
@@ -15,8 +15,9 @@
selected={$selectedId === 'inputs'}
bold
label="Flow Input"
id={'flow_input'}
>
<div slot="icon">
<Icon data={faPen} scale={0.8} />
<Icon data={faFlagCheckered} scale={0.8} />
</div>
</FlowModuleSchemaItem>
@@ -1,17 +1,10 @@
<script lang="ts">
import Badge from '$lib/components/common/badge/Badge.svelte'
import { classNames } from '$lib/utils'
import {
faBed,
faRepeat,
faStop,
faTimesCircle,
faTrashAlt
} from '@fortawesome/free-solid-svg-icons'
import { faBed, faRepeat, faStop, faTimesCircle } from '@fortawesome/free-solid-svg-icons'
import { createEventDispatcher } from 'svelte'
import Icon from 'svelte-awesome'
export let color: 'blue' | 'orange' | 'indigo' = 'blue'
export let isFirst: boolean = false
export let isLast: boolean = false
export let hasLine: boolean = true
@@ -32,25 +25,11 @@
<div class="flex relative" on:click>
<div
class={classNames(
'flex mr-2 ml-0.5',
'flex pl-6 ml-0.5',
hasLine ? 'line' : '',
isFirst ? 'justify-center items-start' : 'justify-center items-center'
)}
>
<div
class={classNames(
'flex justify-center items-center w-6 h-6 border rounded-full text-xs font-bold',
color === 'blue'
? 'bg-blue-200 text-blue-800'
: color === 'orange'
? 'bg-orange-200 text-orange-800'
: 'bg-blue-100 text-blue-600',
''
)}
>
<slot name="icon" />
</div>
</div>
/>
<div
class={classNames(
'w-full flex overflow-hidden rounded-sm cursor-pointer mr-2',
@@ -76,8 +55,12 @@
{/if}
</div>
<div
class="flex justify-between items-center w-full overflow-hidden border p-2 bg-white text-2xs module"
class="flex justify-between items-center w-full overflow-hidden rounded-sm border border-gray-400 p-2 bg-white text-2xs module"
>
{#if $$slots.icon}
<slot name="icon" />
<span class="mr-2" />
{/if}
<div class="flex-1 truncate" class:font-bold={bold}>{label}</div>
<div class="flex items-center space-x-2">
{#if id}
@@ -106,7 +89,7 @@
display: flex !important;
}
.line {
background: repeating-linear-gradient(to bottom, transparent 0 4px, #bbb 4px 8px) 50%/1px 100%
no-repeat;
background: repeating-linear-gradient(to bottom, transparent 0 4px, rgb(120, 120, 120) 4px 8px)
50%/1px 100% no-repeat;
}
</style>
@@ -11,9 +11,9 @@
import FlowSettingsItem from './FlowSettingsItem.svelte'
import FlowInputsItem from './FlowInputsItem.svelte'
import InsertModuleButton from './InsertModuleButton.svelte'
import { slide } from 'svelte/transition'
export let root: boolean = false
export let color: 'blue' | 'orange' | 'indigo' = 'blue'
export let modules: FlowModule[]
let indexToRemove: number | undefined = undefined
@@ -79,7 +79,7 @@
<FlowSettingsItem />
</div>
{/if}
<ul class="w-full flex-auto relative overflow-y-auto overflow-x-hidden px-2 py-1">
<ul class="w-full flex-auto relative overflow-y-auto overflow-x-hidden {root ? 'px-2' : ''} py-1">
{#if root}
<li>
<FlowInputsItem />
@@ -87,21 +87,21 @@
{/if}
{#each modules as mod, index (mod.id ?? index)}
<MapItem
{color}
{index}
bind:mod
on:delete={(event) => {
if (event.detail.detail.shiftKey || isEmptyFlowModule(mod)) {
removeAtIndex(index)
} else {
indexToRemove = index
}
}}
on:insert={() => {
insertNewModuleAtIndex(index)
}}
/>
<div transition:slide|local>
<MapItem
bind:mod
on:delete={(event) => {
if (event.detail.detail.shiftKey || isEmptyFlowModule(mod)) {
removeAtIndex(index)
} else {
indexToRemove = index
}
}}
on:insert={() => {
insertNewModuleAtIndex(index)
}}
/>
</div>
{/each}
<InsertModuleButton on:click={() => insertNewModuleAtIndex(modules.length)} />
@@ -125,17 +125,3 @@
}
}}
/>
<style>
.badge {
@apply whitespace-nowrap text-sm font-medium border px-2.5 py-0.5 rounded cursor-pointer flex items-center;
}
.badge-on {
@apply bg-blue-100 text-blue-800 hover:bg-blue-200;
}
.badge-off {
@apply bg-gray-100 text-gray-800 hover:bg-gray-200;
}
</style>
@@ -9,7 +9,7 @@
const { select, selectedId, schedule } = getContext<FlowEditorContext>('FlowEditorContext')
$: settingsClass = classNames(
'border w-full rounded-md p-2 bg-white text-sm cursor-pointer flex items-center',
'border w-full rounded-md 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>
@@ -7,10 +7,17 @@
import InsertModuleButton from './InsertModuleButton.svelte'
import FlowBranchOneMap from './FlowBranchOneMap.svelte'
import FlowBranchAllMap from './FlowBranchAllMap.svelte'
import {
faBuilding,
faCode,
faCodeBranch,
faLongArrowDown,
faQuestion,
faRepeat
} from '@fortawesome/free-solid-svg-icons'
import Icon from 'svelte-awesome'
export let mod: FlowModule
export let index: number
export let color: 'blue' | 'orange' | 'indigo' = 'blue'
const { select, selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
const dispatch = createEventDispatcher<{ delete: CustomEvent<MouseEvent>; insert: void }>()
@@ -40,14 +47,14 @@
{...itemProps}
>
<div slot="icon">
<span>{index + 1}</span>
<Icon data={faRepeat} scale={0.8} />
</div>
</FlowModuleSchemaItem>
<div class="flex flex-row w-full">
<div class="w-7 shrink-0 line" />
<div class="grow my-1 overflow-auto">
<div class="w-full">
<FlowModuleSchemaMap bind:modules={mod.value.modules} color="orange" />
<FlowModuleSchemaMap bind:modules={mod.value.modules} />
</div>
</div>
</div>
@@ -63,7 +70,7 @@
label={mod.summary || 'Run one branch'}
>
<div slot="icon">
<span>{index + 1}</span>
<Icon data={faCodeBranch} scale={0.8} />
</div>
</FlowModuleSchemaItem>
<FlowBranchOneMap bind:module={mod} />
@@ -79,7 +86,7 @@
label={mod.summary || 'Run all branches'}
>
<div slot="icon">
<span>{index + 1}</span>
<Icon data={faCodeBranch} scale={0.8} />
</div>
</FlowModuleSchemaItem>
<FlowBranchAllMap bind:module={mod} />
@@ -89,7 +96,6 @@
<FlowModuleSchemaItem
on:click={() => select(mod.id)}
on:delete={onDelete}
{color}
deletable
id={mod.id}
{...itemProps}
@@ -98,7 +104,13 @@
(mod.value.type === 'rawscript' ? `Inline ${mod.value.language}` : 'To be defined')}
>
<div slot="icon">
<span>{index + 1}</span>
{#if mod.value.type === 'rawscript'}
<Icon data={faCode} scale={0.8} />
{:else if mod.value.type === 'identity'}
<Icon data={faLongArrowDown} scale={0.8} />
{:else if mod.value.type === 'script'}
<Icon data={faBuilding} scale={0.8} />
{/if}
</div>
</FlowModuleSchemaItem>
</li>
@@ -107,7 +119,7 @@
<style>
.line {
background: repeating-linear-gradient(to bottom, transparent 0 4px, #bbb 4px 8px) 50%/1px 100%
no-repeat;
background: repeating-linear-gradient(to bottom, transparent 0 4px, rgb(120, 120, 120) 4px 8px)
50%/1px 100% no-repeat;
}
</style>
@@ -137,28 +137,28 @@ export function buildExtraLib(flowInput: Record<string, any>, results: Record<st
* get variable (including secret) at path
* @param {string} path - path of the variable (e.g: g/all/pretty_secret)
*/
export function variable(path: string): string;
declare function variable(path: string): string;
/**
* get resource at path
* @param {string} path - path of the resource (e.g: g/all/my_resource)
*/
export function resource(path: string): any;
declare function resource(path: string): any;
/**
* flow input as an object
*/
export const flow_input = ${JSON.stringify(flowInput)};
declare const flow_input = ${JSON.stringify(flowInput)};
/**
* static params of this same step
*/
export const params: any;
declare const params: any;
/**
* result by id
*/
export const results = ${JSON.stringify(results)};
declare const results = ${JSON.stringify(results)};
`
}
+3 -9
View File
@@ -122,15 +122,11 @@ export function isCodeInjection(expr: string | undefined): boolean {
}
export function getDefaultExpr(
importPath: string | undefined = undefined,
key: string = 'myfield',
previousExpr?: string
previousModuleId: string | undefined,
previousExpr?: string,
) {
const expr = previousExpr ?? `results.${key}`
return `import { results, flow_input, variable, resource, params } from 'windmill${importPath ? `@${importPath}` : ''
}'
${expr}`
return previousExpr ?? (previousModuleId ? `results.${previousModuleId}.${key}` : `flow_input.${key}`)
}
export function jobsToResults(jobs: Job[]) {
@@ -160,8 +156,6 @@ export function codeToStaticTemplate(code?: string): string | undefined {
const lines = code
.split('\n')
.slice(1)
.filter((x) => x != '')
if (lines.length == 1) {
const line = lines[0].trim()
@@ -1,7 +1,7 @@
<script lang="ts">
import Svelvet, { type Edge } from 'svelvet'
import { sugiyama, dagStratify, decrossOpt, coordGreedy, coordCenter } from 'd3-dag'
import type { FlowModule, RawScript } from '../../gen'
import type { FlowModule, ForloopFlow, RawScript } from '../../gen'
import {
NODE,
createIdGenerator,
@@ -15,7 +15,7 @@
type NestedNodes,
type ModuleHost
} from '.'
import { truncate, truncateRev } from '$lib/utils'
import { defaultIfEmptyString, truncate, truncateRev } from '$lib/utils'
import { createEventDispatcher } from 'svelte'
import { numberToChars } from '../flows/utils'
@@ -67,7 +67,7 @@
function getConvertedFlowModule(
module: FlowModule,
parent: NestedNodes | undefined = undefined,
parent: NestedNodes | string | undefined = undefined,
edgeLabel: string | undefined = undefined
): GraphItem | undefined {
const type = module.value.type
@@ -96,22 +96,18 @@
edgeLabel
)
} else if (type === 'forloopflow') {
const expr = module.value.iterator['expr']
return flowModuleToLoop(
module.value.modules,
`For each item in: ${truncate(expr, 10)}`,
parent
)
return flowModuleToLoop(module.value.modules, module, parent)
} else if (type === 'branchone') {
const branches = [module.value.default, ...module.value.branches.map((b) => b.modules)]
return flowModuleToBranch(
module,
branches,
['Default', ...module.value.branches.map((x) => `If ${truncateRev(x.expr, 20)}`)],
parent
)
} else if (type === 'branchall') {
const branches = module.value.branches.map((b) => b.modules)
return flowModuleToBranch(branches, [], parent)
return flowModuleToBranch(module, branches, [], parent)
}
return flowModuleToNode(
parentIds,
@@ -124,7 +120,10 @@
)
}
function getParentIds(items: NestedNodes | undefined = undefined): string[] {
function getParentIds(items: string | NestedNodes | undefined = undefined): string[] {
if (typeof items == 'string') {
return [items]
}
const item = items?.at(-1) || nestedNodes.at(-1)
if (!item) return []
@@ -197,37 +196,72 @@
function flowModuleToLoop(
modules: FlowModule[],
startLabel: string,
parent: NestedNodes | undefined = undefined
module: FlowModule,
parent: NestedNodes | string | undefined = undefined
): Loop {
const value = module.value as ForloopFlow
const expr = value.iterator.type == 'static' ? value.iterator.value : value.iterator.expr
const loop: Loop = {
type: 'loop',
items: [createVirtualNode(getParentIds(parent), startLabel)]
items: [
flowModuleToNode(
getParentIds(parent),
module.id,
module.summary || `For Loop: ${defaultIfEmptyString(expr ?? '', 'TBD')}`,
'inline',
module,
undefined
)
]
}
modules.forEach((module) => {
const item = getConvertedFlowModule(module, loop.items)
item && loop.items.push(item)
})
loop.items.push(createVirtualNode(getParentIds(loop.items), "Collect iterations' results"))
loop.items.push(
createVirtualNode(
getParentIds(loop.items),
`Collect iterations' results of For Loop ${module.id}`
)
)
return loop
}
function flowModuleToBranch(
module: FlowModule,
branches: FlowModule[][],
edgesLabel: string[],
parent: NestedNodes | undefined = undefined
parent: string | NestedNodes | undefined = undefined
): Branch {
const branch: Branch = {
type: 'branch',
node: flowModuleToNode(
getParentIds(parent),
module.id,
module.summary || module.value.type == 'branchall'
? 'Run all branches'
: 'Run one branch given predicate',
'inline',
module,
undefined
),
items: []
}
const branchParent = [branch.node.id.toString()]
if (branches.length == 0) {
branch.items.push([createVirtualNode(branchParent, 'No branches')])
}
branches.forEach((modules, i) => {
const items: NestedNodes = []
if (!modules.length) {
items.push(createVirtualNode(getParentIds(parent), 'Empty branch', edgesLabel[i]))
items.push(createVirtualNode(branchParent, 'Empty branch', edgesLabel[i]))
} else {
modules.forEach((module) => {
const item = getConvertedFlowModule(module, items.length ? items : parent, edgesLabel[i])
const item = getConvertedFlowModule(
module,
items.length ? items : branch.node.id.toString(),
edgesLabel[i]
)
item && items.push(item)
})
}
@@ -244,6 +278,7 @@
} else if (isLoop(node)) {
flattenNestedNodes(node.items, array)
} else if (isBranch(node)) {
array.push(node.node)
node.items.forEach((item) => {
flattenNestedNodes(item, array)
})
@@ -10,6 +10,7 @@ export type Loop = {
}
export type Branch = {
node: Node,
type: 'branch',
items: NestedNodes[]
}
@@ -2,6 +2,7 @@
import { truncate } from '$lib/utils'
import { createEventDispatcher } from 'svelte'
import { Badge } from '../common'
import { NEVER_TESTED_THIS_FAR } from '../flows/utils'
import { getTypeAsString } from '../flows/utils'
import { computeKey } from './utils'
@@ -15,6 +16,7 @@
export let collapsed = level == 3 || Array.isArray(json)
export let rawKey = false
export let topBrackets = false
export let topLevelNode = false
const collapsedSymbol = '...'
let keys: string | any[]
@@ -43,17 +45,29 @@
{#if keys.length > 0}
<span class:hidden={collapsed}>
{#if level != 0}
<span class="cursor-pointer hover:bg-gray-200 px-1 rounded" on:click={collapse}> (-) </span>
<span
class="cursor-pointer border border-gray-300 hover:bg-gray-200 px-1 rounded"
on:click={collapse}
>
-
</span>
{/if}
{#if level == 0 && topBrackets}<span class="h-0">{openBracket}</span>{/if}
<ul class="w-full">
{#each keys as key, index}
<li class="pt-1">
<button
on:click={() => selectProp(key)}
class="key {pureViewer ? 'cursor-auto' : ''} font-normal rounded px-1 hover:bg-blue-100"
>
{!isArray ? key : index}:
<button on:click={() => selectProp(key)}>
{#if topLevelNode}
<Badge baseClass="border border-blue-600" color="indigo">{key}</Badge>
{:else}
<span
class="key {pureViewer
? 'cursor-auto'
: 'border border-gray-300'} font-normal rounded px-1 hover:bg-blue-100"
>
{!isArray ? key : index}</span
>
{/if}:
</button>
{#if getTypeAsString(json[key]) === 'object'}
@@ -69,7 +83,7 @@
<button
class="val {pureViewer
? 'cursor-auto'
: ''} rounded px-1 hover:bg-blue-100 {getTypeAsString(json[key])}"
: ''} rounded hover:bg-blue-100 {getTypeAsString(json[key])}"
on:click={() => selectProp(key)}
>
{#if json[key] === NEVER_TESTED_THIS_FAR}
@@ -86,7 +100,11 @@
</ul>
{#if level == 0 && topBrackets}<span class="h-0">{closeBracket}</span>{/if}
</span>
<span class="cursor-pointer hover:bg-gray-200" class:hidden={!collapsed} on:click={collapse}>
<span
class="border border-blue-600 rounded px-1 cursor-pointer hover:bg-gray-200"
class:hidden={!collapsed}
on:click={collapse}
>
{openBracket}{collapsedSymbol}{closeBracket}
</span>
{#if !isLast && collapsed}
@@ -69,7 +69,7 @@
{`Mode: ${$propPickerConfig?.insertionMode}`}
</Badge>
{:else}
<Badge>&leftarrow; Select a step input</Badge>
<Badge color="blue">&leftarrow; Select a step input</Badge>
{/if}
</div>
{/if}
@@ -107,6 +107,7 @@
<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)
@@ -121,6 +122,7 @@
<span class="font-bold text-sm">All Results</span>
<div class="overflow-y-auto mb-2">
<ObjectViewer
topLevelNode
pureViewer={!$propPickerConfig}
collapsed={true}
json={resultByIdFiltered}
@@ -139,9 +141,10 @@
<Button
color="light"
size="xs"
variant="border"
on:click={() => {
displayVariable = false
}}>(-)</Button
}}>-</Button
>
<ObjectViewer
pureViewer={!$propPickerConfig}
@@ -151,7 +154,7 @@
/>
{:else}
<button
class="key font-normal rounded px-1 hover:bg-blue-100 !p-0"
class="border border-blue-600 key font-normal rounded hover:bg-blue-100 px-1"
on:click={async () => {
await loadVariables()
displayVariable = true
@@ -164,10 +167,11 @@
{#if displayResources}
<Button
color="light"
variant="border"
size="xs"
on:click={() => {
displayResources = false
}}>(-)</Button
}}>-</Button
>
<ObjectViewer
pureViewer={!$propPickerConfig}
@@ -177,7 +181,7 @@
/>
{:else}
<button
class="key font-normal rounded px-1 hover:bg-blue-100 !p-0"
class="border border-blue-600 px-1 key font-normal rounded hover:bg-blue-100"
on:click={async () => {
await loadResources()
displayResources = true