Files
windmill/frontend/src/lib/components/FlowTimeline.svelte

211 lines
6.6 KiB
Svelte

<script lang="ts">
import { displayDate, msToSec } from '$lib/utils'
import { Loader2 } from 'lucide-svelte'
import TimelineBar from './TimelineBar.svelte'
import WaitTimeWarning from './common/waitTimeWarning/WaitTimeWarning.svelte'
import { TimelineCompute } from '$lib/timelineCompute.svelte'
import { onMount, untrack } from 'svelte'
import OnChange from './common/OnChange.svelte'
import VirtualList from '@tutorlatin/svelte-tiny-virtual-list'
import type { GraphModuleState } from './graph'
import FlowJobsMenu from './flows/map/FlowJobsMenu.svelte'
import type { FlowModuleForTimeline } from './FlowStatusViewerInner.svelte'
interface Props {
selfWaitTime?: number | undefined
aggregateWaitTime?: number | undefined
flowModules: FlowModuleForTimeline[]
localModuleStates: Record<string, GraphModuleState>
onSelectedIteration?: (
detail:
| { id: string; index: number; manuallySet: true; moduleId: string }
| { manuallySet: false; moduleId: string }
) => Promise<void>
durationStatuses: Record<
string,
{
byJob: Record<string, { created_at?: number; started_at?: number; duration_ms?: number }>
}
>
flowDone?: boolean
buildSubflowKey: (key: string) => string
}
let {
selfWaitTime = undefined,
aggregateWaitTime = undefined,
flowModules,
durationStatuses,
flowDone = false,
localModuleStates,
onSelectedIteration
}: Props = $props()
let timelineCompute = $state<TimelineCompute | undefined>(undefined)
const flowModulesIds = $derived(flowModules.map(({ id }) => id))
// Initialize timeline compute when we have duration statuses
onMount(() => {
timelineCompute = new TimelineCompute(flowModulesIds, durationStatuses, flowDone)
return () => {
timelineCompute?.destroy()
}
})
$effect(() => {
flowDone
untrack(() => {
timelineCompute?.setFlowDone(flowDone)
})
})
// Derived timeline values
const min = $derived(timelineCompute?.min ?? undefined)
const max = $derived(timelineCompute?.max ?? undefined)
const total = $derived(timelineCompute?.total ?? undefined)
const items = $derived(timelineCompute?.items ?? undefined)
const now = $derived(timelineCompute?.now ?? Date.now())
export function reset() {
timelineCompute?.reset()
}
const barHeight = 32
</script>
<OnChange
key={durationStatuses}
onChange={() => {
timelineCompute?.updateInputs(flowModulesIds, durationStatuses, flowDone)
}}
/>
{#if items}
<div class="divide-y border-b">
<div class="px-2 py-2 grid grid-cols-12 w-full"
><div></div>
<div class="col-span-11 pt-1 px-2 flex text-2xs text-secondary justify-between"
><div>{min ? displayDate(new Date(min), true) : ''}</div>{#if max && min}<div
class="hidden lg:block">{msToSec(max - min, 1)}s</div
>
{/if}<div class="flex gap-1 items-center font-mono"
>{max ? displayDate(new Date(max), true) : ''}{#if !max && min}{#if now}
{msToSec(now - min, 1)}s
{/if}<Loader2 size={14} class="animate-spin" />{/if}</div
></div
>
</div>
<div class="flex flex-row-reverse items-center text-sm text-secondary p-2">
<div class="flex gap-4 items-center text-2xs">
<div class="flex gap-2 items-center">
<div>Waiting for executor/Suspend</div>
<div class="h-4 w-4 bg-gray-500"></div>
</div>
<div class="flex gap-2 items-center">
<div>Execution</div>
<div class="h-4 w-4 bg-blue-500/90"></div>
</div>
</div>
</div>
{#if selfWaitTime}
<div class="px-2 py-2 grid grid-cols-6 w-full">
root:
<WaitTimeWarning
self_wait_time_ms={selfWaitTime}
aggregate_wait_time_ms={aggregateWaitTime}
variant="badge-self-wait"
/>
</div>
{/if}
{#each flowModules as { id: k, type: typ } (k)}
{@const subItems = items?.[k]?.filter((x) => x.created_at && x.started_at)}
<div class="shadow-inner dark:shadow-gray-700 relative">
<div class="px-2 py-2 grid grid-cols-6 w-full">
<div class="truncate"
>{k.startsWith('subflow:') ? k.substring(8) : k}
{#if localModuleStates[k]?.selectedForloop && (typ == 'forloopflow' || typ == 'whileloopflow')}
<span class="text-xs font-mono font-medium inline-flex items-center -my-2">
<button onclick={(e) => e.stopPropagation()}>
<FlowJobsMenu
moduleId={k}
id={k}
{onSelectedIteration}
flowJobsSuccess={localModuleStates[k]?.flow_jobs_success}
flowJobs={localModuleStates[k]?.flow_jobs}
selected={localModuleStates[k]?.selectedForloopIndex ?? 0}
selectedManually={localModuleStates[k]?.selectedForLoopSetManually ?? false}
showIcon={false}
/>
</button>
</span>
{/if}
</div>
<div class="col-span-5 flex">
{#if subItems?.length > 1}
<div class="text-xs text-secondary absolute top-1 right-2">
{subItems?.length} jobs
</div>
{/if}
{#if min && total}
<VirtualList
width="100%"
height={Math.min(400, (subItems?.length ?? 0) * barHeight)}
itemCount={subItems?.length ?? 0}
itemSize={barHeight}
getKey={(index) => subItems?.[index]?.id}
>
{#snippet item({ index, style })}
{@const b = subItems?.[index]}
{#if b?.created_at}
<!-- <div class="text-xs text-secondary">{JSON.stringify(b)}</div> -->
{@const waitingLen = b?.created_at
? b.started_at
? b.started_at - b?.created_at
: b.duration_ms
? 0
: now - b?.created_at
: 0}
<div class="flex w-full p-1 pb-2 pl-12" {style}>
<TimelineBar
position="left"
id={b?.id}
{total}
{min}
gray
started_at={b.created_at}
len={waitingLen < 100 ? 0 : waitingLen - 100}
running={b?.started_at == undefined}
/>
{#if b.started_at}
<TimelineBar
position={waitingLen < 100 ? 'center' : 'right'}
id={b?.id}
{total}
{min}
concat
started_at={b.started_at}
len={b.started_at ? (b?.duration_ms ?? now - b?.started_at) : 0}
running={b?.duration_ms == undefined}
/>
{/if}
</div>
{:else}
<div class="flex w-full p-1 pb-2 pl-12">
<div class="text-xs text-secondary">
<!-- Waiting for executor/Suspend {JSON.stringify(b)} -->
</div>
</div>
{/if}
{/snippet}
</VirtualList>
{/if}</div
></div
>
</div>
{/each}
</div>
{:else}
<Loader2 class="animate-spin" />
{/if}