1
This commit is contained in:
parent
a542ee9d2f
commit
20c4184d45
@ -152,8 +152,8 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({
|
||||
fitViewOptions={{
|
||||
padding: 0.1,
|
||||
includeHiddenNodes: false,
|
||||
minZoom: 0.5,
|
||||
maxZoom: 1.5,
|
||||
minZoom: 1.0,
|
||||
maxZoom: 1.0,
|
||||
duration: 800,
|
||||
}}
|
||||
minZoom={0.1}
|
||||
|
||||
@ -0,0 +1,256 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Form, Input, Tabs, Card, Button, Space, message } from 'antd';
|
||||
import { SaveOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import type { FlowNode, FlowNodeData } from '../types';
|
||||
|
||||
interface NodeConfigModalProps {
|
||||
visible: boolean;
|
||||
node: FlowNode | null;
|
||||
onCancel: () => void;
|
||||
onOk: (nodeId: string, updatedData: Partial<FlowNodeData>) => void;
|
||||
}
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
||||
visible,
|
||||
node,
|
||||
onCancel,
|
||||
onOk
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('basic');
|
||||
|
||||
// 重置表单数据
|
||||
useEffect(() => {
|
||||
if (visible && node) {
|
||||
form.setFieldsValue({
|
||||
label: node.data.label,
|
||||
description: node.data.nodeDefinition?.description || '',
|
||||
// 基础配置
|
||||
timeout: node.data.configs?.timeout || 3600,
|
||||
retryCount: node.data.configs?.retryCount || 0,
|
||||
priority: node.data.configs?.priority || 'normal',
|
||||
// 输入映射
|
||||
inputMapping: JSON.stringify(node.data.inputMapping || {}, null, 2),
|
||||
// 输出映射
|
||||
outputMapping: JSON.stringify(node.data.outputMapping || {}, null, 2),
|
||||
});
|
||||
} else {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [visible, node, form]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!node) return;
|
||||
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setLoading(true);
|
||||
|
||||
// 解析JSON字符串
|
||||
let inputMapping = {};
|
||||
let outputMapping = {};
|
||||
|
||||
try {
|
||||
inputMapping = values.inputMapping ? JSON.parse(values.inputMapping) : {};
|
||||
} catch (error) {
|
||||
message.error('输入映射JSON格式错误');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
outputMapping = values.outputMapping ? JSON.parse(values.outputMapping) : {};
|
||||
} catch (error) {
|
||||
message.error('输出映射JSON格式错误');
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedData: Partial<FlowNodeData> = {
|
||||
label: values.label,
|
||||
configs: {
|
||||
...node.data.configs,
|
||||
timeout: values.timeout,
|
||||
retryCount: values.retryCount,
|
||||
priority: values.priority,
|
||||
},
|
||||
inputMapping,
|
||||
outputMapping,
|
||||
};
|
||||
|
||||
onOk(node.id, updatedData);
|
||||
message.success('节点配置保存成功');
|
||||
onCancel();
|
||||
} catch (error) {
|
||||
console.error('保存节点配置失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
form.resetFields();
|
||||
if (node) {
|
||||
form.setFieldsValue({
|
||||
label: node.data.label,
|
||||
description: node.data.nodeDefinition?.description || '',
|
||||
timeout: 3600,
|
||||
retryCount: 0,
|
||||
priority: 'normal',
|
||||
inputMapping: '{}',
|
||||
outputMapping: '{}',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renderBasicConfig = () => (
|
||||
<Card size="small" title="基本信息">
|
||||
<Form.Item
|
||||
label="节点名称"
|
||||
name="label"
|
||||
rules={[{ required: true, message: '请输入节点名称' }]}
|
||||
>
|
||||
<Input placeholder="请输入节点名称" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="节点描述" name="description">
|
||||
<TextArea
|
||||
placeholder="请输入节点描述"
|
||||
rows={3}
|
||||
maxLength={500}
|
||||
showCount
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="超时时间(秒)"
|
||||
name="timeout"
|
||||
rules={[{ required: true, message: '请输入超时时间' }]}
|
||||
>
|
||||
<Input type="number" min={1} placeholder="3600" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="重试次数"
|
||||
name="retryCount"
|
||||
>
|
||||
<Input type="number" min={0} max={10} placeholder="0" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="优先级"
|
||||
name="priority"
|
||||
>
|
||||
<Input placeholder="normal" />
|
||||
</Form.Item>
|
||||
</Card>
|
||||
);
|
||||
|
||||
const renderInputMapping = () => (
|
||||
<Card size="small" title="输入参数映射">
|
||||
<Form.Item
|
||||
label="输入映射配置"
|
||||
name="inputMapping"
|
||||
extra="请输入有效的JSON格式,用于配置节点的输入参数映射"
|
||||
>
|
||||
<TextArea
|
||||
placeholder={`示例:
|
||||
{
|
||||
"param1": "\${workflow.variable1}",
|
||||
"param2": "固定值",
|
||||
"param3": "\${previous.output.result}"
|
||||
}`}
|
||||
rows={12}
|
||||
style={{ fontFamily: 'Monaco, Consolas, monospace' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
);
|
||||
|
||||
const renderOutputMapping = () => (
|
||||
<Card size="small" title="输出参数映射">
|
||||
<Form.Item
|
||||
label="输出映射配置"
|
||||
name="outputMapping"
|
||||
extra="请输入有效的JSON格式,用于配置节点的输出参数映射"
|
||||
>
|
||||
<TextArea
|
||||
placeholder={`示例:
|
||||
{
|
||||
"result": "\${task.output.result}",
|
||||
"status": "\${task.status}",
|
||||
"message": "\${task.output.message}"
|
||||
}`}
|
||||
rows={12}
|
||||
style={{ fontFamily: 'Monaco, Consolas, monospace' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`配置节点: ${node?.data?.label || '未知节点'}`}
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
width={800}
|
||||
style={{ top: 20 }}
|
||||
footer={
|
||||
<Space>
|
||||
<Button onClick={onCancel}>取消</Button>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={handleReset}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={loading}
|
||||
icon={<SaveOutlined />}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
保存配置
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
timeout: 3600,
|
||||
retryCount: 0,
|
||||
priority: 'normal',
|
||||
inputMapping: '{}',
|
||||
outputMapping: '{}'
|
||||
}}
|
||||
>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
items={[
|
||||
{
|
||||
key: 'basic',
|
||||
label: '基本配置',
|
||||
children: renderBasicConfig()
|
||||
},
|
||||
{
|
||||
key: 'input',
|
||||
label: '输入映射',
|
||||
children: renderInputMapping()
|
||||
},
|
||||
{
|
||||
key: 'output',
|
||||
label: '输出映射',
|
||||
children: renderOutputMapping()
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default NodeConfigModal;
|
||||
238
frontend/src/pages/Workflow2/Design/components/PropertyPanel.tsx
Normal file
238
frontend/src/pages/Workflow2/Design/components/PropertyPanel.tsx
Normal file
@ -0,0 +1,238 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Form, Input, Select, Button, Space, message, Divider } from 'antd';
|
||||
import { SaveOutlined, ClearOutlined } from '@ant-design/icons';
|
||||
import type { FlowNode, FlowEdge } from '../types';
|
||||
|
||||
interface PropertyPanelProps {
|
||||
selectedNode?: FlowNode | null;
|
||||
selectedEdge?: FlowEdge | null;
|
||||
onNodeUpdate?: (nodeId: string, updates: any) => void;
|
||||
onEdgeUpdate?: (edgeId: string, updates: any) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
||||
selectedNode,
|
||||
selectedEdge,
|
||||
onNodeUpdate,
|
||||
onEdgeUpdate,
|
||||
className = ''
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
|
||||
// 重置表单
|
||||
useEffect(() => {
|
||||
if (selectedNode) {
|
||||
form.setFieldsValue({
|
||||
label: selectedNode.data.label,
|
||||
timeout: selectedNode.data.configs?.timeout || 3600,
|
||||
retryCount: selectedNode.data.configs?.retryCount || 0,
|
||||
priority: selectedNode.data.configs?.priority || 'normal',
|
||||
});
|
||||
} else if (selectedEdge) {
|
||||
form.setFieldsValue({
|
||||
label: selectedEdge.data?.label || '',
|
||||
conditionType: selectedEdge.data?.condition?.type || 'DEFAULT',
|
||||
expression: selectedEdge.data?.condition?.expression || '',
|
||||
});
|
||||
} else {
|
||||
form.resetFields();
|
||||
}
|
||||
setHasChanges(false);
|
||||
}, [selectedNode, selectedEdge, form]);
|
||||
|
||||
// 监听表单变化
|
||||
const handleFormChange = () => {
|
||||
setHasChanges(true);
|
||||
};
|
||||
|
||||
// 保存配置
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
|
||||
if (selectedNode && onNodeUpdate) {
|
||||
const updates = {
|
||||
label: values.label,
|
||||
configs: {
|
||||
...selectedNode.data.configs,
|
||||
timeout: values.timeout,
|
||||
retryCount: values.retryCount,
|
||||
priority: values.priority,
|
||||
},
|
||||
};
|
||||
onNodeUpdate(selectedNode.id, updates);
|
||||
message.success('节点配置已保存');
|
||||
} else if (selectedEdge && onEdgeUpdate) {
|
||||
const updates = {
|
||||
label: values.label,
|
||||
condition: {
|
||||
type: values.conditionType,
|
||||
expression: values.expression,
|
||||
priority: 0,
|
||||
},
|
||||
};
|
||||
onEdgeUpdate(selectedEdge.id, updates);
|
||||
message.success('连接配置已保存');
|
||||
}
|
||||
|
||||
setHasChanges(false);
|
||||
} catch (error) {
|
||||
message.error('保存失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 清空选择
|
||||
const handleClear = () => {
|
||||
form.resetFields();
|
||||
setHasChanges(false);
|
||||
};
|
||||
|
||||
// 渲染节点属性
|
||||
const renderNodeProperties = () => {
|
||||
if (!selectedNode) return null;
|
||||
|
||||
return (
|
||||
<Card size="small" title={`节点属性: ${selectedNode.data.nodeType}`}>
|
||||
<Form.Item label="节点名称" name="label" rules={[{ required: true }]}>
|
||||
<Input placeholder="请输入节点名称" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="超时时间(秒)" name="timeout">
|
||||
<Input type="number" min={1} placeholder="3600" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="重试次数" name="retryCount">
|
||||
<Input type="number" min={0} max={10} placeholder="0" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="优先级" name="priority">
|
||||
<Select placeholder="选择优先级">
|
||||
<Select.Option value="low">低</Select.Option>
|
||||
<Select.Option value="normal">普通</Select.Option>
|
||||
<Select.Option value="high">高</Select.Option>
|
||||
<Select.Option value="urgent">紧急</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染边属性
|
||||
const renderEdgeProperties = () => {
|
||||
if (!selectedEdge) return null;
|
||||
|
||||
return (
|
||||
<Card size="small" title="连接属性">
|
||||
<Form.Item label="连接名称" name="label">
|
||||
<Input placeholder="请输入连接名称" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="条件类型" name="conditionType">
|
||||
<Select placeholder="选择条件类型">
|
||||
<Select.Option value="DEFAULT">默认</Select.Option>
|
||||
<Select.Option value="EXPRESSION">表达式</Select.Option>
|
||||
<Select.Option value="SCRIPT">脚本</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="条件表达式" name="expression">
|
||||
<Input.TextArea
|
||||
placeholder="请输入条件表达式"
|
||||
rows={4}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`property-panel ${className}`} style={{
|
||||
width: '300px',
|
||||
height: '100%',
|
||||
background: '#f8fafc',
|
||||
borderLeft: '1px solid #e5e7eb',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
<div style={{
|
||||
padding: '16px',
|
||||
background: '#ffffff',
|
||||
borderBottom: '1px solid #e5e7eb',
|
||||
flexShrink: 0
|
||||
}}>
|
||||
<h3 style={{
|
||||
margin: 0,
|
||||
fontSize: '14px',
|
||||
fontWeight: '600',
|
||||
color: '#374151'
|
||||
}}>
|
||||
属性面板
|
||||
</h3>
|
||||
<p style={{
|
||||
margin: '4px 0 0 0',
|
||||
fontSize: '12px',
|
||||
color: '#6b7280'
|
||||
}}>
|
||||
{selectedNode ? '节点配置' : selectedEdge ? '连接配置' : '请选择节点或连接'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, overflow: 'auto', padding: '16px' }}>
|
||||
{!selectedNode && !selectedEdge && (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
color: '#9ca3af',
|
||||
fontSize: '14px',
|
||||
marginTop: '40px'
|
||||
}}>
|
||||
<div style={{ fontSize: '48px', marginBottom: '16px' }}>📋</div>
|
||||
<p>请选择一个节点或连接</p>
|
||||
<p style={{ fontSize: '12px' }}>点击节点或连接来编辑属性</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onValuesChange={handleFormChange}
|
||||
>
|
||||
{renderNodeProperties()}
|
||||
{renderEdgeProperties()}
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
{(selectedNode || selectedEdge) && (
|
||||
<div style={{
|
||||
padding: '16px',
|
||||
background: '#ffffff',
|
||||
borderTop: '1px solid #e5e7eb',
|
||||
flexShrink: 0
|
||||
}}>
|
||||
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
|
||||
<Button
|
||||
icon={<ClearOutlined />}
|
||||
onClick={handleClear}
|
||||
size="small"
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
onClick={handleSave}
|
||||
disabled={!hasChanges}
|
||||
size="small"
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PropertyPanel;
|
||||
163
frontend/src/pages/Workflow2/Design/hooks/useWorkflowLoad.ts
Normal file
163
frontend/src/pages/Workflow2/Design/hooks/useWorkflowLoad.ts
Normal file
@ -0,0 +1,163 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { message } from 'antd';
|
||||
import * as definitionService from '../../Definition/service';
|
||||
import type { FlowNode, FlowEdge } from '../types';
|
||||
import type { WorkflowDefinition } from '../../Definition/types';
|
||||
|
||||
interface LoadedWorkflowData {
|
||||
nodes: FlowNode[];
|
||||
edges: FlowEdge[];
|
||||
definition: WorkflowDefinition;
|
||||
}
|
||||
|
||||
export const useWorkflowLoad = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [workflowDefinition, setWorkflowDefinition] = useState<WorkflowDefinition | null>(null);
|
||||
|
||||
// 从后端数据转换为React Flow格式
|
||||
const convertToFlowFormat = useCallback((definition: WorkflowDefinition): LoadedWorkflowData => {
|
||||
const nodes: FlowNode[] = (definition.graph?.nodes || []).map(node => ({
|
||||
id: node.id.toString(),
|
||||
type: node.nodeType,
|
||||
position: node.uiVariables?.position || { x: 100, y: 100 },
|
||||
data: {
|
||||
label: node.nodeName,
|
||||
nodeType: node.nodeType as any,
|
||||
category: 'TASK' as any, // 默认分类,可以根据nodeType推导
|
||||
icon: getNodeIcon(node.nodeType),
|
||||
color: getNodeColor(node.nodeType),
|
||||
configs: node.panelVariables || {},
|
||||
inputMapping: node.localVariables?.inputMapping || {},
|
||||
outputMapping: node.localVariables?.outputMapping || {},
|
||||
nodeDefinition: {
|
||||
nodeCode: node.nodeCode,
|
||||
nodeName: node.nodeName,
|
||||
nodeType: node.nodeType as any,
|
||||
category: 'TASK' as any,
|
||||
icon: getNodeIcon(node.nodeType),
|
||||
color: getNodeColor(node.nodeType)
|
||||
}
|
||||
},
|
||||
selected: node.uiVariables?.selected || false,
|
||||
dragging: node.uiVariables?.dragging || false
|
||||
}));
|
||||
|
||||
const edges: FlowEdge[] = (definition.graph?.edges || []).map(edge => ({
|
||||
id: edge.id,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
type: edge.type || 'default',
|
||||
animated: edge.animated || true,
|
||||
data: edge.data || {
|
||||
label: '连接',
|
||||
condition: {
|
||||
type: 'DEFAULT',
|
||||
priority: 0
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
return {
|
||||
nodes,
|
||||
edges,
|
||||
definition
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 根据节点类型获取图标
|
||||
const getNodeIcon = (nodeType: string): string => {
|
||||
const iconMap: Record<string, string> = {
|
||||
'START_EVENT': '▶️',
|
||||
'END_EVENT': '⏹️',
|
||||
'USER_TASK': '👤',
|
||||
'SERVICE_TASK': '⚙️',
|
||||
'SCRIPT_TASK': '📜',
|
||||
'DEPLOY_NODE': '🚀',
|
||||
'JENKINS_BUILD': '🔨',
|
||||
'GATEWAY_NODE': '◇',
|
||||
'SUB_PROCESS': '📦',
|
||||
'CALL_ACTIVITY': '📞'
|
||||
};
|
||||
return iconMap[nodeType] || '📋';
|
||||
};
|
||||
|
||||
// 根据节点类型获取颜色
|
||||
const getNodeColor = (nodeType: string): string => {
|
||||
const colorMap: Record<string, string> = {
|
||||
'START_EVENT': '#10b981',
|
||||
'END_EVENT': '#ef4444',
|
||||
'USER_TASK': '#6366f1',
|
||||
'SERVICE_TASK': '#f59e0b',
|
||||
'SCRIPT_TASK': '#8b5cf6',
|
||||
'DEPLOY_NODE': '#06b6d4',
|
||||
'JENKINS_BUILD': '#f97316',
|
||||
'GATEWAY_NODE': '#84cc16',
|
||||
'SUB_PROCESS': '#ec4899',
|
||||
'CALL_ACTIVITY': '#14b8a6'
|
||||
};
|
||||
return colorMap[nodeType] || '#6b7280';
|
||||
};
|
||||
|
||||
// 加载工作流定义
|
||||
const loadWorkflow = useCallback(async (workflowId: number): Promise<LoadedWorkflowData | null> => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const definition = await definitionService.getDefinitionDetail(workflowId);
|
||||
setWorkflowDefinition(definition);
|
||||
|
||||
const flowData = convertToFlowFormat(definition);
|
||||
|
||||
message.success(`工作流 "${definition.name}" 加载成功`);
|
||||
return flowData;
|
||||
} catch (error) {
|
||||
console.error('加载工作流失败:', error);
|
||||
message.error('加载工作流失败');
|
||||
return null;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [convertToFlowFormat]);
|
||||
|
||||
// 创建新的空白工作流
|
||||
const createNewWorkflow = useCallback((): LoadedWorkflowData => {
|
||||
const emptyDefinition: WorkflowDefinition = {
|
||||
id: 0,
|
||||
name: '新建工作流',
|
||||
key: `workflow_${Date.now()}`,
|
||||
description: '',
|
||||
category: 'GENERAL',
|
||||
triggers: ['MANUAL'],
|
||||
status: 'DRAFT',
|
||||
graph: {
|
||||
nodes: [],
|
||||
edges: []
|
||||
},
|
||||
formConfig: {
|
||||
formItems: []
|
||||
}
|
||||
};
|
||||
|
||||
setWorkflowDefinition(emptyDefinition);
|
||||
|
||||
return {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
definition: emptyDefinition
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 重置加载状态
|
||||
const resetLoad = useCallback(() => {
|
||||
setWorkflowDefinition(null);
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
loading,
|
||||
workflowDefinition,
|
||||
loadWorkflow,
|
||||
createNewWorkflow,
|
||||
resetLoad
|
||||
};
|
||||
};
|
||||
125
frontend/src/pages/Workflow2/Design/hooks/useWorkflowSave.ts
Normal file
125
frontend/src/pages/Workflow2/Design/hooks/useWorkflowSave.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { message } from 'antd';
|
||||
import * as definitionService from '../../Definition/service';
|
||||
import type { FlowNode, FlowEdge } from '../types';
|
||||
import type { WorkflowDefinition } from '../../Definition/types';
|
||||
|
||||
interface WorkflowSaveData {
|
||||
nodes: FlowNode[];
|
||||
edges: FlowEdge[];
|
||||
workflowId?: number;
|
||||
name?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export const useWorkflowSave = () => {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [lastSaved, setLastSaved] = useState<Date | null>(null);
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
||||
|
||||
// 保存工作流数据
|
||||
const saveWorkflow = useCallback(async (data: WorkflowSaveData): Promise<boolean> => {
|
||||
setSaving(true);
|
||||
|
||||
try {
|
||||
// 转换节点和边数据为后端格式
|
||||
const graph = {
|
||||
nodes: data.nodes.map(node => ({
|
||||
id: parseInt(node.id) || 0,
|
||||
nodeCode: `node_${node.id}`,
|
||||
nodeType: node.data.nodeType,
|
||||
nodeName: node.data.label,
|
||||
uiVariables: {
|
||||
position: node.position,
|
||||
selected: node.selected || false,
|
||||
dragging: node.dragging || false
|
||||
},
|
||||
panelVariables: node.data.configs || {},
|
||||
localVariables: {
|
||||
inputMapping: node.data.inputMapping || {},
|
||||
outputMapping: node.data.outputMapping || {}
|
||||
}
|
||||
})),
|
||||
edges: data.edges.map(edge => ({
|
||||
id: edge.id,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
type: edge.type || 'default',
|
||||
animated: edge.animated || false,
|
||||
data: edge.data || {}
|
||||
}))
|
||||
};
|
||||
|
||||
const workflowData: Partial<WorkflowDefinition> = {
|
||||
name: data.name || '未命名工作流',
|
||||
description: data.description || '',
|
||||
graph,
|
||||
// 生成简单的表单配置
|
||||
formConfig: {
|
||||
formItems: []
|
||||
}
|
||||
};
|
||||
|
||||
let result: WorkflowDefinition;
|
||||
|
||||
if (data.workflowId) {
|
||||
// 更新现有工作流
|
||||
result = await definitionService.updateDefinition(data.workflowId, workflowData);
|
||||
message.success('工作流保存成功');
|
||||
} else {
|
||||
// 创建新工作流
|
||||
result = await definitionService.createDefinition({
|
||||
...workflowData,
|
||||
key: `workflow_${Date.now()}`,
|
||||
category: 'GENERAL',
|
||||
triggers: ['MANUAL']
|
||||
} as any);
|
||||
message.success('工作流创建成功');
|
||||
}
|
||||
|
||||
setLastSaved(new Date());
|
||||
setHasUnsavedChanges(false);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('保存工作流失败:', error);
|
||||
message.error('保存工作流失败');
|
||||
return false;
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 自动保存功能
|
||||
const autoSave = useCallback(async (data: WorkflowSaveData) => {
|
||||
if (!hasUnsavedChanges || !data.workflowId) return;
|
||||
|
||||
try {
|
||||
await saveWorkflow(data);
|
||||
console.log('自动保存成功');
|
||||
} catch (error) {
|
||||
console.error('自动保存失败:', error);
|
||||
}
|
||||
}, [hasUnsavedChanges, saveWorkflow]);
|
||||
|
||||
// 标记有未保存更改
|
||||
const markUnsaved = useCallback(() => {
|
||||
setHasUnsavedChanges(true);
|
||||
}, []);
|
||||
|
||||
// 标记已保存
|
||||
const markSaved = useCallback(() => {
|
||||
setHasUnsavedChanges(false);
|
||||
setLastSaved(new Date());
|
||||
}, []);
|
||||
|
||||
return {
|
||||
saving,
|
||||
lastSaved,
|
||||
hasUnsavedChanges,
|
||||
saveWorkflow,
|
||||
autoSave,
|
||||
markUnsaved,
|
||||
markSaved
|
||||
};
|
||||
};
|
||||
@ -6,8 +6,12 @@ import { ReactFlowProvider, useReactFlow } from '@xyflow/react';
|
||||
import WorkflowToolbar from './components/WorkflowToolbar';
|
||||
import NodePanel from './components/NodePanel';
|
||||
import FlowCanvas from './components/FlowCanvas';
|
||||
import type { FlowNode, FlowEdge, DragNodeData } from './types';
|
||||
import NodeConfigModal from './components/NodeConfigModal';
|
||||
import PropertyPanel from './components/PropertyPanel';
|
||||
import type { FlowNode, FlowEdge, DragNodeData, FlowNodeData } from './types';
|
||||
import { NodeType } from './types';
|
||||
import { useWorkflowSave } from './hooks/useWorkflowSave';
|
||||
import { useWorkflowLoad } from './hooks/useWorkflowLoad';
|
||||
|
||||
// 样式
|
||||
import '@xyflow/react/dist/style.css';
|
||||
@ -30,6 +34,34 @@ const WorkflowDesignInner: React.FC = () => {
|
||||
|
||||
const [workflowTitle, setWorkflowTitle] = useState('新建工作流');
|
||||
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 当前工作流ID
|
||||
const currentWorkflowId = id ? parseInt(id) : undefined;
|
||||
|
||||
// 节点配置模态框状态
|
||||
const [configModalVisible, setConfigModalVisible] = useState(false);
|
||||
const [selectedNode, setSelectedNode] = useState<FlowNode | null>(null);
|
||||
const [selectedEdge, setSelectedEdge] = useState<FlowEdge | null>(null);
|
||||
|
||||
// 保存和加载hooks
|
||||
const { saving, hasUnsavedChanges, saveWorkflow, markUnsaved } = useWorkflowSave();
|
||||
const { loading: loadingWorkflow, workflowDefinition, loadWorkflow } = useWorkflowLoad();
|
||||
|
||||
// 加载工作流数据
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
if (currentWorkflowId) {
|
||||
const data = await loadWorkflow(currentWorkflowId);
|
||||
if (data) {
|
||||
setNodes(data.nodes);
|
||||
setEdges(data.edges);
|
||||
setWorkflowTitle(data.definition.name);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadData();
|
||||
}, [currentWorkflowId, loadWorkflow, setNodes, setEdges]);
|
||||
|
||||
// 自动适应视图
|
||||
useEffect(() => {
|
||||
@ -38,8 +70,8 @@ const WorkflowDesignInner: React.FC = () => {
|
||||
fitView({
|
||||
padding: 0.1,
|
||||
duration: 800,
|
||||
minZoom: 0.5,
|
||||
maxZoom: 1.5
|
||||
minZoom: 1.0, // 最小缩放100%
|
||||
maxZoom: 1.0 // 最大缩放100%,确保默认100%
|
||||
});
|
||||
}, 100);
|
||||
|
||||
@ -118,12 +150,22 @@ const WorkflowDesignInner: React.FC = () => {
|
||||
];
|
||||
|
||||
// 工具栏事件处理
|
||||
const handleSave = useCallback(() => {
|
||||
const nodes = getNodes();
|
||||
const edges = getEdges();
|
||||
console.log('保存工作流:', { nodes, edges });
|
||||
message.success('工作流保存成功!');
|
||||
}, [getNodes, getEdges]);
|
||||
const handleSave = useCallback(async () => {
|
||||
const nodes = getNodes() as FlowNode[];
|
||||
const edges = getEdges() as FlowEdge[];
|
||||
|
||||
const success = await saveWorkflow({
|
||||
nodes,
|
||||
edges,
|
||||
workflowId: currentWorkflowId,
|
||||
name: workflowTitle,
|
||||
description: workflowDefinition?.description || ''
|
||||
});
|
||||
|
||||
if (success) {
|
||||
console.log('保存工作流成功:', { nodes, edges });
|
||||
}
|
||||
}, [getNodes, getEdges, saveWorkflow, currentWorkflowId, workflowTitle, workflowDefinition]);
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
navigate('/workflow2/definition');
|
||||
@ -180,22 +222,21 @@ const WorkflowDesignInner: React.FC = () => {
|
||||
zoomOut({ duration: 300 });
|
||||
}, [zoomOut]);
|
||||
|
||||
// 处理节点拖拽放置
|
||||
// 处理节点拖拽放置 - 使用官方推荐的screenToFlowPosition方法
|
||||
const handleDrop = useCallback((event: React.DragEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
const reactFlowBounds = reactFlowWrapper.current?.getBoundingClientRect();
|
||||
if (!reactFlowBounds) return;
|
||||
|
||||
const dragData = event.dataTransfer.getData('application/reactflow');
|
||||
if (!dragData) return;
|
||||
|
||||
try {
|
||||
const { nodeType, nodeDefinition }: DragNodeData = JSON.parse(dragData);
|
||||
|
||||
// 根据React Flow官方文档,screenToFlowPosition会自动处理所有边界计算
|
||||
// 不需要手动减去容器边界!
|
||||
const position = screenToFlowPosition({
|
||||
x: event.clientX - reactFlowBounds.left,
|
||||
y: event.clientY - reactFlowBounds.top,
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
|
||||
const newNode: FlowNode = {
|
||||
@ -220,16 +261,73 @@ const WorkflowDesignInner: React.FC = () => {
|
||||
}
|
||||
}, [screenToFlowPosition, setNodes]);
|
||||
|
||||
// 处理节点点击
|
||||
// 处理节点点击 - 单击选择,双击打开配置面板
|
||||
const handleNodeClick = useCallback((event: React.MouseEvent, node: FlowNode) => {
|
||||
console.log('节点点击:', node);
|
||||
message.info(`点击了节点: ${node.data.label}`);
|
||||
|
||||
// 清除边选择
|
||||
setSelectedEdge(null);
|
||||
|
||||
// 检查是否是双击
|
||||
if (event.detail === 2) {
|
||||
setSelectedNode(node);
|
||||
setConfigModalVisible(true);
|
||||
} else {
|
||||
setSelectedNode(node);
|
||||
message.info(`选择了节点: ${node.data.label}`);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 处理边点击
|
||||
const handleEdgeClick = useCallback((event: React.MouseEvent, edge: FlowEdge) => {
|
||||
console.log('边点击:', edge);
|
||||
message.info(`点击了连接: ${edge.data?.label || '连接'}`);
|
||||
|
||||
// 清除节点选择
|
||||
setSelectedNode(null);
|
||||
setSelectedEdge(edge);
|
||||
message.info(`选择了连接: ${edge.data?.label || '连接'}`);
|
||||
}, []);
|
||||
|
||||
// 处理节点配置更新
|
||||
const handleNodeConfigUpdate = useCallback((nodeId: string, updatedData: Partial<FlowNodeData>) => {
|
||||
setNodes((nodes) =>
|
||||
nodes.map((node) =>
|
||||
node.id === nodeId
|
||||
? {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
...updatedData,
|
||||
},
|
||||
}
|
||||
: node
|
||||
)
|
||||
);
|
||||
markUnsaved(); // 标记有未保存更改
|
||||
}, [setNodes, markUnsaved]);
|
||||
|
||||
// 处理边配置更新
|
||||
const handleEdgeConfigUpdate = useCallback((edgeId: string, updatedData: any) => {
|
||||
setEdges((edges) =>
|
||||
edges.map((edge) =>
|
||||
edge.id === edgeId
|
||||
? {
|
||||
...edge,
|
||||
data: {
|
||||
...edge.data,
|
||||
...updatedData,
|
||||
},
|
||||
}
|
||||
: edge
|
||||
)
|
||||
);
|
||||
markUnsaved(); // 标记有未保存更改
|
||||
}, [setEdges, markUnsaved]);
|
||||
|
||||
// 关闭配置模态框
|
||||
const handleCloseConfigModal = useCallback(() => {
|
||||
setConfigModalVisible(false);
|
||||
setSelectedNode(null);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@ -242,9 +340,10 @@ const WorkflowDesignInner: React.FC = () => {
|
||||
>
|
||||
{/* 工具栏 */}
|
||||
<WorkflowToolbar
|
||||
title={workflowTitle}
|
||||
title={`${workflowTitle}${hasUnsavedChanges ? ' *' : ''}`}
|
||||
onSave={handleSave}
|
||||
onBack={handleBack}
|
||||
saving={saving}
|
||||
onUndo={handleUndo}
|
||||
onRedo={handleRedo}
|
||||
onCopy={handleCopy}
|
||||
@ -277,7 +376,23 @@ const WorkflowDesignInner: React.FC = () => {
|
||||
className="workflow-canvas"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 属性面板 */}
|
||||
<PropertyPanel
|
||||
selectedNode={selectedNode}
|
||||
selectedEdge={selectedEdge}
|
||||
onNodeUpdate={handleNodeConfigUpdate}
|
||||
onEdgeUpdate={handleEdgeConfigUpdate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 节点配置弹窗 */}
|
||||
<NodeConfigModal
|
||||
visible={configModalVisible}
|
||||
node={selectedNode}
|
||||
onCancel={handleCloseConfigModal}
|
||||
onOk={handleNodeConfigUpdate}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -41,9 +41,14 @@ export interface FlowNodeData extends Record<string, unknown> {
|
||||
icon: string;
|
||||
color: string;
|
||||
// 节点配置数据
|
||||
configs?: Record<string, any>;
|
||||
inputMapping?: Record<string, any>;
|
||||
outputMapping?: Record<string, any>;
|
||||
configs?: {
|
||||
timeout?: number; // 超时时间(秒)
|
||||
retryCount?: number; // 重试次数
|
||||
priority?: string; // 优先级
|
||||
[key: string]: any; // 其他自定义配置
|
||||
};
|
||||
inputMapping?: Record<string, any>; // 输入参数映射
|
||||
outputMapping?: Record<string, any>; // 输出参数映射
|
||||
// 原始节点定义
|
||||
nodeDefinition?: NodeDefinitionData;
|
||||
}
|
||||
|
||||
@ -1,24 +1,369 @@
|
||||
import React from 'react';
|
||||
import { Card } from 'antd';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Table,
|
||||
Card,
|
||||
Button,
|
||||
Space,
|
||||
Tag,
|
||||
message,
|
||||
Modal,
|
||||
Row,
|
||||
Col,
|
||||
Statistic
|
||||
} from 'antd';
|
||||
import {
|
||||
PlayCircleOutlined,
|
||||
PauseCircleOutlined,
|
||||
StopOutlined,
|
||||
DeleteOutlined,
|
||||
ReloadOutlined,
|
||||
EyeOutlined,
|
||||
BarChartOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import dayjs from 'dayjs';
|
||||
import * as service from './service';
|
||||
import type {
|
||||
WorkflowInstance,
|
||||
WorkflowInstanceQuery,
|
||||
WorkflowInstanceStatus,
|
||||
InstanceStatistics
|
||||
} from './types';
|
||||
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
|
||||
|
||||
const { confirm } = Modal;
|
||||
|
||||
const WorkflowInstanceList: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [pageData, setPageData] = useState<{
|
||||
content: WorkflowInstance[];
|
||||
totalElements: number;
|
||||
size: number;
|
||||
number: number;
|
||||
} | null>(null);
|
||||
const [statistics, setStatistics] = useState<InstanceStatistics | null>(null);
|
||||
const [query, setQuery] = useState<WorkflowInstanceQuery>({
|
||||
pageNum: DEFAULT_CURRENT - 1,
|
||||
pageSize: DEFAULT_PAGE_SIZE
|
||||
});
|
||||
|
||||
// 加载数据
|
||||
const loadData = async (params: WorkflowInstanceQuery) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [instanceData, statsData] = await Promise.all([
|
||||
service.getInstances(params),
|
||||
service.getInstanceStatistics()
|
||||
]);
|
||||
setPageData(instanceData);
|
||||
setStatistics(statsData);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
message.error(error.message);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData(query);
|
||||
}, [query]);
|
||||
|
||||
// 状态标签渲染
|
||||
const renderStatus = (status: WorkflowInstanceStatus) => {
|
||||
const statusConfig = {
|
||||
RUNNING: { color: 'processing', text: '运行中' },
|
||||
COMPLETED: { color: 'success', text: '已完成' },
|
||||
FAILED: { color: 'error', text: '失败' },
|
||||
SUSPENDED: { color: 'warning', text: '已挂起' },
|
||||
TERMINATED: { color: 'default', text: '已终止' }
|
||||
};
|
||||
|
||||
const config = statusConfig[status] || { color: 'default', text: status };
|
||||
return <Tag color={config.color}>{config.text}</Tag>;
|
||||
};
|
||||
|
||||
// 操作处理
|
||||
const handleSuspend = async (instance: WorkflowInstance) => {
|
||||
confirm({
|
||||
title: '确认挂起',
|
||||
content: `确定要挂起实例 "${instance.workflowName}" 吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await service.suspendInstance(instance.id);
|
||||
message.success('实例已挂起');
|
||||
loadData(query);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
message.error(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleActivate = async (instance: WorkflowInstance) => {
|
||||
try {
|
||||
await service.activateInstance(instance.id);
|
||||
message.success('实例已激活');
|
||||
loadData(query);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
message.error(error.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleTerminate = async (instance: WorkflowInstance) => {
|
||||
confirm({
|
||||
title: '确认终止',
|
||||
content: `确定要终止实例 "${instance.workflowName}" 吗?此操作不可逆。`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await service.terminateInstance(instance.id);
|
||||
message.success('实例已终止');
|
||||
loadData(query);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
message.error(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = async (instance: WorkflowInstance) => {
|
||||
confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除实例 "${instance.workflowName}" 吗?此操作不可逆。`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await service.deleteInstance(instance.id);
|
||||
message.success('实例已删除');
|
||||
loadData(query);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
message.error(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 查看详情
|
||||
const handleViewDetail = (instance: WorkflowInstance) => {
|
||||
navigate(`/workflow2/instance/${instance.id}`);
|
||||
};
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{
|
||||
title: '实例ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '工作流名称',
|
||||
dataIndex: 'workflowName',
|
||||
key: 'workflowName',
|
||||
},
|
||||
{
|
||||
title: '业务键',
|
||||
dataIndex: 'businessKey',
|
||||
key: 'businessKey',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: renderStatus,
|
||||
},
|
||||
{
|
||||
title: '启动人',
|
||||
dataIndex: 'startUserName',
|
||||
key: 'startUserName',
|
||||
},
|
||||
{
|
||||
title: '开始时间',
|
||||
dataIndex: 'startTime',
|
||||
key: 'startTime',
|
||||
render: (time: string) => dayjs(time).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
title: '耗时',
|
||||
dataIndex: 'duration',
|
||||
key: 'duration',
|
||||
render: (duration: number) => {
|
||||
if (!duration) return '-';
|
||||
const hours = Math.floor(duration / 3600000);
|
||||
const minutes = Math.floor((duration % 3600000) / 60000);
|
||||
const seconds = Math.floor((duration % 60000) / 1000);
|
||||
return `${hours}h ${minutes}m ${seconds}s`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
width: 200,
|
||||
render: (_: any, record: WorkflowInstance) => (
|
||||
<Space size="small">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => handleViewDetail(record)}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
{record.status === 'RUNNING' && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<PauseCircleOutlined />}
|
||||
onClick={() => handleSuspend(record)}
|
||||
>
|
||||
挂起
|
||||
</Button>
|
||||
)}
|
||||
{record.status === 'SUSPENDED' && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<PlayCircleOutlined />}
|
||||
onClick={() => handleActivate(record)}
|
||||
>
|
||||
激活
|
||||
</Button>
|
||||
)}
|
||||
{(record.status === 'RUNNING' || record.status === 'SUSPENDED') && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
icon={<StopOutlined />}
|
||||
onClick={() => handleTerminate(record)}
|
||||
>
|
||||
终止
|
||||
</Button>
|
||||
)}
|
||||
{(record.status === 'COMPLETED' || record.status === 'FAILED' || record.status === 'TERMINATED') && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleDelete(record)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const WorkflowInstance: React.FC = () => {
|
||||
return (
|
||||
<Card title="工作流实例管理 (React Flow版本)">
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
color: '#6b7280',
|
||||
padding: '40px',
|
||||
fontSize: '16px'
|
||||
}}>
|
||||
🚧 工作流实例管理功能开发中...
|
||||
<br />
|
||||
<br />
|
||||
<span style={{ fontSize: '14px' }}>
|
||||
此模块将复用现有的实例管理API,提供React Flow版本的实例查看功能
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
<div>
|
||||
{/* 统计信息 */}
|
||||
{statistics && (
|
||||
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||||
<Col span={4}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="总实例数"
|
||||
value={statistics.total}
|
||||
prefix={<BarChartOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="运行中"
|
||||
value={statistics.running}
|
||||
valueStyle={{ color: '#1890ff' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="已完成"
|
||||
value={statistics.completed}
|
||||
valueStyle={{ color: '#52c41a' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="失败"
|
||||
value={statistics.failed}
|
||||
valueStyle={{ color: '#ff4d4f' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="今日启动"
|
||||
value={statistics.todayStarted}
|
||||
valueStyle={{ color: '#722ed1' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="平均耗时"
|
||||
value={Math.round(statistics.avgDuration / 1000)}
|
||||
suffix="秒"
|
||||
valueStyle={{ color: '#fa8c16' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{/* 主表格 */}
|
||||
<Card
|
||||
title="工作流实例列表"
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => loadData(query)}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={pageData?.content}
|
||||
loading={loading}
|
||||
rowKey="id"
|
||||
scroll={{ x: 1200 }}
|
||||
pagination={{
|
||||
current: (query.pageNum || 0) + 1,
|
||||
pageSize: query.pageSize,
|
||||
total: pageData?.totalElements || 0,
|
||||
onChange: (page, pageSize) => setQuery({
|
||||
...query,
|
||||
pageNum: page - 1,
|
||||
pageSize
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkflowInstance;
|
||||
export default WorkflowInstanceList;
|
||||
90
frontend/src/pages/Workflow2/Instance/service.ts
Normal file
90
frontend/src/pages/Workflow2/Instance/service.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import request from '@/utils/request';
|
||||
import type { Page } from '@/types/base';
|
||||
import type {
|
||||
WorkflowInstance,
|
||||
WorkflowInstanceQuery,
|
||||
TaskInstance,
|
||||
TaskQuery,
|
||||
InstanceStatistics
|
||||
} from './types';
|
||||
|
||||
// 获取工作流实例列表
|
||||
export async function getInstances(params: WorkflowInstanceQuery): Promise<Page<WorkflowInstance>> {
|
||||
return request.get('/api/v1/workflow/instances/page', { params });
|
||||
}
|
||||
|
||||
// 获取单个工作流实例详情
|
||||
export async function getInstanceDetail(id: number): Promise<WorkflowInstance> {
|
||||
return request.get(`/api/v1/workflow/instances/${id}`);
|
||||
}
|
||||
|
||||
// 启动工作流实例
|
||||
export async function startInstance(data: {
|
||||
workflowKey: string;
|
||||
businessKey?: string;
|
||||
variables?: Record<string, any>;
|
||||
}): Promise<WorkflowInstance> {
|
||||
return request.post('/api/v1/workflow/instances/start', data);
|
||||
}
|
||||
|
||||
// 终止工作流实例
|
||||
export async function terminateInstance(id: number, reason?: string): Promise<void> {
|
||||
return request.post(`/api/v1/workflow/instances/${id}/terminate`, { reason });
|
||||
}
|
||||
|
||||
// 挂起工作流实例
|
||||
export async function suspendInstance(id: number, reason?: string): Promise<void> {
|
||||
return request.post(`/api/v1/workflow/instances/${id}/suspend`, { reason });
|
||||
}
|
||||
|
||||
// 激活工作流实例
|
||||
export async function activateInstance(id: number): Promise<void> {
|
||||
return request.post(`/api/v1/workflow/instances/${id}/activate`);
|
||||
}
|
||||
|
||||
// 删除工作流实例
|
||||
export async function deleteInstance(id: number): Promise<void> {
|
||||
return request.delete(`/api/v1/workflow/instances/${id}`);
|
||||
}
|
||||
|
||||
// 获取工作流实例的任务列表
|
||||
export async function getInstanceTasks(instanceId: number): Promise<TaskInstance[]> {
|
||||
return request.get(`/api/v1/workflow/instances/${instanceId}/tasks`);
|
||||
}
|
||||
|
||||
// 获取任务列表
|
||||
export async function getTasks(params: TaskQuery): Promise<Page<TaskInstance>> {
|
||||
return request.get('/api/v1/workflow/tasks/page', { params });
|
||||
}
|
||||
|
||||
// 完成任务
|
||||
export async function completeTask(taskId: number, variables?: Record<string, any>): Promise<void> {
|
||||
return request.post(`/api/v1/workflow/tasks/${taskId}/complete`, { variables });
|
||||
}
|
||||
|
||||
// 委派任务
|
||||
export async function delegateTask(taskId: number, assignee: string): Promise<void> {
|
||||
return request.post(`/api/v1/workflow/tasks/${taskId}/delegate`, { assignee });
|
||||
}
|
||||
|
||||
// 认领任务
|
||||
export async function claimTask(taskId: number): Promise<void> {
|
||||
return request.post(`/api/v1/workflow/tasks/${taskId}/claim`);
|
||||
}
|
||||
|
||||
// 获取实例统计信息
|
||||
export async function getInstanceStatistics(): Promise<InstanceStatistics> {
|
||||
return request.get('/api/v1/workflow/instances/statistics');
|
||||
}
|
||||
|
||||
// 获取实例执行历史
|
||||
export async function getInstanceHistory(instanceId: number): Promise<any[]> {
|
||||
return request.get(`/api/v1/workflow/instances/${instanceId}/history`);
|
||||
}
|
||||
|
||||
// 获取实例流程图
|
||||
export async function getInstanceDiagram(instanceId: number): Promise<string> {
|
||||
return request.get(`/api/v1/workflow/instances/${instanceId}/diagram`, {
|
||||
responseType: 'text'
|
||||
});
|
||||
}
|
||||
88
frontend/src/pages/Workflow2/Instance/types.ts
Normal file
88
frontend/src/pages/Workflow2/Instance/types.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { BaseResponse, BaseQuery } from '@/types/base';
|
||||
|
||||
// 工作流实例状态枚举
|
||||
export enum WorkflowInstanceStatus {
|
||||
RUNNING = 'RUNNING',
|
||||
COMPLETED = 'COMPLETED',
|
||||
FAILED = 'FAILED',
|
||||
SUSPENDED = 'SUSPENDED',
|
||||
TERMINATED = 'TERMINATED'
|
||||
}
|
||||
|
||||
// 任务状态枚举
|
||||
export enum TaskStatus {
|
||||
PENDING = 'PENDING',
|
||||
RUNNING = 'RUNNING',
|
||||
COMPLETED = 'COMPLETED',
|
||||
FAILED = 'FAILED',
|
||||
SKIPPED = 'SKIPPED'
|
||||
}
|
||||
|
||||
// 工作流实例
|
||||
export interface WorkflowInstance extends BaseResponse {
|
||||
id: number;
|
||||
workflowKey: string;
|
||||
workflowName: string;
|
||||
workflowVersion: number;
|
||||
businessKey?: string;
|
||||
status: WorkflowInstanceStatus;
|
||||
startTime: string;
|
||||
endTime?: string;
|
||||
duration?: number;
|
||||
startUserId?: number;
|
||||
startUserName?: string;
|
||||
currentTasks: TaskInstance[];
|
||||
variables: Record<string, any>;
|
||||
processDefinitionId: string;
|
||||
}
|
||||
|
||||
// 任务实例
|
||||
export interface TaskInstance extends BaseResponse {
|
||||
id: number;
|
||||
taskName: string;
|
||||
taskType: string;
|
||||
assignee?: string;
|
||||
assigneeName?: string;
|
||||
status: TaskStatus;
|
||||
startTime: string;
|
||||
endTime?: string;
|
||||
duration?: number;
|
||||
workflowInstanceId: number;
|
||||
nodeId: string;
|
||||
description?: string;
|
||||
formKey?: string;
|
||||
priority: number;
|
||||
}
|
||||
|
||||
// 工作流实例查询条件
|
||||
export interface WorkflowInstanceQuery extends BaseQuery {
|
||||
workflowKey?: string;
|
||||
workflowName?: string;
|
||||
status?: WorkflowInstanceStatus;
|
||||
businessKey?: string;
|
||||
startUserId?: number;
|
||||
startTimeFrom?: string;
|
||||
startTimeTo?: string;
|
||||
}
|
||||
|
||||
// 任务查询条件
|
||||
export interface TaskQuery extends BaseQuery {
|
||||
taskName?: string;
|
||||
assignee?: string;
|
||||
status?: TaskStatus;
|
||||
workflowInstanceId?: number;
|
||||
taskType?: string;
|
||||
}
|
||||
|
||||
// 实例统计信息
|
||||
export interface InstanceStatistics {
|
||||
total: number;
|
||||
running: number;
|
||||
completed: number;
|
||||
failed: number;
|
||||
suspended: number;
|
||||
terminated: number;
|
||||
avgDuration: number;
|
||||
todayStarted: number;
|
||||
todayCompleted: number;
|
||||
}
|
||||
@ -1,24 +1,298 @@
|
||||
import React from 'react';
|
||||
import { Card } from 'antd';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Row,
|
||||
Col,
|
||||
Statistic,
|
||||
Progress,
|
||||
Table,
|
||||
Tag,
|
||||
Button,
|
||||
Space,
|
||||
message
|
||||
} from 'antd';
|
||||
import {
|
||||
SyncOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
PauseCircleOutlined,
|
||||
BarChartOutlined,
|
||||
LineChartOutlined,
|
||||
ReloadOutlined
|
||||
} from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import * as instanceService from '../Instance/service';
|
||||
import type {
|
||||
InstanceStatistics,
|
||||
WorkflowInstance,
|
||||
WorkflowInstanceStatus
|
||||
} from '../Instance/types';
|
||||
|
||||
const WorkflowMonitor: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [statistics, setStatistics] = useState<InstanceStatistics | null>(null);
|
||||
const [recentInstances, setRecentInstances] = useState<WorkflowInstance[]>([]);
|
||||
|
||||
// 加载监控数据
|
||||
const loadMonitorData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [statsData, instancesData] = await Promise.all([
|
||||
instanceService.getInstanceStatistics(),
|
||||
instanceService.getInstances({ pageNum: 0, pageSize: 10 })
|
||||
]);
|
||||
setStatistics(statsData);
|
||||
setRecentInstances(instancesData.content);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
message.error(error.message);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadMonitorData();
|
||||
// 定时刷新数据
|
||||
const interval = setInterval(loadMonitorData, 30000); // 30秒刷新一次
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// 状态渲染
|
||||
const renderStatus = (status: WorkflowInstanceStatus) => {
|
||||
const statusConfig = {
|
||||
RUNNING: { color: 'processing', text: '运行中', icon: <SyncOutlined spin /> },
|
||||
COMPLETED: { color: 'success', text: '已完成', icon: <CheckCircleOutlined /> },
|
||||
FAILED: { color: 'error', text: '失败', icon: <CloseCircleOutlined /> },
|
||||
SUSPENDED: { color: 'warning', text: '已挂起', icon: <PauseCircleOutlined /> },
|
||||
TERMINATED: { color: 'default', text: '已终止', icon: <CloseCircleOutlined /> }
|
||||
};
|
||||
|
||||
const config = statusConfig[status] || { color: 'default', text: status, icon: null };
|
||||
return (
|
||||
<Tag color={config.color} icon={config.icon}>
|
||||
{config.text}
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
|
||||
// 计算健康度
|
||||
const getHealthScore = () => {
|
||||
if (!statistics || statistics.total === 0) return 0;
|
||||
const successRate = (statistics.completed / statistics.total) * 100;
|
||||
return Math.round(successRate);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '实例ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '工作流名称',
|
||||
dataIndex: 'workflowName',
|
||||
key: 'workflowName',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: renderStatus,
|
||||
},
|
||||
{
|
||||
title: '开始时间',
|
||||
dataIndex: 'startTime',
|
||||
key: 'startTime',
|
||||
render: (time: string) => dayjs(time).format('MM-DD HH:mm'),
|
||||
},
|
||||
{
|
||||
title: '耗时',
|
||||
dataIndex: 'duration',
|
||||
key: 'duration',
|
||||
render: (duration: number) => {
|
||||
if (!duration) return '-';
|
||||
return `${Math.round(duration / 1000)}s`;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Card title="工作流监控 (React Flow版本)">
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
color: '#6b7280',
|
||||
padding: '40px',
|
||||
fontSize: '16px'
|
||||
}}>
|
||||
📊 工作流监控功能开发中...
|
||||
<br />
|
||||
<br />
|
||||
<span style={{ fontSize: '14px' }}>
|
||||
此模块将提供实时的工作流执行监控和统计分析功能
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
<div>
|
||||
{/* 顶部操作栏 */}
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<Row justify="space-between" align="middle">
|
||||
<Col>
|
||||
<Space>
|
||||
<h2 style={{ margin: 0 }}>工作流监控中心</h2>
|
||||
<Tag color="blue">实时监控</Tag>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col>
|
||||
<Space>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={loadMonitorData}
|
||||
loading={loading}
|
||||
>
|
||||
刷新数据
|
||||
</Button>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="总实例数"
|
||||
value={statistics?.total || 0}
|
||||
prefix={<BarChartOutlined />}
|
||||
loading={loading}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="今日启动"
|
||||
value={statistics?.todayStarted || 0}
|
||||
prefix={<LineChartOutlined />}
|
||||
valueStyle={{ color: '#1890ff' }}
|
||||
loading={loading}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="今日完成"
|
||||
value={statistics?.todayCompleted || 0}
|
||||
prefix={<CheckCircleOutlined />}
|
||||
valueStyle={{ color: '#52c41a' }}
|
||||
loading={loading}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{ fontSize: '14px', color: '#8c8c8c', marginBottom: '8px' }}>
|
||||
系统健康度
|
||||
</div>
|
||||
<Progress
|
||||
type="circle"
|
||||
percent={getHealthScore()}
|
||||
size={80}
|
||||
strokeColor={{
|
||||
'0%': '#108ee9',
|
||||
'100%': '#87d068',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 图表和列表 */}
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Card title="实例运行状态" loading={loading}>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Statistic
|
||||
title="运行中"
|
||||
value={statistics?.running || 0}
|
||||
valueStyle={{ color: '#1890ff' }}
|
||||
prefix={<SyncOutlined />}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Statistic
|
||||
title="已完成"
|
||||
value={statistics?.completed || 0}
|
||||
valueStyle={{ color: '#52c41a' }}
|
||||
prefix={<CheckCircleOutlined />}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16} style={{ marginTop: 24 }}>
|
||||
<Col span={12}>
|
||||
<Statistic
|
||||
title="失败"
|
||||
value={statistics?.failed || 0}
|
||||
valueStyle={{ color: '#ff4d4f' }}
|
||||
prefix={<CloseCircleOutlined />}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Statistic
|
||||
title="已挂起"
|
||||
value={statistics?.suspended || 0}
|
||||
valueStyle={{ color: '#faad14' }}
|
||||
prefix={<PauseCircleOutlined />}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card title="性能指标" loading={loading}>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<div style={{ textAlign: 'center', marginBottom: '16px' }}>
|
||||
<div style={{ fontSize: '16px', fontWeight: 'bold', color: '#1890ff' }}>
|
||||
平均执行时间
|
||||
</div>
|
||||
<div style={{ fontSize: '24px', color: '#fa8c16', marginTop: '8px' }}>
|
||||
{Math.round((statistics?.avgDuration || 0) / 1000)}秒
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{ fontSize: '14px' }}>成功率</div>
|
||||
<div style={{ fontSize: '20px', color: '#52c41a' }}>
|
||||
{getHealthScore()}%
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{ fontSize: '14px' }}>活跃实例</div>
|
||||
<div style={{ fontSize: '20px', color: '#1890ff' }}>
|
||||
{(statistics?.running || 0) + (statistics?.suspended || 0)}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 最近实例 */}
|
||||
<Card
|
||||
title="最近实例"
|
||||
style={{ marginTop: 16 }}
|
||||
loading={loading}
|
||||
>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={recentInstances}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
pagination={false}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkflowMonitor;
|
||||
export default WorkflowMonitor;
|
||||
@ -48,6 +48,8 @@ const External = lazy(() => import('../pages/Deploy/External'));
|
||||
// Workflow2 React Flow 版本
|
||||
const Workflow2DefinitionList = lazy(() => import('../pages/Workflow2/Definition'));
|
||||
const Workflow2Design = lazy(() => import('../pages/Workflow2/Design'));
|
||||
const Workflow2InstanceList = lazy(() => import('../pages/Workflow2/Instance'));
|
||||
const Workflow2Monitor = lazy(() => import('../pages/Workflow2/Monitor'));
|
||||
|
||||
// 创建路由
|
||||
const router = createBrowserRouter([
|
||||
@ -228,27 +230,43 @@ const router = createBrowserRouter([
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'workflow2',
|
||||
children: [
|
||||
{
|
||||
path: 'definition',
|
||||
element: (
|
||||
<Suspense fallback={<LoadingComponent/>}>
|
||||
<Workflow2DefinitionList/>
|
||||
</Suspense>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'design/:id',
|
||||
element: (
|
||||
<Suspense fallback={<LoadingComponent/>}>
|
||||
<Workflow2Design/>
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'workflow2',
|
||||
children: [
|
||||
{
|
||||
path: 'definition',
|
||||
element: (
|
||||
<Suspense fallback={<LoadingComponent/>}>
|
||||
<Workflow2DefinitionList/>
|
||||
</Suspense>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'design/:id',
|
||||
element: (
|
||||
<Suspense fallback={<LoadingComponent/>}>
|
||||
<Workflow2Design/>
|
||||
</Suspense>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'instance',
|
||||
element: (
|
||||
<Suspense fallback={<LoadingComponent/>}>
|
||||
<Workflow2InstanceList/>
|
||||
</Suspense>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'monitor',
|
||||
element: (
|
||||
<Suspense fallback={<LoadingComponent/>}>
|
||||
<Workflow2Monitor/>
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
element: <Navigate to="/dashboard"/>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user