320 lines
16 KiB
TypeScript
320 lines
16 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||
import { cn } from "@/lib/utils";
|
||
import {
|
||
Package,
|
||
Shield,
|
||
Loader2,
|
||
Rocket,
|
||
GitBranch,
|
||
Users,
|
||
Server,
|
||
CheckCircle2
|
||
} from "lucide-react";
|
||
import { useToast } from '@/components/ui/use-toast';
|
||
import { getDeployEnvironments, startDeployment } from './service';
|
||
import type { DeployTeam, ApplicationConfig } from './types';
|
||
|
||
const LoadingState = () => (
|
||
<div className="flex-1 p-8">
|
||
<div className="flex flex-col items-center justify-center min-h-[400px]">
|
||
<Loader2 className="h-8 w-8 animate-spin text-primary mb-4"/>
|
||
<p className="text-sm text-muted-foreground">加载中,请稍候...</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
const Dashboard: React.FC = () => {
|
||
const { toast } = useToast();
|
||
const [loading, setLoading] = useState(true);
|
||
const [teams, setTeams] = useState<DeployTeam[]>([]);
|
||
const [currentTeamId, setCurrentTeamId] = useState<number | null>(null);
|
||
const [currentEnvId, setCurrentEnvId] = useState<number | null>(null);
|
||
const [deploying, setDeploying] = useState<Set<number>>(new Set());
|
||
|
||
// 加载部署环境数据
|
||
useEffect(() => {
|
||
const loadData = async () => {
|
||
try {
|
||
setLoading(true);
|
||
const response = await getDeployEnvironments();
|
||
|
||
if (response && response.teams) {
|
||
setTeams(response.teams);
|
||
|
||
// 默认选中第一个团队和第一个环境
|
||
if (response.teams.length > 0) {
|
||
setCurrentTeamId(response.teams[0].teamId);
|
||
|
||
if (response.teams[0].environments.length > 0) {
|
||
setCurrentEnvId(response.teams[0].environments[0].environmentId);
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('加载数据失败:', error);
|
||
toast({
|
||
variant: 'destructive',
|
||
title: '加载失败',
|
||
description: '无法加载部署环境数据,请稍后重试',
|
||
});
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
loadData();
|
||
}, [toast]);
|
||
|
||
// 切换团队时,自动选中第一个环境
|
||
const handleTeamChange = (teamId: string) => {
|
||
const newTeamId = Number(teamId);
|
||
setCurrentTeamId(newTeamId);
|
||
|
||
const team = teams.find(t => t.teamId === newTeamId);
|
||
if (team && team.environments.length > 0) {
|
||
setCurrentEnvId(team.environments[0].environmentId);
|
||
} else {
|
||
setCurrentEnvId(null);
|
||
}
|
||
};
|
||
|
||
// 处理部署
|
||
const handleDeploy = async (app: ApplicationConfig, requiresApproval: boolean) => {
|
||
try {
|
||
setDeploying((prev) => new Set(prev).add(app.teamApplicationId));
|
||
|
||
await startDeployment(app.teamApplicationId);
|
||
|
||
toast({
|
||
title: requiresApproval ? '部署申请已提交' : '部署任务已创建',
|
||
description: requiresApproval
|
||
? '您的部署申请已提交,等待审批人审核'
|
||
: '部署任务已成功创建并开始执行',
|
||
});
|
||
} catch (error: any) {
|
||
toast({
|
||
variant: 'destructive',
|
||
title: '操作失败',
|
||
description: error.response?.data?.message || '部署失败,请稍后重试',
|
||
});
|
||
} finally {
|
||
setDeploying((prev) => {
|
||
const newSet = new Set(prev);
|
||
newSet.delete(app.teamApplicationId);
|
||
return newSet;
|
||
});
|
||
}
|
||
};
|
||
|
||
// 获取当前团队和环境
|
||
const currentTeam = teams.find(t => t.teamId === currentTeamId);
|
||
const currentEnv = currentTeam?.environments.find(e => e.environmentId === currentEnvId);
|
||
const currentApps = currentEnv?.applications || [];
|
||
|
||
if (loading) {
|
||
return <LoadingState/>;
|
||
}
|
||
|
||
if (teams.length === 0) {
|
||
return (
|
||
<div className="flex-1 p-8">
|
||
<h2 className="text-3xl font-bold tracking-tight mb-6">部署管理</h2>
|
||
<Card>
|
||
<CardContent className="flex flex-col items-center justify-center py-12">
|
||
<Package className="h-12 w-12 text-muted-foreground mb-4" />
|
||
<p className="text-sm text-muted-foreground">您还没有加入任何团队</p>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="flex-1 p-8 space-y-6">
|
||
{/* 页面标题和团队选择器 */}
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h2 className="text-3xl font-bold tracking-tight">部署管理</h2>
|
||
<p className="text-muted-foreground mt-1">
|
||
管理和执行应用部署任务
|
||
</p>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-3">
|
||
<div className="flex items-center gap-2">
|
||
<Users className="h-4 w-4 text-muted-foreground" />
|
||
<span className="text-sm text-muted-foreground">当前团队:</span>
|
||
</div>
|
||
<Select value={currentTeamId?.toString()} onValueChange={handleTeamChange}>
|
||
<SelectTrigger className="w-[200px]">
|
||
<SelectValue placeholder="选择团队" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{teams.map((team) => (
|
||
<SelectItem key={team.teamId} value={team.teamId.toString()}>
|
||
{team.teamName}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 当前团队信息 */}
|
||
{currentTeam && (
|
||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||
<span className="font-medium text-foreground">{currentTeam.teamName}</span>
|
||
{currentTeam.teamRole && (
|
||
<>
|
||
<span>·</span>
|
||
<span>{currentTeam.teamRole}</span>
|
||
</>
|
||
)}
|
||
{currentTeam.description && (
|
||
<>
|
||
<span>·</span>
|
||
<span>{currentTeam.description}</span>
|
||
</>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* 环境切换和应用列表 */}
|
||
{currentTeam && (
|
||
currentTeam.environments.length === 0 ? (
|
||
<Card>
|
||
<CardContent className="flex flex-col items-center justify-center py-16">
|
||
<Server className="h-16 w-16 text-muted-foreground mb-4" />
|
||
<h3 className="text-lg font-semibold mb-2">暂无部署环境</h3>
|
||
<p className="text-sm text-muted-foreground">
|
||
当前团队「{currentTeam.teamName}」还没有配置任何部署环境
|
||
</p>
|
||
</CardContent>
|
||
</Card>
|
||
) : (
|
||
<Card>
|
||
<CardHeader className="border-b">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-4">
|
||
<CardTitle>选择部署环境</CardTitle>
|
||
<Select value={currentEnvId?.toString()} onValueChange={(value) => setCurrentEnvId(Number(value))}>
|
||
<SelectTrigger className="w-[200px]">
|
||
<SelectValue placeholder="选择环境" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{currentTeam.environments.map((env) => (
|
||
<SelectItem key={env.environmentId} value={env.environmentId.toString()}>
|
||
<div className="flex items-center gap-2">
|
||
{env.requiresApproval ? (
|
||
<Shield className="h-3 w-3 text-amber-600" />
|
||
) : (
|
||
<CheckCircle2 className="h-3 w-3 text-green-600" />
|
||
)}
|
||
{env.environmentName}
|
||
<span className="text-xs text-muted-foreground">({env.applications.length})</span>
|
||
</div>
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div className="flex items-center gap-2 text-sm text-muted-foreground min-h-[20px]">
|
||
{currentEnv && currentEnv.requiresApproval && currentEnv.approvers.length > 0 ? (
|
||
<>
|
||
<Users className="h-4 w-4" />
|
||
<span>审批人: {currentEnv.approvers.map((a) => a.realName).join('、')}</span>
|
||
</>
|
||
) : (
|
||
<span className="opacity-0">占位</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="pt-6">
|
||
|
||
{/* 应用列表 */}
|
||
{currentApps.length === 0 ? (
|
||
<div className="flex flex-col items-center justify-center py-16">
|
||
<Package className="h-16 w-16 text-muted-foreground mb-4" />
|
||
<h3 className="text-lg font-semibold mb-2">暂无可部署应用</h3>
|
||
<p className="text-sm text-muted-foreground">
|
||
环境「{currentEnv?.environmentName}」暂未配置任何应用
|
||
</p>
|
||
</div>
|
||
) : (
|
||
<div className="grid grid-cols-6 gap-3">
|
||
{currentApps.map((app) => {
|
||
const isDeploying = deploying.has(app.teamApplicationId);
|
||
|
||
return (
|
||
<div
|
||
key={app.teamApplicationId}
|
||
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">
|
||
<h4 className="font-semibold text-sm mb-1 truncate">{app.applicationName}</h4>
|
||
<code className="text-xs text-muted-foreground truncate block">
|
||
{app.applicationCode}
|
||
</code>
|
||
</div>
|
||
</div>
|
||
|
||
<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" />
|
||
<code className="truncate font-mono">{app.branch}</code>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-1.5">
|
||
<Rocket className="h-3 w-3 shrink-0" />
|
||
<span className="truncate">{app.workflowDefinitionName || '未配置'}</span>
|
||
</div>
|
||
|
||
{app.deploySystemName && (
|
||
<div className="flex items-center gap-1.5">
|
||
<Server className="h-3 w-3 shrink-0" />
|
||
<span className="truncate">{app.deploySystemName}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<Button
|
||
onClick={() => handleDeploy(app, currentEnv?.requiresApproval || false)}
|
||
disabled={!currentEnv?.enabled || isDeploying}
|
||
size="sm"
|
||
className={cn(
|
||
'w-full text-xs h-8',
|
||
currentEnv?.requiresApproval && 'bg-amber-600 hover:bg-amber-700'
|
||
)}
|
||
>
|
||
{isDeploying ? (
|
||
<>
|
||
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
|
||
处理中
|
||
</>
|
||
) : (
|
||
<>
|
||
<Rocket className="h-3 w-3 mr-1" />
|
||
{currentEnv?.requiresApproval ? '申请部署' : '立即部署'}
|
||
</>
|
||
)}
|
||
</Button>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
)
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default Dashboard;
|