diff --git a/frontend/src/pages/Dashboard/components/DeployFlowGraphModal.tsx b/frontend/src/pages/Dashboard/components/DeployFlowGraphModal.tsx index a2807dab..aa247cec 100644 --- a/frontend/src/pages/Dashboard/components/DeployFlowGraphModal.tsx +++ b/frontend/src/pages/Dashboard/components/DeployFlowGraphModal.tsx @@ -50,7 +50,6 @@ interface CustomNodeData { endTime?: string | null; duration?: number | null; errorMessage?: string | null; - isUnreachable?: boolean; processInstanceId?: string; onViewLog?: (nodeId: string, nodeName: string) => void; } @@ -59,7 +58,7 @@ interface CustomNodeData { * 自定义流程节点组件 */ const CustomFlowNode: React.FC = ({ data }) => { - const { nodeName, nodeType, status, startTime, endTime, duration, errorMessage, isUnreachable } = data as CustomNodeData; + const { nodeName, nodeType, status, startTime, endTime, duration, errorMessage } = data as CustomNodeData; const statusColor = getNodeStatusColor(status); const isNotStarted = status === 'NOT_STARTED'; const isRunning = status === 'RUNNING'; @@ -89,7 +88,6 @@ const CustomFlowNode: React.FC = ({ data }) => { isNotStarted && 'border-2 border-dashed', !isNotStarted && 'border-2 border-solid shadow-sm', isRunning && 'animate-pulse', - isUnreachable && 'opacity-40', // 不可达节点半透明 canViewLog && 'cursor-pointer hover:shadow-md hover:scale-[1.02]' // 可查看日志的节点增加交互效果 )} style={{ @@ -461,123 +459,95 @@ export const DeployFlowGraphModal: React.FC = ({ return map; }, [flowData]); - // 计算可达节点(从已执行节点出发能到达的节点) - const reachableNodes = useMemo(() => { - if (!flowData?.graph?.edges || !flowData?.nodeInstances) return new Set(); + // 智能过滤:只显示实际执行路径 + 到终点的路径 + const visibleNodeIds = useMemo(() => { + if (!flowData?.graph?.nodes || !flowData?.graph?.edges || !flowData?.nodeInstances) { + return new Set(); + } - const executedNodeIds = flowData.nodeInstances - .filter(ni => ni.status !== 'NOT_STARTED') - .map(ni => ni.nodeId); + const visible = new Set(); + + // 1. 收集所有已执行的节点(id !== null 表示后端创建了实例) + const executedNodes = flowData.nodeInstances.filter(ni => ni.id !== null); + executedNodes.forEach(ni => visible.add(ni.nodeId)); - if (executedNodeIds.length === 0) return new Set(); + // 如果没有执行任何节点,显示所有节点(初始状态) + if (executedNodes.length === 0) { + flowData.graph.nodes.forEach(n => visible.add(n.id)); + return visible; + } - const reachable = new Set(executedNodeIds); - const queue = [...executedNodeIds]; + // 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]; - // BFS 遍历所有可达节点 - while (queue.length > 0) { - const currentId = queue.shift()!; - const outgoingEdges = flowData.graph.edges.filter(e => e.from === currentId); - - for (const edge of outgoingEdges) { - if (!reachable.has(edge.to)) { - reachable.add(edge.to); - queue.push(edge.to); + // 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 reachable; + return visible; }, [flowData]); - // 转换为 React Flow 节点(显示所有节点,但对不可达节点置灰) + // 转换为 React Flow 节点(只显示可见节点) const flowNodes: Node[] = useMemo(() => { if (!flowData?.graph?.nodes || !flowData?.nodeInstances) return []; - const isRunning = flowData.runningNodeCount > 0; + // 过滤并转换为 React Flow 节点 + return flowData.graph.nodes + .filter(node => visibleNodeIds.has(node.id)) // 只显示可见节点 + .map((node) => { + const instance = nodeInstanceMap.get(node.id); - // 按执行顺序排序已执行的节点 - const executedInstances = flowData.nodeInstances - .filter(ni => ni.status !== 'NOT_STARTED') - .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 timeA - timeB; + return { + id: node.id, + type: 'custom', + position: node.position || { x: 0, y: 0 }, // 使用设计器中保存的坐标 + 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, + }, + }; }); + }, [flowData, nodeInstanceMap, visibleNodeIds]); - // 线性布局:从左到右排列 - const nodePositionMap = new Map(); - const horizontalSpacing = 300; // 水平间距 - const verticalSpacing = 150; // 垂直间距(用于分支) - const startX = 100; // 起始X坐标 - const startY = 150; // 起始Y坐标 - - // 已执行的节点:从左到右线性排列 - executedInstances.forEach((instance, index) => { - nodePositionMap.set(instance.nodeId, { - x: startX + index * horizontalSpacing, - y: startY, - }); - }); - - // 未执行的节点:根据在图中的位置和层级布局 - const notStartedNodes = flowData.graph.nodes.filter( - node => !nodePositionMap.has(node.id) - ); - - // 简单布局:按节点在edges中的出现顺序,从左到右、上到下排列 - let currentX = startX + executedInstances.length * horizontalSpacing; - let currentRow = 0; - const nodesPerRow = 3; - - notStartedNodes.forEach((node, index) => { - const row = Math.floor(index / nodesPerRow); - const col = index % nodesPerRow; - nodePositionMap.set(node.id, { - x: currentX + col * horizontalSpacing, - y: startY + row * verticalSpacing, - }); - }); - - // 生成所有节点 - return flowData.graph.nodes.map((node) => { - const instance = nodeInstanceMap.get(node.id); - const position = nodePositionMap.get(node.id) || { x: 0, y: 0 }; - const isReachable = reachableNodes.has(node.id); - const isNotStarted = !instance || instance.status === 'NOT_STARTED'; - - return { - id: node.id, - type: 'custom', - position, - data: { - nodeName: node.nodeName, - nodeType: node.nodeType, - nodeId: node.id, - status: instance?.status || 'NOT_STARTED', - startTime: instance?.startTime, - endTime: instance?.endTime, - duration: instance?.duration, - errorMessage: instance?.errorMessage, - // 新增:不可达且未执行的节点标记为置灰 - isUnreachable: isRunning && isNotStarted && !isReachable, - processInstanceId: flowData.processInstanceId, - }, - }; - }); - }, [flowData, nodeInstanceMap, reachableNodes]); - - // 转换为 React Flow 边 + // 转换为 React Flow 边(只显示连接可见节点的边) const flowEdges: Edge[] = useMemo(() => { if (!flowData?.graph?.edges) return []; - const isRunning = flowData.runningNodeCount > 0; - const displayedNodeIds = new Set(flowNodes.map(n => n.id)); - return flowData.graph.edges .filter(edge => { - // 只显示连接已显示节点的边 - return displayedNodeIds.has(edge.from) && displayedNodeIds.has(edge.to); + // 只显示连接可见节点的边 + return visibleNodeIds.has(edge.from) && visibleNodeIds.has(edge.to); }) .map((edge, index) => { const source = edge.from; @@ -587,20 +557,21 @@ export const DeployFlowGraphModal: React.FC = ({ const sourceStatus = sourceInstance?.status || 'NOT_STARTED'; const targetStatus = targetInstance?.status || 'NOT_STARTED'; - // 判断这条边是否在可达路径上 - const isReachableEdge = reachableNodes.has(source) && reachableNodes.has(target); - // 根据节点状态确定边的样式 let strokeColor = '#d1d5db'; // 默认灰色 let strokeWidth = 2; let animated = false; - let opacity = 1; + let strokeDasharray: string | undefined = undefined; // 源节点已完成 + 目标节点也已完成/运行中 = 绿色实线 if (sourceStatus === 'COMPLETED' && (targetStatus === 'COMPLETED' || targetStatus === 'RUNNING')) { strokeColor = '#10b981'; // 绿色 } - // 源节点失败 = 红色 + // 源节点 TERMINATED = 橙色实线 + else if (sourceStatus === 'TERMINATED') { + strokeColor = '#f59e0b'; // 橙色 + } + // 源节点失败 = 红色实线 else if (sourceStatus === 'FAILED') { strokeColor = '#ef4444'; // 红色 } @@ -609,31 +580,22 @@ export const DeployFlowGraphModal: React.FC = ({ strokeColor = '#3b82f6'; // 蓝色 animated = true; } - // 源节点完成 + 目标节点未开始 - else if (sourceStatus === 'COMPLETED' && targetStatus === 'NOT_STARTED') { - if (isReachableEdge && isRunning) { - strokeColor = '#9ca3af'; // 浅灰色(即将执行) - } else { - strokeColor = '#d1d5db'; // 默认灰色 - opacity = 0.3; // 不可达路径半透明 - } - } - // 两端都未执行 - else if (sourceStatus === 'NOT_STARTED' && targetStatus === 'NOT_STARTED') { - opacity = isRunning && !isReachableEdge ? 0.3 : 0.5; + // 源节点完成 + 目标节点未开始 = 虚线(即将执行的路径) + else if ((sourceStatus === 'COMPLETED' || sourceStatus === 'TERMINATED') && targetStatus === 'NOT_STARTED') { + strokeColor = '#9ca3af'; // 浅灰色 + strokeDasharray = '5,5'; } return { id: edge.id || `edge-${source}-${target}-${index}`, source, target, - type: 'smoothstep', + type: 'straight', // 使用直线类型 animated, style: { stroke: strokeColor, strokeWidth, - opacity, - strokeDasharray: (sourceStatus === 'COMPLETED' && targetStatus === 'NOT_STARTED' && isReachableEdge) ? '5,5' : undefined, + strokeDasharray, }, markerEnd: { type: 'arrowclosed', @@ -643,7 +605,7 @@ export const DeployFlowGraphModal: React.FC = ({ }, }; }); - }, [flowData, nodeInstanceMap, flowNodes, reachableNodes]); + }, [flowData, nodeInstanceMap, visibleNodeIds]); // 获取部署状态信息 const deployStatusInfo = flowData diff --git a/frontend/src/pages/Dashboard/components/DeployNodeLogDialog.tsx b/frontend/src/pages/Dashboard/components/DeployNodeLogDialog.tsx index f04a363b..b7788b72 100644 --- a/frontend/src/pages/Dashboard/components/DeployNodeLogDialog.tsx +++ b/frontend/src/pages/Dashboard/components/DeployNodeLogDialog.tsx @@ -151,35 +151,49 @@ const DeployNodeLogDialog: React.FC = ({
{logData?.logs && logData.logs.length > 0 ? ( - logData.logs.map((log, index) => ( -
- {/* 行号 - 根据总行数动态调整宽度 */} - { + // 计算行号宽度 + const lineNumWidth = Math.max(4, String(logData.logs.length).length + 1); + + // 格式化时间戳为可读格式:2025-11-07 16:27:41.494 + const timestamp = dayjs(log.timestamp).format('YYYY-MM-DD HH:mm:ss.SSS'); + + return ( +
- {index + 1} - - {/* 时间 - 18个字符宽度 */} - - {dayjs(log.timestamp).format('MM-DD HH:mm:ss.SSS')} - - {/* 级别 - 5个字符宽度 */} - - {log.level} - - {/* 日志内容 - 不换行,支持水平滚动 */} - - {log.message} - -
- )) + {/* 行号 - 动态宽度,右对齐 */} + + {index + 1} + + + {/* 时间戳 - 可读格式,23个字符 (2025-11-07 16:27:41.494) */} + + {timestamp} + + + {/* 日志级别 - 5个字符,右对齐 */} + + {log.level} + + + {/* 日志消息 - 占据剩余空间,不换行 */} + + {log.message} + +
+ ); + }) ) : (
diff --git a/frontend/src/pages/Deploy/Application/List/index.tsx b/frontend/src/pages/Deploy/Application/List/index.tsx index f0eebc84..42407cb2 100644 --- a/frontend/src/pages/Deploy/Application/List/index.tsx +++ b/frontend/src/pages/Deploy/Application/List/index.tsx @@ -214,7 +214,7 @@ const ApplicationList: React.FC = () => { { accessorKey: 'appCode', header: '应用编码', - size: 150, + size: 200, }, { accessorKey: 'appName', @@ -249,7 +249,7 @@ const ApplicationList: React.FC = () => { { id: 'repository', header: '代码仓库', - size: 200, + size: 280, cell: ({ row }) => { const project = row.original.repositoryProject; return project ? ( diff --git a/frontend/src/pages/Deploy/Team/List/components/TeamApplicationDialog.tsx b/frontend/src/pages/Deploy/Team/List/components/TeamApplicationDialog.tsx index 4cfa2bbc..c9f0099b 100644 --- a/frontend/src/pages/Deploy/Team/List/components/TeamApplicationDialog.tsx +++ b/frontend/src/pages/Deploy/Team/List/components/TeamApplicationDialog.tsx @@ -39,6 +39,7 @@ interface TeamApplicationDialogProps { applications: Application[]; // 可选择的应用列表 jenkinsSystems: any[]; workflowDefinitions: WorkflowDefinition[]; + existingApplicationIds?: number[]; // 已添加的应用ID列表(用于过滤) onOpenChange: (open: boolean) => void; onSave: (data: { id?: number; @@ -62,6 +63,7 @@ const TeamApplicationDialog: React.FC = ({ applications, jenkinsSystems, workflowDefinitions, + existingApplicationIds = [], onOpenChange, onSave, onLoadBranches, @@ -209,11 +211,7 @@ const TeamApplicationDialog: React.FC = ({ }); onOpenChange(false); } catch (error: any) { - toast({ - variant: 'destructive', - title: mode === 'edit' ? '保存失败' : '添加失败', - description: error.response?.data?.message || error.message, - }); + // 错误已经在 request.ts 中通过 toast 显示了,这里不需要再显示 } finally { setSaving(false); } @@ -254,17 +252,24 @@ const TeamApplicationDialog: React.FC = ({ - {applications.length === 0 ? ( -
- 暂无应用 -
- ) : ( - applications.map((app) => ( - - {app.appName}({app.appCode}) - - )) - )} + {(() => { + // 在新建模式下,过滤掉已添加的应用 + const availableApps = mode === 'create' + ? applications.filter(app => !existingApplicationIds.includes(app.id)) + : applications; + + return availableApps.length === 0 ? ( +
+ {mode === 'create' ? '暂无可添加的应用' : '暂无应用'} +
+ ) : ( + availableApps.map((app) => ( + + {app.appName}({app.appCode}) + + )) + ); + })()}
{mode === 'edit' && ( diff --git a/frontend/src/pages/Deploy/Team/List/components/TeamApplicationManageDialog.tsx b/frontend/src/pages/Deploy/Team/List/components/TeamApplicationManageDialog.tsx index 0516db0c..87999e36 100644 --- a/frontend/src/pages/Deploy/Team/List/components/TeamApplicationManageDialog.tsx +++ b/frontend/src/pages/Deploy/Team/List/components/TeamApplicationManageDialog.tsx @@ -246,7 +246,9 @@ export const TeamApplicationManageDialog: React.FC< {teamApplications.map((app) => ( - {app.applicationName || `应用 ${app.applicationId}`} + {app.applicationName && app.applicationCode + ? `${app.applicationName}(${app.applicationCode})` + : app.applicationName || app.applicationCode || `应用 ${app.applicationId}`} {getEnvironmentName(app.environmentId)} @@ -257,10 +259,7 @@ export const TeamApplicationManageDialog: React.FC< {app.deployJob || '-'} - {app.workflowDefinitionName || - (app.workflowDefinitionId - ? `工作流 ${app.workflowDefinitionId}` - : '-')} + {app.workflowDefinitionName || '-'}
@@ -309,6 +308,12 @@ export const TeamApplicationManageDialog: React.FC< applications={applications} jenkinsSystems={jenkinsSystems} workflowDefinitions={workflowDefinitions} + existingApplicationIds={ + // 传递当前环境已添加的应用ID列表 + teamApplications + .filter(app => app.environmentId === editingEnvironment.id) + .map(app => app.applicationId) + } onSave={handleSaveApplication} onLoadBranches={handleLoadBranches} onLoadJenkinsJobs={handleLoadJenkinsJobs} @@ -320,7 +325,11 @@ export const TeamApplicationManageDialog: React.FC< open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen} title="确认删除" - description={`确定要删除应用配置 "${deletingApp?.applicationName || `应用 ${deletingApp?.applicationId}`}" 吗?此操作无法撤销。`} + description={`确定要删除应用配置 "${ + deletingApp?.applicationName && deletingApp?.applicationCode + ? `${deletingApp.applicationName}(${deletingApp.applicationCode})` + : deletingApp?.applicationName || deletingApp?.applicationCode || `应用 ${deletingApp?.applicationId}` + }" 吗?此操作无法撤销。`} item={deletingApp} onConfirm={handleConfirmDelete} onSuccess={() => { diff --git a/frontend/src/pages/Deploy/Team/List/components/TeamEnvironmentConfigDialog.tsx b/frontend/src/pages/Deploy/Team/List/components/TeamEnvironmentConfigDialog.tsx index ffe3293e..ad55d55c 100644 --- a/frontend/src/pages/Deploy/Team/List/components/TeamEnvironmentConfigDialog.tsx +++ b/frontend/src/pages/Deploy/Team/List/components/TeamEnvironmentConfigDialog.tsx @@ -63,7 +63,7 @@ const formSchema = z.object({ environmentId: z.number().min(1, '请选择环境'), approvalRequired: z.boolean().default(false), approverUserIds: z.array(z.number()).default([]), - notificationChannelId: z.number().optional(), + notificationChannelId: z.number().nullish(), notificationEnabled: z.boolean().default(false), requireCodeReview: z.boolean().default(false), remark: z.string().max(100, '备注最多100个字符').optional(), @@ -253,6 +253,7 @@ export const TeamEnvironmentConfigDialog: React.FC< }; const handleSubmit = async (data: FormData) => { + console.log('表单提交开始', data); setSubmitting(true); try { const payload = { @@ -266,6 +267,8 @@ export const TeamEnvironmentConfigDialog: React.FC< remark: data.remark, }; + console.log('提交数据', payload); + if (configId) { // 更新已有配置 await updateTeamEnvironmentConfig(configId, payload); @@ -285,6 +288,7 @@ export const TeamEnvironmentConfigDialog: React.FC< onOpenChange(false); onSuccess?.(); } catch (error: any) { + console.error('保存失败', error); toast({ title: '保存失败', description: error.message || '保存环境配置失败', @@ -295,6 +299,13 @@ export const TeamEnvironmentConfigDialog: React.FC< } }; + const handleSaveClick = () => { + console.log('保存按钮被点击'); + console.log('表单值', form.getValues()); + console.log('表单错误', form.formState.errors); + form.handleSubmit(handleSubmit)(); + }; + return ( @@ -304,222 +315,37 @@ export const TeamEnvironmentConfigDialog: React.FC< - - {loading ? ( + {loading ? ( +
- ) : ( -
- - {/* 环境选择 */} - ( - - 环境 * - - - - )} - /> - - {/* 是否需要审批 */} - ( - -
- 需要审批 -
- 部署前需要通过审批流程 -
-
- - - -
- )} - /> - - {/* 审批人多选 - 仅在需要审批时显示 */} - {form.watch('approvalRequired') && ( + + ) : ( + + +
+ {/* 环境选择 */} { - const [open, setOpen] = useState(false); - const selectedUsers = (users || []).filter(u => field.value?.includes(u.id)); - - return ( - - 审批人 * - - - - - - ))} -
- ) : ( - "请选择审批人" - )} - - - - - - - - - 未找到审批人 - - {(users || []).map((user) => { - const isSelected = field.value?.includes(user.id); - return ( - { - const newValue = isSelected - ? field.value?.filter(id => id !== user.id) || [] - : [...(field.value || []), user.id]; - field.onChange(newValue); - }} - > - - {user.realName || user.username} - - ); - })} - - - - - - - - ); - }} - /> - )} - - {/* 启用通知 */} - ( - -
- 启用通知 -
- 部署状态变更时发送通知 -
-
- - - -
- )} - /> - - {/* 通知渠道选择 - 仅在启用通知时显示 */} - {form.watch('notificationEnabled') && ( - ( - 通知渠道 * + 环境 *