feat(frontend): Add onboarding tour to home page

This commit is contained in:
Ádám Kovács
2023-03-09 20:11:27 +01:00
parent 3b6ae0cc49
commit 4453690521
12 changed files with 385 additions and 70 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

@@ -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

@@ -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

@@ -242,69 +242,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" />
@@ -315,7 +320,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'}
@@ -348,7 +353,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,134 @@
<script lang="ts">
import { onMount } from 'svelte'
import Shepherd from 'shepherd.js'
import { steps, 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 noCustomFooter: Partial<Record<TutorialName, number[]>> = {
welcome: [0]
}
let tour: Shepherd.Tour | undefined
onMount(() => {
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 (noCustomFooter[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)
}
})
})
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,12 @@
import welcome from './welcome-steps'
import type Shepherd from 'shepherd.js'
export { default as Tour } from './Tour.svelte'
export type TutorialName = 'welcome' | 'script' | 'flow' | 'app'
export const steps: Record<TutorialName, (tour: Shepherd.Tour) => object[] | Shepherd.Step[]> = {
welcome,
script: (tour: Shepherd.Tour) => [],
flow: (tour: Shepherd.Tour) => [],
app: (tour: Shepherd.Tour) => []
}

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

@@ -12,7 +12,7 @@
import PickHubFlow from '$lib/components/flows/pickers/PickHubFlow.svelte'
import FlowViewer from '$lib/components/FlowViewer.svelte'
import HighlightCode from '$lib/components/HighlightCode.svelte'
import { Building, ExternalLink, Globe2 } from 'lucide-svelte'
import { Building, Globe2 } from 'lucide-svelte'
import ItemsList from '$lib/components/home/ItemsList.svelte'
import CreateActionsApp from '$lib/components/flows/CreateActionsApp.svelte'
@@ -20,6 +20,7 @@
import AppPreview from '$lib/components/apps/editor/AppPreview.svelte'
import { writable } from 'svelte/store'
import type { EditorBreakpoint } from '$lib/components/apps/types'
import { Tour } from '../../../lib/components/tutorial'
type Tab = 'hubscripts' | 'hubflows' | 'hubapps' | 'workspace'
@@ -62,6 +63,7 @@
}
</script>
<Tour tutorial="welcome" />
<Drawer bind:this={codeViewer} size="900px">
<DrawerContent title={codeViewerObj?.summary ?? ''} on:close={codeViewer.closeDrawer}>
<svelte:fragment slot="actions">
@@ -181,7 +183,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 +194,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">