mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-19 16:02:14 +00:00
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:
@@ -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"
|
||||
|
||||
@@ -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} → {/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}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
+2
-2
@@ -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}
|
||||
+7
-7
@@ -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>
|
||||
+4
-4
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user