mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-24 16:01:42 +00:00
improve flow UX
This commit is contained in:
@@ -10,14 +10,15 @@
|
||||
sendUserToast,
|
||||
setQueryWithoutLoad
|
||||
} from '$lib/utils'
|
||||
import { faGlobe, faPen } from '@fortawesome/free-solid-svg-icons'
|
||||
import { onMount, setContext } from 'svelte'
|
||||
import { faEye, faPen } from '@fortawesome/free-solid-svg-icons'
|
||||
import { setContext } from 'svelte'
|
||||
import { writable } from 'svelte/store'
|
||||
import CenteredPage from './CenteredPage.svelte'
|
||||
import { Button } from './common'
|
||||
import { Button, Drawer, DrawerContent } from './common'
|
||||
import { dirtyStore } from './common/confirmationModal/dirtyStore'
|
||||
import UnsavedConfirmationModal from './common/confirmationModal/UnsavedConfirmationModal.svelte'
|
||||
import { OFFSET } from './CronInput.svelte'
|
||||
import FlowGraphViewer from './FlowGraphViewer.svelte'
|
||||
import FlowEditor from './flows/FlowEditor.svelte'
|
||||
import { flowStateStore } from './flows/flowState'
|
||||
import { flowStore } from './flows/flowStore'
|
||||
@@ -30,6 +31,7 @@
|
||||
export let initialPath: string = ''
|
||||
export let selectedId: string | undefined
|
||||
export let initialArgs: Record<string, any> = {}
|
||||
export let loading = false
|
||||
|
||||
let pathError = ''
|
||||
|
||||
@@ -145,7 +147,8 @@
|
||||
)
|
||||
}
|
||||
|
||||
const selectedIdStore = writable<string>('settings')
|
||||
const selectedIdStore = writable<string>(selectedId)
|
||||
|
||||
const scheduleStore = writable<Schedule>({ args: {}, cron: '', enabled: false })
|
||||
const previewArgsStore = writable<Record<string, any>>(initialArgs)
|
||||
|
||||
@@ -174,24 +177,40 @@
|
||||
})
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
selectedId && select(selectedId)
|
||||
})
|
||||
$: selectedId && select(selectedId)
|
||||
|
||||
$: initialPath && $workspaceStore && loadSchedule()
|
||||
|
||||
loadHubScripts()
|
||||
|
||||
let flowViewer: Drawer
|
||||
</script>
|
||||
|
||||
<UnsavedConfirmationModal />
|
||||
|
||||
<Drawer bind:this={flowViewer} size="900px">
|
||||
<DrawerContent title="View Graph" on:close={flowViewer.closeDrawer} noPadding>
|
||||
<FlowGraphViewer flow={$flowStore} overflowAuto />
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<div class="flex flex-col flex-1 h-screen">
|
||||
<!-- Nav between steps-->
|
||||
<div
|
||||
class="justify-between flex flex-row w-full py-2 px-4 space-x-4 overflow-x-auto scrollbar-hidden"
|
||||
>
|
||||
<div class="flex flex-row space-x-2">
|
||||
<div class="flex flex-row">
|
||||
<FlowImportExportMenu />
|
||||
<Button
|
||||
btnClasses="inline-flex"
|
||||
startIcon={{ icon: faEye }}
|
||||
variant="border"
|
||||
color="light"
|
||||
size="sm"
|
||||
on:click={flowViewer.openDrawer}
|
||||
>
|
||||
View Graph
|
||||
</Button>
|
||||
</div>
|
||||
<div class="gap-1 flex-row hidden md:flex shrink overflow-hidden">
|
||||
<Button
|
||||
@@ -229,9 +248,8 @@
|
||||
</div>
|
||||
|
||||
<!-- metadata -->
|
||||
|
||||
{#if $flowStateStore}
|
||||
<FlowEditor {initialPath} />
|
||||
<FlowEditor {initialPath} {loading} />
|
||||
{:else}
|
||||
<CenteredPage>Loading...</CenteredPage>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<script lang="ts">
|
||||
import { FlowGraph } from './graph'
|
||||
import HighlightCode from './HighlightCode.svelte'
|
||||
import InputTransformsViewer from './InputTransformsViewer.svelte'
|
||||
import IconedPath from './IconedPath.svelte'
|
||||
import { scriptPathToHref } from '../utils'
|
||||
import type { FlowModule, FlowValue } from '$lib/gen'
|
||||
export let flow: {
|
||||
summary: string
|
||||
description?: string
|
||||
value: FlowValue
|
||||
schema?: any
|
||||
}
|
||||
export let overflowAuto = false
|
||||
let stepDetail: FlowModule | undefined = undefined
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="h-full w-full border border-gray-700" class:overflow-auto={overflowAuto}>
|
||||
<FlowGraph
|
||||
modules={flow?.value?.modules}
|
||||
failureModule={flow?.value?.failure_module}
|
||||
on:click={(e) => (stepDetail = e.detail)}
|
||||
/>
|
||||
</div>
|
||||
<div class="w-full border-l border-r border-b border-gray-700 min-h-[150px] p-2">
|
||||
{#if stepDetail == undefined}
|
||||
<span class="font-black text-lg w-full my-4">
|
||||
<span>Click on a step to see its details</span>
|
||||
</span>
|
||||
{:else}
|
||||
<div class="font-black text-lg w-full mb-2 "
|
||||
>Step {stepDetail.id ?? ''}<span class="ml-2 font-normal">{stepDetail.summary || ''}</span
|
||||
></div
|
||||
>
|
||||
{#if stepDetail.value.type == 'identity'}
|
||||
<div> An identity step return as output its input </div>
|
||||
{:else if stepDetail.value.type == 'rawscript'}
|
||||
<div class="text-2xs mb-4">
|
||||
<h3>Step Inputs</h3>
|
||||
<InputTransformsViewer
|
||||
inputTransforms={stepDetail?.value?.input_transforms ??
|
||||
stepDetail?.input_transforms ??
|
||||
{}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3>Code</h3>
|
||||
<HighlightCode language={stepDetail.value.language} code={stepDetail.value.content} />
|
||||
{:else if stepDetail.value.type == 'script'}
|
||||
<div class="mb-4">
|
||||
<a
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
href={scriptPathToHref(stepDetail?.value?.path ?? '')}
|
||||
class=""
|
||||
>
|
||||
<IconedPath path={stepDetail?.value?.path ?? ''} />
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-2xs mb-4">
|
||||
<h3>Step Inputs</h3>
|
||||
<InputTransformsViewer
|
||||
inputTransforms={stepDetail?.value?.input_transforms ??
|
||||
stepDetail?.input_transforms ??
|
||||
{}}
|
||||
/>
|
||||
</div>
|
||||
{#if stepDetail.value.path.startsWith('hub/')}
|
||||
<div class="mt-6">
|
||||
<h3>Code</h3>
|
||||
<iframe
|
||||
class="w-full h-full text-sm"
|
||||
title="embedded script from hub"
|
||||
frameborder="0"
|
||||
src="https://hub.windmill.dev/embed/script/{stepDetail.value?.path?.substring(4)}"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -7,11 +7,8 @@
|
||||
import SchemaViewer from './SchemaViewer.svelte'
|
||||
import FieldHeader from './FieldHeader.svelte'
|
||||
import SvelteMarkdown from 'svelte-markdown'
|
||||
import { copyToClipboard, scriptPathToHref } from '../utils'
|
||||
import { FlowGraph } from './graph'
|
||||
import HighlightCode from './HighlightCode.svelte'
|
||||
import InputTransformsViewer from './InputTransformsViewer.svelte'
|
||||
import IconedPath from './IconedPath.svelte'
|
||||
import { copyToClipboard } from '../utils'
|
||||
import FlowGraphViewer from './FlowGraphViewer.svelte'
|
||||
|
||||
export let flow: {
|
||||
summary: string
|
||||
@@ -31,8 +28,6 @@
|
||||
export let tab: 'ui' | 'json' | 'schema' = 'ui'
|
||||
export let noSummary = false
|
||||
|
||||
let stepDetail: FlowModule | undefined = undefined
|
||||
|
||||
let open: { [id: number]: boolean } = {}
|
||||
if (initialOpen) {
|
||||
open[initialOpen] = true
|
||||
@@ -81,78 +76,7 @@
|
||||
{:else}
|
||||
<div class="text-gray-700 text-xs italic mb-4">No inputs</div>
|
||||
{/if}
|
||||
<div class="pr-40 flex flex-col">
|
||||
<div class="mt-5 h-full w-full border border-gray-700">
|
||||
<FlowGraph
|
||||
modules={flow?.value?.modules}
|
||||
failureModule={flow?.value?.failure_module}
|
||||
on:click={(e) => (stepDetail = e.detail)}
|
||||
/>
|
||||
</div>
|
||||
<div class="w-full border-l border-r border-b border-gray-700 min-h-[150px] p-2">
|
||||
{#if stepDetail == undefined}
|
||||
<span class="font-black text-lg w-full my-4">
|
||||
<span>Click on a step to see its details</span>
|
||||
</span>
|
||||
{:else}
|
||||
<div class="font-black text-lg w-full mb-2 "
|
||||
>Step {stepDetail.id ?? ''}<span class="ml-2 font-normal"
|
||||
>{stepDetail.summary || ''}</span
|
||||
></div
|
||||
>
|
||||
{#if stepDetail.value.type == 'identity'}
|
||||
<div> An identity step return as output its input </div>
|
||||
{:else if stepDetail.value.type == 'rawscript'}
|
||||
<div class="text-2xs mb-4">
|
||||
<h3>Step Inputs</h3>
|
||||
<InputTransformsViewer
|
||||
inputTransforms={stepDetail?.value?.input_transforms ??
|
||||
stepDetail?.input_transforms ??
|
||||
{}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3>Code</h3>
|
||||
<HighlightCode
|
||||
language={stepDetail.value.language}
|
||||
code={stepDetail.value.content}
|
||||
/>
|
||||
{:else if stepDetail.value.type == 'script'}
|
||||
<div class="mb-4">
|
||||
<a
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
href={scriptPathToHref(stepDetail?.value?.path ?? '')}
|
||||
class=""
|
||||
>
|
||||
<IconedPath path={stepDetail?.value?.path ?? ''} />
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-2xs mb-4">
|
||||
<h3>Step Inputs</h3>
|
||||
<InputTransformsViewer
|
||||
inputTransforms={stepDetail?.value?.input_transforms ??
|
||||
stepDetail?.input_transforms ??
|
||||
{}}
|
||||
/>
|
||||
</div>
|
||||
{#if stepDetail.value.path.startsWith('hub/')}
|
||||
<div class="mt-6">
|
||||
<h3>Code</h3>
|
||||
<iframe
|
||||
class="w-full h-full text-sm"
|
||||
title="embedded script from hub"
|
||||
frameborder="0"
|
||||
src="https://hub.windmill.dev/embed/script/{stepDetail.value?.path?.substring(
|
||||
4
|
||||
)}"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<FlowGraphViewer {flow} overflowAuto />
|
||||
</div>
|
||||
</TabContent>
|
||||
<TabContent value="json">
|
||||
|
||||
@@ -6,11 +6,12 @@
|
||||
|
||||
export let title: string | undefined = undefined
|
||||
export let overflow_y = true
|
||||
export let noPadding = false
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col divide-y h-screen max-h-screen">
|
||||
<div class="flex justify-between items-center py-2 px-6 gap-x-2">
|
||||
<div class="flex justify-between items-center gap-x-2">
|
||||
<Button
|
||||
variant="border"
|
||||
size="lg"
|
||||
@@ -29,7 +30,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="py-2 px-6 grow h-full max-h-full" class:overflow-y-auto={overflow_y}>
|
||||
<div
|
||||
class="{noPadding ? '' : 'py-2 px-6'} grow h-full max-h-full"
|
||||
class:overflow-y-auto={overflow_y}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation'
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import Fuse from 'fuse.js'
|
||||
|
||||
import { loadHubFlows, sendUserToast } from '$lib/utils'
|
||||
import { Button, ButtonPopup, ButtonPopupItem } from '$lib/components/common'
|
||||
import ItemPicker from '../ItemPicker.svelte'
|
||||
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
|
||||
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
|
||||
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
|
||||
import { flowStore, initFlow } from '$lib/components/flows/flowStore'
|
||||
|
||||
let hubFlows: any[] | undefined = undefined
|
||||
|
||||
const drawers: {
|
||||
hub: ItemPicker | undefined
|
||||
json: Drawer | undefined
|
||||
} = {
|
||||
hub: undefined,
|
||||
json: undefined
|
||||
}
|
||||
let hubItems: any[] = []
|
||||
let pendingJson: string
|
||||
let hubFilter = ''
|
||||
|
||||
const flowHubFuse: Fuse<any> = new Fuse([], {
|
||||
includeScore: false,
|
||||
keys: ['summary']
|
||||
})
|
||||
|
||||
$: filteredHubFlows =
|
||||
hubFilter.length > 0 ? flowHubFuse.search(hubFilter).map((value) => value.item) : hubFlows ?? []
|
||||
|
||||
async function importJson() {
|
||||
await goto('/flows/add')
|
||||
Object.assign($flowStore, JSON.parse(pendingJson))
|
||||
|
||||
initFlow($flowStore)
|
||||
drawers.json?.closeDrawer()
|
||||
}
|
||||
|
||||
async function loadHubFlowsWFuse(): Promise<void> {
|
||||
hubFlows = await loadHubFlows()
|
||||
flowHubFuse.setCollection(hubFlows ?? [])
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex flex-row gap-2">
|
||||
<ButtonPopup size="sm" startIcon={{ icon: faPlus }} href="/flows/add">
|
||||
<svelte:fragment slot="main">New Flow</svelte:fragment>
|
||||
<ButtonPopupItem on:click={() => drawers.hub?.openDrawer()}>
|
||||
Import flow from WindmillHub
|
||||
</ButtonPopupItem>
|
||||
<ButtonPopupItem on:click={() => drawers.json?.toggleDrawer()}>
|
||||
Import flow from raw JSON
|
||||
</ButtonPopupItem>
|
||||
</ButtonPopup>
|
||||
</div>
|
||||
|
||||
<!-- WindmillHub script list -->
|
||||
<ItemPicker
|
||||
bind:this={drawers.hub}
|
||||
pickCallback={(path) => {
|
||||
goto('/flows/add?hub=' + path)
|
||||
}}
|
||||
itemName={'Flow'}
|
||||
extraField="summary"
|
||||
loadItems={async () => {
|
||||
await loadHubFlowsWFuse()
|
||||
return filteredHubFlows.map((x) => ({ ...x, path: x.flow_id }))
|
||||
}}
|
||||
/>
|
||||
|
||||
<!-- Raw JSON -->
|
||||
<Drawer bind:this={drawers.json} size="800px">
|
||||
<DrawerContent title="Import flow from JSON" on:close={() => drawers.json?.toggleDrawer()}>
|
||||
<SimpleEditor bind:code={pendingJson} lang="json" class="h-full" />
|
||||
<span slot="submission"><Button size="sm" on:click={importJson}>Import</Button></span>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
@@ -3,21 +3,37 @@
|
||||
import FlowEditorPanel from './content/FlowEditorPanel.svelte'
|
||||
import FlowModuleSchemaMap from './map/FlowModuleSchemaMap.svelte'
|
||||
import { flowStore } from './flowStore'
|
||||
import WindmillIcon from '../icons/WindmillIcon.svelte'
|
||||
|
||||
export let initialPath: string
|
||||
export let loading: boolean
|
||||
</script>
|
||||
|
||||
<div class="h-full overflow-hidden border-t">
|
||||
<Splitpanes>
|
||||
<Pane size={25} minSize={20} class="h-full">
|
||||
<div class="grow overflow-auto bg-gray h-full bg-gray-50 relative">
|
||||
{#if $flowStore.value.modules}
|
||||
{#if loading}
|
||||
<div class="w-full h-full">
|
||||
<div class="block m-auto mt-40 w-10">
|
||||
<WindmillIcon class="animate-[spin_6s_linear_infinite]" height="40px" width="40px" />
|
||||
</div>
|
||||
</div>
|
||||
{:else if $flowStore.value.modules}
|
||||
<FlowModuleSchemaMap bind:modules={$flowStore.value.modules} root />
|
||||
{/if}
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane size={75} minSize={40}>
|
||||
<FlowEditorPanel {initialPath} />
|
||||
{#if loading}
|
||||
<div class="w-full h-full ">
|
||||
<div class="block m-auto mt-40 w-10">
|
||||
<WindmillIcon class="animate-[spin_6s_linear_infinite]" height="40px" width="40px" />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<FlowEditorPanel {initialPath} />
|
||||
{/if}
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
<FlowSettings {initialPath} defaultTab="schedule" />
|
||||
{:else if $selectedId === 'settings-same-worker'}
|
||||
<FlowSettings {initialPath} defaultTab="same-worker" />
|
||||
{:else if $selectedId === 'settings-graph'}
|
||||
<FlowSettings {initialPath} defaultTab="graph" />
|
||||
{:else if $selectedId === 'failure'}
|
||||
<FlowFailureModule />
|
||||
{:else}
|
||||
|
||||
@@ -12,7 +12,10 @@
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import { Alert } from '$lib/components/common'
|
||||
import { FlowGraph } from '$lib/components/graph'
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
||||
import { getContext } from 'svelte'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
|
||||
const { selectedId } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
export let initialPath: string
|
||||
|
||||
@@ -23,106 +26,97 @@
|
||||
<div class="h-full overflow-hidden">
|
||||
<FlowCard title="Settings">
|
||||
<div class="h-full flex-1">
|
||||
<Splitpanes horizontal>
|
||||
<Pane size={50} minSize={20}>
|
||||
<div bind:clientHeight={topHeight} class="max-w-full w-full overflow-hidden h-full">
|
||||
{#if $flowStore.value.modules}
|
||||
<FlowGraph
|
||||
minHeight={topHeight}
|
||||
modules={$flowStore.value.modules}
|
||||
failureModule={$flowStore.value.failure_module}
|
||||
<Tabs selected={defaultTab}>
|
||||
<Tab value="metadata">Metadata</Tab>
|
||||
<Tab value="schedule">Schedule</Tab>
|
||||
<Tab value="same-worker">Same Worker</Tab>
|
||||
<Tab value="graph">Graph</Tab>
|
||||
|
||||
<svelte:fragment slot="content">
|
||||
<TabContent value="metadata" class="p-4">
|
||||
<Path bind:path={$flowStore.path} {initialPath} namePlaceholder="my_flow" kind="flow" />
|
||||
|
||||
<label class="block my-4">
|
||||
<span class="text-gray-700 text-sm">Summary <Required required={false} /></span>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={$flowStore.summary}
|
||||
placeholder="A very short summary of the flow displayed when the flow is listed"
|
||||
rows="1"
|
||||
id="flow-summary"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane size={50} minSize={20}>
|
||||
<Tabs selected={defaultTab}>
|
||||
<Tab value="metadata">Metadata</Tab>
|
||||
<Tab value="schedule">Schedule</Tab>
|
||||
<Tab value="same-worker">Same Worker</Tab>
|
||||
</label>
|
||||
|
||||
<svelte:fragment slot="content">
|
||||
<TabContent value="metadata" class="p-4">
|
||||
<Path
|
||||
bind:path={$flowStore.path}
|
||||
{initialPath}
|
||||
namePlaceholder="my_flow"
|
||||
kind="flow"
|
||||
<label class="block my-4" for="inp">
|
||||
<span class="text-gray-700 text-sm">
|
||||
Description
|
||||
<Required required={false} detail="markdown" />
|
||||
<textarea
|
||||
type="text"
|
||||
class="text-sm"
|
||||
id="inp"
|
||||
bind:value={$flowStore.description}
|
||||
placeholder="A description to help users understand what this flow does and how to use it. Markdown accepted."
|
||||
rows="3"
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="block my-4">
|
||||
<span class="text-gray-700 text-sm">Summary <Required required={false} /></span>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={$flowStore.summary}
|
||||
placeholder="A very short summary of the flow displayed when the flow is listed"
|
||||
rows="1"
|
||||
id="flow-summary"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="block my-4" for="inp">
|
||||
<span class="text-gray-700 text-sm">
|
||||
Description
|
||||
<Required required={false} detail="markdown" />
|
||||
<textarea
|
||||
type="text"
|
||||
class="text-sm"
|
||||
id="inp"
|
||||
bind:value={$flowStore.description}
|
||||
placeholder="A description to help users understand what this flow does and how to use it. Markdown accepted."
|
||||
rows="3"
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div>
|
||||
<div class="font-bold pb-1 mt-4">Description preview</div>
|
||||
{#if $flowStore.description}
|
||||
<div
|
||||
class="prose max-h-48 mt-5 text-xs shadow-inner shadow-blue p-4 overflow-auto"
|
||||
>
|
||||
<SvelteMarkdown source={$flowStore.description} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-sm text-gray-500">
|
||||
Enter a description to see the preview
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
<div class="font-bold pb-1 mt-4">Description preview</div>
|
||||
{#if $flowStore.description}
|
||||
<div class="prose max-h-48 mt-5 text-xs shadow-inner shadow-blue p-4 overflow-auto">
|
||||
<SvelteMarkdown source={$flowStore.description} />
|
||||
</div>
|
||||
</TabContent>
|
||||
<TabContent value="schedule" class="p-4">
|
||||
<Alert type="info" title="Primary Schedule">
|
||||
Flows can be triggered by any schedules, their webhooks or their UI but they only
|
||||
have only one primary schedules with which they share the same path. The primary
|
||||
schedule can be set here.
|
||||
</Alert>
|
||||
<div class="mt-4" />
|
||||
<FlowSchedules />
|
||||
</TabContent>
|
||||
{:else}
|
||||
<div class="text-sm text-gray-500"> Enter a description to see the preview </div>
|
||||
{/if}
|
||||
</div>
|
||||
</TabContent>
|
||||
<TabContent value="schedule" class="p-4">
|
||||
<Alert type="info" title="Primary Schedule">
|
||||
Flows can be triggered by any schedules, their webhooks or their UI but they only have
|
||||
only one primary schedules with which they share the same path. The primary schedule
|
||||
can be set here.
|
||||
</Alert>
|
||||
<div class="mt-4" />
|
||||
<FlowSchedules />
|
||||
</TabContent>
|
||||
|
||||
<TabContent value="same-worker" class="p-4 flex flex-col">
|
||||
<Alert
|
||||
type="info"
|
||||
title="Toggle Same Worker to have all steps be ran on the same worker"
|
||||
>
|
||||
Steps will be run one after the other on the same worker, and will share a folder
|
||||
at `/shared` in which they can store heavier data and pass them to the next step. <br
|
||||
/><br />Beware that the `/shared` folder is not preserved across suspends and
|
||||
sleeps.
|
||||
</Alert>
|
||||
<span class="my-2 text-sm font-bold">Same Worker</span>
|
||||
<Toggle
|
||||
bind:checked={$flowStore.value.same_worker}
|
||||
options={{
|
||||
right: 'Same Worker'
|
||||
<TabContent value="same-worker" class="p-4 flex flex-col">
|
||||
<Alert
|
||||
type="info"
|
||||
title="Toggle Same Worker to have all steps be ran on the same worker"
|
||||
>
|
||||
Steps will be run one after the other on the same worker, and will share a folder at
|
||||
`/shared` in which they can store heavier data and pass them to the next step. <br
|
||||
/><br />Beware that the `/shared` folder is not preserved across suspends and sleeps.
|
||||
</Alert>
|
||||
<span class="my-2 text-sm font-bold">Same Worker</span>
|
||||
<Toggle
|
||||
bind:checked={$flowStore.value.same_worker}
|
||||
options={{
|
||||
right: 'Same Worker'
|
||||
}}
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent value="graph">
|
||||
<div bind:clientHeight={topHeight} class="max-w-full w-full overflow-hidden h-full">
|
||||
{#if $flowStore.value.modules}
|
||||
<FlowGraph
|
||||
on:click={(e) => {
|
||||
$selectedId = e.detail.id
|
||||
}}
|
||||
minHeight={topHeight}
|
||||
modules={$flowStore.value.modules}
|
||||
failureModule={$flowStore.value.failure_module}
|
||||
notSelectable
|
||||
/>
|
||||
</TabContent>
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
{/if}
|
||||
</div>
|
||||
</TabContent>
|
||||
</svelte:fragment>
|
||||
</Tabs>
|
||||
</div>
|
||||
</FlowCard>
|
||||
</div>
|
||||
|
||||
@@ -38,7 +38,7 @@ export function dfs(modules: FlowModule[]): string[] {
|
||||
|
||||
export async function initFlow(flow: Flow) {
|
||||
|
||||
let counter = 40
|
||||
let counter = 0
|
||||
for (const mod of flow.value.modules) {
|
||||
migrateFlowModule(mod)
|
||||
let val = mod.value
|
||||
|
||||
@@ -3,44 +3,29 @@
|
||||
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
|
||||
|
||||
import FlowViewer from '$lib/components/FlowViewer.svelte'
|
||||
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
|
||||
import { sendUserToast } from '$lib/utils'
|
||||
import { faFileExport, faFileImport, faGlobe } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import { faFileExport } from '@fortawesome/free-solid-svg-icons'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { Button } from '../../common'
|
||||
import { flowStore, initFlow } from '../flowStore'
|
||||
import { flowStore } from '../flowStore'
|
||||
import { cleanInputs } from '../utils'
|
||||
|
||||
let jsonSetterDrawer: Drawer
|
||||
let jsonViewerDrawer: Drawer
|
||||
let pendingJson: string
|
||||
|
||||
function importJson() {
|
||||
Object.assign($flowStore, JSON.parse(pendingJson))
|
||||
|
||||
initFlow($flowStore)
|
||||
sendUserToast('OpenFlow imported from JSON')
|
||||
jsonSetterDrawer.toggleDrawer()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex-row gap-x-2 hidden sm:flex">
|
||||
<Button size="sm" variant="border" color="light" on:click={() => jsonSetterDrawer.toggleDrawer()}>
|
||||
<Icon data={faFileImport} scale={0.6} class="inline mr-2" />
|
||||
Import JSON
|
||||
</Button>
|
||||
<Button size="sm" variant="border" color="light" on:click={() => jsonViewerDrawer.toggleDrawer()}>
|
||||
<Button
|
||||
btnClasses="mr-2"
|
||||
size="sm"
|
||||
variant="border"
|
||||
color="light"
|
||||
on:click={() => jsonViewerDrawer.toggleDrawer()}
|
||||
>
|
||||
<Icon data={faFileExport} scale={0.6} class="inline mr-2" />
|
||||
Export JSON
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Drawer bind:this={jsonSetterDrawer} size="800px">
|
||||
<DrawerContent title="Import JSON" on:close={() => jsonSetterDrawer.toggleDrawer()}>
|
||||
<Button slot="submission" size="sm" on:click={importJson}>Import</Button>
|
||||
<SimpleEditor bind:code={pendingJson} lang="json" class="h-full" />
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
<Drawer bind:this={jsonViewerDrawer} size="800px">
|
||||
<DrawerContent title="See JSON" on:close={() => jsonViewerDrawer.toggleDrawer()}>
|
||||
{#if $flowStore}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
'settings',
|
||||
'settings-schedule',
|
||||
'settings-retries',
|
||||
'settings-same-worker',
|
||||
'settings-graph',
|
||||
'inputs',
|
||||
'schedules',
|
||||
'failure'
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
<FlowSettingsItem />
|
||||
</div>
|
||||
{/if}
|
||||
<ul class="w-full flex-auto relative overflow-y-auto overflow-x-hidden p-4">
|
||||
<ul class="w-full flex-auto relative overflow-y-auto overflow-x-hidden py-4">
|
||||
{#if root}
|
||||
<li>
|
||||
<FlowInputsItem />
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<div on:click={() => select('settings')} class={settingsClass}>
|
||||
<Icon data={faSliders} class="mr-2" />
|
||||
<span
|
||||
class="text-xs font-bold flex flex-row justify-between w-full gap-2 items-center flex-wrap"
|
||||
class="text-xs font-bold flex flex-row justify-between w-full gap-2 items-center flex-wrap truncate"
|
||||
>
|
||||
Settings
|
||||
<span
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
export let modules: FlowModule[] | undefined = []
|
||||
export let failureModule: FlowModule | undefined = undefined
|
||||
export let minHeight: number = 0
|
||||
export let notSelectable = false
|
||||
|
||||
let selectedNode: string | undefined = undefined
|
||||
|
||||
const idGenerator = createIdGenerator()
|
||||
let nestedNodes: NestedNodes
|
||||
@@ -32,8 +35,8 @@
|
||||
let dispatch = createEventDispatcher()
|
||||
|
||||
$: {
|
||||
width && height && minHeight
|
||||
if (modules?.length) {
|
||||
width && height && minHeight && selectedNode
|
||||
if (modules) {
|
||||
createGraph(modules, failureModule)
|
||||
} else {
|
||||
nodes = edges = []
|
||||
@@ -94,7 +97,11 @@
|
||||
)
|
||||
} else if (type === 'forloopflow') {
|
||||
const expr = module.value.iterator['expr']
|
||||
return flowModuleToLoop(module.value.modules, `For loop: ${truncate(expr, 10)}`, parent)
|
||||
return flowModuleToLoop(
|
||||
module.value.modules,
|
||||
`For each item in: ${truncate(expr, 10)}`,
|
||||
parent
|
||||
)
|
||||
} else if (type === 'branchone') {
|
||||
const branches = [module.value.default, ...module.value.branches.map((b) => b.modules)]
|
||||
return flowModuleToBranch(
|
||||
@@ -175,9 +182,13 @@
|
||||
host,
|
||||
width: NODE.width,
|
||||
height: NODE.height,
|
||||
bgColor: 'white',
|
||||
borderColor: selectedNode == onClickDetail.id ? 'black' : '#999',
|
||||
bgColor: selectedNode == onClickDetail.id ? '#f5f5f5' : 'white',
|
||||
parentIds,
|
||||
clickCallback: (node) => {
|
||||
if (!notSelectable) {
|
||||
selectedNode = onClickDetail.id
|
||||
}
|
||||
dispatch('click', onClickDetail)
|
||||
},
|
||||
edgeLabel
|
||||
@@ -242,7 +253,6 @@
|
||||
}
|
||||
|
||||
function layoutNodes(nodes: Node[]): { nodes: Node[]; height: number } {
|
||||
if (!nodes.length) return { nodes: [], height: 0 }
|
||||
const stratify = dagStratify().id(({ id }: Node) => '' + id)
|
||||
const dag = stratify(nodes)
|
||||
const layout = sugiyama()
|
||||
@@ -296,6 +306,7 @@
|
||||
},
|
||||
width: NODE.width,
|
||||
height: NODE.height,
|
||||
borderColor: '#999',
|
||||
bgColor: '#d4e4ff',
|
||||
parentIds
|
||||
}
|
||||
@@ -315,8 +326,13 @@
|
||||
width: NODE.width,
|
||||
height: NODE.height,
|
||||
bgColor: 'rgb(248 113 113)',
|
||||
borderColor: '#999',
|
||||
|
||||
parentIds,
|
||||
clickCallback: (node) => {
|
||||
if (!notSelectable) {
|
||||
selectedNode = module.id
|
||||
}
|
||||
dispatch('click', module)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,26 +5,20 @@
|
||||
import { Script } from '$lib/gen'
|
||||
import { ScriptService } from '$lib/gen'
|
||||
import { workspaceStore, hubScripts } from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/utils'
|
||||
import { Button, ButtonPopup, ButtonPopupItem } from '$lib/components/common'
|
||||
import { ButtonPopup, ButtonPopupItem } from '$lib/components/common'
|
||||
import ItemPicker from '../ItemPicker.svelte'
|
||||
import type { HubItem } from '../flows/pickers/model'
|
||||
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
|
||||
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
|
||||
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
|
||||
import { flowStore, initFlow } from '$lib/components/flows/flowStore'
|
||||
|
||||
const drawers: {
|
||||
hub: ItemPicker | undefined
|
||||
template: Drawer | undefined
|
||||
json: Drawer | undefined
|
||||
} = {
|
||||
hub: undefined,
|
||||
template: undefined,
|
||||
json: undefined
|
||||
template: undefined
|
||||
}
|
||||
let hubItems: HubItem[]
|
||||
let pendingJson: string
|
||||
let templateScripts: Script[] = []
|
||||
let templateFilter = ''
|
||||
let filteredTemplates: Script[] | undefined
|
||||
@@ -41,14 +35,6 @@
|
||||
? templateFuse.search(templateFilter).map((value) => value.item)
|
||||
: templateScripts
|
||||
|
||||
function importJson() {
|
||||
Object.assign($flowStore, JSON.parse(pendingJson))
|
||||
|
||||
initFlow($flowStore)
|
||||
sendUserToast('OpenFlow imported from JSON')
|
||||
drawers.json?.toggleDrawer()
|
||||
}
|
||||
|
||||
async function loadTemplateScripts(): Promise<void> {
|
||||
templateScripts = await ScriptService.listScripts({
|
||||
workspace: $workspaceStore!,
|
||||
@@ -61,16 +47,13 @@
|
||||
<!-- Buttons -->
|
||||
<div class="flex flex-row gap-2">
|
||||
<ButtonPopup size="sm" startIcon={{ icon: faPlus }} href="/scripts/add">
|
||||
<svelte:fragment slot="main">New script</svelte:fragment>
|
||||
<svelte:fragment slot="main">New Script</svelte:fragment>
|
||||
<ButtonPopupItem on:click={() => drawers.hub?.openDrawer()}>
|
||||
Import script from WindmillHub
|
||||
</ButtonPopupItem>
|
||||
<ButtonPopupItem on:click={() => drawers.template?.toggleDrawer()}>
|
||||
Import script from template
|
||||
</ButtonPopupItem>
|
||||
<ButtonPopupItem on:click={() => drawers.json?.toggleDrawer()}>
|
||||
Import script from raw JSON
|
||||
</ButtonPopupItem>
|
||||
</ButtonPopup>
|
||||
</div>
|
||||
|
||||
@@ -79,7 +62,6 @@
|
||||
<ItemPicker
|
||||
bind:this={drawers.hub}
|
||||
pickCallback={(path) => {
|
||||
console.log('pick', { path })
|
||||
goto('/scripts/add?hub=' + path)
|
||||
}}
|
||||
itemName={'Script'}
|
||||
@@ -117,10 +99,3 @@
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
<!-- Raw JSON -->
|
||||
<Drawer bind:this={drawers.json} size="800px">
|
||||
<DrawerContent title="Import JSON" on:close={() => drawers.json?.toggleDrawer()}>
|
||||
<SimpleEditor bind:code={pendingJson} lang="json" class="h-full" />
|
||||
<span slot="submission"><Button size="sm" on:click={importJson}>Import</Button></span>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
import { Button, Tabs, Tab, Skeleton } from '../lib/components/common'
|
||||
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
|
||||
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
|
||||
import CreateActions from '$lib/components/flows/CreateActions.svelte'
|
||||
|
||||
type Tab = 'all' | 'personal' | 'groups' | 'shared' | 'hub'
|
||||
type Section = [string, FlowW[]]
|
||||
@@ -60,7 +61,7 @@
|
||||
}
|
||||
const flowFuse: Fuse<FlowW> = new Fuse(flows, flowFuseOptions)
|
||||
|
||||
const flowHubFuse: Fuse<FlowW> = new Fuse(flows, {
|
||||
const flowHubFuse: Fuse<any> = new Fuse([], {
|
||||
includeScore: false,
|
||||
keys: ['summary']
|
||||
})
|
||||
@@ -121,7 +122,7 @@
|
||||
async function loadHubFlowsWFuse(): Promise<void> {
|
||||
hubFlows = await loadHubFlows()
|
||||
loading.hub = false
|
||||
flowFuse.setCollection(flows)
|
||||
flowHubFuse.setCollection(hubFlows ?? [])
|
||||
}
|
||||
|
||||
async function archiveFlow(path: string): Promise<void> {
|
||||
@@ -149,7 +150,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Drawer bind:this={flowViewer}>
|
||||
<Drawer bind:this={flowViewer} size="900px">
|
||||
<DrawerContent
|
||||
title="Hub flow '{flowViewerFlow?.summary ?? ''}'"
|
||||
on:close={flowViewer.closeDrawer}
|
||||
@@ -163,7 +164,7 @@
|
||||
<CenteredPage>
|
||||
<PageHeader title="Flows" tooltip="Flows can compose and chain scripts together">
|
||||
<div class="flex flex-row">
|
||||
<Button href="/flows/add" size="sm" startIcon={{ icon: faPlus }}>New flow</Button>
|
||||
<CreateActions />
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
|
||||
@@ -20,9 +20,11 @@
|
||||
const hubId = $page.url.searchParams.get('hub')
|
||||
|
||||
const templatePath = $page.url.searchParams.get('template')
|
||||
let selectedId: string | undefined
|
||||
let selectedId: string = 'settings'
|
||||
let loading = false
|
||||
|
||||
async function loadFlow() {
|
||||
loading = true
|
||||
let state = initialState ? decodeState(initialState) : undefined
|
||||
let flow: Flow = state?.flow ?? {
|
||||
path: '',
|
||||
@@ -35,6 +37,7 @@
|
||||
schema: emptySchema()
|
||||
}
|
||||
|
||||
state?.selectedId && (selectedId = state?.selectedId)
|
||||
if (templatePath) {
|
||||
const template = await FlowService.getFlowByPath({
|
||||
workspace: $workspaceStore!,
|
||||
@@ -43,14 +46,17 @@
|
||||
Object.assign(flow, template)
|
||||
flow = flow
|
||||
$page.url.searchParams.delete('template')
|
||||
selectedId = 'settings-graph'
|
||||
} else if (hubId) {
|
||||
const hub = (await FlowService.getHubFlowById({ id: Number(hubId) })).flow
|
||||
Object.assign(flow, hub)
|
||||
flow = flow
|
||||
$page.url.searchParams.delete('hub')
|
||||
selectedId = 'settings-graph'
|
||||
}
|
||||
selectedId = state?.selectedId
|
||||
|
||||
await initFlow(flow)
|
||||
loading = false
|
||||
}
|
||||
|
||||
loadFlow()
|
||||
@@ -58,4 +64,4 @@
|
||||
$dirtyStore = true
|
||||
</script>
|
||||
|
||||
<FlowBuilder {selectedId} />
|
||||
<FlowBuilder {selectedId} {loading} />
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
let stateLoadedFromUrl = initialState != undefined ? decodeState(initialState) : undefined
|
||||
const initialArgs = decodeArgs($page.url.searchParams.get('args') ?? undefined)
|
||||
|
||||
let selectedId: string | undefined = undefined
|
||||
let selectedId: string = 'settings-graph'
|
||||
|
||||
let flow: Flow = {
|
||||
path: $page.params.path,
|
||||
|
||||
Reference in New Issue
Block a user