mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-10 08:07:03 +00:00
feat(frontend): add driverjs (#2327)
* feat(frontend): add driverjs * feat(frontend): pipeline done * feat(frontend): texts updated * feat(frontend): split tutorials into two * feat(frontend): Branch one tutorial * wip * feat(frontend): flow editor tutorial v0 done * feat(frontend): flow editor tutorial v0 done * feat(frontend): fix how tutorials are started * feat(frontend): fix how tutorials are started
This commit is contained in:
Generated
+11
@@ -24,6 +24,7 @@
|
||||
"d3-zoom": "^3.0.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"diff": "^5.1.0",
|
||||
"driver.js": "^1.3.0",
|
||||
"esm-env": "^1.0.0",
|
||||
"fast-equals": "^5.0.1",
|
||||
"graphql": "^16.7.1",
|
||||
@@ -3502,6 +3503,11 @@
|
||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/driver.js": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/driver.js/-/driver.js-1.3.0.tgz",
|
||||
"integrity": "sha512-ilUkVc5iMIYfMd8FdWy8n5Wv//gsJuRP+lo8QfWpwP9c0UGOgD7P9nVQMZwcdW84aqAZHHUHrV7GgiopAN6HUQ=="
|
||||
},
|
||||
"node_modules/earcut": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz",
|
||||
@@ -12517,6 +12523,11 @@
|
||||
"domhandler": "^5.0.3"
|
||||
}
|
||||
},
|
||||
"driver.js": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/driver.js/-/driver.js-1.3.0.tgz",
|
||||
"integrity": "sha512-ilUkVc5iMIYfMd8FdWy8n5Wv//gsJuRP+lo8QfWpwP9c0UGOgD7P9nVQMZwcdW84aqAZHHUHrV7GgiopAN6HUQ=="
|
||||
},
|
||||
"earcut": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz",
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
"d3-zoom": "^3.0.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"diff": "^5.1.0",
|
||||
"driver.js": "^1.3.0",
|
||||
"esm-env": "^1.0.0",
|
||||
"fast-equals": "^5.0.1",
|
||||
"graphql": "^16.7.1",
|
||||
|
||||
@@ -289,7 +289,7 @@
|
||||
{#if i < itemsLimit}
|
||||
<div class="flex max-w-md mt-1 w-full items-center">
|
||||
{#if itemsType?.type == 'number'}
|
||||
<input type="number" bind:value={v} />
|
||||
<input type="number" bind:value={v} id="arg-input-number-array" />
|
||||
{:else if itemsType?.type == 'string' && itemsType?.contentEncoding == 'base64'}
|
||||
<input
|
||||
type="file"
|
||||
@@ -348,6 +348,7 @@
|
||||
}
|
||||
value = value.concat('')
|
||||
}}
|
||||
id="arg-input-add-item"
|
||||
>
|
||||
<Icon data={faPlus} class="mr-2" />
|
||||
Add item
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
itemsType = undefined
|
||||
}
|
||||
}}
|
||||
id="array-type-narrowing"
|
||||
>
|
||||
<option value="string"> Items are strings</option>
|
||||
<option value="enum">Items are strings from an enum</option>
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
import { fade } from 'svelte/transition'
|
||||
import { loadFlowModuleState } from './flows/flowStateUtils'
|
||||
import FlowCopilotInputsModal from './copilot/FlowCopilotInputsModal.svelte'
|
||||
import FlowBuilderTutorials from './FlowBuilderTutorials.svelte'
|
||||
|
||||
export let initialPath: string = ''
|
||||
export let selectedId: string | undefined
|
||||
@@ -844,141 +845,150 @@
|
||||
}
|
||||
|
||||
$: $copilotCurrentStepStore === undefined && blurCopilot()
|
||||
|
||||
let renderCount = 0
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
|
||||
{#if !$userStore?.operator}
|
||||
<FlowCopilotDrawer {getHubCompletions} {genFlow} bind:flowCopilotMode />
|
||||
<FlowCopilotInputsModal
|
||||
on:confirmed={async () => {
|
||||
applyCopilotFlowInputs()
|
||||
finishStepGen()
|
||||
}}
|
||||
on:canceled={async () => {
|
||||
clearFlowInputsFromStep($copilotModulesStore[0]?.id)
|
||||
finishStepGen()
|
||||
}}
|
||||
bind:open={openCopilotInputsModal}
|
||||
inputs={Object.keys(copilotFlowInputs)}
|
||||
/>
|
||||
<ScriptEditorDrawer bind:this={$scriptEditorDrawer} />
|
||||
{#key renderCount}
|
||||
{#if !$userStore?.operator}
|
||||
<FlowCopilotDrawer {getHubCompletions} {genFlow} bind:flowCopilotMode />
|
||||
<FlowCopilotInputsModal
|
||||
on:confirmed={async () => {
|
||||
applyCopilotFlowInputs()
|
||||
finishStepGen()
|
||||
}}
|
||||
on:canceled={async () => {
|
||||
clearFlowInputsFromStep($copilotModulesStore[0]?.id)
|
||||
finishStepGen()
|
||||
}}
|
||||
bind:open={openCopilotInputsModal}
|
||||
inputs={Object.keys(copilotFlowInputs)}
|
||||
/>
|
||||
<ScriptEditorDrawer bind:this={$scriptEditorDrawer} />
|
||||
|
||||
<div class="flex flex-col flex-1 h-screen">
|
||||
<!-- Nav between steps-->
|
||||
<div
|
||||
class="justify-between flex flex-row items-center pl-2.5 pr-6 space-x-4 scrollbar-hidden max-h-12 h-full relative"
|
||||
>
|
||||
{#if $copilotCurrentStepStore !== undefined}
|
||||
<div transition:fade class="absolute inset-0 bg-gray-500 bg-opacity-75 z-[900] !m-0" />
|
||||
{/if}
|
||||
<div class="flex w-full max-w-md gap-4 items-center">
|
||||
<div class="min-w-64 w-full">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Flow summary"
|
||||
class="text-sm w-full font-semibold"
|
||||
bind:value={$flowStore.summary}
|
||||
<div class="flex flex-col flex-1 h-screen">
|
||||
<!-- Nav between steps-->
|
||||
<div
|
||||
class="justify-between flex flex-row items-center pl-2.5 pr-6 space-x-4 scrollbar-hidden max-h-12 h-full relative"
|
||||
>
|
||||
{#if $copilotCurrentStepStore !== undefined}
|
||||
<div transition:fade class="absolute inset-0 bg-gray-500 bg-opacity-75 z-[900] !m-0" />
|
||||
{/if}
|
||||
<div class="flex w-full max-w-md gap-4 items-center">
|
||||
<div class="min-w-64 w-full">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Flow summary"
|
||||
class="text-sm w-full font-semibold"
|
||||
bind:value={$flowStore.summary}
|
||||
/>
|
||||
</div>
|
||||
<UndoRedo
|
||||
undoProps={{ disabled: $history.index === 0 }}
|
||||
redoProps={{ disabled: $history.index === $history.history.length - 1 }}
|
||||
on:undo={() => {
|
||||
$flowStore = undo(history, $flowStore)
|
||||
$selectedIdStore = 'Input'
|
||||
}}
|
||||
on:redo={() => {
|
||||
$flowStore = redo(history)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<UndoRedo
|
||||
undoProps={{ disabled: $history.index === 0 }}
|
||||
redoProps={{ disabled: $history.index === $history.history.length - 1 }}
|
||||
on:undo={() => {
|
||||
$flowStore = undo(history, $flowStore)
|
||||
$selectedIdStore = 'Input'
|
||||
}}
|
||||
on:redo={() => {
|
||||
$flowStore = redo(history)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="gap-4 flex-row hidden md:flex w-full max-w-md">
|
||||
{#if $scheduleStore.enabled}
|
||||
<Button
|
||||
btnClasses="hidden lg:inline-flex"
|
||||
startIcon={{ icon: faCalendarAlt }}
|
||||
variant="contained"
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={async () => {
|
||||
select('settings-schedule')
|
||||
}}
|
||||
>
|
||||
{$scheduleStore.cron ?? ''}
|
||||
</Button>
|
||||
{/if}
|
||||
<div class="flex justify-start w-full">
|
||||
<div>
|
||||
<button
|
||||
<div class="gap-4 flex-row hidden md:flex w-full max-w-md">
|
||||
{#if $scheduleStore.enabled}
|
||||
<Button
|
||||
btnClasses="hidden lg:inline-flex"
|
||||
startIcon={{ icon: faCalendarAlt }}
|
||||
variant="contained"
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={async () => {
|
||||
select('settings-metadata')
|
||||
document.getElementById('path')?.focus()
|
||||
select('settings-schedule')
|
||||
}}
|
||||
>
|
||||
<Badge
|
||||
color="gray"
|
||||
class="center-center !bg-gray-300 !text-tertiary dark:!bg-gray-700 dark:!text-gray-300 !h-[28px] !w-[70px] rounded-r-none"
|
||||
{$scheduleStore.cron ?? ''}
|
||||
</Button>
|
||||
{/if}
|
||||
<div class="flex justify-start w-full">
|
||||
<div>
|
||||
<button
|
||||
on:click={async () => {
|
||||
select('settings-metadata')
|
||||
document.getElementById('path')?.focus()
|
||||
}}
|
||||
>
|
||||
<Pen size={12} class="mr-2" /> Path
|
||||
</Badge>
|
||||
</button>
|
||||
<Badge
|
||||
color="gray"
|
||||
class="center-center !bg-gray-300 !text-tertiary dark:!bg-gray-700 dark:!text-gray-300 !h-[28px] !w-[70px] rounded-r-none"
|
||||
>
|
||||
<Pen size={12} class="mr-2" /> Path
|
||||
</Badge>
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
readonly
|
||||
value={$flowStore.path && $flowStore.path != '' ? $flowStore.path : 'Choose a path'}
|
||||
class="font-mono !text-xs !min-w-[96px] !max-w-[300px] !w-full !h-[28px] !my-0 !py-0 !border-l-0 !rounded-l-none"
|
||||
on:focus={({ currentTarget }) => {
|
||||
currentTarget.select()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
readonly
|
||||
value={$flowStore.path && $flowStore.path != '' ? $flowStore.path : 'Choose a path'}
|
||||
class="font-mono !text-xs !min-w-[96px] !max-w-[300px] !w-full !h-[28px] !my-0 !py-0 !border-l-0 !rounded-l-none"
|
||||
on:focus={({ currentTarget }) => {
|
||||
currentTarget.select()
|
||||
</div>
|
||||
<div class="flex flex-row space-x-2">
|
||||
{#if $enterpriseLicense && initialPath != ''}
|
||||
<Awareness />
|
||||
{/if}
|
||||
<FlowBuilderTutorials
|
||||
on:reload={() => {
|
||||
renderCount += 1
|
||||
}}
|
||||
/>
|
||||
|
||||
<FlowCopilotStatus
|
||||
{copilotLoading}
|
||||
bind:copilotStatus
|
||||
{genFlow}
|
||||
{finishCopilotFlowBuilder}
|
||||
{abortController}
|
||||
/>
|
||||
|
||||
<FlowImportExportMenu />
|
||||
|
||||
<FlowPreviewButtons />
|
||||
<Button
|
||||
loading={loadingDraft}
|
||||
size="xs"
|
||||
startIcon={{ icon: faSave }}
|
||||
on:click={() => saveDraft()}
|
||||
>
|
||||
Save draft <Kbd small>Ctrl</Kbd><Kbd small>S</Kbd>
|
||||
</Button>
|
||||
<Button
|
||||
loading={loadingSave}
|
||||
size="xs"
|
||||
startIcon={{ icon: faSave }}
|
||||
on:click={() => saveFlow()}
|
||||
dropdownItems={initialPath != '' ? dropdownItems : undefined}
|
||||
>
|
||||
Deploy
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row space-x-2">
|
||||
{#if $enterpriseLicense && initialPath != ''}
|
||||
<Awareness />
|
||||
{/if}
|
||||
|
||||
<FlowCopilotStatus
|
||||
{copilotLoading}
|
||||
bind:copilotStatus
|
||||
{genFlow}
|
||||
{finishCopilotFlowBuilder}
|
||||
{abortController}
|
||||
/>
|
||||
|
||||
<FlowImportExportMenu />
|
||||
|
||||
<FlowPreviewButtons />
|
||||
<Button
|
||||
loading={loadingDraft}
|
||||
size="xs"
|
||||
startIcon={{ icon: faSave }}
|
||||
on:click={() => saveDraft()}
|
||||
>
|
||||
Save draft <Kbd small>Ctrl</Kbd><Kbd small>S</Kbd>
|
||||
</Button>
|
||||
<Button
|
||||
loading={loadingSave}
|
||||
size="xs"
|
||||
startIcon={{ icon: faSave }}
|
||||
on:click={() => saveFlow()}
|
||||
dropdownItems={initialPath != '' ? dropdownItems : undefined}
|
||||
>
|
||||
Deploy
|
||||
</Button>
|
||||
</div>
|
||||
<!-- metadata -->
|
||||
{#if $flowStateStore}
|
||||
<FlowEditor {loading} />
|
||||
{:else}
|
||||
<CenteredPage>Loading...</CenteredPage>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- metadata -->
|
||||
{#if $flowStateStore}
|
||||
<FlowEditor {loading} />
|
||||
{:else}
|
||||
<CenteredPage>Loading...</CenteredPage>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
Flow Builder not available to operators
|
||||
{/if}
|
||||
{:else}
|
||||
Flow Builder not available to operators
|
||||
{/if}
|
||||
{/key}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<script lang="ts">
|
||||
import { BookOpen } from 'lucide-svelte'
|
||||
import ButtonDropdown from './common/button/ButtonDropdown.svelte'
|
||||
import Button from './common/button/Button.svelte'
|
||||
|
||||
import FlowBuilderTutorialSimpleFlow from './tutorials/FlowBuilderTutorialSimpleFlow.svelte'
|
||||
import FlowBuilderTutorialsForLoop from './tutorials/FlowBuilderTutorialsForLoop.svelte'
|
||||
import FlowBranchOne from './tutorials/FlowBranchOne.svelte'
|
||||
import FlowBranchAll from './tutorials/FlowBranchAll.svelte'
|
||||
import MenuItem from './common/menu/MenuItem.svelte'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { resetAllTodos, skipAllTodos } from '$lib/tutorialUtils'
|
||||
import { tutorialsToDo } from '$lib/stores'
|
||||
import { clickButtonBySelector } from './tutorials/utils'
|
||||
|
||||
let buttonDropdown: ButtonDropdown | undefined = undefined
|
||||
|
||||
$: {
|
||||
if (buttonDropdown && $tutorialsToDo.includes(0)) {
|
||||
clickButtonBySelector('#tutorials-button')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<button on:pointerdown|stopPropagation>
|
||||
<ButtonDropdown hasPadding={false} bind:this={buttonDropdown}>
|
||||
<svelte:fragment slot="buttonReplacement">
|
||||
<Button nonCaptureEvent size="xs" color="light" variant="border" id="tutorials-button">
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<BookOpen size={16} />
|
||||
Tutorials
|
||||
</div>
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="items">
|
||||
<FlowBuilderTutorialSimpleFlow on:reload on:skipAll={skipAllTodos} />
|
||||
<FlowBuilderTutorialsForLoop on:reload on:skipAll={skipAllTodos} />
|
||||
<FlowBranchOne on:reload on:skipAll={skipAllTodos} />
|
||||
<FlowBranchAll on:reload on:skipAll={skipAllTodos} />
|
||||
<div class="border-t border-surface-hover" />
|
||||
<MenuItem
|
||||
on:click={() => {
|
||||
resetAllTodos()
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class={classNames(
|
||||
'text-primary flex flex-row items-center text-left gap-2 cursor-pointer hover:bg-surface-hover !text-xs font-semibold'
|
||||
)}
|
||||
>
|
||||
Reset tutorials
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItem on:click={() => skipAllTodos()}>
|
||||
<div
|
||||
class={classNames(
|
||||
'text-primary flex flex-row items-center text-left gap-2 cursor-pointer hover:bg-surface-hover !text-xs font-semibold'
|
||||
)}
|
||||
>
|
||||
Skip tutorials
|
||||
</div>
|
||||
</MenuItem>
|
||||
</svelte:fragment>
|
||||
</ButtonDropdown>
|
||||
</button>
|
||||
|
||||
<style global>
|
||||
.driver-popover-title {
|
||||
@apply leading-6 text-primary text-base;
|
||||
}
|
||||
|
||||
.driver-popover-description {
|
||||
@apply text-secondary text-sm;
|
||||
}
|
||||
|
||||
.driver-popover {
|
||||
@apply p-6;
|
||||
}
|
||||
</style>
|
||||
@@ -115,7 +115,10 @@
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<div class="flex divide-y flex-col space-y-2 h-screen bg-surface px-6 py-2 w-full">
|
||||
<div
|
||||
class="flex divide-y flex-col space-y-2 h-screen bg-surface px-6 py-2 w-full"
|
||||
id="flow-preview-content"
|
||||
>
|
||||
<div class="flex flex-row justify-between w-full items-center gap-x-2">
|
||||
<div class="w-8">
|
||||
<button
|
||||
@@ -154,6 +157,7 @@
|
||||
size="sm"
|
||||
btnClasses="w-full max-w-lg"
|
||||
on:click={() => runPreview($previewArgs)}
|
||||
id="flow-editor-test-flow-drawer"
|
||||
>
|
||||
Test flow <Kbd small>{getModifierKey()}</Kbd>
|
||||
<Kbd small><span class="text-lg font-bold">⏎</span></Kbd>
|
||||
|
||||
@@ -243,8 +243,11 @@
|
||||
connectProperty(path)
|
||||
return true
|
||||
})
|
||||
}}><Plug size={16} /> →</Button
|
||||
}}
|
||||
id="flow-editor-plug"
|
||||
>
|
||||
<Plug size={16} /> →
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -272,6 +272,7 @@
|
||||
on:click={() => {
|
||||
schemaModal.openDrawer(Object.assign({}, DEFAULT_PROPERTY))
|
||||
}}
|
||||
id="flow-editor-add-property"
|
||||
>
|
||||
Add Property
|
||||
</Button>
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
id="schema-modal-name"
|
||||
autofocus
|
||||
autocomplete="off"
|
||||
type="text"
|
||||
@@ -163,18 +164,19 @@
|
||||
color={isSelected ? 'blue' : 'light'}
|
||||
btnClasses={isSelected ? '!border-2' : 'm-[1px]'}
|
||||
on:click={() => {
|
||||
property.selectedType = argType;
|
||||
property.format = undefined;
|
||||
property.contentEncoding = undefined;
|
||||
property.enum_ = undefined;
|
||||
property.pattern = undefined;
|
||||
property.default = undefined;
|
||||
property.selectedType = argType
|
||||
property.format = undefined
|
||||
property.contentEncoding = undefined
|
||||
property.enum_ = undefined
|
||||
property.pattern = undefined
|
||||
property.default = undefined
|
||||
if (argType == 'array') {
|
||||
property.items = { type: 'string' }
|
||||
} else {
|
||||
property.items = undefined;
|
||||
property.items = undefined
|
||||
}
|
||||
}}
|
||||
id={`schema-modal-type-${argType}`}
|
||||
>
|
||||
{argType}
|
||||
</Button>
|
||||
@@ -185,12 +187,11 @@
|
||||
color={!property.selectedType ? 'blue' : 'light'}
|
||||
btnClasses={!property.selectedType ? '!border-2' : 'm-[1px]'}
|
||||
on:click={() => {
|
||||
property.selectedType = undefined;
|
||||
property.selectedType = undefined
|
||||
}}
|
||||
>
|
||||
any
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
@@ -260,6 +261,7 @@
|
||||
on:click={() => {
|
||||
dispatch('save', property)
|
||||
}}
|
||||
id="schema-modal-save"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
|
||||
@@ -22,6 +22,12 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
let menuRef: Menu<any> | undefined = undefined
|
||||
|
||||
export function toggleOpen() {
|
||||
menuRef?.openMenu()
|
||||
}
|
||||
</script>
|
||||
|
||||
<Menu let:open as="div" class="relative hover:z-50 flex w-full h-full">
|
||||
@@ -29,8 +35,12 @@
|
||||
<MenuButton
|
||||
class={twMerge('h-full w-full flex flex-row gap-2 items-center', hasPadding ? 'px-2' : '')}
|
||||
>
|
||||
<slot name="label" />
|
||||
<ChevronDown class="w-5 h-5" />
|
||||
{#if $$slots.buttonReplacement}
|
||||
<slot name="buttonReplacement" />
|
||||
{:else}
|
||||
<slot name="label" />
|
||||
<ChevronDown class="w-5 h-5" />
|
||||
{/if}
|
||||
</MenuButton>
|
||||
</span>
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<ManualPopover bind:this={copilotPopover}>
|
||||
<Button
|
||||
size="xs"
|
||||
btnClasses={'mr-2 ' + ($currentStepStore !== undefined ? 'z-[901]' : '')}
|
||||
btnClasses={'mr-2' + ($currentStepStore !== undefined ? 'z-[901]' : '')}
|
||||
on:click={() => {
|
||||
if (copilotLoading || ($currentStepStore !== undefined && $currentStepStore !== 'Input')) {
|
||||
abortController?.abort()
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
import type { FlowCopilotContext } from '../copilot/flow'
|
||||
import { classNames } from '$lib/utils'
|
||||
|
||||
export let loading: boolean
|
||||
|
||||
const { flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
export let loading: boolean
|
||||
|
||||
let size = 40
|
||||
|
||||
const { currentStepStore: copilotCurrentStepStore } =
|
||||
|
||||
@@ -55,10 +55,15 @@
|
||||
</PropPickerWrapper>
|
||||
{:else}
|
||||
<div class="flex justify-between gap-4 p-2">
|
||||
<div><pre class="text-sm">{branch.expr}</pre></div><div
|
||||
><Button startIcon={{ icon: faPen }} variant="border" on:click={() => (open = !open)}
|
||||
>Edit Predicate</Button
|
||||
></div
|
||||
>
|
||||
<div><pre class="text-sm">{branch.expr}</pre></div><div>
|
||||
<Button
|
||||
startIcon={{ icon: faPen }}
|
||||
variant="border"
|
||||
on:click={() => (open = !open)}
|
||||
id="flow-editor-edit-predicate"
|
||||
>
|
||||
Edit Predicate
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
let filter = ''
|
||||
</script>
|
||||
|
||||
<div class="p-4 h-full flex flex-col">
|
||||
<div class="p-4 h-full flex flex-col" id="flow-editor-flow-inputs">
|
||||
{#if summary == 'Terminate flow'}
|
||||
<Alert role="info" title="The flow stops here"
|
||||
>This is an identity step with an early stop that has 'true' for expression</Alert
|
||||
@@ -78,7 +78,7 @@
|
||||
</Tooltip>
|
||||
</h3>
|
||||
<div class="flex flex-row">
|
||||
<div class="flex flex-row flex-wrap gap-2">
|
||||
<div class="flex flex-row flex-wrap gap-2" id="flow-editor-action-script">
|
||||
<FlowScriptPicker
|
||||
label="TypeScript (Deno)"
|
||||
lang={Script.language.DENO}
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
</Tooltip>
|
||||
</div>
|
||||
{#if mod.value.iterator.type == 'javascript'}
|
||||
<div class="border w-full">
|
||||
<div class="border w-full" id="flow-editor-iterator-expression">
|
||||
<PropPickerWrapper
|
||||
notSelectable
|
||||
pickableProperties={stepPropPicker.pickableProperties}
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
return modules
|
||||
})
|
||||
}
|
||||
|
||||
$: editor !== undefined && setCopilotModuleEditor()
|
||||
|
||||
$: stepPropPicker = failureModule
|
||||
@@ -222,6 +223,7 @@
|
||||
bind:this={panes}
|
||||
class="h-full"
|
||||
style="max-height: calc(100% - {totalTopGap}px) !important;"
|
||||
id="flow-editor-editor"
|
||||
>
|
||||
<Splitpanes horizontal>
|
||||
<Pane bind:size={editorPanelSize} minSize={20}>
|
||||
@@ -291,7 +293,7 @@
|
||||
</Tabs>
|
||||
<div class="h-[calc(100%-32px)]">
|
||||
{#if selected === 'inputs' && (flowModule.value.type == 'rawscript' || flowModule.value.type == 'script' || flowModule.value.type == 'flow')}
|
||||
<div class="h-full overflow-auto">
|
||||
<div class="h-full overflow-auto" id="flow-editor-step-input">
|
||||
<PropPickerWrapper
|
||||
pickableProperties={stepPropPicker.pickableProperties}
|
||||
error={failureModule}
|
||||
@@ -359,16 +361,16 @@
|
||||
</div>
|
||||
{:else if advancedSelected === 'concurrency'}
|
||||
<div>
|
||||
<h2 class="pb-4">
|
||||
<h2 class="leading-10 !text-sm font-bold">
|
||||
Concurrency Limits
|
||||
<Tooltip>Allowed concurrency within a given timeframe</Tooltip>
|
||||
</h2>
|
||||
</div>
|
||||
{#if flowModule.value.type == 'rawscript'}
|
||||
<div>
|
||||
<div class="text-xs font-bold !mt-2"
|
||||
>Max number of executions within the time window</div
|
||||
>
|
||||
<div class="text-xs !mt-2 font-semibold leading-6">
|
||||
Max number of executions within the time window
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 max-w-sm"
|
||||
><input bind:value={flowModule.value.concurrent_limit} type="number" />
|
||||
<Button
|
||||
@@ -399,17 +401,19 @@
|
||||
btnClasses="mt-4"
|
||||
on:click={() => {
|
||||
$selectedId = 'settings-same-worker'
|
||||
}}>Set shared directory in the flow settings</Button
|
||||
}}
|
||||
>
|
||||
Set shared directory in the flow settings
|
||||
</Button>
|
||||
</div>
|
||||
{:else if advancedSelected === 's3'}
|
||||
<div>
|
||||
<h2 class="pb-4">
|
||||
S3 snippets
|
||||
<Tooltip
|
||||
>Pull, push and aggregate snippets for S3, particularly useful for ETL
|
||||
processes.</Tooltip
|
||||
>
|
||||
<Tooltip>
|
||||
Pull, push and aggregate snippets for S3, particularly useful for ETL
|
||||
processes.
|
||||
</Tooltip>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="flex gap-2 justify-between mb-4 items-center">
|
||||
@@ -424,7 +428,8 @@
|
||||
size="xs"
|
||||
on:click={() =>
|
||||
editor.setCode(s3Scripts[flowModule.value['language']][s3Kind])}
|
||||
>Apply snippet
|
||||
>
|
||||
Apply snippet
|
||||
</Button>
|
||||
</div>
|
||||
<HighlightCode
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
previewOpen = !previewOpen
|
||||
}}
|
||||
startIcon={{ icon: faPlay }}
|
||||
id="flow-editor-test-flow"
|
||||
>
|
||||
Test flow
|
||||
</Button>
|
||||
|
||||
@@ -22,10 +22,18 @@
|
||||
import { getDependentComponents } from '../flowExplorer'
|
||||
import type { FlowCopilotContext } from '$lib/components/copilot/flow'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { tutorialsToDo } from '$lib/stores'
|
||||
import FlowBuilderTutorialsForLoop from '$lib/components/tutorials/FlowBuilderTutorialsForLoop.svelte'
|
||||
import FlowBranchOne from '$lib/components/tutorials/FlowBranchOne.svelte'
|
||||
import FlowBranchAll from '$lib/components/tutorials/FlowBranchAll.svelte'
|
||||
|
||||
export let modules: FlowModule[] | undefined
|
||||
export let sidebarSize: number | undefined = undefined
|
||||
|
||||
let flowBuilderTutorialsForLoop: FlowBuilderTutorialsForLoop | undefined = undefined
|
||||
let flowBranchOne: FlowBranchOne | undefined = undefined
|
||||
let flowBranchAll: FlowBranchAll | undefined = undefined
|
||||
|
||||
const { selectedId, moving, history, flowStateStore, flowStore } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
@@ -206,20 +214,28 @@
|
||||
}
|
||||
}}
|
||||
on:insert={async ({ detail }) => {
|
||||
if (detail.modules) {
|
||||
await tick()
|
||||
if ($moving) {
|
||||
push(history, $flowStore)
|
||||
let indexToRemove = $moving.modules.findIndex((m) => $moving?.module?.id == m.id)
|
||||
$moving.modules.splice(indexToRemove, 1)
|
||||
detail.modules.splice(detail.index, 0, $moving.module)
|
||||
$selectedId = $moving.module.id
|
||||
$moving = undefined
|
||||
} else {
|
||||
await insertNewModuleAtIndex(detail.modules, detail.index ?? 0, detail.detail)
|
||||
$selectedId = detail.modules[detail.index ?? 0].id
|
||||
if ($tutorialsToDo.includes(1) && detail.detail == 'forloop') {
|
||||
flowBuilderTutorialsForLoop?.runTutorial()
|
||||
} else if ($tutorialsToDo.includes(2) && detail.detail == 'branchone') {
|
||||
flowBranchOne?.runTutorial()
|
||||
} else if ($tutorialsToDo.includes(3) && detail.detail == 'branchall') {
|
||||
flowBranchAll?.runTutorial()
|
||||
} else {
|
||||
if (detail.modules) {
|
||||
await tick()
|
||||
if ($moving) {
|
||||
push(history, $flowStore)
|
||||
let indexToRemove = $moving.modules.findIndex((m) => $moving?.module?.id == m.id)
|
||||
$moving.modules.splice(indexToRemove, 1)
|
||||
detail.modules.splice(detail.index, 0, $moving.module)
|
||||
$selectedId = $moving.module.id
|
||||
$moving = undefined
|
||||
} else {
|
||||
await insertNewModuleAtIndex(detail.modules, detail.index ?? 0, detail.detail)
|
||||
$selectedId = detail.modules[detail.index ?? 0].id
|
||||
}
|
||||
$flowStore = $flowStore
|
||||
}
|
||||
$flowStore = $flowStore
|
||||
}
|
||||
}}
|
||||
on:newBranch={async ({ detail }) => {
|
||||
@@ -253,3 +269,7 @@
|
||||
<FlowErrorHandlerItem />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FlowBuilderTutorialsForLoop bind:this={flowBuilderTutorialsForLoop} shouldRenderButton={false} />
|
||||
<FlowBranchOne bind:this={flowBranchOne} shouldRenderButton={false} />
|
||||
<FlowBranchAll bind:this={flowBranchAll} shouldRenderButton={false} />
|
||||
|
||||
@@ -35,115 +35,118 @@
|
||||
<button
|
||||
title="Add step"
|
||||
slot="trigger"
|
||||
id={`flow-editor-add-step-${index}`}
|
||||
type="button"
|
||||
class="text-primary bg-surface border mx-0.5 focus:outline-none hover:bg-surface-hover focus:ring-4 focus:ring-surface-selected font-medium rounded-full text-sm w-6 h-6 flex items-center justify-center"
|
||||
>
|
||||
<Cross size={12} />
|
||||
</button>
|
||||
<StepGen {index} bind:funcDesc bind:open {close} {modules} />
|
||||
{#if funcDesc.length === 0}
|
||||
<div class="font-mono divide-y text-xs w-full text-secondary">
|
||||
<button
|
||||
class="w-full text-left py-2 px-3 hover:bg-surface-hover"
|
||||
on:pointerdown={() => {
|
||||
close()
|
||||
dispatch('new', 'script')
|
||||
}}
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<Icon data={faCode} scale={0.8} class="mr-2" />
|
||||
Action
|
||||
</button>
|
||||
{#if trigger}
|
||||
<div id="flow-editor-insert-module">
|
||||
<StepGen {index} bind:funcDesc bind:open {close} {modules} />
|
||||
{#if funcDesc.length === 0}
|
||||
<div class="font-mono divide-y text-xs w-full text-secondary">
|
||||
<button
|
||||
class="w-full text-left py-2 px-3 hover:bg-surface-hover"
|
||||
on:pointerdown={() => {
|
||||
close()
|
||||
dispatch('new', 'trigger')
|
||||
dispatch('new', 'script')
|
||||
}}
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<Icon data={faBolt} scale={0.8} class="mr-2" />
|
||||
Trigger
|
||||
<Icon data={faCode} scale={0.8} class="mr-2" />
|
||||
Action
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
class="w-full text-left gap-1 py-2 px-3 hover:bg-surface-hover"
|
||||
on:pointerdown={() => {
|
||||
close()
|
||||
dispatch('new', 'approval')
|
||||
}}
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<Icon data={faCheck} class="mr-1.5" scale={0.8} />
|
||||
Approval
|
||||
</button>
|
||||
<button
|
||||
class="w-full inline-flex text-left py-2 px-3 hover:bg-surface-hover"
|
||||
on:pointerdown={() => {
|
||||
close()
|
||||
dispatch('new', 'forloop')
|
||||
}}
|
||||
role="menuitem"
|
||||
>
|
||||
<span class="mr-3">
|
||||
<Repeat size={14} />
|
||||
</span>
|
||||
|
||||
For Loop
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="w-full text-left py-2 px-3 hover:bg-surface-hover"
|
||||
on:pointerdown={() => {
|
||||
close()
|
||||
dispatch('new', 'branchone')
|
||||
}}
|
||||
role="menuitem"
|
||||
>
|
||||
<Icon data={faCodeBranch} scale={0.8} class="mr-2" />
|
||||
Branch to one
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="w-full text-left py-2 px-3 hover:bg-surface-hover"
|
||||
on:pointerdown={() => {
|
||||
close()
|
||||
dispatch('new', 'branchall')
|
||||
}}
|
||||
role="menuitem"
|
||||
>
|
||||
<Icon data={faCodeBranch} scale={0.8} class="mr-2" />
|
||||
Branch to all
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="w-full text-left py-2 px-3 hover:bg-surface-hover rounded-none"
|
||||
on:pointerdown={() => {
|
||||
close()
|
||||
dispatch('new', 'flow')
|
||||
}}
|
||||
role="menuitem"
|
||||
>
|
||||
<Icon data={faBarsStaggered} scale={0.8} class="mr-2" />
|
||||
Flow
|
||||
</button>
|
||||
{#if stop}
|
||||
{#if trigger}
|
||||
<button
|
||||
class="w-full text-left py-2 px-3 hover:bg-surface-hover"
|
||||
on:pointerdown={() => {
|
||||
close()
|
||||
dispatch('new', 'trigger')
|
||||
}}
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<Icon data={faBolt} scale={0.8} class="mr-2" />
|
||||
Trigger
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
class="w-full text-left py-2 px-3 hover:bg-surface-hover inline-flex gap-2.5"
|
||||
class="w-full text-left gap-1 py-2 px-3 hover:bg-surface-hover"
|
||||
on:pointerdown={() => {
|
||||
close()
|
||||
dispatch('new', 'end')
|
||||
dispatch('new', 'approval')
|
||||
}}
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
>
|
||||
<Icon data={faCheck} class="mr-1.5" scale={0.8} />
|
||||
Approval
|
||||
</button>
|
||||
<button
|
||||
class="w-full inline-flex text-left py-2 px-3 hover:bg-surface-hover"
|
||||
on:pointerdown={() => {
|
||||
close()
|
||||
dispatch('new', 'forloop')
|
||||
}}
|
||||
role="menuitem"
|
||||
>
|
||||
<Square size={14} />
|
||||
End Flow
|
||||
<span class="mr-3">
|
||||
<Repeat size={14} />
|
||||
</span>
|
||||
|
||||
For Loop
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="w-full text-left py-2 px-3 hover:bg-surface-hover"
|
||||
on:pointerdown={() => {
|
||||
close()
|
||||
dispatch('new', 'branchone')
|
||||
}}
|
||||
role="menuitem"
|
||||
>
|
||||
<Icon data={faCodeBranch} scale={0.8} class="mr-2" />
|
||||
Branch to one
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="w-full text-left py-2 px-3 hover:bg-surface-hover"
|
||||
on:pointerdown={() => {
|
||||
close()
|
||||
dispatch('new', 'branchall')
|
||||
}}
|
||||
role="menuitem"
|
||||
>
|
||||
<Icon data={faCodeBranch} scale={0.8} class="mr-2" />
|
||||
Branch to all
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="w-full text-left py-2 px-3 hover:bg-surface-hover rounded-none"
|
||||
on:pointerdown={() => {
|
||||
close()
|
||||
dispatch('new', 'flow')
|
||||
}}
|
||||
role="menuitem"
|
||||
>
|
||||
<Icon data={faBarsStaggered} scale={0.8} class="mr-2" />
|
||||
Flow
|
||||
</button>
|
||||
{#if stop}
|
||||
<button
|
||||
class="w-full text-left py-2 px-3 hover:bg-surface-hover inline-flex gap-2.5"
|
||||
on:pointerdown={() => {
|
||||
close()
|
||||
dispatch('new', 'end')
|
||||
}}
|
||||
role="menuitem"
|
||||
>
|
||||
<Square size={14} />
|
||||
End Flow
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Menu>
|
||||
|
||||
@@ -229,6 +229,7 @@
|
||||
dispatch('newBranch', { module: mod })
|
||||
}}
|
||||
type="button"
|
||||
id="add-branch-button"
|
||||
class=" text-primary bg-surface border mx-0.5 rotate-180 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"
|
||||
>
|
||||
<GitBranchPlus size={12} />
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
}
|
||||
}
|
||||
}}
|
||||
id={`flow-editor-virtual-${label}`}
|
||||
>
|
||||
<div
|
||||
class="flex gap-1 justify-between {center
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
<script lang="ts">
|
||||
import { driver } from 'driver.js'
|
||||
import 'driver.js/dist/driver.css'
|
||||
import { createEventDispatcher, getContext, tick } from 'svelte'
|
||||
import type { FlowEditorContext } from '../flows/types'
|
||||
import TutorialItem from './TutorialItem.svelte'
|
||||
import { clickButtonBySelector, triggerAddFlowStep, selectFlowStepKind } from './utils'
|
||||
import { updateProgress } from '$lib/tutorialUtils'
|
||||
import { RawScript } from '$lib/gen'
|
||||
|
||||
export let shouldRenderButton: boolean = true
|
||||
|
||||
const { flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export function runTutorial() {
|
||||
const branchAllTutorial = driver({
|
||||
showProgress: true,
|
||||
allowClose: true,
|
||||
onPopoverRender: (popover, { config, state }) => {
|
||||
if (state.activeIndex == 0) {
|
||||
const skipThisButton = document.createElement('button')
|
||||
skipThisButton.innerText = 'Skip this'
|
||||
skipThisButton.addEventListener('click', () => {
|
||||
updateProgress(3)
|
||||
|
||||
branchAllTutorial.destroy()
|
||||
})
|
||||
skipThisButton.setAttribute(
|
||||
'class',
|
||||
'border px-2 py-1 !text-xs font-normal rounded-md hover:bg-surface-hover transition-all flex items-center'
|
||||
)
|
||||
|
||||
const skipAllButton = document.createElement('button')
|
||||
skipAllButton.innerText = 'Skip all'
|
||||
skipAllButton.addEventListener('click', () => {
|
||||
dispatch('skipAll')
|
||||
branchAllTutorial.destroy()
|
||||
})
|
||||
|
||||
skipAllButton.setAttribute(
|
||||
'class',
|
||||
'border px-2 py-1 !text-xs font-normal rounded-md hover:bg-surface-hover transition-all flex items-center'
|
||||
)
|
||||
|
||||
const popoverDescription = document.querySelector('#driver-popover-description')
|
||||
|
||||
const div = document.createElement('div')
|
||||
|
||||
div.setAttribute('class', 'flex flex-row gap-2')
|
||||
|
||||
if (popoverDescription) {
|
||||
div.appendChild(skipThisButton)
|
||||
div.appendChild(skipAllButton)
|
||||
|
||||
popoverDescription.appendChild(div)
|
||||
}
|
||||
}
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
popover: {
|
||||
title: 'Welcome in the Windmil Flow editor',
|
||||
description: 'Learn how to build our first branch to be executed on a condition'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#flow-editor-add-step-0',
|
||||
popover: {
|
||||
title: 'Add a step',
|
||||
description: 'Click here to add a step to your flow',
|
||||
onNextClick: () => {
|
||||
triggerAddFlowStep(0)
|
||||
|
||||
setTimeout(() => {
|
||||
branchAllTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
popover: {
|
||||
title: 'Steps kind',
|
||||
description: "Choose the kind of step you want to add. Let's start with a simple action"
|
||||
},
|
||||
element: '#flow-editor-insert-module'
|
||||
},
|
||||
|
||||
{
|
||||
popover: {
|
||||
title: 'Insert Branch all',
|
||||
description: "Let's pick branch all",
|
||||
onNextClick: () => {
|
||||
selectFlowStepKind(6)
|
||||
setTimeout(() => {
|
||||
branchAllTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
},
|
||||
element: '#flow-editor-insert-module > div > button:nth-child(6)'
|
||||
},
|
||||
|
||||
{
|
||||
element: '#add-branch-button',
|
||||
popover: {
|
||||
title: 'Add branch',
|
||||
description: 'Click here to add a branch',
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('#add-branch-button')
|
||||
|
||||
setTimeout(() => {
|
||||
branchAllTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
element: '#flow-editor-step-input',
|
||||
popover: {
|
||||
title: 'Branch all',
|
||||
description:
|
||||
'All branches will be executed, and the result will be gathered in an array',
|
||||
|
||||
onNextClick: () => {
|
||||
if ($flowStore.value.modules[0].value.type === 'branchall') {
|
||||
$flowStore.value.modules[0].value = {
|
||||
type: 'branchall',
|
||||
branches: [
|
||||
{
|
||||
modules: [
|
||||
{
|
||||
id: 'b',
|
||||
value: {
|
||||
type: 'rawscript',
|
||||
content:
|
||||
'// import * as wmill from "npm:windmill-client@1"\n\nexport async function main(x: string) {\n return "Hello"\n}\n',
|
||||
language: RawScript.language.DENO,
|
||||
input_transforms: {
|
||||
x: {
|
||||
type: 'static'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
skip_failure: false
|
||||
},
|
||||
{
|
||||
summary: '',
|
||||
modules: [
|
||||
{
|
||||
id: 'c',
|
||||
value: {
|
||||
type: 'rawscript',
|
||||
content:
|
||||
'package inner\n\nfunc main() (interface{}, error) {\n\treturn "World", nil\n}\n',
|
||||
language: RawScript.language.GO,
|
||||
input_transforms: {}
|
||||
}
|
||||
}
|
||||
],
|
||||
skip_failure: false
|
||||
}
|
||||
],
|
||||
parallel: false
|
||||
}
|
||||
}
|
||||
|
||||
$flowStore = $flowStore
|
||||
dispatch('reload')
|
||||
|
||||
tick().then(() => {
|
||||
branchAllTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
element: '#flow-editor-test-flow',
|
||||
popover: {
|
||||
title: 'Test your flow',
|
||||
description: 'We can now test our flow',
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('#flow-editor-test-flow')
|
||||
|
||||
setTimeout(() => {
|
||||
branchAllTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
element: '#flow-editor-test-flow-drawer',
|
||||
popover: {
|
||||
title: 'Test your flow',
|
||||
description: 'Finally we can test our flow, and view the results!',
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('#flow-editor-test-flow-drawer')
|
||||
|
||||
setTimeout(() => {
|
||||
branchAllTutorial.moveNext()
|
||||
updateProgress(3)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
branchAllTutorial.drive()
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if shouldRenderButton}
|
||||
<TutorialItem
|
||||
on:click={() => runTutorial()}
|
||||
label="Branch all tutorial"
|
||||
index={3}
|
||||
id="flow-builder-tutorial-branchall"
|
||||
/>
|
||||
{/if}
|
||||
@@ -0,0 +1,318 @@
|
||||
<script lang="ts">
|
||||
import { driver } from 'driver.js'
|
||||
import 'driver.js/dist/driver.css'
|
||||
import { createEventDispatcher, getContext, tick } from 'svelte'
|
||||
import type { FlowEditorContext } from '../flows/types'
|
||||
import TutorialItem from './TutorialItem.svelte'
|
||||
import {
|
||||
clickButtonBySelector,
|
||||
setInputBySelector,
|
||||
triggerAddFlowStep,
|
||||
selectFlowStepKind
|
||||
} from './utils'
|
||||
import { updateProgress } from '$lib/tutorialUtils'
|
||||
import { RawScript } from '$lib/gen'
|
||||
|
||||
export let shouldRenderButton: boolean = true
|
||||
|
||||
const { flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export function runTutorial() {
|
||||
const branchOneTutorial = driver({
|
||||
showProgress: true,
|
||||
allowClose: true,
|
||||
onPopoverRender: (popover, { config, state }) => {
|
||||
if (state.activeIndex == 0) {
|
||||
const skipThisButton = document.createElement('button')
|
||||
skipThisButton.innerText = 'Skip this'
|
||||
skipThisButton.addEventListener('click', () => {
|
||||
updateProgress(2)
|
||||
|
||||
branchOneTutorial.destroy()
|
||||
})
|
||||
skipThisButton.setAttribute(
|
||||
'class',
|
||||
'border px-2 py-1 !text-xs font-normal rounded-md hover:bg-surface-hover transition-all flex items-center'
|
||||
)
|
||||
|
||||
const skipAllButton = document.createElement('button')
|
||||
skipAllButton.innerText = 'Skip all'
|
||||
skipAllButton.addEventListener('click', () => {
|
||||
dispatch('skipAll')
|
||||
branchOneTutorial.destroy()
|
||||
})
|
||||
|
||||
skipAllButton.setAttribute(
|
||||
'class',
|
||||
'border px-2 py-1 !text-xs font-normal rounded-md hover:bg-surface-hover transition-all flex items-center'
|
||||
)
|
||||
|
||||
const popoverDescription = document.querySelector('#driver-popover-description')
|
||||
|
||||
const div = document.createElement('div')
|
||||
|
||||
div.setAttribute('class', 'flex flex-row gap-2')
|
||||
|
||||
if (popoverDescription) {
|
||||
div.appendChild(skipThisButton)
|
||||
div.appendChild(skipAllButton)
|
||||
|
||||
popoverDescription.appendChild(div)
|
||||
}
|
||||
}
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
popover: {
|
||||
title: 'Welcome in the Windmil Flow editor',
|
||||
description: 'Learn how to build our first branch to be executed on a condition'
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
popover: {
|
||||
title: 'Flows inputs',
|
||||
description: 'Flows have inputs that can be used in the flow',
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('#flow-editor-virtual-Input')
|
||||
|
||||
setTimeout(() => {
|
||||
branchOneTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
},
|
||||
element: '#svelvet-Input'
|
||||
},
|
||||
{
|
||||
element: '#flow-editor-add-property',
|
||||
popover: {
|
||||
title: 'Add a property',
|
||||
description: 'Click here to add a property to your schema',
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('#flow-editor-add-property')
|
||||
setTimeout(() => {
|
||||
branchOneTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#schema-modal-name',
|
||||
popover: {
|
||||
title: 'Name your property',
|
||||
description: 'Give a name to your property. Here we will call it condition',
|
||||
onNextClick: () => {
|
||||
setInputBySelector('#schema-modal-name', 'condition')
|
||||
branchOneTutorial.moveNext()
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#schema-modal-type-boolean',
|
||||
popover: {
|
||||
title: 'Property type',
|
||||
description: 'Choose the type of your property. Here we will choose boolean',
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('#schema-modal-type-boolean')
|
||||
branchOneTutorial.moveNext()
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#schema-modal-save',
|
||||
popover: {
|
||||
title: 'Save your property',
|
||||
description: 'Click here to save your property',
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('#schema-modal-save')
|
||||
|
||||
setTimeout(() => {
|
||||
branchOneTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
element: '#flow-editor-add-step-0',
|
||||
popover: {
|
||||
title: 'Branch one',
|
||||
description: 'Windmill supports branches, let us add one',
|
||||
onNextClick: () => {
|
||||
triggerAddFlowStep(0)
|
||||
|
||||
setTimeout(() => {
|
||||
branchOneTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
popover: {
|
||||
title: 'Steps kind',
|
||||
description: 'Choose the kind of step you want to add.'
|
||||
},
|
||||
element: '#flow-editor-insert-module'
|
||||
},
|
||||
{
|
||||
popover: {
|
||||
title: 'Insert Branch one',
|
||||
description: "Let's pick branch one",
|
||||
onNextClick: () => {
|
||||
selectFlowStepKind(5)
|
||||
|
||||
setTimeout(() => {
|
||||
branchOneTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
},
|
||||
element: '#flow-editor-insert-module > div > button:nth-child(5)'
|
||||
},
|
||||
|
||||
{
|
||||
element: '#add-branch-button',
|
||||
popover: {
|
||||
title: 'Add branch',
|
||||
description: 'Click here to add a branch',
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('#add-branch-button')
|
||||
|
||||
setTimeout(() => {
|
||||
branchOneTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
element: '#flow-editor-edit-predicate',
|
||||
popover: {
|
||||
title: 'Edit predicate',
|
||||
description: 'Click here to edit the predicate of your branch',
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('#flow-editor-edit-predicate')
|
||||
|
||||
setTimeout(() => {
|
||||
branchOneTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
element: '.key',
|
||||
popover: {
|
||||
title: 'Connect',
|
||||
description: 'As we did before, we can connect to the iterator of the loop',
|
||||
onNextClick: () => {
|
||||
if ($flowStore.value.modules[0].value.type === 'branchone') {
|
||||
$flowStore.value.modules[0].value.branches[0].expr = 'flow_input.condition'
|
||||
}
|
||||
|
||||
$flowStore = $flowStore
|
||||
dispatch('reload')
|
||||
|
||||
tick().then(() => {
|
||||
branchOneTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
//flow-editor-add-step-
|
||||
|
||||
{
|
||||
popover: {
|
||||
title: 'Branche modules',
|
||||
description:
|
||||
'We can now add modules to each branch, one in typescript and one in python',
|
||||
onNextClick: () => {
|
||||
if ($flowStore.value.modules[0].value.type === 'branchone') {
|
||||
$flowStore.value.modules[0].value = {
|
||||
type: 'branchone',
|
||||
branches: [
|
||||
{
|
||||
summary: '',
|
||||
expr: 'flow_input.condition',
|
||||
modules: [
|
||||
{
|
||||
id: 'c',
|
||||
value: {
|
||||
type: 'rawscript',
|
||||
content:
|
||||
'export async function main() {\n return "Entered the condition!";\n}\n',
|
||||
language: RawScript.language.DENO,
|
||||
input_transforms: {}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
default: [
|
||||
{
|
||||
id: 'b',
|
||||
value: {
|
||||
type: 'rawscript',
|
||||
content: '# import wmill\n\n\ndef main():\n return "Default Branch"',
|
||||
language: RawScript.language.PYTHON3,
|
||||
input_transforms: {}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
$flowStore = $flowStore
|
||||
dispatch('reload')
|
||||
|
||||
tick().then(() => {
|
||||
branchOneTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
element: '#flow-editor-test-flow',
|
||||
popover: {
|
||||
title: 'Test your flow',
|
||||
description: 'We can now test our flow',
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('#flow-editor-test-flow')
|
||||
|
||||
setTimeout(() => {
|
||||
branchOneTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
element: '#flow-editor-test-flow-drawer',
|
||||
popover: {
|
||||
title: 'Test your flow',
|
||||
description: 'Finally we can test our flow, and view the results!',
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('#flow-editor-test-flow-drawer')
|
||||
|
||||
setTimeout(() => {
|
||||
branchOneTutorial.moveNext()
|
||||
updateProgress(2)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
branchOneTutorial.drive()
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if shouldRenderButton}
|
||||
<TutorialItem
|
||||
on:click={() => runTutorial()}
|
||||
label="Branch one tutorial"
|
||||
index={2}
|
||||
id="flow-builder-tutorial-branchone"
|
||||
/>
|
||||
{/if}
|
||||
@@ -0,0 +1,317 @@
|
||||
<script lang="ts">
|
||||
import { driver } from 'driver.js'
|
||||
import 'driver.js/dist/driver.css'
|
||||
import { createEventDispatcher, getContext, tick } from 'svelte'
|
||||
import type { FlowEditorContext } from '../flows/types'
|
||||
import { updateProgress } from '$lib/tutorialUtils'
|
||||
import TutorialItem from './TutorialItem.svelte'
|
||||
import {
|
||||
clickButtonBySelector,
|
||||
selectFlowStepKind,
|
||||
setInputBySelector,
|
||||
triggerAddFlowStep
|
||||
} from './utils'
|
||||
import { tutorialsToDo } from '$lib/stores'
|
||||
|
||||
const { flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const queue: string[] = []
|
||||
|
||||
const simpleFlowTutorial = driver({
|
||||
showProgress: true,
|
||||
allowClose: true,
|
||||
onPopoverRender: (popover, { config, state }) => {
|
||||
if (state.activeIndex == 0) {
|
||||
const skipThisButton = document.createElement('button')
|
||||
skipThisButton.innerText = 'Skip this'
|
||||
skipThisButton.addEventListener('click', () => {
|
||||
updateProgress(0)
|
||||
simpleFlowTutorial.destroy()
|
||||
})
|
||||
skipThisButton.setAttribute(
|
||||
'class',
|
||||
'border px-2 py-1 !text-xs font-normal rounded-md hover:bg-surface-hover transition-all flex items-center'
|
||||
)
|
||||
|
||||
const skipAllButton = document.createElement('button')
|
||||
skipAllButton.innerText = 'Skip all'
|
||||
skipAllButton.addEventListener('click', () => {
|
||||
dispatch('skipAll')
|
||||
simpleFlowTutorial.destroy()
|
||||
})
|
||||
|
||||
skipAllButton.setAttribute(
|
||||
'class',
|
||||
'border px-2 py-1 !text-xs font-normal rounded-md hover:bg-surface-hover transition-all flex items-center'
|
||||
)
|
||||
|
||||
const popoverDescription = document.querySelector('#driver-popover-description')
|
||||
|
||||
const div = document.createElement('div')
|
||||
|
||||
div.setAttribute('class', 'flex flex-row gap-2')
|
||||
|
||||
if (popoverDescription) {
|
||||
div.appendChild(skipThisButton)
|
||||
div.appendChild(skipAllButton)
|
||||
|
||||
popoverDescription.appendChild(div)
|
||||
}
|
||||
}
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
popover: {
|
||||
title: 'Welcome in the Windmil Flow editor',
|
||||
description: 'Learn how to build powerful flows in a few steps'
|
||||
}
|
||||
},
|
||||
{
|
||||
popover: {
|
||||
title: 'Flows inputs',
|
||||
description: 'Flows have inputs that can be used in the flow',
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('#flow-editor-virtual-Input')
|
||||
setTimeout(() => {
|
||||
simpleFlowTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
},
|
||||
element: '#svelvet-Input'
|
||||
},
|
||||
{
|
||||
element: '#flow-editor-add-property',
|
||||
popover: {
|
||||
title: 'Add a property',
|
||||
description: 'Click here to add a property to your schema',
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('#flow-editor-add-property')
|
||||
|
||||
setTimeout(() => {
|
||||
simpleFlowTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#schema-modal-name',
|
||||
popover: {
|
||||
title: 'Name your property',
|
||||
description: 'Give a name to your property. Here we will call it firstname',
|
||||
onNextClick: () => {
|
||||
setInputBySelector('#schema-modal-name', 'firstname')
|
||||
|
||||
simpleFlowTutorial.moveNext()
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#schema-modal-save',
|
||||
popover: {
|
||||
title: 'Save your property',
|
||||
description: 'Click here to save your property',
|
||||
onNextClick: () => {
|
||||
queue.push(JSON.stringify($flowStore))
|
||||
|
||||
clickButtonBySelector('#schema-modal-save')
|
||||
|
||||
setTimeout(() => {
|
||||
simpleFlowTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#flow-editor-add-step-0',
|
||||
popover: {
|
||||
title: 'Add a step',
|
||||
description: 'Click here to add a step to your flow',
|
||||
onNextClick: () => {
|
||||
triggerAddFlowStep(0)
|
||||
|
||||
setTimeout(() => {
|
||||
simpleFlowTutorial.moveNext()
|
||||
})
|
||||
},
|
||||
|
||||
onPrevClick: () => {
|
||||
simpleFlowTutorial.moveTo(1)
|
||||
debugger
|
||||
$flowStore = JSON.parse(queue.pop() || '{}')
|
||||
dispatch('reload')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
popover: {
|
||||
title: 'Steps kind',
|
||||
description: "Choose the kind of step you want to add. Let's start with a simple action"
|
||||
},
|
||||
element: '#flow-editor-insert-module'
|
||||
},
|
||||
{
|
||||
popover: {
|
||||
title: 'Pick an action',
|
||||
description: 'Let’s pick an action to add to your flow',
|
||||
onNextClick: () => {
|
||||
selectFlowStepKind(1)
|
||||
|
||||
setTimeout(() => {
|
||||
simpleFlowTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
},
|
||||
element: '#flow-editor-insert-module > div > button:nth-child(1)'
|
||||
},
|
||||
{
|
||||
element: '#flow-editor-flow-inputs',
|
||||
popover: {
|
||||
title: 'Action configuration',
|
||||
description: 'An action can be inlined, imported from your workspace or the Hub.'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#flow-editor-action-script',
|
||||
popover: {
|
||||
title: 'Supported languages',
|
||||
description: 'Windmill support the following languages/runtimes.'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#flow-editor-action-script > button:nth-child(1)',
|
||||
popover: {
|
||||
title: 'Typescript',
|
||||
description: "Let's pick an action to add to your flow",
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('#flow-editor-action-script > button > div > button:nth-child(1)')
|
||||
|
||||
setTimeout(() => {
|
||||
simpleFlowTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
element: '#flow-editor-editor',
|
||||
popover: {
|
||||
title: 'Action editor',
|
||||
description: 'Windmill provides a full code editor to write your actions'
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
element: '#flow-editor-step-input',
|
||||
popover: {
|
||||
title: 'Autogenerated schema',
|
||||
description: 'The schema and the UI is autogenerated from your code'
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
element: '#flow-editor-plug',
|
||||
popover: {
|
||||
title: 'Connect',
|
||||
description:
|
||||
'You can provide static values or connect to other nodes result. Here we will connect to the firstname input',
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('#flow-editor-plug')
|
||||
|
||||
setTimeout(() => {
|
||||
simpleFlowTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '.key',
|
||||
popover: {
|
||||
title: 'Connection mode',
|
||||
description: 'Once you pressed the connect button, you can choose what to connect to.',
|
||||
onNextClick: () => {
|
||||
if ($flowStore.value.modules[0].value.type === 'rawscript') {
|
||||
$flowStore.value.modules[0].value.input_transforms = {
|
||||
x: {
|
||||
type: 'javascript',
|
||||
expr: 'flow_input.firstname'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$flowStore = $flowStore
|
||||
dispatch('reload')
|
||||
|
||||
tick().then(() => {
|
||||
simpleFlowTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
element: '#flow-editor-step-input',
|
||||
popover: {
|
||||
title: 'Input connected!',
|
||||
description: 'The input is now connected to the firstname input'
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
element: '#flow-editor-test-flow',
|
||||
popover: {
|
||||
title: 'Test your flow',
|
||||
description: 'We can now test our flow',
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('#flow-editor-test-flow')
|
||||
|
||||
setTimeout(() => {
|
||||
simpleFlowTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
element: '#flow-preview-content',
|
||||
popover: {
|
||||
title: 'Flow input',
|
||||
description: 'Let’s provide an input to our flow',
|
||||
onNextClick: () => {
|
||||
setInputBySelector('textarea.w-full', 'Hello World!')
|
||||
|
||||
setTimeout(() => {
|
||||
simpleFlowTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#flow-editor-test-flow-drawer',
|
||||
popover: {
|
||||
title: 'Test your flow',
|
||||
description: 'Finally we can test our flow, and view the results!',
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('#flow-editor-test-flow-drawer')
|
||||
|
||||
setTimeout(() => {
|
||||
simpleFlowTutorial.moveNext()
|
||||
|
||||
updateProgress(0)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
$: $tutorialsToDo.includes(0) && simpleFlowTutorial.drive()
|
||||
</script>
|
||||
|
||||
<TutorialItem
|
||||
on:click={() => simpleFlowTutorial.drive()}
|
||||
label="Simple flow tutorial"
|
||||
index={0}
|
||||
id="flow-builder-tutorial-action"
|
||||
/>
|
||||
@@ -0,0 +1,415 @@
|
||||
<script lang="ts">
|
||||
import { driver } from 'driver.js'
|
||||
import 'driver.js/dist/driver.css'
|
||||
import { createEventDispatcher, getContext, tick } from 'svelte'
|
||||
import type { FlowEditorContext } from '../flows/types'
|
||||
import TutorialItem from './TutorialItem.svelte'
|
||||
import { emptyFlowModuleState } from '../flows/utils'
|
||||
import {
|
||||
clickButtonBySelector,
|
||||
setInputBySelector,
|
||||
triggerAddFlowStep,
|
||||
selectFlowStepKind,
|
||||
selectOptionsBySelector
|
||||
} from './utils'
|
||||
import { updateProgress } from '$lib/tutorialUtils'
|
||||
|
||||
export let shouldRenderButton: boolean = true
|
||||
|
||||
const { flowStore, selectedId, flowStateStore } =
|
||||
getContext<FlowEditorContext>('FlowEditorContext')
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export function runTutorial() {
|
||||
const forloopTutorial = driver({
|
||||
showProgress: true,
|
||||
allowClose: true,
|
||||
onPopoverRender: (popover, { config, state }) => {
|
||||
if (state.activeIndex == 0) {
|
||||
const skipThisButton = document.createElement('button')
|
||||
skipThisButton.innerText = 'Skip this'
|
||||
skipThisButton.addEventListener('click', () => {
|
||||
updateProgress(1)
|
||||
forloopTutorial.destroy()
|
||||
})
|
||||
skipThisButton.setAttribute(
|
||||
'class',
|
||||
'border px-2 py-1 !text-xs font-normal rounded-md hover:bg-surface-hover transition-all flex items-center'
|
||||
)
|
||||
|
||||
const skipAllButton = document.createElement('button')
|
||||
skipAllButton.innerText = 'Skip all'
|
||||
skipAllButton.addEventListener('click', () => {
|
||||
dispatch('skipAll')
|
||||
forloopTutorial.destroy()
|
||||
})
|
||||
|
||||
skipAllButton.setAttribute(
|
||||
'class',
|
||||
'border px-2 py-1 !text-xs font-normal rounded-md hover:bg-surface-hover transition-all flex items-center'
|
||||
)
|
||||
|
||||
const popoverDescription = document.querySelector('#driver-popover-description')
|
||||
|
||||
const div = document.createElement('div')
|
||||
|
||||
div.setAttribute('class', 'flex flex-row gap-2')
|
||||
|
||||
if (popoverDescription) {
|
||||
div.appendChild(skipThisButton)
|
||||
div.appendChild(skipAllButton)
|
||||
|
||||
popoverDescription.appendChild(div)
|
||||
}
|
||||
}
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
popover: {
|
||||
title: 'Welcome in the Windmil Flow editor',
|
||||
description: 'Learn how to build our first for loop to iterate on'
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
popover: {
|
||||
title: 'Flows inputs',
|
||||
description: 'Flows have inputs that can be used in the flow',
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('#flow-editor-virtual-Input')
|
||||
|
||||
setTimeout(() => {
|
||||
forloopTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
},
|
||||
element: '#svelvet-Input'
|
||||
},
|
||||
{
|
||||
element: '#flow-editor-add-property',
|
||||
popover: {
|
||||
title: 'Add a property',
|
||||
description: 'Click here to add a property to your schema',
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('#flow-editor-add-property')
|
||||
|
||||
setTimeout(() => {
|
||||
forloopTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#schema-modal-name',
|
||||
popover: {
|
||||
title: 'Name your property',
|
||||
description: 'Give a name to your property. Here we will call it firstname',
|
||||
onNextClick: () => {
|
||||
setInputBySelector('#schema-modal-name', 'array')
|
||||
forloopTutorial.moveNext()
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#schema-modal-type-array',
|
||||
popover: {
|
||||
title: 'Property type',
|
||||
description: 'Choose the type of your property. Here we will choose array',
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('#schema-modal-type-array')
|
||||
forloopTutorial.moveNext()
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#array-type-narrowing',
|
||||
popover: {
|
||||
title: 'Array type narrowing',
|
||||
description: 'You can narrow the type of your array. Here we will choose numbers',
|
||||
onNextClick: () => {
|
||||
selectOptionsBySelector('#array-type-narrowing', 'number')
|
||||
forloopTutorial.moveNext()
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#schema-modal-save',
|
||||
popover: {
|
||||
title: 'Save your property',
|
||||
description: 'Click here to save your property',
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('#schema-modal-save')
|
||||
|
||||
setTimeout(() => {
|
||||
forloopTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#flow-editor-add-step-0',
|
||||
popover: {
|
||||
title: 'Add a step',
|
||||
description: 'Click here to add a step to your flow',
|
||||
onNextClick: () => {
|
||||
triggerAddFlowStep(0)
|
||||
|
||||
setTimeout(() => {
|
||||
forloopTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
popover: {
|
||||
title: 'Steps kind',
|
||||
description: "Choose the kind of step you want to add. Let's start with a simple action"
|
||||
},
|
||||
element: '#flow-editor-insert-module'
|
||||
},
|
||||
|
||||
{
|
||||
popover: {
|
||||
title: 'Insert loop',
|
||||
description: "Let's pick forloop",
|
||||
onNextClick: () => {
|
||||
selectFlowStepKind(4)
|
||||
setTimeout(() => {
|
||||
forloopTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
},
|
||||
element: '#flow-editor-insert-module > div > button:nth-child(4)'
|
||||
},
|
||||
|
||||
{
|
||||
element: '#flow-editor-iterator-expression',
|
||||
popover: {
|
||||
title: 'Iterator expression',
|
||||
description:
|
||||
'The iterator expression is a javascript expression that respresents the array to iterate on. Here we will iterate on the firstname input letter by letter',
|
||||
onNextClick: () => {
|
||||
if ($flowStore.value.modules[0].value.type === 'forloopflow') {
|
||||
if ($flowStore.value.modules[0].value.iterator.type === 'javascript') {
|
||||
$flowStore.value.modules[0].value.iterator.expr = 'flow_input.array'
|
||||
}
|
||||
}
|
||||
|
||||
$flowStore = $flowStore
|
||||
|
||||
dispatch('reload')
|
||||
|
||||
tick().then(() => {
|
||||
forloopTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
element: '#flow-editor-iterator-expression',
|
||||
popover: {
|
||||
title: 'Iterator expression',
|
||||
description:
|
||||
'We can refer to the result of previous steps using the results object: results.a',
|
||||
onNextClick: () => {
|
||||
if ($flowStore.value.modules[0].value.type === 'forloopflow') {
|
||||
$flowStore.value.modules[0].value.modules = [
|
||||
{
|
||||
id: 'b',
|
||||
value: {
|
||||
type: 'rawscript',
|
||||
content: 'def main(x: int):\n return x*2',
|
||||
// @ts-ignore
|
||||
language: 'python3',
|
||||
input_transforms: {
|
||||
x: {
|
||||
type: 'javascript',
|
||||
// @ts-ignore
|
||||
value: '',
|
||||
expr: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
$flowStateStore['b'] = emptyFlowModuleState()
|
||||
|
||||
$flowStateStore['b'].schema.properties = {
|
||||
x: {
|
||||
type: 'number',
|
||||
description: '',
|
||||
default: null
|
||||
}
|
||||
}
|
||||
|
||||
$flowStore = $flowStore
|
||||
|
||||
dispatch('reload')
|
||||
|
||||
tick().then(() => {
|
||||
forloopTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#svelvet-c',
|
||||
popover: {
|
||||
title: 'Step of the loop',
|
||||
description: 'We added an action to the loop. Let’s configure it',
|
||||
onNextClick: () => {
|
||||
$selectedId = 'b'
|
||||
$flowStore = $flowStore
|
||||
|
||||
dispatch('reload')
|
||||
tick().then(() => {
|
||||
forloopTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#flow-editor-editor',
|
||||
popover: {
|
||||
title: 'Python step',
|
||||
description:
|
||||
'We can write python code in the editor. In this example we will capitalize the letter'
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
element: '#flow-editor-step-input',
|
||||
popover: {
|
||||
title: 'flow editor',
|
||||
description: 'Description'
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
element: '#flow-editor-plug',
|
||||
popover: {
|
||||
title: 'Input configuration',
|
||||
description:
|
||||
'UI is autogenerated from your code. Let’s connect the input to the letter input',
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('#flow-editor-plug')
|
||||
|
||||
setTimeout(() => {
|
||||
forloopTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '.key',
|
||||
popover: {
|
||||
title: 'Connect',
|
||||
description: 'As we did before, we can connect to the iterator of the loop',
|
||||
onNextClick: () => {
|
||||
if (
|
||||
$flowStore.value.modules[0].value.type === 'forloopflow' &&
|
||||
$flowStore.value.modules[0].value.modules[0].value.type === 'rawscript'
|
||||
) {
|
||||
$flowStore.value.modules[0].value.modules[0].value.input_transforms = {
|
||||
x: {
|
||||
type: 'javascript',
|
||||
expr: 'flow_input.iter.value'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$flowStore = $flowStore
|
||||
dispatch('reload')
|
||||
|
||||
tick().then(() => {
|
||||
forloopTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#flow-editor-step-input',
|
||||
popover: {
|
||||
title: 'Iterator',
|
||||
description:
|
||||
'Loops expose an iterator object that contains the current value of the loop and the index'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#flow-editor-test-flow',
|
||||
popover: {
|
||||
title: 'Test your flow',
|
||||
description: 'We can now test our flow',
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('#flow-editor-test-flow')
|
||||
|
||||
setTimeout(() => {
|
||||
forloopTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
element: 'arg-input-add-item',
|
||||
popover: {
|
||||
title: 'Flow input',
|
||||
description: 'Let’s add an item to our array',
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('#arg-input-add-item')
|
||||
|
||||
setTimeout(() => {
|
||||
forloopTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
element: 'arg-input-add-item',
|
||||
popover: {
|
||||
title: 'Flow input',
|
||||
description: 'We can set the value of the item',
|
||||
onNextClick: () => {
|
||||
setInputBySelector('#arg-input-number-array', '25')
|
||||
|
||||
setTimeout(() => {
|
||||
forloopTutorial.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
element: '#flow-editor-test-flow-drawer',
|
||||
popover: {
|
||||
title: 'Test your flow',
|
||||
description: 'Finally we can test our flow, and view the results!',
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('#flow-editor-test-flow-drawer')
|
||||
|
||||
setTimeout(() => {
|
||||
forloopTutorial.moveNext()
|
||||
updateProgress(1)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
forloopTutorial.drive()
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if shouldRenderButton}
|
||||
<TutorialItem
|
||||
on:click={() => runTutorial()}
|
||||
label="For loops tutorial"
|
||||
index={1}
|
||||
id="flow-builder-tutorial-forloops"
|
||||
/>
|
||||
{/if}
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils'
|
||||
import { MenuItem } from '@rgossiaux/svelte-headlessui'
|
||||
|
||||
import { tutorialsToDo } from '$lib/stores'
|
||||
import { CheckCircle, Circle } from 'lucide-svelte'
|
||||
|
||||
export let label: string
|
||||
export let index: number
|
||||
export let id: string
|
||||
</script>
|
||||
|
||||
<MenuItem on:click>
|
||||
<div
|
||||
{id}
|
||||
class={classNames(
|
||||
'text-primary flex flex-row items-center text-left px-4 py-2 gap-2 cursor-pointer hover:bg-surface-hover !text-xs font-semibold'
|
||||
)}
|
||||
>
|
||||
{#if $tutorialsToDo.includes(index)}
|
||||
<Circle size={16} />
|
||||
{:else}
|
||||
<CheckCircle size={16} color="green" />
|
||||
{/if}
|
||||
{label}
|
||||
</div>
|
||||
</MenuItem>
|
||||
@@ -0,0 +1,43 @@
|
||||
export function setInputBySelector(selector: string, value: string) {
|
||||
const input = document.querySelector(selector) as HTMLInputElement
|
||||
|
||||
if (input) {
|
||||
input.value = value
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
}
|
||||
}
|
||||
|
||||
export function clickButtonBySelector(selector: string) {
|
||||
const button = document.querySelector(selector) as HTMLButtonElement
|
||||
|
||||
if (button) {
|
||||
button.click()
|
||||
}
|
||||
}
|
||||
|
||||
export function triggerAddFlowStep(index: number) {
|
||||
const button = document.querySelector(`#flow-editor-add-step-${index}`) as HTMLButtonElement
|
||||
|
||||
if (button) {
|
||||
button.parentElement?.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
|
||||
}
|
||||
}
|
||||
|
||||
export function selectFlowStepKind(index: number) {
|
||||
const button = document.querySelector(
|
||||
`#flow-editor-insert-module > div > button:nth-child(${index})`
|
||||
) as HTMLButtonElement
|
||||
|
||||
if (button) {
|
||||
button?.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
|
||||
}
|
||||
}
|
||||
|
||||
export function selectOptionsBySelector(selector: string, value: string) {
|
||||
const select = document.querySelector(selector) as HTMLSelectElement
|
||||
|
||||
if (select) {
|
||||
select.value = value
|
||||
select.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
}
|
||||
@@ -1,52 +1,51 @@
|
||||
import { get } from "svelte/store";
|
||||
import { tutorialsToDo } from "./stores";
|
||||
import { UserService } from "./gen";
|
||||
import { get } from 'svelte/store'
|
||||
import { tutorialsToDo } from './stores'
|
||||
import { UserService } from './gen'
|
||||
|
||||
const MAX_TUTORIAL_ID = 6
|
||||
|
||||
export async function updateProgress(id: number) {
|
||||
const bef = get(tutorialsToDo)
|
||||
const aft = bef.filter((x) => x != id)
|
||||
tutorialsToDo.set(aft)
|
||||
let bits = 0
|
||||
for (let i = 0; i <= MAX_TUTORIAL_ID; i++) {
|
||||
let mask = 1 << i;
|
||||
if (!aft.includes(i)) {
|
||||
bits = bits | mask
|
||||
}
|
||||
}
|
||||
await UserService.updateTutorialProgress({requestBody: { progress: bits}})
|
||||
|
||||
const bef = get(tutorialsToDo)
|
||||
const aft = bef.filter((x) => x != id)
|
||||
tutorialsToDo.set(aft)
|
||||
let bits = 0
|
||||
for (let i = 0; i <= MAX_TUTORIAL_ID; i++) {
|
||||
let mask = 1 << i
|
||||
if (!aft.includes(i)) {
|
||||
bits = bits | mask
|
||||
}
|
||||
}
|
||||
await UserService.updateTutorialProgress({ requestBody: { progress: bits } })
|
||||
}
|
||||
|
||||
export async function skipAllTodos() {
|
||||
let bits = 0
|
||||
for (let i = 0; i <= MAX_TUTORIAL_ID; i++) {
|
||||
let mask = 1 << i;
|
||||
bits = bits | mask
|
||||
}
|
||||
tutorialsToDo.set([])
|
||||
await UserService.updateTutorialProgress({requestBody: { progress: bits}})
|
||||
|
||||
let bits = 0
|
||||
for (let i = 0; i <= MAX_TUTORIAL_ID; i++) {
|
||||
let mask = 1 << i
|
||||
bits = bits | mask
|
||||
}
|
||||
tutorialsToDo.set([])
|
||||
await UserService.updateTutorialProgress({ requestBody: { progress: bits } })
|
||||
}
|
||||
|
||||
export async function resetAllTodos() {
|
||||
let todos: number[] = []
|
||||
for (let i = 0; i <= MAX_TUTORIAL_ID; i++) {
|
||||
todos.push(i)
|
||||
}
|
||||
tutorialsToDo.set(todos)
|
||||
await UserService.updateTutorialProgress({requestBody: { progress: 0 }})
|
||||
let todos: number[] = []
|
||||
for (let i = 0; i <= MAX_TUTORIAL_ID; i++) {
|
||||
todos.push(i)
|
||||
}
|
||||
tutorialsToDo.set(todos)
|
||||
|
||||
await UserService.updateTutorialProgress({ requestBody: { progress: 0 } })
|
||||
}
|
||||
|
||||
export async function syncTutorialsTodos() {
|
||||
const bits: number = (await UserService.getTutorialProgress()).progress!
|
||||
const todos: number[] = []
|
||||
for (let i = 0; i <= MAX_TUTORIAL_ID; i++) {
|
||||
let mask = 1 << i;
|
||||
if ((bits & mask) == 0) {
|
||||
todos.push(i)
|
||||
}
|
||||
}
|
||||
tutorialsToDo.set(todos)
|
||||
}
|
||||
const bits: number = (await UserService.getTutorialProgress()).progress!
|
||||
const todos: number[] = []
|
||||
for (let i = 0; i <= MAX_TUTORIAL_ID; i++) {
|
||||
let mask = 1 << i
|
||||
if ((bits & mask) == 0) {
|
||||
todos.push(i)
|
||||
}
|
||||
}
|
||||
tutorialsToDo.set(todos)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user