Feat/workflow retry (#11885)
parent
dacd457478
commit
0c0120ef27
@ -0,0 +1,41 @@
|
||||
import {
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import type { WorkflowRetryConfig } from './types'
|
||||
import {
|
||||
useNodeDataUpdate,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
|
||||
export const useRetryConfig = (
|
||||
id: string,
|
||||
) => {
|
||||
const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
|
||||
|
||||
const handleRetryConfigChange = useCallback((value?: WorkflowRetryConfig) => {
|
||||
handleNodeDataUpdateWithSyncDraft({
|
||||
id,
|
||||
data: {
|
||||
retry_config: value,
|
||||
},
|
||||
})
|
||||
}, [id, handleNodeDataUpdateWithSyncDraft])
|
||||
|
||||
return {
|
||||
handleRetryConfigChange,
|
||||
}
|
||||
}
|
||||
|
||||
export const useRetryDetailShowInSingleRun = () => {
|
||||
const [retryDetails, setRetryDetails] = useState<NodeTracing[] | undefined>()
|
||||
|
||||
const handleRetryDetailsChange = useCallback((details: NodeTracing[] | undefined) => {
|
||||
setRetryDetails(details)
|
||||
}, [])
|
||||
|
||||
return {
|
||||
retryDetails,
|
||||
handleRetryDetailsChange,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,88 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiAlertFill,
|
||||
RiCheckboxCircleFill,
|
||||
RiLoader2Line,
|
||||
} from '@remixicon/react'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { NodeRunningStatus } from '@/app/components/workflow/types'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type RetryOnNodeProps = Pick<Node, 'id' | 'data'>
|
||||
const RetryOnNode = ({
|
||||
data,
|
||||
}: RetryOnNodeProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { retry_config } = data
|
||||
const showSelectedBorder = data.selected || data._isBundled || data._isEntering
|
||||
const {
|
||||
isRunning,
|
||||
isSuccessful,
|
||||
isException,
|
||||
isFailed,
|
||||
} = useMemo(() => {
|
||||
return {
|
||||
isRunning: data._runningStatus === NodeRunningStatus.Running && !showSelectedBorder,
|
||||
isSuccessful: data._runningStatus === NodeRunningStatus.Succeeded && !showSelectedBorder,
|
||||
isFailed: data._runningStatus === NodeRunningStatus.Failed && !showSelectedBorder,
|
||||
isException: data._runningStatus === NodeRunningStatus.Exception && !showSelectedBorder,
|
||||
}
|
||||
}, [data._runningStatus, showSelectedBorder])
|
||||
const showDefault = !isRunning && !isSuccessful && !isException && !isFailed
|
||||
|
||||
if (!retry_config)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className='px-3'>
|
||||
<div className={cn(
|
||||
'flex items-center justify-between px-[5px] py-1 bg-workflow-block-parma-bg border-[0.5px] border-transparent rounded-md system-xs-medium-uppercase text-text-tertiary',
|
||||
isRunning && 'bg-state-accent-hover border-state-accent-active text-text-accent',
|
||||
isSuccessful && 'bg-state-success-hover border-state-success-active text-text-success',
|
||||
(isException || isFailed) && 'bg-state-warning-hover border-state-warning-active text-text-warning',
|
||||
)}>
|
||||
<div className='flex items-center'>
|
||||
{
|
||||
showDefault && (
|
||||
t('workflow.nodes.common.retry.retryTimes', { times: retry_config.max_retries })
|
||||
)
|
||||
}
|
||||
{
|
||||
isRunning && (
|
||||
<>
|
||||
<RiLoader2Line className='animate-spin mr-1 w-3.5 h-3.5' />
|
||||
{t('workflow.nodes.common.retry.retrying')}
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
isSuccessful && (
|
||||
<>
|
||||
<RiCheckboxCircleFill className='mr-1 w-3.5 h-3.5' />
|
||||
{t('workflow.nodes.common.retry.retrySuccessful')}
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
(isFailed || isException) && (
|
||||
<>
|
||||
<RiAlertFill className='mr-1 w-3.5 h-3.5' />
|
||||
{t('workflow.nodes.common.retry.retryFailed')}
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
!showDefault && (
|
||||
<div>
|
||||
{data._retryIndex}/{data.retry_config?.max_retries}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RetryOnNode
|
||||
@ -0,0 +1,117 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRetryConfig } from './hooks'
|
||||
import s from './style.module.css'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import Slider from '@/app/components/base/slider'
|
||||
import Input from '@/app/components/base/input'
|
||||
import type {
|
||||
Node,
|
||||
} from '@/app/components/workflow/types'
|
||||
import Split from '@/app/components/workflow/nodes/_base/components/split'
|
||||
|
||||
type RetryOnPanelProps = Pick<Node, 'id' | 'data'>
|
||||
const RetryOnPanel = ({
|
||||
id,
|
||||
data,
|
||||
}: RetryOnPanelProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleRetryConfigChange } = useRetryConfig(id)
|
||||
const { retry_config } = data
|
||||
|
||||
const handleRetryEnabledChange = (value: boolean) => {
|
||||
handleRetryConfigChange({
|
||||
retry_enabled: value,
|
||||
max_retries: retry_config?.max_retries || 3,
|
||||
retry_interval: retry_config?.retry_interval || 1000,
|
||||
})
|
||||
}
|
||||
|
||||
const handleMaxRetriesChange = (value: number) => {
|
||||
if (value > 10)
|
||||
value = 10
|
||||
else if (value < 1)
|
||||
value = 1
|
||||
handleRetryConfigChange({
|
||||
retry_enabled: true,
|
||||
max_retries: value,
|
||||
retry_interval: retry_config?.retry_interval || 1000,
|
||||
})
|
||||
}
|
||||
|
||||
const handleRetryIntervalChange = (value: number) => {
|
||||
if (value > 5000)
|
||||
value = 5000
|
||||
else if (value < 100)
|
||||
value = 100
|
||||
handleRetryConfigChange({
|
||||
retry_enabled: true,
|
||||
max_retries: retry_config?.max_retries || 3,
|
||||
retry_interval: value,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='pt-2'>
|
||||
<div className='flex items-center justify-between px-4 py-2 h-10'>
|
||||
<div className='flex items-center'>
|
||||
<div className='mr-0.5 system-sm-semibold-uppercase text-text-secondary'>{t('workflow.nodes.common.retry.retryOnFailure')}</div>
|
||||
</div>
|
||||
<Switch
|
||||
defaultValue={retry_config?.retry_enabled}
|
||||
onChange={v => handleRetryEnabledChange(v)}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
retry_config?.retry_enabled && (
|
||||
<div className='px-4 pb-2'>
|
||||
<div className='flex items-center mb-1 w-full'>
|
||||
<div className='grow mr-2 system-xs-medium-uppercase'>{t('workflow.nodes.common.retry.maxRetries')}</div>
|
||||
<Slider
|
||||
className='mr-3 w-[108px]'
|
||||
value={retry_config?.max_retries || 3}
|
||||
onChange={handleMaxRetriesChange}
|
||||
min={1}
|
||||
max={10}
|
||||
/>
|
||||
<Input
|
||||
type='number'
|
||||
wrapperClassName='w-[80px]'
|
||||
value={retry_config?.max_retries || 3}
|
||||
onChange={e => handleMaxRetriesChange(e.target.value as any)}
|
||||
min={1}
|
||||
max={10}
|
||||
unit={t('workflow.nodes.common.retry.times') || ''}
|
||||
className={s.input}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex items-center'>
|
||||
<div className='grow mr-2 system-xs-medium-uppercase'>{t('workflow.nodes.common.retry.retryInterval')}</div>
|
||||
<Slider
|
||||
className='mr-3 w-[108px]'
|
||||
value={retry_config?.retry_interval || 1000}
|
||||
onChange={handleRetryIntervalChange}
|
||||
min={100}
|
||||
max={5000}
|
||||
/>
|
||||
<Input
|
||||
type='number'
|
||||
wrapperClassName='w-[80px]'
|
||||
value={retry_config?.retry_interval || 1000}
|
||||
onChange={e => handleRetryIntervalChange(e.target.value as any)}
|
||||
min={100}
|
||||
max={5000}
|
||||
unit={t('workflow.nodes.common.retry.ms') || ''}
|
||||
className={s.input}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<Split className='mx-4 mt-2' />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default RetryOnPanel
|
||||
@ -0,0 +1,5 @@
|
||||
.input::-webkit-inner-spin-button,
|
||||
.input::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
export type WorkflowRetryConfig = {
|
||||
max_retries: number
|
||||
retry_interval: number
|
||||
retry_enabled: boolean
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
'use client'
|
||||
|
||||
import type { FC } from 'react'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiArrowLeftLine,
|
||||
} from '@remixicon/react'
|
||||
import TracingPanel from './tracing-panel'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
|
||||
type Props = {
|
||||
list: NodeTracing[]
|
||||
onBack: () => void
|
||||
}
|
||||
|
||||
const RetryResultPanel: FC<Props> = ({
|
||||
list,
|
||||
onBack,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className='flex items-center px-4 h-8 text-text-accent-secondary bg-components-panel-bg system-sm-medium cursor-pointer'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
onBack()
|
||||
}}
|
||||
>
|
||||
<RiArrowLeftLine className='mr-1 w-4 h-4' />
|
||||
{t('workflow.singleRun.back')}
|
||||
</div>
|
||||
<TracingPanel
|
||||
list={list.map((item, index) => ({
|
||||
...item,
|
||||
title: `${t('workflow.nodes.common.retry.retry')} ${index + 1}`,
|
||||
}))}
|
||||
className='bg-background-section-burn'
|
||||
/>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
export default memo(RetryResultPanel)
|
||||
Loading…
Reference in New Issue