mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-18 16:03:24 +00:00
* 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>
191 lines
5.2 KiB
Svelte
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}
|
|
/>
|