This commit is contained in:
asp_ly 2024-12-28 21:40:07 +08:00
parent 244989c3ee
commit 7c8b7dfe2f
5 changed files with 296 additions and 340 deletions

View File

@ -22,7 +22,7 @@ const ExternalPage: React.FC = () => {
refresh refresh
} = useTableData<ExternalSystemResponse, ExternalSystemQuery, ExternalSystemRequest, ExternalSystemRequest>({ } = useTableData<ExternalSystemResponse, ExternalSystemQuery, ExternalSystemRequest, ExternalSystemRequest>({
service: { service: {
list: service.getExternalSystems, list: service.getExternalSystemsPage,
create: service.createExternalSystem, create: service.createExternalSystem,
update: service.updateExternalSystem, update: service.updateExternalSystem,
delete: service.deleteExternalSystem delete: service.deleteExternalSystem

View File

@ -5,6 +5,9 @@ import type { ExternalSystemResponse, ExternalSystemRequest, ExternalSystemQuery
const BASE_URL = '/api/v1/external-system'; const BASE_URL = '/api/v1/external-system';
// 获取三方系统列表 // 获取三方系统列表
export const getExternalSystemsPage = (params: ExternalSystemQuery) =>
request.get<Page<ExternalSystemResponse>>(`${BASE_URL}/page`, { params });
export const getExternalSystems = (params: ExternalSystemQuery) => export const getExternalSystems = (params: ExternalSystemQuery) =>
request.get<Page<ExternalSystemResponse>>(`${BASE_URL}/page`, { params }); request.get<Page<ExternalSystemResponse>>(`${BASE_URL}/page`, { params });

View File

@ -1,22 +1,11 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { PageContainer } from '@/components/ui/page-container'; import { PageContainer } from '@/components/ui/page-container';
import { Plus, Pencil, Trash2, Loader2 } from 'lucide-react'; import { RefreshCw, ChevronRight } from 'lucide-react';
import {
Table,
TableHeader,
TableBody,
TableHead,
TableRow,
TableCell,
} from "@/components/ui/table";
import { import {
Card, Card,
CardContent, CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { import {
Select, Select,
SelectContent, SelectContent,
@ -26,71 +15,54 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
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 { import { Separator } from "@/components/ui/separator";
AlertDialog, import type { JenkinsInstance, JenkinsView, SyncType } from './types';
AlertDialogAction, import { getJenkinsInstances, getJenkinsViews, syncJenkinsData } from './service';
AlertDialogCancel, import { SyncStatus } from '@/pages/Deploy/External/types';
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { searchFormSchema, type SearchFormValues } from "./schema";
import { DataTablePagination } from "@/components/ui/pagination";
import type { JenkinsManagerResponse, JenkinsManagerQuery } from './types';
import { getJenkinsManagerPage, deleteJenkinsManager } from './service';
import JenkinsManagerModal from './components/JenkinsManagerModal';
interface Column {
accessorKey?: keyof JenkinsManagerResponse;
id?: string;
header: string;
size: number;
cell?: (props: { row: { original: JenkinsManagerResponse } }) => React.ReactNode;
}
const JenkinsManagerList: React.FC = () => { const JenkinsManagerList: React.FC = () => {
const [modalVisible, setModalVisible] = useState(false); const [jenkinsList, setJenkinsList] = useState<JenkinsInstance[]>([]);
const [currentRecord, setCurrentRecord] = useState<JenkinsManagerResponse>(); const [currentJenkinsId, setCurrentJenkinsId] = useState<string>();
const [list, setList] = useState<JenkinsManagerResponse[]>([]); const [currentJenkins, setCurrentJenkins] = useState<JenkinsInstance>();
const [views, setViews] = useState<JenkinsView[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [pagination, setPagination] = useState({ const [syncing, setSyncing] = useState<Record<SyncType, boolean>>({
pageNum: 1, views: false,
pageSize: 10, jobs: false,
totalElements: 0, builds: false
}); });
const { toast } = useToast(); const { toast } = useToast();
const form = useForm<SearchFormValues>({ // 获取 Jenkins 实例列表
resolver: zodResolver(searchFormSchema), const loadJenkinsList = async () => {
defaultValues: {
name: "",
enabled: undefined,
}
});
const loadData = async (params?: JenkinsManagerQuery) => {
setLoading(true);
try { try {
const queryParams: JenkinsManagerQuery = { const data = await getJenkinsInstances();
...params, setJenkinsList(data);
pageNum: pagination.pageNum, // 如果没有选中的实例,默认选择第一个
pageSize: pagination.pageSize, if (!currentJenkinsId && data.length > 0) {
}; setCurrentJenkinsId(String(data[0].id));
const data = await getJenkinsManagerPage(queryParams); setCurrentJenkins(data[0]);
setList(data.content || []); }
setPagination({
...pagination,
totalElements: data.totalElements,
});
} catch (error) { } catch (error) {
toast({ toast({
variant: "destructive", variant: "destructive",
title: "获取列表失败", title: "获取 Jenkins 实例列表失败",
duration: 3000,
});
}
};
// 获取视图列表
const loadViews = async () => {
if (!currentJenkinsId) return;
setLoading(true);
try {
const data = await getJenkinsViews(currentJenkinsId);
setViews(data);
} catch (error) {
toast({
variant: "destructive",
title: "获取视图列表失败",
duration: 3000, duration: 3000,
}); });
} finally { } finally {
@ -98,261 +70,199 @@ const JenkinsManagerList: React.FC = () => {
} }
}; };
const handlePageChange = (page: number) => { // 切换 Jenkins 实例
setPagination({ const handleJenkinsChange = (id: string) => {
...pagination, setCurrentJenkinsId(id);
pageNum: page, const jenkins = jenkinsList.find(j => String(j.id) === id);
}); setCurrentJenkins(jenkins);
setViews([]);
}; };
useEffect(() => { // 同步数据
loadData(form.getValues()); const handleSync = async (type: SyncType) => {
}, [pagination.pageNum, pagination.pageSize]); if (!currentJenkinsId) return;
setSyncing(prev => ({ ...prev, [type]: true }));
const handleDelete = async (id: number) => {
try { try {
await deleteJenkinsManager(id); await syncJenkinsData(currentJenkinsId, type);
await loadJenkinsList();
if (type === 'views') {
await loadViews();
}
toast({ toast({
title: "删除成功", title: "同步成功",
duration: 3000, duration: 3000,
}); });
loadData(form.getValues());
} catch (error) { } catch (error) {
toast({ toast({
variant: "destructive", variant: "destructive",
title: "删除失败", title: "同步失败",
duration: 3000, duration: 3000,
}); });
} finally {
setSyncing(prev => ({ ...prev, [type]: false }));
} }
}; };
const handleAdd = () => { useEffect(() => {
setCurrentRecord(undefined); loadJenkinsList();
setModalVisible(true); }, []);
useEffect(() => {
if (currentJenkinsId) {
loadViews();
}
}, [currentJenkinsId]);
const formatTime = (time: string | null) => {
if (!time) return 'Never';
return time;
}; };
const handleEdit = (record: JenkinsManagerResponse) => { // 模拟统计数据
setCurrentRecord(record); const mockStats = {
setModalVisible(true); views: 5,
jobs: 20,
builds: 100
}; };
const handleModalClose = () => {
setModalVisible(false);
setCurrentRecord(undefined);
};
const handleSuccess = () => {
setModalVisible(false);
setCurrentRecord(undefined);
loadData(form.getValues());
};
const handleReset = () => {
form.reset({
name: "",
enabled: undefined,
});
loadData();
};
const columns: Column[] = [
{
accessorKey: 'name',
header: '名称',
size: 180,
},
{
accessorKey: 'description',
header: '描述',
size: 200,
},
{
accessorKey: 'enabled',
header: '状态',
size: 100,
cell: ({row}) => (
<Badge variant={row.original.enabled ? "outline" : "secondary"}>
{row.original.enabled ? '启用' : '禁用'}
</Badge>
),
},
{
accessorKey: 'sort',
header: '排序',
size: 80,
},
{
accessorKey: 'createTime',
header: '创建时间',
size: 180,
},
{
id: 'actions',
header: '操作',
size: 180,
cell: ({row}) => (
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleEdit(row.original)}
>
<Pencil className="mr-2 h-4 w-4" />
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="ghost"
size="sm"
className="text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction
onClick={() => handleDelete(row.original.id)}
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
),
},
];
return ( return (
<PageContainer> <PageContainer>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between mb-6">
<h2 className="text-3xl font-bold tracking-tight">Jenkins管理</h2> <h2 className="text-3xl font-bold tracking-tight">Jenkins Management</h2>
<div className="flex items-center gap-4"> <Select
<Button onClick={handleAdd}> value={currentJenkinsId}
<Plus className="mr-2 h-4 w-4" /> onValueChange={handleJenkinsChange}
>
</Button> <SelectTrigger className="w-[300px]">
</div> <SelectValue placeholder="Select a Jenkins instance"/>
</SelectTrigger>
<SelectContent>
{jenkinsList.map(jenkins => (
<SelectItem key={jenkins.id} value={String(jenkins.id)}>
<div className="flex items-center gap-2">
<Badge variant={jenkins.syncStatus === SyncStatus.SUCCESS ? "outline" : "secondary"}>
{jenkins.name}
</Badge>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div> </div>
<Card> {currentJenkins && (
<div className="p-6"> <Card className="mb-6">
<div className="flex items-center gap-4"> <CardContent className="pt-6">
<Input <div className="space-y-4">
placeholder="名称" <div>
{...form.register('name')} <h3 className="text-lg font-semibold">{currentJenkins.name}</h3>
className="max-w-[200px]" <p className="text-sm text-muted-foreground">{currentJenkins.url}</p>
/> </div>
<Select
value={form.getValues('enabled')?.toString()} <div className="grid grid-cols-3 gap-4">
onValueChange={(value) => form.setValue('enabled', value === 'true')} <div className="space-y-2">
> <div className="flex items-center justify-between">
<SelectTrigger className="max-w-[200px]"> <span className="text-sm font-medium">Views</span>
<SelectValue placeholder="状态"/> <span className="text-2xl font-bold">{mockStats.views}</span>
</SelectTrigger> </div>
<SelectContent> <div className="flex items-center justify-between">
<SelectItem value="true"></SelectItem> <span className="text-sm text-muted-foreground">Last sync: {formatTime(currentJenkins.lastSyncTime)}</span>
<SelectItem value="false"></SelectItem> <Button
</SelectContent> variant="ghost"
</Select> size="sm"
<Button variant="outline" onClick={handleReset}> onClick={() => handleSync('views')}
disabled={syncing.views}
</Button> >
<Button onClick={() => loadData(form.getValues())}> <RefreshCw className={`h-4 w-4 ${syncing.views ? 'animate-spin' : ''}`} />
<span className="ml-2">Sync Views</span>
</Button> </Button>
</div> </div>
</div> </div>
</Card>
<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>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Last sync: {formatTime(currentJenkins.lastSyncTime)}</span>
<Button
variant="ghost"
size="sm"
onClick={() => handleSync('jobs')}
disabled={syncing.jobs}
>
<RefreshCw className={`h-4 w-4 ${syncing.jobs ? 'animate-spin' : ''}`} />
<span className="ml-2">Sync Jobs</span>
</Button>
</div>
</div>
<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>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Last sync: {formatTime(currentJenkins.lastSyncTime)}</span>
<Button
variant="ghost"
size="sm"
onClick={() => handleSync('builds')}
disabled={syncing.builds}
>
<RefreshCw className={`h-4 w-4 ${syncing.builds ? 'animate-spin' : ''}`} />
<span className="ml-2">Sync Builds</span>
</Button>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
)}
<Card> <Card>
<CardHeader className="flex flex-row items-center justify-between"> <CardContent className="pt-6">
<CardTitle></CardTitle> <h3 className="text-lg font-semibold mb-4">Views</h3>
</CardHeader> <div className="space-y-4">
<CardContent> {loading ? (
<div className="rounded-md border"> <div className="flex items-center justify-center py-8">
<Table> <RefreshCw className="h-6 w-6 animate-spin" />
<TableHeader> </div>
<TableRow> ) : views.map((view, index) => (
{columns.map((column) => ( <div key={view.id}>
<TableHead {index > 0 && <Separator className="my-4" />}
key={column.accessorKey || column.id} <div className="space-y-4">
style={{width: column.size}} <div className="flex items-center justify-between">
> <h4 className="text-base font-medium">{view.name}</h4>
{column.header} <Button variant="ghost" size="sm">
</TableHead> <ChevronRight className="h-4 w-4" />
))} </Button>
</TableRow> </div>
</TableHeader> {view.jobs.length > 0 && (
<TableBody> <div className="space-y-2">
{loading ? ( {view.jobs.map(job => (
<TableRow> <div key={job.id} className="flex items-center justify-between text-sm">
<TableCell <span>{job.name}</span>
colSpan={columns.length} {job.lastBuild && (
className="h-24 text-center" <div className="flex items-center gap-2">
> <Badge variant={job.lastBuild.result === 'SUCCESS' ? 'outline' : 'destructive'}>
<div className="flex justify-center items-center"> #{job.lastBuild.number} - {job.lastBuild.result}
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> </Badge>
... <span className="text-muted-foreground">{job.lastBuild.timestamp}</span>
</div> </div>
</TableCell> )}
</TableRow> </div>
) : list.length === 0 ? (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
</TableCell>
</TableRow>
) : (
list.map((item) => (
<TableRow key={item.id}>
{columns.map((column) => (
<TableCell
key={column.accessorKey || column.id}
>
{column.cell
? column.cell({row: {original: item}})
: item[column.accessorKey!]}
</TableCell>
))} ))}
</TableRow> </div>
)) )}
)} </div>
</TableBody> </div>
</Table> ))}
<div className="flex justify-end border-t border-border bg-muted/40">
<DataTablePagination
pageIndex={pagination.pageNum}
pageSize={pagination.pageSize}
pageCount={Math.ceil(pagination.totalElements / pagination.pageSize)}
onPageChange={handlePageChange}
/>
</div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
{modalVisible && (
<JenkinsManagerModal
open={modalVisible}
onCancel={handleModalClose}
onSuccess={handleSuccess}
initialValues={currentRecord}
/>
)}
</PageContainer> </PageContainer>
); );
}; };

View File

@ -1,33 +1,66 @@
import request from '@/utils/request'; import request from '@/utils/request';
import type { Page } from '@/types/base'; import type { JenkinsInstance, JenkinsView, SyncType } from './types';
import type { JenkinsManagerResponse, JenkinsManagerQuery, JenkinsManagerRequest } from './types'; import { getExternalSystems } from '@/pages/Deploy/External/service';
import { SystemType } from '@/pages/Deploy/External/types';
const BASE_URL = '/api/v1/jenkins-manager'; const BASE_URL = '/api/v1/jenkins-manager';
// 创建 // 获取 Jenkins 实例列表
export const createJenkinsManager = (data: JenkinsManagerRequest) => export const getJenkinsInstances = () =>
request.post<void>(BASE_URL, data); getExternalSystems({
type: SystemType.JENKINS,
enabled: true
}).then(response => response.content);
// 更新 // 获取 Jenkins 视图列表
export const updateJenkinsManager = (id: number, data: JenkinsManagerRequest) => export const getJenkinsViews = (jenkinsId: string) =>
request.put<void>(`${BASE_URL}/${id}`, data); // 模拟数据
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 deleteJenkinsManager = (id: number) => export const syncJenkinsData = (jenkinsId: string, type: SyncType) =>
request.delete<void>(`${BASE_URL}/${id}`); request.post<void>(`${BASE_URL}/${jenkinsId}/sync/${type}`);
// 获取详情
export const getJenkinsManager = (id: number) =>
request.get<JenkinsManagerResponse>(`${BASE_URL}/${id}`);
// 分页查询列表
export const getJenkinsManagerPage = (params?: JenkinsManagerQuery) =>
request.get<Page<JenkinsManagerResponse>>(`${BASE_URL}/page`, { params });
// 获取所有列表
export const getJenkinsManagerList = () =>
request.get<JenkinsManagerResponse[]>(BASE_URL);
// 条件查询列表
export const getJenkinsManagerListByCondition = (params?: JenkinsManagerQuery) =>
request.get<JenkinsManagerResponse[]>(`${BASE_URL}/list`, { params });

View File

@ -1,23 +1,33 @@
import type { BaseResponse, BaseQuery } from '@/types/base'; import type { BaseResponse } from '@/types/base';
import type { ExternalSystemResponse } from '@/pages/Deploy/External/types';
export interface JenkinsManagerResponse extends BaseResponse { // 使用外部系统响应作为 Jenkins 实例
name: string; export type JenkinsInstance = ExternalSystemResponse;
description?: string;
enabled: boolean; // Jenkins 视图类型
sort: number; export interface JenkinsView {
// TODO: 添加其他字段 id: string;
name: string;
url: string;
jobs: JenkinsJob[];
} }
export interface JenkinsManagerQuery extends BaseQuery { // Jenkins Job 类型
name?: string; export interface JenkinsJob {
enabled?: boolean; id: string;
// TODO: 添加其他查询字段 name: string;
url: string;
lastBuild?: JenkinsBuild;
} }
export interface JenkinsManagerRequest { // Jenkins 构建类型
name: string; export interface JenkinsBuild {
description?: string; id: string;
enabled: boolean; number: number;
sort: number; result: 'SUCCESS' | 'FAILURE' | 'RUNNING' | 'ABORTED';
// TODO: 添加其他请求字段 timestamp: string;
url: string;
} }
// 同步类型
export type SyncType = 'views' | 'jobs' | 'builds';