simplify log tree structure (#6389)

* fix(frontend): fix bad log tree build

* remove entry structure to use modules as input for log tree

* clean

* fix typo

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
This commit is contained in:
Guilhem
2025-08-14 17:47:09 +01:00
committed by GitHub
parent 1bd3b77985
commit 7362b9dc03
3 changed files with 161 additions and 263 deletions

View File

@@ -1,11 +0,0 @@
import type { FlowModuleValue } from '$lib/gen'
export interface FlowLogEntry {
id: string
stepId: string
stepNumber?: number
summary?: string
stepType?: FlowModuleValue['type']
subflows?: FlowLogEntry[][]
subflowsSummary?: string[]
}

View File

@@ -16,18 +16,17 @@
import ObjectViewer from './propertyPicker/ObjectViewer.svelte'
import LogViewer from './LogViewer.svelte'
import FlowLogViewer from './FlowLogViewer.svelte'
import type { FlowModuleValue, FlowStatusModule, Job } from '$lib/gen'
import type { FlowModule, FlowModuleValue, FlowStatusModule, Job } from '$lib/gen'
import { twMerge } from 'tailwind-merge'
import FlowJobsMenu from './flows/map/FlowJobsMenu.svelte'
import BarsStaggered from './icons/BarsStaggered.svelte'
import type { GraphModuleState } from './graph/model'
import type { Writable } from 'svelte/store'
import type { FlowLogEntry } from './FlowLogUtils'
type RootJobData = Partial<Job>
interface Props {
logEntries: FlowLogEntry[]
modules: FlowModule[]
localModuleStates: Writable<Record<string, GraphModuleState>>
rootJob: RootJobData
flowStatus: FlowStatusModule['type'] | undefined
@@ -50,7 +49,7 @@
}
let {
logEntries,
modules,
localModuleStates,
rootJob,
flowStatus,
@@ -133,28 +132,47 @@
}
// Find all parents of error steps
function findParentsOfErrors(entries: FlowLogEntry[]): Set<string> {
function findParentsOfErrors(modules: FlowModule[]): Set<string> {
const parentsWithErrors = new Set<string>()
function traverseEntries(entryList: FlowLogEntry[], parentId?: string) {
function traverseModules(modules: FlowModule[], parentId?: string) {
let hasChildError = false
for (const entry of entryList) {
for (const module of modules) {
let currentEntryHasError = false
// Check if this entry has subflows with errors
if (entry.subflows && entry.subflows.length > 0) {
for (const subflow of entry.subflows) {
const subflowHasError = traverseEntries(subflow, entry.stepId)
if (
module.value.type === 'forloopflow' ||
(module.value.type === 'whileloopflow' && module.value.modules.length > 0)
) {
const subflowHasError = traverseModules(module.value.modules, module.id)
if (subflowHasError) {
currentEntryHasError = true
parentsWithErrors.add(module.id)
}
} else if (module.value.type === 'branchone' || module.value.type === 'branchall') {
if (module.value.branches.length > 0) {
for (const branch of module.value.branches) {
const subflowHasError = traverseModules(branch.modules, module.id)
if (subflowHasError) {
currentEntryHasError = true
parentsWithErrors.add(module.id)
}
}
}
if (module.value.type === 'branchone' && module.value.default.length > 0) {
// Also check default branch
const subflowHasError = traverseModules(module.value.default, module.id)
if (subflowHasError) {
currentEntryHasError = true
parentsWithErrors.add(entry.stepId)
parentsWithErrors.add(module.id)
}
}
}
// Check if this entry itself has an error (but don't flag it - only its parents)
const stepStatus = $localModuleStates[entry.stepId]?.type
const stepStatus = $localModuleStates[module.id]?.type
if (stepStatus === 'Failure') {
currentEntryHasError = true
// Don't add the entry itself to parentsWithErrors
@@ -170,13 +188,13 @@
return hasChildError
}
traverseEntries(entries, flowId)
traverseModules(modules, flowId)
return parentsWithErrors
}
// Get flow info for display
const flowInfo = $derived.by(() => {
const parentsWithErrors = findParentsOfErrors(logEntries)
const parentsWithErrors = findParentsOfErrors(modules)
return {
jobId: rootJob.id,
inputs: rootJob.args || {},
@@ -188,6 +206,47 @@
parentsWithErrors
}
})
function hasSubflows(module: FlowModule) {
return (
module.value.type === 'forloopflow' ||
module.value.type === 'whileloopflow' ||
module.value.type === 'branchall' ||
module.value.type === 'branchone'
)
}
function getSubflows(module: FlowModule) {
const subflows: Array<{ modules: FlowModule[]; label: string; flowId: string }> = []
if (module.value.type === 'forloopflow' || module.value.type === 'whileloopflow') {
subflows.push({
modules: module.value.modules,
label: module.summary || '',
flowId: `${module.id}-subflow`
})
} else if (module.value.type === 'branchall' || module.value.type === 'branchone') {
// Add all branches
for (let i = 0; i < module.value.branches.length; i++) {
const branch = module.value.branches[i]
subflows.push({
modules: branch.modules,
label: branch.summary || `branch ${i + 1}`,
flowId: `${module.id}-subflow-${i}`
})
}
// Add default branch for branchone
if (module.value.type === 'branchone') {
subflows.push({
modules: module.value.default,
label: 'default',
flowId: `${module.id}-subflow-default`
})
}
}
return subflows
}
</script>
{#if render}
@@ -242,7 +301,7 @@
<div class="w-4"></div>
{/if}
</div>
<div class="grow min-w-0 leading-tigh">
<div class="grow min-w-0 leading-tight">
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
@@ -263,7 +322,7 @@
{#if flowInfo.label}
: {flowInfo.label}
{/if}
<span class="text-tertiary">{getStepProgress(rootJob, logEntries.length)}</span>
<span class="text-tertiary">{getStepProgress(rootJob, modules.length)}</span>
</span>
</div>
</div>
@@ -340,25 +399,21 @@
</li>
{/if}
{#if logEntries.length > 0}
{#each logEntries as entry (entry.id)}
{@const isLeafStep =
entry.stepType !== 'branchall' &&
entry.stepType !== 'branchone' &&
entry.stepType !== 'forloopflow' &&
entry.stepType !== 'whileloopflow'}
{@const status = $localModuleStates[entry.stepId]?.type}
{#if modules.length > 0}
{#each modules as module (module.id)}
{@const isLeafStep = !hasSubflows(module)}
{@const status = $localModuleStates[module.id]?.type}
{@const isRunning = status === 'InProgress' || status === 'WaitingForExecutor'}
{@const hasEmptySubflowValue = hasEmptySubflow(entry.stepId, entry.stepType)}
{@const hasEmptySubflowValue = hasEmptySubflow(module.id, module.value.type)}
{@const isCollapsible = !hasEmptySubflowValue}
<li class="border-b flex flex-row">
<div class="py-2 leading-tight align-top">
{#if isCollapsible}
<button
class="w-4 flex items-center justify-center text-xs text-tertiary hover:text-primary transition-colors"
onclick={() => toggleExpanded(entry.id)}
onclick={() => toggleExpanded(module.id)}
>
{#if isExpanded(entry.id, isRunning)}
{#if isExpanded(module.id, isRunning)}
<ChevronDown size={8} />
{:else}
<ChevronRight size={8} />
@@ -383,68 +438,68 @@
? 'opacity-50'
: ''
)}
onclick={isCollapsible ? () => toggleExpanded(entry.id) : undefined}
onclick={isCollapsible ? () => toggleExpanded(module.id) : undefined}
>
<div class="flex items-center gap-2 grow min-w-0">
<!-- Step icon -->
{@render stepIcon(
entry.stepType,
module.value.type,
status as FlowStatusModule['type'],
flowInfo.parentsWithErrors.has(entry.stepId)
flowInfo.parentsWithErrors.has(module.id)
)}
<div class="flex items-center gap-2">
<span class="text-xs font-mono">
<b>
{entry.stepId}
{module.id}
</b>
{#if entry.stepType === 'forloopflow'}
{#if module.value.type === 'forloopflow'}
For loop
{:else if entry.stepType === 'whileloopflow'}
{:else if module.value.type === 'whileloopflow'}
While loop
{:else if entry.stepType === 'branchall'}
{:else if module.value.type === 'branchall'}
Branch to all
{:else if entry.stepType === 'branchone'}
{:else if module.value.type === 'branchone'}
Branch to one
{:else if entry.stepType === 'flow'}
{:else if module.value.type === 'flow'}
Subflow
{:else}
Step
{/if}
{#if entry.summary}
: {entry.summary}
{#if module.summary}
: {module.summary}
{/if}
{#if hasEmptySubflowValue}
<span class="text-tertiary">
{#if entry.stepType === 'forloopflow' || entry.stepType === 'whileloopflow'}
{#if module.value.type === 'forloopflow' || module.value.type === 'whileloopflow'}
(empty loop)
{:else if entry.stepType === 'branchall' || entry.stepType === 'branchone'}
{:else if module.value.type === 'branchall' || module.value.type === 'branchone'}
(no branch)
{/if}
</span>
{/if}
</span>
{#if !hasEmptySubflowValue && $localModuleStates[entry.stepId]?.flow_jobs && (entry.stepType === 'forloopflow' || entry.stepType === 'whileloopflow')}
{#if !hasEmptySubflowValue && $localModuleStates[module.id]?.flow_jobs && (module.value.type === 'forloopflow' || module.value.type === 'whileloopflow')}
<span
class="text-xs font-mono font-medium inline-flex items-center grow min-w-0 -my-2"
>
<span onclick={(e) => e.stopPropagation()}>
<FlowJobsMenu
moduleId={entry.stepId}
id={entry.stepId}
moduleId={module.id}
id={module.id}
{onSelectedIteration}
flowJobsSuccess={$localModuleStates[entry.stepId]
flowJobsSuccess={$localModuleStates[module.id]
?.flow_jobs_success}
flowJobs={$localModuleStates[entry.stepId]?.flow_jobs}
selected={$localModuleStates[entry.stepId]
?.selectedForloopIndex ?? 0}
selectedManually={$localModuleStates[entry.stepId]
flowJobs={$localModuleStates[module.id]?.flow_jobs}
selected={$localModuleStates[module.id]?.selectedForloopIndex ??
0}
selectedManually={$localModuleStates[module.id]
?.selectedForLoopSetManually ?? false}
showIcon={false}
/>
</span>
{#if entry.stepType === 'forloopflow'}
{`/${$localModuleStates[entry.stepId]?.iteration_total ?? 0}`}
{#if module.value.type === 'forloopflow'}
{`/${$localModuleStates[module.id]?.iteration_total ?? 0}`}
{/if}
</span>
{/if}
@@ -452,7 +507,7 @@
</div>
{#if isLeafStep}
{@const jobId = $localModuleStates[entry.stepId]?.job_id}
{@const jobId = $localModuleStates[module.id]?.job_id}
<a
href={getJobLink(jobId ?? '')}
class="text-xs text-primary hover:underline font-mono"
@@ -464,68 +519,66 @@
{/if}
</div>
{#if isCollapsible && isExpanded(entry.id, isRunning)}
{@const args = $localModuleStates[entry.stepId]?.args}
{@const logs = $localModuleStates[entry.stepId]?.logs}
{@const result = $localModuleStates[entry.stepId]?.result}
{@const jobId = $localModuleStates[entry.stepId]?.job_id}
{#if isCollapsible && isExpanded(module.id, isRunning)}
{@const args = $localModuleStates[module.id]?.args}
{@const logs = $localModuleStates[module.id]?.logs}
{@const result = $localModuleStates[module.id]?.result}
{@const jobId = $localModuleStates[module.id]?.job_id}
<div class="my-1 transition-all duration-200 ease-in-out">
<!-- Show child steps if they exist -->
{#if entry.subflows && entry.subflows.length > 0}
{#each entry.subflows as subflow, index}
{@const subflowLabel = entry.subflowsSummary?.[index]}
{@const subflowJob = {
id: jobId,
type:
$localModuleStates[entry.stepId]?.type === 'Failure' ||
$localModuleStates[entry.stepId]?.type === 'Success'
? 'CompletedJob'
: ('QueuedJob' as Job['type']),
logs,
result,
args,
success: $localModuleStates[entry.stepId]?.type === 'Success'
}}
<div class="border-l mb-2">
<!-- Recursively render child steps using FlowLogViewer -->
<FlowLogViewer
logEntries={subflow}
{localModuleStates}
rootJob={subflowJob}
flowStatus={$localModuleStates[entry.stepId]?.type}
{expandedRows}
{allExpanded}
{showResultsInputs}
{toggleExpanded}
toggleExpandAll={undefined}
{workspaceId}
{render}
level={level + 1}
flowId={`${entry.stepId}-subflow-${index}`}
flowSummary={subflowLabel}
{onSelectedIteration}
{getSelectedIteration}
/>
</div>
{/each}
<!-- Show input arguments -->
{:else}
{#each getSubflows(module) as subflow}
{@const subflowJob = {
id: jobId,
type:
$localModuleStates[module.id]?.type === 'Failure' ||
$localModuleStates[module.id]?.type === 'Success'
? 'CompletedJob'
: ('QueuedJob' as Job['type']),
logs,
result,
args,
success: $localModuleStates[module.id]?.type === 'Success'
}}
<div class="border-l mb-2">
<!-- Recursively render child steps using FlowLogViewer -->
<FlowLogViewer
modules={subflow.modules}
{localModuleStates}
rootJob={subflowJob}
flowStatus={$localModuleStates[module.id]?.type}
{expandedRows}
{allExpanded}
{showResultsInputs}
{toggleExpanded}
toggleExpandAll={undefined}
{workspaceId}
{render}
level={level + 1}
flowId={subflow.flowId}
flowSummary={subflow.label}
{onSelectedIteration}
{getSelectedIteration}
/>
</div>
{/each}
<!-- Show input arguments -->
{#if getSubflows(module).length === 0}
{#if showResultsInputs && isLeafStep && args && Object.keys(args).length > 0}
<div class="mb-2">
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="flex items-center gap-1 cursor-pointer hover:text-primary text-xs font-mono font-medium mb-1"
onclick={() => toggleExpanded(`${entry.id}-input`)}
onclick={() => toggleExpanded(`${module.id}-input`)}
>
{#if isExpanded(`${entry.id}-input`)}
{#if isExpanded(`${module.id}-input`)}
<ChevronDown size={8} />
{:else}
<ChevronRight size={8} />
{/if}
Input
</div>
{#if isExpanded(`${entry.id}-input`)}
{#if isExpanded(`${module.id}-input`)}
<div class="pl-4">
<ObjectViewer json={args} pureViewer={true} />
</div>
@@ -546,7 +599,7 @@
noPadding
wrapperClass="w-full mb-2 pr-2"
/>
{:else if jobId && !entry.subflows?.[0]?.length}
{:else if jobId && !hasSubflows(module)}
<div class="mb-2">
<div class="text-xs text-tertiary font-mono">
No logs available
@@ -562,16 +615,16 @@
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="flex items-center gap-1 cursor-pointer hover:text-primary text-xs font-mono font-medium mb-1"
onclick={() => toggleExpanded(`${entry.id}-result`)}
onclick={() => toggleExpanded(`${module.id}-result`)}
>
{#if isExpanded(`${entry.id}-result`)}
{#if isExpanded(`${module.id}-result`)}
<ChevronDown size={8} />
{:else}
<ChevronRight size={8} />
{/if}
Result
</div>
{#if isExpanded(`${entry.id}-result`)}
{#if isExpanded(`${module.id}-result`)}
<div class="pl-4">
<ObjectViewer json={result} pureViewer={true} />
</div>

View File

@@ -1,10 +1,8 @@
<script lang="ts">
import type { Job } from '$lib/gen'
import { writable, type Writable } from 'svelte/store'
import { type Writable } from 'svelte/store'
import type { GraphModuleState } from './graph'
import FlowLogViewer from './FlowLogViewer.svelte'
import { graphBuilder, type NodeLayout } from './graph/graphBuilder.svelte'
import type { FlowLogEntry } from './FlowLogUtils'
import { untrack } from 'svelte'
import { ChangeTracker } from '$lib/svelte5Utils.svelte'
import { readFieldsRecursively } from '$lib/utils'
@@ -28,68 +26,14 @@
let allExpanded = $state(false)
let showResultsInputs = $state(true)
let emptyEventHandler = {
deleteBranch: () => {},
insert: () => {},
select: () => {},
changeId: () => {},
delete: () => {},
newBranch: () => {},
move: () => {},
selectedIteration: () => {},
simplifyFlow: () => {},
expandSubflow: () => {},
minimizeSubflow: () => {},
updateMock: () => {},
testUpTo: () => {},
editInput: () => {},
testFlow: () => {},
cancelTestFlow: () => {},
openPreview: () => {},
hideJobStatus: () => {}
}
let modules = $derived(job.raw_flow?.modules ?? [])
let moduleTracker = new ChangeTracker($state.snapshot(job.raw_flow?.modules ?? []))
$effect(() => {
readFieldsRecursively(modules)
untrack(() => moduleTracker.track($state.snapshot(modules)))
readFieldsRecursively(job.raw_flow?.modules ?? [])
untrack(() => moduleTracker.track($state.snapshot(job.raw_flow?.modules ?? [])))
})
let nodes: NodeLayout[] | undefined = $derived.by(() => {
let modules = $derived.by(() => {
moduleTracker.counter
const graph = graphBuilder(
untrack(() => modules),
{
disableAi: false,
insertable: false,
flowModuleStates: undefined,
selectedId: undefined,
path: undefined,
newFlow: false,
cache: false,
earlyStop: false,
editMode: false,
isOwner: false,
isRunning: false,
individualStepTests: false,
flowJob: undefined,
showJobStatus: false,
suspendStatus: writable({}),
flowHasChanged: false
},
untrack(() => job.raw_flow?.failure_module),
untrack(() => job.raw_flow?.preprocessor_module),
emptyEventHandler, // eventHandler - empty for logs view
undefined,
false, // useDataflow
undefined, // selectedId
undefined, // moving
undefined, // simplifiableFlow
undefined, // triggerNode path
{} // expandedSubflows
)
return graph.error ? undefined : graph.nodes
return untrack(() => job.raw_flow?.modules ?? [])
})
function toggleExpanded(id: string) {
@@ -107,99 +51,11 @@
allExpanded = !allExpanded
expandedRows = {}
}
// Build tree structure from modules using bottom-up traversal from result node
function buildFlowTree(nodes: NodeLayout[]): FlowLogEntry[] {
// Index nodes for quick access
const nodeById: Record<string, NodeLayout> = {}
for (const n of nodes) nodeById[n.id] = n
function traverseFromId(id: string): FlowLogEntry[] {
const entries: FlowLogEntry[] = []
const currentNode = nodeById[id]
if (currentNode.type === 'module') {
entries.push({
id: currentNode.id,
stepId: currentNode.id,
stepNumber: 0,
summary: currentNode.data.module.summary ?? '',
stepType: currentNode.data.module.value.type
})
}
if (
currentNode.type === 'whileLoopStart' ||
currentNode.type === 'branchOneStart' ||
currentNode.type === 'branchAllStart'
) {
// Reaching the end of a subflow
return entries
}
let nextParentId = currentNode.parentIds?.[0]
if (!nextParentId) {
// Reached the root of the flow
return entries
}
if (
currentNode.type === 'forLoopEnd' ||
currentNode.type === 'whileLoopEnd' ||
currentNode.type === 'branchOneEnd'
) {
const subflow = traverseFromId(nextParentId)
const subflowId = currentNode.id.slice(0, -4) // Remove '-end' suffix
const subflowNode = nodeById[subflowId]
const subflowSummary = currentNode.type === 'branchOneEnd' ? 'branch' : 'iteration'
if (subflowNode.type === 'module') {
entries.push({
id: subflowId,
stepId: subflowId,
subflows: [subflow],
stepType:
currentNode.type === 'forLoopEnd'
? 'forloopflow'
: currentNode.type === 'whileLoopEnd'
? 'whileloopflow'
: 'branchone',
summary: subflowNode.data.module.summary ?? '',
subflowsSummary: [subflowSummary]
})
nextParentId = subflowNode.parentIds?.[0] ?? ''
}
} else if (currentNode.type === 'branchAllEnd') {
const subflowId = currentNode.id.slice(0, -4) // Remove '-end' suffix
const subflowNode = nodeById[subflowId]
if (subflowNode.type === 'module' && subflowNode.data.module.value.type === 'branchall') {
const subflows = currentNode.parentIds?.map((id) => traverseFromId(id)) ?? []
const subflowsSummary = subflowNode.data.module.value.branches.map((b) => b.summary ?? '')
entries.push({
id: subflowId,
stepId: subflowId,
subflows: subflows,
stepType: 'branchall',
subflowsSummary
})
nextParentId = subflowNode.parentIds?.[0] ?? '' // a module can only have one parent
}
}
// Get entries from parent nodes
const parentEntries = traverseFromId(nextParentId)
return [...parentEntries, ...entries]
}
// Start from the result node and traverse backwards
return traverseFromId('result')
}
let logEntries = $derived(nodes ? buildFlowTree(nodes) : [])
</script>
<div class="w-full rounded-md overflow-hidden border">
<FlowLogViewer
{logEntries}
{modules}
{localModuleStates}
rootJob={job}
{expandedRows}