import React, { useEffect, useState, useMemo, useCallback } from 'react'; import { ReactFlowProvider, ReactFlow, Background, Node, Edge, Handle, Position, BackgroundVariant, NodeProps, Controls, MiniMap } from '@xyflow/react'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Badge } from '@/components/ui/badge'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Progress } from '@/components/ui/progress'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Separator } from '@/components/ui/separator'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { Button } from '@/components/ui/button'; import { Loader2, AlertCircle, User, Building2, Layers, Calendar, Clock, CheckCircle2, XCircle, AlertTriangle, FileText, ArrowRightLeft, ArrowDownUp } from 'lucide-react'; import { cn } from '@/lib/utils'; import '@xyflow/react/dist/style.css'; import dagre from 'dagre'; import { getDeployRecordFlowGraph } from '../service'; import type { DeployRecordFlowGraph, WorkflowNodeInstance } from '../types'; import { getStatusIcon, getStatusText, getNodeStatusText, getNodeStatusColor, formatTime, formatDuration, calculateRunningDuration } from '../utils/dashboardUtils'; import DeployNodeLogDialog from './DeployNodeLogDialog'; interface DeployFlowGraphModalProps { open: boolean; deployRecordId: number | null; onOpenChange: (open: boolean) => void; } interface CustomNodeData { nodeName: string; nodeType: string; nodeId: string; status: string; startTime?: string | null; endTime?: string | null; duration?: number | null; errorMessage?: string | null; processInstanceId?: string; onViewLog?: (nodeId: string, nodeName: string) => void; } type LayoutDirection = 'TB' | 'LR'; /** * 使用 dagre 进行自动布局 */ const getLayoutedElements = ( nodes: Node[], edges: Edge[], direction: LayoutDirection = 'TB' ) => { const dagreGraph = new dagre.graphlib.Graph(); dagreGraph.setDefaultEdgeLabel(() => ({})); const nodeWidth = 180; const nodeHeight = 120; const isHorizontal = direction === 'LR'; dagreGraph.setGraph({ rankdir: direction, nodesep: isHorizontal ? 90 : 120, // 同一层节点间距:横向80,纵向100 ranksep: isHorizontal ? 170 : 160, // 层级间距:横向150,纵向120 marginx: 40, marginy: 40, align: 'UL', // 对齐方式 }); nodes.forEach((node) => { dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight }); }); edges.forEach((edge) => { dagreGraph.setEdge(edge.source, edge.target); }); dagre.layout(dagreGraph); const layoutedNodes = nodes.map((node) => { const nodeWithPosition = dagreGraph.node(node.id); return { ...node, position: { x: nodeWithPosition.x - nodeWidth / 2, y: nodeWithPosition.y - nodeHeight / 2, }, }; }); return { nodes: layoutedNodes, edges }; }; /** * 自定义流程节点组件 */ const CustomFlowNode: React.FC = ({ data }) => { const { nodeName, nodeType, status, startTime, endTime, duration, errorMessage } = data as CustomNodeData; const statusColor = getNodeStatusColor(status); const isNotStarted = status === 'NOT_STARTED'; const isRunning = status === 'RUNNING'; const hasFailed = status === 'FAILED'; // 判断是否可以查看日志(具有日志输出能力的节点类型) const loggableNodeTypes = ['JENKINS_BUILD', 'ServiceTask']; const canViewLog = loggableNodeTypes.includes(nodeType) && status !== 'NOT_STARTED'; // 计算显示的时长 const displayDuration = useMemo(() => { if (duration !== null && duration !== undefined) { // 后端返回的 duration 单位已经是毫秒 return formatDuration(duration); } if (isRunning && startTime) { const runningDuration = calculateRunningDuration(startTime); return formatDuration(runningDuration); } return null; }, [duration, isRunning, startTime]); const nodeContent = (
{/* 顶部状态指示条 */} {!isNotStarted && (
)} {/* 节点名称 */}
{nodeName}
{canViewLog && ( )}
{/* 节点状态 - 使用徽章样式 */}
{getNodeStatusText(status)}
{/* 时间信息 - 更紧凑的显示 */} {!isNotStarted && displayDuration && (
⏱️ {displayDuration}
)} {/* 弹性空间,让底部提示始终在底部 */}
{/* 错误提示 - 增强样式 */} {hasFailed && errorMessage && (
查看错误
)} {/* 查看日志提示 - 增强样式 */} {canViewLog && !hasFailed && (
查看日志
)}
); return ( <> {/* 输入连接点 */} {nodeType !== 'START_EVENT' && ( )} {/* 节点内容 - 如果有错误信息,包装在 Tooltip 中 */} {hasFailed && errorMessage ? ( {nodeContent}

错误信息:

{errorMessage}

) : ( nodeContent )} {/* 输出连接点 */} {nodeType !== 'END_EVENT' && ( )} ); }; const nodeTypes = { custom: CustomFlowNode, }; /** * 业务信息卡片 */ const BusinessInfoCard: React.FC<{ flowData: DeployRecordFlowGraph }> = ({ flowData }) => { return ( 业务信息
应用名称: {flowData.applicationName}
应用编码: {flowData.applicationCode}
部署环境: {flowData.environmentName}
所属团队: {flowData.teamName}
部署人: {flowData.deployBy}
{flowData.deployRemark && ( <>
备注:

{flowData.deployRemark}

)}
); }; /** * 进度信息卡片 */ const ProgressCard: React.FC<{ flowData: DeployRecordFlowGraph }> = ({ flowData }) => { const { executedNodeCount, totalNodeCount, successNodeCount, failedNodeCount, runningNodeCount } = flowData; const progress = totalNodeCount > 0 ? (executedNodeCount / totalNodeCount) * 100 : 0; return ( 执行进度
已执行 {executedNodeCount} / {totalNodeCount}
{Math.round(progress)}%
成功 {successNodeCount}
失败 {failedNodeCount}
0 && "animate-spin")} /> 运行中 {runningNodeCount}
); }; /** * 时长统计卡片 */ const DurationCard: React.FC<{ flowData: DeployRecordFlowGraph }> = ({ flowData }) => { const { deployStartTime, deployDuration, nodeInstances, runningNodeCount } = flowData; const isRunning = runningNodeCount > 0; // 计算当前总时长 const currentDuration = useMemo(() => { if (deployDuration) return deployDuration; // 后端返回的已经是毫秒 if (isRunning) return calculateRunningDuration(deployStartTime); return 0; }, [deployDuration, isRunning, deployStartTime]); return ( 时长统计
开始时间: {deployStartTime ? formatTime(deployStartTime) : '-'}
总时长: {formatDuration(currentDuration)} {isRunning && (运行中...)}
节点执行时长:
{nodeInstances .filter(ni => ni.status !== 'NOT_STARTED') .map((ni) => { // 后端返回的 duration 单位已经是毫秒 const nodeDuration = ni.duration ? ni.duration : (ni.status === 'RUNNING' ? calculateRunningDuration(ni.startTime) : 0); return (
{ni.nodeName} {formatDuration(nodeDuration)} {ni.status === 'RUNNING' && '...'}
); })}
); }; /** * 实时监控提示 */ const MonitoringAlert: React.FC<{ flowData: DeployRecordFlowGraph }> = ({ flowData }) => { const { runningNodeCount } = flowData; if (runningNodeCount === 0) return null; return ( 部署正在进行中 当前有 {runningNodeCount} 个节点正在执行,请稍候... ); }; /** * 信息面板(左侧边栏) */ const DeployInfoPanel: React.FC<{ flowData: DeployRecordFlowGraph }> = ({ flowData }) => { return (
); }; /** * 部署流程图模态框(主组件) */ export const DeployFlowGraphModal: React.FC = ({ open, deployRecordId, onOpenChange, }) => { const [loading, setLoading] = useState(false); const [flowData, setFlowData] = useState(null); const [layoutDirection, setLayoutDirection] = useState('TB'); // 日志对话框状态 const [logDialogOpen, setLogDialogOpen] = useState(false); const [selectedNodeId, setSelectedNodeId] = useState(''); const [selectedNodeName, setSelectedNodeName] = useState(''); // 查看日志处理函数 const handleViewLog = (nodeId: string, nodeName: string) => { setSelectedNodeId(nodeId); setSelectedNodeName(nodeName); setLogDialogOpen(true); }; // 切换布局方向 const toggleLayoutDirection = () => { setLayoutDirection(prev => prev === 'TB' ? 'LR' : 'TB'); }; // ReactFlow 节点点击事件处理 const onNodeClick = useCallback((_event: React.MouseEvent, node: Node) => { const nodeData = node.data as unknown as CustomNodeData; // 可以查看日志的节点类型 const loggableNodeTypes = ['JENKINS_BUILD', 'ServiceTask']; const canViewLog = loggableNodeTypes.includes(nodeData.nodeType) && nodeData.status !== 'NOT_STARTED'; if (canViewLog) { console.log('Node clicked, opening log dialog:', nodeData.nodeId, nodeData.nodeName); handleViewLog(nodeData.nodeId, nodeData.nodeName); } else { console.log('Node clicked but cannot view log:', nodeData.nodeType, nodeData.status); } }, []); // 加载流程图数据 useEffect(() => { if (open && deployRecordId) { setLoading(true); getDeployRecordFlowGraph(deployRecordId) .then((data) => { setFlowData(data); }) .catch((error) => { console.error('加载部署流程图失败:', error); }) .finally(() => { setLoading(false); }); } else { setFlowData(null); } }, [open, deployRecordId]); // 创建节点ID到实例的映射 const nodeInstanceMap = useMemo(() => { if (!flowData?.nodeInstances) return new Map(); const map = new Map(); flowData.nodeInstances.forEach((instance) => { map.set(instance.nodeId, instance); }); return map; }, [flowData]); // 智能过滤:只显示实际执行路径 + 到终点的路径 const visibleNodeIds = useMemo(() => { if (!flowData?.graph?.nodes || !flowData?.graph?.edges || !flowData?.nodeInstances) { return new Set(); } const visible = new Set(); // 1. 收集所有已执行的节点(id !== null 表示后端创建了实例) const executedNodes = flowData.nodeInstances.filter(ni => ni.id !== null); executedNodes.forEach(ni => visible.add(ni.nodeId)); // 如果没有执行任何节点,显示所有节点(初始状态) if (executedNodes.length === 0) { flowData.graph.nodes.forEach(n => visible.add(n.id)); return visible; } // 2. 找出最后执行的节点(按时间排序) const lastExecutedNode = [...executedNodes].sort((a, b) => { const timeA = a.startTime ? new Date(a.startTime.replace(' ', 'T')).getTime() : 0; const timeB = b.startTime ? new Date(b.startTime.replace(' ', 'T')).getTime() : 0; return timeB - timeA; // 降序 })[0]; // 3. 找出所有终点节点(没有出边的节点,通常是 END_EVENT) const allNodeIds = new Set(flowData.graph.nodes.map(n => n.id)); const nodesWithOutgoingEdges = new Set(flowData.graph.edges.map(e => e.from)); const endNodes = flowData.graph.nodes.filter(n => !nodesWithOutgoingEdges.has(n.id)); // 4. BFS 从最后执行的节点到所有终点节点的路径 if (lastExecutedNode && endNodes.length > 0) { const queue = [lastExecutedNode.nodeId]; const visited = new Set([lastExecutedNode.nodeId]); while (queue.length > 0) { const currentId = queue.shift()!; const outgoingEdges = flowData.graph.edges.filter(e => e.from === currentId); for (const edge of outgoingEdges) { if (!visited.has(edge.to)) { visited.add(edge.to); visible.add(edge.to); // 添加到可见集合 queue.push(edge.to); } } } } return visible; }, [flowData]); // 转换为 React Flow 节点(只显示可见节点) const flowNodes: Node[] = useMemo(() => { if (!flowData?.graph?.nodes || !flowData?.nodeInstances) return []; // 过滤并转换为 React Flow 节点 const nodes = flowData.graph.nodes .filter(node => visibleNodeIds.has(node.id)) // 只显示可见节点 .map((node) => { const instance = nodeInstanceMap.get(node.id); return { id: node.id, type: 'custom', position: { x: 0, y: 0 }, // 忽略设计器坐标,使用dagre自动布局 data: { nodeName: node.nodeName, nodeType: instance?.nodeType || node.nodeType, // 优先使用运行时的 nodeType nodeId: node.id, status: instance?.status || 'NOT_STARTED', startTime: instance?.startTime, endTime: instance?.endTime, duration: instance?.duration, errorMessage: instance?.errorMessage, processInstanceId: flowData.processInstanceId, }, }; }); return nodes; }, [flowData, nodeInstanceMap, visibleNodeIds]); // 转换为 React Flow 边(只显示连接可见节点的边) const flowEdges: Edge[] = useMemo(() => { if (!flowData?.graph?.edges) return []; return flowData.graph.edges .filter(edge => { // 只显示连接可见节点的边 return visibleNodeIds.has(edge.from) && visibleNodeIds.has(edge.to); }) .map((edge, index) => { const source = edge.from; const target = edge.to; const sourceInstance = nodeInstanceMap.get(source); const targetInstance = nodeInstanceMap.get(target); const sourceStatus = sourceInstance?.status || 'NOT_STARTED'; const targetStatus = targetInstance?.status || 'NOT_STARTED'; // 根据节点状态确定边的样式 let strokeColor = '#cbd5e1'; // 默认浅灰色 let strokeWidth = 3; // 增加连线粗细 let animated = false; let strokeDasharray: string | undefined = undefined; // 源节点已完成 + 目标节点也已完成/运行中 = 绿色实线 if (sourceStatus === 'COMPLETED' && (targetStatus === 'COMPLETED' || targetStatus === 'RUNNING')) { strokeColor = '#10b981'; // 绿色 strokeWidth = 3.5; } // 源节点 TERMINATED = 橙色实线 else if (sourceStatus === 'TERMINATED') { strokeColor = '#f59e0b'; // 橙色 strokeWidth = 3.5; } // 源节点失败 = 红色实线 else if (sourceStatus === 'FAILED') { strokeColor = '#ef4444'; // 红色 strokeWidth = 3.5; } // 源节点运行中 = 蓝色动画 else if (sourceStatus === 'RUNNING') { strokeColor = '#3b82f6'; // 蓝色 strokeWidth = 4; // 运行中的线更粗 animated = true; } // 源节点完成 + 目标节点未开始 = 虚线(即将执行的路径) else if ((sourceStatus === 'COMPLETED' || sourceStatus === 'TERMINATED') && targetStatus === 'NOT_STARTED') { strokeColor = '#94a3b8'; // 稍深的灰色 strokeDasharray = '8,4'; strokeWidth = 2.5; } return { id: edge.id || `edge-${source}-${target}-${index}`, source, target, type: layoutDirection === 'TB' ? 'step' : 'smoothstep', // TB用直角折线,LR用平滑曲线 animated, style: { stroke: strokeColor, strokeWidth, strokeDasharray, }, markerEnd: { type: 'arrowclosed', color: strokeColor, width: 24, height: 24, }, }; }); }, [flowData, nodeInstanceMap, visibleNodeIds, layoutDirection]); // 应用自动布局 const { nodes: layoutedNodes, edges: layoutedEdges } = useMemo(() => { if (flowNodes.length === 0) { return { nodes: [], edges: [] }; } return getLayoutedElements(flowNodes, flowEdges, layoutDirection); }, [flowNodes, flowEdges, layoutDirection]); // 获取部署状态信息 const deployStatusInfo = flowData ? (() => { const { icon: StatusIcon, color } = getStatusIcon(flowData.deployStatus); return { icon: StatusIcon, color, text: getStatusText(flowData.deployStatus), }; })() : null; return ( <>
{flowData && ( #{flowData.deployRecordId} )} 部署流程图 {deployStatusInfo && ( {deployStatusInfo.text} )}
{/* 布局切换按钮 */}
{loading ? (

加载流程图数据...

) : flowData ? ( <> {/* 左侧信息面板 */} {/* 右侧流程图可视化 */}
{ const nodeData = node.data as unknown as CustomNodeData; return getNodeStatusColor(nodeData.status); }} maskColor="rgba(0, 0, 0, 0.1)" className="!shadow-lg !border !border-border" style={{ height: 100, width: 150, }} />
) : (

暂无流程图数据

)}
{/* 节点日志对话框 */} {flowData && ( )} ); };