增加代码编辑器表单组件
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 { Button } from "@/components/ui/button";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
@ -19,7 +19,8 @@ import {
|
|||||||
Hash,
|
Hash,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { formatDuration, formatTime, getStatusIcon, getStatusText } from '../utils/dashboardUtils';
|
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 {
|
interface ApplicationCardProps {
|
||||||
app: ApplicationConfig;
|
app: ApplicationConfig;
|
||||||
@ -34,6 +35,14 @@ export const ApplicationCard: React.FC<ApplicationCardProps> = ({
|
|||||||
onDeploy,
|
onDeploy,
|
||||||
isDeploying,
|
isDeploying,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [selectedDeployRecordId, setSelectedDeployRecordId] = useState<number | null>(null);
|
||||||
|
const [flowModalOpen, setFlowModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleDeployRecordClick = (record: DeployRecord) => {
|
||||||
|
setSelectedDeployRecordId(record.id);
|
||||||
|
setFlowModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col p-3 rounded-lg border hover:bg-accent/50 transition-colors">
|
<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")} />
|
<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>
|
<span className={cn("text-[10px] font-semibold", color)}>{getStatusText(record.status)}</span>
|
||||||
</div>
|
</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" />
|
<Hash className="h-2.5 w-2.5 text-muted-foreground" />
|
||||||
<span className="text-[10px] text-muted-foreground font-mono">{record.id}</span>
|
<span className="text-[10px] text-muted-foreground font-mono hover:text-primary transition-colors">
|
||||||
</div>
|
{record.id}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
{record.deployBy && (
|
{record.deployBy && (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<User className="h-2.5 w-2.5 text-muted-foreground" />
|
<User className="h-2.5 w-2.5 text-muted-foreground" />
|
||||||
@ -335,6 +350,13 @@ export const ApplicationCard: React.FC<ApplicationCardProps> = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{/* 部署流程图模态框 */}
|
||||||
|
<DeployFlowGraphModal
|
||||||
|
open={flowModalOpen}
|
||||||
|
deployRecordId={selectedDeployRecordId}
|
||||||
|
onOpenChange={setFlowModalOpen}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -157,11 +157,104 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
|
|||||||
return instance ? instance.status : 'NOT_STARTED';
|
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(() => {
|
const flowNodes: Node[] = useMemo(() => {
|
||||||
if (!flowData?.graph?.nodes) return [];
|
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. 匹配执行状态
|
// 1. 匹配执行状态
|
||||||
const instance = nodeInstanceMap.get(node.id);
|
const instance = nodeInstanceMap.get(node.id);
|
||||||
const status = instance ? instance.status : 'NOT_STARTED';
|
const status = instance ? instance.status : 'NOT_STARTED';
|
||||||
@ -171,16 +264,15 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
|
|||||||
return {
|
return {
|
||||||
id: node.id,
|
id: node.id,
|
||||||
type: 'default',
|
type: 'default',
|
||||||
// 2. 使用后端返回的 position(不要重新计算)
|
// 自动水平布局
|
||||||
position: node.position,
|
position: {
|
||||||
|
x: startX + index * horizontalSpacing,
|
||||||
|
y: startY,
|
||||||
|
},
|
||||||
data: {
|
data: {
|
||||||
nodeName: node.nodeName,
|
nodeName: node.nodeName,
|
||||||
nodeType: node.nodeType,
|
nodeType: node.nodeType,
|
||||||
status: status,
|
status: status,
|
||||||
startTime: instance?.startTime || null,
|
|
||||||
endTime: instance?.endTime || null,
|
|
||||||
// 3. 显示执行结果(如果有)
|
|
||||||
outputs: instance?.outputs || null,
|
|
||||||
},
|
},
|
||||||
style: {
|
style: {
|
||||||
background: colors.bg,
|
background: colors.bg,
|
||||||
@ -189,7 +281,7 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [flowData, nodeInstanceMap]);
|
}, [flowData, nodeInstanceMap, calculateNodeOrder]);
|
||||||
|
|
||||||
// 判断边是否已执行
|
// 判断边是否已执行
|
||||||
const isEdgeExecuted = (sourceNodeId: string): boolean => {
|
const isEdgeExecuted = (sourceNodeId: string): boolean => {
|
||||||
|
|||||||
@ -14,3 +14,10 @@ export const getDeployEnvironments = () =>
|
|||||||
*/
|
*/
|
||||||
export const startDeployment = (teamApplicationId: number) =>
|
export const startDeployment = (teamApplicationId: number) =>
|
||||||
request.post<StartDeploymentResponse>(`${DEPLOY_URL}/execute`, { teamApplicationId });
|
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 {
|
export interface DeployStatistics {
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
@ -88,3 +97,61 @@ export interface StartDeploymentResponse {
|
|||||||
processKey: string;
|
processKey: string;
|
||||||
startTime?: 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,
|
Loader2,
|
||||||
Clock,
|
Clock,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
|
CircleDot,
|
||||||
|
Ban,
|
||||||
|
StopCircle,
|
||||||
type LucideIcon
|
type LucideIcon
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
@ -64,16 +67,24 @@ export const formatTime = (timeStr?: string): string => {
|
|||||||
*/
|
*/
|
||||||
export const getStatusIcon = (status?: string): { icon: LucideIcon; color: string } => {
|
export const getStatusIcon = (status?: string): { icon: LucideIcon; color: string } => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'SUCCESS':
|
case 'CREATED':
|
||||||
return { icon: CheckCircle2, color: 'text-green-600' };
|
return { icon: CircleDot, color: 'text-blue-500' };
|
||||||
case 'PARTIAL_SUCCESS':
|
case 'PENDING_APPROVAL':
|
||||||
return { icon: AlertCircle, color: 'text-amber-600' };
|
return { icon: Clock, color: 'text-orange-500' };
|
||||||
case 'FAILED':
|
|
||||||
return { icon: XCircle, color: 'text-red-600' };
|
|
||||||
case 'RUNNING':
|
case 'RUNNING':
|
||||||
return { icon: Loader2, color: 'text-blue-600' };
|
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':
|
case 'CANCELLED':
|
||||||
return { icon: Clock, color: 'text-gray-400' };
|
return { icon: Clock, color: 'text-gray-400' };
|
||||||
|
case 'TERMINATED':
|
||||||
|
return { icon: StopCircle, color: 'text-gray-500' };
|
||||||
default:
|
default:
|
||||||
return { icon: Clock, color: 'text-gray-400' };
|
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 => {
|
export const getStatusText = (status?: string): string => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'SUCCESS':
|
case 'CREATED':
|
||||||
return '成功';
|
return '已创建';
|
||||||
case 'PARTIAL_SUCCESS':
|
case 'PENDING_APPROVAL':
|
||||||
return '部分成功';
|
return '待审批';
|
||||||
case 'FAILED':
|
|
||||||
return '失败';
|
|
||||||
case 'RUNNING':
|
case 'RUNNING':
|
||||||
return '运行中';
|
return '运行中';
|
||||||
|
case 'SUCCESS':
|
||||||
|
return '部署成功';
|
||||||
|
case 'FAILED':
|
||||||
|
return '部署失败';
|
||||||
|
case 'PARTIAL_SUCCESS':
|
||||||
|
return '部分成功';
|
||||||
|
case 'REJECTED':
|
||||||
|
return '审批被拒绝';
|
||||||
case 'CANCELLED':
|
case 'CANCELLED':
|
||||||
return '已取消';
|
return '已取消';
|
||||||
|
case 'TERMINATED':
|
||||||
|
return '已终止';
|
||||||
default:
|
default:
|
||||||
return '未知';
|
return '未知';
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user