feat(web): workflow publish add check list validate
This commit is contained in:
21
web/src/store/workflow.ts
Normal file
21
web/src/store/workflow.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* @Author: ZhaoYing
|
||||||
|
* @Date: 2026-04-10 18:11:19
|
||||||
|
* @Last Modified by: ZhaoYing
|
||||||
|
* @Last Modified time: 2026-04-10 18:11:19
|
||||||
|
*/
|
||||||
|
import { create } from 'zustand'
|
||||||
|
import type { NodeCheckResult } from '@/views/Workflow/components/CheckList'
|
||||||
|
|
||||||
|
interface WorkflowState {
|
||||||
|
checkResults: Record<string, NodeCheckResult[]>
|
||||||
|
setCheckResults: (appId: string, results: NodeCheckResult[]) => void
|
||||||
|
getCheckResults: (appId: string) => NodeCheckResult[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
||||||
|
checkResults: {},
|
||||||
|
setCheckResults: (appId, results) =>
|
||||||
|
set(state => ({ checkResults: { ...state.checkResults, [appId]: results } })),
|
||||||
|
getCheckResults: (appId) => get().checkResults[appId] ?? [],
|
||||||
|
}))
|
||||||
@@ -16,9 +16,9 @@ import { getReleaseList, rollbackRelease, appExport } from '@/api/application'
|
|||||||
import ReleaseModal from './components/ReleaseModal'
|
import ReleaseModal from './components/ReleaseModal'
|
||||||
import ReleaseShareModal from './components/ReleaseShareModal'
|
import ReleaseShareModal from './components/ReleaseShareModal'
|
||||||
import AppSharingModal from './components/AppSharingModal'
|
import AppSharingModal from './components/AppSharingModal'
|
||||||
import type { Release, ReleaseModalRef, ReleaseShareModalRef, AppSharingModalRef, WorkflowRef } from './types'
|
import type { Release, ReleaseModalRef, ReleaseShareModalRef, AppSharingModalRef } from './types'
|
||||||
import type { Application } from '@/views/ApplicationManagement/types'
|
import type { Application } from '@/views/ApplicationManagement/types'
|
||||||
import { runCheckOnGraph } from '@/views/Workflow/components/CheckList'
|
import { useWorkflowStore } from '@/store/workflow'
|
||||||
import Empty from '@/components/Empty'
|
import Empty from '@/components/Empty'
|
||||||
import { formatDateTime } from '@/utils/format';
|
import { formatDateTime } from '@/utils/format';
|
||||||
import Markdown from '@/components/Markdown'
|
import Markdown from '@/components/Markdown'
|
||||||
@@ -39,9 +39,10 @@ const heightClass = 'rb:max-h-[calc(100vh-140px)]'
|
|||||||
* @param data - Application data
|
* @param data - Application data
|
||||||
* @param refresh - Function to refresh application data
|
* @param refresh - Function to refresh application data
|
||||||
*/
|
*/
|
||||||
const ReleasePage: FC<{data: Application; refresh: () => void; workflowRef?: React.RefObject<WorkflowRef>}> = ({data, refresh, workflowRef}) => {
|
const ReleasePage: FC<{data: Application; refresh: () => void}> = ({data, refresh}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { message } = App.useApp()
|
const { message } = App.useApp()
|
||||||
|
const { getCheckResults } = useWorkflowStore()
|
||||||
const releaseModalRef = useRef<ReleaseModalRef>(null)
|
const releaseModalRef = useRef<ReleaseModalRef>(null)
|
||||||
const releaseShareModalRef = useRef<ReleaseShareModalRef>(null)
|
const releaseShareModalRef = useRef<ReleaseShareModalRef>(null)
|
||||||
const appSharingModalRef = useRef<AppSharingModalRef>(null)
|
const appSharingModalRef = useRef<AppSharingModalRef>(null)
|
||||||
@@ -148,13 +149,10 @@ const ReleasePage: FC<{data: Application; refresh: () => void; workflowRef?: Rea
|
|||||||
</>}
|
</>}
|
||||||
<RbButton type="primary" onClick={async () => {
|
<RbButton type="primary" onClick={async () => {
|
||||||
if (data?.type === 'workflow') {
|
if (data?.type === 'workflow') {
|
||||||
const graph = workflowRef?.current?.graphRef?.current
|
const errors = getCheckResults(data.id)
|
||||||
if (graph) {
|
if (errors.length) {
|
||||||
const errors = await runCheckOnGraph(graph, t)
|
message.error(t('workflow.checkListHasErrors'))
|
||||||
if (errors.length) {
|
return
|
||||||
message.error(t('workflow.checkListHasErrors'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
releaseModalRef.current?.handleOpen()
|
releaseModalRef.current?.handleOpen()
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
|
|||||||
</Flex>}
|
</Flex>}
|
||||||
extra={application?.type === 'workflow' && source !== 'sharing' && activeTab === 'arrangement'
|
extra={application?.type === 'workflow' && source !== 'sharing' && activeTab === 'arrangement'
|
||||||
? <Flex align="center" justify="end" gap={10} className="rb:h-8">
|
? <Flex align="center" justify="end" gap={10} className="rb:h-8">
|
||||||
<CheckList workflowRef={workflowRef} />
|
<CheckList workflowRef={workflowRef} appId={application?.id ?? ''} />
|
||||||
<Popover content={t('application.features')} classNames={{ body: 'rb:py-0.5! rb:px-1! rb:rounded-[6px]! rb:text-[12px]!' }}>
|
<Popover content={t('application.features')} classNames={{ body: 'rb:py-0.5! rb:px-1! rb:rounded-[6px]! rb:text-[12px]!' }}>
|
||||||
<div
|
<div
|
||||||
className="rb:cursor-pointer rb:size-7.5 rb:border rb:border-[#EBEBEB] rb:hover:bg-[#F6F6F6] rb:rounded-[10px] rb:bg-[url('@/assets/images/workflow/features.svg')] rb:bg-size-[16px_16px] rb:bg-center rb:bg-no-repeat"
|
className="rb:cursor-pointer rb:size-7.5 rb:border rb:border-[#EBEBEB] rb:hover:bg-[#F6F6F6] rb:rounded-[10px] rb:bg-[url('@/assets/images/workflow/features.svg')] rb:bg-size-[16px_16px] rb:bg-center rb:bg-no-repeat"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type FC, useState, useCallback, useEffect, useRef } from 'react'
|
import { useState, useCallback, useEffect, useRef, type FC } from 'react'
|
||||||
import { Popover, Flex } from 'antd'
|
import { Popover, Flex } from 'antd'
|
||||||
import { WarningFilled } from '@ant-design/icons'
|
import { WarningFilled } from '@ant-design/icons'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@@ -8,17 +8,19 @@ import type { WorkflowRef } from '@/views/ApplicationConfig/types'
|
|||||||
import { nodeLibrary } from '../../constant'
|
import { nodeLibrary } from '../../constant'
|
||||||
import { getToolMethods } from '@/api/tools'
|
import { getToolMethods } from '@/api/tools'
|
||||||
import RbDrawer from '@/components/RbDrawer'
|
import RbDrawer from '@/components/RbDrawer'
|
||||||
|
import { useWorkflowStore } from '@/store/workflow'
|
||||||
|
|
||||||
interface CheckListProps {
|
interface CheckListProps {
|
||||||
workflowRef: React.RefObject<WorkflowRef>
|
workflowRef: React.RefObject<WorkflowRef>
|
||||||
|
appId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CheckError {
|
export interface CheckError {
|
||||||
key: string
|
key: string
|
||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NodeCheckResult {
|
export interface NodeCheckResult {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
type: string
|
type: string
|
||||||
@@ -112,10 +114,67 @@ function validateNode(type: string, config: Record<string, any>): CheckError[] {
|
|||||||
return errors
|
return errors
|
||||||
}
|
}
|
||||||
|
|
||||||
const CheckList: FC<CheckListProps> = ({ workflowRef }) => {
|
export async function runCheckOnGraph(
|
||||||
|
graph: import('@antv/x6').Graph,
|
||||||
|
t: (key: string) => string
|
||||||
|
): Promise<NodeCheckResult[]> {
|
||||||
|
const nodes = graph.getNodes()
|
||||||
|
const edges = graph.getEdges()
|
||||||
|
const targetIds = new Set<string>()
|
||||||
|
const childTargetIds = new Set<string>()
|
||||||
|
edges.forEach(e => {
|
||||||
|
targetIds.add(e.getTargetCellId())
|
||||||
|
const srcData = graph.getCellById(e.getSourceCellId())?.getData()
|
||||||
|
const tgtData = graph.getCellById(e.getTargetCellId())?.getData()
|
||||||
|
if (srcData?.cycle && tgtData?.cycle && srcData.cycle === tgtData.cycle) {
|
||||||
|
childTargetIds.add(e.getTargetCellId())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const checked: NodeCheckResult[] = []
|
||||||
|
for (const node of nodes) {
|
||||||
|
const data = node.getData()
|
||||||
|
if (!data || ['add-node', 'notes', 'cycle-start', 'break'].includes(data.type)) continue
|
||||||
|
|
||||||
|
const errors: CheckError[] = []
|
||||||
|
const isChildNode = !!data.cycle
|
||||||
|
const hasIncoming = isChildNode ? childTargetIds.has(node.id) : !['start', 'cycle-start'].includes(data.type) ? targetIds.has(node.id) : true
|
||||||
|
if (!hasIncoming) errors.push({ key: 'notConnected', message: t('workflow.notConnected') })
|
||||||
|
|
||||||
|
const configErrors = validateNode(data.type, data.config ?? {})
|
||||||
|
configErrors.forEach(e => {
|
||||||
|
errors.push({ key: e.key, message: `${t(`workflow.checkListErrors.${e.key}`)} ${t('workflow.cannotBeEmpty')}`.trim() })
|
||||||
|
})
|
||||||
|
|
||||||
|
if (data.type === 'tool') {
|
||||||
|
const toolId = data.config?.tool_id?.defaultValue ?? data.config?.tool_id
|
||||||
|
const toolParameters = data.config?.tool_parameters?.defaultValue ?? data.config?.tool_parameters ?? {}
|
||||||
|
if (toolId) {
|
||||||
|
try {
|
||||||
|
const methods = await getToolMethods(toolId) as Array<{ name: string; parameters: Array<{ name: string; required: boolean }> }>
|
||||||
|
const operation = toolParameters?.operation
|
||||||
|
const method = operation ? methods.find(m => m.name === operation) : methods[0]
|
||||||
|
if (method) {
|
||||||
|
method.parameters
|
||||||
|
.filter(p => p.required && (toolParameters[p.name] === undefined || toolParameters[p.name] === null || toolParameters[p.name] === ''))
|
||||||
|
.forEach(p => errors.push({ key: 'tool.tool_parameters', message: `${p.name} ${t('workflow.cannotBeEmpty')}` }))
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length) {
|
||||||
|
checked.push({ id: node.id, name: data.name || t(`workflow.${data.type}`), type: data.type, icon: nodeIconMap[data.type] ?? '', errors })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return checked
|
||||||
|
}
|
||||||
|
|
||||||
|
const CheckList: FC<CheckListProps> = ({ workflowRef, appId }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [results, setResults] = useState<NodeCheckResult[]>([])
|
const { setCheckResults, getCheckResults } = useWorkflowStore()
|
||||||
|
const results = getCheckResults(appId)
|
||||||
const timerRef = useRef<ReturnType<typeof setTimeout>>()
|
const timerRef = useRef<ReturnType<typeof setTimeout>>()
|
||||||
|
|
||||||
const runCheck = useCallback(async () => {
|
const runCheck = useCallback(async () => {
|
||||||
@@ -195,7 +254,7 @@ const CheckList: FC<CheckListProps> = ({ workflowRef }) => {
|
|||||||
const scheduleCheck = useCallback(() => {
|
const scheduleCheck = useCallback(() => {
|
||||||
clearTimeout(timerRef.current)
|
clearTimeout(timerRef.current)
|
||||||
timerRef.current = setTimeout(async () => {
|
timerRef.current = setTimeout(async () => {
|
||||||
setResults(await runCheck())
|
setCheckResults(appId, await runCheck())
|
||||||
}, 500)
|
}, 500)
|
||||||
}, [runCheck])
|
}, [runCheck])
|
||||||
|
|
||||||
@@ -211,7 +270,7 @@ const CheckList: FC<CheckListProps> = ({ workflowRef }) => {
|
|||||||
}
|
}
|
||||||
}, [workflowRef.current?.graphRef?.current])
|
}, [workflowRef.current?.graphRef?.current])
|
||||||
|
|
||||||
const handleOpen = () => {
|
const handleOpen = () => {
|
||||||
setOpen(true)
|
setOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user