增加团队管理页面

This commit is contained in:
dengqichen 2025-10-29 20:57:32 +08:00
parent a265275b97
commit e5f0f3bba4
3 changed files with 588 additions and 515 deletions

View File

@ -1,28 +1,12 @@
import React, { useState, useEffect, useMemo } from 'react';
import { PageContainer } from '@/components/ui/page-container';
import {
RefreshCw,
Layers,
Box,
Activity,
Search,
CheckCircle,
XCircle,
AlertTriangle,
Loader2,
ExternalLink,
GitBranch,
Calendar
} from 'lucide-react';
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import {
Select,
SelectContent,
@ -30,12 +14,45 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';
import { useToast } from '@/components/ui/use-toast';
import { Progress } from '@/components/ui/progress';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import type { JenkinsInstance, JenkinsInstanceDTO, JenkinsViewDTO, JenkinsJobDTO } from './types';
import { getJenkinsInstances, getJenkinsInstance, syncViews, syncJobs, syncBuilds } from './service';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
import {
Layers,
RefreshCw,
Search,
Loader2,
ExternalLink,
Box,
GitBranch,
CheckCircle,
XCircle,
Clock,
AlertTriangle,
Calendar,
Activity,
} from 'lucide-react';
import type {
JenkinsInstance,
JenkinsViewDTO,
JenkinsJobDTO,
JenkinsBuildDTO,
} from './types';
import {
getJenkinsInstances,
getJenkinsViews,
getJenkinsJobs,
getJenkinsBuilds,
syncJenkinsViews,
syncJenkinsJobs,
syncJenkinsBuilds,
} from './service';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import 'dayjs/locale/zh-cn';
@ -43,14 +60,35 @@ import 'dayjs/locale/zh-cn';
dayjs.extend(relativeTime);
dayjs.locale('zh-cn');
const JenkinsManagerList: React.FC = () => {
const [jenkinsList, setJenkinsList] = useState<JenkinsInstance[]>([]);
const [selectedInstanceId, setSelectedInstanceId] = useState<number>();
const [instanceDetails, setInstanceDetails] = useState<JenkinsInstanceDTO>();
// 构建状态配置
const BUILD_STATUS_CONFIG: Record<string, {
label: string;
variant: 'default' | 'secondary' | 'destructive' | 'outline';
icon: React.ElementType;
color: string;
}> = {
SUCCESS: { label: '成功', variant: 'default', icon: CheckCircle, color: 'text-green-600' },
FAILURE: { label: '失败', variant: 'destructive', icon: XCircle, color: 'text-red-600' },
UNSTABLE: { label: '不稳定', variant: 'secondary', icon: AlertTriangle, color: 'text-yellow-600' },
ABORTED: { label: '已中止', variant: 'outline', icon: Clock, color: 'text-gray-600' },
NOT_BUILT: { label: '未构建', variant: 'outline', icon: Box, color: 'text-gray-400' },
};
// 三栏选中状态
const [selectedView, setSelectedView] = useState<JenkinsViewDTO | null>(null);
const [selectedJob, setSelectedJob] = useState<JenkinsJobDTO | null>(null);
const JenkinsManager: React.FC = () => {
const { toast } = useToast();
// Jenkins 实例选择
const [instances, setInstances] = useState<JenkinsInstance[]>([]);
const [selectedInstanceId, setSelectedInstanceId] = useState<number>();
// 数据状态
const [views, setViews] = useState<JenkinsViewDTO[]>([]);
const [jobs, setJobs] = useState<JenkinsJobDTO[]>([]);
const [builds, setBuilds] = useState<JenkinsBuildDTO[]>([]);
// 选中状态
const [selectedView, setSelectedView] = useState<JenkinsViewDTO>();
const [selectedJob, setSelectedJob] = useState<JenkinsJobDTO>();
// 加载状态
const [loading, setLoading] = useState({
@ -61,7 +99,6 @@ const JenkinsManagerList: React.FC = () => {
// 同步状态
const [syncing, setSyncing] = useState({
all: false,
views: false,
jobs: false,
builds: false,
@ -72,322 +109,276 @@ const JenkinsManagerList: React.FC = () => {
const [jobSearch, setJobSearch] = useState('');
const [buildSearch, setBuildSearch] = useState('');
const { toast } = useToast();
// 获取 Jenkins 实例列表
const loadJenkinsList = async () => {
// 加载 Jenkins 实例列表
useEffect(() => {
const loadInstances = async () => {
try {
const data = await getJenkinsInstances();
setJenkinsList(data);
if (!selectedInstanceId && data.length > 0) {
setInstances(data);
if (data.length > 0 && !selectedInstanceId) {
setSelectedInstanceId(data[0].id);
}
} catch (error) {
toast({
variant: 'destructive',
title: '获取 Jenkins 实例列表失败',
duration: 3000,
title: '加载失败',
description: '获取 Jenkins 实例列表失败',
});
}
};
loadInstances();
}, []);
// 获取 Jenkins 实例详情
const loadInstanceDetails = async () => {
// 加载视图列表
const loadViews = async () => {
if (!selectedInstanceId) return;
setLoading((prev) => ({ ...prev, views: true }));
try {
const data = await getJenkinsInstance(String(selectedInstanceId));
setInstanceDetails(data);
// 自动选择第一个 View
if (data.jenkinsViewList.length > 0 && !selectedView) {
setSelectedView(data.jenkinsViewList[0]);
}
const data = await getJenkinsViews(selectedInstanceId);
setViews(data || []);
} catch (error) {
toast({
variant: 'destructive',
title: '获取实例详情失败',
duration: 3000,
title: '加载失败',
description: '获取视图列表失败',
});
} finally {
setLoading((prev) => ({ ...prev, views: false }));
}
};
// 切换 Jenkins 实例
const handleInstanceChange = (id: string) => {
setSelectedInstanceId(Number(id));
setInstanceDetails(undefined);
setSelectedView(null);
setSelectedJob(null);
setViewSearch('');
setJobSearch('');
setBuildSearch('');
// 加载任务列表
const loadJobs = async (viewId?: number) => {
if (!selectedInstanceId) return;
setLoading((prev) => ({ ...prev, jobs: true }));
try {
const data = await getJenkinsJobs({
externalSystemId: selectedInstanceId,
viewId,
});
setJobs(data || []);
} catch (error) {
toast({
variant: 'destructive',
title: '加载失败',
description: '获取任务列表失败',
});
} finally {
setLoading((prev) => ({ ...prev, jobs: false }));
}
};
// 选择 View
const handleSelectView = (view: JenkinsViewDTO) => {
setSelectedView(view);
setSelectedJob(null);
// 加载构建列表
const loadBuilds = async (jobId?: number) => {
if (!selectedInstanceId) return;
setLoading((prev) => ({ ...prev, builds: true }));
try {
const data = await getJenkinsBuilds({
externalSystemId: selectedInstanceId,
jobId,
});
setBuilds(data || []);
} catch (error) {
toast({
variant: 'destructive',
title: '加载失败',
description: '获取构建列表失败',
});
} finally {
setLoading((prev) => ({ ...prev, builds: false }));
}
};
// 选择 Job
const handleSelectJob = (job: JenkinsJobDTO) => {
setSelectedJob(job);
// TODO: 加载 Builds 列表
};
// 监听实例变化
useEffect(() => {
if (selectedInstanceId) {
setSelectedView(undefined);
setSelectedJob(undefined);
setJobs([]);
setBuilds([]);
loadViews();
}
}, [selectedInstanceId]);
// 同步 Views
// 监听视图选择变化
useEffect(() => {
if (selectedView) {
setSelectedJob(undefined);
setBuilds([]);
loadJobs(selectedView.id);
}
}, [selectedView]);
// 监听任务选择变化
useEffect(() => {
if (selectedJob) {
loadBuilds(selectedJob.id);
}
}, [selectedJob]);
// 同步视图
const handleSyncViews = async () => {
if (!selectedInstanceId) return;
setSyncing((prev) => ({ ...prev, views: true }));
try {
await syncViews(String(selectedInstanceId));
await syncJenkinsViews(selectedInstanceId);
toast({
title: '同步成功',
description: 'Views 同步任务已启动',
duration: 2000,
description: '视图同步任务已启动',
});
setTimeout(() => loadViews(), 2000);
} catch (error) {
toast({
variant: 'destructive',
title: '同步失败',
duration: 3000,
description: '视图同步失败',
});
} finally {
setSyncing((prev) => ({ ...prev, views: false }));
}
};
// 同步 Jobs
// 同步任务
const handleSyncJobs = async () => {
if (!selectedInstanceId) return;
setSyncing((prev) => ({ ...prev, jobs: true }));
try {
await syncJobs(String(selectedInstanceId));
await syncJenkinsJobs({
externalSystemId: selectedInstanceId,
viewId: selectedView?.id,
});
toast({
title: '同步成功',
description: 'Jobs 同步任务已启动',
duration: 2000,
description: '任务同步任务已启动',
});
setTimeout(() => {
if (selectedView) {
loadJobs(selectedView.id);
}
}, 2000);
} catch (error) {
toast({
variant: 'destructive',
title: '同步失败',
duration: 3000,
description: '任务同步失败',
});
} finally {
setSyncing((prev) => ({ ...prev, jobs: false }));
}
};
// 同步 Builds
// 同步构建
const handleSyncBuilds = async () => {
if (!selectedInstanceId) return;
setSyncing((prev) => ({ ...prev, builds: true }));
try {
await syncBuilds(String(selectedInstanceId));
await syncJenkinsBuilds({
externalSystemId: selectedInstanceId,
jobId: selectedJob?.id,
});
toast({
title: '同步成功',
description: 'Builds 同步任务已启动',
duration: 2000,
description: '构建同步任务已启动',
});
setTimeout(() => {
if (selectedJob) {
loadBuilds(selectedJob.id);
}
}, 2000);
} catch (error) {
toast({
variant: 'destructive',
title: '同步失败',
duration: 3000,
description: '构建同步失败',
});
} finally {
setSyncing((prev) => ({ ...prev, builds: false }));
}
};
// 全部同步
const handleSyncAll = async () => {
if (!selectedInstanceId) return;
setSyncing((prev) => ({ ...prev, all: true }));
try {
await Promise.all([
syncViews(String(selectedInstanceId)),
syncJobs(String(selectedInstanceId)),
syncBuilds(String(selectedInstanceId)),
]);
toast({
title: '全部同步任务已启动',
description: '请稍后刷新页面查看同步结果',
duration: 2000,
});
} catch (error) {
toast({
variant: 'destructive',
title: '同步失败',
description: '部分数据同步失败,请重试',
duration: 3000,
});
} finally {
setSyncing((prev) => ({ ...prev, all: false }));
}
};
// 格式化时间
const formatTime = (time?: string | null) => {
if (!time) return '-';
const diff = dayjs().diff(dayjs(time), 'day');
if (diff > 7) {
return dayjs(time).format('YYYY-MM-DD HH:mm');
}
return dayjs(time).fromNow();
};
// 获取构建状态信息
const getBuildStatusInfo = (status: string) => {
const statusMap: Record<string, {
variant: 'default' | 'destructive' | 'secondary' | 'outline' | 'success';
icon: React.ReactNode;
label: string;
}> = {
SUCCESS: {
variant: 'success',
icon: <CheckCircle className="h-3 w-3" />,
label: '成功',
},
FAILURE: {
variant: 'destructive',
icon: <XCircle className="h-3 w-3" />,
label: '失败',
},
UNSTABLE: {
variant: 'secondary',
icon: <AlertTriangle className="h-3 w-3" />,
label: '不稳定',
},
ABORTED: {
variant: 'outline',
icon: <XCircle className="h-3 w-3" />,
label: '已中止',
},
RUNNING: {
variant: 'outline',
icon: <Loader2 className="h-3 w-3 animate-spin" />,
label: '构建中',
},
};
return statusMap[status] || {
variant: 'outline' as const,
icon: <AlertTriangle className="h-3 w-3" />,
label: status || '未知',
};
};
// 获取健康度颜色
const getHealthColor = (score: number) => {
if (score >= 80) return 'bg-green-500';
if (score >= 60) return 'bg-green-400';
if (score >= 40) return 'bg-yellow-400';
if (score >= 20) return 'bg-orange-400';
return 'bg-red-500';
};
// 过滤 Views
// 过滤视图
const filteredViews = useMemo(() => {
if (!instanceDetails) return [];
if (!viewSearch) return instanceDetails.jenkinsViewList;
return instanceDetails.jenkinsViewList.filter((view) =>
view.viewName.toLowerCase().includes(viewSearch.toLowerCase()) ||
(view.description && view.description.toLowerCase().includes(viewSearch.toLowerCase()))
return views.filter((view) =>
view.viewName.toLowerCase().includes(viewSearch.toLowerCase())
);
}, [instanceDetails, viewSearch]);
}, [views, viewSearch]);
// 过滤 Jobs基于选中的 View
// 过滤任务
const filteredJobs = useMemo(() => {
if (!instanceDetails || !selectedView) return [];
let jobs = instanceDetails.jenkinsJobList.filter(
(job) => job.viewId === selectedView.id
return jobs.filter((job) =>
job.jobName.toLowerCase().includes(jobSearch.toLowerCase())
);
if (jobSearch) {
jobs = jobs.filter((job) =>
job.jobName.toLowerCase().includes(jobSearch.toLowerCase()) ||
(job.description && job.description.toLowerCase().includes(jobSearch.toLowerCase()))
}, [jobs, jobSearch]);
// 过滤构建
const filteredBuilds = useMemo(() => {
if (!buildSearch) return builds;
const searchLower = buildSearch.toLowerCase();
return builds.filter((build) =>
build.buildNumber.toString().includes(searchLower) ||
build.buildStatus.toLowerCase().includes(searchLower)
);
}
return jobs;
}, [instanceDetails, selectedView, jobSearch]);
}, [builds, buildSearch]);
useEffect(() => {
loadJenkinsList();
}, []);
useEffect(() => {
if (selectedInstanceId) {
loadInstanceDetails();
}
}, [selectedInstanceId]);
// 空状态:没有实例
if (jenkinsList.length === 0) {
// 获取构建状态徽章
const getBuildStatusBadge = (status: string) => {
const config = BUILD_STATUS_CONFIG[status] || BUILD_STATUS_CONFIG.NOT_BUILT;
const Icon = config.icon;
return (
<PageContainer>
<div className="flex items-center justify-between mb-6">
<h2 className="text-3xl font-bold tracking-tight">Jenkins </h2>
</div>
<Card>
<CardContent className="flex flex-col items-center justify-center py-16">
<Activity className="h-16 w-16 text-muted-foreground mb-4" />
<h3 className="text-lg font-semibold mb-2"> Jenkins </h3>
<p className="text-sm text-muted-foreground mb-4">
Jenkins
<Badge variant={config.variant} className="inline-flex items-center gap-1">
<Icon className="h-3 w-3" />
{config.label}
</Badge>
);
};
// 格式化时长
const formatDuration = (duration?: number) => {
if (!duration) return '-';
const seconds = Math.floor(duration / 1000);
if (seconds < 60) return `${seconds}`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}分钟`;
const hours = Math.floor(minutes / 60);
return `${hours}小时${minutes % 60}分钟`;
};
return (
<div className="p-6 flex flex-col h-screen">
{/* 页面标题 */}
<div className="flex items-center justify-between mb-6 flex-shrink-0">
<div>
<h1 className="text-3xl font-bold text-foreground">Jenkins </h1>
<p className="text-muted-foreground mt-2">
Jenkins
</p>
<Button onClick={() => (window.location.href = '/deploy/external')}>
</Button>
</CardContent>
</Card>
</PageContainer>
);
}
return (
<TooltipProvider>
<PageContainer className="h-full flex flex-col">
{/* 页面标题和实例选择 */}
<div className="flex items-center justify-between flex-shrink-0">
<h2 className="text-3xl font-bold tracking-tight">Jenkins </h2>
<div className="flex items-center gap-4">
</div>
<Select
value={selectedInstanceId?.toString()}
onValueChange={handleInstanceChange}
value={selectedInstanceId?.toString() || ''}
onValueChange={(value) => setSelectedInstanceId(Number(value))}
>
<SelectTrigger className="w-[300px]">
<SelectTrigger className="w-[240px]">
<SelectValue placeholder="选择Jenkins实例" />
</SelectTrigger>
<SelectContent>
{jenkinsList.map((jenkins) => (
<SelectItem key={jenkins.id} value={String(jenkins.id)}>
{jenkins.name}
{instances.map((instance) => (
<SelectItem key={instance.id} value={instance.id.toString()}>
{instance.name}
</SelectItem>
))}
</SelectContent>
</Select>
<Button onClick={handleSyncAll} disabled={syncing.all} variant="outline">
{syncing.all ? (
<Loader2 className="h-4 w-4 animate-spin mr-2" />
) : (
<RefreshCw className="h-4 w-4 mr-2" />
)}
</Button>
</div>
</div>
{/* 三栏布局 */}
<div className="flex-1 grid grid-cols-12 gap-4 min-h-0 overflow-hidden">
{/* 左栏Views 列表 */}
{/* 左栏:视图列表 */}
<Card className="col-span-3 flex flex-col min-h-0">
<CardHeader className="border-b flex-shrink-0">
<div className="flex items-center justify-between">
<CardTitle className="text-base">
Views {instanceDetails && `(${filteredViews.length})`}
<CardTitle className="text-base flex items-center gap-2">
<Layers className="h-4 w-4" />
({filteredViews.length})
</CardTitle>
<Button
variant="ghost"
@ -411,43 +402,39 @@ const JenkinsManagerList: React.FC = () => {
/>
</div>
</CardHeader>
<div className="flex-1 overflow-auto">
<ScrollArea className="flex-1">
<div className="p-2 space-y-1">
{loading.views ? (
<div className="flex items-center justify-center h-full">
<div className="flex items-center justify-center py-12">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : filteredViews.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-muted-foreground">
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
<Layers className="h-12 w-12 mb-2 opacity-20" />
<p className="text-sm"></p>
</div>
) : (
<div className="p-2 space-y-2">
{filteredViews.map((view) => (
filteredViews.map((view) => (
<div
key={view.id}
className={`p-3 border rounded-lg cursor-pointer hover:bg-accent transition-colors ${
className={`group p-3 border rounded-lg cursor-pointer hover:bg-accent transition-colors ${
selectedView?.id === view.id
? 'bg-accent border-primary'
: ''
}`}
onClick={() => handleSelectView(view)}
onClick={() => setSelectedView(view)}
>
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<Layers className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
<span className="font-medium truncate">
<div className="flex items-center gap-2 mb-2">
<span className="flex-1 truncate font-medium">
{view.viewName}
</span>
</div>
{view.description && (
<p className="text-xs text-muted-foreground line-clamp-2">
{view.description}
</p>
{view.jobCount !== undefined && view.jobCount > 0 && (
<Badge variant="secondary" className="text-xs px-1.5 py-0 h-5">
{view.jobCount}
</Badge>
)}
</div>
{view.viewUrl && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<a
@ -455,13 +442,9 @@ const JenkinsManagerList: React.FC = () => {
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="flex-shrink-0"
>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
>
<Button variant="ghost" size="icon" className="h-5 w-5">
<ExternalLink className="h-3 w-3" />
</Button>
</a>
@ -470,21 +453,27 @@ const JenkinsManagerList: React.FC = () => {
<p> Jenkins </p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
</div>
))}
</div>
{view.description && (
<p className="text-xs text-muted-foreground line-clamp-2">
{view.description}
</p>
)}
</div>
))
)}
</div>
</ScrollArea>
</Card>
{/* 中栏Jobs 列表 */}
{/* 中栏:任务列表 */}
<Card className="col-span-5 flex flex-col min-h-0">
<CardHeader className="border-b flex-shrink-0">
<div className="flex items-center justify-between">
<CardTitle className="text-base">
Jobs {selectedView && `(${filteredJobs.length})`}
{selectedView && `(${filteredJobs.length})`}
</CardTitle>
<Button
variant="ghost"
@ -525,64 +514,25 @@ const JenkinsManagerList: React.FC = () => {
</div>
) : (
<div className="p-2 space-y-2">
{filteredJobs.map((job) => {
const statusInfo = getBuildStatusInfo(job.lastBuildStatus);
return (
{filteredJobs.map((job) => (
<div
key={job.id}
className={`p-3 border rounded-lg cursor-pointer hover:bg-accent transition-colors ${
className={`group p-3 border rounded-lg cursor-pointer hover:bg-accent transition-colors ${
selectedJob?.id === job.id
? 'bg-accent border-primary'
: ''
}`}
onClick={() => handleSelectJob(job)}
onClick={() => setSelectedJob(job)}
>
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<Box className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
<span className="font-medium truncate">
{job.jobName}
</span>
</div>
{job.description && (
<p className="text-xs text-muted-foreground line-clamp-2 mb-2">
{job.description}
</p>
)}
<div className="flex items-center gap-3 flex-wrap">
<Badge
variant={statusInfo.variant}
className="inline-flex items-center gap-1"
>
{statusInfo.icon}
{statusInfo.label}
<div className="flex items-center gap-2 mb-2">
<span className="flex-1 truncate font-medium">{job.jobName}</span>
{job.buildCount !== undefined && job.buildCount > 0 && (
<Badge variant="secondary" className="text-xs px-1.5 py-0 h-5">
{job.buildCount}
</Badge>
<span className="text-xs text-muted-foreground flex items-center gap-1">
<GitBranch className="h-3 w-3" />
#{job.lastBuildNumber}
</span>
<div className="flex items-center gap-1">
<Progress
value={job.healthReportScore}
className="h-2 w-16"
indicatorClassName={getHealthColor(
job.healthReportScore
)}
/>
<span className="text-xs text-muted-foreground">
{job.healthReportScore}%
</span>
</div>
</div>
{job.lastBuildTime && (
<div className="flex items-center gap-1 mt-2 text-xs text-muted-foreground">
<Calendar className="h-3 w-3" />
{formatTime(job.lastBuildTime)}
</div>
)}
</div>
{job.jobUrl && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<a
@ -590,13 +540,9 @@ const JenkinsManagerList: React.FC = () => {
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="flex-shrink-0"
>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
>
<Button variant="ghost" size="icon" className="h-6 w-6">
<ExternalLink className="h-3 w-3" />
</Button>
</a>
@ -605,27 +551,67 @@ const JenkinsManagerList: React.FC = () => {
<p> Jenkins </p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
{job.description && (
<p className="text-xs text-muted-foreground line-clamp-2 mb-2">
{job.description}
</p>
)}
<div className="flex items-center gap-3 text-xs">
{job.lastBuildStatus && getBuildStatusBadge(job.lastBuildStatus)}
{job.healthReportScore !== undefined && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-1">
<Activity className="h-3 w-3 text-muted-foreground" />
<Progress
value={job.healthReportScore}
className="w-16 h-2"
/>
<span className="text-muted-foreground">
{job.healthReportScore}%
</span>
</div>
);
})}
</TooltipTrigger>
<TooltipContent>
: {job.healthReportScore}%
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
{job.lastBuildTime && (
<div className="flex items-center gap-1 text-xs text-muted-foreground mt-2">
<Calendar className="h-3 w-3" />
: {dayjs(job.lastBuildTime).fromNow()}
</div>
)}
</div>
))}
</div>
)}
</div>
</Card>
{/* 右栏Builds 列表(待实现) */}
{/* 右栏:构建列表 */}
<Card className="col-span-4 flex flex-col min-h-0">
<CardHeader className="border-b flex-shrink-0">
<div className="flex items-center justify-between">
<CardTitle className="text-base">Builds</CardTitle>
<CardTitle className="text-base">
{selectedJob && `(${filteredBuilds.length})`}
</CardTitle>
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={handleSyncBuilds}
disabled={syncing.builds || !selectedJob}
disabled={syncing.builds || !selectedInstanceId}
>
<RefreshCw
className={`h-4 w-4 ${syncing.builds ? 'animate-spin' : ''}`}
@ -643,24 +629,79 @@ const JenkinsManagerList: React.FC = () => {
</div>
</CardHeader>
<div className="flex-1 overflow-auto">
{!selectedJob ? (
{loading.builds ? (
<div className="flex items-center justify-center h-full">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : !selectedJob ? (
<div className="flex flex-col items-center justify-center h-full text-muted-foreground">
<Activity className="h-12 w-12 mb-2 opacity-20" />
<GitBranch className="h-12 w-12 mb-2 opacity-20" />
<p className="text-sm"></p>
</div>
) : (
) : filteredBuilds.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-muted-foreground">
<Activity className="h-12 w-12 mb-2 opacity-20" />
<p className="text-sm"></p>
<p className="text-xs mt-1"></p>
<GitBranch className="h-12 w-12 mb-2 opacity-20" />
<p className="text-sm"></p>
</div>
) : (
<div className="divide-y">
{filteredBuilds.map((build) => (
<div
key={build.id}
className="group p-3 hover:bg-accent transition-colors"
>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="font-mono font-semibold text-sm">
#{build.buildNumber}
</span>
{getBuildStatusBadge(build.buildStatus)}
</div>
{build.buildUrl && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<a
href={build.buildUrl}
target="_blank"
rel="noopener noreferrer"
className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
>
<Button variant="ghost" size="icon" className="h-6 w-6">
<ExternalLink className="h-3 w-3" />
</Button>
</a>
</TooltipTrigger>
<TooltipContent>
<p> Jenkins </p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
{build.starttime && (
<div className="flex items-center gap-1 text-xs text-muted-foreground mb-1">
<Calendar className="h-3 w-3" />
{dayjs(build.starttime).format('YYYY-MM-DD HH:mm:ss')}
</div>
)}
{build.duration !== undefined && (
<div className="flex items-center gap-1 text-xs text-muted-foreground mb-1">
<Clock className="h-3 w-3" />
: {formatDuration(build.duration)}
</div>
)}
</div>
))}
</div>
)}
</div>
</Card>
</div>
</PageContainer>
</TooltipProvider>
</div>
);
};
export default JenkinsManagerList;
export default JenkinsManager;

View File

@ -1,5 +1,5 @@
import request from '@/utils/request';
import type { JenkinsInstance, JenkinsInstanceDTO } from './types';
import type { JenkinsViewDTO, JenkinsJobDTO, JenkinsBuildDTO } from './types';
import { getExternalSystems } from '@/pages/Deploy/External/service';
import { SystemType } from '@/pages/Deploy/External/types';
@ -10,18 +10,36 @@ export const getJenkinsInstances = () =>
enabled: true
}).then(response => response.content);
// 获取 Jenkins 实例详情
export const getJenkinsInstance = (externalSystemId: string) =>
request.get<JenkinsInstanceDTO>(`/api/v1/jenkins-manager/${externalSystemId}/instance`);
// ==================== Jenkins 视图 ====================
// 获取视图列表
export const getJenkinsViews = (externalSystemId: number) =>
request.get<JenkinsViewDTO[]>(`/api/v1/jenkins-view`, {
params: { externalSystemId }
});
// 同步视图
export const syncViews = (externalSystemId: string) =>
request.post<void>(`/api/v1/jenkins-manager/${externalSystemId}/sync-views`);
export const syncJenkinsViews = (externalSystemId: number) =>
request.post<void>(`/api/v1/jenkins-view/sync`, null, {
params: { externalSystemId }
});
// ==================== Jenkins 任务 ====================
// 获取任务列表
export const getJenkinsJobs = (params: { externalSystemId: number; viewId?: number }) =>
request.get<JenkinsJobDTO[]>(`/api/v1/jenkins-job`, { params });
// 同步任务
export const syncJobs = (externalSystemId: string) =>
request.post<void>(`/api/v1/jenkins-manager/${externalSystemId}/sync-jobs`);
export const syncJenkinsJobs = (params: { externalSystemId: number; viewId?: number }) =>
request.post<void>(`/api/v1/jenkins-job/sync`, null, { params });
// 同步构建
export const syncBuilds = (externalSystemId: string) =>
request.post<void>(`/api/v1/jenkins-manager/${externalSystemId}/sync-builds`);
// ==================== Jenkins 构建 ====================
// 获取构建列表
export const getJenkinsBuilds = (params: { externalSystemId: number; jobId?: number }) =>
request.get<JenkinsBuildDTO[]>(`/api/v1/jenkins-build`, { params });
// 同步构建根据jobId
export const syncJenkinsBuilds = (params: { externalSystemId: number; jobId?: number }) =>
request.post<void>(`/api/v1/jenkins-build/sync`, null, { params });

View File

@ -18,25 +18,39 @@ export interface JenkinsInstanceDTO extends BaseResponse {
// Jenkins 视图
export interface JenkinsViewDTO extends BaseResponse {
description: string;
description?: string;
externalSystemId: number;
viewName: string;
viewUrl: string;
jobCount?: number; // 关联的任务数量
}
// Jenkins 任务
export interface JenkinsJobDTO extends BaseResponse {
buildable: boolean;
description: string;
buildable?: boolean;
description?: string;
jobName: string;
jobUrl: string;
nextBuildNumber: number;
lastBuildNumber: number;
lastBuildStatus: string;
healthReportScore: number;
lastBuildTime: string;
nextBuildNumber?: number;
lastBuildNumber?: number;
lastBuildStatus?: string;
healthReportScore?: number;
lastBuildTime?: string;
externalSystemId: number;
viewId: number;
viewId?: number;
buildCount?: number; // 关联的构建数量
}
// Jenkins 构建
export interface JenkinsBuildDTO extends BaseResponse {
buildNumber: number;
buildStatus: string;
buildUrl: string;
duration?: number;
starttime?: string;
actions?: string;
externalSystemId: number;
jobId: number;
}
// 同步类型