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 { useToast } from "@/components/ui/use-toast";
import { Separator } from "@/components/ui/separator";
import type { JenkinsInstance, JenkinsView, SyncType } from './types';
import { getJenkinsInstances, getJenkinsViews, syncViews, syncJobs, syncBuilds } from './service';
import type { JenkinsInstance, JenkinsInstanceDTO } from './types';
import { getJenkinsInstances, getJenkinsInstance, syncViews, syncJobs, syncBuilds } from './service';
const JenkinsManagerList: React.FC = () => {
const [jenkinsList, setJenkinsList] = useState<JenkinsInstance[]>([]);
const [currentJenkinsId, setCurrentJenkinsId] = useState<string>();
const [currentJenkins, setCurrentJenkins] = useState<JenkinsInstance>();
const [views, setViews] = useState<JenkinsView[]>([]);
const [instanceDetails, setInstanceDetails] = useState<JenkinsInstanceDTO>();
const [loading, setLoading] = useState(false);
const [syncing, setSyncing] = useState<Record<SyncType, boolean>>({
const [syncing, setSyncing] = useState<Record<string, boolean>>({
views: false,
jobs: false,
builds: false
});
const [expandedViews, setExpandedViews] = useState<Record<number, boolean>>({});
const { toast } = useToast();
// 获取 Jenkins 实例列表
@ -51,17 +52,17 @@ const JenkinsManagerList: React.FC = () => {
}
};
// 获取视图列表
const loadViews = async () => {
// 获取 Jenkins 实例详情
const loadInstanceDetails = async () => {
if (!currentJenkinsId) return;
setLoading(true);
try {
const data = await getJenkinsViews(currentJenkinsId);
setViews(data);
const data = await getJenkinsInstance(currentJenkinsId);
setInstanceDetails(data);
} catch (error) {
toast({
variant: "destructive",
title: "获取视图列表失败",
title: "获取实例详情失败",
duration: 3000,
});
} finally {
@ -74,18 +75,17 @@ const JenkinsManagerList: React.FC = () => {
setCurrentJenkinsId(id);
const jenkins = jenkinsList.find(j => String(j.id) === id);
setCurrentJenkins(jenkins);
setViews([]);
setInstanceDetails(undefined);
};
// 同步数据
const handleSync = async (type: SyncType) => {
const handleSync = async (type: 'views' | 'jobs' | 'builds') => {
if (!currentJenkinsId) return;
setSyncing(prev => ({ ...prev, [type]: true }));
try {
switch (type) {
case 'views':
await syncViews(currentJenkinsId);
await loadViews(); // 重新加载视图数据
break;
case 'jobs':
await syncJobs(currentJenkinsId);
@ -94,7 +94,7 @@ const JenkinsManagerList: React.FC = () => {
await syncBuilds(currentJenkinsId);
break;
}
await loadJenkinsList(); // 重新加载实例<E5AE9E><E4BE8B>据以更新同步时间
await loadInstanceDetails(); // 重新加载实例详情
toast({
title: "同步成功",
duration: 3000,
@ -110,13 +110,21 @@ const JenkinsManagerList: React.FC = () => {
}
};
// 添加切换展开/收起的处理函数
const toggleView = (viewId: number) => {
setExpandedViews(prev => ({
...prev,
[viewId]: !prev[viewId]
}));
};
useEffect(() => {
loadJenkinsList();
}, []);
useEffect(() => {
if (currentJenkinsId) {
loadViews();
loadInstanceDetails();
}
}, [currentJenkinsId]);
@ -125,13 +133,6 @@ const JenkinsManagerList: React.FC = () => {
return time;
};
// 模拟统计数据
const mockStats = {
views: 5,
jobs: 20,
builds: 100
};
return (
<PageContainer>
<div className="flex items-center justify-between mb-6">
@ -166,7 +167,7 @@ const JenkinsManagerList: React.FC = () => {
<div className="space-y-2">
<div className="flex items-center justify-between">
<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 className="flex items-center justify-between">
<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="flex items-center justify-between">
<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 className="flex items-center justify-between">
<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="flex items-center justify-between">
<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 className="flex items-center justify-between">
<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">
<RefreshCw className="h-6 w-6 animate-spin" />
</div>
) : views.map((view, index) => (
) : instanceDetails?.jenkinsViewList.map((view, index) => (
<div key={view.id}>
{index > 0 && <Separator className="my-4" />}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h4 className="text-base font-medium">{view.name}</h4>
<Button variant="ghost" size="sm">
<ChevronRight className="h-4 w-4" />
</Button>
</div>
{view.jobs.length > 0 && (
<div className="space-y-2">
{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
className="flex items-center justify-between cursor-pointer"
onClick={() => toggleView(view.id)}
>
<div>
<h4 className="text-base font-medium">{view.viewName}</h4>
{view.description && (
<p className="text-sm text-muted-foreground">{view.description}</p>
)}
</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>
)}

View File

@ -1,5 +1,5 @@
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 { SystemType } from '@/pages/Deploy/External/types';
@ -10,54 +10,9 @@ export const getJenkinsInstances = () =>
enabled: true
}).then(response => response.content);
// 获取 Jenkins 视图列表
export const getJenkinsViews = (jenkinsId: string) =>
// 模拟数据
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: []
}
]);
// 获取 Jenkins 实例详情
export const getJenkinsInstance = (externalSystemId: string) =>
request.get<JenkinsInstanceDTO>(`/api/v1/jenkins-manager/${externalSystemId}/instance`);
// 同步视图
export const syncViews = (externalSystemId: string) =>

View File

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