Files
windmill/frontend/src/lib/components/FlowLogViewerWrapper.svelte
T
Guilhem d400fe76c0 fix(frontend): flow log viewer add keyboard navigation (#6523)
* prevent navigation in the flow when the preview is open

* keyboardNav with registration link

* Improve keyboard nav UI

* Focus log with keyboard nav

* Add native focus

* initialize focused on key down

* fix log expand not sticking

* Improve tree structure

* nit

* nit

* Update active section on click

* add tooltip for keyboard navigation
2025-09-03 18:27:08 +00:00

129 lines
3.1 KiB
Svelte

<script lang="ts">
import type { Job } from '$lib/gen'
import type { GraphModuleState } from './graph'
import FlowLogViewer from './FlowLogViewer.svelte'
import { untrack } from 'svelte'
import { ChangeTracker } from '$lib/svelte5Utils.svelte'
import { readFieldsRecursively } from '$lib/utils'
import type { NavigationChain } from '$lib/keyboardChain'
interface Props {
job: Partial<Job>
localModuleStates: Record<string, GraphModuleState>
workspaceId: string | undefined
render: boolean
onSelectedIteration: (
detail:
| { id: string; index: number; manuallySet: true; moduleId: string }
| { manuallySet: false; moduleId: string }
) => Promise<void>
mode?: 'flow' | 'aiagent'
}
let {
job,
localModuleStates,
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>({})
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
}
</script>
<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"
flowStatus={undefined}
{mode}
{currentId}
bind:navigationChain
{select}
/>
</div>