improve flow UX

This commit is contained in:
Ruben Fiszel
2022-11-14 23:30:09 +01:00
parent 21c8440ef5
commit a15b3117c2
18 changed files with 363 additions and 255 deletions
+28 -10
View File
@@ -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>
+3 -79
View File
@@ -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>
+5 -4
View File
@@ -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>
+9 -3
View File
@@ -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,