Files
windmill/frontend/src/lib/components/flows/map/FlowJobsMenu.svelte
T
GuilhemandRuben Fiszel 235354fe13 feat(frontend): add flow log view (#6330)
* componentize detail module

* Add logs tab

* add flow log viewer

* fetch logs

* handle subflows

* add forloop iteration picker

* polish

* expand steps log by default

* move logic handling to wrapper component

* improve iteration picker

* clean code

* polishing

* Add flow start and flow end rows

* fix infinite loop

* nit

* use list instead of table

* use custom id for collapsing subflow

* remove debug logs

* Use status dot instead of text

* fetch log from moduleState

* wip

* only fetch subflow jobs from cache if job is completed

* Add job polling for expanded steps

* handle subflows

* Init logs for steps

* update localModuleState logs

* use selected iteration from local module state

* handle branchone

* Add branch one and branch all label

* remove redondant innerModule prop

* Improve UX

* Add expand/collapse

* Add filter to hide result and inputs

* Steps are now flow children

* improve UX

* Open flow and steps sction when executing

* Handle empty subflows

* remove unnecessary sequence viewer component

* nit

* use iteration picker in log view

* Replace dot with step type icon

* indicate subflows

* add step number and progression

* Incorporate inputs and results in the list of steps

* Add error indicator when subflow has error

* improve topbar

* improve log polling

* Improve log polling

* Add root flow log fetching and polling

* Add debounce for loading subflow jobs

* write a function to build the tree view from the graph

* remove unnecessary log polling

* fix flow result display

* flag errors

* preprocessor

* remove all flow logs drawer

* grenerate graph from component

* wip

* Check module change before building graph

* nit

* fix log overflow

* fix log viewer borders

* mini jobs run preview fix

* elegent job logs loading

* nit

* nit

* nit

* all

* all

* all

* all

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2025-08-14 00:39:21 +00:00

184 lines
4.8 KiB
Svelte

<script lang="ts">
import { Menu, Menubar, MenuItem, MeltButton } from '$lib/components/meltComponents'
import { ListFilter, Lock, LockOpen } from 'lucide-svelte'
import Popover from '$lib/components/Popover.svelte'
import { twMerge } from 'tailwind-merge'
import VirtualList from '@tutorlatin/svelte-tiny-virtual-list'
import type { onSelectedIteration } from '$lib/components/graph/graphBuilder.svelte'
import { untrack } from 'svelte'
interface Props {
id: string
moduleId: string
flowJobs: string[] | undefined
flowJobsSuccess: (boolean | undefined)[] | undefined
selected: number
selectedManually: boolean | undefined
onSelectedIteration: onSelectedIteration
showIcon?: boolean
}
let {
id,
flowJobs,
flowJobsSuccess,
selected,
selectedManually,
onSelectedIteration,
moduleId,
showIcon = true
}: Props = $props()
let filter: number | undefined = $state(undefined)
function onKeydown(event: KeyboardEvent) {
if (
event.key === 'Enter' &&
filter != undefined &&
flowJobs &&
filter < flowJobs.length &&
filter > 0
) {
event.preventDefault()
onSelectedIteration({
index: filter - 1,
id: flowJobs[filter - 1],
manuallySet: true,
moduleId: moduleId
})
menu?.close()
}
}
let buttonHover = $state(false)
let menu: Menu | undefined = $state(undefined)
let items: { id: string; success: boolean | undefined; index: number }[] = $state([])
function updateItems() {
items = (flowJobs ?? [])
.map((v, i) => {
return {
id: v,
success: flowJobsSuccess?.[i],
index: i
}
})
.filter((v) => {
return filter == undefined || (v.index + 1).toString().includes(filter.toString())
})
}
let isOpen = $state(false)
$effect(() => {
isOpen && flowJobs && untrack(() => updateItems())
})
</script>
{#if selectedManually && flowJobsSuccess?.some((x) => x == undefined || x == null)}
<Popover class="absolute top-1.5 right-12 cursor-pointer">
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
onmouseenter={() => (buttonHover = true)}
onmouseleave={() => (buttonHover = false)}
onclick={(e) => {
buttonHover = false
onSelectedIteration({ manuallySet: false, moduleId: moduleId })
}}
>
{#if buttonHover}
<LockOpen class="text-primary" size={14} />
{:else}
<Lock class="text-primary" size={12} />
{/if}
</div>
{#snippet text()}
<span>
The iteration picked is locked. Click to unlock to pick automatically new iterations.
</span>
{/snippet}
</Popover>
{/if}
<Menubar>
{#snippet children({ createMenu })}
<Menu
on:open={() => {
isOpen = true
updateItems()
}}
on:close={() => {
isOpen = false
}}
{createMenu}
placement="bottom"
bind:this={menu}
usePointerDownOutside
>
{#snippet triggr({ trigger })}
<MeltButton
title="Pick an iteration"
id={`flow-editor-iteration picker-${id}`}
type="button"
class={twMerge(
'text-xs bg-surface border-[1px] border-gray-300 dark:border-gray-500 focus:outline-none',
'hover:bg-surface-hover focus:ring-4 focus:ring-surface-selected font-medium rounded-sm w-[40px] gap-1 h-[20px]',
'flex items-center justify-center',
flowJobsSuccess?.[selected] == false ? 'text-red-400' : 'text-secondary'
)}
meltElement={trigger}
>
#{selected == -1 ? '?' : selected + 1}
{#if showIcon}
<ListFilter size={15} />
{/if}
</MeltButton>
{/snippet}
{#snippet children({ item })}
<div class="flex flex-col px-1">
<input type="number" bind:value={filter} onkeydown={onKeydown} />
<div class="max-h-[300px]">
{#key items}
<VirtualList height={300} width="100%" itemCount={items.length} itemSize={24}>
{#snippet header()}{/snippet}
{#snippet footer()}{/snippet}
{#snippet children({ index: idx, style })}
<div {style}>
<MenuItem
class={twMerge(
'text-primary text-xs w-full text-left py-1 pl-2 hover:bg-surface-hover whitespace-nowrap flex flex-row gap-2 items-center',
items[idx].success == false ? 'text-red-400' : '',
'data-[highlighted]:bg-surface-hover'
)}
onClick={() => {
onSelectedIteration({
moduleId: moduleId,
index: items[idx].index,
id: items[idx].id,
manuallySet: true
})
menu?.close()
}}
{item}
>
#{items[idx].index + 1}
</MenuItem>
</div>
{/snippet}
</VirtualList>
{/key}
<!-- {#each flowJobs ?? [] as id, idx (id)}
{#if filter == undefined || (idx + 1).toString().includes(filter.toString())}
{/if}
{/each} -->
</div>
</div>
{/snippet}
</Menu>
{/snippet}
</Menubar>