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:
Faton Ramadani
2023-10-04 10:12:07 +02:00
committed by GitHub
parent 8319f5d0e5
commit 33b1c752ae
29 changed files with 1800 additions and 297 deletions
+11
View File
@@ -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",
+1
View File
@@ -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",
+2 -1
View File
@@ -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>
+129 -119
View File
@@ -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&nbsp;<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&nbsp;<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 &nbsp;<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} /> &rightarrow;</Button
}}
id="flow-editor-plug"
>
<Plug size={16} /> &rightarrow;
</Button>
</div>
</div>
{/if}
@@ -272,6 +272,7 @@
on:click={() => {
schemaModal.openDrawer(Object.assign({}, DEFAULT_PROPERTY))
}}
id="flow-editor-add-property"
>
Add Property
</Button>
+11 -9
View File
@@ -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: 'Lets 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: 'Lets 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. Lets 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. Lets 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: 'Lets 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 }))
}
}
+38 -39
View File
@@ -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)
}