1
This commit is contained in:
parent
7bfc716eb5
commit
43670b8142
@ -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,31 +234,51 @@ 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
|
||||
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>
|
||||
{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>
|
||||
{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>
|
||||
))}
|
||||
<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>
|
||||
|
||||
@ -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) =>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
// 同步类型
|
||||
|
||||
Loading…
Reference in New Issue
Block a user