Compare commits

...

6 Commits

Author SHA1 Message Date
Ádám Kovács
b76426ba83 faet(frontend): Add tour manager store 2023-03-10 20:11:25 +01:00
Ádám Kovács
1cb2a177c6 Merge branch 'main' into tutorials 2023-03-10 19:13:04 +01:00
Ádám Kovács
db87577a1b feat(frontend): Add app editor tour 2023-03-10 19:11:34 +01:00
Ádám Kovács
c219eb0ee9 feat(frontend): Add flow editor tour 2023-03-10 18:36:53 +01:00
Ádám Kovács
bfe5b56c99 feat(frontend): Add script editor tour 2023-03-10 09:38:34 +01:00
Ádám Kovács
4453690521 feat(frontend): Add onboarding tour to home page 2023-03-09 20:11:27 +01:00
29 changed files with 869 additions and 105 deletions

View File

@@ -64,6 +64,7 @@
"postcss-load-config": "^4.0.1",
"prettier": "^2.8.3",
"prettier-plugin-svelte": "^2.9.0",
"shepherd.js": "^10.0.1",
"simple-svelte-autocomplete": "^2.5.1",
"stylelint-config-recommended": "^9.0.0",
"svelte": "^3.55.1",
@@ -6653,6 +6654,24 @@
"node": ">=8"
}
},
"node_modules/shepherd.js": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/shepherd.js/-/shepherd.js-10.0.1.tgz",
"integrity": "sha512-R32v4b4b0N1gK/vxvRtdhty+SBZlBPcPTSYCwcaAQvFd0n6Xki7cRH6Sx0oD9WB/HCkqCNM1msyIyylXmq635w==",
"dev": true,
"dependencies": {
"@popperjs/core": "^2.11.5",
"deepmerge": "^4.2.2",
"smoothscroll-polyfill": "^0.4.4"
},
"engines": {
"node": "12.* || 14.* || >= 16"
},
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/rwwagner90"
}
},
"node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
@@ -6739,6 +6758,12 @@
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/smoothscroll-polyfill": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/smoothscroll-polyfill/-/smoothscroll-polyfill-0.4.4.tgz",
"integrity": "sha512-TK5ZA9U5RqCwMpfoMq/l1mrH0JAR7y7KRvOBx0n2869aLxch+gT9GhN3yUfjiw+d/DiF1mKo14+hd62JyMmoBg==",
"dev": true
},
"node_modules/sorcery": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz",
@@ -12989,6 +13014,17 @@
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true
},
"shepherd.js": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/shepherd.js/-/shepherd.js-10.0.1.tgz",
"integrity": "sha512-R32v4b4b0N1gK/vxvRtdhty+SBZlBPcPTSYCwcaAQvFd0n6Xki7cRH6Sx0oD9WB/HCkqCNM1msyIyylXmq635w==",
"dev": true,
"requires": {
"@popperjs/core": "^2.11.5",
"deepmerge": "^4.2.2",
"smoothscroll-polyfill": "^0.4.4"
}
},
"signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
@@ -13049,6 +13085,12 @@
"is-fullwidth-code-point": "^3.0.0"
}
},
"smoothscroll-polyfill": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/smoothscroll-polyfill/-/smoothscroll-polyfill-0.4.4.tgz",
"integrity": "sha512-TK5ZA9U5RqCwMpfoMq/l1mrH0JAR7y7KRvOBx0n2869aLxch+gT9GhN3yUfjiw+d/DiF1mKo14+hd62JyMmoBg==",
"dev": true
},
"sorcery": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz",

View File

@@ -43,6 +43,7 @@
"postcss-load-config": "^4.0.1",
"prettier": "^2.8.3",
"prettier-plugin-svelte": "^2.9.0",
"shepherd.js": "^10.0.1",
"simple-svelte-autocomplete": "^2.5.1",
"stylelint-config-recommended": "^9.0.0",
"svelte": "^3.55.1",

View File

@@ -232,8 +232,14 @@
<Badge color={validCode ? 'green' : 'red'} class="min-w-[60px] mr-3">
{validCode ? 'Valid' : 'Invalid'}
</Badge>
<div class="flex items-center divide-x">
<Popover notClickable placement="bottom" disapperTimoout={0} class="pr-1" disablePopup={!iconOnly}>
<div id="script-tutorial-3" class="flex items-center divide-x">
<Popover
notClickable
placement="bottom"
disapperTimoout={0}
class="pr-1"
disablePopup={!iconOnly}
>
<Button
color="light"
btnClasses="!font-medium !h-full"
@@ -245,11 +251,15 @@
>
+Context Var
</Button>
<svelte:fragment slot="text">
Add context variable
</svelte:fragment>
<svelte:fragment slot="text">Add context variable</svelte:fragment>
</Popover>
<Popover notClickable placement="bottom" disapperTimoout={0} class="px-1" disablePopup={!iconOnly}>
<Popover
notClickable
placement="bottom"
disapperTimoout={0}
class="px-1"
disablePopup={!iconOnly}
>
<Button
color="light"
btnClasses="!font-medium !h-full"
@@ -261,11 +271,15 @@
>
+Variable
</Button>
<svelte:fragment slot="text">
Add variable
</svelte:fragment>
<svelte:fragment slot="text">Add variable</svelte:fragment>
</Popover>
<Popover notClickable placement="bottom" disapperTimoout={0} class="px-1" disablePopup={!iconOnly}>
<Popover
notClickable
placement="bottom"
disapperTimoout={0}
class="px-1"
disablePopup={!iconOnly}
>
<Button
btnClasses="!font-medium !h-full"
size="xs"
@@ -277,11 +291,15 @@
>
+Resource
</Button>
<svelte:fragment slot="text">
Add resource
</svelte:fragment>
<svelte:fragment slot="text">Add resource</svelte:fragment>
</Popover>
<Popover notClickable placement="bottom" disapperTimoout={0} class="px-1" disablePopup={!iconOnly}>
<Popover
notClickable
placement="bottom"
disapperTimoout={0}
class="px-1"
disablePopup={!iconOnly}
>
<Button
btnClasses="!font-medium !h-full"
size="xs"
@@ -293,11 +311,15 @@
>
Reset
</Button>
<svelte:fragment slot="text">
Reset
</svelte:fragment>
<svelte:fragment slot="text">Reset</svelte:fragment>
</Popover>
<Popover notClickable placement="bottom" disapperTimoout={0} class="px-1" disablePopup={!iconOnly}>
<Popover
notClickable
placement="bottom"
disapperTimoout={0}
class="px-1"
disablePopup={!iconOnly}
>
<Button
btnClasses="!font-medium !h-full"
size="xs"
@@ -320,11 +342,15 @@
{/if}
</span>
</Button>
<svelte:fragment slot="text">
Reload assistant
</svelte:fragment>
<svelte:fragment slot="text">Reload assistant</svelte:fragment>
</Popover>
<Popover notClickable placement="bottom" disapperTimoout={0} class="px-1" disablePopup={!iconOnly}>
<Popover
notClickable
placement="bottom"
disapperTimoout={0}
class="px-1"
disablePopup={!iconOnly}
>
<Button
btnClasses="!font-medium"
size="xs"
@@ -342,7 +368,13 @@
</Popover>
</div>
</div>
<Popover notClickable placement="bottom" disapperTimoout={0} class="px-1" disablePopup={!iconOnly}>
<Popover
notClickable
placement="bottom"
disapperTimoout={0}
class="px-1"
disablePopup={!iconOnly}
>
<Button
btnClasses="!font-medium"
size="xs"
@@ -354,9 +386,7 @@
>
Script
</Button>
<svelte:fragment slot="text">
Script
</svelte:fragment>
<svelte:fragment slot="text">Script</svelte:fragment>
</Popover>
</div>

View File

@@ -20,6 +20,7 @@
import { loadFlowSchedule, type Schedule } from './flows/scheduleUtils'
import type { FlowEditorContext } from './flows/types'
import { cleanInputs } from './flows/utils'
import { Tour } from './tutorial'
export let initialPath: string = ''
export let selectedId: string | undefined
@@ -27,6 +28,7 @@
export let loading = false
export let flowStore: Writable<Flow>
export let flowStateStore: Writable<FlowState>
export let tour = false
async function createSchedule(path: string) {
const { cron, args, enabled } = $scheduleStore
@@ -246,6 +248,10 @@
<svelte:window on:keydown={onKeyDown} />
{#if tour}
<Tour tutorial="flow" />
{/if}
{#if !$userStore?.operator}
<ScriptEditorDrawer bind:this={$scriptEditorDrawer} />

View File

@@ -346,6 +346,7 @@
lang={script.language}
{initialArgs}
{kind}
tour
/>
{:else if step === 3}
<CenteredPage>

View File

@@ -17,6 +17,7 @@
import { Button, Kbd } from './common'
import SplitPanesWrapper from './splitPanes/SplitPanesWrapper.svelte'
import WindmillIcon from './icons/WindmillIcon.svelte'
import { Tour } from './tutorial'
// Exported
export let schema: Schema = emptySchema()
@@ -27,6 +28,7 @@
export let initialArgs: Record<string, any> = {}
export let fixedOverflowWidgets = true
export let noSyncFromGithub = false
export let tour = false
let websocketAlive = { pyright: false, black: false, deno: false, go: false }
@@ -114,6 +116,10 @@
<svelte:window on:keydown={onKeyDown} />
{#if tour}
<Tour tutorial="script" />
{/if}
<div class="border-b-2 shadow-sm px-1 pr-4" bind:clientWidth={width}>
<div class="flex justify-between space-x-2">
<EditorBar
@@ -146,6 +152,7 @@
<Splitpanes class="!overflow-visible">
<Pane size={60} minSize={10} class="!overflow-visible">
<div
id="script-tutorial-1"
class="pl-2 h-full !overflow-visible"
on:mouseleave={() => {
inferSchema(code)
@@ -180,7 +187,7 @@
</Pane>
<Pane size={40} minSize={10}>
<div class="flex flex-col h-full">
<div class="px-2 w-full border-b py-1">
<div id="script-tutorial-4" class="px-2 w-full border-b py-1">
{#if testIsLoading}
<Button on:click={testJobLoader?.cancelJob} btnClasses="w-full" color="red" size="xs">
<WindmillIcon
@@ -211,7 +218,7 @@
</div>
<Splitpanes horizontal class="!max-h-[calc(100%-43px)]">
<Pane size={33}>
<div class="px-2">
<div id="script-tutorial-2" class="px-2">
<div class="break-words relative font-sans">
<SchemaForm compact {schema} bind:args bind:isValid />
</div>

View File

@@ -16,7 +16,7 @@
const dispatch = createEventDispatcher()
</script>
<span class="{$$props.class} z-auto">
<span class="{$$props.class ?? ''} z-auto">
<label
for={id}
class="inline-flex items-center mt-2 duration-200 {disabled

View File

@@ -37,6 +37,7 @@
import { page } from '$app/stores'
import CssSettings from './componentsPanel/CssSettings.svelte'
import { initHistory } from '$lib/history'
import { Tour } from '../../tutorial'
import ComponentNavigation from './component/ComponentNavigation.svelte'
import ItemPicker from '$lib/components/ItemPicker.svelte'
import VariableEditor from '$lib/components/VariableEditor.svelte'
@@ -47,6 +48,7 @@
export let policy: Policy
export let summary: string
export let fromHub: boolean = false
export let tour: boolean = false
const appStore = writable<App>(app)
const worldStore = writable<World | undefined>(undefined)
@@ -152,6 +154,10 @@
<svelte:window on:hashchange={hashchange} />
{#if tour}
<Tour tutorial="app" />
{/if}
{#if $connectingInput.opened}
<div
class="absolute w-full h-screen bg-black border-2 bg-opacity-25 z-20 flex justify-center items-center"
@@ -196,6 +202,7 @@
$selectedComponent = undefined
$focusedGrid = undefined
}}
id="app-tutorial-3"
class={twMerge(
'bg-gray-100 h-full w-full',
$appStore.css?.['app']?.['viewer']?.class
@@ -221,7 +228,7 @@
</div>
</Pane>
<Pane size={$connectingInput?.opened ? 0 : 30}>
<div class="relative h-full w-full">
<div id="app-tutorial-4" class="relative h-full w-full">
<InlineScriptsPanel />
</div>
</Pane>

View File

@@ -65,7 +65,11 @@
No components found
</div>
{:else}
<div in:fade|local={{ duration: 50, delay: 50 }} out:fade|local={{ duration: 50 }}>
<div
id="app-tutorial-1"
in:fade|local={{ duration: 50, delay: 50 }}
out:fade|local={{ duration: 50 }}
>
{#each componentsFiltered as { title, components }, index (index)}
{#if components.length}
<div transition:slide|local={{ duration: 300 }}>

View File

@@ -78,7 +78,7 @@
{/if}
</div>
</div>
<div class="relative p-2">
<div id="app-tutorial-2" class="relative p-2">
{#each filteredPanels as [componentId, outputs] (componentId)}
<div
animate:flip={{ duration: 300 }}

View File

@@ -21,7 +21,7 @@
}[kind]
</script>
<div
<li
class="hover:bg-gray-50 w-full inline-flex items-center p-4 gap-4 first-of-type:!border-t-0
first-of-type:rounded-t-md last-of-type:rounded-b-md {color}"
>
@@ -61,4 +61,4 @@ first-of-type:rounded-t-md last-of-type:rounded-b-md {color}"
/>
</div>
{/if}
</div>
</li>

View File

@@ -18,6 +18,7 @@
export { c as class }
export let wrapperClass = ''
export let style = ''
export let id = ''
$: selected && updateSelected()
@@ -42,6 +43,7 @@
<div
class={twMerge('border-b border-gray-200 flex flex-row whitespace-nowrap scrollbar-hidden', c)}
{style}
{id}
>
<slot {selected} />
</div>

View File

@@ -29,7 +29,7 @@
<div class="h-full overflow-hidden">
<FlowCard title="Settings">
<div class="h-full flex-1">
<Tabs bind:selected={$selectedId}>
<Tabs bind:selected={$selectedId} id="flow-tutorial-3">
<Tab value="settings-metadata">Metadata</Tab>
<Tab value="settings-schedule">Schedule</Tab>
<Tab value="settings-same-worker">Shared Directory</Tab>

View File

@@ -35,7 +35,7 @@
let is_owner = false
</script>
<div class="flex flex-row-reverse justify-between items-center gap-x-2">
<div id="flow-tutorial-4" class="flex flex-row-reverse justify-between items-center gap-x-2">
<Button
on:click={() => {
previewMode = 'whole'

View File

@@ -133,7 +133,7 @@
<FlowConstantsItem />
</div>
<div class="flex-auto grow" bind:clientHeight={minHeight}>
<div id="flow-tutorial-1" class="flex-auto grow" bind:clientHeight={minHeight}>
<FlowGraph
insertable
scroll

View File

@@ -84,6 +84,7 @@
{#if insertable && modules && (label != 'Input' || modules.length == 0)}
<div
id="flow-tutorial-2"
class="{openMenu ? 'z-10' : ''} w-7 absolute {whereInsert == 'after'
? 'top-12'
: '-top-10'} left-[50%] right-[50%] -translate-x-1/2"

View File

@@ -245,69 +245,74 @@
/>
<CenteredPage>
<div class="flex flex-wrap gap-2 items-center justify-between w-full">
<div class="flex justify-start">
<ToggleButtonGroup bind:selected={itemKind}>
<ToggleButton light position="left" value="all" size="sm">All</ToggleButton>
<ToggleButton light position="center" value="script" size="sm">
<div class="flex gap-1 items-center">
<Code2 size={16} />
Scripts
</div>
</ToggleButton>
<ToggleButton light position="center" value="flow" size="sm">
<div class="flex gap-1 items-center">
<Icon data={faBarsStaggered} scale={0.8} class="mr-1" />
Flows
</div>
</ToggleButton>
<ToggleButton light position="right" value="app" size="sm">
<div class="flex gap-1 items-center">
<LayoutDashboard size={16} />
Apps
</div>
</ToggleButton>
</ToggleButtonGroup>
<div id="welcome-tutorial-1">
<div class="flex flex-wrap gap-2 items-center justify-between w-full">
<div class="flex justify-start">
<ToggleButtonGroup bind:selected={itemKind}>
<ToggleButton light position="left" value="all" size="sm">All</ToggleButton>
<ToggleButton light position="center" value="script" size="sm">
<div class="flex gap-1 items-center">
<Code2 size={16} />
Scripts
</div>
</ToggleButton>
<ToggleButton light position="center" value="flow" size="sm">
<div class="flex gap-1 items-center">
<Icon data={faBarsStaggered} scale={0.8} class="mr-1" />
Flows
</div>
</ToggleButton>
<ToggleButton light position="right" value="app" size="sm">
<div class="flex gap-1 items-center">
<LayoutDashboard size={16} />
Apps
</div>
</ToggleButton>
</ToggleButtonGroup>
</div>
<div class="relative text-gray-600 grow min-w-[100px]">
<!-- svelte-ignore a11y-autofocus -->
<input
autofocus
placeholder="Search Scripts, Flows & Apps"
bind:value={filter}
class="bg-white !h-10 !px-4 !pr-10 !rounded-lg text-sm focus:outline-none"
/>
<button type="submit" class="absolute right-0 top-0 mt-3 mr-4">
<svg
class="h-4 w-4 fill-current"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
id="Capa_1"
x="0px"
y="0px"
viewBox="0 0 56.966 56.966"
style="enable-background:new 0 0 56.966 56.966;"
xml:space="preserve"
width="512px"
height="512px"
>
<path
d="M55.146,51.887L41.588,37.786c3.486-4.144,5.396-9.358,5.396-14.786c0-12.682-10.318-23-23-23s-23,10.318-23,23 s10.318,23,23,23c4.761,0,9.298-1.436,13.177-4.162l13.661,14.208c0.571,0.593,1.339,0.92,2.162,0.92 c0.779,0,1.518-0.297,2.079-0.837C56.255,54.982,56.293,53.08,55.146,51.887z M23.984,6c9.374,0,17,7.626,17,17s-7.626,17-17,17 s-17-7.626-17-17S14.61,6,23.984,6z"
/>
</svg>
</button>
</div>
</div>
<div class="relative text-gray-600 grow min-w-[100px]">
<!-- svelte-ignore a11y-autofocus -->
<input
autofocus
placeholder="Search Scripts, Flows & Apps"
bind:value={filter}
class="bg-white !h-10 !px-4 !pr-10 !rounded-lg text-sm focus:outline-none"
/>
<button type="submit" class="absolute right-0 top-0 mt-3 mr-4">
<svg
class="h-4 w-4 fill-current"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
id="Capa_1"
x="0px"
y="0px"
viewBox="0 0 56.966 56.966"
style="enable-background:new 0 0 56.966 56.966;"
xml:space="preserve"
width="512px"
height="512px"
>
<path
d="M55.146,51.887L41.588,37.786c3.486-4.144,5.396-9.358,5.396-14.786c0-12.682-10.318-23-23-23s-23,10.318-23,23 s10.318,23,23,23c4.761,0,9.298-1.436,13.177-4.162l13.661,14.208c0.571,0.593,1.339,0.92,2.162,0.92 c0.779,0,1.518-0.297,2.079-0.837C56.255,54.982,56.293,53.08,55.146,51.887z M23.984,6c9.374,0,17,7.626,17,17s-7.626,17-17,17 s-17-7.626-17-17S14.61,6,23.984,6z"
/>
</svg>
</button>
<div class="relative flex justify-between items-start">
<ListFilters bind:selectedFilter={ownerFilter} filters={owners} />
{#if !loading}
<Toggle
size="xs"
bind:checked={archived}
options={{ right: 'Show archived' }}
class="pt-2"
textClass="whitespace-nowrap"
/>
{/if}
</div>
</div>
<div class="relative">
<ListFilters bind:selectedFilter={ownerFilter} filters={owners} />
{#if !loading}
<div class="absolute -bottom-2 right-0 bg-white/90">
<Toggle size="xs" bind:checked={archived} options={{ right: 'Show archived' }} /></div
>
{/if}
</div>
<div>
{#if filteredItems == undefined}
<div class="mt-4" />
@@ -318,7 +323,7 @@
{:else if filteredItems.length === 0}
<NoItemFound />
{:else}
<div class="border rounded-md divide-y divide-gray-200">
<ul class="border rounded-md divide-y divide-gray-200">
<!-- <VirtualList {items} let:item bind:start bind:end> -->
{#each (items ?? []).slice(0, nbDisplayed) as item, i (item.type + '/' + item.path + (item.summary ?? ''))}
{#if item.type == 'script'}
@@ -354,7 +359,7 @@
{/if}
{/each}
<!-- </VirtualList> -->
</div>
</ul>
{#if items && items?.length > 30}
<span class="text-xs"
>{nbDisplayed} items out of {items.length}

View File

@@ -10,6 +10,7 @@
export let icon: IconDefinition
export let isCollapsed: boolean
export let disabled: boolean = false
export let id: string = ''
let isSelected = false
@@ -33,6 +34,7 @@
$$props.class
)}
target={href.includes('http') ? '_blank' : null}
{id}
>
<Icon
data={icon}

View File

@@ -18,7 +18,7 @@
const mainMenuLinks = [
{ label: 'Home', href: '/', icon: faHomeAlt },
{ label: 'Runs', href: '/runs', icon: faPlay },
{ label: 'Runs', href: '/runs', icon: faPlay, id: 'welcome-tutorial-4' },
{ label: 'Variables', href: '/variables', icon: faDollarSign, disabled: $userStore?.operator },
{ label: 'Resources', href: '/resources', icon: faCubes, disabled: $userStore?.operator }
]

View File

@@ -0,0 +1,143 @@
<script lang="ts">
import Shepherd from 'shepherd.js'
import { steps, tourStore, type TutorialName } from './'
export let tutorial: TutorialName
/**
* If the index of a step is added to a tour, the custom footer will not be added
* (ie. the steps won't be shown and the buttons won't be positioned)
*/
const noPageNumber: Partial<Record<TutorialName, number[]>> = {
welcome: [0]
}
let tour: Shepherd.Tour | undefined
$: $tourStore.includes(tutorial) && initiate()
function initiate() {
if (!tourStore.isTourActive(tutorial)) {
return
}
tour?.cancel()
tour = new Shepherd.Tour({
tourName: tutorial,
useModalOverlay: true,
defaultStepOptions: {
classes: 'max-w-[460px] shadow-xl bg-white border rounded px-6 py-4 z-[1100]',
scrollTo: true
}
})
tour.addSteps(steps[tutorial](tour))
tour.steps.forEach((step, i) => {
if (noPageNumber[tutorial]?.includes(i)) {
return
}
step.on('show', () => {
const currentStep = tour?.getCurrentStep()
const currentStepElement = currentStep?.getElement()
const header = currentStepElement?.querySelector('.shepherd-header')
if (currentStep && currentStepElement && header) {
const progress = document.createElement('span')
progress.className = 'shepherd-progress text-sm text-gray-500 pr-2'
progress.innerText = `${(tour?.steps.indexOf(currentStep) ?? 0) + 1}/${
tour?.steps.length
}`
header.prepend(progress)
}
})
})
;['complete', 'cancel'].forEach((event) => {
tour?.on(event, () => {
tourStore.markTourAsDone(tutorial)
})
})
tour.start()
}
</script>
<style global>
.shepherd-footer {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 0.5rem;
}
.shepherd-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.5rem;
}
.shepherd-button {
padding: 4px 12px;
border-radius: 4px;
transition-duration: 200ms;
}
/* Simple primary button */
.shepherd-button:not(.shepherd-button-secondary.special) {
color: #3b82f6;
}
.shepherd-button:not(.shepherd-button-secondary.special):hover,
.shepherd-button:not(.shepherd-button-secondary.special):focus {
background-color: #eff6ff;
}
/* Simple secondary button */
.shepherd-button.shepherd-button-secondary:not(.special) {
color: #6b7280;
}
.shepherd-button.shepherd-button-secondary:not(.special):hover,
.shepherd-button.shepherd-button-secondary:not(.special):focus {
background-color: #f3f4f6;
}
/* Special primary button */
.shepherd-button.special:not(.shepherd-button-secondary) {
background-color: #3b82f6;
color: #fff;
}
.shepherd-button.special:not(.shepherd-button-secondary):hover,
.shepherd-button.special:not(.shepherd-button-secondary):focus {
background-color: #2563eb;
}
/* Special secondary button */
.shepherd-button.special.shepherd-button-secondary {
background-color: #eff6ff;
color: #3b82f6;
}
.shepherd-button.special.shepherd-button-secondary:hover,
.shepherd-button.special.shepherd-button-secondary:focus {
background-color: #dbeafe;
}
.shepherd-cancel-icon {
display: flex;
justify-content: center;
align-items: center;
width: 32px;
height: 32px;
border-radius: 16px;
background-color: #fff;
transition-duration: 200ms;
}
.shepherd-cancel-icon:hover,
.shepherd-cancel-icon:focus {
background-color: #ededed;
}
.shepherd-highlighted-element {
box-shadow: 0 0 0 6px #93c4fdbb;
background-color: #93c4fd3a;
position: relative;
z-index: 1090;
}
</style>

View File

@@ -0,0 +1,113 @@
import type Shepherd from 'shepherd.js'
export default (tour: Shepherd.Tour) => {
const steps: object[] | Shepherd.Step[] = [
{
id: 'welcome',
text: `<div class="text-xl font-bold mb-6">Windmill App Editor</div>
<p class="mb-6">Build your own UI quick and easy with drag-and-drop components. Connect your scripts and data to them and deploy a fully functioning application.</p>`,
buttons: [
{
text: 'See by myself',
action: tour.cancel,
secondary: true,
classes: 'special'
},
{
text: 'Give me an overview',
action: tour.next,
classes: 'special'
}
]
},
{
id: 'components',
text: `<div class="text-xl font-bold mb-2">Components</div>
<p class="mb-2">Click to add and move them around by dragging. Configure the inputs, settings and styling, even with Tailwind classes.</p>`,
cancelIcon: {
enabled: true
},
attachTo: { element: '#app-tutorial-1', on: 'left-start' },
highlightClass: 'shepherd-highlighted-element',
scrollTo: false,
buttons: [
{
text: 'Back',
action: tour.back,
secondary: true
},
{
text: 'Next',
action: tour.next
}
]
},
{
id: 'outputs',
text: `<div class="text-xl font-bold mb-2">Outputs</div>
<p class="mb-2">Each component has their outputs, which can be used to easily hook into the app state.</p>`,
cancelIcon: {
enabled: true
},
attachTo: { element: '#app-tutorial-2', on: 'right-start' },
highlightClass: 'shepherd-highlighted-element',
scrollTo: false,
buttons: [
{
text: 'Back',
action: tour.back,
secondary: true
},
{
text: 'Next',
action: tour.next
}
]
},
{
id: 'parameters',
text: `<div class="text-xl font-bold mb-2">Canvas</div>
<p class="mb-2">It allows you to position, scale and group the components.</p>`,
cancelIcon: {
enabled: true
},
attachTo: { element: '#app-tutorial-3', on: 'bottom' },
highlightClass: 'shepherd-highlighted-element',
scrollTo: false,
buttons: [
{
text: 'Back',
action: tour.back,
secondary: true
},
{
text: 'Next',
action: tour.next
}
]
},
{
id: 'test',
text: `<div class="text-xl font-bold mb-2">Runnable editor</div>
<p class="mb-2">Here you can create, edit and manage the scripts of your app.</p>`,
cancelIcon: {
enabled: true
},
attachTo: { element: '#app-tutorial-4', on: 'top-start' },
highlightClass: 'shepherd-highlighted-element',
scrollTo: false,
buttons: [
{
text: 'Back',
action: tour.back,
secondary: true
},
{
text: 'Finish',
action: tour.complete
}
]
}
]
return steps
}

View File

@@ -0,0 +1,113 @@
import type Shepherd from 'shepherd.js'
export default (tour: Shepherd.Tour) => {
const steps: object[] | Shepherd.Step[] = [
{
id: 'welcome',
text: `<div class="text-xl font-bold mb-6">Windmill Flow Editor</div>
<p class="mb-6">Build complex workflows from your scripts, configure them and get a clear visualization.</p>`,
buttons: [
{
text: 'See by myself',
action: tour.cancel,
secondary: true,
classes: 'special'
},
{
text: 'Give me an overview',
action: tour.next,
classes: 'special'
}
]
},
{
id: 'graph',
text: `<div class="text-xl font-bold mb-2">Flow graph</div>
<p class="mb-2">Know how your flow is structured at a glimpse.</p>`,
cancelIcon: {
enabled: true
},
attachTo: { element: '#flow-tutorial-1', on: 'right-start' },
highlightClass: 'shepherd-highlighted-element',
scrollTo: false,
buttons: [
{
text: 'Back',
action: tour.back,
secondary: true
},
{
text: 'Next',
action: tour.next
}
]
},
{
id: 'modules',
text: `<div class="text-xl font-bold mb-2">Insert modules</div>
<p class="mb-2">Start building by adding modules. A module can be a simple script, a loop or a branching for example.</p>`,
cancelIcon: {
enabled: true
},
attachTo: { element: '#flow-tutorial-2 button', on: 'right-start' },
highlightClass: 'shepherd-highlighted-element',
scrollTo: false,
buttons: [
{
text: 'Back',
action: tour.back,
secondary: true
},
{
text: 'Next',
action: tour.next
}
]
},
{
id: 'settings',
text: `<div class="text-xl font-bold mb-2">Settings</div>
<p class="mb-2">Configure schedules, authorizations, sharing parameters and many more.</p>`,
cancelIcon: {
enabled: true
},
attachTo: { element: '#flow-tutorial-3', on: 'bottom-start' },
highlightClass: 'shepherd-highlighted-element',
scrollTo: false,
buttons: [
{
text: 'Back',
action: tour.back,
secondary: true
},
{
text: 'Next',
action: tour.next
}
]
},
{
id: 'test',
text: `<div class="text-xl font-bold mb-2">Test flow</div>
<p class="mb-2">Time to try your work! Test the flow and iterate as much as you want.</p>`,
cancelIcon: {
enabled: true
},
attachTo: { element: '#flow-tutorial-4', on: 'left-start' },
highlightClass: 'shepherd-highlighted-element',
scrollTo: false,
buttons: [
{
text: 'Back',
action: tour.back,
secondary: true
},
{
text: 'Finish',
action: tour.complete
}
]
}
]
return steps
}

View File

@@ -0,0 +1,51 @@
import { browser } from '$app/environment'
import type Shepherd from 'shepherd.js'
import welcome from './welcome-steps'
import script from './script-steps'
import flow from './flow-steps'
import app from './app-steps'
import { writable } from 'svelte/store'
export { default as Tour } from './Tour.svelte'
export const TUTORIAL_NAMES = ['welcome', 'script', 'flow', 'app'] as const
export type TutorialName = (typeof TUTORIAL_NAMES)[number]
export const steps: Record<TutorialName, (tour: Shepherd.Tour) => object[] | Shepherd.Step[]> = {
welcome,
script,
flow,
app
}
const STORAGE_KEY = 'tutorials-to-show' as const
const store = writable<Readonly<TutorialName[]>>([])
export const tourStore = {
subscribe: store.subscribe,
initiateTours: () => {
if (!browser) {
return
}
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(TUTORIAL_NAMES))
store.set(TUTORIAL_NAMES)
},
isTourActive: (name: TutorialName) => {
if (!browser) {
return false
}
const toursToShow = JSON.parse(
window.localStorage.getItem(STORAGE_KEY) || '[]'
) as TutorialName[]
return toursToShow.includes(name)
},
markTourAsDone: (name: TutorialName) => {
if (!browser) {
return
}
const toursToShow = (
JSON.parse(window.localStorage.getItem(STORAGE_KEY) || '[]') as TutorialName[]
).filter((t) => t !== name)
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(toursToShow))
store.set(toursToShow)
}
}

View File

@@ -0,0 +1,113 @@
import type Shepherd from 'shepherd.js'
export default (tour: Shepherd.Tour) => {
const steps: object[] | Shepherd.Step[] = [
{
id: 'welcome',
text: `<div class="text-xl font-bold mb-6">Windmill Script Builder</div>
<p class="mb-6">Build scripts with our own web editor, then use them anywhere in your flows, apps or as standalone computations with their auto-generated UI.</p>`,
buttons: [
{
text: 'See by myself',
action: tour.cancel,
secondary: true,
classes: 'special'
},
{
text: 'Give me an overview',
action: tour.next,
classes: 'special'
}
]
},
{
id: 'editor',
text: `<div class="text-xl font-bold mb-2">Web editor</div>
<p class="mb-2">Code directly from the web editor.</p>`,
cancelIcon: {
enabled: true
},
attachTo: { element: '#script-tutorial-1', on: 'right-start' },
highlightClass: 'shepherd-highlighted-element',
scrollTo: false,
buttons: [
{
text: 'Back',
action: tour.back,
secondary: true
},
{
text: 'Next',
action: tour.next
}
]
},
{
id: 'preview',
text: `<div class="text-xl font-bold mb-2">UI preview</div>
<p class="mb-2">Preview the user interface generated from the signature of the script.</p>`,
cancelIcon: {
enabled: true
},
attachTo: { element: '#script-tutorial-2', on: 'left-start' },
highlightClass: 'shepherd-highlighted-element',
scrollTo: false,
buttons: [
{
text: 'Back',
action: tour.back,
secondary: true
},
{
text: 'Next',
action: tour.next
}
]
},
{
id: 'parameters',
text: `<div class="text-xl font-bold mb-2">Parameters</div>
<p class="mb-2">Add variables, resources and configure your script.</p>`,
cancelIcon: {
enabled: true
},
attachTo: { element: '#script-tutorial-3', on: 'bottom-start' },
highlightClass: 'shepherd-highlighted-element',
scrollTo: false,
buttons: [
{
text: 'Back',
action: tour.back,
secondary: true
},
{
text: 'Next',
action: tour.next
}
]
},
{
id: 'test',
text: `<div class="text-xl font-bold mb-2">Test script</div>
<p class="mb-2">Time to try your work! Test the script and iterate as much as you want.</p>`,
cancelIcon: {
enabled: true
},
attachTo: { element: '#script-tutorial-4', on: 'left-start' },
highlightClass: 'shepherd-highlighted-element',
scrollTo: false,
buttons: [
{
text: 'Back',
action: tour.back,
secondary: true
},
{
text: 'Finish',
action: tour.complete
}
]
}
]
return steps
}

View File

@@ -0,0 +1,114 @@
import type Shepherd from 'shepherd.js'
export default (tour: Shepherd.Tour) => {
const steps: object[] | Shepherd.Step[] = [
{
id: 'welcome',
text: `<div class="text-xl font-bold mb-6">Welcome to Windmill Cloud App!</div>
<p class="mb-6">Thank your for your interest in Windmill. Let us guide you through the home screen in strictly less than a minute.</p>`,
buttons: [
{
text: 'See by myself',
action: tour.cancel,
secondary: true,
classes: 'special'
},
{
text: 'Give me an overview',
action: tour.next,
classes: 'special'
}
]
},
{
id: 'workspace',
text: `<div class="text-xl font-bold mb-2">Workspace</div>
<p>You can see all the scripts, flows and apps here.</p>
<p class="mb-2">Use the search and filters to find just what you are looking for.</p>`,
cancelIcon: {
enabled: true
},
attachTo: { element: '#welcome-tutorial-1', on: 'bottom-start' },
highlightClass: 'shepherd-highlighted-element',
scrollTo: false,
buttons: [
{
text: 'Back',
action: tour.back,
secondary: true
},
{
text: 'Next',
action: tour.next
}
]
},
{
id: 'create',
text: `<div class="text-xl font-bold mb-2">Create</div>
<p class="mb-2">Build your own tools in minutes.</p>`,
cancelIcon: {
enabled: true
},
attachTo: { element: '#welcome-tutorial-2', on: 'bottom-end' },
highlightClass: 'shepherd-highlighted-element',
scrollTo: false,
buttons: [
{
text: 'Back',
action: tour.back,
secondary: true
},
{
text: 'Next',
action: tour.next
}
]
},
{
id: 'inspiration',
text: `<div class="text-xl font-bold mb-2">Take inspiration</div>
<p class="mb-2">Explore templates built by the community and import them directly from <a href="https://hub.windmill.dev" target="_blank">Windmill Hub</a>.</p>`,
cancelIcon: {
enabled: true
},
attachTo: { element: '#welcome-tutorial-3', on: 'bottom-start' },
highlightClass: 'shepherd-highlighted-element',
scrollTo: false,
buttons: [
{
text: 'Back',
action: tour.back,
secondary: true
},
{
text: 'Next',
action: tour.next
}
]
},
{
id: 'monitor',
text: `<div class="text-xl font-bold mb-2">Monitor</div>
<p class="mb-2">Keep an eye an all past and scheduled executions of scripts and flows.</p>`,
cancelIcon: {
enabled: true
},
attachTo: { element: '#welcome-tutorial-4-wrapper #welcome-tutorial-4', on: 'right-start' },
highlightClass: 'shepherd-highlighted-element',
scrollTo: false,
buttons: [
{
text: 'Back',
action: tour.back,
secondary: true
},
{
text: 'Finish',
action: tour.complete
}
]
}
]
return steps
}

View File

@@ -202,7 +202,10 @@
isCollapsed ? 'md:w-12' : 'md:w-40'
)}
>
<div class="flex-1 flex flex-col min-h-0 h-screen shadow-lg bg-[#2e3440]">
<div
id="welcome-tutorial-4-wrapper"
class="flex-1 flex flex-col min-h-0 h-screen shadow-lg bg-[#2e3440]"
>
<button
on:click={() => {
goto('/')

View File

@@ -20,6 +20,8 @@
import AppPreview from '$lib/components/apps/editor/AppPreview.svelte'
import { writable } from 'svelte/store'
import type { EditorBreakpoint } from '$lib/components/apps/types'
import { Tour, tourStore } from '../../../lib/components/tutorial'
import { onMount } from 'svelte'
type Tab = 'hubscripts' | 'hubflows' | 'hubapps' | 'workspace'
@@ -60,8 +62,12 @@
appViewerApp = hub
appViewer.openDrawer?.()
}
onMount(tourStore.initiateTours)
</script>
<Tour tutorial="welcome" />
<Drawer bind:this={codeViewer} size="900px">
<DrawerContent title={codeViewerObj?.summary ?? ''} on:close={codeViewer.closeDrawer}>
<svelte:fragment slot="actions">
@@ -181,7 +187,7 @@
</Alert>
{/if}
<PageHeader title="Home">
<div class="flex flex-row gap-4 flex-wrap justify-end items-center">
<div id="welcome-tutorial-2" class="flex flex-row gap-4 flex-wrap justify-end items-center">
{#if !$userStore?.operator}
<span class="text-sm text-gray-500">Create a new:</span>
<CreateActionsScript />
@@ -192,7 +198,7 @@
</PageHeader>
{#if !$userStore?.operator}
<div class="w-full overflow-auto scrollbar-hidden">
<div id="welcome-tutorial-3" class="w-full">
<Tabs bind:selected={tab}>
<Tab size="md" value="workspace">
<div class="flex gap-2 items-center my-1">

View File

@@ -84,7 +84,7 @@
{#if value}
<div class="h-screen">
{#key value}
<AppEditor {summary} app={value} path={''} {policy} fromHub={hubId != null} />
<AppEditor {summary} app={value} path={''} {policy} fromHub={hubId != null} tour />
{/key}
</div>
{/if}

View File

@@ -89,4 +89,4 @@
$dirtyStore = true
</script>
<FlowBuilder {flowStore} {flowStateStore} {selectedId} {loading} />
<FlowBuilder {flowStore} {flowStateStore} {selectedId} {loading} tour />