增加代码编辑器表单组件

This commit is contained in:
dengqichen 2025-11-04 22:37:13 +08:00
parent c49f345a58
commit d99c46eb48
5 changed files with 234 additions and 27 deletions

View File

@ -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<ApplicationCardProps> = ({
onDeploy,
isDeploying,
}) => {
const [selectedDeployRecordId, setSelectedDeployRecordId] = useState<number | null>(null);
const [flowModalOpen, setFlowModalOpen] = useState(false);
const handleDeployRecordClick = (record: DeployRecord) => {
setSelectedDeployRecordId(record.id);
setFlowModalOpen(true);
};
return (
<div className="flex flex-col p-3 rounded-lg border hover:bg-accent/50 transition-colors">
{/* 应用基本信息 */}
@ -237,10 +246,16 @@ export const ApplicationCard: React.FC<ApplicationCardProps> = ({
<StatusIcon className={cn("h-3.5 w-3.5 shrink-0", color, record.status === 'RUNNING' && "animate-spin")} />
<span className={cn("text-[10px] font-semibold", color)}>{getStatusText(record.status)}</span>
</div>
<div className="flex items-center gap-1 px-1.5 py-0.5 rounded bg-background/50">
<button
onClick={() => handleDeployRecordClick(record)}
className="flex items-center gap-1 px-1.5 py-0.5 rounded bg-background/50 hover:bg-background/80 transition-colors cursor-pointer"
title="点击查看工作流详情"
>
<Hash className="h-2.5 w-2.5 text-muted-foreground" />
<span className="text-[10px] text-muted-foreground font-mono">{record.id}</span>
</div>
<span className="text-[10px] text-muted-foreground font-mono hover:text-primary transition-colors">
{record.id}
</span>
</button>
{record.deployBy && (
<div className="flex items-center gap-1">
<User className="h-2.5 w-2.5 text-muted-foreground" />
@ -335,6 +350,13 @@ export const ApplicationCard: React.FC<ApplicationCardProps> = ({
</>
)}
</Button>
{/* 部署流程图模态框 */}
<DeployFlowGraphModal
open={flowModalOpen}
deployRecordId={selectedDeployRecordId}
onOpenChange={setFlowModalOpen}
/>
</div>
);
};

View File

@ -157,11 +157,104 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
return instance ? instance.status : 'NOT_STARTED';
};
// 转换为 React Flow 节点(使用后端返回的 position
// 计算节点拓扑顺序(用于自动布局)
const calculateNodeOrder = useMemo(() => {
if (!flowData?.graph?.nodes || !flowData?.graph?.edges) {
return new Map<string, number>();
}
const edges = flowData.graph.edges;
const nodes = flowData.graph.nodes;
const nodeIds = new Set(nodes.map(n => n.id));
// 构建图的邻接表和入度
const adjacencyList = new Map<string, string[]>();
const inDegree = new Map<string, number>();
// 初始化
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<string>();
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<string, number>();
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<DeployFlowGraphModalProps> = ({
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<DeployFlowGraphModalProps> = ({
},
};
});
}, [flowData, nodeInstanceMap]);
}, [flowData, nodeInstanceMap, calculateNodeOrder]);
// 判断边是否已执行
const isEdgeExecuted = (sourceNodeId: string): boolean => {

View File

@ -14,3 +14,10 @@ export const getDeployEnvironments = () =>
*/
export const startDeployment = (teamApplicationId: number) =>
request.post<StartDeploymentResponse>(`${DEPLOY_URL}/execute`, { teamApplicationId });
/**
*
* @param deployRecordId ID
*/
export const getDeployRecordFlowGraph = (deployRecordId: number) =>
request.get<import('./types').DeployRecordFlowGraph>(`${DEPLOY_URL}/records/${deployRecordId}/flow-graph`);

View File

@ -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<string, any>;
}
/**
*
*/
export interface WorkflowDefinitionGraphNode {
id: string;
nodeCode?: string;
nodeType: string;
nodeName: string;
position: {
x: number;
y: number;
};
configs?: Record<string, any>;
inputMapping?: Record<string, any>;
outputs?: any[];
}
/**
*
*/
export interface WorkflowDefinitionGraphEdge {
from: string;
to: string;
config?: Record<string, any>;
}
/**
*
*/
export interface WorkflowDefinitionGraph {
nodes: WorkflowDefinitionGraphNode[];
edges: WorkflowDefinitionGraphEdge[];
}
/**
*
*/
export interface DeployRecordFlowGraph {
deployRecordId: number;
workflowInstanceId: number;
processInstanceId: string;
deployStatus: DeployStatus;
graph: WorkflowDefinitionGraph;
nodeInstances: WorkflowNodeInstance[];
}

View File

@ -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 '未知';
}