Files
windmill/frontend/src/lib/components/FlowStatusViewer.svelte
T
Tristan TR c0aafee9a9 feat: improve-replay-ui (#8250)
* Improve UI of script record

* Improve UI for scripts

* Remove Result & Logs loading container while flow not finised

* Improve Graph view

* Add click on a step mention

* Fix spacing when empty

* Fix step duration disappearing in recorded flows

* Modernize timeline tab

* Improve Script recording result UI

* feat: externalize recording player controls for fake-window embedding

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: reorder FlowViewer tab sync effects for clarity

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: eliminate tab sync effects in FlowViewer, use selectedTab directly

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: remove unnecessary untrack in FlowViewer tab init

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: skip tab auto-selection when selectedTab is controlled externally

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: export recording types from package

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: non-null assertion for recording.flow in FlowGraphViewer

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace banned $bindable(default_value) pattern and simplify tab sync

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use svelte 5 onclick syntax on replay page

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: skip db clock endpoint during replay mode

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove line numbers from script recording code display

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: hugocasa <hugo@casademont.ch>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 18:52:15 +00:00

184 lines
5.4 KiB
Svelte

<script lang="ts">
import FlowStatusViewerInner from './FlowStatusViewerInner.svelte'
import type { FlowState } from './flows/flowState'
import { setContext, untrack } from 'svelte'
import type { DurationStatus, FlowStatusViewerContext, GraphModuleState } from './graph'
import { isOwner as loadIsOwner, type StateStore } from '$lib/utils'
import { userStore, workspaceStore } from '$lib/stores'
import type { CompletedJob, FlowNote, FlowValue, Job } from '$lib/gen'
interface Props {
jobId: string
initialJob?: Job | undefined
workspaceId?: string | undefined
flowState?: FlowState
selectedJobStep?: string | undefined
hideFlowResult?: boolean
hideTimeline?: boolean
hideDownloadInGraph?: boolean
hideNodeDefinition?: boolean
hideJobId?: boolean
hideDownloadLogs?: boolean
rightColumnSelect?: 'timeline' | 'node_status' | 'node_definition' | 'user_states'
isOwner?: boolean
wideResults?: boolean
localModuleStates?: Record<string, GraphModuleState>
localDurationStatuses?: Record<string, DurationStatus>
job?: Job | undefined
render?: boolean
suspendStatus?: StateStore<Record<string, { job: Job; nb: number }>>
customUi?: {
tagLabel?: string | undefined
}
onStart?: () => void
onJobsLoaded?: ({ job, force }: { job: Job; force: boolean }) => void
onDone?: ({ job }: { job: CompletedJob }) => void
showLogsWithResult?: boolean
notes?: FlowNote[]
groups?: FlowValue['groups']
}
let {
jobId,
initialJob = undefined,
workspaceId = undefined,
flowState = $bindable({}),
selectedJobStep = $bindable(undefined),
hideFlowResult = false,
hideTimeline = false,
hideDownloadInGraph = false,
hideNodeDefinition = false,
hideJobId = false,
hideDownloadLogs = false,
rightColumnSelect = $bindable('timeline'),
isOwner = $bindable(false),
wideResults = false,
localModuleStates = $bindable({}),
localDurationStatuses = $bindable({}),
job = $bindable(undefined),
render = true,
suspendStatus = $bindable({ val: {} }),
customUi,
onStart,
onJobsLoaded,
onDone,
showLogsWithResult = false,
notes: notesProp = undefined,
groups: groupsProp = undefined
}: Props = $props()
let lastJobId: string = untrack(() => jobId)
let retryStatus = $state({ val: {} })
let globalRefreshes: Record<string, ((clear, root) => Promise<void>)[]> = $state({})
setContext<FlowStatusViewerContext>('FlowStatusViewer', {
flowState,
suspendStatus,
retryStatus,
hideDownloadInGraph: untrack(() => hideDownloadInGraph),
hideNodeDefinition: untrack(() => hideNodeDefinition),
hideTimeline: untrack(() => hideTimeline),
hideJobId: untrack(() => hideJobId),
hideDownloadLogs: untrack(() => hideDownloadLogs)
})
function loadOwner(path: string) {
isOwner = loadIsOwner(path, $userStore!, workspaceId ?? $workspaceStore!)
}
async function updateJobId() {
if (jobId !== lastJobId) {
console.log('updateJobId 3', jobId)
lastJobId = jobId
retryStatus.val = {}
suspendStatus.val = {}
globalRefreshes = {}
for (let key in localModuleStates) delete flowState[key]
localDurationStatuses = {}
localModuleStates = {}
}
}
let lastScriptPath: string | undefined = $state(undefined)
$effect.pre(() => {
jobId
untrack(() => {
jobId && updateJobId()
})
})
let refreshGlobal = async (moduleId: string, clear: boolean, root: string) => {
let allFns = globalRefreshes?.[moduleId]?.map((x) => x(clear, root)) ?? []
await Promise.all(allFns)
}
let updateGlobalRefresh = (moduleId: string, updateFn: (clear, root) => Promise<void>) => {
globalRefreshes[moduleId] = [...(globalRefreshes[moduleId] ?? []), updateFn]
}
let storedToolCallJobs: Record<string, Job> = $state({})
let toolCallIndicesToLoad: string[] = $state([])
</script>
{#key jobId}
<FlowStatusViewerInner
onJobsLoaded={({ job, force }) => {
if (job.script_path != lastScriptPath && job.script_path) {
lastScriptPath = job.script_path
loadOwner(lastScriptPath ?? '')
}
onJobsLoaded?.({ job, force })
}}
globalModuleStates={[]}
bind:localModuleStates
bind:selectedNode={selectedJobStep}
bind:localDurationStatuses
{onStart}
{onDone}
bind:job
{initialJob}
{jobId}
{workspaceId}
{wideResults}
bind:rightColumnSelect
{render}
{customUi}
graphTabOpen={true}
isNodeSelected={true}
{refreshGlobal}
{updateGlobalRefresh}
toolCallStore={{
getStoredToolCallJob: (storeKey: string) => storedToolCallJobs[storeKey],
setStoredToolCallJob: (storeKey: string, job: Job) => {
storedToolCallJobs[storeKey] = job
},
getLocalToolCallJobs: (prefix: string) => {
// we return a map from tool call index to job
// to do so, we filter the storedToolCallJobs object by the prefix and we make sure what's left in the key is a tool call index: 2 part of format agentModuleId-toolCallIndex
// and not a further nested tool call index
return Object.fromEntries(
Object.entries(storedToolCallJobs)
.filter(
([key]) => key.startsWith(prefix) && key.replace(prefix, '').split('-').length === 2
)
.map(([key, job]) => [Number(key.replace(prefix, '').split('-').pop()), job])
)
},
isToolCallToBeLoaded: (storeKey: string) => {
return toolCallIndicesToLoad.includes(storeKey)
},
addToolCallToLoad: (storeKey: string) => {
if (!toolCallIndicesToLoad.includes(storeKey)) {
toolCallIndicesToLoad.push(storeKey)
}
}
}}
{showLogsWithResult}
{hideFlowResult}
notes={notesProp}
groups={groupsProp}
/>
{/key}