Files
windmill/frontend/src/lib/components/flows/FlowProgressBar.svelte
T
GuilhemandClaude Opus 5.5 62d0088de5 feat: show restart from failed step in run page top bar (#11317)
* feat: show restart from failed step in run page top bar

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

* fix: keep failed-step restart button when selection is not restartable

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

* feat: add restart link to the failed run status line

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

* fix: use the input error color for the progress bar error state

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

* feat: mention restarting on a fixed flow version in the error line

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

* feat: show a chevron on the restart button when it opens a picker

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

* fix: shrink run page top bar buttons so the bar fits on one line

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

* fix: target the step that failed the run, not a tolerated failure

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

* fix: address review nits on the restart link

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

* fix: skip canceled runs and match the restart link label to its text

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 17:37:41 +02:00

191 lines
5.2 KiB
Svelte

<script lang="ts">
import { type Job } from '$lib/gen'
import ProgressBar from '../progressBar/ProgressBar.svelte'
import { forLater } from '$lib/forLater'
import type { Snippet } from 'svelte'
interface Props {
job?: Job | undefined
currentSubJobProgress?: number | undefined
class?: string
slim?: boolean
textPosition?: 'top' | 'bottom'
// Prefer step ID over step index when both are available
showStepId?: boolean
errorAction?: Snippet
}
let {
job = undefined,
currentSubJobProgress = $bindable(undefined),
class: className,
slim = false,
textPosition = 'top',
showStepId = false,
errorAction
}: Props = $props()
let error: number | undefined = $state(undefined)
let index = $state(0)
let subIndex: number | undefined = $state(undefined)
let subLength: number | undefined = $state(undefined)
let length = $state(1)
let nextInProgress = $state(false)
let subIndexIsPercent: boolean = $state(false)
let currentStepId: string | undefined = $state(undefined)
let isWaitingForEvents = $state(false)
let isCanceled = $state(false)
let isScheduled = $state(false)
let isSkipped = $state(false)
let progressBar = $state<ProgressBar | undefined>(undefined)
function updateJobProgress(job: Job) {
// Check if job is scheduled for later
const isJobScheduled = Boolean(
'running' in job && 'scheduled_for' in job && job.scheduled_for && forLater(job.scheduled_for)
)
isScheduled = isJobScheduled
const modules = job?.flow_status?.modules
if (!modules?.length) {
return
}
let subStepIndex: undefined | number = undefined
let subStepLength: undefined | number = undefined
let newError: number | undefined = undefined
let newNextInProgress = false
let maxDone = Math.max(job?.flow_status?.step ?? 0, 0)
let newCurrentStepId: string | undefined = undefined
let newIsWaitingForEvents = false
// Error handler (failure_module) was triggered: backend sets step >= modules.length
// and runs the failure_module. Clamp progress to the failed module so the bar shows
// the error indicator at the correct position instead of overflowing past 100%.
if (
maxDone >= modules.length &&
job?.flow_status?.failure_module?.type !== 'WaitingForPriorSteps'
) {
const failedIdx = modules.findIndex((m) => m?.type === 'Failure')
if (failedIdx >= 0) {
newError = failedIdx
maxDone = failedIdx + 1
} else {
maxDone = modules.length
}
}
if (modules.length > maxDone) {
const nextModule = modules[maxDone]
if (nextModule.type === 'InProgress') {
newNextInProgress = true
// Get the step ID if available
if (nextModule.id) {
newCurrentStepId = nextModule.id
}
} else if (nextModule.type === 'WaitingForEvents') {
newIsWaitingForEvents = true
// Get the step ID if available for waiting events
if (nextModule.id) {
newCurrentStepId = nextModule.id
}
}
}
let module = modules[maxDone]
if (module) {
if (module.type === 'Failure' || (module.type === 'Success' && job['success'] === false)) {
newError = maxDone
maxDone = maxDone + 1
}
}
subIndexIsPercent = false
// Loop is still iterating
if (module?.iterator) {
const stepIndex = module.iterator.index || 0
const stepLength = module.iterator.itered_len || 0
if (module.iterator.index != undefined) {
subStepIndex = stepIndex
subStepLength = stepLength
}
} else if (module?.branchall) {
subStepIndex = module.branchall.branch
subStepLength = module.branchall.len
} else if (module?.progress) {
const clamp = (num, min, max) => Math.min(Math.max(num, min), max)
subStepIndex = clamp(module?.progress, subIndex ?? 0, 99)
// Jitter protection >^^^^^^^^
subStepLength = 100
subIndexIsPercent = true
currentSubJobProgress = subStepIndex
} else {
currentSubJobProgress = undefined
}
error = newError
subLength = subStepLength ? Math.max(subStepLength, 1) : undefined
subIndex = subStepIndex
length = Math.max(modules.length, 1)
const newIsSkipped = 'is_skipped' in job && Boolean(job.is_skipped)
// Early-stop completion: stop_after_if (without 'label as skipped') ends the flow
// with step < modules.length. Without this, the bar stays at < 100% with 'Running'.
if (
'success' in job &&
job.success === true &&
!newIsSkipped &&
newError === undefined &&
maxDone < modules.length
) {
index = modules.length
} else {
index = maxDone
}
nextInProgress = newNextInProgress
currentStepId = newCurrentStepId
isWaitingForEvents = newIsWaitingForEvents
isCanceled = job?.canceled || false
isSkipped = newIsSkipped
}
export function reset() {
progressBar?.resetP()
error = undefined
subIndex = undefined
subLength = undefined
length = 1
index = 0
currentStepId = undefined
isWaitingForEvents = false
isCanceled = false
isScheduled = false
isSkipped = false
}
$effect(() => {
job && updateJobProgress(job)
})
</script>
<ProgressBar
bind:this={progressBar}
{length}
{index}
{nextInProgress}
{subLength}
{subIndex}
{error}
{subIndexIsPercent}
{slim}
class={className}
{textPosition}
stepId={currentStepId}
{showStepId}
{isWaitingForEvents}
{isCanceled}
{isScheduled}
{isSkipped}
{errorAction}
/>