feat(frontend): add an eval input component for flow (#1494)

* fix sqlx

* all
This commit is contained in:
Ruben Fiszel
2023-04-28 23:09:28 +02:00
committed by GitHub
parent ed61b5a0f0
commit a0cec91f90
12 changed files with 292 additions and 81 deletions
+10 -3
View File
@@ -57,6 +57,7 @@
export let itemPicker: ItemPicker | undefined = undefined
export let noMargin = false
export let extra: Record<string, any> = {}
export let minW = true
let seeEditable: boolean = enum_ != undefined || pattern != undefined
const dispatch = createEventDispatcher()
@@ -141,6 +142,12 @@
}
}
function onKeyDown(e: KeyboardEvent) {
if ((e.ctrlKey || e.metaKey) && e.key == 'Enter') {
return
}
e.stopPropagation()
}
$: {
if (value == undefined || value == null) {
value = defaultValue
@@ -165,7 +172,7 @@
</script>
<!-- svelte-ignore a11y-autofocus -->
<div class="flex flex-col w-full min-w-[250px]">
<div class="flex flex-col w-full {minW ? 'min-w-[250px]' : ''}">
<div>
{#if displayHeader}
<FieldHeader {label} {required} {type} {contentEncoding} {format} />
@@ -192,7 +199,7 @@
use:autosize
rows="1"
bind:value={description}
on:keydown|stopPropagation
on:keydown={onKeyDown}
placeholder="Field description"
/>
{#if type == 'string' && format != 'date-time'}
@@ -408,7 +415,7 @@
dispatch('focus')
}}
use:autosize
on:keydown|stopPropagation
on:keydown={onKeyDown}
type="text"
{disabled}
class="col-span-10 {valid
@@ -203,7 +203,8 @@
</div>
{:else if !forceJson && resultKind == 'error'}<div>
<span class="text-red-500 font-semibold text-sm whitespace-pre-wrap"
>{result.error.name}: {result.error.message}</span
>{#if result.error.name || result.error.message}{result.error.name}: {result.error
.message}{:else}{JSON.stringify(result.error, null, 4)}{/if}</span
>
<pre class="text-sm whitespace-pre-wrap text-gray-900">{result.error.stack ?? ''}</pre>
</div>
@@ -261,7 +262,15 @@
<div class="flex gap-2 items-center">Copy to clipboard <ClipboardCopy /> </div>
</Button>
</svelte:fragment>
<Highlight language={json} code={JSON.stringify(result, null, 4).replace(/\\n/g, '\n')} />
{@const str = JSON.stringify(result, null, 4).replace(/\\n/g, '\n')}
{#if str.length > 100000}
JSON too large. <a
download="{filename ?? 'result'}.json"
href="data:text/json;charset=utf-8,{encodeURIComponent(str)}">Download</a
>
{:else}
<Highlight language={json} code={JSON.stringify(result, null, 4).replace(/\\n/g, '\n')} />
{/if}
</DrawerContent>
</Drawer>
</Portal>
@@ -1,6 +1,6 @@
<script lang="ts">
import type { Schema } from '$lib/common'
import { ScriptService, type FlowModule, type InputTransform, type Job } from '$lib/gen'
import { ScriptService, type FlowModule, type Job } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { getModifierKey, getScriptByPath } from '$lib/utils'
import { Loader2 } from 'lucide-svelte'
@@ -10,11 +10,15 @@
import DisplayResult from './DisplayResult.svelte'
import type { FlowEditorContext } from './flows/types'
import LogViewer from './LogViewer.svelte'
import RunForm from './RunForm.svelte'
import TestJobLoader from './TestJobLoader.svelte'
import ModulePreviewForm from './ModulePreviewForm.svelte'
import { Kbd } from './common'
import { evalValue } from './flows/utils'
import type { PickableProperties } from './flows/previousResults'
export let mod: FlowModule
export let schema: Schema
export let pickableProperties: PickableProperties | undefined
const { flowStore, flowStateStore, testStepStore } =
getContext<FlowEditorContext>('FlowEditorContext')
@@ -24,16 +28,12 @@
let testIsLoading = false
let testJob: Job | undefined = undefined
let stepArgs: Record<string, any> | undefined =
$testStepStore[mod.id] ??
Object.entries(mod.value['input_transforms'] ?? {}).reduce((acc, [k, v]) => {
let t = v as InputTransform
if (t.type == 'static') {
acc[k] = t.value
return acc
}
return acc
}, {})
let stepArgs: Record<string, any> | undefined = Object.fromEntries(
Object.keys(schema.properties).map((k) => [
k,
evalValue(k, mod, $testStepStore, pickableProperties, false)
])
)
$: $testStepStore[mod.id] = stepArgs
@@ -58,7 +58,7 @@
function jobDone() {
if (testJob && !testJob.canceled && testJob.type == 'CompletedJob' && `result` in testJob) {
if ($flowStateStore[mod.id]?.previewResult) {
if ($flowStateStore[mod.id]) {
$flowStateStore[mod.id].previewResult = testJob.result
$flowStateStore = $flowStateStore
}
@@ -80,24 +80,18 @@
>
{/if}
<RunForm
noVariablePicker
loading={testIsLoading}
runnable={{ summary: mod.summary ?? '', schema, description: '' }}
runAction={(_, args) => runTest(args)}
schedulable={false}
buttonText={`Test (${getModifierKey()}+Enter)`}
detailed={false}
topButton
bind:args={stepArgs}
isFlow={false}
/>
{#if testIsLoading}
<Button on:click={testJobLoader?.cancelJob} btnClasses="w-full mt-4" color="red" size="sm">
<Loader2 class="animate-spin mr-1" />
<Button on:click={testJobLoader?.cancelJob} btnClasses="w-full" color="red" size="sm">
<Loader2 size={16} class="animate-spin mr-1" />
Cancel
</Button>
{:else}
<Button btnClasses="w-full truncate" size="sm" on:click={() => runTest(stepArgs)}
>Run&nbsp;<Kbd>{getModifierKey()}</Kbd>+<Kbd>Enter</Kbd></Button
>
{/if}
<ModulePreviewForm {pickableProperties} {mod} {schema} bind:args={stepArgs} />
</Pane>
<Pane size={50} minSize={20}>
<Splitpanes horizontal>
@@ -0,0 +1,93 @@
<script lang="ts">
import type { Schema } from '$lib/common'
import { allTrue } from '$lib/utils'
import { Plug } from 'lucide-svelte'
import ArgInput from './ArgInput.svelte'
import { Button } from './common'
import { getContext } from 'svelte'
import type { FlowEditorContext } from './flows/types'
import { evalValue } from './flows/utils'
import type { FlowModule } from '$lib/gen'
import type { PickableProperties } from './flows/previousResults'
export let schema: Schema
export let args: Record<string, any> = {}
export let mod: FlowModule
export let pickableProperties: PickableProperties | undefined
export let isValid: boolean = true
export let autofocus = false
const { testStepStore } = getContext<FlowEditorContext>('FlowEditorContext')
let inputCheck: { [id: string]: boolean } = {}
$: isValid = allTrue(inputCheck) ?? false
$: if (args == undefined || typeof args !== 'object') {
args = {}
}
function removeExtraKey() {
const nargs = {}
Object.keys(args ?? {}).forEach((key) => {
if (keys.includes(key)) {
nargs[key] = args[key]
}
})
args = nargs
}
let keys: string[] = []
$: {
let lkeys = Object.keys(schema?.properties ?? {})
if (schema?.properties && JSON.stringify(lkeys) != JSON.stringify(keys)) {
keys = lkeys
removeExtraKey()
}
}
function plugIt(argName: string) {
args[argName] = evalValue(argName, mod, testStepStore, pickableProperties, true)
}
</script>
<div class="w-full pt-4">
{#if keys.length > 0}
{#each keys as argName, i (argName)}
{#if Object.keys(schema.properties ?? {}).includes(argName)}
<div class="flex gap-2 items-center">
{#if typeof args == 'object' && schema?.properties[argName]}
<ArgInput
minW={false}
autofocus={i == 0 && autofocus}
label={argName}
description={schema.properties[argName].description}
bind:value={args[argName]}
type={schema.properties[argName].type}
required={schema.required.includes(argName)}
pattern={schema.properties[argName].pattern}
bind:valid={inputCheck[argName]}
defaultValue={schema.properties[argName].default}
enum_={schema.properties[argName].enum}
format={schema.properties[argName].format}
contentEncoding={schema.properties[argName].contentEncoding}
properties={schema.properties[argName].properties}
itemsType={schema.properties[argName].items}
extra={schema.properties[argName]}
/>
{/if}
<div>
<Button
on:click={() => plugIt(argName)}
size="sm"
variant="border"
color="light"
title="Eval input component"><Plug size={14} /></Button
>
</div>
</div>
{/if}
{/each}
{/if}
</div>
@@ -2,10 +2,12 @@
import { classNames } from '$lib/utils'
import { createEventDispatcher } from 'svelte'
import { twMerge } from 'tailwind-merge'
import Tooltip from './Tooltip.svelte'
export let options: {
left?: string
right?: string
rightTooltip?: string
} = {}
export let checked: boolean = false
export let disabled = false
@@ -74,6 +76,9 @@
style={textStyle}
>
{options?.right}
{#if options?.rightTooltip}
<Tooltip>{options?.rightTooltip}</Tooltip>
{/if}
</span>
{/if}
</label>
@@ -63,6 +63,9 @@
if (onDeleteComponentControl) {
onDeleteComponentControl()
}
if (onDelete) {
onDelete()
}
if (componentSettings?.item.id) {
delete $worldStore.outputsById[componentSettings?.item.id]
@@ -5,6 +5,7 @@
export let error = ''
export let editor: SimpleEditor | undefined = undefined
$: tooBig = code && code?.length > 1000000
function parseJson() {
try {
value = JSON.parse(code ?? '')
@@ -16,11 +17,15 @@
$: code && parseJson()
</script>
<div class="flex flex-col w-full">
<div class="border border-gray-300 w-full">
<SimpleEditor on:focus bind:this={editor} on:change autoHeight lang="json" bind:code />
{#if tooBig}
<span class="text-gray-600">JSON to edit is too big</span>
{:else}
<div class="flex flex-col w-full">
<div class="border border-gray-300 w-full">
<SimpleEditor on:focus bind:this={editor} on:change autoHeight lang="json" bind:code />
</div>
{#if error != ''}
<span class="text-red-600 text-xs">{error}</span>
{/if}
</div>
{#if error != ''}
<span class="text-red-600 text-xs">{error}</span>
{/if}
</div>
{/if}
@@ -242,6 +242,7 @@
</div>
{:else if selected === 'test'}
<ModulePreview
pickableProperties={stepPropPicker.pickableProperties}
bind:this={modulePreview}
mod={flowModule}
schema={$flowStateStore[$selectedId]?.schema ?? {}}
@@ -18,12 +18,14 @@
import { copyToClipboard } from '$lib/utils'
import { Icon } from 'svelte-awesome'
import { faClipboard } from '@fortawesome/free-solid-svg-icons'
import Tooltip from '$lib/components/Tooltip.svelte'
const { selectedId, flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
export let initialPath: string
$: url = `${$page.url.hostname}/api/w/${$workspaceStore}/jobs/run/f/${$flowStore?.path}`
$: syncedUrl = `${$page.url.hostname}/api/w/${$workspaceStore}/jobs/run_wait_result/f/${$flowStore?.path}`
</script>
<div class="h-full overflow-hidden">
@@ -69,52 +71,87 @@
On-demand:
<ul class="pt-4">
<li>
1. <a href="https://docs.windmill.dev/docs/core_concepts/auto_generated_uis" target="_blank">Auto-generated UIs</a>
</li>
1. <a
href="https://docs.windmill.dev/docs/core_concepts/auto_generated_uis"
target="_blank">Auto-generated UIs</a
>
</li>
<li>
3. <a href="/apps/add?nodraft=true" target="_blank"> App Editor</a> for customized-UIs
2. <a href="/apps/add?nodraft=true" target="_blank"> App Editor</a> for customized-UIs
</li>
<li>
3. <a href="/schedules" target="_blank">Scheduling</a>
</li>
<li>
4. <a href="https://docs.windmill.dev/docs/advanced/cli" target="_blank">Windmill CLI</a>
4. <a href="https://docs.windmill.dev/docs/advanced/cli" target="_blank"
>Windmill CLI</a
>
</li>
<br>
<br />
<li class="mt-2">
<div class="flex flex-col gap-2">
<p>
From external events:
</p>
<p> From external events: </p>
</div>
</li>
<li class="mt-2">
5. Send a <a href="https://docs.windmill.dev/docs/core_concepts/webhooks" target="_blank">webhook</a> after each event: <a
on:click={(e) => {
e.preventDefault()
copyToClipboard(url)
}}
href={$page.url.protocol + '//' + url}
class="whitespace-nowrap text-ellipsis overflow-hidden mr-1"
5. Send a <a
href="https://docs.windmill.dev/docs/core_concepts/webhooks"
target="_blank">webhook</a
>
after each event:
<ul class="list-disc pl-4"
><li
>Async <Tooltip
>Return an uuid instantly that you can use to fetch status and result</Tooltip
>:
<a
on:click={(e) => {
e.preventDefault()
copyToClipboard(url)
}}
href={$page.url.protocol + '//' + url}
class="whitespace-nowrap text-ellipsis overflow-hidden mr-1"
>
{url}
<span class="text-gray-700 ml-2">
<Icon data={faClipboard} />
</span>
</a>
</li>
<li
>Sync <Tooltip>Wait for result within a timeout of 20s</Tooltip>:
<a
on:click={(e) => {
e.preventDefault()
copyToClipboard(syncedUrl)
}}
href={$page.url.protocol + '//' + syncedUrl}
class="whitespace-nowrap text-ellipsis overflow-hidden mr-1"
>
{syncedUrl}
<span class="text-gray-700 ml-2">
<Icon data={faClipboard} />
</span>
</a>
</li>
</ul></li
>
{url}
<span class="text-gray-700 ml-2">
<Icon data={faClipboard} />
</span>
</a></li>
<br>
<br />
<li>
6. Use a <a href="https://docs.windmill.dev/docs/flows/flow_trigger" target="_blank">trigger script</a> and schedule this flow to run as frequently as
needed and compare a state persisted in Windmill to the state of the
external system. If a difference is detected, then the rest of the flow is
triggered. Oftentimes, the second step of a flow is a for-loop that will
iterate over every elements. When using a trigger, a default schedule will
be created.
6. Use a <a
href="https://docs.windmill.dev/docs/flows/flow_trigger"
target="_blank">trigger script</a
>
and schedule this flow to run as frequently as needed and compare a state persisted
in Windmill to the state of the external system. If a difference is detected, then
the rest of the flow is triggered. Oftentimes, the second step of a flow is a for-loop
that will iterate over every elements. When using a trigger, a default schedule
will be created.
<img
class="shadow-lg border rounded"
alt="static button"
src="/trigger_button.png"
/>
class="shadow-lg border rounded"
alt="static button"
src="/trigger_button.png"
/>
</li></ul
>
</div>
@@ -122,7 +159,11 @@
</div>
</TabContent>
<TabContent value="settings-schedule" class="p-4">
<Alert type="info" title="Primary Schedule" documentationLink="https://docs.windmill.dev/docs/core_concepts/scheduling">
<Alert
type="info"
title="Primary Schedule"
documentationLink="https://docs.windmill.dev/docs/core_concepts/scheduling"
>
Flows can be triggered by any schedules, their webhooks or their UI but they only have
only one primary schedules with which they share the same path. The primary schedule
can be set here.
+52 -1
View File
@@ -10,9 +10,60 @@ import {
import { inferArgs } from '$lib/infer'
import { loadSchema, loadSchemaFlow } from '$lib/scripts'
import { workspaceStore } from '$lib/stores'
import { emptySchema } from '$lib/utils'
import { emptySchema, sendUserToast } from '$lib/utils'
import { get } from 'svelte/store'
import type { FlowModuleState } from './flowState'
import type { PickableProperties } from './previousResults'
function create_context_function_template(eval_string: string, context: Record<string, any>) {
return `
return function (context) {
"use strict";
${
Object.keys(context).length > 0
? `let ${Object.keys(context).map((key) => ` ${key} = context['${key}']`)};`
: ``
}
return ${eval_string}
}`
}
function make_context_evaluator(eval_string, context): (context) => any {
let template = create_context_function_template(eval_string, context)
let functor = Function(template)
return functor()
}
export function evalValue(
k: string,
mod: FlowModule,
testStepStore: Record<string, any>,
pickableProperties: PickableProperties | undefined,
showError: boolean
) {
let inputTransforms = (mod.value['input_transforms'] ?? {}) as Record<string, InputTransform>
let v = testStepStore[mod.id]?.[k]
let t = inputTransforms?.[k]
if (!v) {
if (t.type == 'static') {
v = t.value
} else {
try {
let context = {
flow_input: pickableProperties?.flow_input,
results: pickableProperties?.priorIds
}
v = make_context_evaluator(t.expr, context)(context)
} catch (e) {
if (showError) {
sendUserToast(`Error evaluating ${k}: ${e.message}`, true)
}
v = undefined
}
}
}
return v
}
export function cleanInputs(flow: Flow | any): Flow {
const newFlow: Flow = JSON.parse(JSON.stringify(flow))
@@ -136,7 +136,7 @@
{:else if topBrackets}
<span class="text-black">{openBracket}{closeBracket}</span>
{:else}
<span class="text-gray-400 text-xs ml-2">No items</span>
<span class="text-gray-400 text-xs ml-2">No items ([])</span>
{/if}
<style lang="postcss">
@@ -172,14 +172,16 @@
<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
>
<div class="flex">
<Button
color="light"
size="xs"
variant="border"
on:click={() => {
displayVariable = false
}}>-</Button
>
</div>
<ObjectViewer
allowCopy={false}
pureViewer={!$propPickerConfig}