deploy-ease-platform/frontend/src/pages/Dashboard/components/ApplicationCard.tsx
2025-11-06 17:51:55 +08:00

394 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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";
import { cn } from "@/lib/utils";
import {
Package,
GitBranch,
Rocket,
Server,
CheckCircle2,
XCircle,
TrendingUp,
Clock,
History,
Loader2,
User,
FileText,
Hash,
} from "lucide-react";
import { formatDuration, formatTime, getStatusIcon, getStatusText } from '../utils/dashboardUtils';
import type { ApplicationConfig, DeployEnvironment, DeployRecord } from '../types';
import { DeployFlowGraphModal } from './DeployFlowGraphModal';
import DeploymentFormModal from './DeploymentFormModal';
interface ApplicationCardProps {
app: ApplicationConfig;
environment: DeployEnvironment;
teamId: number;
onDeploy: (app: ApplicationConfig, remark: string) => void;
isDeploying: boolean;
}
export const ApplicationCard: React.FC<ApplicationCardProps> = ({
app,
environment,
teamId,
onDeploy,
isDeploying,
}) => {
const [selectedDeployRecordId, setSelectedDeployRecordId] = useState<number | null>(null);
const [flowModalOpen, setFlowModalOpen] = useState(false);
const [deployDialogOpen, setDeployDialogOpen] = 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">
{/* 应用基本信息 */}
<div className="flex items-start gap-2 mb-3">
<Package className="h-4 w-4 text-muted-foreground shrink-0 mt-0.5" />
<div className="flex-1 min-w-0">
{app.applicationName ? (
<h4 className="font-semibold text-sm mb-1 truncate">{app.applicationName}</h4>
) : (
<Skeleton className="h-4 w-24 mb-1" />
)}
{app.applicationCode ? (
<code className="text-xs text-muted-foreground truncate block">
{app.applicationCode}
</code>
) : (
<Skeleton className="h-3 w-20" />
)}
{app.applicationDesc && (
<p className="text-[10px] text-muted-foreground mt-1 line-clamp-2">
{app.applicationDesc}
</p>
)}
</div>
</div>
{/* Git/工作流/系统信息 */}
<div className="space-y-1.5 mb-3 text-xs text-muted-foreground">
{/* 分支 */}
<div className="flex items-center gap-1.5">
<GitBranch className="h-3 w-3 shrink-0" />
{app.branch ? (
<code className="truncate font-mono">{app.branch}</code>
) : (
<Skeleton className="h-3 w-16" />
)}
</div>
{/* 工作流 */}
<div className="flex items-center gap-1.5">
<Rocket className="h-3 w-3 shrink-0" />
{app.workflowDefinitionName ? (
<span className="truncate">{app.workflowDefinitionName}</span>
) : (
<Skeleton className="h-3 w-20" />
)}
</div>
{/* Jenkins */}
{app.deploySystemName ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-1.5 cursor-default">
<Server className="h-3 w-3 shrink-0" />
<span className="truncate">{app.deploySystemName}</span>
</div>
</TooltipTrigger>
{app.deployJob && (
<TooltipContent>
<p>{app.deploySystemName} ({app.deployJob})</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
) : (
<div className="flex items-center gap-1.5">
<Server className="h-3 w-3 shrink-0" />
<Skeleton className="h-3 w-24" />
</div>
)}
</div>
{/* 部署统计信息 */}
<div className="mb-3 pt-2 border-t border-dashed">
<div className="grid grid-cols-4 gap-1 mb-2 text-xs">
{/* 总次数 */}
<div className="text-center">
{app.deployStatistics ? (
<>
<div className="flex items-center justify-center gap-0.5">
<TrendingUp className="h-3 w-3 text-muted-foreground" />
<span className="font-semibold">{app.deployStatistics.totalCount ?? 0}</span>
</div>
<div className="text-[10px] text-muted-foreground"></div>
</>
) : (
<>
<Skeleton className="h-4 w-8 mx-auto mb-1" />
<Skeleton className="h-3 w-12 mx-auto" />
</>
)}
</div>
{/* 成功次数 */}
<div className="text-center">
{app.deployStatistics ? (
<>
<div className="flex items-center justify-center gap-0.5">
<CheckCircle2 className="h-3 w-3 text-green-600" />
<span className="font-semibold text-green-600">{app.deployStatistics.successCount ?? 0}</span>
</div>
<div className="text-[10px] text-muted-foreground"></div>
</>
) : (
<>
<Skeleton className="h-4 w-8 mx-auto mb-1" />
<Skeleton className="h-3 w-12 mx-auto" />
</>
)}
</div>
{/* 失败次数 */}
<div className="text-center">
{app.deployStatistics ? (
<>
<div className="flex items-center justify-center gap-0.5">
<XCircle className="h-3 w-3 text-red-600" />
<span className="font-semibold text-red-600">{app.deployStatistics.failedCount ?? 0}</span>
</div>
<div className="text-[10px] text-muted-foreground"></div>
</>
) : (
<>
<Skeleton className="h-4 w-8 mx-auto mb-1" />
<Skeleton className="h-3 w-12 mx-auto" />
</>
)}
</div>
{/* 运行中次数 */}
<div className="text-center">
{app.deployStatistics ? (
<>
<div className="flex items-center justify-center gap-0.5">
<Loader2 className="h-3 w-3 text-blue-600" />
<span className="font-semibold text-blue-600">{app.deployStatistics.runningCount ?? 0}</span>
</div>
<div className="text-[10px] text-muted-foreground"></div>
</>
) : (
<>
<Skeleton className="h-4 w-8 mx-auto mb-1" />
<Skeleton className="h-3 w-12 mx-auto" />
</>
)}
</div>
</div>
{/* 最近部署信息 */}
{app.deployStatistics ? (
<div className="flex items-center gap-1 text-[10px] text-muted-foreground flex-wrap">
<Clock className="h-3 w-3 shrink-0" />
{app.deployStatistics.lastDeployTime ? (
<>
<span>: {formatTime(app.deployStatistics.lastDeployTime)}</span>
{app.deployStatistics.lastDeployBy ? (
<span>by {app.deployStatistics.lastDeployBy}</span>
) : (
<Skeleton className="h-3 w-16" />
)}
{app.deployStatistics.latestStatus ? (() => {
const { icon: StatusIcon, color } = getStatusIcon(app.deployStatistics.latestStatus);
return (
<span className={cn("flex items-center gap-0.5", color)}>
<StatusIcon className={cn("h-3 w-3", app.deployStatistics.latestStatus === 'RUNNING' && "animate-spin")} />
{getStatusText(app.deployStatistics.latestStatus)}
</span>
);
})() : (
<Skeleton className="h-3 w-12" />
)}
</>
) : (
<Skeleton className="h-3 w-32" />
)}
</div>
) : (
<div className="flex items-center gap-1 text-[10px]">
<Clock className="h-3 w-3 shrink-0" />
<Skeleton className="h-3 w-32" />
</div>
)}
{/* 最近部署记录 */}
<div className="mt-2 space-y-2">
<div className="flex items-center gap-1 text-[10px] font-medium text-muted-foreground mb-1">
<History className="h-3 w-3" />
<span></span>
</div>
{app.recentDeployRecords && app.recentDeployRecords.length > 0 ? (
<>
{/* 显示实际记录最多2条 */}
{app.recentDeployRecords.slice(0, 2).map((record) => {
const { icon: StatusIcon, color } = getStatusIcon(record.status);
return (
<div key={record.id} className="p-1.5 rounded-md bg-muted/30 border border-muted/50 space-y-1">
{/* 第一行:状态、编号、部署人 */}
<div className="flex items-center gap-2 flex-wrap">
<div className="flex items-center gap-1">
<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>
<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 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" />
<span className="text-[10px] text-muted-foreground">{record.deployBy}</span>
</div>
)}
</div>
{/* 第二行:时间信息(一行显示) */}
{(record.startTime || record.endTime || record.duration) && (
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground flex-nowrap overflow-hidden">
<Clock className="h-2.5 w-2.5 shrink-0" />
<div className="flex items-center gap-1 flex-nowrap min-w-0">
{record.startTime && (
<span className="whitespace-nowrap">{formatTime(record.startTime)}</span>
)}
{record.endTime && (
<>
<span className="px-0.5 shrink-0"></span>
<span className="whitespace-nowrap">{formatTime(record.endTime)}</span>
</>
)}
{record.duration && (
<>
<span className="px-0.5 text-muted-foreground/50 shrink-0"></span>
<span className="font-medium whitespace-nowrap">{formatDuration(record.duration)}</span>
</>
)}
</div>
</div>
)}
{/* 第三行:备注 */}
{record.deployRemark && (
<div className="flex items-center gap-1 text-[10px] text-muted-foreground pt-0.5 border-t border-muted/30">
<FileText className="h-2.5 w-2.5 shrink-0" />
<span className="truncate">{record.deployRemark}</span>
</div>
)}
</div>
);
})}
{/* 如果记录少于2条用骨架屏补充 */}
{app.recentDeployRecords.length < 2 && (
Array.from({ length: 2 - app.recentDeployRecords.length }).map((_, index) => (
<div key={`skeleton-${index}`} className="p-1.5 rounded-md bg-muted/30 border border-muted/50 space-y-1">
<div className="flex items-center gap-2">
<Skeleton className="h-3.5 w-16" />
<Skeleton className="h-4 w-12" />
<Skeleton className="h-3.5 w-12 ml-auto" />
</div>
<Skeleton className="h-3 w-32" />
</div>
))
)}
</>
) : (
/* 如果没有记录显示2条骨架记录 */
Array.from({ length: 2 }).map((_, index) => (
<div key={`skeleton-${index}`} className="p-1.5 rounded-md bg-muted/30 border border-muted/50 space-y-1">
<div className="flex items-center gap-2">
<Skeleton className="h-3.5 w-16" />
<Skeleton className="h-4 w-12" />
<Skeleton className="h-3.5 w-12 ml-auto" />
</div>
<Skeleton className="h-3 w-32" />
</div>
))
)}
</div>
</div>
{/* 部署按钮 */}
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="w-full">
<Button
onClick={() => setDeployDialogOpen(true)}
disabled={!environment.enabled || !environment.canDeploy || isDeploying || app.isDeploying}
size="sm"
className={cn(
'w-full text-xs h-8',
environment.requiresApproval && 'bg-amber-600 hover:bg-amber-700'
)}
>
{(isDeploying || app.isDeploying) ? (
<>
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
</>
) : (
<>
<Rocket className="h-3 w-3 mr-1" />
{environment.requiresApproval ? '申请部署' : '立即部署'}
</>
)}
</Button>
</div>
</TooltipTrigger>
{!environment.canDeploy && (
<TooltipContent>
<p></p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
{/* 部署表单对话框 */}
<DeploymentFormModal
open={deployDialogOpen}
onClose={() => setDeployDialogOpen(false)}
app={app}
environment={environment}
teamId={teamId}
onSuccess={() => {
// 部署成功后,触发父组件的 onDeploy 回调(用于刷新数据)
onDeploy(app, '');
}}
/>
{/* 部署流程图模态框 */}
<DeployFlowGraphModal
open={flowModalOpen}
deployRecordId={selectedDeployRecordId}
onOpenChange={setFlowModalOpen}
/>
</div>
);
};