394 lines
20 KiB
TypeScript
394 lines
20 KiB
TypeScript
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>
|
||
);
|
||
};
|
||
|