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-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",
|
||||
|
||||
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 || ''}
|
||||
variant="textarea"
|
||||
placeholder="请输入条件表达式,如:${Jenkins构建.buildStatus == 'SUCCESS'}"
|
||||
rows={4}
|
||||
rows={4}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<DetailModalProps> = ({ visible, onCancel, instanceData }) => {
|
||||
if (!instanceData) return null;
|
||||
|
||||
const getStatusTag = (status: string) => {
|
||||
const statusMap: Record<string, { color: string; text: string }> = {
|
||||
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<string, { variant: 'default' | 'secondary' | 'destructive' | 'outline' | 'success'; text: string }> = {
|
||||
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 <Tag color={statusInfo.color}>{statusInfo.text}</Tag>;
|
||||
const statusInfo = statusMap[status] || { variant: 'outline', text: status };
|
||||
return <Badge variant={statusInfo.variant}>{statusInfo.text}</Badge>;
|
||||
};
|
||||
|
||||
const getNodeTypeText = (nodeType: string) => {
|
||||
const nodeTypeMap: Record<string, string> = {
|
||||
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 <CheckCircle2 className="h-4 w-4 text-green-500" />;
|
||||
case 'RUNNING':
|
||||
return 'process';
|
||||
return <Clock className="h-4 w-4 text-blue-500 animate-pulse" />;
|
||||
case 'FAILED':
|
||||
return 'error';
|
||||
case 'TERMINATED':
|
||||
return 'wait';
|
||||
return <XCircle className="h-4 w-4 text-red-500" />;
|
||||
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 (
|
||||
<Modal
|
||||
title="流程执行详情"
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
width={1200}
|
||||
footer={null}
|
||||
>
|
||||
<Card className="mb-4">
|
||||
<Descriptions title="基本信息" bordered column={2}>
|
||||
<Descriptions.Item label="业务标识">{instanceData.businessKey}</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">{getStatusTag(instanceData.status)}</Descriptions.Item>
|
||||
<Descriptions.Item label="开始时间">{dayjs(instanceData.startTime).format('YYYY-MM-DD HH:mm:ss')}</Descriptions.Item>
|
||||
<Descriptions.Item label="结束时间">
|
||||
{instanceData.endTime ? dayjs(instanceData.endTime).format('YYYY-MM-DD HH:mm:ss') : '暂无'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="流程实例ID">{instanceData.processInstanceId}</Descriptions.Item>
|
||||
<Descriptions.Item label="流程定义ID">{instanceData.processDefinitionId}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
<Dialog open={visible} onOpenChange={(open) => !open && onCancel()}>
|
||||
<DialogPortal>
|
||||
<DialogOverlay className="z-[60]" />
|
||||
<DialogPrimitive.Content
|
||||
className={cn(
|
||||
"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"
|
||||
)}
|
||||
>
|
||||
<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">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
<DialogHeader>
|
||||
<DialogTitle>流程执行详情</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<Tabs defaultValue="graph" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="graph">流程图</TabsTrigger>
|
||||
<TabsTrigger value="timeline">执行时间线</TabsTrigger>
|
||||
<TabsTrigger value="info">详细信息</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<Card title="执行阶段" className="mb-4">
|
||||
<Steps
|
||||
progressDot
|
||||
current={instanceData.stages.length}
|
||||
items={instanceData.stages.map((stage) => ({
|
||||
title: stage.nodeName,
|
||||
description: (
|
||||
<div className="text-xs">
|
||||
<div>{getNodeTypeText(stage.nodeType)}</div>
|
||||
<div>{getStatusTag(stage.status)}</div>
|
||||
</div>
|
||||
),
|
||||
status: getStepStatus(stage)
|
||||
}))}
|
||||
/>
|
||||
</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')}`}
|
||||
{/* 流程图标签页 */}
|
||||
<TabsContent value="graph" className="space-y-4">
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<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') : '暂无'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div>{getStatusTag(stage.status)}</div>
|
||||
</div>
|
||||
)
|
||||
}))}
|
||||
/>
|
||||
</Card>
|
||||
</Modal>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{instanceData.graph ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>流程执行图</CardTitle>
|
||||
</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 { 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<HistoryModalProps> = ({ visible, onCancel, workflow
|
||||
setDetailVisible(true);
|
||||
};
|
||||
|
||||
const getStatusTag = (status: string) => {
|
||||
const statusMap: Record<string, { color: string; text: string }> = {
|
||||
COMPLETED: { color: 'success', text: '已完成' },
|
||||
RUNNING: { color: 'processing', text: '运行中' },
|
||||
FAILED: { color: 'error', text: '失败' },
|
||||
TERMINATED: { color: 'warning', text: '已终止' }
|
||||
const getStatusBadge = (status: string) => {
|
||||
const statusMap: Record<string, { variant: 'default' | 'secondary' | 'destructive' | 'outline' | 'success'; text: string }> = {
|
||||
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 <Tag color={statusInfo.color}>{statusInfo.text}</Tag>;
|
||||
const statusInfo = statusMap[status] || { variant: 'outline', text: status };
|
||||
return <Badge variant={statusInfo.variant}>{statusInfo.text}</Badge>;
|
||||
};
|
||||
|
||||
const columns: ColumnsType<WorkflowHistoricalInstance> = [
|
||||
{
|
||||
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>
|
||||
),
|
||||
},
|
||||
];
|
||||
const pageCount = Math.ceil(total / query.pageSize);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
title="历史执行记录"
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
width={1000}
|
||||
footer={null}
|
||||
>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
loading={loading}
|
||||
rowKey="id"
|
||||
scroll={{ x: 800 }}
|
||||
pagination={{
|
||||
current: query.pageNum + 1,
|
||||
pageSize: query.pageSize,
|
||||
total: total,
|
||||
onChange: (page, pageSize) => setQuery(prev => ({
|
||||
...prev,
|
||||
pageNum: page - 1,
|
||||
pageSize
|
||||
})),
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
<Dialog open={visible} onOpenChange={(open) => !open && onCancel()}>
|
||||
<DialogContent className="max-w-6xl max-h-[85vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>历史执行记录</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="mt-4">
|
||||
{loading ? (
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<div className="text-muted-foreground">加载中...</div>
|
||||
</div>
|
||||
) : data.length === 0 ? (
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<div className="text-muted-foreground">暂无数据</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="border rounded-lg">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[200px]">业务标识</TableHead>
|
||||
<TableHead className="w-[100px]">状态</TableHead>
|
||||
<TableHead className="w-[180px]">开始时间</TableHead>
|
||||
<TableHead className="w-[180px]">结束时间</TableHead>
|
||||
<TableHead className="w-[100px] text-right">操作</TableHead>
|
||||
</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 && (
|
||||
<DetailModal
|
||||
visible={detailVisible}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user