1
This commit is contained in:
parent
7bfc716eb5
commit
43670b8142
@ -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,31 +234,51 @@ 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>
|
||||||
|
<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>
|
</div>
|
||||||
{view.jobs.length > 0 && (
|
{expandedViews[view.id] && (
|
||||||
<div className="space-y-2">
|
<div className="pl-4 space-y-2 border-l">
|
||||||
{view.jobs.map(job => (
|
{instanceDetails.jenkinsJobList
|
||||||
<div key={job.id} className="flex items-center justify-between text-sm">
|
.filter(job => job.viewId === view.id)
|
||||||
<span>{job.name}</span>
|
.map(job => (
|
||||||
{job.lastBuild && (
|
<div key={job.id} className="flex items-center justify-between text-sm py-2">
|
||||||
<div className="flex items-center gap-2">
|
<div>
|
||||||
<Badge variant={job.lastBuild.result === 'SUCCESS' ? 'outline' : 'destructive'}>
|
<a
|
||||||
#{job.lastBuild.number} - {job.lastBuild.result}
|
href={job.jobUrl}
|
||||||
</Badge>
|
target="_blank"
|
||||||
<span className="text-muted-foreground">{job.lastBuild.timestamp}</span>
|
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">
|
||||||
</div>
|
<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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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) =>
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 同步类型
|
// 同步类型
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user