重构前端逻辑
This commit is contained in:
parent
ccc8bc591e
commit
3b77cf7539
@ -50,7 +50,6 @@ interface CustomNodeData {
|
|||||||
endTime?: string | null;
|
endTime?: string | null;
|
||||||
duration?: number | null;
|
duration?: number | null;
|
||||||
errorMessage?: string | null;
|
errorMessage?: string | null;
|
||||||
isUnreachable?: boolean;
|
|
||||||
processInstanceId?: string;
|
processInstanceId?: string;
|
||||||
onViewLog?: (nodeId: string, nodeName: string) => void;
|
onViewLog?: (nodeId: string, nodeName: string) => void;
|
||||||
}
|
}
|
||||||
@ -59,7 +58,7 @@ interface CustomNodeData {
|
|||||||
* 自定义流程节点组件
|
* 自定义流程节点组件
|
||||||
*/
|
*/
|
||||||
const CustomFlowNode: React.FC<any> = ({ data }) => {
|
const CustomFlowNode: React.FC<any> = ({ 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 statusColor = getNodeStatusColor(status);
|
||||||
const isNotStarted = status === 'NOT_STARTED';
|
const isNotStarted = status === 'NOT_STARTED';
|
||||||
const isRunning = status === 'RUNNING';
|
const isRunning = status === 'RUNNING';
|
||||||
@ -89,7 +88,6 @@ const CustomFlowNode: React.FC<any> = ({ data }) => {
|
|||||||
isNotStarted && 'border-2 border-dashed',
|
isNotStarted && 'border-2 border-dashed',
|
||||||
!isNotStarted && 'border-2 border-solid shadow-sm',
|
!isNotStarted && 'border-2 border-solid shadow-sm',
|
||||||
isRunning && 'animate-pulse',
|
isRunning && 'animate-pulse',
|
||||||
isUnreachable && 'opacity-40', // 不可达节点半透明
|
|
||||||
canViewLog && 'cursor-pointer hover:shadow-md hover:scale-[1.02]' // 可查看日志的节点增加交互效果
|
canViewLog && 'cursor-pointer hover:shadow-md hover:scale-[1.02]' // 可查看日志的节点增加交互效果
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
@ -461,123 +459,95 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
|
|||||||
return map;
|
return map;
|
||||||
}, [flowData]);
|
}, [flowData]);
|
||||||
|
|
||||||
// 计算可达节点(从已执行节点出发能到达的节点)
|
// 智能过滤:只显示实际执行路径 + 到终点的路径
|
||||||
const reachableNodes = useMemo(() => {
|
const visibleNodeIds = useMemo(() => {
|
||||||
if (!flowData?.graph?.edges || !flowData?.nodeInstances) return new Set<string>();
|
if (!flowData?.graph?.nodes || !flowData?.graph?.edges || !flowData?.nodeInstances) {
|
||||||
|
return new Set<string>();
|
||||||
|
}
|
||||||
|
|
||||||
const executedNodeIds = flowData.nodeInstances
|
const visible = new Set<string>();
|
||||||
.filter(ni => ni.status !== 'NOT_STARTED')
|
|
||||||
.map(ni => ni.nodeId);
|
|
||||||
|
|
||||||
if (executedNodeIds.length === 0) return new Set<string>();
|
// 1. 收集所有已执行的节点(id !== null 表示后端创建了实例)
|
||||||
|
const executedNodes = flowData.nodeInstances.filter(ni => ni.id !== null);
|
||||||
|
executedNodes.forEach(ni => visible.add(ni.nodeId));
|
||||||
|
|
||||||
const reachable = new Set<string>(executedNodeIds);
|
// 如果没有执行任何节点,显示所有节点(初始状态)
|
||||||
const queue = [...executedNodeIds];
|
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<string>([lastExecutedNode.nodeId]);
|
||||||
|
|
||||||
// BFS 遍历所有可达节点
|
|
||||||
while (queue.length > 0) {
|
while (queue.length > 0) {
|
||||||
const currentId = queue.shift()!;
|
const currentId = queue.shift()!;
|
||||||
const outgoingEdges = flowData.graph.edges.filter(e => e.from === currentId);
|
const outgoingEdges = flowData.graph.edges.filter(e => e.from === currentId);
|
||||||
|
|
||||||
for (const edge of outgoingEdges) {
|
for (const edge of outgoingEdges) {
|
||||||
if (!reachable.has(edge.to)) {
|
if (!visited.has(edge.to)) {
|
||||||
reachable.add(edge.to);
|
visited.add(edge.to);
|
||||||
|
visible.add(edge.to); // 添加到可见集合
|
||||||
queue.push(edge.to);
|
queue.push(edge.to);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return reachable;
|
return visible;
|
||||||
}, [flowData]);
|
}, [flowData]);
|
||||||
|
|
||||||
// 转换为 React Flow 节点(显示所有节点,但对不可达节点置灰)
|
// 转换为 React Flow 节点(只显示可见节点)
|
||||||
const flowNodes: Node[] = useMemo(() => {
|
const flowNodes: Node[] = useMemo(() => {
|
||||||
if (!flowData?.graph?.nodes || !flowData?.nodeInstances) return [];
|
if (!flowData?.graph?.nodes || !flowData?.nodeInstances) return [];
|
||||||
|
|
||||||
const isRunning = flowData.runningNodeCount > 0;
|
// 过滤并转换为 React Flow 节点
|
||||||
|
return flowData.graph.nodes
|
||||||
// 按执行顺序排序已执行的节点
|
.filter(node => visibleNodeIds.has(node.id)) // 只显示可见节点
|
||||||
const executedInstances = flowData.nodeInstances
|
.map((node) => {
|
||||||
.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;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 线性布局:从左到右排列
|
|
||||||
const nodePositionMap = new Map<string, { x: number; y: number }>();
|
|
||||||
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 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 {
|
return {
|
||||||
id: node.id,
|
id: node.id,
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
position,
|
position: node.position || { x: 0, y: 0 }, // 使用设计器中保存的坐标
|
||||||
data: {
|
data: {
|
||||||
nodeName: node.nodeName,
|
nodeName: node.nodeName,
|
||||||
nodeType: node.nodeType,
|
nodeType: instance?.nodeType || node.nodeType, // 优先使用运行时的 nodeType
|
||||||
nodeId: node.id,
|
nodeId: node.id,
|
||||||
status: instance?.status || 'NOT_STARTED',
|
status: instance?.status || 'NOT_STARTED',
|
||||||
startTime: instance?.startTime,
|
startTime: instance?.startTime,
|
||||||
endTime: instance?.endTime,
|
endTime: instance?.endTime,
|
||||||
duration: instance?.duration,
|
duration: instance?.duration,
|
||||||
errorMessage: instance?.errorMessage,
|
errorMessage: instance?.errorMessage,
|
||||||
// 新增:不可达且未执行的节点标记为置灰
|
|
||||||
isUnreachable: isRunning && isNotStarted && !isReachable,
|
|
||||||
processInstanceId: flowData.processInstanceId,
|
processInstanceId: flowData.processInstanceId,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [flowData, nodeInstanceMap, reachableNodes]);
|
}, [flowData, nodeInstanceMap, visibleNodeIds]);
|
||||||
|
|
||||||
// 转换为 React Flow 边
|
// 转换为 React Flow 边(只显示连接可见节点的边)
|
||||||
const flowEdges: Edge[] = useMemo(() => {
|
const flowEdges: Edge[] = useMemo(() => {
|
||||||
if (!flowData?.graph?.edges) return [];
|
if (!flowData?.graph?.edges) return [];
|
||||||
|
|
||||||
const isRunning = flowData.runningNodeCount > 0;
|
|
||||||
const displayedNodeIds = new Set(flowNodes.map(n => n.id));
|
|
||||||
|
|
||||||
return flowData.graph.edges
|
return flowData.graph.edges
|
||||||
.filter(edge => {
|
.filter(edge => {
|
||||||
// 只显示连接已显示节点的边
|
// 只显示连接可见节点的边
|
||||||
return displayedNodeIds.has(edge.from) && displayedNodeIds.has(edge.to);
|
return visibleNodeIds.has(edge.from) && visibleNodeIds.has(edge.to);
|
||||||
})
|
})
|
||||||
.map((edge, index) => {
|
.map((edge, index) => {
|
||||||
const source = edge.from;
|
const source = edge.from;
|
||||||
@ -587,20 +557,21 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
|
|||||||
const sourceStatus = sourceInstance?.status || 'NOT_STARTED';
|
const sourceStatus = sourceInstance?.status || 'NOT_STARTED';
|
||||||
const targetStatus = targetInstance?.status || 'NOT_STARTED';
|
const targetStatus = targetInstance?.status || 'NOT_STARTED';
|
||||||
|
|
||||||
// 判断这条边是否在可达路径上
|
|
||||||
const isReachableEdge = reachableNodes.has(source) && reachableNodes.has(target);
|
|
||||||
|
|
||||||
// 根据节点状态确定边的样式
|
// 根据节点状态确定边的样式
|
||||||
let strokeColor = '#d1d5db'; // 默认灰色
|
let strokeColor = '#d1d5db'; // 默认灰色
|
||||||
let strokeWidth = 2;
|
let strokeWidth = 2;
|
||||||
let animated = false;
|
let animated = false;
|
||||||
let opacity = 1;
|
let strokeDasharray: string | undefined = undefined;
|
||||||
|
|
||||||
// 源节点已完成 + 目标节点也已完成/运行中 = 绿色实线
|
// 源节点已完成 + 目标节点也已完成/运行中 = 绿色实线
|
||||||
if (sourceStatus === 'COMPLETED' && (targetStatus === 'COMPLETED' || targetStatus === 'RUNNING')) {
|
if (sourceStatus === 'COMPLETED' && (targetStatus === 'COMPLETED' || targetStatus === 'RUNNING')) {
|
||||||
strokeColor = '#10b981'; // 绿色
|
strokeColor = '#10b981'; // 绿色
|
||||||
}
|
}
|
||||||
// 源节点失败 = 红色
|
// 源节点 TERMINATED = 橙色实线
|
||||||
|
else if (sourceStatus === 'TERMINATED') {
|
||||||
|
strokeColor = '#f59e0b'; // 橙色
|
||||||
|
}
|
||||||
|
// 源节点失败 = 红色实线
|
||||||
else if (sourceStatus === 'FAILED') {
|
else if (sourceStatus === 'FAILED') {
|
||||||
strokeColor = '#ef4444'; // 红色
|
strokeColor = '#ef4444'; // 红色
|
||||||
}
|
}
|
||||||
@ -609,31 +580,22 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
|
|||||||
strokeColor = '#3b82f6'; // 蓝色
|
strokeColor = '#3b82f6'; // 蓝色
|
||||||
animated = true;
|
animated = true;
|
||||||
}
|
}
|
||||||
// 源节点完成 + 目标节点未开始
|
// 源节点完成 + 目标节点未开始 = 虚线(即将执行的路径)
|
||||||
else if (sourceStatus === 'COMPLETED' && targetStatus === 'NOT_STARTED') {
|
else if ((sourceStatus === 'COMPLETED' || sourceStatus === 'TERMINATED') && targetStatus === 'NOT_STARTED') {
|
||||||
if (isReachableEdge && isRunning) {
|
strokeColor = '#9ca3af'; // 浅灰色
|
||||||
strokeColor = '#9ca3af'; // 浅灰色(即将执行)
|
strokeDasharray = '5,5';
|
||||||
} else {
|
|
||||||
strokeColor = '#d1d5db'; // 默认灰色
|
|
||||||
opacity = 0.3; // 不可达路径半透明
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 两端都未执行
|
|
||||||
else if (sourceStatus === 'NOT_STARTED' && targetStatus === 'NOT_STARTED') {
|
|
||||||
opacity = isRunning && !isReachableEdge ? 0.3 : 0.5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: edge.id || `edge-${source}-${target}-${index}`,
|
id: edge.id || `edge-${source}-${target}-${index}`,
|
||||||
source,
|
source,
|
||||||
target,
|
target,
|
||||||
type: 'smoothstep',
|
type: 'straight', // 使用直线类型
|
||||||
animated,
|
animated,
|
||||||
style: {
|
style: {
|
||||||
stroke: strokeColor,
|
stroke: strokeColor,
|
||||||
strokeWidth,
|
strokeWidth,
|
||||||
opacity,
|
strokeDasharray,
|
||||||
strokeDasharray: (sourceStatus === 'COMPLETED' && targetStatus === 'NOT_STARTED' && isReachableEdge) ? '5,5' : undefined,
|
|
||||||
},
|
},
|
||||||
markerEnd: {
|
markerEnd: {
|
||||||
type: 'arrowclosed',
|
type: 'arrowclosed',
|
||||||
@ -643,7 +605,7 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [flowData, nodeInstanceMap, flowNodes, reachableNodes]);
|
}, [flowData, nodeInstanceMap, visibleNodeIds]);
|
||||||
|
|
||||||
// 获取部署状态信息
|
// 获取部署状态信息
|
||||||
const deployStatusInfo = flowData
|
const deployStatusInfo = flowData
|
||||||
|
|||||||
@ -151,35 +151,49 @@ const DeployNodeLogDialog: React.FC<DeployNodeLogDialogProps> = ({
|
|||||||
<ScrollArea className="flex-1 border rounded-md bg-gray-50" ref={scrollAreaRef}>
|
<ScrollArea className="flex-1 border rounded-md bg-gray-50" ref={scrollAreaRef}>
|
||||||
<div className="p-2 font-mono text-xs">
|
<div className="p-2 font-mono text-xs">
|
||||||
{logData?.logs && logData.logs.length > 0 ? (
|
{logData?.logs && logData.logs.length > 0 ? (
|
||||||
logData.logs.map((log, index) => (
|
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 (
|
||||||
<div
|
<div
|
||||||
key={log.sequenceId}
|
key={log.sequenceId}
|
||||||
className="flex items-start hover:bg-gray-200 px-2 py-0.5 whitespace-nowrap"
|
className="flex items-start hover:bg-gray-100 px-2 py-0.5 whitespace-nowrap"
|
||||||
>
|
>
|
||||||
{/* 行号 - 根据总行数动态调整宽度 */}
|
{/* 行号 - 动态宽度,右对齐 */}
|
||||||
<span
|
<span
|
||||||
className="text-muted-foreground flex-shrink-0 text-right pr-3 select-none"
|
className="text-gray-400 flex-shrink-0 text-right select-none"
|
||||||
style={{ width: `${Math.max(3, String(logData.logs.length).length)}ch` }}
|
style={{ width: `${lineNumWidth}ch`, marginRight: '1ch' }}
|
||||||
>
|
>
|
||||||
{index + 1}
|
{index + 1}
|
||||||
</span>
|
</span>
|
||||||
{/* 时间 - 18个字符宽度 */}
|
|
||||||
<span className="text-muted-foreground flex-shrink-0 pr-2" style={{ width: '18ch' }}>
|
{/* 时间戳 - 可读格式,23个字符 (2025-11-07 16:27:41.494) */}
|
||||||
{dayjs(log.timestamp).format('MM-DD HH:mm:ss.SSS')}
|
|
||||||
</span>
|
|
||||||
{/* 级别 - 5个字符宽度 */}
|
|
||||||
<span
|
<span
|
||||||
className={cn('flex-shrink-0 font-semibold pr-2', getLevelClass(log.level))}
|
className="text-gray-600 flex-shrink-0"
|
||||||
style={{ width: '5ch' }}
|
style={{ width: '23ch', marginRight: '2ch' }}
|
||||||
|
>
|
||||||
|
{timestamp}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* 日志级别 - 5个字符,右对齐 */}
|
||||||
|
<span
|
||||||
|
className={cn('flex-shrink-0 font-semibold text-right', getLevelClass(log.level))}
|
||||||
|
style={{ width: '5ch', marginRight: '2ch' }}
|
||||||
>
|
>
|
||||||
{log.level}
|
{log.level}
|
||||||
</span>
|
</span>
|
||||||
{/* 日志内容 - 不换行,支持水平滚动 */}
|
|
||||||
|
{/* 日志消息 - 占据剩余空间,不换行 */}
|
||||||
<span className="flex-1 text-gray-800 whitespace-nowrap overflow-x-auto">
|
<span className="flex-1 text-gray-800 whitespace-nowrap overflow-x-auto">
|
||||||
{log.message}
|
{log.message}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))
|
);
|
||||||
|
})
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center h-64 text-muted-foreground">
|
<div className="flex flex-col items-center justify-center h-64 text-muted-foreground">
|
||||||
<Clock className="h-12 w-12 mb-4 text-muted-foreground/30" />
|
<Clock className="h-12 w-12 mb-4 text-muted-foreground/30" />
|
||||||
|
|||||||
@ -214,7 +214,7 @@ const ApplicationList: React.FC = () => {
|
|||||||
{
|
{
|
||||||
accessorKey: 'appCode',
|
accessorKey: 'appCode',
|
||||||
header: '应用编码',
|
header: '应用编码',
|
||||||
size: 150,
|
size: 200,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'appName',
|
accessorKey: 'appName',
|
||||||
@ -249,7 +249,7 @@ const ApplicationList: React.FC = () => {
|
|||||||
{
|
{
|
||||||
id: 'repository',
|
id: 'repository',
|
||||||
header: '代码仓库',
|
header: '代码仓库',
|
||||||
size: 200,
|
size: 280,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const project = row.original.repositoryProject;
|
const project = row.original.repositoryProject;
|
||||||
return project ? (
|
return project ? (
|
||||||
|
|||||||
@ -39,6 +39,7 @@ interface TeamApplicationDialogProps {
|
|||||||
applications: Application[]; // 可选择的应用列表
|
applications: Application[]; // 可选择的应用列表
|
||||||
jenkinsSystems: any[];
|
jenkinsSystems: any[];
|
||||||
workflowDefinitions: WorkflowDefinition[];
|
workflowDefinitions: WorkflowDefinition[];
|
||||||
|
existingApplicationIds?: number[]; // 已添加的应用ID列表(用于过滤)
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
onSave: (data: {
|
onSave: (data: {
|
||||||
id?: number;
|
id?: number;
|
||||||
@ -62,6 +63,7 @@ const TeamApplicationDialog: React.FC<TeamApplicationDialogProps> = ({
|
|||||||
applications,
|
applications,
|
||||||
jenkinsSystems,
|
jenkinsSystems,
|
||||||
workflowDefinitions,
|
workflowDefinitions,
|
||||||
|
existingApplicationIds = [],
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
onSave,
|
onSave,
|
||||||
onLoadBranches,
|
onLoadBranches,
|
||||||
@ -209,11 +211,7 @@ const TeamApplicationDialog: React.FC<TeamApplicationDialogProps> = ({
|
|||||||
});
|
});
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast({
|
// 错误已经在 request.ts 中通过 toast 显示了,这里不需要再显示
|
||||||
variant: 'destructive',
|
|
||||||
title: mode === 'edit' ? '保存失败' : '添加失败',
|
|
||||||
description: error.response?.data?.message || error.message,
|
|
||||||
});
|
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@ -254,17 +252,24 @@ const TeamApplicationDialog: React.FC<TeamApplicationDialogProps> = ({
|
|||||||
<SelectValue placeholder="选择应用" />
|
<SelectValue placeholder="选择应用" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{applications.length === 0 ? (
|
{(() => {
|
||||||
|
// 在新建模式下,过滤掉已添加的应用
|
||||||
|
const availableApps = mode === 'create'
|
||||||
|
? applications.filter(app => !existingApplicationIds.includes(app.id))
|
||||||
|
: applications;
|
||||||
|
|
||||||
|
return availableApps.length === 0 ? (
|
||||||
<div className="p-4 text-center text-sm text-muted-foreground">
|
<div className="p-4 text-center text-sm text-muted-foreground">
|
||||||
暂无应用
|
{mode === 'create' ? '暂无可添加的应用' : '暂无应用'}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
applications.map((app) => (
|
availableApps.map((app) => (
|
||||||
<SelectItem key={app.id} value={app.id.toString()}>
|
<SelectItem key={app.id} value={app.id.toString()}>
|
||||||
{app.appName}({app.appCode})
|
{app.appName}({app.appCode})
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))
|
))
|
||||||
)}
|
);
|
||||||
|
})()}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
{mode === 'edit' && (
|
{mode === 'edit' && (
|
||||||
|
|||||||
@ -246,7 +246,9 @@ export const TeamApplicationManageDialog: React.FC<
|
|||||||
{teamApplications.map((app) => (
|
{teamApplications.map((app) => (
|
||||||
<TableRow key={app.id}>
|
<TableRow key={app.id}>
|
||||||
<TableCell className="font-medium">
|
<TableCell className="font-medium">
|
||||||
{app.applicationName || `应用 ${app.applicationId}`}
|
{app.applicationName && app.applicationCode
|
||||||
|
? `${app.applicationName}(${app.applicationCode})`
|
||||||
|
: app.applicationName || app.applicationCode || `应用 ${app.applicationId}`}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{getEnvironmentName(app.environmentId)}
|
{getEnvironmentName(app.environmentId)}
|
||||||
@ -257,10 +259,7 @@ export const TeamApplicationManageDialog: React.FC<
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{app.deployJob || '-'}</TableCell>
|
<TableCell>{app.deployJob || '-'}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{app.workflowDefinitionName ||
|
{app.workflowDefinitionName || '-'}
|
||||||
(app.workflowDefinitionId
|
|
||||||
? `工作流 ${app.workflowDefinitionId}`
|
|
||||||
: '-')}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<div className="flex items-center justify-end gap-2">
|
<div className="flex items-center justify-end gap-2">
|
||||||
@ -309,6 +308,12 @@ export const TeamApplicationManageDialog: React.FC<
|
|||||||
applications={applications}
|
applications={applications}
|
||||||
jenkinsSystems={jenkinsSystems}
|
jenkinsSystems={jenkinsSystems}
|
||||||
workflowDefinitions={workflowDefinitions}
|
workflowDefinitions={workflowDefinitions}
|
||||||
|
existingApplicationIds={
|
||||||
|
// 传递当前环境已添加的应用ID列表
|
||||||
|
teamApplications
|
||||||
|
.filter(app => app.environmentId === editingEnvironment.id)
|
||||||
|
.map(app => app.applicationId)
|
||||||
|
}
|
||||||
onSave={handleSaveApplication}
|
onSave={handleSaveApplication}
|
||||||
onLoadBranches={handleLoadBranches}
|
onLoadBranches={handleLoadBranches}
|
||||||
onLoadJenkinsJobs={handleLoadJenkinsJobs}
|
onLoadJenkinsJobs={handleLoadJenkinsJobs}
|
||||||
@ -320,7 +325,11 @@ export const TeamApplicationManageDialog: React.FC<
|
|||||||
open={deleteDialogOpen}
|
open={deleteDialogOpen}
|
||||||
onOpenChange={setDeleteDialogOpen}
|
onOpenChange={setDeleteDialogOpen}
|
||||||
title="确认删除"
|
title="确认删除"
|
||||||
description={`确定要删除应用配置 "${deletingApp?.applicationName || `应用 ${deletingApp?.applicationId}`}" 吗?此操作无法撤销。`}
|
description={`确定要删除应用配置 "${
|
||||||
|
deletingApp?.applicationName && deletingApp?.applicationCode
|
||||||
|
? `${deletingApp.applicationName}(${deletingApp.applicationCode})`
|
||||||
|
: deletingApp?.applicationName || deletingApp?.applicationCode || `应用 ${deletingApp?.applicationId}`
|
||||||
|
}" 吗?此操作无法撤销。`}
|
||||||
item={deletingApp}
|
item={deletingApp}
|
||||||
onConfirm={handleConfirmDelete}
|
onConfirm={handleConfirmDelete}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
|
|||||||
@ -63,7 +63,7 @@ const formSchema = z.object({
|
|||||||
environmentId: z.number().min(1, '请选择环境'),
|
environmentId: z.number().min(1, '请选择环境'),
|
||||||
approvalRequired: z.boolean().default(false),
|
approvalRequired: z.boolean().default(false),
|
||||||
approverUserIds: z.array(z.number()).default([]),
|
approverUserIds: z.array(z.number()).default([]),
|
||||||
notificationChannelId: z.number().optional(),
|
notificationChannelId: z.number().nullish(),
|
||||||
notificationEnabled: z.boolean().default(false),
|
notificationEnabled: z.boolean().default(false),
|
||||||
requireCodeReview: z.boolean().default(false),
|
requireCodeReview: z.boolean().default(false),
|
||||||
remark: z.string().max(100, '备注最多100个字符').optional(),
|
remark: z.string().max(100, '备注最多100个字符').optional(),
|
||||||
@ -253,6 +253,7 @@ export const TeamEnvironmentConfigDialog: React.FC<
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (data: FormData) => {
|
const handleSubmit = async (data: FormData) => {
|
||||||
|
console.log('表单提交开始', data);
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
@ -266,6 +267,8 @@ export const TeamEnvironmentConfigDialog: React.FC<
|
|||||||
remark: data.remark,
|
remark: data.remark,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log('提交数据', payload);
|
||||||
|
|
||||||
if (configId) {
|
if (configId) {
|
||||||
// 更新已有配置
|
// 更新已有配置
|
||||||
await updateTeamEnvironmentConfig(configId, payload);
|
await updateTeamEnvironmentConfig(configId, payload);
|
||||||
@ -285,6 +288,7 @@ export const TeamEnvironmentConfigDialog: React.FC<
|
|||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
onSuccess?.();
|
onSuccess?.();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
console.error('保存失败', error);
|
||||||
toast({
|
toast({
|
||||||
title: '保存失败',
|
title: '保存失败',
|
||||||
description: error.message || '保存环境配置失败',
|
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 (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent className="max-w-2xl">
|
||||||
@ -304,17 +315,16 @@ export const TeamEnvironmentConfigDialog: React.FC<
|
|||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<DialogBody>
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
|
<DialogBody>
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
|
</DialogBody>
|
||||||
) : (
|
) : (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<DialogBody>
|
||||||
onSubmit={form.handleSubmit(handleSubmit)}
|
<div className="space-y-6">
|
||||||
className="space-y-6"
|
|
||||||
>
|
|
||||||
{/* 环境选择 */}
|
{/* 环境选择 */}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@ -383,6 +393,7 @@ export const TeamEnvironmentConfigDialog: React.FC<
|
|||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Button
|
<Button
|
||||||
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
@ -409,7 +420,8 @@ export const TeamEnvironmentConfigDialog: React.FC<
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
field.onChange(field.value?.filter(id => id !== user.id) || []);
|
field.onChange(field.value?.filter(id => id !== user.id) || []);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -571,13 +583,12 @@ export const TeamEnvironmentConfigDialog: React.FC<
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</form>
|
</div>
|
||||||
</Form>
|
|
||||||
)}
|
|
||||||
</DialogBody>
|
</DialogBody>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => onOpenChange(false)}
|
onClick={() => onOpenChange(false)}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
@ -585,15 +596,17 @@ export const TeamEnvironmentConfigDialog: React.FC<
|
|||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={form.handleSubmit(handleSubmit)}
|
type="button"
|
||||||
disabled={submitting}
|
onClick={handleSaveClick}
|
||||||
|
disabled={submitting || loading}
|
||||||
>
|
>
|
||||||
{submitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
{submitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||||
保存
|
保存
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -38,7 +38,7 @@ const FormDataDetail: React.FC = () => {
|
|||||||
|
|
||||||
// 返回列表
|
// 返回列表
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
navigate('/workflow/form/data');
|
navigate(-1); // 返回上一页,避免硬编码路径
|
||||||
};
|
};
|
||||||
|
|
||||||
// 状态徽章
|
// 状态徽章
|
||||||
|
|||||||
@ -81,7 +81,7 @@ const FormDesignerPage: React.FC = () => {
|
|||||||
|
|
||||||
// 返回列表
|
// 返回列表
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
navigate('/workflow/form');
|
navigate(-1); // 返回上一页,避免硬编码路径
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -180,7 +180,7 @@ const WorkflowDesignInner: React.FC = () => {
|
|||||||
}, [getNodes, getEdges, saveWorkflow, currentWorkflowId, workflowTitle, workflowDefinition]);
|
}, [getNodes, getEdges, saveWorkflow, currentWorkflowId, workflowTitle, workflowDefinition]);
|
||||||
|
|
||||||
const handleBack = useCallback(() => {
|
const handleBack = useCallback(() => {
|
||||||
navigate('/workflow/definition');
|
navigate(-1); // 返回上一页,避免硬编码路径
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
// 预览表单
|
// 预览表单
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user