1
This commit is contained in:
parent
244989c3ee
commit
7c8b7dfe2f
2
frontend/src/pages/Deploy/External/index.tsx
vendored
2
frontend/src/pages/Deploy/External/index.tsx
vendored
@ -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
|
||||||
|
|||||||
@ -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 });
|
||||||
|
|
||||||
|
|||||||
@ -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">
|
|
||||||
<Button onClick={handleAdd}>
|
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
|
||||||
新建
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<div className="p-6">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<Input
|
|
||||||
placeholder="名称"
|
|
||||||
{...form.register('name')}
|
|
||||||
className="max-w-[200px]"
|
|
||||||
/>
|
|
||||||
<Select
|
<Select
|
||||||
value={form.getValues('enabled')?.toString()}
|
value={currentJenkinsId}
|
||||||
onValueChange={(value) => form.setValue('enabled', value === 'true')}
|
onValueChange={handleJenkinsChange}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="max-w-[200px]">
|
<SelectTrigger className="w-[300px]">
|
||||||
<SelectValue placeholder="状态"/>
|
<SelectValue placeholder="Select a Jenkins instance"/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="true">启用</SelectItem>
|
{jenkinsList.map(jenkins => (
|
||||||
<SelectItem value="false">禁用</SelectItem>
|
<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>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Button variant="outline" onClick={handleReset}>
|
|
||||||
重置
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => loadData(form.getValues())}>
|
|
||||||
搜索
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
{currentJenkins && (
|
||||||
<CardHeader className="flex flex-row items-center justify-between">
|
<Card className="mb-6">
|
||||||
<CardTitle>列表</CardTitle>
|
<CardContent className="pt-6">
|
||||||
</CardHeader>
|
<div className="space-y-4">
|
||||||
<CardContent>
|
<div>
|
||||||
<div className="rounded-md border">
|
<h3 className="text-lg font-semibold">{currentJenkins.name}</h3>
|
||||||
<Table>
|
<p className="text-sm text-muted-foreground">{currentJenkins.url}</p>
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
{columns.map((column) => (
|
|
||||||
<TableHead
|
|
||||||
key={column.accessorKey || column.id}
|
|
||||||
style={{width: column.size}}
|
|
||||||
>
|
|
||||||
{column.header}
|
|
||||||
</TableHead>
|
|
||||||
))}
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{loading ? (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell
|
|
||||||
colSpan={columns.length}
|
|
||||||
className="h-24 text-center"
|
|
||||||
>
|
|
||||||
<div className="flex justify-center items-center">
|
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
加载中...
|
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
<div className="grid grid-cols-3 gap-4">
|
||||||
) : list.length === 0 ? (
|
<div className="space-y-2">
|
||||||
<TableRow>
|
<div className="flex items-center justify-between">
|
||||||
<TableCell
|
<span className="text-sm font-medium">Views</span>
|
||||||
colSpan={columns.length}
|
<span className="text-2xl font-bold">{mockStats.views}</span>
|
||||||
className="h-24 text-center"
|
</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('views')}
|
||||||
|
disabled={syncing.views}
|
||||||
>
|
>
|
||||||
暂无数据
|
<RefreshCw className={`h-4 w-4 ${syncing.views ? 'animate-spin' : ''}`} />
|
||||||
</TableCell>
|
<span className="ml-2">Sync Views</span>
|
||||||
</TableRow>
|
</Button>
|
||||||
) : (
|
</div>
|
||||||
list.map((item) => (
|
</div>
|
||||||
<TableRow key={item.id}>
|
|
||||||
{columns.map((column) => (
|
<div className="space-y-2">
|
||||||
<TableCell
|
<div className="flex items-center justify-between">
|
||||||
key={column.accessorKey || column.id}
|
<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}
|
||||||
>
|
>
|
||||||
{column.cell
|
<RefreshCw className={`h-4 w-4 ${syncing.jobs ? 'animate-spin' : ''}`} />
|
||||||
? column.cell({row: {original: item}})
|
<span className="ml-2">Sync Jobs</span>
|
||||||
: item[column.accessorKey!]}
|
</Button>
|
||||||
</TableCell>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</TableRow>
|
|
||||||
))
|
<div className="space-y-2">
|
||||||
)}
|
<div className="flex items-center justify-between">
|
||||||
</TableBody>
|
<span className="text-sm font-medium">Builds</span>
|
||||||
</Table>
|
<span className="text-2xl font-bold">{mockStats.builds}</span>
|
||||||
<div className="flex justify-end border-t border-border bg-muted/40">
|
</div>
|
||||||
<DataTablePagination
|
<div className="flex items-center justify-between">
|
||||||
pageIndex={pagination.pageNum}
|
<span className="text-sm text-muted-foreground">Last sync: {formatTime(currentJenkins.lastSyncTime)}</span>
|
||||||
pageSize={pagination.pageSize}
|
<Button
|
||||||
pageCount={Math.ceil(pagination.totalElements / pagination.pageSize)}
|
variant="ghost"
|
||||||
onPageChange={handlePageChange}
|
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>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{modalVisible && (
|
|
||||||
<JenkinsManagerModal
|
|
||||||
open={modalVisible}
|
|
||||||
onCancel={handleModalClose}
|
|
||||||
onSuccess={handleSuccess}
|
|
||||||
initialValues={currentRecord}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<h3 className="text-lg font-semibold mb-4">Views</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center py-8">
|
||||||
|
<RefreshCw className="h-6 w-6 animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : views.map((view, index) => (
|
||||||
|
<div key={view.id}>
|
||||||
|
{index > 0 && <Separator className="my-4" />}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h4 className="text-base font-medium">{view.name}</h4>
|
||||||
|
<Button variant="ghost" size="sm">
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{view.jobs.length > 0 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{view.jobs.map(job => (
|
||||||
|
<div key={job.id} className="flex items-center justify-between text-sm">
|
||||||
|
<span>{job.name}</span>
|
||||||
|
{job.lastBuild && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant={job.lastBuild.result === 'SUCCESS' ? 'outline' : 'destructive'}>
|
||||||
|
#{job.lastBuild.number} - {job.lastBuild.result}
|
||||||
|
</Badge>
|
||||||
|
<span className="text-muted-foreground">{job.lastBuild.timestamp}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 });
|
|
||||||
|
|||||||
@ -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 实例
|
||||||
|
export type JenkinsInstance = ExternalSystemResponse;
|
||||||
|
|
||||||
|
// Jenkins 视图类型
|
||||||
|
export interface JenkinsView {
|
||||||
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
url: string;
|
||||||
enabled: boolean;
|
jobs: JenkinsJob[];
|
||||||
sort: number;
|
|
||||||
// TODO: 添加其他字段
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface JenkinsManagerQuery extends BaseQuery {
|
// Jenkins Job 类型
|
||||||
name?: string;
|
export interface JenkinsJob {
|
||||||
enabled?: boolean;
|
id: string;
|
||||||
// TODO: 添加其他查询字段
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface JenkinsManagerRequest {
|
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
url: string;
|
||||||
enabled: boolean;
|
lastBuild?: JenkinsBuild;
|
||||||
sort: number;
|
|
||||||
// TODO: 添加其他请求字段
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Jenkins 构建类型
|
||||||
|
export interface JenkinsBuild {
|
||||||
|
id: string;
|
||||||
|
number: number;
|
||||||
|
result: 'SUCCESS' | 'FAILURE' | 'RUNNING' | 'ABORTED';
|
||||||
|
timestamp: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同步类型
|
||||||
|
export type SyncType = 'views' | 'jobs' | 'builds';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user