feat(frontend): Flow UX entire rework (#552)

* feature(frontend): Flow rework WIP

* feature(frontend): Right panel behavior done

* feature(frontend): Split panel working

* feature(frontend): Flows working

* feature(frontend): Add inputs + adapt style + remove duplicate library

* feature(frontend): remove old implementation

* feature(frontend): revert package-lock

* feature(frontend): revert old FlowBuilder component

* feature(frontend): Fix margins + add remove button on the minimap

* feature(frontend): Fix wording

* feature(frontend): add PR UI comments

* feature(frontend): Display the module title

* feature(frontend): Previews working

* feature(frontend): Fix schedule load + update

* feature(frontend): fix build

* feature(frontend): UI fix

* fix just this step

* feature(frontend): for loop iterator and skip failures

* just this step

* script helpers

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
This commit is contained in:
Faton Ramadani
2022-09-14 12:34:51 +02:00
committed by GitHub
parent 5a2c45e6b2
commit c9ae5905a8
42 changed files with 1097 additions and 1003 deletions
+1 -1
View File
@@ -171,7 +171,7 @@
<ResourceEditor bind:this={resourceEditor} on:refresh={resourcePicker.openModal} />
<VariableEditor bind:this={variableEditor} on:create={variablePicker.openModal} />
<div class="flex divide-x items-center">
<div class="flex divide-x items-center overflow-x-auto">
<div>
<button
type="button"
+103 -108
View File
@@ -7,20 +7,19 @@
encodeState,
formatCron,
loadHubScripts,
pathIsEmpty,
sendUserToast,
setQueryWithoutLoad
} from '$lib/utils'
import { faPlay } from '@fortawesome/free-solid-svg-icons'
import { Breadcrumb, BreadcrumbItem, Button } from 'flowbite-svelte'
import { onDestroy, onMount } from 'svelte'
import Icon from 'svelte-awesome'
import { Breadcrumb, BreadcrumbItem } from 'flowbite-svelte'
import { onDestroy, onMount, setContext } from 'svelte'
import { writable } from 'svelte/store'
import CenteredPage from './CenteredPage.svelte'
import { OFFSET } from './CronInput.svelte'
import Drawer from './common/drawer/Drawer.svelte'
import FlowEditor from './FlowEditor.svelte'
import FlowPreviewContent from './FlowPreviewContent.svelte'
import FlowEditor from './flows/FlowEditor.svelte'
import { flowStateStore, flowStateToFlow, type FlowState } from './flows/flowState'
import { flowStore } from './flows/flowStore'
import { loadFlowSchedule, type Schedule } from './flows/scheduleUtils'
import type { FlowEditorContext } from './flows/types'
import { cleanInputs } from './flows/utils'
import ScriptSchema from './ScriptSchema.svelte'
@@ -28,32 +27,28 @@
export let initialPath: string = ''
let pathError = ''
let previewOpen = false
let scheduleArgs: Record<string, any> = {}
let previewArgs: Record<string, any> = {}
let scheduleEnabled: boolean = false
let scheduleCron: string = ''
$: step = Number($page.url.searchParams.get('step')) || 1
async function createSchedule(path: string) {
const { cron, args, enabled } = $scheduleStore
await ScheduleService.createSchedule({
workspace: $workspaceStore!,
requestBody: {
path: path,
schedule: formatCron(scheduleCron),
schedule: formatCron(cron),
offset: OFFSET,
script_path: path,
is_flow: true,
args: scheduleArgs,
enabled: scheduleEnabled
args,
enabled
}
})
}
async function saveFlow(): Promise<void> {
const flow = cleanInputs(flowStateToFlow($flowStateStore, $flowStore))
const { cron, args, enabled } = $scheduleStore
if (initialPath === '') {
await FlowService.createFlow({
@@ -66,7 +61,7 @@
schema: flow.schema
}
})
if (scheduleEnabled) {
if (enabled) {
await createSchedule(flow.path)
}
} else {
@@ -92,25 +87,25 @@
})
if (
schedule.path != flow.path ||
JSON.stringify(schedule.args) != JSON.stringify(scheduleArgs) ||
schedule.schedule != scheduleCron
JSON.stringify(schedule.args) != JSON.stringify(args) ||
schedule.schedule != cron
) {
await ScheduleService.updateSchedule({
workspace: $workspaceStore ?? '',
path: initialPath,
requestBody: {
schedule: formatCron(scheduleCron),
schedule: formatCron(cron),
script_path: flow.path,
is_flow: true,
args: scheduleArgs
args
}
})
}
if (scheduleEnabled != schedule.enabled) {
if (enabled != schedule.enabled) {
await ScheduleService.setScheduleEnabled({
workspace: $workspaceStore ?? '',
path: flow.path,
requestBody: { enabled: scheduleEnabled }
requestBody: { enabled }
})
}
} else {
@@ -142,6 +137,35 @@
}
})
const selectedIdStore = writable<string>('settings')
const scheduleStore = writable<Schedule>(undefined)
function select(selectedId: string) {
selectedIdStore.set(selectedId)
}
setContext<FlowEditorContext>('FlowEditorContext', {
selectedId: selectedIdStore,
schedule: scheduleStore,
select,
path: initialPath
})
$: {
loadFlowSchedule(initialPath, $workspaceStore)
.then((schedule: Schedule) => {
scheduleStore.set(schedule)
})
.catch(() => {
scheduleStore.set({
cron: '0 */5 * * *',
args: {},
enabled: false,
previewArgs: {}
})
})
}
onMount(() => {
loadHubScripts()
})
@@ -154,96 +178,67 @@
})
</script>
<div class="flex flex-row w-full h-full justify-between ">
<div class={`flex flex-col mb-96 m-auto w-full`}>
<!-- Nav between steps-->
<div class="justify-between flex flex-row w-full my-4">
<Breadcrumb>
<BreadcrumbItem>
<button on:click={() => changeStep(1)} class={step === 1 ? 'font-bold' : null}>
Flow Editor
</button>
</BreadcrumbItem>
<BreadcrumbItem>
<button on:click={() => changeStep(2)} class={step === 2 ? 'font-bold' : null}>
UI customisation
</button>
</BreadcrumbItem>
</Breadcrumb>
<div class="flex flex-row-reverse ml-2">
{#if step == 1}
<button
disabled={pathError != ''}
class="default-button px-6 max-h-8"
on:click={() => changeStep(2)}
>
Next
</button>
<button
disabled={pathError != ''}
class="default-button-secondary px-6 max-h-8 mr-2"
on:click={saveFlow}
>
Save
</button>
{:else}
<button class="default-button px-6 self-end" on:click={saveFlow}>Save</button>
<button
class="default-button-secondary px-6 max-h-8 mr-2"
on:click={async () => {
changeStep(1)
}}>Back</button
>
{/if}
</div>
</div>
<div class="flex flex-row-reverse">
<span class="my-1 text-sm text-gray-500 italic">
{#if initialPath && initialPath != $flowStore?.path} {initialPath} &rightarrow; {/if}
{$flowStore?.path}
</span>
</div>
<!-- metadata -->
{#if $flowStateStore}
{#if step === 1}
<FlowEditor
bind:pathError
bind:initialPath
bind:scheduleEnabled
bind:scheduleCron
bind:scheduleArgs
bind:previewArgs
/>
<Button
disabled={pathIsEmpty($flowStore.path)}
size="lg"
pill
on:click={() => (previewOpen = !previewOpen)}
class={`blue-button fixed bottom-10 right-10 ${previewOpen ? 'hidden' : ''}`}
<div class="flex flex-col flex-1 h-full">
<!-- Nav between steps-->
<div class="justify-between flex flex-row w-full my-2 px-4">
<Breadcrumb>
<BreadcrumbItem>
<button on:click={() => changeStep(1)} class={step === 1 ? 'font-bold' : null}>
Flow Editor
</button>
</BreadcrumbItem>
<BreadcrumbItem>
<button on:click={() => changeStep(2)} class={step === 2 ? 'font-bold' : null}>
UI customisation
</button>
</BreadcrumbItem>
</Breadcrumb>
<div class="flex flex-row-reverse ml-2">
{#if step == 1}
<button
disabled={pathError != ''}
class="default-button px-6 max-h-8"
on:click={() => changeStep(2)}
>
Preview flow
{pathIsEmpty($flowStore.path) ? '(pick a name first!)' : ''}
Next
</button>
<button
disabled={pathError != ''}
class="default-button-secondary px-6 max-h-8 mr-2"
on:click={saveFlow}
>
Save
</button>
{:else}
<button class="default-button px-6 self-end" on:click={saveFlow}>Save</button>
<button
class="default-button-secondary px-6 max-h-8 mr-2"
on:click={async () => {
changeStep(1)
}}
>
Back
</button>
{/if}
</div>
</div>
<Icon data={faPlay} class="ml-2" />
</Button>
{:else if step === 2}
<!-- metadata -->
{#if $flowStateStore}
{#if step === 1}
<FlowEditor />
{:else if step === 2}
<CenteredPage>
<ScriptSchema
synchronizedHeader={false}
bind:summary={$flowStore.summary}
bind:description={$flowStore.description}
bind:schema={$flowStore.schema}
/>
{/if}
{:else}
<p>Loading</p>
</CenteredPage>
{/if}
</div>
{:else}
<CenteredPage>Loading...</CenteredPage>
{/if}
</div>
{#if $flowStateStore && $flowStore}
<Drawer bind:open={previewOpen} size="800px">
<FlowPreviewContent bind:args={previewArgs} on:close={() => (previewOpen = !previewOpen)} />
</Drawer>
{/if}
@@ -1,65 +0,0 @@
<script lang="ts">
import { ScheduleService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import FlowSettings from './flows/FlowSettings.svelte'
import { flowStateStore } from './flows/flowState'
import { flowStore } from './flows/flowStore'
import FlowTimeline from './flows/FlowTimeline.svelte'
export let pathError = ''
export let initialPath: string = ''
export let scheduleArgs: Record<string, any> = {}
export let scheduleEnabled = false
export let scheduleCron: string = '0 */5 * * *'
export let previewArgs: Record<string, any> = {}
let scheduleLoaded = false
async function loadSchedule() {
if (!scheduleLoaded) {
scheduleLoaded = true
const existsSchedule = await ScheduleService.existsSchedule({
workspace: $workspaceStore ?? '',
path: initialPath
})
if (existsSchedule) {
const schedule = await ScheduleService.getSchedule({
workspace: $workspaceStore ?? '',
path: initialPath
})
scheduleEnabled = schedule.enabled!
scheduleCron = schedule.schedule
scheduleArgs = schedule.args ?? {}
previewArgs = JSON.parse(JSON.stringify(scheduleArgs))
}
}
}
$: if ($flowStore && $workspaceStore && initialPath != '') {
loadSchedule()
}
</script>
{#if $flowStateStore}
<div class="flex space-y-8 flex-col items-center">
<FlowTimeline bind:args={previewArgs} bind:flowModuleSchemas={$flowStateStore}>
<div slot="settings">
<FlowSettings
bind:pathError
bind:initialPath
bind:scheduleArgs
{previewArgs}
bind:scheduleCron
bind:scheduleEnabled
/>
</div>
</FlowTimeline>
</div>
{:else}
<h3>Loading flow</h3>
{/if}
+28 -77
View File
@@ -2,52 +2,41 @@
import type { Schema } from '$lib/common'
import type { Flow } from '$lib/gen'
import { sendUserToast, truncateRev } from '$lib/utils'
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'
import Icon from 'svelte-awesome'
import { flowStateStore, flowStateToFlow } from './flows/flowState'
import { mapJobResultsToFlowState } from './flows/flowStateUtils'
import { runFlowPreview } from './flows/utils'
import FlowStatusViewer from './FlowStatusViewer.svelte'
import RunForm from './RunForm.svelte'
import Tab from './common/tabs/Tab.svelte'
import TabContent from './common/tabs/TabContent.svelte'
import Tabs from './common/tabs/Tabs.svelte'
export let i: number
export let indexes: string
export let flow: Flow
export let schema: Schema
export let args: Record<string, any> = {}
const [i, j] = indexes.split('-').map(Number)
let stepArgs: Record<string, any> = {}
let tab: 'upto' | 'justthis' = 'upto'
let viewPreview = false
let uptoText =
i >= flow.value.modules.length - 1 ? 'Preview whole flow' : 'Preview up to this step'
let jobId: string
export async function runPreview(args: any) {
viewPreview = true
flow = flowStateToFlow($flowStateStore, flow)
let newFlow: Flow =
tab == 'upto' ? truncateFlow(flow) : setInputTransformFromArgs(extractStep(flow), args)
let newFlow: Flow = setInputTransformFromArgs(extractStep(flow), args)
jobId = await runFlowPreview(args, newFlow)
sendUserToast(`started preview ${truncateRev(jobId, 10)}`)
}
function truncateFlow(flow: Flow): Flow {
const localFlow = JSON.parse(JSON.stringify(flow))
localFlow.value.modules = flow.value.modules.slice(0, i + 1)
return localFlow
}
function extractStep(flow: Flow): Flow {
const localFlow = JSON.parse(JSON.stringify(flow))
localFlow.value.modules = flow.value.modules.slice(i, i + 1)
const mod = flow.value.modules[i].value
console.log(mod, j)
if (j != undefined && mod.type === 'forloopflow') {
localFlow.value.modules = mod.modules.slice(j, j + 1)
} else {
localFlow.value.modules = flow.value.modules.slice(i, i + 1)
}
console.log(localFlow)
localFlow.schema = schema
return localFlow
}
@@ -65,59 +54,21 @@
}
</script>
<button
class="w-full rounded border-1 border border-gray-200"
on:click={() => {
viewPreview = !viewPreview
}}
>
<h2 class="flex justify-center text-gray-600">
<div>
Preview
<Icon class="ml-1" data={viewPreview ? faChevronUp : faChevronDown} scale={1} />
</div>
</h2>
</button>
<RunForm
runnable={extractStep(flow)}
runAction={(_, args) => runPreview(args)}
schedulable={false}
buttonText="Test just this step"
detailed={false}
args={stepArgs}
/>
{#if viewPreview}
{#if i != flow.value.modules.length}
<div class="mt-2">
<Tabs bind:selected={tab}>
<Tab value="upto">{uptoText}</Tab>
<Tab value="justthis">Preview just this step</Tab>
<svelte:fragment slot="content">
<TabContent value="upto">
<RunForm
runnable={truncateFlow(flow)}
runAction={(_, args) => runPreview(args)}
schedulable={false}
buttonText={uptoText}
detailed={false}
bind:args
/>
</TabContent>
<TabContent value="justthis">
<RunForm
runnable={extractStep(flow)}
runAction={(_, args) => runPreview(args)}
schedulable={false}
buttonText="Preview just this step"
detailed={false}
args={stepArgs}
/>
</TabContent>
</svelte:fragment>
</Tabs>
</div>
{/if}
{#if jobId}
<div class="w-full flex justify-center">
<FlowStatusViewer
{jobId}
on:jobsLoaded={(e) => mapJobResultsToFlowState(e.detail, tab, i)}
root={true}
/>
</div>
{/if}
{#if jobId}
<div class="w-full flex justify-center">
<FlowStatusViewer
{jobId}
on:jobsLoaded={(e) => mapJobResultsToFlowState(e.detail, 'justthis', i, j)}
root={true}
/>
</div>
{/if}
@@ -4,27 +4,70 @@
import { faClose, faPlay } from '@fortawesome/free-solid-svg-icons'
import { Button } from 'flowbite-svelte'
import { createEventDispatcher, onDestroy } from 'svelte'
import { createEventDispatcher, getContext, onDestroy } from 'svelte'
import Icon from 'svelte-awesome'
import { flowStateStore, flowStateToFlow } from './flows/flowState'
import { flowStateStore, flowStateToFlow, type FlowModuleSchema } from './flows/flowState'
import { mapJobResultsToFlowState } from './flows/flowStateUtils'
import { flowStore } from './flows/flowStore'
import type { FlowEditorContext } from './flows/types'
import { runFlowPreview } from './flows/utils'
import FlowStatusViewer from './FlowStatusViewer.svelte'
import SchemaForm from './SchemaForm.svelte'
export let args: Record<string, any> = {}
export let previewMode: 'upTo' | 'whole'
let args: Record<string, any> = {}
let jobId: string | undefined = undefined
let isValid: boolean = false
let intervalState: 'idle' | 'canceled' | 'done' | 'running' = 'idle'
$: newFlow = flowStateToFlow($flowStateStore, $flowStore)
$: steps = newFlow?.value.modules.length ?? 0
const { selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
function extractFlow(previewMode: 'upTo' | 'whole') {
if (previewMode === 'whole') {
return flowStateToFlow($flowStateStore, $flowStore)
} else {
const [parentIndex, childIndex] = $selectedId.split('-')
if (childIndex === undefined) {
const modules = $flowStateStore.modules.slice(0, Number(parentIndex) + 1)
const flowState = {
modules: modules,
failureModule: $flowStateStore.failureModule
}
return flowStateToFlow(flowState, $flowStore)
} else {
const modules = $flowStateStore.modules.slice(0, Number(parentIndex) + 1)
const flowModuleSchemas: FlowModuleSchema[] = JSON.parse(JSON.stringify(modules))
const flowModuleSchema = flowModuleSchemas[modules.length - 1]
flowModuleSchemas[modules.length - 1] = {
...flowModuleSchemas[modules.length - 1],
childFlowModules: flowModuleSchema.childFlowModules!.slice(0, Number(childIndex) + 1)
}
if (flowModuleSchema.flowModule.value.type === 'forloopflow') {
flowModuleSchema.flowModule.value.modules =
flowModuleSchema.flowModule.value.modules.slice(0, Number(childIndex) + 1)
flowModuleSchemas[modules.length - 1].flowModule = flowModuleSchema.flowModule
}
const flowState = {
modules: flowModuleSchemas,
failureModule: $flowStateStore.failureModule
}
return flowStateToFlow(flowState, $flowStore)
}
}
}
const dispatch = createEventDispatcher()
export async function runPreview(args: Record<string, any>) {
const newFlow = extractFlow(previewMode)
jobId = await runFlowPreview(args, newFlow)
intervalState = 'running'
@@ -37,19 +80,32 @@
<div class="flex divide-y flex-col space-y-2 h-screen bg-white p-6 w-full">
<div class="flex justify-between">
<div class="flex flex-row justify-center items-center">
<div class="flex justify-center p-2 w-8 h-8 bg-blue-200 rounded-lg mr-2">
<div class="flex flex-row justify-center items-center ">
<div class="flex justify-center p-2 w-8 h-8 bg-blue-200 rounded-lg mr-2 ">
<Icon data={faPlay} scale={1} class="text-blue-500" />
</div>
<h3 class="text-lg leading-6 font-bold text-gray-900">Flow preview</h3>
<h3 class="text-lg leading-6 font-bold text-gray-900">
Test preview - {previewMode === 'upTo'
? `up to step ${$selectedId.split('-').join(',')}`
: ' whole flow'}
</h3>
</div>
<Button color="alternative" on:click={() => dispatch('close')}>
<Button
color="alternative"
on:click={() => {
jobId = undefined
intervalState = 'idle'
dispatch('close')
}}
>
<Icon data={faClose} />
</Button>
</div>
<div class="max-h-80 overflow-y-auto">
<SchemaForm schema={$flowStore.schema} bind:isValid bind:args />
<div class="pb-4">
<div class="mt-4">
<SchemaForm schema={$flowStore.schema} bind:isValid bind:args />
</div>
</div>
{#if intervalState === 'running'}
<Button
@@ -83,7 +139,10 @@
{jobId}
on:jobsLoaded={(e) => {
intervalState = 'done'
mapJobResultsToFlowState(e.detail, 'upto', steps - 1)
const [parentIndex] = $selectedId.split('-')
const configIndex =
previewMode === 'upTo' ? Number(parentIndex) : $flowStateStore.modules.length
mapJobResultsToFlowState(e.detail, 'upto', configIndex, undefined)
}}
root={true}
/>
@@ -7,7 +7,7 @@
import Icon from 'svelte-awesome'
import ArgInput from './ArgInput.svelte'
import FieldHeader from './FieldHeader.svelte'
import DynamicInputHelpBox from './flows/DynamicInputHelpBox.svelte'
import DynamicInputHelpBox from './flows/content/DynamicInputHelpBox.svelte'
import { codeToStaticTemplate, getDefaultExpr, isCodeInjection } from './flows/utils'
import OverlayPropertyPicker from './propertyPicker/OverlayPropertyPicker.svelte'
import SimpleEditor from './SimpleEditor.svelte'
@@ -1,154 +0,0 @@
<script lang="ts">
import type { FlowModule } from '$lib/gen'
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'
import Icon from 'svelte-awesome'
import Editor from './Editor.svelte'
import EditorBar from './EditorBar.svelte'
import FlowPreview from './FlowPreview.svelte'
import FlowBox from './flows/FlowBox.svelte'
import FlowInputs from './flows/FlowInputs.svelte'
import FlowModuleHeader from './flows/FlowModuleHeader.svelte'
import { flowStore } from './flows/flowStore'
import {
createInlineScriptModule,
createLoop,
fork,
loadFlowModuleSchema,
pickScript,
createScriptFromInlineScript,
isEmptyFlowModule,
getStepPropPicker
} from './flows/flowStateUtils'
import SchemaForm from './SchemaForm.svelte'
import type { Schema } from '$lib/common'
import { flowStateStore, type FlowModuleSchema } from './flows/flowState'
import { stepOpened } from './flows/stepOpenedStore'
import { scriptLangToEditorLang } from '$lib/utils'
export let indexes: number[]
export let mod: FlowModule
export let args: Record<string, any> = {}
export let schema: Schema
export let childFlowModules: FlowModuleSchema[] | undefined = undefined
let editor: Editor
let websocketAlive = { pyright: false, black: false, deno: false }
let bigEditor = false
const i = indexes[0]
$: shouldPick = isEmptyFlowModule(mod)
$: stepPropPicker = getStepPropPicker(indexes, $flowStore.schema, $flowStateStore, args)
async function apply<T>(fn: (arg: T) => Promise<FlowModuleSchema>, arg: T) {
const flowModuleSchema = await fn(arg)
mod = flowModuleSchema.flowModule
schema = flowModuleSchema.schema
if (flowModuleSchema.childFlowModules) {
childFlowModules = flowModuleSchema.childFlowModules
}
}
async function reload(flowModule: FlowModule) {
apply(loadFlowModuleSchema, flowModule)
}
async function applyCreateLoop() {
await apply(createLoop, null)
stepOpened.update(() => `${indexes[0]}-0`)
}
$: opened = $stepOpened === String(indexes.join('-'))
</script>
<FlowBox
headerClickable={true}
on:clickheader={() => ($stepOpened = !opened ? String(indexes.join('-')) : undefined)}
>
<svelte:fragment slot="header">
<FlowModuleHeader
bind:mod
bind:indexes
on:delete
on:fork={() => apply(fork, mod)}
on:createScriptFromInlineScript={() => {
apply(createScriptFromInlineScript, {
flowModule: mod,
suffix: indexes.join('-'),
schema
})
}}
/>
</svelte:fragment>
<div slot="content">
{#if opened}
<div class="p-6 border-t border-gray-300">
{#if shouldPick}
<FlowInputs
shouldDisableTriggerScripts={i != 0}
shouldDisableLoopCreation={indexes.length > 1 || i == 0}
on:pick={(e) => apply(pickScript, e.detail.path)}
on:new={(e) =>
apply(createInlineScriptModule, {
language: e.detail.language,
kind: e.detail.kind,
subkind: e.detail.subkind
})}
on:loop={() => applyCreateLoop()}
/>
{/if}
{#if mod.value.type === 'rawscript'}
<div class="mb-2 overflow-hidden">
<EditorBar {editor} {websocketAlive} lang={mod.value.language ?? 'deno'} />
</div>
<div on:mouseleave={() => reload(mod)}>
<Editor
bind:websocketAlive
bind:this={editor}
class="{bigEditor ? 'h-2/3' : 'h-80'} border p-2 rounded"
bind:code={mod.value.content}
lang={scriptLangToEditorLang(mod.value.language)}
automaticLayout={true}
formatAction={() => reload(mod)}
/>
<button
class="w-full text-center"
on:click={() => {
bigEditor = !bigEditor
}}
>
<Icon data={bigEditor ? faChevronUp : faChevronDown} scale={1.0} />
</button>
</div>
<div class="mt-2 mb-8">
<p class="text-gray-500 italic">
Move the focus outside of the text editor to recompute the input schema or press
Ctrl/Cmd+S
</p>
</div>
{/if}
{#if !shouldPick}
<p class="text-lg font-bold text-gray-900 mb-2">Step inputs</p>
<SchemaForm
{schema}
inputTransform={true}
importPath={String(indexes.join('-'))}
bind:pickableProperties={stepPropPicker.pickableProperties}
bind:args={mod.input_transforms}
bind:extraLib={stepPropPicker.extraLib}
/>
{/if}
{#if !shouldPick}
<div class="border-b border-gray-200" />
<div class="pt-2">
<FlowPreview bind:args flow={$flowStore} {i} {schema} />
</div>
{/if}
</div>
{/if}
</div>
</FlowBox>
+1 -3
View File
@@ -69,9 +69,7 @@
{#if !runnable.schema.properties || Object.keys(runnable.schema.properties).length === 0}
<div class="text-sm p-4">No arguments</div>
{:else}
<div class="bg-gray-50 border shadow-blue-100 shadow-inner rounded border-gray-300 p-6">
<SchemaForm schema={runnable.schema} bind:isValid bind:args />
</div>
<SchemaForm schema={runnable.schema} bind:isValid bind:args />
{/if}
{:else}
<div class="text-sm">No schema</div>
@@ -1,19 +0,0 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import FlowBoxHeader from './FlowBoxHeader.svelte'
export let title: string | undefined = undefined
export let headerClickable = false
const dispatch = createEventDispatcher()
let slots = $$props.$$slots
</script>
<div class="bg-white border border-gray-300 rounded-md shadow-md">
<FlowBoxHeader clickable={headerClickable} on:click={() => dispatch('clickheader')} {title}>
<slot name="header" />
</FlowBoxHeader>
{#if slots.content}
<slot name="content" />
{/if}
</div>
@@ -1,18 +0,0 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
export let title: string | undefined = undefined
export let clickable: boolean = false
const dispatch = createEventDispatcher()
</script>
<div
on:click={() => dispatch('click')}
class="flex items-center justify-between flex-wrap px-6 py-2"
class:cursor-pointer={clickable}
>
{#if title}
<h3 class="text-sm font-bold text-gray-900">{title}</h3>
{/if}
<slot />
</div>
@@ -0,0 +1,25 @@
<!-- Flow Editor: Top level component -->
<script lang="ts">
import { HSplitPane } from 'svelte-split-pane'
import FlowEditorHeader from './header/FlowEditorHeader.svelte'
import FlowEditorPanel from './content/FlowEditorPanel.svelte'
import { flowStateStore } from './flowState'
import FlowModuleSchemaMap from './map/FlowModuleSchemaMap.svelte'
</script>
<FlowEditorHeader />
<div class="h-full overflow-hidden">
<HSplitPane leftPaneSize="25%" rightPaneSize="75%" minLeftPaneSize="20%" minRightPaneSize="20%">
<left slot="left" class="h-full ">
<div class="h-full overflow-auto p-4 bg-gray-50">
<FlowModuleSchemaMap bind:flowModuleSchemas={$flowStateStore.modules} />
</div>
</left>
<right slot="right" class="h-full">
<div class="h-full overflow-auto bg-white ">
<FlowEditorPanel />
</div>
</right>
</HSplitPane>
</div>
@@ -1,116 +0,0 @@
<script lang="ts">
import type { FlowModule } from '$lib/gen'
import { getScriptByPath } from '$lib/utils'
import {
faArrowDown,
faClose,
faCode,
faCodeBranch,
faSave,
faTrashAlt
} from '@fortawesome/free-solid-svg-icons'
import { Button } from 'flowbite-svelte'
import { createEventDispatcher } from 'svelte'
import Icon from 'svelte-awesome'
import HighlightCode from '../HighlightCode.svelte'
import IconedPath from '../IconedPath.svelte'
import Modal from '../Modal.svelte'
import { isEmptyFlowModule } from './flowStateUtils'
import { stepOpened } from './stepOpenedStore'
export let indexes: number[]
export let mod: FlowModule
$: shouldPick = isEmptyFlowModule(mod)
let modalViewer: Modal
let modalViewerContent = ''
let modalViewerLanguage: 'deno' | 'python3' | 'go' = 'deno'
async function viewCode() {
if (mod.value.type == 'script') {
const { content, language } = await getScriptByPath(mod.value.path!)
modalViewerContent = content
modalViewerLanguage = language
modalViewer.openModal()
} else {
throw Error('Not a script')
}
}
const dispatch = createEventDispatcher()
$: opened = $stepOpened === String(indexes.join('-'))
</script>
<div>
<h3 class="text-sm font-bold text-gray-900 flex flex-col">
<span class="text-xs shrink">
{#if 'path' in mod.value && mod.value.path}
<IconedPath path={mod.value.path} />
{:else if 'language' in mod.value && mod.value.language}
Inline {mod.value.language}
{:else}
Select a script
{/if}
</span>
{#if ('path' in mod.value && mod.value.path) || ('language' in mod.value && mod.value.language)}
<input
on:click|stopPropagation={() => undefined}
class="overflow-x-auto"
type="text"
bind:value={mod.summary}
placeholder="Summary"
/>
{/if}
</h3>
</div>
<div class="flex flex-row space-x-2" on:click|stopPropagation={() => undefined}>
{#if mod.value.type === 'script' && !shouldPick}
<Button size="xs" color="alternative" on:click={() => dispatch('fork')}>
<Icon data={faCodeBranch} class="mr-2" />
Fork
</Button>
<Button size="xs" color="alternative" on:click={viewCode}>
<Icon data={faCode} class="mr-2" />
View code
</Button>
{/if}
{#if mod.value.type === 'rawscript' && !shouldPick}
<Button size="xs" color="alternative" on:click={() => dispatch('createScriptFromInlineScript')}>
<Icon data={faSave} class="mr-2" />
Save to workspace
</Button>
{/if}
<Button
size="xs"
color="alternative"
on:click={() => {
dispatch('delete')
}}
>
<Icon data={faTrashAlt} class="mr-2" />
Remove step
</Button>
{#if opened}
<Button size="xs" color="dark" on:click={() => stepOpened.update(() => undefined)}>
<Icon data={faClose} />
</Button>
{:else}
<Button
size="xs"
color="light"
on:click={() => stepOpened.update(() => String(indexes.join('-')))}
>
<Icon data={faArrowDown} />
</Button>
{/if}
</div>
<Modal bind:this={modalViewer}>
<div slot="title">Script {'path' in mod?.value ? mod?.value.path : ''}</div>
<div slot="content">
<HighlightCode language={modalViewerLanguage} code={modalViewerContent} />
</div>
</Modal>
@@ -1,169 +0,0 @@
<script lang="ts">
import { sendUserToast } from '$lib/utils'
import { faFileExport, faFileImport, faGlobe } from '@fortawesome/free-solid-svg-icons'
import { Button } from 'flowbite-svelte'
import Icon from 'svelte-awesome'
import Menu from '../common/menu/Menu.svelte'
import MenuItem from '../common/menu/MenuItem.svelte'
import FlowViewer from '../FlowViewer.svelte'
import Modal from '../Modal.svelte'
import SimpleEditor from '../SimpleEditor.svelte'
import CollapseLink from './../CollapseLink.svelte'
import CronInput from './../CronInput.svelte'
import FlowBox from './../flows/FlowBox.svelte'
import { flowStore, initFlow } from './../flows/flowStore'
import Path from './../Path.svelte'
import Required from './../Required.svelte'
import SchemaForm from './../SchemaForm.svelte'
import Toggle from './../Toggle.svelte'
import Tooltip from './../Tooltip.svelte'
import { stepOpened } from './stepOpenedStore'
import { cleanInputs } from './utils'
export let pathError = ''
export let initialPath: string = ''
export let previewArgs: Record<string, any> = {}
export let scheduleArgs: Record<string, any> = {}
export let scheduleEnabled = false
export let scheduleCron: string = '0 */5 * * *'
let jsonSetter: Modal
let jsonViewer: Modal
let jsonValue: string = ''
</script>
<Modal bind:this={jsonSetter}>
<div slot="title">Import JSON</div>
<div slot="content" class="h-full">
<SimpleEditor bind:code={jsonValue} lang="json" class="h-full" />
</div>
<div slot="submission">
<button
class="default-button px-4 py-2 font-semibold"
on:click={() => {
Object.assign($flowStore, JSON.parse(jsonValue))
initFlow($flowStore)
stepOpened.update(() => undefined)
sendUserToast('OpenFlow imported from JSON')
jsonSetter.closeModal()
}}
>
Import
</button>
</div>
</Modal>
<Modal bind:this={jsonViewer}>
<div slot="title">See JSON</div>
<div slot="content" class="h-full">
<FlowViewer flow={cleanInputs($flowStore)} tab="json" />
</div>
</Modal>
<FlowBox title="Flow Settings">
<div slot="header">
<div class="flex flex-row-reverse">
<Menu placement="bottom-end">
<button slot="trigger" class="text-gray-900 bg-white dark:text-white dark:bg-gray-800">
...
</button>
<div class="divide-y divide-gray-200">
<MenuItem
on:click={() => {
jsonSetter.openModal()
}}
>
<Icon data={faFileImport} scale={0.6} class="inline mr-2" />
Import from a JSON OpenFlow
</MenuItem>
<MenuItem
on:click={() => {
jsonViewer.openModal()
}}
>
<Icon data={faFileExport} scale={0.6} class="inline mr-2" />
Export to a JSON OpenFlow
</MenuItem>
<MenuItem
on:click={() => {
const url = new URL('https://hub.windmill.dev/flows/add')
const openFlow = {
value: $flowStore.value,
summary: $flowStore.summary,
description: $flowStore.description,
schema: $flowStore.schema
}
url.searchParams.append('flow', btoa(JSON.stringify(openFlow)))
window.open(url, '_blank')?.focus()
}}
>
<Icon data={faGlobe} scale={0.6} class="inline mr-2" />
Publish to Hub
</MenuItem>
</div>
</Menu>
</div>
</div>
<div slot="content">
<div class="p-6 border-t border-gray-300">
<Path
bind:error={pathError}
bind:path={$flowStore.path}
{initialPath}
namePlaceholder="my_flow"
kind="flow"
>
<div slot="ownerToolkit">
Flow permissions depend on their path. Select the group <span class="font-mono">all</span>
to share your flow, and <span class="font-mono">user</span> to keep it private.
<a href="https://docs.windmill.dev/docs/reference/namespaces">docs</a>
</div>
</Path>
<label class="block mt-4">
<span class="text-gray-700">Summary <Required required={false} /></span>
<textarea
bind:value={$flowStore.summary}
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
placeholder="A very short summary of the flow displayed when the flow is listed"
rows="1"
/>
</label>
<CollapseLink text="set primary schedule" open={true}>
<Tooltip>
The primary schedule of a flow is simply a schedule that has the same name as a flow. It
can be set and enabled directly within the flow editor. "Watching for new changes" flows
are meant to be watching regularly for new items in an external systems. The primary
schedule purpose is there to set the periodicity at which you want this watcher to
operate.
</Tooltip>
<Toggle
bind:checked={scheduleEnabled}
options={{
left: 'disabled',
right: 'enabled'
}}
/>
<div class="p-2 my-2 rounded" class:bg-gray-300={!scheduleEnabled}>
{#if !scheduleEnabled}
<span class="font-black">No next scheduled run when disabled</span>
{/if}
<CronInput bind:schedule={scheduleCron} />
</div>
<div class="flex flex-row-reverse">
<Button
color="alternative"
size="sm"
on:click={() => (scheduleArgs = JSON.parse(JSON.stringify(previewArgs)))}
>
Copy from preview arguments
</Button>
</div>
<SchemaForm schema={$flowStore.schema} bind:args={scheduleArgs} />
</CollapseLink>
</div>
</div>
</FlowBox>
@@ -1,186 +0,0 @@
<script lang="ts">
import {
faInfoCircle,
faPen,
faPlus,
faSliders,
faTrashAlt
} from '@fortawesome/free-solid-svg-icons'
import { Button, Toggle, Tooltip } from 'flowbite-svelte'
import Icon from 'svelte-awesome'
import ModuleStep from '../ModuleStep.svelte'
import FlowInput from './FlowInput.svelte'
import type { FlowState } from './flowState'
import { emptyFlowModuleSchema } from './flowStateUtils'
import { stepOpened } from './stepOpenedStore'
export let args: Record<string, any> = {}
export let flowModuleSchemas: FlowState
export let parentIndex: number | undefined = undefined
const root = parentIndex === undefined
function insertAtIndex(index: number): void {
flowModuleSchemas.splice(index, 0, emptyFlowModuleSchema())
flowModuleSchemas = flowModuleSchemas
const indexes = getIndexes(parentIndex, index)
stepOpened.update(() => String(indexes.join('-')))
}
function removeAtIndex(index: number): void {
flowModuleSchemas.splice(index, 1)
flowModuleSchemas = flowModuleSchemas
}
function getIndexes(parentIndex: number | undefined, childIndex: number): number[] {
const indexes: number[] = []
if (parentIndex !== undefined) {
indexes.push(parentIndex)
}
indexes.push(childIndex)
return indexes
}
const color = root ? 'blue' : 'orange'
</script>
<div class="w-full">
<ol class="relative ml-4 border-l border-gray-200 dark:border-gray-700 space-y-12 border-dashed">
{#if root}
<li class="ml-8">
<span class="relative">
<span
class={`flex absolute -left-12 justify-center items-center w-8 h-8 bg-${color}-200 rounded-full ring-8 ring-white dark:ring-gray-900 dark:bg-${color}-900`}
>
<Icon
class={`text-${color}-600 dark:text-${color}-400 text-font-bold text-center`}
data={faSliders}
/>
</span>
</span>
<slot name="settings" />
</li>
<li class="ml-8 ">
<span class="relative">
<span
class={`flex absolute top-4 -left-12 justify-center items-center w-8 h-8 bg-${color}-200 rounded-full ring-8 ring-white dark:ring-gray-900 dark:bg-${color}-900`}
>
<Icon
class={`text-${color}-600 dark:text-${color}-400 font-bold text-center`}
data={faPen}
/>
</span>
</span>
<FlowInput />
</li>
{/if}
{#each flowModuleSchemas as flowModuleSchema, index (index)}
{#if flowModuleSchema.flowModule.value.type === 'forloopflow'}
<li id="module-{index}" class="ml-4 relative">
<button
on:click={() => insertAtIndex(index)}
class="flex absolute -top-10 -left-8 justify-center items-center bg-white border-2 border-gray-200 w-8 h-8 rounded-full ring-8 ring-white dark:ring-gray-900 dark:bg-${color}-900"
>
<Icon class="text-gray-400" data={faPlus} />
</button>
<div
class="py-2 px-6 ml-4 mb-16 flex justify-between text-sm font-bold border border-gray-300 rounded-md shadow-md"
role="alert"
>
<span class="flex items-center z-50">
For loop
<Tooltip
content="Inside a loop, the flow input has an 'iter' property. It contains the value and the index of the iteration"
placement="bottom"
arrow
>
<Icon data={faInfoCircle} class="ml-2" /></Tooltip
>
</span>
<span class="flex items-center space-x-2">
<Toggle size="small" bind:checked={flowModuleSchema.flowModule.value.skip_failures}>
Skip failures
</Toggle>
<Button size="xs" color="alternative" on:click={() => removeAtIndex(index)}>
<Icon data={faTrashAlt} class="mr-2" />
Remove loop
</Button>
</span>
</div>
<span
class="flex absolute top-3 -left-8 justify-center items-center w-8 h-8 bg-orange-200 rounded-full ring-8 ring-white dark:ring-gray-900 dark:bg-orange-900"
>
<span class={`text-orange-600 dark:text-orange-400 font-bold text-center`}>
{index + 1}
</span>
</span>
<svelte:self
bind:args
bind:flowModuleSchemas={flowModuleSchema.childFlowModules}
parentIndex={index}
/>
<div
class="flex px-6 py-4 ml-4 mt-4 text-sm font-bold border border-gray-300 rounded-md shadow-md z-50"
role="alert"
>
End of For Loop
<Tooltip
content="The results of each iteration are collecting in an array and is the result of the step. "
placement="bottom"
arrow
>
<Icon data={faInfoCircle} class="ml-2" /></Tooltip
>
</div>
</li>
{:else}
<li id="module-{String(getIndexes(parentIndex, index).join('-'))}" class="ml-8">
<span class="relative">
<button
on:click={() => insertAtIndex(index)}
class="flex absolute -top-10 -left-12 justify-center items-center bg-white border-2 border-gray-200 w-8 h-8 rounded-full ring-8 ring-white dark:ring-gray-900 dark:bg-${color}-900"
>
<Icon class="text-gray-400" data={faPlus} />
</button>
<span
class={`flex absolute top-4 -left-12 justify-center items-center w-8 h-8 bg-${color}-200 rounded-full ring-8 ring-white dark:ring-gray-900 dark:bg-${color}-900`}
>
<span class={`text-${color}-600 dark:text-${color}-400 font-bold text-center`}>
{index + 1}
</span>
</span>
<ModuleStep
indexes={getIndexes(parentIndex, index)}
bind:mod={flowModuleSchema.flowModule}
bind:args
bind:schema={flowModuleSchema.schema}
bind:childFlowModules={flowModuleSchema.childFlowModules}
on:delete={() => removeAtIndex(index)}
/>
</span>
</li>
{/if}
{#if flowModuleSchemas.length - 1 === index}
<button
on:click={() => insertAtIndex(flowModuleSchemas.length)}
class="-ml-4 -mt-4 flex justify-center items-center bg-white border-2 border-gray-200 w-8 h-8 rounded-full ring-8 ring-white dark:ring-gray-900 dark:bg-${color}-900"
>
<Icon class="text-gray-400" data={faPlus} />
</button>
{/if}
{/each}
{#if flowModuleSchemas.length === 0}
<button
on:click={() => insertAtIndex(0)}
class="-ml-4 flex justify-center items-center bg-white border-2 border-gray-200 w-8 h-8 rounded-full ring-8 ring-white dark:ring-gray-900 dark:bg-${color}-900"
>
<Icon class="text-gray-400" data={faPlus} />
</button>
{/if}
</ol>
</div>
@@ -0,0 +1,12 @@
<script lang="ts">
import type { FlowModule } from '$lib/gen'
import FlowCardHeader from './FlowCardHeader.svelte'
export let title: string | undefined = undefined
export let flowModule: FlowModule | undefined = undefined
</script>
<FlowCardHeader {title} {flowModule}>
<slot name="header" />
</FlowCardHeader>
<slot />
@@ -0,0 +1,31 @@
<script lang="ts">
import IconedPath from '$lib/components/IconedPath.svelte'
import type { FlowModule } from '$lib/gen'
export let flowModule: FlowModule | undefined = undefined
export let title: string | undefined = undefined
$: flowModuleTitle =
flowModule?.summary ||
(flowModule?.value.type === 'rawscript'
? `Inline ${flowModule?.value.language}`
: 'Select a script')
</script>
<div
class="flex items-center justify-between flex-wrap py-2 px-4 border-b bg-gray-50 shadow-sm h-12"
>
{#if flowModule}
<span class="text-xs font-bold text-gray-900 flex flex-col shrink">
{#if 'path' in flowModule.value && flowModule.value.path}
<IconedPath path={flowModule.value.path} />
{:else if 'language' in flowModule.value && flowModule.value.language}
{flowModuleTitle}
{/if}
</span>
{/if}
{#if title}
<span class="text-xs font-bold text-gray-900 flex flex-col">{title}</span>
{/if}
<slot />
</div>
@@ -1,7 +1,7 @@
<script lang="ts">
import { Button } from 'flowbite-svelte'
import { isCopyFirstStepSchemaDisabled } from './flowState'
import { copyFirstStepSchema } from './flowStore'
import { isCopyFirstStepSchemaDisabled } from '../flowState'
import { copyFirstStepSchema } from '../flowStore'
</script>
<Button
@@ -0,0 +1,24 @@
<script lang="ts">
import { getContext } from 'svelte'
import type { FlowEditorContext } from '../types'
import FlowModuleWrapper from './FlowModuleWrapper.svelte'
import FlowSchedules from './FlowSchedules.svelte'
import FlowSettings from './FlowSettings.svelte'
import FlowInput from './FlowInput.svelte'
import FlowLoop from './FlowLoop.svelte'
const { selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
</script>
{#if $selectedId === 'settings'}
<FlowSettings />
{:else if $selectedId === 'settings-schedule'}
<FlowSettings defaultTab="schedule" />
{:else if $selectedId.includes('loop')}
<FlowLoop />
{:else if $selectedId === 'inputs'}
<FlowInput />
{:else}
<FlowModuleWrapper />
{/if}
@@ -1,18 +1,18 @@
<script lang="ts">
import { flowStore } from '$lib/components/flows/flowStore'
import SchemaEditor from '$lib/components/SchemaEditor.svelte'
import { emptySchema } from '$lib/utils'
import FlowCard from '../common/FlowCard.svelte'
import SchemaEditor from '../SchemaEditor.svelte'
import { flowStore } from './../flows/flowStore'
import CopyFirstStepSchema from './CopyFirstStepSchema.svelte'
import FlowBox from './FlowBox.svelte'
</script>
<FlowBox title="Flow Inputs">
<FlowCard title="Flow Inputs">
<div slot="header">
<CopyFirstStepSchema />
</div>
<div slot="content">
<div class="p-6 border-t border-gray-300">
<div>
<div class="p-6">
<SchemaEditor
on:change={() => {
$flowStore = $flowStore
@@ -21,4 +21,4 @@
/>
</div>
</div>
</FlowBox>
</FlowCard>
@@ -3,16 +3,16 @@
import { faCode, faRepeat } 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 FlowScriptPicker from '../pickers/FlowScriptPicker.svelte'
import PickHubScript from '../pickers/PickHubScript.svelte'
import PickScript from '../pickers/PickScript.svelte'
export let shouldDisableLoopCreation: boolean = false
export let shouldDisableTriggerScripts: boolean = false
const dispatch = createEventDispatcher()
</script>
<div class="space-y-4">
<div class="space-y-4 p-4">
{#if !shouldDisableTriggerScripts}
<div class="text-sm font-bold">Scripts</div>
{/if}
@@ -0,0 +1,36 @@
<script lang="ts">
import { getContext } from 'svelte'
import FlowCard from '../common/FlowCard.svelte'
import { flowStateStore } from '../flowState'
import type { FlowEditorContext } from '../types'
import Toggle from '$lib/components/Toggle.svelte'
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
const { selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
const [prefix, index] = $selectedId.split('-')
</script>
<FlowCard title="For loop">
<div>
<div class="p-6 flex flex-col">
{#if $flowStateStore.modules[index].flowModule.value.type === 'forloopflow'}
<span class="mb-2 text-sm font-bold">Iterator expression</span>
<SimpleEditor
lang="javascript"
bind:code={$flowStateStore.modules[index].flowModule.value.iterator.expr}
class="few-lines-editor"
/>
<span class="mb-2 text-sm font-bold">Skip failures</span>
<Toggle
bind:checked={$flowStateStore.modules[index].flowModule.value.skip_failures}
options={{
right: 'Skip failures'
}}
/>
{/if}
</div>
</div>
</FlowCard>
@@ -0,0 +1,173 @@
<script lang="ts">
import { VSplitPane } from 'svelte-split-pane'
import type { Schema } from '$lib/common'
import Tab from '$lib/components/common/tabs/Tab.svelte'
import TabContent from '$lib/components/common/tabs/TabContent.svelte'
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
import Editor from '$lib/components/Editor.svelte'
import EditorBar from '$lib/components/EditorBar.svelte'
import FlowPreview from '$lib/components/FlowPreview.svelte'
import FlowInputs from './FlowInputs.svelte'
import {
createInlineScriptModule,
createLoop,
createScriptFromInlineScript,
fork,
getStepPropPicker,
isEmptyFlowModule,
loadFlowModuleSchema,
pickScript
} from '$lib/components/flows/flowStateUtils'
import { flowStore } from '$lib/components/flows/flowStore'
import SchemaForm from '$lib/components/SchemaForm.svelte'
import { RawScript, type FlowModule } from '$lib/gen'
import FlowCard from '../common/FlowCard.svelte'
import FlowModuleHeader from './FlowModuleHeader.svelte'
import { flowStateStore, type FlowModuleSchema } from '../flowState'
import { scriptLangToEditorLang } from '$lib/utils'
export let indexes: string
export let flowModule: FlowModule
export let args: Record<string, any> = {}
export let schema: Schema
export let childFlowModules: FlowModuleSchema[] | undefined = undefined
const [i] = indexes.split('-').map(Number)
let editor: Editor
let websocketAlive = { pyright: false, black: false, deno: false }
$: shouldPick = isEmptyFlowModule(flowModule)
$: stepPropPicker = getStepPropPicker(
indexes.split('-').map(Number),
$flowStore.schema,
$flowStateStore,
args
)
async function apply<T>(fn: (arg: T) => Promise<FlowModuleSchema>, arg: T) {
const flowModuleSchema = await fn(arg)
flowModule = flowModuleSchema.flowModule
schema = flowModuleSchema.schema
if (flowModuleSchema.childFlowModules) {
childFlowModules = flowModuleSchema.childFlowModules
}
}
async function reload(flowModule: FlowModule) {
apply(loadFlowModuleSchema, flowModule)
}
async function applyCreateLoop() {
await apply(createLoop, null)
}
</script>
<div class="flex flex-col h-full ">
<FlowCard {flowModule}>
<svelte:fragment slot="header">
<div class="flex-shrink-0">
<FlowModuleHeader
bind:module={flowModule}
on:delete
on:fork={() => apply(fork, flowModule)}
on:createScriptFromInlineScript={() => {
apply(createScriptFromInlineScript, {
flowModule: flowModule,
suffix: indexes,
schema
})
}}
/>
</div>
</svelte:fragment>
{#if shouldPick}
<FlowInputs
shouldDisableTriggerScripts={i != 0}
shouldDisableLoopCreation={indexes.length > 1 || i == 0}
on:loop={() => applyCreateLoop()}
on:pick={(e) => apply(pickScript, e.detail.path)}
on:new={(e) =>
apply(createInlineScriptModule, {
language: e.detail.language,
kind: e.detail.kind,
subkind: e.detail.subkind
})}
/>
{:else}
{#if flowModule.value.type === 'rawscript'}
<div class="flex-shrink-0 border-b p-1">
<EditorBar {editor} lang={flowModule.value['language'] ?? 'deno'} {websocketAlive} />
</div>
{/if}
<div class="overflow-hidden flex-grow">
<VSplitPane
topPanelSize={flowModule.value.type === 'rawscript' ? '50%' : '0%'}
downPanelSize={flowModule.value.type === 'rawscript' ? '50%' : '100%'}
>
<top slot="top">
{#if flowModule.value.type === 'rawscript'}
<div on:mouseleave={() => reload(flowModule)} class="h-full overflow-auto">
<Editor
bind:websocketAlive
bind:this={editor}
class="h-full px-2"
bind:code={flowModule.value.content}
deno={flowModule.value.language === RawScript.language.DENO}
lang={scriptLangToEditorLang(flowModule.value.language)}
automaticLayout={true}
formatAction={() => reload(flowModule)}
/>
</div>
{/if}
</top>
<down slot="down">
<Tabs selected="inputs">
<Tab value="inputs">Inputs</Tab>
<Tab value="preview">Test</Tab>
<svelte:fragment slot="content">
<div class="h-full pb-16 overflow-y-scroll bg-white">
<div class="p-4 overflow-hidden">
<TabContent value="inputs">
<div class="w-11/12">
<SchemaForm
{schema}
inputTransform={true}
importPath={indexes}
bind:pickableProperties={stepPropPicker.pickableProperties}
bind:args={flowModule.input_transforms}
bind:extraLib={stepPropPicker.extraLib}
/>
</div>
</TabContent>
<TabContent value="preview">
<FlowPreview flow={$flowStore} {indexes} {schema} />
</TabContent>
<TabContent value="settings">
{#if ('path' in flowModule.value && flowModule.value.path) || ('language' in flowModule.value && flowModule.value.language)}
<input
on:click|stopPropagation={() => undefined}
class="overflow-x-auto"
type="text"
bind:value={flowModule.summary}
placeholder="Summary"
/>
{/if}
</TabContent>
</div>
</div>
</svelte:fragment>
</Tabs>
</down>
</VSplitPane>
</div>
{/if}
</FlowCard>
</div>
@@ -0,0 +1,70 @@
<script lang="ts">
import { isEmptyFlowModule } from '$lib/components/flows/flowStateUtils'
import HighlightCode from '$lib/components/HighlightCode.svelte'
import Modal from '$lib/components/Modal.svelte'
import type { FlowModule } from '$lib/gen'
import { getScriptByPath } from '$lib/utils'
import { faCode, faCodeBranch, faSave, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
import { Button } from 'flowbite-svelte'
import { createEventDispatcher } from 'svelte'
import Icon from 'svelte-awesome'
export let module: FlowModule
$: shouldPick = isEmptyFlowModule(module)
let modalViewer: Modal
let modalViewerContent = ''
let modalViewerLanguage: 'deno' | 'python3' | 'go' = 'deno'
async function viewCode() {
if (module.value.type == 'script') {
const { content, language } = await getScriptByPath(module.value.path!)
modalViewerContent = content
modalViewerLanguage = language
modalViewer.openModal()
} else {
throw Error('Not a script')
}
}
const dispatch = createEventDispatcher()
</script>
<div class="flex flex-row space-x-2" on:click|stopPropagation={() => undefined}>
{#if module.value.type === 'script' && !shouldPick}
<Button size="xs" color="alternative" on:click={() => dispatch('fork')}>
<Icon data={faCodeBranch} class="mr-2" />
Fork
</Button>
<Button size="xs" color="alternative" on:click={viewCode}>
<Icon data={faCode} class="mr-2" />
View code
</Button>
{/if}
{#if module.value.type === 'rawscript' && !shouldPick}
<Button size="xs" color="alternative" on:click={() => dispatch('createScriptFromInlineScript')}>
<Icon data={faSave} class="mr-2" />
Save to workspace
</Button>
{/if}
<Button
size="xs"
color="alternative"
on:click={() => {
dispatch('delete')
}}
>
<Icon data={faTrashAlt} class="mr-2" />
Remove step
</Button>
</div>
<Modal bind:this={modalViewer}>
<div slot="title">Script {'path' in module?.value ? module?.value.path : ''}</div>
<div slot="content">
<HighlightCode language={modalViewerLanguage} code={modalViewerContent} />
</div>
</Modal>
@@ -0,0 +1,48 @@
<script lang="ts">
import { getContext } from 'svelte'
import { flowStateStore } from '../flowState'
import type { FlowEditorContext } from '../types'
import FlowModule from './FlowModule.svelte'
const { selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
function selectedIdToIndexes(selectedId: string): number[] {
return selectedId.split('-').map(Number)
}
$: [parentIndex, childIndex] = selectedIdToIndexes($selectedId)
</script>
{#if $flowStateStore.modules[parentIndex] && $flowStateStore.modules[parentIndex].childFlowModules !== undefined}
{#each $flowStateStore.modules[parentIndex].childFlowModules ?? [] as fa, index}
{#if index === childIndex}
<FlowModule
indexes={$selectedId}
args={{}}
bind:flowModule={fa.flowModule}
bind:schema={fa.schema}
bind:childFlowModules={fa.childFlowModules}
on:delete={() => {
$flowStateStore.modules[parentIndex].childFlowModules?.splice(index, 1)
$flowStateStore = $flowStateStore
}}
/>
{/if}
{/each}
{:else}
{#each $flowStateStore.modules ?? [] as fa, index}
{#if index === parentIndex}
<FlowModule
indexes={$selectedId}
args={{}}
bind:flowModule={fa.flowModule}
bind:schema={fa.schema}
bind:childFlowModules={fa.childFlowModules}
on:delete={() => {
$flowStateStore.modules.splice(index, 1)
$flowStateStore = $flowStateStore
}}
/>
{/if}
{/each}
{/if}
@@ -0,0 +1,20 @@
<script lang="ts">
import CronInput from '$lib/components/CronInput.svelte'
import SchemaForm from '$lib/components/SchemaForm.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import { getContext } from 'svelte'
import { flowStore } from '../flowStore'
import type { FlowEditorContext } from '../types'
const { schedule } = getContext<FlowEditorContext>('FlowEditorContext')
</script>
<CronInput bind:schedule={$schedule.cron} />
<SchemaForm schema={$flowStore.schema} bind:args={$schedule.args} />
<Toggle
bind:checked={$schedule.enabled}
options={{
right: 'Schedule enabled'
}}
/>
@@ -0,0 +1,50 @@
<script lang="ts">
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
import Tab from '$lib/components/common/tabs/Tab.svelte'
import TabContent from '$lib/components/common/tabs/TabContent.svelte'
import { flowStore } from '$lib/components/flows/flowStore'
import Path from '$lib/components/Path.svelte'
import Required from '$lib/components/Required.svelte'
import { getContext } from 'svelte'
import FlowCard from '../common/FlowCard.svelte'
import type { FlowEditorContext } from '../types'
import FlowSchedules from './FlowSchedules.svelte'
const { path } = getContext<FlowEditorContext>('FlowEditorContext')
export let defaultTab = 'configuration'
</script>
<FlowCard title="Settings">
<Tabs selected={defaultTab}>
<Tab value="configuration">Configuration</Tab>
<Tab value="schedule">Schedule</Tab>
<svelte:fragment slot="content">
<TabContent value="configuration" class="px-4">
<Path bind:path={$flowStore.path} initialPath={path} namePlaceholder="my_flow" kind="flow">
<div slot="ownerToolkit">
Flow permissions depend on their path. Select the group <span class="font-mono"
>all</span
>
to share your flow, and <span class="font-mono">user</span> to keep it private.
<a href="https://docs.windmill.dev/docs/reference/namespaces">docs</a>
</div>
</Path>
<label class="block mt-4">
<span class="text-gray-700">Summary <Required required={false} /></span>
<textarea
bind:value={$flowStore.summary}
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
placeholder="A very short summary of the flow displayed when the flow is listed"
rows="1"
/>
</label>
</TabContent>
<TabContent value="schedule" class="px-4">
<FlowSchedules />
</TabContent>
</svelte:fragment>
</Tabs>
</FlowCard>
+37 -26
View File
@@ -10,54 +10,65 @@ export type FlowModuleSchema = {
previewResult?: any
}
export type FlowState = FlowModuleSchema[]
export type FlowState = {
modules: FlowModuleSchema[]
failureModule: FlowModuleSchema
}
export const flowStateStore = writable<FlowState>(undefined)
export async function initFlowState(flow: Flow) {
const flowState = await flowModulesToFlowState(flow.value.modules)
flowStateStore.set(flowState)
const modules = await mapFlowModules(flow.value.modules)
const failureModule = flow.value.failure_module
? await mapFlowModule(flow.value.failure_module)
: emptyFlowModuleSchema()
flowStateStore.set({
modules,
failureModule
})
}
export const isCopyFirstStepSchemaDisabled = derived(
flowStateStore,
(flowState: FlowState | undefined) => {
if (flowState) {
const firstModule = flowState[0]
const firstModule = flowState.modules[0]
if (!firstModule) {
return true
}
const fm = firstModule.flowModule
return flowState.length === 0 || isEmptyFlowModule(fm)
return flowState.modules.length === 0 || isEmptyFlowModule(fm)
} else {
return true
}
}
)
export async function flowModulesToFlowState(flowModules: FlowModule[]): Promise<FlowState> {
return Promise.all(
flowModules.map(async (flowModule: FlowModule) => {
const value = flowModule.value
if (value.type === 'forloopflow') {
const childFlowModules = await Promise.all(
value.modules.map(async (module) => loadFlowModuleSchema(module))
)
const loopFlowModule = await loadFlowModuleSchema(flowModule)
async function mapFlowModule(flowModule: FlowModule) {
const value = flowModule.value
if (value.type === 'forloopflow') {
const childFlowModules = await Promise.all(
value.modules.map(async (module) => loadFlowModuleSchema(module))
)
const loopFlowModule = await loadFlowModuleSchema(flowModule)
return {
...loopFlowModule,
childFlowModules
}
}
return {
...loopFlowModule,
childFlowModules
}
}
if (isEmptyFlowModule(flowModule)) {
return emptyFlowModuleSchema()
}
if (isEmptyFlowModule(flowModule)) {
return emptyFlowModuleSchema()
}
return loadFlowModuleSchema(flowModule)
})
)
return loadFlowModuleSchema(flowModule)
}
export async function mapFlowModules(flowModules: FlowModule[]): Promise<FlowModuleSchema[]> {
return Promise.all(flowModules.map(async (flowModule: FlowModule) => mapFlowModule(flowModule)))
}
export function flowStateToFlow(flowState: FlowState, flow: Flow): Flow {
@@ -65,7 +76,7 @@ export function flowStateToFlow(flowState: FlowState, flow: Flow): Flow {
return flow
}
const modules = flowState.map(({ flowModule, childFlowModules }) => {
const modules = flowState.modules.map(({ flowModule, childFlowModules }) => {
const fmv = flowModule.value
if (fmv.type === 'forloopflow' && childFlowModules && Array.isArray(childFlowModules)) {
@@ -57,7 +57,7 @@ export async function createInlineScriptModule({
subkind
}: {
language: RawScript.language
kind: Script.kind,
kind: Script.kind
subkind: 'pgsql' | 'flow'
}): Promise<FlowModuleSchema> {
const code = initialCode(language, kind, subkind)
@@ -210,7 +210,7 @@ export function getStepPropPicker(
const [parentIndex] = indexes
const flowInput = schemaToObject(flowInputSchema, args)
const results = getPreviousResults(flowState, parentIndex)
const results = getPreviousResults(flowState.modules, parentIndex)
const lastResult = results.length > 0 ? results[results.length - 1] : undefined
if (isInsideLoop) {
@@ -280,7 +280,8 @@ export type JobResult = {
export function mapJobResultsToFlowState(
jobs: JobResult,
config: 'upto' | 'justthis',
configIndex: number
i: number,
j: number | undefined
): void {
if (!Array.isArray(jobs.innerJobs) || jobs.innerJobs.length === 0) {
return
@@ -290,7 +291,11 @@ export function mapJobResultsToFlowState(
const job = jobs.job as CompletedJob
flowStateStore.update((flowState: FlowState) => {
flowState[configIndex] = job.result
if (j) {
flowState[i].childFlowModules[j].previewResult = job.result
} else {
flowState[i].previewResult = job.result
}
return flowState
})
} else {
@@ -313,13 +318,18 @@ export function mapJobResultsToFlowState(
return flowState
}
return flowState.map((flowModuleSchema: FlowModuleSchema, index) => {
if (index <= configIndex) {
const modules = flowState.modules.map((flowModuleSchema: FlowModuleSchema, index) => {
if (index <= i) {
flowModuleSchema.previewResult = results[index]
}
return flowModuleSchema
})
return {
modules,
failureModule: flowState.failureModule
}
})
}
}
+13 -6
View File
@@ -23,15 +23,22 @@ export function initFlow(flow: Flow) {
initFlowState(flow)
function migrateFlowModule(mod: FlowModule) {
let modVal = mod as FlowModule & { input_transform?: Record<string, InputTransform>, stop_after_if_expr?: string, skip_if_stopped?: boolean}
let modVal = mod as FlowModule & {
input_transform?: Record<string, InputTransform>
stop_after_if_expr?: string
skip_if_stopped?: boolean
}
if (modVal.input_transform) {
modVal.input_transforms = modVal.input_transform
delete modVal.input_transform
}
if (modVal.stop_after_if_expr) {
modVal.stop_after_if = { expr: modVal.stop_after_if_expr, skip_if_stopped: modVal.skip_if_stopped}
delete modVal.stop_after_if_expr;
delete modVal.skip_if_stopped;
modVal.stop_after_if = {
expr: modVal.stop_after_if_expr,
skip_if_stopped: modVal.skip_if_stopped
}
delete modVal.stop_after_if_expr
delete modVal.skip_if_stopped
}
}
}
@@ -39,8 +46,8 @@ export function initFlow(flow: Flow) {
export async function copyFirstStepSchema() {
const flowState = get(flowStateStore)
flowStore.update((flow) => {
if (flowState[0].schema) {
flow.schema = flowState[0].schema
if (flowState.modules[0].schema) {
flow.schema = flowState.modules[0].schema
}
return flow
})
@@ -0,0 +1,27 @@
<script lang="ts">
import { flowStore } from '$lib/components/flows/flowStore'
import { faPen } from '@fortawesome/free-solid-svg-icons'
import Icon from 'svelte-awesome'
import FlowPreviewButtons from './FlowPreviewButtons.svelte'
import FlowStatus from './FlowStatus.svelte'
import { getContext } from 'svelte'
import type { FlowEditorContext } from '../types'
const { select } = getContext<FlowEditorContext>('FlowEditorContext')
</script>
<div class="flex justify-between items-center border-y p-2 px-4">
<div id="flow_title" class="flex justify-between items-center">
<button on:click={() => select('settings')}>
<span class="font-mono text-sm "> {$flowStore.path}</span>
<Icon
data={faPen}
scale={0.8}
class="text-gray-500 ml-2 flex justify-center items-center mb-0.5"
/>
</button>
</div>
<FlowStatus />
<FlowPreviewButtons />
</div>
@@ -0,0 +1,46 @@
<script lang="ts">
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
import FlowPreviewContent from '$lib/components/FlowPreviewContent.svelte'
import { faPlay } from '@fortawesome/free-solid-svg-icons'
import { getContext } from 'svelte'
import Icon from 'svelte-awesome'
import type { FlowEditorContext } from '../types'
const { selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
let previewOpen = false
let previewMode: 'upTo' | 'whole' = 'whole'
$: upToDisabled = ['settings', 'settings-schedule', 'inputs', 'schedules'].includes($selectedId)
</script>
<span class="space-x-2 flex h-8">
<button
type="button"
disabled={upToDisabled}
on:click={() => {
previewMode = 'upTo'
previewOpen = !previewOpen
}}
class="flex items-center text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 font-medium rounded-lg text-sm px-4 py-2"
>
Test up to this step
<Icon data={faPlay} class="ml-2" scale={0.8} />
</button>
<button
type="button"
on:click={() => {
previewMode = 'whole'
previewOpen = !previewOpen
}}
class="flex items-center text-white bg-blue-500 hover:bg-blue-700 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-1 focus:outline-none "
>
Test flow
<Icon data={faPlay} class="ml-2" scale={0.8} />
</button>
</span>
<Drawer bind:open={previewOpen} size="800px">
<FlowPreviewContent bind:previewMode on:close={() => (previewOpen = !previewOpen)} />
</Drawer>
@@ -0,0 +1,19 @@
<script lang="ts">
import { classNames } from '$lib/utils'
import { getContext } from 'svelte'
import type { FlowEditorContext } from '../types'
const { schedule, select } = getContext<FlowEditorContext>('FlowEditorContext')
</script>
<div class="flex space-x-1">
<span
class={classNames(
' text-sm font-medium mr-2 px-2.5 py-0.5 rounded cursor-pointer',
$schedule?.enabled ? 'bg-sky-100 text-sky-800' : 'bg-gray-100 text-gray-800'
)}
on:click={() => select('settings-schedule')}
>
{$schedule?.enabled ? `Schedule: ${$schedule?.cron}` : 'Schedule disabled'}
</span>
</div>
@@ -0,0 +1,61 @@
<script lang="ts">
import { classNames } from '$lib/utils'
import { faClose, faCross, faTrash, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
import { createEventDispatcher } from 'svelte'
import Icon from 'svelte-awesome'
export let color: 'blue' | 'orange' = 'blue'
export let isFirst: boolean = false
export let isLast: boolean = false
export let hasLine: boolean = true
export let selected: boolean = false
export let deletable: boolean = false
const margin = isLast ? '' : isFirst ? 'mb-0.5' : 'my-0.5'
const dispatch = createEventDispatcher()
</script>
<div class="flex" on:click>
<div
class={classNames(
'flex mr-2 w-8 ',
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-100 text-blue-400' : 'bg-orange-100 text-orange-400',
margin
)}
>
<slot name="icon" />
</div>
</div>
<div
class={classNames(
'border w-full rounded-sm p-2 bg-white text-sm cursor-pointer flex justify-between items-center space-x-2',
margin,
selected ? 'outline outline-offset-1 outline-2 outline-gray-600' : ''
)}
>
<slot name="content" />
{#if deletable}
<button
type="button"
on:click={() => dispatch('delete')}
class="text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 font-medium rounded-sm text-xs px-2 py-1"
>
<Icon data={faTrashAlt} scale={0.8} />
</button>
{/if}
</div>
</div>
<style>
.line {
background: repeating-linear-gradient(to bottom, transparent 0 4px, #bbb 4px 8px) 50%/1px 100%
no-repeat;
}
</style>
@@ -0,0 +1,141 @@
<script lang="ts">
import type { FlowEditorContext } from '../types'
import { getContext } from 'svelte'
import FlowModuleSchemaItem from './FlowModuleSchemaItem.svelte'
import Icon from 'svelte-awesome'
import { faFlagCheckered, faPen, faPlus, faSliders } from '@fortawesome/free-solid-svg-icons'
import { emptyFlowModuleSchema } from '$lib/components/flows/flowStateUtils'
import { classNames } from '$lib/utils'
import type { FlowModuleSchema } from '../flowState'
export let flowModuleSchemas: FlowModuleSchema[]
export let prefix: string | undefined = undefined
const { select, selectedId, path } = getContext<FlowEditorContext>('FlowEditorContext')
function insertAtIndex(index: number): void {
flowModuleSchemas.splice(index, 0, emptyFlowModuleSchema())
flowModuleSchemas = flowModuleSchemas
select([prefix, String(index)].filter(Boolean).join('-'))
}
function removeAtIndex(index: number): void {
flowModuleSchemas.splice(index, 1)
flowModuleSchemas = flowModuleSchemas
}
</script>
<ul class="w-full">
{#if prefix === undefined}
<div
on:click={() => select('settings')}
class={classNames(
'border w-full rounded-md p-2 bg-white text-sm cursor-pointer flex items-center mb-4',
$selectedId.includes('settings')
? 'outline outline-offset-1 outline-2 outline-slate-900'
: ''
)}
>
<Icon data={faSliders} class="mr-2" />
<span class="font-bold">Settings</span>
</div>
<FlowModuleSchemaItem
on:click={() => select('inputs')}
isFirst
hasLine
selected={$selectedId === 'inputs'}
>
<div slot="icon">
<Icon data={faPen} scale={0.8} />
</div>
<div slot="content">
<span>Inputs</span>
</div>
</FlowModuleSchemaItem>
{/if}
{#each flowModuleSchemas as flowModuleSchema, index (index)}
<button
on:click={() => insertAtIndex(index)}
type="button"
class="text-gray-900 m-0.5 my-0.5 bg-white border 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>
{#if flowModuleSchema.flowModule.value.type === 'forloopflow'}
<li>
<FlowModuleSchemaItem
deletable
on:delete={() => removeAtIndex(index)}
on:click={() => select(['loop', String(index)].join('-'))}
selected={$selectedId === ['loop', String(index)].join('-')}
>
<div slot="icon">
<span>{index}</span>
</div>
<div slot="content">
<span>For loop</span>
</div>
</FlowModuleSchemaItem>
<div class="flex text-xs">
<div class="line mr-2 w-8" />
<div class="w-full mb-2">
<svelte:self
prefix={String(index)}
flowModuleSchemas={flowModuleSchema.childFlowModules}
/>
</div>
</div>
<div class="italic text-xs pl-10 text-gray-600">
The loop's result is the list of every iteration's result.
</div>
</li>
{:else}
<li>
<FlowModuleSchemaItem
on:click={() => select([prefix, String(index)].filter(Boolean).join('-'))}
color={prefix ? 'orange' : 'blue'}
isFirst={index === 0}
isLast={index === flowModuleSchemas.length - 1}
selected={$selectedId === [prefix, String(index)].filter(Boolean).join('-')}
deletable
on:delete={() => removeAtIndex(index)}
>
<div slot="icon">
<span>{index}</span>
</div>
<div slot="content" class="w-full">
<input
bind:value={flowModuleSchema.flowModule.summary}
placeholder={flowModuleSchema.flowModule.summary ||
flowModuleSchema.flowModule.value.path ||
(flowModuleSchema.flowModule.value.type === 'rawscript'
? `Inline ${flowModuleSchema.flowModule.value.language}`
: 'Select a script')}
/>
</div>
</FlowModuleSchemaItem>
</li>
{/if}
{/each}
<button
on:click={() => insertAtIndex(flowModuleSchemas.length)}
type="button"
class="text-gray-900 bg-white border m-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>
</ul>
<style>
.line {
background: repeating-linear-gradient(to bottom, transparent 0 4px, #bbb 4px 8px) 50%/1px 100%
no-repeat;
}
</style>
@@ -1,8 +0,0 @@
import { writable } from 'svelte/store'
import { scrollIntoView } from './utils'
export const stepOpened = writable<string | undefined>(undefined)
stepOpened.subscribe((insertAt) => {
setTimeout(() => scrollIntoView(document.querySelector(`#module-${insertAt}`)), 100)
})
@@ -0,0 +1,9 @@
import type { Writable } from 'svelte/store'
import type { Schedule } from './scheduleUtils'
export type FlowEditorContext = {
selectedId: Writable<string>
select: (id: string) => void
schedule: Writable<Schedule>
path: string
}
+14 -2
View File
@@ -122,8 +122,9 @@ export function getDefaultExpr(
previousExpr?: string
) {
const expr = previousExpr ?? `previous_result.${key}`
return `import { previous_result, flow_input, step, variable, resource, params } from 'windmill${importPath ? `@${importPath}` : ''
}'
return `import { previous_result, flow_input, step, variable, resource, params } from 'windmill${
importPath ? `@${importPath}` : ''
}'
${expr}`
}
@@ -166,3 +167,14 @@ export function codeToStaticTemplate(code?: string): string | undefined {
}
return undefined
}
export function getIndexes(parentIndex: number | undefined, childIndex: number): number[] {
const indexes: number[] = []
if (parentIndex !== undefined) {
indexes.push(parentIndex)
}
indexes.push(childIndex)
return indexes
}
+4 -4
View File
@@ -63,15 +63,15 @@ func main(x string) (interface{}, error) {
export const DENO_INIT_CODE_CLEAR = `// import * as wmill from "https://deno.land/x/windmill@v${__pkg__.version}/mod.ts"
export async function main() {
return
export async function main(x: string) {
return x
}
`
export const PYTHON_INIT_CODE_CLEAR = `#import wmill
def main():
return
def main(x: str):
return x
`
export const POSTGRES_INIT_CODE = `import {
+1 -1
View File
@@ -136,7 +136,7 @@
)}
>
<main>
<div class="w-full">
<div class="w-full h-screen">
<div
class="py-2 px-2 sm:px-4 md:px-8 flex justify-between items-center shadow-sm max-w-6xl mx-auto md:hidden"
>
+1 -4
View File
@@ -8,7 +8,6 @@
<script lang="ts">
import { page } from '$app/stores'
import CenteredPage from '$lib/components/CenteredPage.svelte'
import FlowBuilder from '$lib/components/FlowBuilder.svelte'
import { initFlow } from '$lib/components/flows/flowStore'
@@ -58,6 +57,4 @@
loadFlow()
</script>
<CenteredPage>
<FlowBuilder />
</CenteredPage>
<FlowBuilder />
@@ -14,7 +14,6 @@
import { workspaceStore } from '$lib/stores'
import { decodeState, emptySchema } from '$lib/utils'
import { initFlow } from '$lib/components/flows/flowStore'
import CenteredPage from '$lib/components/CenteredPage.svelte'
const initialState = $page.url.searchParams.get('state')
let flowLoadedFromUrl = initialState != undefined ? decodeState(initialState) : undefined
@@ -54,6 +53,4 @@
}
</script>
<CenteredPage>
<FlowBuilder {initialPath} />
</CenteredPage>
<FlowBuilder {initialPath} />