diff --git a/frontend/package.json b/frontend/package.json index cd5b78f5..7b8a04e2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,9 +24,11 @@ "@radix-ui/react-navigation-menu": "^1.2.3", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-progress": "^1.1.1", + "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.1.4", "@radix-ui/react-separator": "^1.1.1", + "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-tabs": "^1.1.13", @@ -57,6 +59,7 @@ "react-hook-form": "^7.54.2", "react-redux": "^9.0.4", "react-router-dom": "^6.21.0", + "reactflow": "^11.11.4", "recharts": "^2.15.0", "rsuite": "^5.83.3", "uuid": "^13.0.0", diff --git a/frontend/src/components/ui/radio-group.tsx b/frontend/src/components/ui/radio-group.tsx new file mode 100644 index 00000000..0bdf6853 --- /dev/null +++ b/frontend/src/components/ui/radio-group.tsx @@ -0,0 +1,44 @@ +"use client" + +import * as React from "react" +import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" +import { Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const RadioGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ) +}) +RadioGroup.displayName = RadioGroupPrimitive.Root.displayName + +const RadioGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + + + + + ) +}) +RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName + +export { RadioGroup, RadioGroupItem } diff --git a/frontend/src/components/ui/slider.tsx b/frontend/src/components/ui/slider.tsx new file mode 100644 index 00000000..9398b331 --- /dev/null +++ b/frontend/src/components/ui/slider.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import * as SliderPrimitive from "@radix-ui/react-slider" + +import { cn } from "@/lib/utils" + +const Slider = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + + +)) +Slider.displayName = SliderPrimitive.Root.displayName + +export { Slider } diff --git a/frontend/src/pages/Workflow/Design/components/EdgeConfigModal.tsx b/frontend/src/pages/Workflow/Design/components/EdgeConfigModal.tsx index 1d1ab5d0..1f81b635 100644 --- a/frontend/src/pages/Workflow/Design/components/EdgeConfigModal.tsx +++ b/frontend/src/pages/Workflow/Design/components/EdgeConfigModal.tsx @@ -202,7 +202,7 @@ const EdgeConfigModal: React.FC = ({ currentNodeId={edge?.target || ''} variant="textarea" placeholder="请输入条件表达式,如:${Jenkins构建.buildStatus == 'SUCCESS'}" - rows={4} + rows={4} /> diff --git a/frontend/src/pages/Workflow/Design/nodes/JenkinsBuildNode.tsx b/frontend/src/pages/Workflow/Design/nodes/JenkinsBuildNode.tsx index 1924511c..805d21dd 100644 --- a/frontend/src/pages/Workflow/Design/nodes/JenkinsBuildNode.tsx +++ b/frontend/src/pages/Workflow/Design/nodes/JenkinsBuildNode.tsx @@ -63,7 +63,7 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = { { name: "buildNumber", title: "构建编号", - type: "number", + type: "number", description: "Jenkins构建的唯一编号", example: 123, required: true @@ -71,7 +71,7 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = { { name: "buildUrl", title: "构建URL", - type: "string", + type: "string", description: "Jenkins构建页面的访问地址", example: "http://jenkins.example.com/job/app/123/", required: true @@ -79,7 +79,7 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = { { name: "artifactUrl", title: "构建产物地址", - type: "string", + type: "string", description: "构建生成的jar/war包下载地址", example: "http://jenkins.example.com/job/app/123/artifact/target/app-1.0.0.jar", required: false @@ -87,7 +87,7 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = { { name: "gitCommitId", title: "Git提交ID", - type: "string", + type: "string", description: "本次构建使用的Git提交哈希值", example: "a3f5e8d2c4b1a5e9f2d3e7b8c9d1a2f3e4b5c6d7", required: true @@ -95,7 +95,7 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = { { name: "buildDuration", title: "构建时长", - type: "number", + type: "number", description: "构建执行的时长(秒)", example: 120, required: true diff --git a/frontend/src/pages/Workflow/Instance/components/DetailModal.tsx b/frontend/src/pages/Workflow/Instance/components/DetailModal.tsx index e6d0e003..396dae75 100644 --- a/frontend/src/pages/Workflow/Instance/components/DetailModal.tsx +++ b/frontend/src/pages/Workflow/Instance/components/DetailModal.tsx @@ -1,7 +1,15 @@ -import React from 'react'; -import { Modal, Steps, Card, Descriptions, Tag, Timeline } from 'antd'; +import React, { useMemo } from 'react'; +import { Dialog, DialogPortal, DialogOverlay, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Badge } from '@/components/ui/badge'; import { WorkflowHistoricalInstance, WorkflowInstanceStage } from '../types'; +import ReactFlow, { Background, Controls, MiniMap, Node, Edge } from 'reactflow'; import dayjs from 'dayjs'; +import 'reactflow/dist/style.css'; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { X, Clock, CheckCircle2, XCircle, AlertCircle } from "lucide-react"; +import { cn } from "@/lib/utils"; interface DetailModalProps { visible: boolean; @@ -12,104 +20,345 @@ interface DetailModalProps { const DetailModal: React.FC = ({ visible, onCancel, instanceData }) => { if (!instanceData) return null; - const getStatusTag = (status: string) => { - const statusMap: Record = { - COMPLETED: { color: 'success', text: '已完成' }, - RUNNING: { color: 'processing', text: '运行中' }, - FAILED: { color: 'error', text: '失败' }, - TERMINATED: { color: 'warning', text: '已终止' }, - NOT_STARTED: { color: 'default', text: '未执行' } + const getStatusBadge = (status: string) => { + const statusMap: Record = { + COMPLETED: { variant: 'success', text: '已完成' }, + RUNNING: { variant: 'default', text: '运行中' }, + FAILED: { variant: 'destructive', text: '失败' }, + TERMINATED: { variant: 'secondary', text: '已终止' }, + NOT_STARTED: { variant: 'outline', text: '未执行' } }; - const statusInfo = statusMap[status] || { color: 'default', text: status }; - return {statusInfo.text}; + const statusInfo = statusMap[status] || { variant: 'outline', text: status }; + return {statusInfo.text}; }; const getNodeTypeText = (nodeType: string) => { const nodeTypeMap: Record = { - startEvent: '开始节点', - endEvent: '结束节点', - serviceTask: '服务任务', - userTask: '用户任务', - scriptTask: '脚本任务' + START_EVENT: '开始节点', + END_EVENT: '结束节点', + SERVICE_TASK: '服务任务', + USER_TASK: '用户任务', + SCRIPT_TASK: '脚本任务', + NOTIFICATION: '通知任务', + JENKINS_BUILD: 'Jenkins构建', + APPROVAL: '审批任务', + StartEvent: '开始节点', + EndEvent: '结束节点', + ServiceTask: '服务任务', + UserTask: '用户任务', + ScriptTask: '脚本任务' }; return nodeTypeMap[nodeType] || nodeType; }; - const getStepStatus = (stage: WorkflowInstanceStage) => { - switch (stage.status) { + const getStatusIcon = (status: string) => { + switch (status) { case 'COMPLETED': - return 'finish'; + return ; case 'RUNNING': - return 'process'; + return ; case 'FAILED': - return 'error'; - case 'TERMINATED': - return 'wait'; + return ; default: - return 'wait'; + return ; } }; + // 构建节点状态映射 + const nodeStatusMap = useMemo(() => { + const map = new Map(); + instanceData.stages.forEach(stage => { + map.set(stage.nodeId, stage); + }); + return map; + }, [instanceData.stages]); + + // 获取节点状态 + const getNodeStatus = (nodeId: string): string => { + const stage = nodeStatusMap.get(nodeId); + return stage?.status || 'NOT_STARTED'; + }; + + // 获取节点颜色 + const getNodeColor = (status: string) => { + const colorMap: Record = { + COMPLETED: '#52c41a', + RUNNING: '#1890ff', + FAILED: '#ff4d4f', + TERMINATED: '#faad14', + NOT_STARTED: '#d9d9d9' + }; + return colorMap[status] || '#d9d9d9'; + }; + + // 转换为React Flow节点 + const flowNodes: Node[] = useMemo(() => { + if (!instanceData.graph?.nodes) return []; + + return instanceData.graph.nodes.map(node => { + const status = getNodeStatus(node.id); + const stage = nodeStatusMap.get(node.id); + + return { + id: node.id, + type: 'default', + position: { x: node.position.x + 400, y: node.position.y + 200 }, + data: { + label: ( +
+
+ {node.nodeName} +
+
+ {getNodeTypeText(node.nodeType)} +
+ {stage && ( +
+ {getStatusBadge(status)} +
+ )} +
+ ) + }, + style: { + background: getNodeColor(status), + color: status === 'NOT_STARTED' ? '#333' : '#fff', + border: `2px solid ${getNodeColor(status)}`, + borderRadius: '8px', + padding: '10px', + minWidth: '120px', + boxShadow: status === 'RUNNING' ? '0 0 10px rgba(24, 144, 255, 0.5)' : undefined + } + }; + }); + }, [instanceData.graph, nodeStatusMap]); + + // 判断连线是否已执行 + const isEdgeExecuted = (fromNodeId: string, _toNodeId: string): boolean => { + const fromStage = nodeStatusMap.get(fromNodeId); + + // 如果起始节点已完成,则连线已执行 + return fromStage?.status === 'COMPLETED' || fromStage?.status === 'RUNNING'; + }; + + // 转换为React Flow边 + const flowEdges: Edge[] = useMemo(() => { + if (!instanceData.graph?.edges) return []; + + return instanceData.graph.edges.map(edge => { + const executed = isEdgeExecuted(edge.from, edge.to); + + return { + id: edge.id, + source: edge.from, + target: edge.to, + label: edge.name || undefined, + type: 'smoothstep', + animated: isEdgeExecuted(edge.from, edge.to) && getNodeStatus(edge.to) === 'RUNNING', + style: { + stroke: executed ? '#52c41a' : '#d9d9d9', + strokeWidth: 2 + }, + labelStyle: { + fontSize: 10, + fill: '#666' + } + }; + }); + }, [instanceData.graph, nodeStatusMap]); + + // 描述信息组件 + const DescriptionItem = ({ label, value }: { label: string; value: React.ReactNode }) => ( +
+
{label}
+
{value}
+
+ ); + return ( - - - - {instanceData.businessKey} - {getStatusTag(instanceData.status)} - {dayjs(instanceData.startTime).format('YYYY-MM-DD HH:mm:ss')} - - {instanceData.endTime ? dayjs(instanceData.endTime).format('YYYY-MM-DD HH:mm:ss') : '暂无'} - - {instanceData.processInstanceId} - {instanceData.processDefinitionId} - - + !open && onCancel()}> + + + + + + Close + + + 流程执行详情 + + + + + 流程图 + 执行时间线 + 详细信息 + - - ({ - title: stage.nodeName, - description: ( -
-
{getNodeTypeText(stage.nodeType)}
-
{getStatusTag(stage.status)}
-
- ), - status: getStepStatus(stage) - }))} - /> -
- - - ({ - color: stage.status === 'COMPLETED' ? 'green' : - stage.status === 'FAILED' ? 'red' : - stage.status === 'RUNNING' ? 'blue' : 'gray', - children: ( -
-
{stage.nodeName}
- {stage.id && ( -
- {dayjs(stage.startTime).format('YYYY-MM-DD HH:mm:ss')} - {stage.endTime && ` - ${dayjs(stage.endTime).format('YYYY-MM-DD HH:mm:ss')}`} + {/* 流程图标签页 */} + + + +
+ + + +
- )} -
{getStatusTag(stage.status)}
-
- ) - }))} - /> - - + + + + {instanceData.graph ? ( + + + 流程执行图 + + +
+ + + + { + const status = getNodeStatus(node.id); + return getNodeColor(status); + }} + /> + +
+
+
+
+ 已完成 +
+
+
+ 运行中 +
+
+
+ 失败 +
+
+
+ 未执行 +
+
+ + + ) : ( + + +
+ 暂无流程图数据 +
+
+
+ )} + + + {/* 执行时间线标签页 */} + + + +
+ {instanceData.stages.map((stage, index) => ( +
+ {/* 时间线图标 */} +
+
+ {getStatusIcon(stage.status)} +
+ {index < instanceData.stages.length - 1 && ( +
+ )} +
+ + {/* 时间线内容 */} +
+
+ {stage.nodeName} + + {getNodeTypeText(stage.nodeType)} + +
+
+ {stage.startTime && dayjs(stage.startTime).format('YYYY-MM-DD HH:mm:ss')} + {stage.endTime && ` → ${dayjs(stage.endTime).format('HH:mm:ss')}`} + {stage.startTime && stage.endTime && ( + + (耗时: {dayjs(stage.endTime).diff(dayjs(stage.startTime), 'second')}秒) + + )} +
+
{getStatusBadge(stage.status)}
+
+
+ ))} +
+ + + + + {/* 详细信息标签页 */} + + + +
+ + + + + + + {instanceData.startTime && instanceData.endTime && ( + + )} + +
+
+
+
+ + + +
); }; diff --git a/frontend/src/pages/Workflow/Instance/components/HistoryModal.tsx b/frontend/src/pages/Workflow/Instance/components/HistoryModal.tsx index 75181a9d..9b976395 100644 --- a/frontend/src/pages/Workflow/Instance/components/HistoryModal.tsx +++ b/frontend/src/pages/Workflow/Instance/components/HistoryModal.tsx @@ -1,6 +1,9 @@ import React, { useState, useEffect } from 'react'; -import { Modal, Table, Tag, Button } from 'antd'; -import type { ColumnsType } from 'antd/es/table'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { DataTablePagination } from '@/components/ui/pagination'; import { WorkflowHistoricalInstance } from '../types'; import { getHistoricalInstances } from '../service'; import DetailModal from './DetailModal'; @@ -54,86 +57,89 @@ const HistoryModal: React.FC = ({ visible, onCancel, workflow setDetailVisible(true); }; - const getStatusTag = (status: string) => { - const statusMap: Record = { - COMPLETED: { color: 'success', text: '已完成' }, - RUNNING: { color: 'processing', text: '运行中' }, - FAILED: { color: 'error', text: '失败' }, - TERMINATED: { color: 'warning', text: '已终止' } + const getStatusBadge = (status: string) => { + const statusMap: Record = { + COMPLETED: { variant: 'success', text: '已完成' }, + RUNNING: { variant: 'default', text: '运行中' }, + FAILED: { variant: 'destructive', text: '失败' }, + TERMINATED: { variant: 'secondary', text: '已终止' } }; - const statusInfo = statusMap[status] || { color: 'default', text: status }; - return {statusInfo.text}; + const statusInfo = statusMap[status] || { variant: 'outline', text: status }; + return {statusInfo.text}; }; - const columns: ColumnsType = [ - { - title: '业务标识', - dataIndex: 'businessKey', - key: 'businessKey', - width: 200, - }, - { - title: '状态', - dataIndex: 'status', - key: 'status', - width: 100, - render: status => getStatusTag(status) - }, - { - title: '开始时间', - dataIndex: 'startTime', - key: 'startTime', - width: 180, - }, - { - title: '结束时间', - dataIndex: 'endTime', - key: 'endTime', - width: 180, - render: time => time || '暂无' - }, - { - title: '操作', - key: 'action', - fixed: 'right', - width: 100, - render: (_, record) => ( - - ), - }, - ]; + const pageCount = Math.ceil(total / query.pageSize); return ( <> - - setQuery(prev => ({ - ...prev, - pageNum: page - 1, - pageSize - })), - showSizeChanger: true, - showQuickJumper: true, - }} - /> - + !open && onCancel()}> + + + 历史执行记录 + + +
+ {loading ? ( +
+
加载中...
+
+ ) : data.length === 0 ? ( +
+
暂无数据
+
+ ) : ( + <> +
+
+ + + 业务标识 + 状态 + 开始时间 + 结束时间 + 操作 + + + + {data.map((record) => ( + + {record.businessKey} + {getStatusBadge(record.status)} + {record.startTime} + {record.endTime || '暂无'} + + + + + ))} + +
+ + + {/* 分页器 */} + {pageCount > 1 && ( + setQuery(prev => ({ + ...prev, + pageNum: page - 1 + }))} + /> + )} + + )} + + + + {selectedInstance && (