feat(frontend): flow status viewer include a graph

This commit is contained in:
Ruben Fiszel
2022-11-27 09:49:16 +01:00
parent dc6bcae03d
commit 5b618ec1d0
11 changed files with 255 additions and 187 deletions
@@ -2,7 +2,7 @@
import { Job, JobService, type Flow, type FlowModule } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { faClose, faPlay, faRefresh } from '@fortawesome/free-solid-svg-icons'
import { Button } from './common'
import { Button, Kbd } from './common'
import { createEventDispatcher, getContext } from 'svelte'
import Icon from 'svelte-awesome'
import { dfs, flowStore } from './flows/flowStore'
@@ -96,12 +96,12 @@
<svelte:window on:keydown={onKeyDown} />
<div class="flex divide-y flex-col space-y-2 h-screen bg-white px-6 py-2 w-full">
<div class="flex flex-row justify-between w-full items-center gap-x-1">
<div class="flex flex-row justify-between w-full items-center gap-x-2">
<Button
variant="border"
size="lg"
color="dark"
btnClasses="!p-0 !w-8 !h-8"
btnClasses="!p-0 !w-16 !h-full"
on:click={() => {
dispatch('close')
}}
@@ -109,80 +109,70 @@
<Icon data={faClose} />
</Button>
<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>
<h3
class="text-lg leading-6 font-bold text-gray-900 inline-flex flex-row justify-between w-full"
{#if isRunning}
<Button
disabled={!isValid}
color="red"
on:click={async () => {
isRunning = false
try {
jobId &&
(await JobService.cancelQueuedJob({
workspace: $workspaceStore ?? '',
id: jobId,
requestBody: {}
}))
} catch {}
jobId = undefined
}}
size="md"
btnClasses="w-full"
>
<div>
Test {previewMode === 'upTo'
? `up to step ${$selectedId.split('-').join(',')}`
: ' whole flow'}
</div>
</h3>
</div>
Cancel
</Button>
{:else}
<Button
variant="contained"
startIcon={{ icon: isRunning ? faRefresh : faPlay }}
color="blue"
size="sm"
btnClasses="w-full"
disabled={!isValid}
on:click={() => runPreview($previewArgs)}
>
Test flow <Kbd class="ml-2">Ctrl+Enter</Kbd>
</Button>
{/if}
<Button
btnClasses="ml-2"
btnClasses="h-full"
size="sm"
variant="border"
on:click={() => {
capturePayload.openDrawer()
}}>Fill test args from payload</Button
}}>Fill test args from a request</Button
>
</div>
<div class="h-full grow pb-20 max-h-60 overflow-auto">
<FlowProgressBar {job} bind:reset={jobProgressReset} />
<div class="overflow-y-auto grow flex-col flex divide-y divide-gray-600 ">
<SchemaForm
class="h-full pt-4"
compact
class="py-4"
schema={$flowStore.schema}
bind:isValid
bind:args={$previewArgs}
/>
</div>
{#if isRunning}
<Button
disabled={!isValid}
color="red"
on:click={async () => {
isRunning = false
try {
jobId &&
(await JobService.cancelQueuedJob({
workspace: $workspaceStore ?? '',
id: jobId,
requestBody: {}
}))
} catch {}
jobId = undefined
}}
size="md"
>
Cancel
</Button>
{:else}
<Button
variant="contained"
endIcon={{ icon: isRunning ? faRefresh : faPlay }}
color="blue"
btnClasses="w-full"
disabled={!isValid}
on:click={() => runPreview($previewArgs)}
>
Test flow (Ctrl/Cmd + Enter)
</Button>
{/if}
<FlowProgressBar {job} bind:reset={jobProgressReset} class="py-4" />
<div class="h-full overflow-y-auto mb-16 pt-4 grow">
{#if jobId}
<FlowStatusViewer
bind:flowState={$flowStateStore}
{jobId}
on:jobsLoaded={({ detail }) => onJobsLoaded(detail)}
/>
{/if}
<div class="h-full pt-4 grow">
{#if jobId}
<FlowStatusViewer
bind:flowState={$flowStateStore}
{jobId}
on:jobsLoaded={({ detail }) => onJobsLoaded(detail)}
/>
{:else}
<div class="italic text-gray-500 h-full grow"> Flow status will be displayed here </div>
{/if}
</div>
</div>
</div>
@@ -8,8 +8,10 @@
import { createEventDispatcher } from 'svelte'
import { onDestroy } from 'svelte'
import type { FlowState } from './flows/flowState'
import { Button } from './common'
import { Button, Tab } from './common'
import DisplayResult from './DisplayResult.svelte'
import Tabs from './common/tabs/Tabs.svelte'
import { FlowGraph } from './graph'
const dispatch = createEventDispatcher()
@@ -23,12 +25,28 @@
}
| undefined = undefined
export let job: Job | undefined = undefined
export let flowModuleStates: Record<string, FlowStatusModule.type> = {}
let localFlowModuleStates: Record<string, FlowStatusModule.type> = {}
let jobResults: any[] = []
let jobFailures: boolean[] = []
let forloop_selected = ''
let timeout: NodeJS.Timeout
$: localFlowModuleStates && updateFlowModuleStates()
function updateFlowModuleStates() {
Object.entries(localFlowModuleStates).forEach(([moduleId, state]) => {
if (
flowModuleStates[moduleId] !== state &&
flowModuleStates[moduleId] !== FlowStatusModule.type.FAILURE
) {
flowModuleStates[moduleId] = state
}
})
}
let lastSize = 0
$: {
let len = (flowJobIds?.flowJobs ?? []).length
@@ -70,6 +88,7 @@
function updateJobId() {
if (jobId !== job?.id) {
localFlowModuleStates = {}
loadJobInProgress()
}
}
@@ -81,10 +100,12 @@
onDestroy(() => {
timeout && clearTimeout(timeout)
})
let selected: 'graph' | 'sequence' = 'graph'
</script>
{#if job}
<div class="flow-root w-full space-y-4 ">
<div class="flow-root w-full space-y-4">
{#if innerModules.length > 0}
<h3 class="text-md leading-6 font-bold text-gray-900 border-b pb-2">Flow result</h3>
{/if}
@@ -107,119 +128,153 @@
</div>
{/if}
{#if isListJob}
<h3 class="text-md leading-6 font-bold text-gray-600 border-b mb-4">
Embedded flows: ({flowJobIds?.flowJobs.length} items)
</h3>
{#each flowJobIds?.flowJobs ?? [] as loopJobId, j}
<Button
variant={forloop_selected === loopJobId ? 'contained' : 'border'}
color={forloop_selected === loopJobId ? 'dark' : 'light'}
btnClasses="w-full flex justify-start"
on:click={() => {
if (forloop_selected == loopJobId) {
forloop_selected = ''
} else {
forloop_selected = loopJobId
}
}}
>
<span class="truncate">
#{j + 1}: {loopJobId}
</span>
{#if innerModules.length > 0 && !isListJob}
<Tabs bind:selected>
<Tab value="graph"><span class="font-semibold text-md">Graph</span></Tab>
<Tab value="sequence"><span class="font-semibold">Details</span></Tab>
</Tabs>
{/if}
<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
{flowState}
jobId={loopJobId}
on:jobsLoaded={(e) => {
if (flowJobIds?.moduleId) {
if (flowState) {
if (
!flowState[flowJobIds.moduleId].previewResult ||
!Array.isArray(flowState[flowJobIds.moduleId]?.previewResult)
) {
flowState[flowJobIds.moduleId].previewResult = []
}
flowState[flowJobIds.moduleId].previewResult[j] = e.detail.result
flowState[flowJobIds.moduleId].previewArgs = e.detail.args
jobResults[j] = e.detail.result == null ? 'Job in progress ...' : e.detail.result
}
<div class="{selected == 'graph' && !isListJob ? 'hidden' : ''} ">
{#if isListJob}
<h3 class="text-md leading-6 font-bold text-gray-600 border-b mb-4">
Embedded flows: ({flowJobIds?.flowJobs.length} items)
</h3>
{#each flowJobIds?.flowJobs ?? [] as loopJobId, j}
<Button
variant={forloop_selected === loopJobId ? 'contained' : 'border'}
color={jobFailures[j] === true
? 'red'
: forloop_selected === loopJobId
? 'dark'
: 'light'}
btnClasses="w-full flex justify-start"
on:click={() => {
if (forloop_selected == loopJobId) {
forloop_selected = ''
} else {
forloop_selected = loopJobId
}
}}
/>
</div>
{/each}
{:else if innerModules.length > 0}
<ul class="w-full">
<h3 class="text-md leading-6 font-bold text-gray-900 border-b mb-4 py-2">
Step-by-step results
</h3>
>
<span class="truncate">
#{j + 1}: {loopJobId}
</span>
{#each innerModules as mod, i}
<div class="line w-8 h-10" />
<h3 class="text-gray-500 mb-2 w-full">
{#if job?.raw_flow?.modules && i < job?.raw_flow?.modules.length}
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>
{#if job.raw_flow?.modules[i]?.summary}
: <span class="font-medium text-gray-900">
{job.raw_flow?.modules[i]?.summary ?? ''}
<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
bind:flowState
bind:flowModuleStates={localFlowModuleStates}
jobId={loopJobId}
on:jobsLoaded={(e) => {
if (flowJobIds?.moduleId) {
if (flowState) {
if (
!flowState[flowJobIds.moduleId].previewResult ||
!Array.isArray(flowState[flowJobIds.moduleId]?.previewResult)
) {
flowState[flowJobIds.moduleId].previewResult = []
}
flowState[flowJobIds.moduleId].previewResult[j] = e.detail.result
flowState[flowJobIds.moduleId].previewArgs = e.detail.args
jobResults[j] =
e.detail.result == null ? 'Job in progress ...' : e.detail.result
jobFailures[j] = e.detail.success === false
}
}
}}
/>
</div>
{/each}
{:else if innerModules.length > 0}
<ul class="w-full">
<h3 class="text-md leading-6 font-bold text-gray-900 border-b mb-4 py-2">
Step-by-step results
</h3>
{#each innerModules as mod, i}
<div class="line w-8 h-10" />
<h3 class="text-gray-500 mb-2 w-full">
{#if job?.raw_flow?.modules && i < job?.raw_flow?.modules.length}
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>
{#if job.raw_flow?.modules[i]?.summary}
: <span class="font-medium text-gray-900">
{job.raw_flow?.modules[i]?.summary ?? ''}
</span>
{/if}
{:else}
<h3>Failure module</h3>
{/if}
</h3>
<div class="line w-8 h-10" />
<li class="w-full border border-gray-600 p-6 space-y-2 bg-blue-50/50">
{#if [FlowStatusModule.type.IN_PROGRESS, FlowStatusModule.type.SUCCESS, FlowStatusModule.type.FAILURE].includes(mod.type)}
<svelte:self
bind:flowState
bind:flowModuleStates={localFlowModuleStates}
jobId={mod.job}
flowJobIds={mod.flow_jobs
? {
moduleId: mod.id,
flowJobs: mod.flow_jobs
}
: undefined}
on:jobsLoaded={(e) => {
if (mod.id && (mod.flow_jobs ?? []).length == 0) {
if (flowState && flowState[mod.id]) {
flowState[mod.id].previewResult = e.detail.result
flowState[mod.id].previewArgs = e.detail.args
}
if (e.detail.type == 'QueuedJob') {
localFlowModuleStates[mod.id] = FlowStatusModule.type.IN_PROGRESS
} else {
localFlowModuleStates[mod.id] = e.detail.success
? FlowStatusModule.type.SUCCESS
: FlowStatusModule.type.FAILURE
}
}
}}
/>
{:else}
<span class="italic text-gray-600">
<Icon data={faHourglassHalf} class="mr-2" />
{#if mod.type == FlowStatusModule.type.WAITING_FOR_EVENT}
Waiting to be resumed by receivent events such as approvals
{:else if mod.type == FlowStatusModule.type.WAITING_FOR_PRIOR_STEPS}
Waiting for prior steps to complete
{:else if mod.type == FlowStatusModule.type.WAITING_FOR_EXECUTOR}
Job is ready to be executed and will be picked up by the next available worker
{/if}
</span>
{/if}
{:else}
<h3>Failure module</h3>
{/if}
</h3>
<div class="line w-8 h-10" />
<li class="w-full border border-gray-600 p-6 space-y-2 bg-blue-50/50">
{#if [FlowStatusModule.type.IN_PROGRESS, FlowStatusModule.type.SUCCESS, FlowStatusModule.type.FAILURE].includes(mod.type)}
<svelte:self
{flowState}
jobId={mod.job}
flowJobIds={mod.flow_jobs
? {
moduleId: mod.id,
flowJobs: mod.flow_jobs
}
: undefined}
on:jobsLoaded={(e) => {
if (mod.id && (mod.flow_jobs ?? []).length == 0) {
if (flowState && flowState[mod.id]) {
flowState[mod.id].previewResult = e.detail.result
flowState[mod.id].previewArgs = e.detail.args
}
}
}}
/>
{:else}
<span class="italic text-gray-600">
<Icon data={faHourglassHalf} class="mr-2" />
{#if mod.type == FlowStatusModule.type.WAITING_FOR_EVENT}
Waiting to be resumed by receivent events such as approvals
{:else if mod.type == FlowStatusModule.type.WAITING_FOR_PRIOR_STEPS}
Waiting for prior steps to complete
{:else if mod.type == FlowStatusModule.type.WAITING_FOR_EXECUTOR}
Job is ready to be executed and will be picked up by the next available worker
{/if}
</span>
{/if}
</li>
{/each}
</ul>
{/if}
</li>
{/each}
</ul>
{/if}
</div>
</div>
{#if job.raw_flow && !isListJob}
<div class="{selected != 'graph' ? 'hidden' : ''} lg:mx-40 mx-10 border border-gray-400 mt-4">
<FlowGraph
flowModuleStates={localFlowModuleStates}
notSelectable
modules={job.raw_flow?.modules ?? []}
failureModule={job.raw_flow?.failure_module}
/>
</div>
{/if}
{:else}
Job loading...
{/if}
@@ -48,8 +48,8 @@
<div class="relative" bind:this={menu}>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div on:click={() => (show = !show)}>
<slot name="trigger" />
<div on:click={() => (show = !show)} class="cursor-pointer hover:bg-gray-100/30">
<slot class="triggerable" name="trigger" />
</div>
{#if show}
@@ -74,7 +74,7 @@
dispatch('new', {
language: RawScript.language.PYTHON3,
kind,
subkind: kind
subkind: 'flow'
})
}}
/>
@@ -40,7 +40,10 @@
<Icon data={faArrowRotateForward} scale={0.8} />
</span>
<span
class={classNames('badge', Boolean(module.suspend) ? 'badge-on' : 'badge-off')}
class={classNames(
'badge',
Boolean(module.suspend) || Boolean(module.sleep) ? 'badge-on' : 'badge-off'
)}
on:click={() => dispatch('toggleSuspend')}
>
<Icon data={faBed} scale={0.8} />
@@ -73,8 +73,9 @@
<h2 class="mt-4"
>Sleep<Tooltip>
If defined, at the end of the step, the flow will sleep for a number of seconds before being
resumed. Sleeping is passive and does not consume any resources.</Tooltip
If defined, at the end of the step, the flow will sleep for a number of seconds before
scheduling the next job (if any, no effect if the step is the last one). Sleeping is passive and
does not consume any resources.</Tooltip
></h2
>
<Toggle
@@ -1,5 +1,5 @@
import type { Schema } from '$lib/common'
import type { Flow, FlowModule } from '$lib/gen'
import type { Flow, FlowModule, FlowStatusModule } from '$lib/gen'
import { writable } from 'svelte/store'
import { loadFlowModuleState } from './flowStateUtils'
import { emptyFlowModuleState, isEmptyFlowModule } from './utils'
@@ -13,6 +13,7 @@
export let retry: boolean = false
export let earlyStop: boolean = false
export let suspend: boolean = false
export let sleep: boolean = false
export let bold: boolean = false
export let id: string | undefined = undefined
export let label: string
@@ -48,7 +49,7 @@
<Icon scale={0.8} data={faStop} />
</div>
{/if}
{#if suspend}
{#if suspend || sleep}
<div class="bg-white rounded border text-gray-600 px-1">
<Icon scale={0.8} data={faBed} />
</div>
@@ -28,7 +28,8 @@
selected: $selectedId === mod.id,
retry: mod.retry?.constant != undefined || mod.retry?.exponential != undefined,
earlyStop: mod.stop_after_if != undefined,
suspend: Boolean(mod.suspend)
suspend: Boolean(mod.suspend),
sleep: Boolean(mod.sleep)
}
function onDelete(event: CustomEvent<MouseEvent>) {
@@ -14,7 +14,7 @@ export async function loadFlowSchedule(path: string, workspace: string = ''): Pr
})
if (!existsSchedule) {
throw new Error(`Flow at path: ${path} doesn't exit`)
throw new Error(`Flow at path: ${path} doesn't exist`)
}
const schedule = await ScheduleService.getSchedule({
@@ -1,7 +1,7 @@
<script lang="ts">
import Svelvet, { type Edge } from 'svelvet'
import { sugiyama, dagStratify, decrossOpt, coordGreedy, coordCenter } from 'd3-dag'
import type { FlowModule, ForloopFlow, RawScript } from '../../gen'
import { sugiyama, dagStratify, decrossOpt, coordCenter } from 'd3-dag'
import { type FlowModule, FlowStatusModule, type ForloopFlow, RawScript } from '../../gen'
import {
NODE,
createIdGenerator,
@@ -15,14 +15,16 @@
type NestedNodes,
type ModuleHost
} from '.'
import { defaultIfEmptyString, truncate, truncateRev } from '$lib/utils'
import { defaultIfEmptyString, truncateRev } from '$lib/utils'
import { createEventDispatcher } from 'svelte'
import { numberToChars } from '../flows/utils'
import type { FlowState } from '../flows/flowState'
export let modules: FlowModule[] | undefined = []
export let failureModule: FlowModule | undefined = undefined
export let minHeight: number = 0
export let notSelectable = false
export let flowModuleStates: Record<string, FlowStatusModule.type> | undefined = undefined
let selectedNode: string | undefined = undefined
@@ -35,7 +37,7 @@
let dispatch = createEventDispatcher()
$: {
width && height && minHeight && selectedNode
width && height && minHeight && selectedNode && flowModuleStates
if (modules) {
createGraph(modules, failureModule)
} else {
@@ -137,6 +139,18 @@
return []
}
function getStateColor(state: FlowStatusModule.type | undefined): string {
switch (state) {
case FlowStatusModule.type.SUCCESS:
return 'rgb(34 197 94)'
case FlowStatusModule.type.FAILURE:
return 'rgb(248 113 113)'
case FlowStatusModule.type.IN_PROGRESS:
return 'rgb(253 224 71)'
default:
return '#fff'
}
}
function flowModuleToNode(
parentIds: string[],
id: string,
@@ -182,7 +196,10 @@
width: NODE.width,
height: NODE.height,
borderColor: selectedNode == onClickDetail.id ? 'black' : '#999',
bgColor: selectedNode == onClickDetail.id ? '#f5f5f5' : 'white',
bgColor:
selectedNode == onClickDetail.id
? '#f5f5f5'
: getStateColor(flowModuleStates?.[onClickDetail.id]),
parentIds,
clickCallback: (node) => {
if (!notSelectable) {