1
This commit is contained in:
parent
3003bb6abe
commit
0ca4aa58b7
@ -24,9 +24,11 @@
|
|||||||
"@radix-ui/react-navigation-menu": "^1.2.3",
|
"@radix-ui/react-navigation-menu": "^1.2.3",
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
"@radix-ui/react-progress": "^1.1.1",
|
"@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-scroll-area": "^1.2.10",
|
||||||
"@radix-ui/react-select": "^2.1.4",
|
"@radix-ui/react-select": "^2.1.4",
|
||||||
"@radix-ui/react-separator": "^1.1.1",
|
"@radix-ui/react-separator": "^1.1.1",
|
||||||
|
"@radix-ui/react-slider": "^1.3.6",
|
||||||
"@radix-ui/react-slot": "^1.1.1",
|
"@radix-ui/react-slot": "^1.1.1",
|
||||||
"@radix-ui/react-switch": "^1.1.2",
|
"@radix-ui/react-switch": "^1.1.2",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
@ -57,6 +59,7 @@
|
|||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
"react-redux": "^9.0.4",
|
"react-redux": "^9.0.4",
|
||||||
"react-router-dom": "^6.21.0",
|
"react-router-dom": "^6.21.0",
|
||||||
|
"reactflow": "^11.11.4",
|
||||||
"recharts": "^2.15.0",
|
"recharts": "^2.15.0",
|
||||||
"rsuite": "^5.83.3",
|
"rsuite": "^5.83.3",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
|
|||||||
44
frontend/src/components/ui/radio-group.tsx
Normal file
44
frontend/src/components/ui/radio-group.tsx
Normal file
@ -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<typeof RadioGroupPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Root
|
||||||
|
className={cn("grid gap-2", className)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
|
||||||
|
|
||||||
|
const RadioGroupItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
||||||
|
<Circle className="h-3.5 w-3.5 fill-primary" />
|
||||||
|
</RadioGroupPrimitive.Indicator>
|
||||||
|
</RadioGroupPrimitive.Item>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
|
||||||
|
|
||||||
|
export { RadioGroup, RadioGroupItem }
|
||||||
26
frontend/src/components/ui/slider.tsx
Normal file
26
frontend/src/components/ui/slider.tsx
Normal file
@ -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<typeof SliderPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SliderPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full touch-none select-none items-center",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20">
|
||||||
|
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
||||||
|
</SliderPrimitive.Track>
|
||||||
|
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
|
||||||
|
</SliderPrimitive.Root>
|
||||||
|
))
|
||||||
|
Slider.displayName = SliderPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Slider }
|
||||||
@ -202,7 +202,7 @@ const EdgeConfigModal: React.FC<EdgeConfigModalProps> = ({
|
|||||||
currentNodeId={edge?.target || ''}
|
currentNodeId={edge?.target || ''}
|
||||||
variant="textarea"
|
variant="textarea"
|
||||||
placeholder="请输入条件表达式,如:${Jenkins构建.buildStatus == 'SUCCESS'}"
|
placeholder="请输入条件表达式,如:${Jenkins构建.buildStatus == 'SUCCESS'}"
|
||||||
rows={4}
|
rows={4}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
|
|||||||
@ -63,7 +63,7 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = {
|
|||||||
{
|
{
|
||||||
name: "buildNumber",
|
name: "buildNumber",
|
||||||
title: "构建编号",
|
title: "构建编号",
|
||||||
type: "number",
|
type: "number",
|
||||||
description: "Jenkins构建的唯一编号",
|
description: "Jenkins构建的唯一编号",
|
||||||
example: 123,
|
example: 123,
|
||||||
required: true
|
required: true
|
||||||
@ -71,7 +71,7 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = {
|
|||||||
{
|
{
|
||||||
name: "buildUrl",
|
name: "buildUrl",
|
||||||
title: "构建URL",
|
title: "构建URL",
|
||||||
type: "string",
|
type: "string",
|
||||||
description: "Jenkins构建页面的访问地址",
|
description: "Jenkins构建页面的访问地址",
|
||||||
example: "http://jenkins.example.com/job/app/123/",
|
example: "http://jenkins.example.com/job/app/123/",
|
||||||
required: true
|
required: true
|
||||||
@ -79,7 +79,7 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = {
|
|||||||
{
|
{
|
||||||
name: "artifactUrl",
|
name: "artifactUrl",
|
||||||
title: "构建产物地址",
|
title: "构建产物地址",
|
||||||
type: "string",
|
type: "string",
|
||||||
description: "构建生成的jar/war包下载地址",
|
description: "构建生成的jar/war包下载地址",
|
||||||
example: "http://jenkins.example.com/job/app/123/artifact/target/app-1.0.0.jar",
|
example: "http://jenkins.example.com/job/app/123/artifact/target/app-1.0.0.jar",
|
||||||
required: false
|
required: false
|
||||||
@ -87,7 +87,7 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = {
|
|||||||
{
|
{
|
||||||
name: "gitCommitId",
|
name: "gitCommitId",
|
||||||
title: "Git提交ID",
|
title: "Git提交ID",
|
||||||
type: "string",
|
type: "string",
|
||||||
description: "本次构建使用的Git提交哈希值",
|
description: "本次构建使用的Git提交哈希值",
|
||||||
example: "a3f5e8d2c4b1a5e9f2d3e7b8c9d1a2f3e4b5c6d7",
|
example: "a3f5e8d2c4b1a5e9f2d3e7b8c9d1a2f3e4b5c6d7",
|
||||||
required: true
|
required: true
|
||||||
@ -95,7 +95,7 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = {
|
|||||||
{
|
{
|
||||||
name: "buildDuration",
|
name: "buildDuration",
|
||||||
title: "构建时长",
|
title: "构建时长",
|
||||||
type: "number",
|
type: "number",
|
||||||
description: "构建执行的时长(秒)",
|
description: "构建执行的时长(秒)",
|
||||||
example: 120,
|
example: 120,
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@ -1,7 +1,15 @@
|
|||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { Modal, Steps, Card, Descriptions, Tag, Timeline } from 'antd';
|
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 { WorkflowHistoricalInstance, WorkflowInstanceStage } from '../types';
|
||||||
|
import ReactFlow, { Background, Controls, MiniMap, Node, Edge } from 'reactflow';
|
||||||
import dayjs from 'dayjs';
|
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 {
|
interface DetailModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@ -12,104 +20,345 @@ interface DetailModalProps {
|
|||||||
const DetailModal: React.FC<DetailModalProps> = ({ visible, onCancel, instanceData }) => {
|
const DetailModal: React.FC<DetailModalProps> = ({ visible, onCancel, instanceData }) => {
|
||||||
if (!instanceData) return null;
|
if (!instanceData) return null;
|
||||||
|
|
||||||
const getStatusTag = (status: string) => {
|
const getStatusBadge = (status: string) => {
|
||||||
const statusMap: Record<string, { color: string; text: string }> = {
|
const statusMap: Record<string, { variant: 'default' | 'secondary' | 'destructive' | 'outline' | 'success'; text: string }> = {
|
||||||
COMPLETED: { color: 'success', text: '已完成' },
|
COMPLETED: { variant: 'success', text: '已完成' },
|
||||||
RUNNING: { color: 'processing', text: '运行中' },
|
RUNNING: { variant: 'default', text: '运行中' },
|
||||||
FAILED: { color: 'error', text: '失败' },
|
FAILED: { variant: 'destructive', text: '失败' },
|
||||||
TERMINATED: { color: 'warning', text: '已终止' },
|
TERMINATED: { variant: 'secondary', text: '已终止' },
|
||||||
NOT_STARTED: { color: 'default', text: '未执行' }
|
NOT_STARTED: { variant: 'outline', text: '未执行' }
|
||||||
};
|
};
|
||||||
const statusInfo = statusMap[status] || { color: 'default', text: status };
|
const statusInfo = statusMap[status] || { variant: 'outline', text: status };
|
||||||
return <Tag color={statusInfo.color}>{statusInfo.text}</Tag>;
|
return <Badge variant={statusInfo.variant}>{statusInfo.text}</Badge>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getNodeTypeText = (nodeType: string) => {
|
const getNodeTypeText = (nodeType: string) => {
|
||||||
const nodeTypeMap: Record<string, string> = {
|
const nodeTypeMap: Record<string, string> = {
|
||||||
startEvent: '开始节点',
|
START_EVENT: '开始节点',
|
||||||
endEvent: '结束节点',
|
END_EVENT: '结束节点',
|
||||||
serviceTask: '服务任务',
|
SERVICE_TASK: '服务任务',
|
||||||
userTask: '用户任务',
|
USER_TASK: '用户任务',
|
||||||
scriptTask: '脚本任务'
|
SCRIPT_TASK: '脚本任务',
|
||||||
|
NOTIFICATION: '通知任务',
|
||||||
|
JENKINS_BUILD: 'Jenkins构建',
|
||||||
|
APPROVAL: '审批任务',
|
||||||
|
StartEvent: '开始节点',
|
||||||
|
EndEvent: '结束节点',
|
||||||
|
ServiceTask: '服务任务',
|
||||||
|
UserTask: '用户任务',
|
||||||
|
ScriptTask: '脚本任务'
|
||||||
};
|
};
|
||||||
return nodeTypeMap[nodeType] || nodeType;
|
return nodeTypeMap[nodeType] || nodeType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStepStatus = (stage: WorkflowInstanceStage) => {
|
const getStatusIcon = (status: string) => {
|
||||||
switch (stage.status) {
|
switch (status) {
|
||||||
case 'COMPLETED':
|
case 'COMPLETED':
|
||||||
return 'finish';
|
return <CheckCircle2 className="h-4 w-4 text-green-500" />;
|
||||||
case 'RUNNING':
|
case 'RUNNING':
|
||||||
return 'process';
|
return <Clock className="h-4 w-4 text-blue-500 animate-pulse" />;
|
||||||
case 'FAILED':
|
case 'FAILED':
|
||||||
return 'error';
|
return <XCircle className="h-4 w-4 text-red-500" />;
|
||||||
case 'TERMINATED':
|
|
||||||
return 'wait';
|
|
||||||
default:
|
default:
|
||||||
return 'wait';
|
return <AlertCircle className="h-4 w-4 text-gray-400" />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 构建节点状态映射
|
||||||
|
const nodeStatusMap = useMemo(() => {
|
||||||
|
const map = new Map<string, WorkflowInstanceStage>();
|
||||||
|
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<string, string> = {
|
||||||
|
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: (
|
||||||
|
<div style={{ textAlign: 'center', minWidth: '80px' }}>
|
||||||
|
<div style={{
|
||||||
|
fontSize: '12px',
|
||||||
|
fontWeight: 500,
|
||||||
|
marginBottom: '4px'
|
||||||
|
}}>
|
||||||
|
{node.nodeName}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '10px', color: '#666' }}>
|
||||||
|
{getNodeTypeText(node.nodeType)}
|
||||||
|
</div>
|
||||||
|
{stage && (
|
||||||
|
<div style={{ fontSize: '10px', marginTop: '4px' }}>
|
||||||
|
{getStatusBadge(status)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
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 }) => (
|
||||||
|
<div className="flex items-start py-2 border-b last:border-b-0">
|
||||||
|
<div className="w-32 text-sm font-medium text-muted-foreground flex-shrink-0">{label}</div>
|
||||||
|
<div className="flex-1 text-sm">{value}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Dialog open={visible} onOpenChange={(open) => !open && onCancel()}>
|
||||||
title="流程执行详情"
|
<DialogPortal>
|
||||||
open={visible}
|
<DialogOverlay className="z-[60]" />
|
||||||
onCancel={onCancel}
|
<DialogPrimitive.Content
|
||||||
width={1200}
|
className={cn(
|
||||||
footer={null}
|
"fixed left-[50%] top-[50%] z-[60] grid w-full max-w-7xl translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg max-h-[90vh] overflow-y-auto"
|
||||||
>
|
)}
|
||||||
<Card className="mb-4">
|
>
|
||||||
<Descriptions title="基本信息" bordered column={2}>
|
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||||
<Descriptions.Item label="业务标识">{instanceData.businessKey}</Descriptions.Item>
|
<X className="h-4 w-4" />
|
||||||
<Descriptions.Item label="状态">{getStatusTag(instanceData.status)}</Descriptions.Item>
|
<span className="sr-only">Close</span>
|
||||||
<Descriptions.Item label="开始时间">{dayjs(instanceData.startTime).format('YYYY-MM-DD HH:mm:ss')}</Descriptions.Item>
|
</DialogPrimitive.Close>
|
||||||
<Descriptions.Item label="结束时间">
|
<DialogHeader>
|
||||||
{instanceData.endTime ? dayjs(instanceData.endTime).format('YYYY-MM-DD HH:mm:ss') : '暂无'}
|
<DialogTitle>流程执行详情</DialogTitle>
|
||||||
</Descriptions.Item>
|
</DialogHeader>
|
||||||
<Descriptions.Item label="流程实例ID">{instanceData.processInstanceId}</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="流程定义ID">{instanceData.processDefinitionId}</Descriptions.Item>
|
<Tabs defaultValue="graph" className="w-full">
|
||||||
</Descriptions>
|
<TabsList className="grid w-full grid-cols-3">
|
||||||
</Card>
|
<TabsTrigger value="graph">流程图</TabsTrigger>
|
||||||
|
<TabsTrigger value="timeline">执行时间线</TabsTrigger>
|
||||||
|
<TabsTrigger value="info">详细信息</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
<Card title="执行阶段" className="mb-4">
|
{/* 流程图标签页 */}
|
||||||
<Steps
|
<TabsContent value="graph" className="space-y-4">
|
||||||
progressDot
|
<Card>
|
||||||
current={instanceData.stages.length}
|
<CardContent className="pt-6">
|
||||||
items={instanceData.stages.map((stage) => ({
|
<div className="grid grid-cols-2 gap-4">
|
||||||
title: stage.nodeName,
|
<DescriptionItem label="业务标识" value={instanceData.businessKey} />
|
||||||
description: (
|
<DescriptionItem label="状态" value={getStatusBadge(instanceData.status)} />
|
||||||
<div className="text-xs">
|
<DescriptionItem
|
||||||
<div>{getNodeTypeText(stage.nodeType)}</div>
|
label="开始时间"
|
||||||
<div>{getStatusTag(stage.status)}</div>
|
value={dayjs(instanceData.startTime).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
</div>
|
/>
|
||||||
),
|
<DescriptionItem
|
||||||
status: getStepStatus(stage)
|
label="结束时间"
|
||||||
}))}
|
value={instanceData.endTime ? dayjs(instanceData.endTime).format('YYYY-MM-DD HH:mm:ss') : '暂无'}
|
||||||
/>
|
/>
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card title="详细时间线">
|
|
||||||
<Timeline
|
|
||||||
items={instanceData.stages.map((stage) => ({
|
|
||||||
color: stage.status === 'COMPLETED' ? 'green' :
|
|
||||||
stage.status === 'FAILED' ? 'red' :
|
|
||||||
stage.status === 'RUNNING' ? 'blue' : 'gray',
|
|
||||||
children: (
|
|
||||||
<div>
|
|
||||||
<div className="font-medium">{stage.nodeName}</div>
|
|
||||||
{stage.id && (
|
|
||||||
<div className="text-gray-500 text-sm">
|
|
||||||
{dayjs(stage.startTime).format('YYYY-MM-DD HH:mm:ss')}
|
|
||||||
{stage.endTime && ` - ${dayjs(stage.endTime).format('YYYY-MM-DD HH:mm:ss')}`}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</CardContent>
|
||||||
<div>{getStatusTag(stage.status)}</div>
|
</Card>
|
||||||
</div>
|
|
||||||
)
|
{instanceData.graph ? (
|
||||||
}))}
|
<Card>
|
||||||
/>
|
<CardHeader>
|
||||||
</Card>
|
<CardTitle>流程执行图</CardTitle>
|
||||||
</Modal>
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div style={{ height: '400px', border: '1px solid hsl(var(--border))', borderRadius: '8px' }}>
|
||||||
|
<ReactFlow
|
||||||
|
nodes={flowNodes}
|
||||||
|
edges={flowEdges}
|
||||||
|
fitView
|
||||||
|
minZoom={0.5}
|
||||||
|
maxZoom={1.5}
|
||||||
|
defaultViewport={{ x: 0, y: 0, zoom: 0.8 }}
|
||||||
|
>
|
||||||
|
<Background />
|
||||||
|
<Controls />
|
||||||
|
<MiniMap
|
||||||
|
nodeColor={(node: Node) => {
|
||||||
|
const status = getNodeStatus(node.id);
|
||||||
|
return getNodeColor(status);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ReactFlow>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex gap-4 justify-center text-sm">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-4 h-4 bg-green-500 rounded" />
|
||||||
|
<span>已完成</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-4 h-4 bg-blue-500 rounded" />
|
||||||
|
<span>运行中</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-4 h-4 bg-red-500 rounded" />
|
||||||
|
<span>失败</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-4 h-4 bg-gray-300 rounded" />
|
||||||
|
<span>未执行</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="text-center py-10 text-muted-foreground">
|
||||||
|
暂无流程图数据
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* 执行时间线标签页 */}
|
||||||
|
<TabsContent value="timeline">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="relative space-y-4">
|
||||||
|
{instanceData.stages.map((stage, index) => (
|
||||||
|
<div key={stage.id || index} className="flex gap-4">
|
||||||
|
{/* 时间线图标 */}
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div className="flex items-center justify-center w-8 h-8 rounded-full border-2 bg-background z-10"
|
||||||
|
style={{
|
||||||
|
borderColor: stage.status === 'COMPLETED' ? '#52c41a' :
|
||||||
|
stage.status === 'FAILED' ? '#ff4d4f' :
|
||||||
|
stage.status === 'RUNNING' ? '#1890ff' : '#d9d9d9'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getStatusIcon(stage.status)}
|
||||||
|
</div>
|
||||||
|
{index < instanceData.stages.length - 1 && (
|
||||||
|
<div className="w-0.5 h-full bg-border mt-1" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 时间线内容 */}
|
||||||
|
<div className="flex-1 pb-8">
|
||||||
|
<div className="font-medium mb-1 flex items-center gap-2">
|
||||||
|
{stage.nodeName}
|
||||||
|
<span className="text-xs text-muted-foreground font-normal">
|
||||||
|
{getNodeTypeText(stage.nodeType)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-muted-foreground mb-2">
|
||||||
|
{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 && (
|
||||||
|
<span className="ml-2">
|
||||||
|
(耗时: {dayjs(stage.endTime).diff(dayjs(stage.startTime), 'second')}秒)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>{getStatusBadge(stage.status)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* 详细信息标签页 */}
|
||||||
|
<TabsContent value="info">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="space-y-0">
|
||||||
|
<DescriptionItem label="流程实例ID" value={instanceData.processInstanceId} />
|
||||||
|
<DescriptionItem label="流程定义ID" value={instanceData.processDefinitionId} />
|
||||||
|
<DescriptionItem label="业务标识" value={instanceData.businessKey} />
|
||||||
|
<DescriptionItem label="状态" value={getStatusBadge(instanceData.status)} />
|
||||||
|
<DescriptionItem
|
||||||
|
label="开始时间"
|
||||||
|
value={dayjs(instanceData.startTime).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
|
/>
|
||||||
|
<DescriptionItem
|
||||||
|
label="结束时间"
|
||||||
|
value={instanceData.endTime ? dayjs(instanceData.endTime).format('YYYY-MM-DD HH:mm:ss') : '暂无'}
|
||||||
|
/>
|
||||||
|
{instanceData.startTime && instanceData.endTime && (
|
||||||
|
<DescriptionItem
|
||||||
|
label="总耗时"
|
||||||
|
value={`${dayjs(instanceData.endTime).diff(dayjs(instanceData.startTime), 'second')}秒`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<DescriptionItem label="执行节点数" value={instanceData.stages.length} />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Modal, Table, Tag, Button } from 'antd';
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
import type { ColumnsType } from 'antd/es/table';
|
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 { WorkflowHistoricalInstance } from '../types';
|
||||||
import { getHistoricalInstances } from '../service';
|
import { getHistoricalInstances } from '../service';
|
||||||
import DetailModal from './DetailModal';
|
import DetailModal from './DetailModal';
|
||||||
@ -54,86 +57,89 @@ const HistoryModal: React.FC<HistoryModalProps> = ({ visible, onCancel, workflow
|
|||||||
setDetailVisible(true);
|
setDetailVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusTag = (status: string) => {
|
const getStatusBadge = (status: string) => {
|
||||||
const statusMap: Record<string, { color: string; text: string }> = {
|
const statusMap: Record<string, { variant: 'default' | 'secondary' | 'destructive' | 'outline' | 'success'; text: string }> = {
|
||||||
COMPLETED: { color: 'success', text: '已完成' },
|
COMPLETED: { variant: 'success', text: '已完成' },
|
||||||
RUNNING: { color: 'processing', text: '运行中' },
|
RUNNING: { variant: 'default', text: '运行中' },
|
||||||
FAILED: { color: 'error', text: '失败' },
|
FAILED: { variant: 'destructive', text: '失败' },
|
||||||
TERMINATED: { color: 'warning', text: '已终止' }
|
TERMINATED: { variant: 'secondary', text: '已终止' }
|
||||||
};
|
};
|
||||||
const statusInfo = statusMap[status] || { color: 'default', text: status };
|
const statusInfo = statusMap[status] || { variant: 'outline', text: status };
|
||||||
return <Tag color={statusInfo.color}>{statusInfo.text}</Tag>;
|
return <Badge variant={statusInfo.variant}>{statusInfo.text}</Badge>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns: ColumnsType<WorkflowHistoricalInstance> = [
|
const pageCount = Math.ceil(total / query.pageSize);
|
||||||
{
|
|
||||||
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) => (
|
|
||||||
<Button type="link" onClick={() => handleViewDetail(record)}>
|
|
||||||
详情
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Dialog open={visible} onOpenChange={(open) => !open && onCancel()}>
|
||||||
title="历史执行记录"
|
<DialogContent className="max-w-6xl max-h-[85vh] overflow-y-auto">
|
||||||
open={visible}
|
<DialogHeader>
|
||||||
onCancel={onCancel}
|
<DialogTitle>历史执行记录</DialogTitle>
|
||||||
width={1000}
|
</DialogHeader>
|
||||||
footer={null}
|
|
||||||
>
|
<div className="mt-4">
|
||||||
<Table
|
{loading ? (
|
||||||
columns={columns}
|
<div className="flex justify-center items-center py-8">
|
||||||
dataSource={data}
|
<div className="text-muted-foreground">加载中...</div>
|
||||||
loading={loading}
|
</div>
|
||||||
rowKey="id"
|
) : data.length === 0 ? (
|
||||||
scroll={{ x: 800 }}
|
<div className="flex justify-center items-center py-8">
|
||||||
pagination={{
|
<div className="text-muted-foreground">暂无数据</div>
|
||||||
current: query.pageNum + 1,
|
</div>
|
||||||
pageSize: query.pageSize,
|
) : (
|
||||||
total: total,
|
<>
|
||||||
onChange: (page, pageSize) => setQuery(prev => ({
|
<div className="border rounded-lg">
|
||||||
...prev,
|
<Table>
|
||||||
pageNum: page - 1,
|
<TableHeader>
|
||||||
pageSize
|
<TableRow>
|
||||||
})),
|
<TableHead className="w-[200px]">业务标识</TableHead>
|
||||||
showSizeChanger: true,
|
<TableHead className="w-[100px]">状态</TableHead>
|
||||||
showQuickJumper: true,
|
<TableHead className="w-[180px]">开始时间</TableHead>
|
||||||
}}
|
<TableHead className="w-[180px]">结束时间</TableHead>
|
||||||
/>
|
<TableHead className="w-[100px] text-right">操作</TableHead>
|
||||||
</Modal>
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{data.map((record) => (
|
||||||
|
<TableRow key={record.id}>
|
||||||
|
<TableCell className="font-medium">{record.businessKey}</TableCell>
|
||||||
|
<TableCell>{getStatusBadge(record.status)}</TableCell>
|
||||||
|
<TableCell>{record.startTime}</TableCell>
|
||||||
|
<TableCell>{record.endTime || '暂无'}</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleViewDetail(record)}
|
||||||
|
>
|
||||||
|
详情
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 分页器 */}
|
||||||
|
{pageCount > 1 && (
|
||||||
|
<DataTablePagination
|
||||||
|
pageIndex={query.pageNum + 1}
|
||||||
|
pageSize={query.pageSize}
|
||||||
|
pageCount={pageCount}
|
||||||
|
onPageChange={(page) => setQuery(prev => ({
|
||||||
|
...prev,
|
||||||
|
pageNum: page - 1
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{selectedInstance && (
|
{selectedInstance && (
|
||||||
<DetailModal
|
<DetailModal
|
||||||
visible={detailVisible}
|
visible={detailVisible}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user