This commit is contained in:
asp_ly 2024-12-29 19:32:05 +08:00
parent 7bfc716eb5
commit 43670b8142
3 changed files with 95 additions and 112 deletions

View File

@ -16,20 +16,21 @@ import {
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { useToast } from "@/components/ui/use-toast"; import { useToast } from "@/components/ui/use-toast";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import type { JenkinsInstance, JenkinsView, SyncType } from './types'; import type { JenkinsInstance, JenkinsInstanceDTO } from './types';
import { getJenkinsInstances, getJenkinsViews, syncViews, syncJobs, syncBuilds } from './service'; import { getJenkinsInstances, getJenkinsInstance, syncViews, syncJobs, syncBuilds } from './service';
const JenkinsManagerList: React.FC = () => { const JenkinsManagerList: React.FC = () => {
const [jenkinsList, setJenkinsList] = useState<JenkinsInstance[]>([]); const [jenkinsList, setJenkinsList] = useState<JenkinsInstance[]>([]);
const [currentJenkinsId, setCurrentJenkinsId] = useState<string>(); const [currentJenkinsId, setCurrentJenkinsId] = useState<string>();
const [currentJenkins, setCurrentJenkins] = useState<JenkinsInstance>(); const [currentJenkins, setCurrentJenkins] = useState<JenkinsInstance>();
const [views, setViews] = useState<JenkinsView[]>([]); const [instanceDetails, setInstanceDetails] = useState<JenkinsInstanceDTO>();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [syncing, setSyncing] = useState<Record<SyncType, boolean>>({ const [syncing, setSyncing] = useState<Record<string, boolean>>({
views: false, views: false,
jobs: false, jobs: false,
builds: false builds: false
}); });
const [expandedViews, setExpandedViews] = useState<Record<number, boolean>>({});
const { toast } = useToast(); const { toast } = useToast();
// 获取 Jenkins 实例列表 // 获取 Jenkins 实例列表
@ -51,17 +52,17 @@ const JenkinsManagerList: React.FC = () => {
} }
}; };
// 获取视图列表 // 获取 Jenkins 实例详情
const loadViews = async () => { const loadInstanceDetails = async () => {
if (!currentJenkinsId) return; if (!currentJenkinsId) return;
setLoading(true); setLoading(true);
try { try {
const data = await getJenkinsViews(currentJenkinsId); const data = await getJenkinsInstance(currentJenkinsId);
setViews(data); setInstanceDetails(data);
} catch (error) { } catch (error) {
toast({ toast({
variant: "destructive", variant: "destructive",
title: "获取视图列表失败", title: "获取实例详情失败",
duration: 3000, duration: 3000,
}); });
} finally { } finally {
@ -74,18 +75,17 @@ const JenkinsManagerList: React.FC = () => {
setCurrentJenkinsId(id); setCurrentJenkinsId(id);
const jenkins = jenkinsList.find(j => String(j.id) === id); const jenkins = jenkinsList.find(j => String(j.id) === id);
setCurrentJenkins(jenkins); setCurrentJenkins(jenkins);
setViews([]); setInstanceDetails(undefined);
}; };
// 同步数据 // 同步数据
const handleSync = async (type: SyncType) => { const handleSync = async (type: 'views' | 'jobs' | 'builds') => {
if (!currentJenkinsId) return; if (!currentJenkinsId) return;
setSyncing(prev => ({ ...prev, [type]: true })); setSyncing(prev => ({ ...prev, [type]: true }));
try { try {
switch (type) { switch (type) {
case 'views': case 'views':
await syncViews(currentJenkinsId); await syncViews(currentJenkinsId);
await loadViews(); // 重新加载视图数据
break; break;
case 'jobs': case 'jobs':
await syncJobs(currentJenkinsId); await syncJobs(currentJenkinsId);
@ -94,7 +94,7 @@ const JenkinsManagerList: React.FC = () => {
await syncBuilds(currentJenkinsId); await syncBuilds(currentJenkinsId);
break; break;
} }
await loadJenkinsList(); // 重新加载实例<E5AE9E><E4BE8B>据以更新同步时间 await loadInstanceDetails(); // 重新加载实例详情
toast({ toast({
title: "同步成功", title: "同步成功",
duration: 3000, duration: 3000,
@ -110,13 +110,21 @@ const JenkinsManagerList: React.FC = () => {
} }
}; };
// 添加切换展开/收起的处理函数
const toggleView = (viewId: number) => {
setExpandedViews(prev => ({
...prev,
[viewId]: !prev[viewId]
}));
};
useEffect(() => { useEffect(() => {
loadJenkinsList(); loadJenkinsList();
}, []); }, []);
useEffect(() => { useEffect(() => {
if (currentJenkinsId) { if (currentJenkinsId) {
loadViews(); loadInstanceDetails();
} }
}, [currentJenkinsId]); }, [currentJenkinsId]);
@ -125,13 +133,6 @@ const JenkinsManagerList: React.FC = () => {
return time; return time;
}; };
// 模拟统计数据
const mockStats = {
views: 5,
jobs: 20,
builds: 100
};
return ( return (
<PageContainer> <PageContainer>
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
@ -166,7 +167,7 @@ const JenkinsManagerList: React.FC = () => {
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm font-medium">Views</span> <span className="text-sm font-medium">Views</span>
<span className="text-2xl font-bold">{mockStats.views}</span> <span className="text-2xl font-bold">{instanceDetails?.totalViews || 0}</span>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Last sync: {formatTime(currentJenkins.lastSyncTime)}</span> <span className="text-sm text-muted-foreground">Last sync: {formatTime(currentJenkins.lastSyncTime)}</span>
@ -185,7 +186,7 @@ const JenkinsManagerList: React.FC = () => {
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm font-medium">Jobs</span> <span className="text-sm font-medium">Jobs</span>
<span className="text-2xl font-bold">{mockStats.jobs}</span> <span className="text-2xl font-bold">{instanceDetails?.totalJobs || 0}</span>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Last sync: {formatTime(currentJenkins.lastSyncTime)}</span> <span className="text-sm text-muted-foreground">Last sync: {formatTime(currentJenkins.lastSyncTime)}</span>
@ -204,7 +205,7 @@ const JenkinsManagerList: React.FC = () => {
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm font-medium">Builds</span> <span className="text-sm font-medium">Builds</span>
<span className="text-2xl font-bold">{mockStats.builds}</span> <span className="text-2xl font-bold">{instanceDetails?.totalBuilds || 0}</span>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Last sync: {formatTime(currentJenkins.lastSyncTime)}</span> <span className="text-sm text-muted-foreground">Last sync: {formatTime(currentJenkins.lastSyncTime)}</span>
@ -233,30 +234,50 @@ const JenkinsManagerList: React.FC = () => {
<div className="flex items-center justify-center py-8"> <div className="flex items-center justify-center py-8">
<RefreshCw className="h-6 w-6 animate-spin" /> <RefreshCw className="h-6 w-6 animate-spin" />
</div> </div>
) : views.map((view, index) => ( ) : instanceDetails?.jenkinsViewList.map((view, index) => (
<div key={view.id}> <div key={view.id}>
{index > 0 && <Separator className="my-4" />} {index > 0 && <Separator className="my-4" />}
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div
<h4 className="text-base font-medium">{view.name}</h4> className="flex items-center justify-between cursor-pointer"
<Button variant="ghost" size="sm"> onClick={() => toggleView(view.id)}
<ChevronRight className="h-4 w-4" /> >
</Button> <div>
</div> <h4 className="text-base font-medium">{view.viewName}</h4>
{view.jobs.length > 0 && ( {view.description && (
<div className="space-y-2"> <p className="text-sm text-muted-foreground">{view.description}</p>
{view.jobs.map(job => (
<div key={job.id} className="flex items-center justify-between text-sm">
<span>{job.name}</span>
{job.lastBuild && (
<div className="flex items-center gap-2">
<Badge variant={job.lastBuild.result === 'SUCCESS' ? 'outline' : 'destructive'}>
#{job.lastBuild.number} - {job.lastBuild.result}
</Badge>
<span className="text-muted-foreground">{job.lastBuild.timestamp}</span>
</div>
)} )}
</div> </div>
<ChevronRight
className={`h-4 w-4 transition-transform ${expandedViews[view.id] ? 'rotate-90' : ''}`}
/>
</div>
{expandedViews[view.id] && (
<div className="pl-4 space-y-2 border-l">
{instanceDetails.jenkinsJobList
.filter(job => job.viewId === view.id)
.map(job => (
<div key={job.id} className="flex items-center justify-between text-sm py-2">
<div>
<a
href={job.jobUrl}
target="_blank"
rel="noopener noreferrer"
className="hover:underline"
>
{job.jobName}
</a>
{job.description && (
<p className="text-xs text-muted-foreground">{job.description}</p>
)}
</div>
<div className="flex items-center gap-2">
<Badge variant={job.lastBuildStatus === 'SUCCESS' ? 'outline' : 'destructive'}>
#{job.lastBuildNumber} - {job.lastBuildStatus}
</Badge>
<span className="text-muted-foreground">{formatTime(job.lastBuildTime)}</span>
</div>
</div>
))} ))}
</div> </div>
)} )}

View File

@ -1,5 +1,5 @@
import request from '@/utils/request'; import request from '@/utils/request';
import type { JenkinsInstance, JenkinsView } from './types'; import type { JenkinsInstance, JenkinsInstanceDTO } from './types';
import { getExternalSystems } from '@/pages/Deploy/External/service'; import { getExternalSystems } from '@/pages/Deploy/External/service';
import { SystemType } from '@/pages/Deploy/External/types'; import { SystemType } from '@/pages/Deploy/External/types';
@ -10,54 +10,9 @@ export const getJenkinsInstances = () =>
enabled: true enabled: true
}).then(response => response.content); }).then(response => response.content);
// 获取 Jenkins 视图列表 // 获取 Jenkins 实例详情
export const getJenkinsViews = (jenkinsId: string) => export const getJenkinsInstance = (externalSystemId: string) =>
// 模拟数据 request.get<JenkinsInstanceDTO>(`/api/v1/jenkins-manager/${externalSystemId}/instance`);
Promise.resolve<JenkinsView[]>([
{
id: '1',
name: 'All',
url: 'https://jenkins.prod.example.com/view/all',
jobs: []
},
{
id: '2',
name: 'Frontend',
url: 'https://jenkins.prod.example.com/view/frontend',
jobs: [
{
id: '1',
name: 'Build Frontend',
url: 'https://jenkins.prod.example.com/job/build-frontend',
lastBuild: {
id: '42',
number: 42,
result: 'SUCCESS',
timestamp: '2024/12/28 20:28:43',
url: 'https://jenkins.prod.example.com/job/build-frontend/42'
}
},
{
id: '2',
name: 'Test Backend',
url: 'https://jenkins.prod.example.com/job/test-backend',
lastBuild: {
id: '41',
number: 41,
result: 'FAILURE',
timestamp: '2024/12/28 19:28:43',
url: 'https://jenkins.prod.example.com/job/test-backend/41'
}
}
]
},
{
id: '3',
name: 'Backend',
url: 'https://jenkins.prod.example.com/view/backend',
jobs: []
}
]);
// 同步视图 // 同步视图
export const syncViews = (externalSystemId: string) => export const syncViews = (externalSystemId: string) =>

View File

@ -4,29 +4,36 @@ import type { ExternalSystemResponse } from '@/pages/Deploy/External/types';
// 使用外部系统响应作为 Jenkins 实例 // 使用外部系统响应作为 Jenkins 实例
export type JenkinsInstance = ExternalSystemResponse; export type JenkinsInstance = ExternalSystemResponse;
// Jenkins 视图类型 // Jenkins 实例详情
export interface JenkinsView { export interface JenkinsInstanceDTO extends BaseResponse {
id: string; totalViews: number;
name: string; totalJobs: number;
url: string; totalBuilds: number;
jobs: JenkinsJob[]; jenkinsViewList: JenkinsViewDTO[];
jenkinsJobList: JenkinsJobDTO[];
} }
// Jenkins Job 类型 // Jenkins 视图
export interface JenkinsJob { export interface JenkinsViewDTO extends BaseResponse {
id: string; description: string;
name: string; externalSystemId: number;
url: string; viewName: string;
lastBuild?: JenkinsBuild; viewUrl: string;
} }
// Jenkins 构建类型 // Jenkins 任务
export interface JenkinsBuild { export interface JenkinsJobDTO extends BaseResponse {
id: string; buildable: boolean;
number: number; description: string;
result: 'SUCCESS' | 'FAILURE' | 'RUNNING' | 'ABORTED'; jobName: string;
timestamp: string; jobUrl: string;
url: string; nextBuildNumber: number;
lastBuildNumber: number;
lastBuildStatus: string;
healthReportScore: number;
lastBuildTime: string;
externalSystemId: number;
viewId: number;
} }
// 同步类型 // 同步类型