deploy-ease-platform/frontend/src/pages/Dashboard/index.tsx
2025-11-02 23:39:26 +08:00

387 lines
17 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, useEffect, useRef } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import {
Package,
Shield,
Loader2,
Users,
Server,
CheckCircle2,
} from "lucide-react";
import { useToast } from '@/components/ui/use-toast';
import { getDeployEnvironments, startDeployment } from './service';
import { ApplicationCard } from './components/ApplicationCard';
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());
const [isInitialLoad, setIsInitialLoad] = useState(true);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
// 加载部署环境数据
const loadData = React.useCallback(async (showLoading = false) => {
try {
if (showLoading) {
setLoading(true);
}
const response = await getDeployEnvironments();
if (response && response.teams) {
const prevTeamId = currentTeamId;
const prevEnvId = currentEnvId;
setTeams(response.teams);
if (isInitialLoad) {
// 首次加载:默认选中第一个团队和第一个环境
if (response.teams.length > 0) {
setCurrentTeamId(response.teams[0].teamId);
if (response.teams[0].environments.length > 0) {
setCurrentEnvId(response.teams[0].environments[0].environmentId);
}
}
setIsInitialLoad(false);
} else {
// 后续刷新:保持当前选中的团队和环境
// 如果之前选中的团队/环境仍然存在,保持选中
if (prevTeamId !== null) {
const teamExists = response.teams.some(t => t.teamId === prevTeamId);
if (teamExists) {
setCurrentTeamId(prevTeamId);
if (prevEnvId !== null) {
const team = response.teams.find(t => t.teamId === prevTeamId);
const envExists = team?.environments.some(e => e.environmentId === prevEnvId);
if (envExists) {
setCurrentEnvId(prevEnvId);
} else if (team && team.environments.length > 0) {
// 如果之前的环境不存在了,选择第一个环境
setCurrentEnvId(team.environments[0].environmentId);
}
}
} else if (response.teams.length > 0) {
// 如果之前的团队不存在了,选择第一个团队
setCurrentTeamId(response.teams[0].teamId);
if (response.teams[0].environments.length > 0) {
setCurrentEnvId(response.teams[0].environments[0].environmentId);
}
}
}
// 检查部署状态,如果应用不再是 RUNNING 状态,从 deploying 状态中移除
setDeploying((prevDeploying) => {
const newDeploying = new Set(prevDeploying);
let hasChanges = false;
// 遍历所有团队、环境、应用,检查部署状态
response.teams.forEach(team => {
team.environments.forEach(env => {
env.applications.forEach(app => {
// 如果应用在 deploying 状态中,但最新状态不是 RUNNING则移除
if (newDeploying.has(app.teamApplicationId)) {
const latestStatus = app.deployStatistics?.latestStatus;
if (latestStatus && latestStatus !== 'RUNNING') {
newDeploying.delete(app.teamApplicationId);
hasChanges = true;
}
}
});
});
});
return hasChanges ? newDeploying : prevDeploying;
});
}
}
} catch (error) {
console.error('加载数据失败:', error);
// 只在首次加载失败时显示错误提示
if (isInitialLoad) {
toast({
variant: 'destructive',
title: '加载失败',
description: '无法加载部署环境数据,请稍后重试',
});
}
} finally {
if (showLoading) {
setLoading(false);
}
}
}, [currentTeamId, currentEnvId, isInitialLoad, toast]);
// 首次加载数据
useEffect(() => {
loadData(true);
}, []);
// 定时刷新数据每5秒
useEffect(() => {
// 只在非首次加载后开始轮询
if (isInitialLoad) return;
// 启动定时器的函数
const startPolling = () => {
// 清除旧的定时器
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
// 立即刷新一次
loadData(false);
// 设置新的定时器
intervalRef.current = setInterval(() => {
loadData(false);
}, 5000);
};
// 停止定时器的函数
const stopPolling = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
// 页面可见性检测:当页面隐藏时暂停轮询,显示时恢复
const handleVisibilityChange = () => {
if (document.hidden) {
stopPolling();
} else {
startPolling();
}
};
// 启动轮询
startPolling();
// 监听页面可见性变化
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
stopPolling();
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [isInitialLoad, loadData]);
// 切换团队时,自动选中第一个环境
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) => {
if (!currentEnv) return;
// 立即显示部署中状态
setDeploying((prev) => new Set(prev).add(app.teamApplicationId));
try {
await startDeployment(app.teamApplicationId);
toast({
title: currentEnv.requiresApproval ? '部署申请已提交' : '部署任务已创建',
description: currentEnv.requiresApproval
? '您的部署申请已提交,等待审批人审核'
: '部署任务已成功创建并开始执行',
});
// 接口成功后,保持部署中状态,等待自动刷新更新实际状态
// deploying 状态会在 loadData 中根据实际部署状态自动清除
} catch (error: any) {
// 接口失败时,立即清除部署中状态
setDeploying((prev) => {
const newSet = new Set(prev);
newSet.delete(app.teamApplicationId);
return newSet;
});
toast({
variant: 'destructive',
title: '操作失败',
description: error.response?.data?.message || '部署失败,请稍后重试',
});
}
};
// 获取当前团队和环境
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) => (
<ApplicationCard
key={app.teamApplicationId}
app={app}
environment={currentEnv!}
onDeploy={handleDeploy}
isDeploying={deploying.has(app.teamApplicationId)}
/>
))}
</div>
)}
</CardContent>
</Card>
)
)}
</div>
);
};
export default Dashboard;