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

195 lines
4.8 KiB
Svelte

<script lang="ts">
import type { Job } from '$lib/gen'
import type { DurationStatus, GraphModuleState } from './graph'
import FlowLogViewer from './FlowLogViewer.svelte'
import { TimelineCompute } from '$lib/timelineCompute.svelte'
import { onMount, untrack } from 'svelte'
import { ChangeTracker } from '$lib/svelte5Utils.svelte'
import { readFieldsRecursively } from '$lib/utils'
import type { NavigationChain } from '$lib/keyboardChain'
import OnChange from './common/OnChange.svelte'
interface Props {
job: Partial<Job>
localDurationStatuses?: Record<string, DurationStatus>
workspaceId: string | undefined
render: boolean
localModuleStates: Record<string, GraphModuleState>
onSelectedIteration?: (
detail:
| { id: string; index: number; manuallySet: true; moduleId: string }
| { manuallySet: false; moduleId: string }
) => Promise<void>
mode?: 'flow' | 'aiagent'
}
let {
job,
localModuleStates,
localDurationStatuses,
workspaceId,
render,
onSelectedIteration,
mode = 'flow'
}: Props = $props()
// State for tracking expanded rows - using Record to allow explicit control
let expandedRows: Record<string, boolean> = $state({})
let allExpanded = $state(false)
let showResultsInputs = $state(true)
// Keyboard navigation state - incremental like expandedRows
let currentId = $state<string | null>('flow-root')
let navigationChain = $state<NavigationChain>({})
// Timeline state
let timelineCompute = $state<TimelineCompute | undefined>(undefined)
onMount(() => {
timelineCompute = new TimelineCompute(
modules.map((m) => m.id),
localDurationStatuses ?? {},
job.type === 'CompletedJob'
)
return () => {
timelineCompute?.destroy()
}
})
// Derived timeline values
const timelineMin = $derived(timelineCompute?.min ?? undefined)
const timelineTotal = $derived(timelineCompute?.total ?? undefined)
const timelineItems = $derived(timelineCompute?.items ?? undefined)
const timelineNow = $derived(timelineCompute?.now ?? Date.now())
let moduleTracker = new ChangeTracker($state.snapshot(job.raw_flow?.modules ?? []))
$effect(() => {
readFieldsRecursively(job.raw_flow?.modules ?? [])
untrack(() => moduleTracker.track($state.snapshot(job.raw_flow?.modules ?? [])))
})
let modules = $derived.by(() => {
moduleTracker.counter
return untrack(() => job.raw_flow?.modules ?? [])
})
function toggleExpanded(id: string) {
// If not in record, use opposite of allExpanded as new state
// If in record, toggle the current state
const currentState = expandedRows[id] ?? allExpanded
expandedRows[id] = !currentState
}
function getSelectedIteration(stepId: string): number {
return localModuleStates[stepId]?.selectedForloopIndex ?? 0
}
function toggleExpandAll() {
allExpanded = !allExpanded
expandedRows = {}
}
// Keyboard event handler using navigation links
function handleKeydown(event: KeyboardEvent) {
if (!currentId && job.raw_flow?.modules) {
currentId = 'flow-root'
} else if (!currentId) {
return
}
switch (event.key) {
case 'ArrowDown':
event.preventDefault()
const downId = navigationChain[currentId]?.downId
if (downId) {
currentId = downId
}
break
case 'ArrowUp':
event.preventDefault()
const upId = navigationChain[currentId]?.upId
if (upId) {
currentId = upId
}
break
case 'Enter':
event.preventDefault()
toggleExpanded(currentId)
break
}
}
function select(id: string) {
currentId = id
}
let timelineAvailableWidths = $state<Record<string, number>>({})
let lastJobId: string | undefined = $state(job.id)
const timelinelWidth = $derived.by(() => {
const widths = Object.values(timelineAvailableWidths)
return widths.length > 0 ? Math.max(Math.min(...widths) - 12, 0) : 0
})
function updateJobId() {
if (job.id !== lastJobId) {
lastJobId = job.id
navigationChain = {}
timelineAvailableWidths = {}
currentId = 'flow-root'
showResultsInputs = true
timelineCompute?.reset()
}
}
$effect.pre(() => {
job.id
untrack(() => {
job.id && updateJobId()
})
})
</script>
<OnChange
key={localDurationStatuses}
onChange={() => {
timelineCompute?.updateInputs(
modules.map((m) => m.id),
localDurationStatuses ?? {},
job.type === 'CompletedJob'
)
}}
/>
<div
class="w-full rounded-md overflow-hidden border focus:border-gray-400 dark:focus:border-gray-400"
role="tree"
tabindex="0"
onkeydown={handleKeydown}
>
<FlowLogViewer
{modules}
{localModuleStates}
rootJob={job}
{expandedRows}
{allExpanded}
{showResultsInputs}
{toggleExpanded}
{toggleExpandAll}
{onSelectedIteration}
{workspaceId}
{render}
{getSelectedIteration}
flowId="root"
{mode}
{currentId}
bind:navigationChain
{select}
{timelineMin}
{timelineTotal}
{timelineItems}
{timelineNow}
bind:timelineAvailableWidths
{timelinelWidth}
/>
</div>