mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-05 16:03:47 +00:00
fix: improve flow progress bar
This commit is contained in:
+13
-8
@@ -269,14 +269,19 @@ await new Command()
|
||||
const updateState = setInterval(async () => {
|
||||
const elapsed = start ? Math.ceil((Date.now() - start) / 1000) : 0;
|
||||
const sum = jobsSent.reduce((a, b) => a + b, 0);
|
||||
const queue_length = (
|
||||
await (
|
||||
await fetch(
|
||||
host + "/api/w/" + config.workspace_id + "/jobs/queue/count",
|
||||
{ headers: { ["Authorization"]: "Bearer " + config.token } }
|
||||
)
|
||||
).json()
|
||||
).database_length;
|
||||
const queue_length = -1;
|
||||
try {
|
||||
(
|
||||
await (
|
||||
await fetch(
|
||||
host + "/api/w/" + config.workspace_id + "/jobs/queue/count",
|
||||
{ headers: { ["Authorization"]: "Bearer " + config.token } }
|
||||
)
|
||||
).json()
|
||||
).database_length;
|
||||
} catch (e) {
|
||||
console.error("Error reading queue count: " + e);
|
||||
}
|
||||
await Deno.stdout.write(
|
||||
enc(
|
||||
`elapsed: ${elapsed}/${seconds} | jobs sent: ${JSON.stringify(
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { FlowStatusModule, Job } from '$lib/gen'
|
||||
import { ProgressBar, type GeneralStep, type LoopStep, type ProgressStep } from '../progressBar'
|
||||
import ProgressBar from '../progressBar/ProgressBar.svelte'
|
||||
|
||||
export let job: Job | undefined = undefined
|
||||
export let duration = 200
|
||||
let jobProgress: { steps: ProgressStep[]; error: boolean } = { steps: [], error: false }
|
||||
|
||||
let error: number | undefined = undefined
|
||||
let index = 0
|
||||
let subIndex: number | undefined = undefined
|
||||
let subLength: number | undefined = undefined
|
||||
let length = 1
|
||||
|
||||
$: if (job) updateJobProgress(job)
|
||||
|
||||
@@ -14,51 +18,58 @@
|
||||
return
|
||||
}
|
||||
|
||||
jobProgress.steps = modules.map((module) => {
|
||||
let maxDone = 0
|
||||
let subStepIndex: undefined | number = undefined
|
||||
let subStepLength: undefined | number = undefined
|
||||
let newError: number | undefined = undefined
|
||||
modules.forEach((module, i) => {
|
||||
if (
|
||||
module.type === FlowStatusModule.type.FAILURE ||
|
||||
(module.type === FlowStatusModule.type.SUCCESS && job['success'] === false)
|
||||
) {
|
||||
jobProgress.error = true
|
||||
newError = i
|
||||
}
|
||||
|
||||
let isDone = isJobStepDone(module.type)
|
||||
if (isDone) {
|
||||
maxDone = i + 1
|
||||
return
|
||||
}
|
||||
|
||||
// Loop is still iterating
|
||||
if (module.iterator) {
|
||||
return <LoopStep>{
|
||||
type: 'loop',
|
||||
isDone: isJobStepDone(module.type),
|
||||
index: module.iterator.index || 0,
|
||||
length: module.iterator.itered?.length || 0
|
||||
const stepIndex = module.iterator.index || 0
|
||||
const stepLength = module.iterator.itered?.length || 0
|
||||
if (module.iterator.index != undefined) {
|
||||
subStepIndex = stepIndex
|
||||
subStepLength = stepLength
|
||||
}
|
||||
}
|
||||
// Loop is finished
|
||||
else if (module['flow_jobs']) {
|
||||
return <LoopStep>{
|
||||
type: 'loop',
|
||||
isDone: isJobStepDone(module.type),
|
||||
index: module['flow_jobs'].length,
|
||||
length: module['flow_jobs'].length
|
||||
}
|
||||
}
|
||||
// Not a loop
|
||||
return <GeneralStep>{
|
||||
type: 'general',
|
||||
isDone: isJobStepDone(module.type)
|
||||
} else if (module.branchall) {
|
||||
subStepIndex = module.branchall.branch
|
||||
subStepLength = module.branchall.len
|
||||
}
|
||||
})
|
||||
error = newError
|
||||
subLength = subStepLength ? Math.max(subStepLength, 1) : undefined
|
||||
subIndex = subStepIndex
|
||||
length = Math.max(modules.length, 1)
|
||||
index = maxDone
|
||||
}
|
||||
|
||||
function isJobStepDone(type: FlowStatusModule.type | undefined) {
|
||||
if (!type) {
|
||||
return false
|
||||
}
|
||||
return type === FlowStatusModule.type.SUCCESS || type === FlowStatusModule.type.FAILURE
|
||||
}
|
||||
|
||||
let resetP: any
|
||||
|
||||
export function reset() {
|
||||
jobProgress.error = false
|
||||
jobProgress.steps = []
|
||||
resetP?.()
|
||||
error = undefined
|
||||
subIndex = undefined
|
||||
subLength = undefined
|
||||
length = 1
|
||||
index = 0
|
||||
}
|
||||
</script>
|
||||
|
||||
<ProgressBar {...jobProgress} {duration} class={$$props.class} />
|
||||
<ProgressBar bind:resetP {length} {index} {subLength} {subIndex} {error} class={$$props.class} />
|
||||
|
||||
@@ -1,73 +1,49 @@
|
||||
<script lang="ts">
|
||||
import { setContext } from 'svelte'
|
||||
import { writable } from 'svelte/store'
|
||||
import {
|
||||
type ProgressStep,
|
||||
type ProgressState,
|
||||
type LoopState,
|
||||
isLoopStep,
|
||||
isLoopState,
|
||||
getTween,
|
||||
type ProgressStateStoreValue
|
||||
} from './model'
|
||||
import ProgressBarGeneralPart from './ProgressBarGeneralPart.svelte'
|
||||
import ProgressBarLoopPart from './ProgressBarLoopPart.svelte'
|
||||
import { tweened } from 'svelte/motion'
|
||||
import { linear } from 'svelte/easing'
|
||||
|
||||
export function getTween(initialValue = 0, duration = 200) {
|
||||
return tweened(initialValue, {
|
||||
duration,
|
||||
easing: linear
|
||||
})
|
||||
}
|
||||
|
||||
export let error: number | undefined = undefined
|
||||
export let index: number
|
||||
export let subIndex: number | undefined
|
||||
export let subLength: number | undefined
|
||||
|
||||
export let length: number
|
||||
let duration = 200
|
||||
|
||||
export let steps: ProgressStep[]
|
||||
export let error = false
|
||||
export let duration = 100
|
||||
let percent = getTween(0, duration)
|
||||
let finished = false
|
||||
let state: ProgressState[] = []
|
||||
|
||||
const stateStore = writable<ProgressStateStoreValue>({
|
||||
length: state.length,
|
||||
index: 0,
|
||||
finished,
|
||||
error
|
||||
})
|
||||
setContext('state', stateStore)
|
||||
export function resetP() {
|
||||
percent = getTween(0, duration)
|
||||
}
|
||||
|
||||
$: state = steps.map((step, i) => {
|
||||
if (isLoopStep(step)) {
|
||||
return {
|
||||
type: step.type,
|
||||
isDone: error ? false : step.isDone,
|
||||
isDoneChanged: !state[i]?.isDone && step.isDone,
|
||||
length: step.length,
|
||||
index: step.index,
|
||||
indexChanged: (state[i] as LoopState)?.index !== step.index
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
type: step.type,
|
||||
isDone: step.isDone,
|
||||
isDoneChanged: !state[i]?.isDone && step.isDone
|
||||
}
|
||||
}
|
||||
})
|
||||
$: stateStore.update(({ index }) => ({ length: state.length, index, finished, error }))
|
||||
$: stepIndex = state.findIndex(({ isDone }) => !isDone)
|
||||
$: lastStep = state.at(-1)
|
||||
$: if (typeof lastStep?.isDone === 'boolean') {
|
||||
finished = lastStep.isDone
|
||||
if ($percent >= 100 && !lastStep.isDone) {
|
||||
percent = getTween(0, duration)
|
||||
$: percent.set(
|
||||
(length
|
||||
? index / length + (subIndex && subLength ? subIndex / (subLength ?? 1) / length : 0)
|
||||
: 0) * 100
|
||||
)
|
||||
|
||||
function getPercent(partIndex: number, _pct: number) {
|
||||
if (!length) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const res = Math.min(($percent - (partIndex / length) * 100) * length, 100)
|
||||
return res
|
||||
}
|
||||
$: subStepIndex = lastStep ? lastStep['index'] : undefined
|
||||
$: length = 100 / (state.length || 1)
|
||||
$: if (finished) {
|
||||
percent.set(100)
|
||||
} else {
|
||||
const product = length * stepIndex
|
||||
percent.set(product < 0 ? 0 : product)
|
||||
}
|
||||
|
||||
$: finished = index == length
|
||||
</script>
|
||||
|
||||
<div class={$$props.class}>
|
||||
<div
|
||||
class="flex justify-between items-end font-medium mb-1 {error
|
||||
class="flex justify-between items-end font-medium mb-1 {error != undefined
|
||||
? 'text-red-700 dark:text-red-200'
|
||||
: 'text-blue-700 dark:text-blue-200'}"
|
||||
>
|
||||
@@ -76,25 +52,37 @@
|
||||
? 'Error occured'
|
||||
: finished
|
||||
? 'Done'
|
||||
: `Step ${stepIndex + 1}${subStepIndex !== undefined ? `.${subStepIndex + 1}` : ''}`}
|
||||
: `Step ${index + 1}${subIndex !== undefined ? `.${subIndex + 1}` : ''}`}
|
||||
</span>
|
||||
<span class="text-sm">
|
||||
{state.length ? $percent.toFixed(0) : 0}%
|
||||
{$percent.toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
<!-- {#each state as step, index}
|
||||
{index} {JSON.stringify(step)}
|
||||
{/each} -->
|
||||
<!-- A: {index}, B: {length}
|
||||
{#each new Array(length) as _, index (index)}
|
||||
{index} -
|
||||
{getPercent(index)}
|
||||
|
|
||||
{/each} -->
|
||||
<div class="flex w-full bg-gray-200 rounded-full h-4 overflow-hidden">
|
||||
{#each state as step, index}
|
||||
<div
|
||||
class="h-full relative border-white {index === 0 ? '' : 'border-l'}"
|
||||
style="width: {length}%;"
|
||||
>
|
||||
{#if isLoopState(step)}
|
||||
<ProgressBarLoopPart {step} {index} {duration} />
|
||||
{:else}
|
||||
<ProgressBarGeneralPart {step} {index} {duration} />
|
||||
{#each new Array(length) as _, partIndex (partIndex)}
|
||||
<div class="h-full relative border-white {partIndex === 0 ? '' : 'border-l'} w-full">
|
||||
{#if partIndex < index - 1}
|
||||
<div
|
||||
class="absolute left-0 bottom-0 h-full w-full {error == partIndex
|
||||
? 'bg-red-400'
|
||||
: 'bg-blue-400'}"
|
||||
/>
|
||||
{:else if partIndex == index - 1 || (partIndex == index && subIndex !== undefined)}
|
||||
<div
|
||||
class="absolute left-0 bottom-0 h-full {error == partIndex
|
||||
? 'bg-red-400'
|
||||
: 'bg-blue-400'}"
|
||||
style="width: {getPercent(partIndex, $percent)}%"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import { getTween, type GeneralState, type ProgressStateStoreValue } from './model'
|
||||
|
||||
export let step: GeneralState
|
||||
export let index: number
|
||||
export let duration = 200
|
||||
const state = getContext<Writable<ProgressStateStoreValue>>('state')
|
||||
let progress = getTween(0, duration)
|
||||
|
||||
$: finishedAndNotLast = $state.finished && $state.length - 1 !== index
|
||||
$: if (finishedAndNotLast || $state.index > index) progress = getTween(100, duration)
|
||||
$: if ($state.finished || step.isDone) {
|
||||
if (index > $state.index) {
|
||||
state.update((prev) => ({ ...prev, index }))
|
||||
}
|
||||
progress.set(100)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="absolute left-0 bottom-0 h-full {$state.error ? 'bg-red-400' : 'bg-blue-400'}"
|
||||
style="width: {$progress}%"
|
||||
/>
|
||||
@@ -1,20 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import type { Tweened } from 'svelte/motion'
|
||||
import type { ProgressStateStoreValue } from './model'
|
||||
|
||||
export let progress: Tweened<number>
|
||||
export let index: number
|
||||
export let length: number
|
||||
const state = getContext<Writable<ProgressStateStoreValue>>('state')
|
||||
|
||||
$: isLast = index === length - 1
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="h-full absolute left-0 bottom-0 overflow-hidden {$state.error
|
||||
? 'bg-red-400'
|
||||
: 'bg-blue-400'}"
|
||||
style="width: {$progress}%; opacity: {isLast ? 100 : 100 / length}%"
|
||||
/>
|
||||
@@ -1,37 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import { get, type Writable } from 'svelte/store'
|
||||
import ProgressBarLoopAccessor from './ProgressBarLoopAccessor.svelte'
|
||||
import { getTween, type LoopState, type ProgressStateStoreValue } from './model'
|
||||
|
||||
export let step: LoopState
|
||||
export let index: number
|
||||
export let duration = 200
|
||||
const state = getContext<Writable<ProgressStateStoreValue>>('state')
|
||||
let progresses = getTweenArray()
|
||||
|
||||
$: finishedAndNotLast = $state.finished && $state.length - 1 !== index
|
||||
$: if (!$state.error && ($state.index > index || finishedAndNotLast)) {
|
||||
progresses = getTweenArray(100)
|
||||
}
|
||||
$: if (!$state.error && step.indexChanged) {
|
||||
progresses.filter((p, i) => get(p) === 0 && i < step.index).forEach((p) => p.set(100))
|
||||
progresses.forEach((p, i) => {
|
||||
if (get(p) === 0 && i < step.index) {
|
||||
p.set(100)
|
||||
if (index > $state.index) {
|
||||
state.update((prev) => ({ ...prev, index }))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
$: if (!$state.error && $state.finished) progresses.forEach((p) => p.set(100))
|
||||
|
||||
function getTweenArray(initial = 0) {
|
||||
return Array.from({ length: step.length }, () => getTween(initial, duration))
|
||||
}
|
||||
</script>
|
||||
|
||||
{#each progresses as progress, i}
|
||||
<ProgressBarLoopAccessor {progress} index={i} length={step.length} />
|
||||
{/each}
|
||||
@@ -1,3 +0,0 @@
|
||||
export { default as ProgressBar } from './ProgressBar.svelte'
|
||||
|
||||
export * from './model'
|
||||
@@ -1,45 +0,0 @@
|
||||
import { tweened } from 'svelte/motion'
|
||||
import { cubicOut } from 'svelte/easing'
|
||||
|
||||
export interface ProgressStateStoreValue {
|
||||
length: number
|
||||
index: number
|
||||
finished: boolean
|
||||
error: boolean
|
||||
}
|
||||
|
||||
export type StepKind = 'general' | 'loop'
|
||||
|
||||
type Step<T extends StepKind> = {
|
||||
type: T
|
||||
isDone: boolean
|
||||
}
|
||||
|
||||
export type GeneralStep = Step<'general'>
|
||||
export type LoopStep = Step<'loop'> & { index: number; length: number }
|
||||
export type ProgressStep = GeneralStep | LoopStep
|
||||
|
||||
export function isLoopStep(step: ProgressStep | undefined): step is LoopStep {
|
||||
return step?.type === 'loop'
|
||||
}
|
||||
|
||||
type State<T extends StepKind> = {
|
||||
type: T
|
||||
isDone: boolean
|
||||
isDoneChanged: boolean
|
||||
}
|
||||
|
||||
export type GeneralState = State<'general'>
|
||||
export type LoopState = LoopStep & { index: number; indexChanged: boolean }
|
||||
export type ProgressState = GeneralState | LoopState
|
||||
|
||||
export function isLoopState(state: ProgressState | undefined): state is LoopState {
|
||||
return state?.type === 'loop'
|
||||
}
|
||||
|
||||
export function getTween(initialValue = 0, duration = 200) {
|
||||
return tweened(initialValue, {
|
||||
duration,
|
||||
easing: cubicOut
|
||||
})
|
||||
}
|
||||
@@ -25,7 +25,8 @@
|
||||
'Canceled',
|
||||
'Missing service editorService',
|
||||
'Unexpected usage',
|
||||
'NetworkError when attempting to fetch resource.'
|
||||
'NetworkError when attempting to fetch resource.',
|
||||
"Client got disposed and can't be restarted."
|
||||
]
|
||||
|
||||
async function setUserWorkspaceStore() {
|
||||
@@ -162,7 +163,8 @@
|
||||
interval && clearInterval(interval)
|
||||
})
|
||||
|
||||
const darkMode = window.localStorage.getItem('dark-mode') ??
|
||||
const darkMode =
|
||||
window.localStorage.getItem('dark-mode') ??
|
||||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
|
||||
|
||||
if (darkMode === 'dark') {
|
||||
|
||||
Reference in New Issue
Block a user