From d99c46eb4867ee1a68b5e889d3714d5b996b2130 Mon Sep 17 00:00:00 2001 From: dengqichen Date: Tue, 4 Nov 2025 22:37:13 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BB=A3=E7=A0=81=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E5=99=A8=E8=A1=A8=E5=8D=95=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dashboard/components/ApplicationCard.tsx | 32 ++++- .../components/DeployFlowGraphModal.tsx | 110 ++++++++++++++++-- frontend/src/pages/Dashboard/service.ts | 7 ++ frontend/src/pages/Dashboard/types.ts | 69 ++++++++++- .../pages/Dashboard/utils/dashboardUtils.ts | 43 +++++-- 5 files changed, 234 insertions(+), 27 deletions(-) diff --git a/frontend/src/pages/Dashboard/components/ApplicationCard.tsx b/frontend/src/pages/Dashboard/components/ApplicationCard.tsx index 6ba7a814..84bfcbd7 100644 --- a/frontend/src/pages/Dashboard/components/ApplicationCard.tsx +++ b/frontend/src/pages/Dashboard/components/ApplicationCard.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; @@ -19,7 +19,8 @@ import { Hash, } from "lucide-react"; import { formatDuration, formatTime, getStatusIcon, getStatusText } from '../utils/dashboardUtils'; -import type { ApplicationConfig, DeployEnvironment } from '../types'; +import type { ApplicationConfig, DeployEnvironment, DeployRecord } from '../types'; +import { DeployFlowGraphModal } from './DeployFlowGraphModal'; interface ApplicationCardProps { app: ApplicationConfig; @@ -34,6 +35,14 @@ export const ApplicationCard: React.FC = ({ onDeploy, isDeploying, }) => { + const [selectedDeployRecordId, setSelectedDeployRecordId] = useState(null); + const [flowModalOpen, setFlowModalOpen] = useState(false); + + const handleDeployRecordClick = (record: DeployRecord) => { + setSelectedDeployRecordId(record.id); + setFlowModalOpen(true); + }; + return (
{/* 应用基本信息 */} @@ -237,10 +246,16 @@ export const ApplicationCard: React.FC = ({ {getStatusText(record.status)}
-
+
+ + {record.id} + + {record.deployBy && (
@@ -335,6 +350,13 @@ export const ApplicationCard: React.FC = ({ )} + + {/* 部署流程图模态框 */} +
); }; diff --git a/frontend/src/pages/Dashboard/components/DeployFlowGraphModal.tsx b/frontend/src/pages/Dashboard/components/DeployFlowGraphModal.tsx index 94e53f7b..bce0bb52 100644 --- a/frontend/src/pages/Dashboard/components/DeployFlowGraphModal.tsx +++ b/frontend/src/pages/Dashboard/components/DeployFlowGraphModal.tsx @@ -157,11 +157,104 @@ export const DeployFlowGraphModal: React.FC = ({ return instance ? instance.status : 'NOT_STARTED'; }; - // 转换为 React Flow 节点(使用后端返回的 position) + // 计算节点拓扑顺序(用于自动布局) + const calculateNodeOrder = useMemo(() => { + if (!flowData?.graph?.nodes || !flowData?.graph?.edges) { + return new Map(); + } + + const edges = flowData.graph.edges; + const nodes = flowData.graph.nodes; + const nodeIds = new Set(nodes.map(n => n.id)); + + // 构建图的邻接表和入度 + const adjacencyList = new Map(); + const inDegree = new Map(); + + // 初始化 + nodes.forEach(node => { + adjacencyList.set(node.id, []); + inDegree.set(node.id, 0); + }); + + // 构建图 + edges.forEach(edge => { + const source = edge.from; + const target = edge.to; + if (nodeIds.has(source) && nodeIds.has(target)) { + adjacencyList.get(source)!.push(target); + inDegree.set(target, (inDegree.get(target) || 0) + 1); + } + }); + + // 拓扑排序:找到所有没有入边的节点(起始节点) + const queue: string[] = []; + nodes.forEach(node => { + if (inDegree.get(node.id) === 0) { + queue.push(node.id); + } + }); + + // 拓扑排序 + const topoOrder: string[] = []; + const processed = new Set(); + let currentLevelNodes = [...queue]; + + while (currentLevelNodes.length > 0) { + const nextLevelNodes: string[] = []; + + currentLevelNodes.forEach(nodeId => { + if (processed.has(nodeId)) return; + processed.add(nodeId); + topoOrder.push(nodeId); + + // 找到所有从当前节点出发的边,减少目标节点的入度 + adjacencyList.get(nodeId)?.forEach(targetId => { + const currentInDegree = (inDegree.get(targetId) || 0) - 1; + inDegree.set(targetId, currentInDegree); + + if (currentInDegree === 0 && !processed.has(targetId)) { + nextLevelNodes.push(targetId); + } + }); + }); + + currentLevelNodes = nextLevelNodes; + } + + // 处理剩余节点 + nodes.forEach(node => { + if (!processed.has(node.id)) { + topoOrder.push(node.id); + } + }); + + // 创建顺序映射 + const orderMap = new Map(); + topoOrder.forEach((nodeId, index) => { + orderMap.set(nodeId, index); + }); + + return orderMap; + }, [flowData]); + + // 转换为 React Flow 节点(自动水平布局,简化版) const flowNodes: Node[] = useMemo(() => { if (!flowData?.graph?.nodes) return []; - return flowData.graph.nodes.map((node) => { + // 按拓扑顺序排序节点 + const sortedNodes = Array.from(flowData.graph.nodes).sort((a, b) => { + const orderA = calculateNodeOrder.get(a.id) ?? Infinity; + const orderB = calculateNodeOrder.get(b.id) ?? Infinity; + return orderA - orderB; + }); + + // 自动水平排列(紧凑布局) + const horizontalSpacing = 150; // 缩小间距 + const startX = 100; + const startY = 200; // 固定Y坐标,所有节点在同一行 + + return sortedNodes.map((node, index) => { // 1. 匹配执行状态 const instance = nodeInstanceMap.get(node.id); const status = instance ? instance.status : 'NOT_STARTED'; @@ -171,16 +264,15 @@ export const DeployFlowGraphModal: React.FC = ({ return { id: node.id, type: 'default', - // 2. 使用后端返回的 position(不要重新计算) - position: node.position, + // 自动水平布局 + position: { + x: startX + index * horizontalSpacing, + y: startY, + }, data: { nodeName: node.nodeName, nodeType: node.nodeType, status: status, - startTime: instance?.startTime || null, - endTime: instance?.endTime || null, - // 3. 显示执行结果(如果有) - outputs: instance?.outputs || null, }, style: { background: colors.bg, @@ -189,7 +281,7 @@ export const DeployFlowGraphModal: React.FC = ({ }, }; }); - }, [flowData, nodeInstanceMap]); + }, [flowData, nodeInstanceMap, calculateNodeOrder]); // 判断边是否已执行 const isEdgeExecuted = (sourceNodeId: string): boolean => { diff --git a/frontend/src/pages/Dashboard/service.ts b/frontend/src/pages/Dashboard/service.ts index 6f9aeb7b..79ac1ecf 100644 --- a/frontend/src/pages/Dashboard/service.ts +++ b/frontend/src/pages/Dashboard/service.ts @@ -14,3 +14,10 @@ export const getDeployEnvironments = () => */ export const startDeployment = (teamApplicationId: number) => request.post(`${DEPLOY_URL}/execute`, { teamApplicationId }); + +/** + * 获取部署流程图数据 + * @param deployRecordId 部署记录ID + */ +export const getDeployRecordFlowGraph = (deployRecordId: number) => + request.get(`${DEPLOY_URL}/records/${deployRecordId}/flow-graph`); diff --git a/frontend/src/pages/Dashboard/types.ts b/frontend/src/pages/Dashboard/types.ts index 8ebf3663..8c5aa146 100644 --- a/frontend/src/pages/Dashboard/types.ts +++ b/frontend/src/pages/Dashboard/types.ts @@ -7,7 +7,16 @@ export interface Approver { /** * 部署状态类型 */ -export type DeployStatus = 'SUCCESS' | 'FAILED' | 'RUNNING' | 'CANCELLED' | 'PARTIAL_SUCCESS'; +export type DeployStatus = + | 'CREATED' // 已创建 + | 'PENDING_APPROVAL' // 待审批 + | 'RUNNING' // 运行中 + | 'SUCCESS' // 部署成功 + | 'FAILED' // 部署失败 + | 'PARTIAL_SUCCESS' // 部分成功(工作流完成但存在失败的节点) + | 'REJECTED' // 审批被拒绝(终态) + | 'CANCELLED' // 已取消(终态) + | 'TERMINATED'; // 已终止(终态) export interface DeployStatistics { totalCount: number; @@ -88,3 +97,61 @@ export interface StartDeploymentResponse { processKey: string; startTime?: string; } + +/** + * 工作流节点实例 + */ +export interface WorkflowNodeInstance { + id: number; + nodeId: string; + status: string; // NOT_STARTED/RUNNING/COMPLETED/FAILED/REJECTED等 + startTime?: string | null; + endTime?: string | null; + outputs?: Record; +} + +/** + * 工作流定义图节点 + */ +export interface WorkflowDefinitionGraphNode { + id: string; + nodeCode?: string; + nodeType: string; + nodeName: string; + position: { + x: number; + y: number; + }; + configs?: Record; + inputMapping?: Record; + outputs?: any[]; +} + +/** + * 工作流定义图边 + */ +export interface WorkflowDefinitionGraphEdge { + from: string; + to: string; + config?: Record; +} + +/** + * 工作流定义图 + */ +export interface WorkflowDefinitionGraph { + nodes: WorkflowDefinitionGraphNode[]; + edges: WorkflowDefinitionGraphEdge[]; +} + +/** + * 部署流程图数据 + */ +export interface DeployRecordFlowGraph { + deployRecordId: number; + workflowInstanceId: number; + processInstanceId: string; + deployStatus: DeployStatus; + graph: WorkflowDefinitionGraph; + nodeInstances: WorkflowNodeInstance[]; +} diff --git a/frontend/src/pages/Dashboard/utils/dashboardUtils.ts b/frontend/src/pages/Dashboard/utils/dashboardUtils.ts index 021a03e1..82582ae4 100644 --- a/frontend/src/pages/Dashboard/utils/dashboardUtils.ts +++ b/frontend/src/pages/Dashboard/utils/dashboardUtils.ts @@ -4,6 +4,9 @@ import { Loader2, Clock, AlertCircle, + CircleDot, + Ban, + StopCircle, type LucideIcon } from "lucide-react"; @@ -64,16 +67,24 @@ export const formatTime = (timeStr?: string): string => { */ export const getStatusIcon = (status?: string): { icon: LucideIcon; color: string } => { switch (status) { - case 'SUCCESS': - return { icon: CheckCircle2, color: 'text-green-600' }; - case 'PARTIAL_SUCCESS': - return { icon: AlertCircle, color: 'text-amber-600' }; - case 'FAILED': - return { icon: XCircle, color: 'text-red-600' }; + case 'CREATED': + return { icon: CircleDot, color: 'text-blue-500' }; + case 'PENDING_APPROVAL': + return { icon: Clock, color: 'text-orange-500' }; case 'RUNNING': return { icon: Loader2, color: 'text-blue-600' }; + case 'SUCCESS': + return { icon: CheckCircle2, color: 'text-green-600' }; + case 'FAILED': + return { icon: XCircle, color: 'text-red-600' }; + case 'PARTIAL_SUCCESS': + return { icon: AlertCircle, color: 'text-amber-600' }; + case 'REJECTED': + return { icon: Ban, color: 'text-red-500' }; case 'CANCELLED': return { icon: Clock, color: 'text-gray-400' }; + case 'TERMINATED': + return { icon: StopCircle, color: 'text-gray-500' }; default: return { icon: Clock, color: 'text-gray-400' }; } @@ -86,16 +97,24 @@ export const getStatusIcon = (status?: string): { icon: LucideIcon; color: strin */ export const getStatusText = (status?: string): string => { switch (status) { - case 'SUCCESS': - return '成功'; - case 'PARTIAL_SUCCESS': - return '部分成功'; - case 'FAILED': - return '失败'; + case 'CREATED': + return '已创建'; + case 'PENDING_APPROVAL': + return '待审批'; case 'RUNNING': return '运行中'; + case 'SUCCESS': + return '部署成功'; + case 'FAILED': + return '部署失败'; + case 'PARTIAL_SUCCESS': + return '部分成功'; + case 'REJECTED': + return '审批被拒绝'; case 'CANCELLED': return '已取消'; + case 'TERMINATED': + return '已终止'; default: return '未知'; }