feat(frontend): global flow preview (#329)

* Added flow preview

* Fix blue

* Disable preview button is not inputs are valid

* Fix layout top margin
This commit is contained in:
Faton Ramadani
2022-08-04 17:46:40 +02:00
committed by GitHub
parent 986e76dc87
commit 615f69e935
13 changed files with 267 additions and 154 deletions
+5
View File
@@ -77,6 +77,11 @@ html {
@apply rounded-sm;
}
.blue-button {
@apply bg-blue-500 !important;
@apply hover:bg-blue-700 !important;
}
.default-button {
/*this styling does not impact the Dropdown component */
@apply bg-blue-500;
@@ -1,5 +1,5 @@
<div class="max-w-screen-lg flex grow w-full h-full xl:-ml-20">
<div class="grow w-full h-full">
<div class="grow w-full h-full mt-4">
<slot />
</div>
</div>
+92 -61
View File
@@ -10,10 +10,13 @@
sendUserToast,
setQueryWithoutLoad
} from '$lib/utils'
import { Breadcrumb, BreadcrumbItem } from 'flowbite-svelte'
import { faPlay } from '@fortawesome/free-solid-svg-icons'
import { Breadcrumb, BreadcrumbItem, Button } from 'flowbite-svelte'
import { onMount } from 'svelte'
import Icon from 'svelte-awesome'
import { OFFSET } from './CronInput.svelte'
import FlowEditor from './FlowEditor.svelte'
import FlowPreviewContent from './FlowPreviewContent.svelte'
import { flowStore, mode } from './flows/flowStore'
import { flowToMode } from './flows/utils'
@@ -26,6 +29,8 @@
let scheduleEnabled
let scheduleCron: string
let previewOpen = false
$: step = Number($page.url.searchParams.get('step')) || 1
async function createSchedule(path: string) {
@@ -127,69 +132,95 @@
})
</script>
<div class="flex flex-col max-w-screen-lg w-full mb-96 px-8">
<!-- Nav between steps-->
<div class="justify-between flex flex-row w-full">
<Breadcrumb>
<BreadcrumbItem>
<button on:click={() => changeStep(1)} class={step === 1 ? 'font-bold' : null}>
Flow Editor
</button>
</BreadcrumbItem>
<BreadcrumbItem>
<button on:click={() => changeStep(2)} class={step === 2 ? 'font-bold' : null}>
UI customisation
</button>
</BreadcrumbItem>
</Breadcrumb>
<div class="flex flex-row-reverse ml-2">
{#if step == 1}
<button
disabled={pathError != ''}
class="default-button px-6 max-h-8"
on:click={() => changeStep(2)}
>
Next
</button>
<button
disabled={pathError != ''}
class="default-button-secondary px-6 max-h-8 mr-2"
on:click={saveFlow}
>
Save
</button>
<div class="flex flex-row w-full h-full justify-between">
<div class="flex flex-col max-w-screen-md mb-96 m-auto">
<div class="mx-4">
<!-- Nav between steps-->
<div class="justify-between flex flex-row w-full my-4">
<Breadcrumb>
<BreadcrumbItem>
<button on:click={() => changeStep(1)} class={step === 1 ? 'font-bold' : null}>
Flow Editor
</button>
</BreadcrumbItem>
<BreadcrumbItem>
<button on:click={() => changeStep(2)} class={step === 2 ? 'font-bold' : null}>
UI customisation
</button>
</BreadcrumbItem>
</Breadcrumb>
<div class="flex flex-row-reverse ml-2">
{#if step == 1}
<button
disabled={pathError != ''}
class="default-button px-6 max-h-8"
on:click={() => changeStep(2)}
>
Next
</button>
<button
disabled={pathError != ''}
class="default-button-secondary px-6 max-h-8 mr-2"
on:click={saveFlow}
>
Save
</button>
{:else}
<button class="default-button px-6 self-end" on:click={saveFlow}>Save</button>
{/if}
</div>
</div>
<div class="flex flex-row-reverse">
<span class="my-1 text-sm text-gray-500 italic">
{#if initialPath && initialPath != $flowStore?.path} {initialPath} &rightarrow; {/if}
{$flowStore?.path}
</span>
</div>
<!-- metadata -->
{#if $flowStore}
{#if step === 1}
<FlowEditor
bind:pathError
bind:initialPath
bind:scheduleEnabled
bind:scheduleCron
bind:scheduleArgs
/>
{:else if step === 2}
<ScriptSchema
synchronizedHeader={false}
bind:summary={$flowStore.summary}
bind:description={$flowStore.description}
bind:schema={$flowStore.schema}
/>
{/if}
{:else}
<button class="default-button px-6 self-end" on:click={saveFlow}>Save</button>
<p>Loading</p>
{/if}
</div>
</div>
<div class="flex flex-row-reverse">
<span class="my-1 text-sm text-gray-500 italic">
{#if initialPath && initialPath != $flowStore?.path} {initialPath} &rightarrow; {/if}
{$flowStore?.path}
</span>
<Button
size="lg"
pill
on:click={() => (previewOpen = !previewOpen)}
class={`blue-button fixed bottom-10 right-10 ${previewOpen ? 'hidden' : ''}`}
>
Preview flow
<Icon data={faPlay} class="ml-2" />
</Button>
<div class={`relative h-screen w-1/3 ${previewOpen ? '' : 'hidden'}`}>
<div class="absolute top-0 h-full">
<div class="fixed border-l-2 right-0 h-screen w-1/3">
<FlowPreviewContent
flow={$flowStore}
bind:args={scheduleArgs}
on:close={() => (previewOpen = !previewOpen)}
/>
</div>
</div>
</div>
<!-- metadata -->
{#if $flowStore}
{#if step === 1}
<FlowEditor
bind:pathError
bind:initialPath
bind:scheduleEnabled
bind:scheduleCron
bind:scheduleArgs
/>
{:else if step === 2}
<ScriptSchema
synchronizedHeader={false}
bind:summary={$flowStore.summary}
bind:description={$flowStore.description}
bind:schema={$flowStore.schema}
/>
{/if}
{:else}
<p>Loading</p>
{/if}
</div>
@@ -69,7 +69,7 @@
{/each}
<Button
disabled={pathIsEmpty($flowStore.path)}
color="blue"
class="blue-button"
on:click={() => {
addModule()
open = $flowStore?.value.modules.length - 1
@@ -0,0 +1,82 @@
<script lang="ts">
import { Job, JobService, type Flow } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { sendUserToast, truncateRev } from '$lib/utils'
import { faClose } from '@fortawesome/free-solid-svg-icons'
import { Button } from 'flowbite-svelte'
import { createEventDispatcher, onDestroy } from 'svelte'
import Icon from 'svelte-awesome'
import FlowJobResult from './FlowJobResult.svelte'
import FlowStatusViewer from './FlowStatusViewer.svelte'
import SchemaForm from './SchemaForm.svelte'
const dispatch = createEventDispatcher()
export let flow: Flow
export let args: Record<string, any> = {}
let intervalId: NodeJS.Timer
let job: Job | undefined
let jobs = []
let jobId: string
let isValid: boolean = false
$: dispatch('change', jobs)
export async function runPreview(args: Record<string, any>) {
intervalId && clearInterval(intervalId)
jobId = await JobService.runFlowPreview({
workspace: $workspaceStore ?? '',
requestBody: {
args,
value: flow.value,
path: flow.path
}
})
jobs = []
intervalId = setInterval(loadJob, 1000)
sendUserToast(`started preview ${truncateRev(jobId, 10)}`)
}
async function loadJob() {
try {
job = await JobService.getJob({ workspace: $workspaceStore!, id: jobId })
if (job?.type == 'CompletedJob') {
clearInterval(intervalId)
}
} catch (err) {
sendUserToast(err, true)
}
}
onDestroy(() => {
intervalId && clearInterval(intervalId)
})
</script>
<div class="flex flex-col space-y-4 h-screen bg-white ">
<div class="flex flex-col space-y-4 p-6 border-b-2">
<div class="flex justify-between">
<h3 class="text-lg leading-6 font-bold text-gray-900">Flow Preview</h3>
<Button color="alternative" on:click={() => dispatch('close')}>
<Icon data={faClose} />
</Button>
</div>
<SchemaForm schema={flow.schema} bind:isValid bind:args />
<Button disabled={!isValid} class="blue-button" on:click={() => runPreview(args)} size="md"
>Preview
</Button>
</div>
<div class="h-full overflow-y-auto mb-16">
{#if job}
<div class="w-full">
<FlowStatusViewer {job} bind:jobs />
</div>
{#if `result` in job}
<FlowJobResult {job} />
{/if}
{/if}
</div>
</div>
@@ -1,14 +1,14 @@
<script lang="ts">
import { faHourglassHalf, faSpinner, faTimes } from '@fortawesome/free-solid-svg-icons'
import { scriptPathToHref, truncateRev } from '$lib/utils'
import { faHourglassHalf, faSpinner, faTimes } from '@fortawesome/free-solid-svg-icons'
import Icon from 'svelte-awesome'
import { check } from 'svelte-awesome/icons'
import { CompletedJob, FlowStatusModule, JobService, QueuedJob } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import JobStatus from './JobStatus.svelte'
import FlowJobResult from './FlowJobResult.svelte'
import JobStatus from './JobStatus.svelte'
export let job: QueuedJob | CompletedJob
export let jobs: (CompletedJob | CompletedJob[] | undefined)[] = []
@@ -74,29 +74,28 @@
$: $workspaceStore && job && loadResults()
</script>
<div class="flow-root w-full p-4">
<div class="flex flex-row-reverse">
<div class="flow-root w-full p-6">
<div class="flex ">
{#if job}
<div class="flex-col">
<a href="/run/{job?.id}" class="font-medium text-blue-600"
>{truncateRev(job?.id ?? '', 10)}</a
>
<div>
<JobStatus {job} />
</div>
<a href="/run/{job?.id}" class="font-medium text-blue-600">
{truncateRev(job?.id ?? '', 10)}
</a>
</div>
{/if}
</div>
<JobStatus {job} />
<p class="text-gray-500 mb-6 w-full text-center">
Step
<span class="font-medium text-gray-900"
>{Math.min((job?.flow_status?.step ?? 0) + 1, job?.raw_flow?.modules.length ?? 0)}</span
>
<span class="font-medium text-gray-900">
{Math.min((job?.flow_status?.step ?? 0) + 1, job?.raw_flow?.modules.length ?? 0)}
</span>
out of <span class="font-medium text-gray-900">{job?.raw_flow?.modules.length}</span>
<span class="mt-4" />
</p>
<ul class="-mb-8 w-full">
<ul class="w-full">
{#each job?.raw_flow?.modules ?? [] as mod, i}
<li class="w-full">
<div class="relative pb-8 w-full">
@@ -162,17 +161,18 @@
{#if job.flow_status?.modules[i].forloop_jobs}
{#each job.flow_status?.modules[i].forloop_jobs ?? [] as job}
<div class="flex flex-col">
<a href="/run/{job}" class="font-medium text-blue-600"
>{truncateRev(job ?? '', 10)}</a
>
<a href="/run/{job}" class="font-medium text-blue-600">
{truncateRev(job ?? '', 10)}
</a>
</div>
{/each}
{:else if job.flow_status?.modules[i].job}
<a
href="/run/{job.flow_status?.modules[i].job}"
class="font-medium text-blue-600"
>{truncateRev(job.flow_status?.modules[i].job ?? '', 10)}</a
>
{truncateRev(job.flow_status?.modules[i].job ?? '', 10)}
</a>
{/if}
</div>
</div>
@@ -182,7 +182,7 @@
<div slot="actions">
<div on:click={() => overlays[argName]?.focus('connect')}>
<Tooltip placement="bottom" content="Input connect">
<Button color="blue" size="sm" class="h-8">
<Button size="sm" class="blue-button h-8">
<Icon data={faChain} />
</Button>
</Tooltip>
+32 -24
View File
@@ -1,17 +1,18 @@
<script lang="ts">
import { displayDate, forLater, msToSec } from '$lib/utils'
import {
faCalendar,
faCheck,
faCircle,
faClock,
faHourglassHalf,
faTimes
} from '@fortawesome/free-solid-svg-icons'
import { displayDate, forLater, msToSec } from '$lib/utils'
import Icon from 'svelte-awesome'
import { check } from 'svelte-awesome/icons'
import type { CompletedJob, QueuedJob } from '$lib/gen'
import { Badge } from 'flowbite-svelte'
const SMALL_ICON_SCALE = 0.7
@@ -19,32 +20,39 @@
</script>
{#if job && 'success' in job && job.success}
<Icon class="text-green-600" data={check} scale={SMALL_ICON_SCALE} />
<span class="mx-2">Succeeded {job.is_skipped ? '(Skipped)' : ''}</span>
<div>
<Icon class="text-gray-700" data={faHourglassHalf} scale={SMALL_ICON_SCALE} /><span class="mx-2"
>Job ran in {msToSec(job.duration_ms)}
s</span
>
</div>
<Badge large color="green">
<Icon data={faCheck} scale={SMALL_ICON_SCALE} class="mr-2" />
Succeeded {job.is_skipped ? '(Skipped)' : ''}
</Badge>
<Badge large>
<Icon data={faHourglassHalf} scale={SMALL_ICON_SCALE} class="mr-2" />
Job ran in {msToSec(job.duration_ms)} s
</Badge>
{:else if job && 'success' in job}
<Icon class="text-red-700" data={faTimes} scale={SMALL_ICON_SCALE} />
<span class="mx-2">Failed</span>
<div>
<Icon class="text-gray-700" data={faHourglassHalf} scale={SMALL_ICON_SCALE} /><span class="mx-2"
>Job ran in {msToSec(job.duration_ms)}
s</span
>
</div>
<Badge large color="red">
<Icon data={faTimes} scale={SMALL_ICON_SCALE} class="mr-2" />
Failed
</Badge>
<Badge large>
<Icon data={faHourglassHalf} scale={SMALL_ICON_SCALE} class="mr-2" />
Job ran in {msToSec(job.duration_ms)}s
</Badge>
{:else if job && 'running' in job && job.running}
<Icon class="text-yellow-500" data={faCircle} scale={SMALL_ICON_SCALE} />
<span class="mx-2">Running</span>
<Badge large color="yellow">
<Icon data={faCircle} scale={SMALL_ICON_SCALE} class="mr-2" />
Running
</Badge>
{:else if job && 'running' in job && 'scheduled_for' in job && job.scheduled_for && forLater(job.scheduled_for)}
<Icon class="text-gray-700" data={faCalendar} scale={SMALL_ICON_SCALE} />
<span class="mx-2">Scheduled for {displayDate(job.scheduled_for)}</span>
<Badge>
<Icon data={faCalendar} scale={SMALL_ICON_SCALE} class="mr-2" />
Scheduled for {displayDate(job.scheduled_for)}
</Badge>
{:else if job && 'running' in job}
<Icon class="text-gray-700" data={faClock} scale={SMALL_ICON_SCALE} />
<span class="mx-2">Queued</span>
<Badge>
<Icon data={faClock} scale={SMALL_ICON_SCALE} class="mr-2" />
Queued
</Badge>
{:else}
<Icon class="text-gray-200" data={faCircle} scale={SMALL_ICON_SCALE} />
{/if}
@@ -1,10 +1,10 @@
<script lang="ts">
import { ResourceService } from '$lib/gen'
import ResourcePicker from './ResourcePicker.svelte'
import { workspaceStore } from '$lib/stores'
import SchemaForm from './SchemaForm.svelte'
import RadioButton from './RadioButton.svelte'
import ResourcePicker from './ResourcePicker.svelte'
import SchemaForm from './SchemaForm.svelte'
export let format: string
export let value: any
@@ -144,7 +144,7 @@
modalProperty = Object.assign({}, DEFAULT_PROPERTY)
schemaModal.openModal()
}}
color="blue"
class="blue-button"
>
<Icon data={faPlus} class="mr-1" />
Add argument
@@ -96,7 +96,7 @@
<div class="flex flex-col h-screen max-w-screen-lg xl:-ml-20 xl:pl-4 w-full -mt-4 pt-4 md:mx-10 ">
<!-- Nav between steps-->
<div class="flex flex-col w-full">
<div class="justify-between flex flex-row drop-shadow-sm w-full">
<div class="justify-between flex flex-row drop-shadow-sm w-full mt-4">
<div class="wizard-nav flex flex-row w-full">
<button
class="{step === 1
@@ -279,7 +279,6 @@
/* .wizard-nav {
@apply w-1/2 sm:w-1/4;
} */
.wizard-nav button {
max-height: 30px;
}
@@ -4,7 +4,6 @@
import { Dropdown, DropdownItem } from 'flowbite-svelte'
import Icon from 'svelte-awesome'
import Editor from '../Editor.svelte'
import FlowPreview from '../FlowPreview.svelte'
import FlowViewer from '../FlowViewer.svelte'
import Modal from '../Modal.svelte'
import RadioButton from '../RadioButton.svelte'
@@ -129,31 +128,29 @@
rows="1"
/>
</label>
</div>
<div class="mx-6">
<RadioButton
options={[
[
{
title: 'UI or webhook triggered',
desc: 'Trigger this flow through the generated UI, a manual schedule or by calling the associated webhook'
},
'push'
],
[
{
title: 'Watching changes regularly',
desc: 'The first module of this flow is a trigger script whose purpose is to pull data from an external source and return all new items since last run. This flow is meant to be scheduled very regularly to reduce latency to react to new events. It will trigger the rest of the flow once per item. If no new items, the flow will be skipped.'
},
'pull'
]
]}
bind:value={$mode}
/>
</div>
{#if $mode == 'pull'}
<div class="p-4">
<div class="mt-4">
<RadioButton
options={[
[
{
title: 'UI or webhook triggered',
desc: 'Trigger this flow through the generated UI, a manual schedule or by calling the associated webhook'
},
'push'
],
[
{
title: 'Watching changes regularly',
desc: 'The first module of this flow is a trigger script whose purpose is to pull data from an external source and return all new items since last run. This flow is meant to be scheduled very regularly to reduce latency to react to new events. It will trigger the rest of the flow once per item. If no new items, the flow will be skipped.'
},
'pull'
]
]}
bind:value={$mode}
/>
</div>
{#if $mode == 'pull'}
<CollapseLink text="set primary schedule" open={true}>
<Tooltip>
The primary schedule of a flow is simply a schedule that has the same name as a flow. It
@@ -177,15 +174,6 @@
</div>
<SchemaForm schema={$flowStore.schema} bind:args={scheduleArgs} />
</CollapseLink>
</div>
{/if}
<div class="p-6">
<FlowPreview
mode={$mode}
flow={$flowStore}
i={$flowStore?.value.modules.length}
bind:args={scheduleArgs}
/>
{/if}
</div>
</FlowBox>
+8 -8
View File
@@ -1,4 +1,9 @@
<script lang="ts">
import { goto } from '$app/navigation'
import { OpenAPI } from '$lib/gen'
import { logout } from '$lib/logout'
import { superadmin, userStore, usersWorkspaceStore, workspaceStore } from '$lib/stores'
import { clickOutside } from '$lib/utils'
import { faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons'
import {
faBookOpen,
@@ -21,11 +26,6 @@
import { onMount } from 'svelte'
import Icon from 'svelte-awesome'
import '../app.css'
import { OpenAPI } from '$lib/gen'
import { superadmin, userStore, usersWorkspaceStore, workspaceStore } from '$lib/stores'
import { clickOutside } from '$lib/utils'
import { logout } from '$lib/logout'
import { goto } from '$app/navigation'
OpenAPI.WITH_CREDENTIALS = true
@@ -342,9 +342,9 @@
</ul>
</nav>
<div
class="bg-white antialiased text-gray-900 pt-4 {isCollapsed
? 'pl-12'
: 'pl-44'} pr-8 flex h-full max-w-screen flex-col items-center"
class="bg-white antialiased text-gray-900 {isCollapsed
? 'pl-8'
: 'pl-36'} flex h-full max-w-screen flex-col items-center"
>
<slot />
</div>