This commit is contained in:
dengqichen 2024-12-27 17:40:23 +08:00
parent 4c3342f0fa
commit 82e36f0b39
3 changed files with 375 additions and 217 deletions

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress";
@ -20,90 +20,195 @@ import {
ChevronRight, ChevronRight,
CheckCircle, CheckCircle,
AlertTriangle, AlertTriangle,
XCircle XCircle,
Loader2,
ServerCrash,
Server
} from "lucide-react"; } from "lucide-react";
import { getEnvironmentList } from '@/pages/Deploy/Environment/List/service';
import { getDevelopmentLanguages } from '@/pages/Deploy/Application/List/service';
import { getDeploymentConfigPage } from '@/pages/Deploy/Deployment/List/service';
import type { Environment } from '@/pages/Deploy/Environment/List/types';
import type { DevelopmentLanguageType } from '@/pages/Deploy/Application/List/types';
import type { DeploymentConfig, DeploymentConfigQueryParams } from '@/pages/Deploy/Deployment/List/types';
import type { Page } from '@/types/base';
// Mock环境数据 type EnvironmentStatus = 'success' | 'warning' | 'error';
const environments = [
{
id: "dev",
name: "开发环境",
projectCount: 10,
status: "success",
lastDeployment: "10分钟前",
cpu: 60,
memory: 70,
storage: 50
},
{
id: "test",
name: "测试环境",
projectCount: 8,
status: "warning",
lastDeployment: "1小时前",
cpu: 80,
memory: 75,
storage: 60
},
{
id: "staging",
name: "预发环境",
projectCount: 5,
status: "success",
lastDeployment: "2小时前",
cpu: 40,
memory: 50,
storage: 30
},
{
id: "prod",
name: "生产环境",
projectCount: 12,
status: "error",
lastDeployment: "1天前",
cpu: 90,
memory: 85,
storage: 70
}
];
// Mock项目数据 // 扩展环境类型,添加监控数据
const projects = [ interface EnhancedEnvironment extends Environment {
{ projectCount: number;
id: 1, status: EnvironmentStatus;
name: "用户中心", lastDeployment: string;
code: "user-center", cpu: number;
type: "微服务", memory: number;
version: "v2.3.1", storage: number;
status: "活跃",
buildStatus: "success",
lastDeployment: "30分钟前"
},
{
id: 2,
name: "订单系统",
code: "order-system",
type: "后端服务",
version: "v1.7.0",
status: "活跃",
buildStatus: "error",
lastDeployment: "2小时前"
},
{
id: 3,
name: "前端门户",
code: "frontend-portal",
type: "前端应用",
version: "v3.1.2",
status: "活跃",
buildStatus: "running",
lastDeployment: "1天前"
} }
];
const EmptyState: React.FC<{ title: string; description: string; icon: React.ReactNode }> = ({
title,
description,
icon
}) => (
<div className="flex flex-col items-center justify-center p-8 text-center">
<div className="rounded-full bg-muted p-3 mb-4">
{icon}
</div>
<h3 className="text-lg font-semibold mb-2">{title}</h3>
<p className="text-sm text-muted-foreground max-w-sm">{description}</p>
</div>
);
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 Dashboard: React.FC = () => {
const [projectType, setProjectType] = useState(""); const [projectType, setProjectType] = useState("");
const [status, setStatus] = useState(""); const [status, setStatus] = useState("");
const [environments, setEnvironments] = useState<EnhancedEnvironment[]>([]);
const [loading, setLoading] = useState(true);
const [languages, setLanguages] = useState<DevelopmentLanguageType[]>([]);
const [deployConfigs, setDeployConfigs] = useState<DeploymentConfig[]>([]);
const [searchText, setSearchText] = useState("");
const [currentEnvId, setCurrentEnvId] = useState<number>();
// 获取环境和语言数据
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const [envResponse, langResponse] = await Promise.all([
getEnvironmentList(),
getDevelopmentLanguages()
]);
if (envResponse) {
const enrichedEnvironments = envResponse.map(env => {
const randomStatus: EnvironmentStatus = Math.random() > 0.7 ? 'warning' : 'success';
return {
...env,
projectCount: 0, // 初始化为0后续更新
status: randomStatus,
lastDeployment: '暂无部署',
cpu: Math.floor(Math.random() * 40) + 40,
memory: Math.floor(Math.random() * 30) + 50,
storage: Math.floor(Math.random() * 40) + 30,
};
});
setEnvironments(enrichedEnvironments);
if (enrichedEnvironments.length > 0) {
setCurrentEnvId(enrichedEnvironments[0].id);
}
}
if (langResponse) {
setLanguages(langResponse);
}
} catch (error) {
console.error('Failed to fetch data:', error);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
// 获取部署配置数据
useEffect(() => {
const fetchDeployConfigs = async () => {
if (!currentEnvId) return;
try {
const queryParams: DeploymentConfigQueryParams = {
pageSize: 100,
pageNum: 1,
environmentId: currentEnvId,
workflowDefinitionId: 0
};
const response = await getDeploymentConfigPage(queryParams);
if (response) {
setDeployConfigs(response.content);
// 更新环境的项目数量
setEnvironments(prevEnvs =>
prevEnvs.map(env =>
env.id === currentEnvId
? { ...env, projectCount: response.totalElements }
: env
)
);
}
} catch (error) {
console.error('Failed to fetch deployment configs:', error);
}
};
fetchDeployConfigs();
}, [currentEnvId]);
const handleSearch = async () => {
if (!currentEnvId) return;
try {
const queryParams: DeploymentConfigQueryParams = {
pageSize: 100,
pageNum: 1,
environmentId: currentEnvId,
workflowDefinitionId: 0,
enabled: status === 'active' ? true : status === 'paused' ? false : undefined
};
const response = await getDeploymentConfigPage(queryParams);
if (response) {
// 在前端过滤开发语言
const filteredConfigs = response.content.filter(config => {
if (projectType && config.languageType !== projectType) {
return false;
}
return true;
});
setDeployConfigs(filteredConfigs);
}
} catch (error) {
console.error('Failed to search deployment configs:', error);
}
};
const handleReset = () => {
setSearchText("");
setProjectType("");
setStatus("");
if (currentEnvId) {
const queryParams: DeploymentConfigQueryParams = {
pageSize: 100,
pageNum: 1,
environmentId: currentEnvId,
workflowDefinitionId: 0
};
getDeploymentConfigPage(queryParams).then(response => {
if (response) {
setDeployConfigs(response.content);
}
});
}
};
const handleTabChange = (envCode: string) => {
const env = environments.find(e => e.envCode === envCode);
if (env) {
setCurrentEnvId(env.id);
}
};
const getStatusIcon = (status: string) => { const getStatusIcon = (status: string) => {
switch (status) { switch (status) {
@ -118,13 +223,11 @@ const Dashboard: React.FC = () => {
} }
}; };
const getBuildStatusBadge = (status: string) => { const getBuildStatusBadge = (enabled: boolean) => {
const statusConfig = { const config = enabled
success: { className: "bg-green-100 text-green-800", text: "成功" }, ? { className: "bg-green-100 text-green-800", text: "活跃" }
error: { className: "bg-red-100 text-red-800", text: "失败" }, : { className: "bg-red-100 text-red-800", text: "暂停" };
running: { className: "bg-blue-100 text-blue-800", text: "进行中" }
};
const config = statusConfig[status as keyof typeof statusConfig] || statusConfig.running;
return ( return (
<Badge className={cn("rounded-full", config.className)}> <Badge className={cn("rounded-full", config.className)}>
{config.text} {config.text}
@ -132,14 +235,34 @@ const Dashboard: React.FC = () => {
); );
}; };
if (loading) {
return <LoadingState />;
}
const hasEnvironments = environments.length > 0;
const hasProjects = deployConfigs.length > 0;
return ( return (
<div className="flex-1 p-8"> <div className="flex-1 p-8">
<h2 className="text-2xl font-semibold mb-6"></h2> <h2 className="text-2xl font-semibold mb-6"></h2>
{!hasEnvironments ? (
<Card>
<CardContent>
<EmptyState
icon={<ServerCrash className="h-8 w-8 text-muted-foreground" />}
title="暂无环境数据"
description="当前系统中还没有配置任何环境。请先添加部署环境,然后开始使用部署功能。"
/>
</CardContent>
</Card>
) : (
<>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-6 mb-8"> <div className="grid grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
{environments.map((env) => ( {environments.map((env) => (
<Card key={env.id} className="hover:shadow-lg transition-shadow duration-300"> <Card key={env.id} className="hover:shadow-lg transition-shadow duration-300">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">{env.name}</CardTitle> <CardTitle className="text-sm font-medium">{env.envName}</CardTitle>
{getStatusIcon(env.status)} {getStatusIcon(env.status)}
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@ -178,34 +301,44 @@ const Dashboard: React.FC = () => {
</div> </div>
<div className="bg-white rounded-lg shadow"> <div className="bg-white rounded-lg shadow">
<Tabs defaultValue={environments[0].id} className="w-full"> <Tabs
defaultValue={environments[0]?.envCode}
className="w-full"
onValueChange={handleTabChange}
>
<div className="border-b px-6"> <div className="border-b px-6">
<TabsList className="h-16"> <TabsList className="h-16">
{environments.map((env) => ( {environments.map((env) => (
<TabsTrigger <TabsTrigger
key={env.id} key={env.id}
value={env.id} value={env.envCode}
className="px-6 py-3 data-[state=active]:border-b-2 data-[state=active]:border-blue-600" className="px-6 py-3 data-[state=active]:border-b-2 data-[state=active]:border-blue-600"
> >
{env.name} {env.envName}
</TabsTrigger> </TabsTrigger>
))} ))}
</TabsList> </TabsList>
</div> </div>
{environments.map((env) => ( {environments.map((env) => (
<TabsContent key={env.id} value={env.id} className="p-6"> <TabsContent key={env.id} value={env.envCode} className="p-6">
<div className="mb-6"> <div className="mb-6">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Input placeholder="项目名称或代码" /> <Input
placeholder="项目名称或代码"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
/>
<Select value={projectType} onValueChange={setProjectType}> <Select value={projectType} onValueChange={setProjectType}>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="项目类型" /> <SelectValue placeholder="开发语言" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="frontend"></SelectItem> {languages.map(lang => (
<SelectItem value="backend"></SelectItem> <SelectItem key={lang.code} value={lang.code}>
<SelectItem value="microservice"></SelectItem> {lang.name}
</SelectItem>
))}
</SelectContent> </SelectContent>
</Select> </Select>
<Select value={status} onValueChange={setStatus}> <Select value={status} onValueChange={setStatus}>
@ -214,47 +347,59 @@ const Dashboard: React.FC = () => {
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="active"></SelectItem> <SelectItem value="active"></SelectItem>
<SelectItem value="archived"></SelectItem>
<SelectItem value="paused"></SelectItem> <SelectItem value="paused"></SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<div className="flex space-x-2"> <div className="flex space-x-2">
<Button className="flex-1"> <Button className="flex-1" onClick={handleSearch}>
<Search className="w-4 h-4 mr-2" /> <Search className="w-4 h-4 mr-2" />
</Button> </Button>
<Button variant="outline" className="flex-1"></Button> <Button variant="outline" className="flex-1" onClick={handleReset}>
</Button>
</div> </div>
</div> </div>
</div> </div>
{!hasProjects ? (
<Card>
<CardContent>
<EmptyState
icon={<Server className="h-8 w-8 text-muted-foreground" />}
title="暂无项目数据"
description="当前环境中还没有配置任何项目。请先创建项目,然后开始部署。"
/>
</CardContent>
</Card>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{projects.map((project) => ( {deployConfigs.map((config) => (
<Card key={project.id} className="hover:shadow-md transition-shadow duration-300"> <Card key={config.id} className="hover:shadow-md transition-shadow duration-300">
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex justify-between items-start mb-2"> <div className="flex justify-between items-start mb-2">
<div> <div>
<h3 className="text-lg font-semibold">{project.name}</h3> <h3 className="text-lg font-semibold">{config.application.appName}</h3>
<p className="text-sm text-gray-500">{project.code}</p> <p className="text-sm text-gray-500">{config.application.appCode}</p>
</div> </div>
{getBuildStatusBadge(project.buildStatus)} {getBuildStatusBadge(config.enabled)}
</div> </div>
<div className="grid grid-cols-2 gap-2 text-sm mb-4"> <div className="grid grid-cols-2 gap-2 text-sm mb-4">
<div> <div>
<p className="text-gray-500"></p> <p className="text-gray-500"></p>
<p className="font-medium">{project.type}</p> <p className="font-medium">{config.buildType}</p>
</div> </div>
<div> <div>
<p className="text-gray-500"></p> <p className="text-gray-500"></p>
<p className="font-medium">{project.version}</p> <p className="font-medium">{config.languageType}</p>
</div> </div>
<div> <div>
<p className="text-gray-500"></p> <p className="text-gray-500"></p>
<p className="font-medium">{project.status}</p> <p className="font-medium">{config.publishedWorkflowDefinition?.name || '未配置'}</p>
</div> </div>
<div> <div>
<p className="text-gray-500"></p> <p className="text-gray-500"></p>
<p className="font-medium">{project.lastDeployment}</p> <p className="font-medium">{new Date(config.updateTime).toLocaleString()}</p>
</div> </div>
</div> </div>
<div className="flex justify-end space-x-2"> <div className="flex justify-end space-x-2">
@ -271,10 +416,13 @@ const Dashboard: React.FC = () => {
</Card> </Card>
))} ))}
</div> </div>
)}
</TabsContent> </TabsContent>
))} ))}
</Tabs> </Tabs>
</div> </div>
</>
)}
</div> </div>
); );
}; };

View File

@ -1,6 +1,7 @@
import request from '@/utils/request'; import request from '@/utils/request';
import type { CreateApplicationRequest, UpdateApplicationRequest, Application, ApplicationQuery } from './types'; import type { CreateApplicationRequest, UpdateApplicationRequest, Application, ApplicationQuery } from './types';
import type { Page } from '@/types/base'; import type { Page } from '@/types/base';
import {DevelopmentLanguageType} from "./types";
const BASE_URL = '/api/v1/applications'; const BASE_URL = '/api/v1/applications';
@ -31,3 +32,6 @@ export const getApplicationList = () =>
// 条件查询应用列表 // 条件查询应用列表
export const getApplicationListByCondition = (params?: ApplicationQuery) => export const getApplicationListByCondition = (params?: ApplicationQuery) =>
request.get<Application[]>(`${BASE_URL}/list`, { params }); request.get<Application[]>(`${BASE_URL}/list`, { params });
export const getDevelopmentLanguages = () =>
request.get<DevelopmentLanguageType[]>(`${BASE_URL}/development-languages`);

View File

@ -40,4 +40,10 @@ export interface ApplicationQuery extends BaseQuery {
appCode?: string; appCode?: string;
appName?: string; appName?: string;
enabled?: boolean; enabled?: boolean;
language?: string;
}
export interface DevelopmentLanguageType {
code: string;
name: string;
} }