增加团队管理页面
This commit is contained in:
parent
a265275b97
commit
e5f0f3bba4
@ -1,28 +1,12 @@
|
|||||||
import React, { useState, useEffect, useMemo } from 'react';
|
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 {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/components/ui/card';
|
} from '@/components/ui/card';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@ -30,12 +14,45 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} 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 { useToast } from '@/components/ui/use-toast';
|
||||||
import { Progress } from '@/components/ui/progress';
|
import { Progress } from '@/components/ui/progress';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import {
|
||||||
import type { JenkinsInstance, JenkinsInstanceDTO, JenkinsViewDTO, JenkinsJobDTO } from './types';
|
Tooltip,
|
||||||
import { getJenkinsInstances, getJenkinsInstance, syncViews, syncJobs, syncBuilds } from './service';
|
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 dayjs from 'dayjs';
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
import 'dayjs/locale/zh-cn';
|
import 'dayjs/locale/zh-cn';
|
||||||
@ -43,14 +60,35 @@ import 'dayjs/locale/zh-cn';
|
|||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
dayjs.locale('zh-cn');
|
dayjs.locale('zh-cn');
|
||||||
|
|
||||||
const JenkinsManagerList: React.FC = () => {
|
// 构建状态配置
|
||||||
const [jenkinsList, setJenkinsList] = useState<JenkinsInstance[]>([]);
|
const BUILD_STATUS_CONFIG: Record<string, {
|
||||||
const [selectedInstanceId, setSelectedInstanceId] = useState<number>();
|
label: string;
|
||||||
const [instanceDetails, setInstanceDetails] = useState<JenkinsInstanceDTO>();
|
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 JenkinsManager: React.FC = () => {
|
||||||
const [selectedView, setSelectedView] = useState<JenkinsViewDTO | null>(null);
|
const { toast } = useToast();
|
||||||
const [selectedJob, setSelectedJob] = useState<JenkinsJobDTO | null>(null);
|
|
||||||
|
// 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({
|
const [loading, setLoading] = useState({
|
||||||
@ -61,7 +99,6 @@ const JenkinsManagerList: React.FC = () => {
|
|||||||
|
|
||||||
// 同步状态
|
// 同步状态
|
||||||
const [syncing, setSyncing] = useState({
|
const [syncing, setSyncing] = useState({
|
||||||
all: false,
|
|
||||||
views: false,
|
views: false,
|
||||||
jobs: false,
|
jobs: false,
|
||||||
builds: false,
|
builds: false,
|
||||||
@ -72,322 +109,276 @@ const JenkinsManagerList: React.FC = () => {
|
|||||||
const [jobSearch, setJobSearch] = useState('');
|
const [jobSearch, setJobSearch] = useState('');
|
||||||
const [buildSearch, setBuildSearch] = useState('');
|
const [buildSearch, setBuildSearch] = useState('');
|
||||||
|
|
||||||
const { toast } = useToast();
|
// 加载 Jenkins 实例列表
|
||||||
|
useEffect(() => {
|
||||||
// 获取 Jenkins 实例列表
|
const loadInstances = async () => {
|
||||||
const loadJenkinsList = async () => {
|
|
||||||
try {
|
try {
|
||||||
const data = await getJenkinsInstances();
|
const data = await getJenkinsInstances();
|
||||||
setJenkinsList(data);
|
setInstances(data);
|
||||||
if (!selectedInstanceId && data.length > 0) {
|
if (data.length > 0 && !selectedInstanceId) {
|
||||||
setSelectedInstanceId(data[0].id);
|
setSelectedInstanceId(data[0].id);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast({
|
toast({
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
title: '获取 Jenkins 实例列表失败',
|
title: '加载失败',
|
||||||
duration: 3000,
|
description: '获取 Jenkins 实例列表失败',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
loadInstances();
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 获取 Jenkins 实例详情
|
// 加载视图列表
|
||||||
const loadInstanceDetails = async () => {
|
const loadViews = async () => {
|
||||||
if (!selectedInstanceId) return;
|
if (!selectedInstanceId) return;
|
||||||
setLoading((prev) => ({ ...prev, views: true }));
|
setLoading((prev) => ({ ...prev, views: true }));
|
||||||
try {
|
try {
|
||||||
const data = await getJenkinsInstance(String(selectedInstanceId));
|
const data = await getJenkinsViews(selectedInstanceId);
|
||||||
setInstanceDetails(data);
|
setViews(data || []);
|
||||||
|
|
||||||
// 自动选择第一个 View
|
|
||||||
if (data.jenkinsViewList.length > 0 && !selectedView) {
|
|
||||||
setSelectedView(data.jenkinsViewList[0]);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast({
|
toast({
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
title: '获取实例详情失败',
|
title: '加载失败',
|
||||||
duration: 3000,
|
description: '获取视图列表失败',
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoading((prev) => ({ ...prev, views: false }));
|
setLoading((prev) => ({ ...prev, views: false }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 切换 Jenkins 实例
|
// 加载任务列表
|
||||||
const handleInstanceChange = (id: string) => {
|
const loadJobs = async (viewId?: number) => {
|
||||||
setSelectedInstanceId(Number(id));
|
if (!selectedInstanceId) return;
|
||||||
setInstanceDetails(undefined);
|
setLoading((prev) => ({ ...prev, jobs: true }));
|
||||||
setSelectedView(null);
|
try {
|
||||||
setSelectedJob(null);
|
const data = await getJenkinsJobs({
|
||||||
setViewSearch('');
|
externalSystemId: selectedInstanceId,
|
||||||
setJobSearch('');
|
viewId,
|
||||||
setBuildSearch('');
|
});
|
||||||
|
setJobs(data || []);
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
variant: 'destructive',
|
||||||
|
title: '加载失败',
|
||||||
|
description: '获取任务列表失败',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading((prev) => ({ ...prev, jobs: false }));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 选择 View
|
// 加载构建列表
|
||||||
const handleSelectView = (view: JenkinsViewDTO) => {
|
const loadBuilds = async (jobId?: number) => {
|
||||||
setSelectedView(view);
|
if (!selectedInstanceId) return;
|
||||||
setSelectedJob(null);
|
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) => {
|
useEffect(() => {
|
||||||
setSelectedJob(job);
|
if (selectedInstanceId) {
|
||||||
// TODO: 加载 Builds 列表
|
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 () => {
|
const handleSyncViews = async () => {
|
||||||
if (!selectedInstanceId) return;
|
if (!selectedInstanceId) return;
|
||||||
setSyncing((prev) => ({ ...prev, views: true }));
|
setSyncing((prev) => ({ ...prev, views: true }));
|
||||||
try {
|
try {
|
||||||
await syncViews(String(selectedInstanceId));
|
await syncJenkinsViews(selectedInstanceId);
|
||||||
toast({
|
toast({
|
||||||
title: '同步成功',
|
title: '同步成功',
|
||||||
description: 'Views 同步任务已启动',
|
description: '视图同步任务已启动',
|
||||||
duration: 2000,
|
|
||||||
});
|
});
|
||||||
|
setTimeout(() => loadViews(), 2000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast({
|
toast({
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
title: '同步失败',
|
title: '同步失败',
|
||||||
duration: 3000,
|
description: '视图同步失败',
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setSyncing((prev) => ({ ...prev, views: false }));
|
setSyncing((prev) => ({ ...prev, views: false }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 同步 Jobs
|
// 同步任务
|
||||||
const handleSyncJobs = async () => {
|
const handleSyncJobs = async () => {
|
||||||
if (!selectedInstanceId) return;
|
if (!selectedInstanceId) return;
|
||||||
setSyncing((prev) => ({ ...prev, jobs: true }));
|
setSyncing((prev) => ({ ...prev, jobs: true }));
|
||||||
try {
|
try {
|
||||||
await syncJobs(String(selectedInstanceId));
|
await syncJenkinsJobs({
|
||||||
|
externalSystemId: selectedInstanceId,
|
||||||
|
viewId: selectedView?.id,
|
||||||
|
});
|
||||||
toast({
|
toast({
|
||||||
title: '同步成功',
|
title: '同步成功',
|
||||||
description: 'Jobs 同步任务已启动',
|
description: '任务同步任务已启动',
|
||||||
duration: 2000,
|
|
||||||
});
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
if (selectedView) {
|
||||||
|
loadJobs(selectedView.id);
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast({
|
toast({
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
title: '同步失败',
|
title: '同步失败',
|
||||||
duration: 3000,
|
description: '任务同步失败',
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setSyncing((prev) => ({ ...prev, jobs: false }));
|
setSyncing((prev) => ({ ...prev, jobs: false }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 同步 Builds
|
// 同步构建
|
||||||
const handleSyncBuilds = async () => {
|
const handleSyncBuilds = async () => {
|
||||||
if (!selectedInstanceId) return;
|
if (!selectedInstanceId) return;
|
||||||
setSyncing((prev) => ({ ...prev, builds: true }));
|
setSyncing((prev) => ({ ...prev, builds: true }));
|
||||||
try {
|
try {
|
||||||
await syncBuilds(String(selectedInstanceId));
|
await syncJenkinsBuilds({
|
||||||
|
externalSystemId: selectedInstanceId,
|
||||||
|
jobId: selectedJob?.id,
|
||||||
|
});
|
||||||
toast({
|
toast({
|
||||||
title: '同步成功',
|
title: '同步成功',
|
||||||
description: 'Builds 同步任务已启动',
|
description: '构建同步任务已启动',
|
||||||
duration: 2000,
|
|
||||||
});
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
if (selectedJob) {
|
||||||
|
loadBuilds(selectedJob.id);
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast({
|
toast({
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
title: '同步失败',
|
title: '同步失败',
|
||||||
duration: 3000,
|
description: '构建同步失败',
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setSyncing((prev) => ({ ...prev, builds: false }));
|
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(() => {
|
const filteredViews = useMemo(() => {
|
||||||
if (!instanceDetails) return [];
|
return views.filter((view) =>
|
||||||
if (!viewSearch) return instanceDetails.jenkinsViewList;
|
view.viewName.toLowerCase().includes(viewSearch.toLowerCase())
|
||||||
return instanceDetails.jenkinsViewList.filter((view) =>
|
|
||||||
view.viewName.toLowerCase().includes(viewSearch.toLowerCase()) ||
|
|
||||||
(view.description && view.description.toLowerCase().includes(viewSearch.toLowerCase()))
|
|
||||||
);
|
);
|
||||||
}, [instanceDetails, viewSearch]);
|
}, [views, viewSearch]);
|
||||||
|
|
||||||
// 过滤 Jobs(基于选中的 View)
|
// 过滤任务
|
||||||
const filteredJobs = useMemo(() => {
|
const filteredJobs = useMemo(() => {
|
||||||
if (!instanceDetails || !selectedView) return [];
|
return jobs.filter((job) =>
|
||||||
let jobs = instanceDetails.jenkinsJobList.filter(
|
job.jobName.toLowerCase().includes(jobSearch.toLowerCase())
|
||||||
(job) => job.viewId === selectedView.id
|
|
||||||
);
|
);
|
||||||
if (jobSearch) {
|
}, [jobs, jobSearch]);
|
||||||
jobs = jobs.filter((job) =>
|
|
||||||
job.jobName.toLowerCase().includes(jobSearch.toLowerCase()) ||
|
// 过滤构建
|
||||||
(job.description && job.description.toLowerCase().includes(jobSearch.toLowerCase()))
|
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)
|
||||||
);
|
);
|
||||||
}
|
}, [builds, buildSearch]);
|
||||||
return jobs;
|
|
||||||
}, [instanceDetails, selectedView, jobSearch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
// 获取构建状态徽章
|
||||||
loadJenkinsList();
|
const getBuildStatusBadge = (status: string) => {
|
||||||
}, []);
|
const config = BUILD_STATUS_CONFIG[status] || BUILD_STATUS_CONFIG.NOT_BUILT;
|
||||||
|
const Icon = config.icon;
|
||||||
useEffect(() => {
|
|
||||||
if (selectedInstanceId) {
|
|
||||||
loadInstanceDetails();
|
|
||||||
}
|
|
||||||
}, [selectedInstanceId]);
|
|
||||||
|
|
||||||
// 空状态:没有实例
|
|
||||||
if (jenkinsList.length === 0) {
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<Badge variant={config.variant} className="inline-flex items-center gap-1">
|
||||||
<div className="flex items-center justify-between mb-6">
|
<Icon className="h-3 w-3" />
|
||||||
<h2 className="text-3xl font-bold tracking-tight">Jenkins 管理</h2>
|
{config.label}
|
||||||
</div>
|
</Badge>
|
||||||
<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">
|
const formatDuration = (duration?: number) => {
|
||||||
请先在外部服务管理中添加 Jenkins 系统
|
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>
|
</p>
|
||||||
<Button onClick={() => (window.location.href = '/deploy/external')}>
|
</div>
|
||||||
前往外部服务管理
|
|
||||||
</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">
|
|
||||||
<Select
|
<Select
|
||||||
value={selectedInstanceId?.toString()}
|
value={selectedInstanceId?.toString() || ''}
|
||||||
onValueChange={handleInstanceChange}
|
onValueChange={(value) => setSelectedInstanceId(Number(value))}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[300px]">
|
<SelectTrigger className="w-[240px]">
|
||||||
<SelectValue placeholder="选择Jenkins实例" />
|
<SelectValue placeholder="选择Jenkins实例" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{jenkinsList.map((jenkins) => (
|
{instances.map((instance) => (
|
||||||
<SelectItem key={jenkins.id} value={String(jenkins.id)}>
|
<SelectItem key={instance.id} value={instance.id.toString()}>
|
||||||
{jenkins.name}
|
{instance.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</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>
|
||||||
|
|
||||||
{/* 三栏布局 */}
|
{/* 三栏布局 */}
|
||||||
<div className="flex-1 grid grid-cols-12 gap-4 min-h-0 overflow-hidden">
|
<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">
|
<Card className="col-span-3 flex flex-col min-h-0">
|
||||||
<CardHeader className="border-b flex-shrink-0">
|
<CardHeader className="border-b flex-shrink-0">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<CardTitle className="text-base">
|
<CardTitle className="text-base flex items-center gap-2">
|
||||||
Views {instanceDetails && `(${filteredViews.length})`}
|
<Layers className="h-4 w-4" />
|
||||||
|
视图 ({filteredViews.length})
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@ -411,43 +402,39 @@ const JenkinsManagerList: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<div className="flex-1 overflow-auto">
|
<ScrollArea className="flex-1">
|
||||||
|
<div className="p-2 space-y-1">
|
||||||
{loading.views ? (
|
{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" />
|
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
) : filteredViews.length === 0 ? (
|
) : 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" />
|
<Layers className="h-12 w-12 mb-2 opacity-20" />
|
||||||
<p className="text-sm">暂无视图</p>
|
<p className="text-sm">暂无视图</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="p-2 space-y-2">
|
filteredViews.map((view) => (
|
||||||
{filteredViews.map((view) => (
|
|
||||||
<div
|
<div
|
||||||
key={view.id}
|
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
|
selectedView?.id === view.id
|
||||||
? 'bg-accent border-primary'
|
? 'bg-accent border-primary'
|
||||||
: ''
|
: ''
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleSelectView(view)}
|
onClick={() => setSelectedView(view)}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<div className="flex-1 min-w-0">
|
<span className="flex-1 truncate font-medium">
|
||||||
<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">
|
|
||||||
{view.viewName}
|
{view.viewName}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
{view.jobCount !== undefined && view.jobCount > 0 && (
|
||||||
{view.description && (
|
<Badge variant="secondary" className="text-xs px-1.5 py-0 h-5">
|
||||||
<p className="text-xs text-muted-foreground line-clamp-2">
|
{view.jobCount}
|
||||||
{view.description}
|
</Badge>
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
{view.viewUrl && (
|
{view.viewUrl && (
|
||||||
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<a
|
<a
|
||||||
@ -455,13 +442,9 @@ const JenkinsManagerList: React.FC = () => {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
className="flex-shrink-0"
|
className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-6 w-6"
|
|
||||||
>
|
>
|
||||||
|
<Button variant="ghost" size="icon" className="h-5 w-5">
|
||||||
<ExternalLink className="h-3 w-3" />
|
<ExternalLink className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
@ -470,21 +453,27 @@ const JenkinsManagerList: React.FC = () => {
|
|||||||
<p>在 Jenkins 中查看</p>
|
<p>在 Jenkins 中查看</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{view.description && (
|
||||||
))}
|
<p className="text-xs text-muted-foreground line-clamp-2">
|
||||||
</div>
|
{view.description}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 中栏:Jobs 列表 */}
|
{/* 中栏:任务列表 */}
|
||||||
<Card className="col-span-5 flex flex-col min-h-0">
|
<Card className="col-span-5 flex flex-col min-h-0">
|
||||||
<CardHeader className="border-b flex-shrink-0">
|
<CardHeader className="border-b flex-shrink-0">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<CardTitle className="text-base">
|
<CardTitle className="text-base">
|
||||||
Jobs {selectedView && `(${filteredJobs.length})`}
|
任务 {selectedView && `(${filteredJobs.length})`}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@ -525,64 +514,25 @@ const JenkinsManagerList: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="p-2 space-y-2">
|
<div className="p-2 space-y-2">
|
||||||
{filteredJobs.map((job) => {
|
{filteredJobs.map((job) => (
|
||||||
const statusInfo = getBuildStatusInfo(job.lastBuildStatus);
|
|
||||||
return (
|
|
||||||
<div
|
<div
|
||||||
key={job.id}
|
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
|
selectedJob?.id === job.id
|
||||||
? 'bg-accent border-primary'
|
? 'bg-accent border-primary'
|
||||||
: ''
|
: ''
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleSelectJob(job)}
|
onClick={() => setSelectedJob(job)}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<div className="flex-1 min-w-0">
|
<span className="flex-1 truncate font-medium">{job.jobName}</span>
|
||||||
<div className="flex items-center gap-2 mb-1">
|
{job.buildCount !== undefined && job.buildCount > 0 && (
|
||||||
<Box className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
|
<Badge variant="secondary" className="text-xs px-1.5 py-0 h-5">
|
||||||
<span className="font-medium truncate">
|
{job.buildCount}
|
||||||
{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}
|
|
||||||
</Badge>
|
</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 && (
|
{job.jobUrl && (
|
||||||
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<a
|
<a
|
||||||
@ -590,13 +540,9 @@ const JenkinsManagerList: React.FC = () => {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
className="flex-shrink-0"
|
className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-6 w-6"
|
|
||||||
>
|
>
|
||||||
|
<Button variant="ghost" size="icon" className="h-6 w-6">
|
||||||
<ExternalLink className="h-3 w-3" />
|
<ExternalLink className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
@ -605,27 +551,67 @@ const JenkinsManagerList: React.FC = () => {
|
|||||||
<p>在 Jenkins 中查看</p>
|
<p>在 Jenkins 中查看</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 右栏:Builds 列表(待实现) */}
|
{/* 右栏:构建列表 */}
|
||||||
<Card className="col-span-4 flex flex-col min-h-0">
|
<Card className="col-span-4 flex flex-col min-h-0">
|
||||||
<CardHeader className="border-b flex-shrink-0">
|
<CardHeader className="border-b flex-shrink-0">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<CardTitle className="text-base">Builds</CardTitle>
|
<CardTitle className="text-base">
|
||||||
|
构建 {selectedJob && `(${filteredBuilds.length})`}
|
||||||
|
</CardTitle>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-7 w-7"
|
className="h-7 w-7"
|
||||||
onClick={handleSyncBuilds}
|
onClick={handleSyncBuilds}
|
||||||
disabled={syncing.builds || !selectedJob}
|
disabled={syncing.builds || !selectedInstanceId}
|
||||||
>
|
>
|
||||||
<RefreshCw
|
<RefreshCw
|
||||||
className={`h-4 w-4 ${syncing.builds ? 'animate-spin' : ''}`}
|
className={`h-4 w-4 ${syncing.builds ? 'animate-spin' : ''}`}
|
||||||
@ -643,24 +629,79 @@ const JenkinsManagerList: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<div className="flex-1 overflow-auto">
|
<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">
|
<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>
|
<p className="text-sm">请先选择任务</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : filteredBuilds.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 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>
|
<p className="text-sm">暂无构建</p>
|
||||||
<p className="text-xs mt-1">等待接口定义</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</PageContainer>
|
</div>
|
||||||
</TooltipProvider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default JenkinsManagerList;
|
export default JenkinsManager;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import request from '@/utils/request';
|
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 { getExternalSystems } from '@/pages/Deploy/External/service';
|
||||||
import { SystemType } from '@/pages/Deploy/External/types';
|
import { SystemType } from '@/pages/Deploy/External/types';
|
||||||
|
|
||||||
@ -10,18 +10,36 @@ export const getJenkinsInstances = () =>
|
|||||||
enabled: true
|
enabled: true
|
||||||
}).then(response => response.content);
|
}).then(response => response.content);
|
||||||
|
|
||||||
// 获取 Jenkins 实例详情
|
// ==================== Jenkins 视图 ====================
|
||||||
export const getJenkinsInstance = (externalSystemId: string) =>
|
|
||||||
request.get<JenkinsInstanceDTO>(`/api/v1/jenkins-manager/${externalSystemId}/instance`);
|
// 获取视图列表
|
||||||
|
export const getJenkinsViews = (externalSystemId: number) =>
|
||||||
|
request.get<JenkinsViewDTO[]>(`/api/v1/jenkins-view`, {
|
||||||
|
params: { externalSystemId }
|
||||||
|
});
|
||||||
|
|
||||||
// 同步视图
|
// 同步视图
|
||||||
export const syncViews = (externalSystemId: string) =>
|
export const syncJenkinsViews = (externalSystemId: number) =>
|
||||||
request.post<void>(`/api/v1/jenkins-manager/${externalSystemId}/sync-views`);
|
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) =>
|
export const syncJenkinsJobs = (params: { externalSystemId: number; viewId?: number }) =>
|
||||||
request.post<void>(`/api/v1/jenkins-manager/${externalSystemId}/sync-jobs`);
|
request.post<void>(`/api/v1/jenkins-job/sync`, null, { params });
|
||||||
|
|
||||||
// 同步构建
|
// ==================== Jenkins 构建 ====================
|
||||||
export const syncBuilds = (externalSystemId: string) =>
|
|
||||||
request.post<void>(`/api/v1/jenkins-manager/${externalSystemId}/sync-builds`);
|
// 获取构建列表
|
||||||
|
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 });
|
||||||
|
|||||||
@ -18,25 +18,39 @@ export interface JenkinsInstanceDTO extends BaseResponse {
|
|||||||
|
|
||||||
// Jenkins 视图
|
// Jenkins 视图
|
||||||
export interface JenkinsViewDTO extends BaseResponse {
|
export interface JenkinsViewDTO extends BaseResponse {
|
||||||
description: string;
|
description?: string;
|
||||||
externalSystemId: number;
|
externalSystemId: number;
|
||||||
viewName: string;
|
viewName: string;
|
||||||
viewUrl: string;
|
viewUrl: string;
|
||||||
|
jobCount?: number; // 关联的任务数量
|
||||||
}
|
}
|
||||||
|
|
||||||
// Jenkins 任务
|
// Jenkins 任务
|
||||||
export interface JenkinsJobDTO extends BaseResponse {
|
export interface JenkinsJobDTO extends BaseResponse {
|
||||||
buildable: boolean;
|
buildable?: boolean;
|
||||||
description: string;
|
description?: string;
|
||||||
jobName: string;
|
jobName: string;
|
||||||
jobUrl: string;
|
jobUrl: string;
|
||||||
nextBuildNumber: number;
|
nextBuildNumber?: number;
|
||||||
lastBuildNumber: number;
|
lastBuildNumber?: number;
|
||||||
lastBuildStatus: string;
|
lastBuildStatus?: string;
|
||||||
healthReportScore: number;
|
healthReportScore?: number;
|
||||||
lastBuildTime: string;
|
lastBuildTime?: string;
|
||||||
externalSystemId: number;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 同步类型
|
// 同步类型
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user