Flow preview UI (#474)

* feat(frontend): Rework Flow preview UI

* feat(frontend): Rework Flow done

* feat(frontend): Fix SchemaForm height

* feat(frontend): Clean up
This commit is contained in:
Faton Ramadani
2022-08-23 19:58:03 +02:00
committed by GitHub
parent 2ab9e4c203
commit 169f680a6e
19 changed files with 459 additions and 326 deletions
-6
View File
@@ -3,12 +3,6 @@
@tailwind components;
@tailwind utilities;
html {
/* Avoid content shifting */
overflow-y: overlay;
}
@layer base {
h1 {
@apply text-2xl;
+118
View File
@@ -0,0 +1,118 @@
<script lang="ts">
import { onMount } from 'svelte'
import { createEventDispatcher } from 'svelte'
export let open = false
export let duration = 0.3
export let placement = 'right'
export let size = '600px'
let mounted = false
const dispatch = createEventDispatcher()
$: style = `--duration: ${duration}s; --size: ${size};`
function scrollLock(open: boolean) {
const body = document.querySelector('body')
if (mounted && body) {
body.style.overflowY = open ? 'hidden' : 'auto'
}
}
$: scrollLock(open)
function handleClickAway() {
dispatch('clickAway')
open = !open
}
onMount(() => {
mounted = true
scrollLock(open)
})
</script>
<aside class="drawer" class:open {style}>
<div class="overlay" on:click={handleClickAway} />
<div class="panel {placement}" class:size>
<slot />
</div>
</aside>
<style>
.drawer {
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
z-index: -1;
transition: z-index var(--duration) step-end;
}
.drawer.open {
z-index: 99;
transition: z-index var(--duration) step-start;
}
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(100, 100, 100, 0.5);
opacity: 0;
z-index: 2;
transition: opacity var(--duration) ease;
}
.drawer.open .overlay {
opacity: 1;
}
.panel {
position: fixed;
width: 100%;
background: white;
z-index: 3;
transition: transform var(--duration) ease;
overflow: auto;
}
.panel.left {
left: 0;
transform: translate(-100%, 0);
}
.panel.right {
right: 0;
transform: translate(100%, 0);
}
.panel.top {
top: 0;
transform: translate(0, -100%);
}
.panel.bottom {
bottom: 0;
transform: translate(0, 100%);
}
.panel.left.size,
.panel.right.size {
max-width: var(--size);
}
.panel.top.size,
.panel.bottom.size {
max-height: var(--size);
}
.drawer.open .panel {
transform: translate(0, 0);
}
</style>
+8 -21
View File
@@ -16,6 +16,7 @@
import { onDestroy, onMount } from 'svelte'
import Icon from 'svelte-awesome'
import { OFFSET } from './CronInput.svelte'
import Drawer from './Drawer.svelte'
import FlowEditor from './FlowEditor.svelte'
import FlowPreviewContent from './FlowPreviewContent.svelte'
import { flowStateStore, flowStateToFlow, type FlowState } from './flows/flowState'
@@ -27,13 +28,13 @@
export let initialPath: string = ''
let pathError = ''
let previewOpen = false
let scheduleArgs: Record<string, any>
let previewArgs: Record<string, any>
let scheduleEnabled: boolean
let scheduleCron: string
let previewOpen = false
$: step = Number($page.url.searchParams.get('step')) || 1
async function createSchedule(path: string) {
@@ -121,9 +122,6 @@
}
async function changeStep(step: number) {
if (step === 2 && previewOpen) {
previewOpen = false
}
goto(`?step=${step}`)
}
@@ -157,9 +155,7 @@
</script>
<div class="flex flex-row w-full h-full justify-between">
<div
class={`flex flex-col mb-96 m-auto w-full sm:w-3/4 lg:w-2/3 ${previewOpen ? 'xl:w-1/2' : ''}`}
>
<div class={`flex flex-col mb-96 m-auto w-full sm:w-3/4 lg:w-2/3`}>
<!-- Nav between steps-->
<div class="justify-between flex flex-row w-full my-4">
<Breadcrumb>
@@ -244,17 +240,8 @@
<p>Loading</p>
{/if}
</div>
<div class={`relative h-screen w-1/3 ${previewOpen ? '' : 'hidden'}`}>
<div class="absolute top-0 h-full">
{#if $flowStore && step === 1}
<div class="fixed border-l-2 right-0 h-screen w-1/2 sm:w-1/3">
<FlowPreviewContent
bind:args={previewArgs}
on:close={() => (previewOpen = !previewOpen)}
/>
</div>
{/if}
</div>
</div>
</div>
<Drawer bind:open={previewOpen} size="800px">
<FlowPreviewContent bind:args={previewArgs} on:close={() => (previewOpen = !previewOpen)} />
</Drawer>
@@ -2,6 +2,7 @@
import { ScheduleService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import FlowSettings from './flows/FlowSettings.svelte'
import { flowStateStore } from './flows/flowState'
import { flowStore } from './flows/flowStore'
@@ -1,27 +1,28 @@
<script lang="ts">
import type { CompletedJob } from '$lib/gen'
import ChevronButton from './ChevronButton.svelte'
import DisplayResult from './DisplayResult.svelte'
import Tabs from './tabs/Tabs.svelte'
import Tab from './tabs/Tab.svelte'
import TabPanel from './tabs/TabPanel.svelte'
let value = 0
export let job: CompletedJob | undefined
</script>
{#if job}
<div class="flex flex-col ml-10">
<div>
<ChevronButton text="result" viewOptions={true}>
<div class="text-xs">
<DisplayResult result={job.result} />
</div>
</ChevronButton>
</div>
<div>
<ChevronButton text="logs" viewOptions={true}>
<div class="text-xs p-4 bg-gray-50 overflow-auto max-h-80 border mt-1">
<pre class="w-full">{job.logs}</pre>
</div>
</ChevronButton>
</div>
<div>
<Tabs>
<Tab bind:value index={0}>Results</Tab>
<Tab bind:value index={1}>Logs</Tab>
</Tabs>
<TabPanel bind:value index={0} class="border p-2 h-36 overflow-y-scroll">
<DisplayResult result={job.result} />
</TabPanel>
<TabPanel bind:value index={1} class="border p-2 h-36 overflow-y-scroll">
<div class="text-xs p-4 bg-gray-50 overflow-auto max-h-80 border">
<pre class="w-full">{job.logs}</pre>
</div>
</TabPanel>
</div>
{/if}
+6 -26
View File
@@ -1,12 +1,11 @@
<script lang="ts">
import type { Schema } from '$lib/common'
import { Job, JobService, type Flow } from '$lib/gen'
import type { Job, JobService, Flow } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { sendUserToast, truncateRev } from '$lib/utils'
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'
import { onDestroy } from 'svelte'
import Icon from 'svelte-awesome'
import FlowJobResult from './FlowJobResult.svelte'
import { flowStateStore, flowStateToFlow } from './flows/flowState'
import { mapJobResultsToFlowState } from './flows/flowStateUtils'
import { runFlowPreview } from './flows/utils'
@@ -24,7 +23,6 @@
let tab: 'upto' | 'justthis' = 'upto'
let viewPreview = false
let intervalId: NodeJS.Timer
let uptoText =
i >= flow.value.modules.length - 1 ? 'Preview whole flow' : 'Preview up to this step'
@@ -33,15 +31,12 @@
export async function runPreview(args: any) {
viewPreview = true
intervalId && clearInterval(intervalId)
flow = flowStateToFlow($flowStateStore, flow)
let newFlow: Flow =
tab == 'upto' ? truncateFlow(flow) : setInputTransformFromArgs(extractStep(flow), args)
jobId = await runFlowPreview(args, newFlow)
intervalId = setInterval(loadJob, 1000)
sendUserToast(`started preview ${truncateRev(jobId, 10)}`)
}
@@ -69,22 +64,6 @@
flow.value.modules[0].input_transform = input_transform
return flow
}
async function loadJob() {
try {
job = await JobService.getJob({ workspace: $workspaceStore!, id: jobId })
if (job?.type == 'CompletedJob') {
//only CompletedJob has success property
clearInterval(intervalId)
}
} catch (err) {
sendUserToast(err, true)
}
}
onDestroy(() => {
intervalId && clearInterval(intervalId)
})
</script>
<button
@@ -134,10 +113,11 @@
{#if job}
<div class="w-full flex justify-center">
<FlowStatusViewer {job} on:jobsLoaded={(e) => mapJobResultsToFlowState(e.detail, tab, i)} />
<FlowStatusViewer
{jobId}
on:jobsLoaded={(e) => mapJobResultsToFlowState(e.detail, tab, i)}
root={true}
/>
</div>
{#if `result` in job}
<FlowJobResult {job} />
{/if}
{/if}
{/if}
@@ -2,16 +2,16 @@
import { Job, JobService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { sendUserToast, truncateRev } from '$lib/utils'
import { faClose } from '@fortawesome/free-solid-svg-icons'
import { faClose, faPlay } 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 { flowStateStore, flowStateToFlow } from './flows/flowState'
import { mapJobResultsToFlowState } from './flows/flowStateUtils'
import { flowStore } from './flows/flowStore'
import { runFlowPreview } from './flows/utils'
import FlowStatusViewer from './FlowStatusViewer.svelte'
import ProgressBar from './ProgressBar.svelte'
import SchemaForm from './SchemaForm.svelte'
export let args: Record<string, any> = {}
@@ -20,6 +20,7 @@
let job: Job | undefined
let jobId: string
let isValid: boolean = false
let intervalState: 'idle' | 'canceled' | 'done' | 'running' = 'idle'
$: newFlow = flowStateToFlow($flowStateStore, $flowStore)
$: steps = newFlow.value.modules.length
@@ -27,18 +28,20 @@
const dispatch = createEventDispatcher()
export async function runPreview(args: Record<string, any>) {
job = undefined
intervalId && clearInterval(intervalId)
jobId = await runFlowPreview(args, newFlow)
intervalId = setInterval(loadJob, 1000)
intervalState = 'running'
sendUserToast(`started preview ${truncateRev(jobId, 10)}`)
}
async function loadJob() {
try {
job = await JobService.getJob({ workspace: $workspaceStore!, id: jobId })
if (job?.type == 'CompletedJob') {
clearInterval(intervalId)
intervalState = 'done'
}
} catch (err) {
sendUserToast(err, true)
@@ -47,34 +50,52 @@
onDestroy(() => {
intervalId && clearInterval(intervalId)
intervalState = 'done'
})
</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 overflow-y-auto grow">
<div class="flex justify-between">
<h3 class="text-lg leading-6 font-bold text-gray-900">Flow Preview</h3>
<div class="flex flex-col space-y-8 h-screen bg-white p-6 w-full">
<div class="flex justify-between">
<div class="flex flex-row justify-center items-center">
<div class="flex justify-center p-2 w-8 h-8 bg-blue-200 rounded-lg mr-2">
<Icon data={faPlay} scale={1} class="text-blue-500" />
</div>
<Button color="alternative" on:click={() => dispatch('close')}>
<Icon data={faClose} />
</Button>
<h3 class="text-lg leading-6 font-bold text-gray-900">Flow preview</h3>
</div>
<Button color="alternative" on:click={() => dispatch('close')}>
<Icon data={faClose} />
</Button>
</div>
<div class="max-h-80 overflow-y-auto">
<SchemaForm schema={$flowStore.schema} bind:isValid bind:args />
</div>
<Button disabled={!isValid} class="blue-button mx-4" on:click={() => runPreview(args)} size="md">
Preview
</Button>
{#if intervalState === 'running'}
<Button
disabled={!isValid}
color="red"
on:click={() => {
clearInterval(intervalId)
intervalState = 'canceled'
job = undefined
}}
size="md"
>
Cancel
</Button>
{:else}
<Button disabled={!isValid} class="blue-button" on:click={() => runPreview(args)} size="md">
{`Run${intervalState === 'done' ? ' again' : ''}`}
</Button>
{/if}
<div class="h-full overflow-y-auto mb-16 grow">
{#if job}
<div class="w-full">
<FlowStatusViewer
{job}
on:jobsLoaded={(e) => mapJobResultsToFlowState(e.detail, 'upto', steps - 1)}
/>
</div>
{#if `result` in job}
<FlowJobResult {job} />
{/if}
<FlowStatusViewer
jobId={job.id}
on:jobsLoaded={(e) => mapJobResultsToFlowState(e.detail, 'upto', steps - 1)}
root={true}
/>
{/if}
</div>
</div>
@@ -1,243 +1,128 @@
<script lang="ts">
import { scriptPathToHref, truncateRev } from '$lib/utils'
import { faHourglassHalf, faSpinner, faTimes } from '@fortawesome/free-solid-svg-icons'
import { scriptPathToHref } from '$lib/utils'
import Icon from 'svelte-awesome'
import { check } from 'svelte-awesome/icons'
import { CompletedJob, FlowStatusModule, Job, JobService, QueuedJob } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { Job, JobService } from '$lib/gen'
import { arePreviewsReady, workspaceStore } from '$lib/stores'
import FlowJobResult from './FlowJobResult.svelte'
import JobStatus from './JobStatus.svelte'
import IconedPath from './IconedPath.svelte'
import FlowPreviewStatus from './preview/FlowPreviewStatus.svelte'
import { Button } from 'flowbite-svelte'
import Icon from 'svelte-awesome'
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'
import ProgressBar from './ProgressBar.svelte'
import { createEventDispatcher } from 'svelte'
import type { JobResult } from './flows/flowStateUtils'
const dispatch = createEventDispatcher()
export let job: QueuedJob | CompletedJob
export let fullyRetrieved = -1
export let jobId: string
export let root: boolean = false
export let forloopJobIds: string[] | undefined = undefined
export let jobResult: JobResult = {
job: undefined,
innerJobs: [],
loopJobs: []
}
let lastJobid: string | undefined
let forloop_selected = ''
let pres: { [key: number]: HTMLElement } = {}
let isReadyIndex = $arePreviewsReady.push(false)
$: jobs = [] as Array<any>
$: jobs && dispatch('jobsLoaded', jobs)
$: $workspaceStore && job && loadResults()
async function loadResults() {
if (!('success' in job)) {
const mods = job?.flow_status?.modules
if (mods) {
let i = mods?.findIndex((x) => x.type == FlowStatusModule.type.IN_PROGRESS)
if (i != -1) {
let last = mods[i]
jobs[i] = await JobService.getJob({
workspace: $workspaceStore ?? '',
id: last.job ?? ''
})
jobs = jobs
pres[i]?.scroll({ top: pres[i]?.scrollHeight, behavior: 'smooth' })
}
}
}
if (job.id != lastJobid) {
lastJobid = job.id
jobs = []
fullyRetrieved = -1
}
job?.flow_status?.modules?.forEach(async (x, i) => {
if (
(i > fullyRetrieved && x.type == FlowStatusModule.type.SUCCESS) ||
x.type == FlowStatusModule.type.FAILURE
) {
const completedJob = await JobService.getCompletedJob({
workspace: $workspaceStore!,
id: x.job!
})
if (x.forloop_jobs) {
const forloop_jobs: CompletedJob[] = []
for (let j of x.forloop_jobs) {
forloop_jobs.push(
await JobService.getCompletedJob({ workspace: $workspaceStore!, id: j })
)
}
jobs[i] = forloop_jobs
} else {
jobs[i] = completedJob
}
jobs = jobs
fullyRetrieved = i
}
async function loadJobInProgress() {
const job = await JobService.getJob({
workspace: $workspaceStore ?? '',
id: jobId ?? ''
})
jobResult.job = job
jobResult = jobResult
if (job.type === 'CompletedJob') {
arePreviewsReady.update((isReady: boolean[]) => {
isReady[isReadyIndex - 1] = true
return isReady
})
} else {
loadJobInProgress()
}
}
function toJob(x: any): Job {
return x as Job
$: {
if (root) {
if ($arePreviewsReady.every(Boolean) && !(hasModules && $arePreviewsReady.length === 1)) {
arePreviewsReady.update(() => [])
dispatch('jobsLoaded', jobResult)
}
}
}
function toCompletedJob(x: any): CompletedJob {
return x as CompletedJob
}
function toCompletedJobs(x: any): CompletedJob[] {
return x as CompletedJob[]
}
$: job = jobResult.job
$: innerJobs = jobResult.innerJobs
$: loopJobs = jobResult.loopJobs
$: hasModules = job && Array.isArray(job?.raw_flow?.modules) && job?.raw_flow?.modules.length! > 1
$: loadJobInProgress()
</script>
<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>
{#if job}
<div class="flow-root w-full space-y-4">
<h3 class="text-md leading-6 font-bold text-gray-900 border-b pb-2">Preview results</h3>
<FlowPreviewStatus {job} />
{#if `result` in job}
<FlowJobResult {job} />
{/if}
{#if Array.isArray(forloopJobIds) && forloopJobIds?.length > 0 && Array.isArray(loopJobs)}
<h3 class="text-md leading-6 font-bold text-gray-900 border-b mb-4">
Loop results ({forloopJobIds.length} items)
</h3>
{#each forloopJobIds as loopJobId, j}
<Button
color={forloop_selected == loopJobId ? 'dark' : 'light'}
class="flex justify-between w-full"
on:click={() => {
if (forloop_selected == loopJobId) {
forloop_selected = ''
} else {
forloop_selected = loopJobId
}
}}
>
Iteration: #{j}: {loopJobId}
<Icon
class="ml-2"
data={forloop_selected == loopJobId ? faChevronUp : faChevronDown}
scale={0.8}
/>
</Button>
<div class="border p-6" class:hidden={forloop_selected != loopJobId}>
<svelte:self jobId={loopJobId} bind:jobResult={loopJobs[j]} />
</div>
{/each}
{:else if hasModules && 'result' in job && Array.isArray(innerJobs)}
<ul class="w-full">
<h3 class="text-md leading-6 font-bold text-gray-900 border-b mb-4 py-2">
Detailed results
</h3>
{#each job?.flow_status?.modules ?? [] as module, i}
<p class="text-gray-500 mb-6 w-full ">
Step
<span class="font-medium text-gray-900"> {i + 1} </span> out of
<span class="font-medium text-gray-900">{job?.raw_flow?.modules.length}</span>
</p>
<li class="w-full border p-6 space-y-2">
<svelte:self
jobId={module.job}
bind:jobResult={innerJobs[i]}
forloopJobIds={module.forloop_jobs}
/>
</li>
{/each}
</ul>
{/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>
out of <span class="font-medium text-gray-900">{job?.raw_flow?.modules.length}</span>
<span class="mt-4" />
</p>
<ul class="w-full">
{#each job?.raw_flow?.modules ?? [] as mod, i}
<li class="w-full">
<div class="relative w-full">
{#if i < (job?.raw_flow?.modules ?? []).length - 1}
<span
class="absolute top-4 left-4 -ml-px h-full w-0.5 bg-gray-200"
aria-hidden="true"
/>
{/if}
<div class="relative flex space-x-3">
<div>
{#if job.flow_status?.modules[i].type == FlowStatusModule.type.SUCCESS}
<span
class="h-8 w-8 rounded-full bg-green-600 flex items-center justify-center ring-8 ring-white"
>
<Icon
class="text-white"
data={check}
scale={0.8}
label="Job completed successfully"
/>
</span>
{:else if job.flow_status?.modules[i].type == FlowStatusModule.type.FAILURE}
<span
class="h-8 w-8 rounded-full bg-red-600 flex items-center justify-center ring-8 ring-white"
>
<Icon class="text-white" data={faTimes} scale={0.8} label="Job failed" />
</span>
{:else if job.flow_status?.modules[i].type == FlowStatusModule.type.IN_PROGRESS}
<span
class="h-8 w-8 rounded-full bg-yellow-500 flex items-center justify-center ring-8 ring-white"
>
<Icon
class="text-white animate-spin"
data={faSpinner}
scale={1}
label="Job failed"
/>
</span>
{:else}
<span
class="h-8 w-8 rounded-full bg-gray-400 flex items-center justify-center ring-8 ring-white"
>
<Icon class="text-white" data={faHourglassHalf} scale={1} label="Job failed" />
</span>
{/if}
</div>
<div class="min-w-0 flex-1 pt-1.5 flex justify-between space-x-4 w-full">
<div class="w-full">
<p class="text-sm text-gray-500">
{#if mod.value.type == 'script'}
Script at path <a
target="_blank"
href={scriptPathToHref(mod.value.path ?? '')}
class="font-medium text-gray-900"
>
<IconedPath path={mod.value.path} />
</a>
{/if}
</p>
</div>
<div class="text-right text-sm whitespace-nowrap text-gray-500">
{job.flow_status?.modules[i].type}
<div class=" max-h-40 overflow-y-auto">
{#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>
</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>
{/if}
</div>
</div>
</div>
</div>
{#if jobs[i]}
{#if Array.isArray(jobs[i])}
<div class="flex flex-col mt-2 space-y-2 max-h-60 overflow-y-auto shadow-inner">
{#each toCompletedJobs(jobs[i]) as job, i}
<button
class="underline text-blue-600 hover:text-blue-700"
class:text-red-600={!job.success}
on:click={() => {
if (forloop_selected == job.id) {
forloop_selected = ''
} else {
forloop_selected = job.id
}
}}
>Iteration: #{i}: {job.id} {forloop_selected == job.id ? '(-)' : '(+)'}</button
>
{#if forloop_selected == job.id}
<svelte:self {job} />
{#if `result` in job}
<FlowJobResult {job} />
{/if}
{/if}
{/each}
</div>
{:else if toJob(jobs[i]).type == 'CompletedJob'}
<FlowJobResult job={toCompletedJob(jobs[i])} />
{:else if jobs[i]}
{#if toJob(jobs[i])?.raw_flow}
<div class="border-2">
<h2>Forloop current iteration</h2>
<svelte:self job={jobs[i]} />
</div>
{:else}
<div class="max-w-2xl mt-2 h-full">
<pre
bind:this={pres[i]}
class="break-all p-4 relative h-full mx-2 bg-gray-50 text-xs max-h-40 overflow-y-auto border">{toJob(
jobs[i]
).logs ?? ''}
</pre>
</div>
{/if}
{/if}
{/if}
</div>
</li>
{/each}
</ul>
</div>
{:else}
Loading
{/if}
+1 -1
View File
@@ -22,7 +22,7 @@
{#if job && 'success' in job && job.success}
<Badge large color="green">
<Icon data={faCheck} scale={SMALL_ICON_SCALE} class="mr-2" />
Succeeded {job.is_skipped ? '(Skipped)' : ''}
Success {job.is_skipped ? '(Skipped)' : ''}
</Badge>
<Badge large>
@@ -142,7 +142,7 @@
{#if !shouldPick}
<div class="border-b border-gray-200" />
<div class="p-3">
<div class="pt-2">
<FlowPreview bind:args flow={$flowStore} {i} {schema} />
</div>
{/if}
@@ -0,0 +1,36 @@
<script lang="ts">
import ProgressBarPart from './ProgressBarPart.svelte'
export let i = -1
export let steps: number = 0
$: series = Array.from(Array(steps).keys()).map((x) => Math.floor(((100 / steps) * x) / 100))
function toggle() {
toggled[i] = true
}
$: toggled = series.map(() => false)
$: text = i < series.length ? `Step ${i + 1}` : 'Done'
$: toggled[i] === false && toggle()
</script>
<div>
<div class="flex justify-between mb-1">
<span class="text-base font-medium text-blue-700 dark:text-white">{text}</span>
<span class="text-sm font-medium text-blue-700 dark:text-white">
{series.slice(0, i).reduce((x, y) => y + x, 0)}%
</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700 relative">
{#each series as serie, index}
<ProgressBarPart
isFirst={index === 0}
isLast={index === series.length - 1}
sumUpTo={series.slice(0, index).reduce((x, y) => y + x, 0)}
length={serie}
shouldToggle={toggled[index]}
/>
{/each}
</div>
</div>
@@ -0,0 +1,25 @@
<script lang="ts">
import { tweened } from 'svelte/motion'
import { cubicOut } from 'svelte/easing'
const progress = tweened(0, {
duration: 400,
easing: cubicOut
})
export let isFirst: boolean
export let isLast: boolean
export let sumUpTo: number
export let length: number
export let shouldToggle: boolean
$: shouldToggle && progress.set(length)
</script>
<div
class={`bg-blue-200 h-2.5 absolute ${isFirst ? 'rounded-l-full' : ''} ${
isLast ? 'rounded-r-full' : ''
}`}
style={`${isFirst ? 'left:0%;' : `left:${sumUpTo}%;`} width: ${$progress}%`}
/>
@@ -1,5 +1,12 @@
import type { Schema } from '$lib/common'
import { CompletedJob, ScriptService, type Flow, type FlowModule, type RawScript } from '$lib/gen'
import {
CompletedJob,
Job,
ScriptService,
type Flow,
type FlowModule,
type RawScript
} from '$lib/gen'
import { initialCode } from '$lib/script_helpers'
import { userStore, workspaceStore } from '$lib/stores'
import {
@@ -15,7 +22,6 @@ import { get } from 'svelte/store'
import { flowStateStore, type FlowModuleSchema, type FlowState } from './flowState'
import { flowStore } from './flowStore'
import { jobsToResults, loadSchemaFromModule } from './utils'
export function emptyFlowModuleSchema(): FlowModuleSchema {
return {
flowModule: emptyModule(),
@@ -264,24 +270,42 @@ function extractPreviewResults(flowModuleSchemas: FlowModuleSchema[]) {
return flowModuleSchemas.map((fms) => fms.previewResult)
}
export type JobResult = {
job?: Job
innerJobs?: JobResult[]
loopJobs?: JobResult[]
}
export function mapJobResultsToFlowState(
jobs: CompletedJob[],
jobs: JobResult,
config: 'upto' | 'justthis',
configIndex: number
): void {
if (!Array.isArray(jobs) || jobs.length === 0) {
if (!Array.isArray(jobs.innerJobs) || jobs.innerJobs.length === 0) {
return
}
if (config === 'justthis') {
const [result] = jobsToResults(jobs)
const job = jobs.job as CompletedJob
flowStateStore.update((flowState: FlowState) => {
flowState[configIndex] = result
flowState[configIndex] = job.result
return flowState
})
} else {
const result = jobsToResults(jobs)
const results = jobs.innerJobs.map(({ job, loopJobs }) => {
if (Array.isArray(loopJobs) && loopJobs.length > 0) {
return loopJobs.map(({ job }) => {
if (job && 'result' in job) {
return job.result
}
})
} else {
if (job && 'result' in job) {
return job.result
}
}
})
flowStateStore.update((flowState: FlowState) => {
if (!Array.isArray(flowState)) {
@@ -290,7 +314,7 @@ export function mapJobResultsToFlowState(
return flowState.map((flowModuleSchema: FlowModuleSchema, index) => {
if (index <= configIndex) {
flowModuleSchema.previewResult = result[index]
flowModuleSchema.previewResult = results[index]
}
return flowModuleSchema
@@ -0,0 +1,31 @@
<script lang="ts">
import type { CompletedJob, QueuedJob } from '$lib/gen'
import JobStatus from '../JobStatus.svelte'
export let job: QueuedJob | CompletedJob
</script>
<div class="overflow-x-auto relative">
<table class="text-sm text-left text-gray-500 dark:text-gray-400">
<tbody>
<tr>
<th scope="row" class="py-2 pr-6 font-bold text-gray-900 whitespace-nowrap dark:text-white">
Status
</th>
<td class="py-2 "> <JobStatus {job} /></td>
</tr>
{#if job}
<tr>
<th scope="row" class="py-2 pr-6 font-bold text-gray-900 whitespace-nowrap ">
Job Id
</th>
<td class="py-2">
<a href="/run/{job?.id}">
{job?.id}
</a>
</td>
</tr>
{/if}
</tbody>
</table>
</div>
@@ -0,0 +1,13 @@
<script lang="ts">
export let value: number
export let index: number
</script>
<div
class={value === index
? 'border-b-2 border-gray-900 text-gray-900 py-1 px-2 cursor-pointer font-bold text-sm'
: 'py-1 px-2 cursor-pointer font-medium text-sm'}
on:click={() => (value = index)}
>
<slot />
</div>
@@ -0,0 +1,12 @@
<script lang="ts">
export let value: number
export let index: number
let clazz: string = ''
export { clazz as class }
</script>
{#if value === index}
<div class={clazz}>
<slot />
</div>
{/if}
@@ -0,0 +1,3 @@
<div class="border-b border-gray-200 flex flex-row space-x-4 mb-4">
<slot />
</div>
+2
View File
@@ -51,3 +51,5 @@ export function clearStores(): void {
usersWorkspaceStore.set(undefined)
superadmin.set(undefined)
}
export const arePreviewsReady = writable<boolean[]>([])
+1 -1
View File
@@ -330,7 +330,7 @@
{#if job?.job_kind == 'flow' || job?.job_kind == 'flowpreview'}
<div class="mt-10" />
<div class="max-w-lg">
<FlowStatusViewer {job} />
<FlowStatusViewer jobId={job.id} root={true} />
</div>
{/if}
</div>