mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 16:02:19 +00:00
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:
@@ -3,12 +3,6 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html {
|
||||
|
||||
/* Avoid content shifting */
|
||||
overflow-y: overlay;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
h1 {
|
||||
@apply text-2xl;
|
||||
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
@@ -51,3 +51,5 @@ export function clearStores(): void {
|
||||
usersWorkspaceStore.set(undefined)
|
||||
superadmin.set(undefined)
|
||||
}
|
||||
|
||||
export const arePreviewsReady = writable<boolean[]>([])
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user