增加代码编辑器表单组件
This commit is contained in:
parent
c49f345a58
commit
d99c46eb48
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 => {
|
||||
|
||||
@ -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`);
|
||||
|
||||
@ -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[];
|
||||
}
|
||||
|
||||
@ -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 '未知';
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user