diff --git a/frontend/src/components/ui/pagination.tsx b/frontend/src/components/ui/pagination.tsx index 0d7ef77f..0dc2b2c8 100644 --- a/frontend/src/components/ui/pagination.tsx +++ b/frontend/src/components/ui/pagination.tsx @@ -118,12 +118,15 @@ const DataTablePagination: React.FC = ({ pageCount, onPageChange, }) => { + // 将 0-based 的 pageIndex 转换为 1-based 的显示页码 + const currentPage = pageIndex + 1; + const renderPageNumbers = () => { const pages = []; const maxVisiblePages = 5; const halfMaxVisiblePages = Math.floor(maxVisiblePages / 2); - let startPage = Math.max(1, pageIndex - halfMaxVisiblePages); + let startPage = Math.max(1, currentPage - halfMaxVisiblePages); let endPage = Math.min(pageCount, startPage + maxVisiblePages - 1); if (endPage - startPage + 1 < maxVisiblePages) { @@ -134,7 +137,12 @@ const DataTablePagination: React.FC = ({ if (startPage > 1) { pages.push( - onPageChange(1)}>1 + onPageChange(0)} + > + 1 + ); if (startPage > 2) { @@ -151,8 +159,8 @@ const DataTablePagination: React.FC = ({ pages.push( onPageChange(i)} + isActive={i === currentPage} + onClick={() => onPageChange(i - 1)} > {i} @@ -171,7 +179,10 @@ const DataTablePagination: React.FC = ({ } pages.push( - onPageChange(pageCount)}> + onPageChange(pageCount - 1)} + > {pageCount} @@ -186,15 +197,15 @@ const DataTablePagination: React.FC = ({ onPageChange(Math.max(1, pageIndex - 1))} - className={cn(pageIndex <= 1 && "pointer-events-none opacity-50")} + onClick={() => onPageChange(Math.max(0, pageIndex - 1))} + className={cn(pageIndex <= 0 && "pointer-events-none opacity-50")} /> {renderPageNumbers()} onPageChange(Math.min(pageCount, pageIndex + 1))} - className={cn(pageIndex >= pageCount && "pointer-events-none opacity-50")} + onClick={() => onPageChange(Math.min(pageCount - 1, pageIndex + 1))} + className={cn(pageIndex >= pageCount - 1 && "pointer-events-none opacity-50")} /> diff --git a/frontend/src/pages/Deploy/JenkinsManager/List/index.tsx b/frontend/src/pages/Deploy/JenkinsManager/List/index.tsx index 0f17b839..fdb183e0 100644 --- a/frontend/src/pages/Deploy/JenkinsManager/List/index.tsx +++ b/frontend/src/pages/Deploy/JenkinsManager/List/index.tsx @@ -344,9 +344,10 @@ const JenkinsManager: React.FC = () => { }; return ( -
+ +
{/* 页面标题 */} -
+

Jenkins 管理

@@ -403,7 +404,7 @@ const JenkinsManager: React.FC = () => {

-
+
{loading.views ? (
@@ -434,26 +435,24 @@ const JenkinsManager: React.FC = () => { )} {view.viewUrl && ( - - - - e.stopPropagation()} - className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity" - > - - - - -

在 Jenkins 中查看

-
-
-
+ + + e.stopPropagation()} + className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity" + > + + + + +

在 Jenkins 中查看

+
+
)}
{view.description && ( @@ -532,26 +531,24 @@ const JenkinsManager: React.FC = () => { )} {job.jobUrl && ( - - - - e.stopPropagation()} - className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity" - > - - - - -

在 Jenkins 中查看

-
-
-
+ + + e.stopPropagation()} + className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity" + > + + + + +

在 Jenkins 中查看

+
+
)}
@@ -564,25 +561,23 @@ const JenkinsManager: React.FC = () => {
{job.lastBuildStatus && getBuildStatusBadge(job.lastBuildStatus)} {job.healthReportScore !== undefined && ( - - - -
- - - - {job.healthReportScore}% - -
-
- - 健康度: {job.healthReportScore}% - -
-
+ + +
+ + + + {job.healthReportScore}% + +
+
+ + 健康度: {job.healthReportScore}% + +
)}
@@ -658,25 +653,23 @@ const JenkinsManager: React.FC = () => { {getBuildStatusBadge(build.buildStatus)}
{build.buildUrl && ( - - - - - - - - -

在 Jenkins 中查看

-
-
-
+ + + + + + + +

在 Jenkins 中查看

+
+
)}
@@ -701,6 +694,7 @@ const JenkinsManager: React.FC = () => {
+
); }; diff --git a/frontend/src/pages/Deploy/ScheduleJob/List/components/JobEditDialog.tsx b/frontend/src/pages/Deploy/ScheduleJob/List/components/JobEditDialog.tsx index 5752fc0b..6deda110 100644 --- a/frontend/src/pages/Deploy/ScheduleJob/List/components/JobEditDialog.tsx +++ b/frontend/src/pages/Deploy/ScheduleJob/List/components/JobEditDialog.tsx @@ -23,7 +23,7 @@ import { Switch } from '@/components/ui/switch'; import { useToast } from '@/components/ui/use-toast'; import { useForm } from 'react-hook-form'; import { Loader2 } from 'lucide-react'; -import { createScheduleJob, updateScheduleJob } from '../service'; +import { createScheduleJob, updateScheduleJob, updateJobCron } from '../service'; import type { ScheduleJobResponse, ScheduleJobRequest, JobCategoryResponse } from '../types'; interface JobEditDialogProps { @@ -46,6 +46,8 @@ const JobEditDialog: React.FC = ({ }) => { const { toast } = useToast(); const [submitting, setSubmitting] = React.useState(false); + // 保存原始的 Cron 表达式,用于判断是否需要更新调度器 + const [originalCronExpression, setOriginalCronExpression] = React.useState(''); const form = useForm({ defaultValues: { @@ -69,7 +71,8 @@ const JobEditDialog: React.FC = ({ useEffect(() => { if (open) { if (job) { - // 编辑模式 + // 编辑模式 - 保存原始的 Cron 表达式 + setOriginalCronExpression(job.cronExpression); form.reset({ jobName: job.jobName, jobDescription: job.jobDescription || '', @@ -86,7 +89,8 @@ const JobEditDialog: React.FC = ({ alertEmail: job.alertEmail || '', }); } else { - // 新建模式 + // 新建模式 - 重置原始 Cron 表达式 + setOriginalCronExpression(''); form.reset({ jobName: '', jobDescription: '', @@ -110,11 +114,35 @@ const JobEditDialog: React.FC = ({ setSubmitting(true); try { if (job) { + // 更新任务 await updateScheduleJob(job.id, values); - toast({ - title: '更新成功', - description: `任务 "${values.jobName}" 已更新`, - }); + + // 检查 Cron 表达式是否变化 + const cronChanged = values.cronExpression !== originalCronExpression; + + if (cronChanged) { + // Cron 表达式变化了,需要额外调用更新 Cron 的接口 + try { + await updateJobCron(job.id, values.cronExpression); + toast({ + title: '更新成功', + description: `任务 "${values.jobName}" 已更新,调度器已重新配置`, + }); + } catch (cronError) { + // Cron 更新失败,提示用户 + toast({ + variant: 'destructive', + title: '调度器更新失败', + description: '任务已保存,但调度器配置更新失败,请重试或联系管理员', + }); + console.error('更新 Cron 失败:', cronError); + } + } else { + toast({ + title: '更新成功', + description: `任务 "${values.jobName}" 已更新`, + }); + } } else { await createScheduleJob(values); toast({ diff --git a/frontend/src/pages/System/Jenkins/index.tsx b/frontend/src/pages/System/Jenkins/index.tsx index 558de259..71db8958 100644 --- a/frontend/src/pages/System/Jenkins/index.tsx +++ b/frontend/src/pages/System/Jenkins/index.tsx @@ -1,644 +1,863 @@ -import React, { useEffect, useState } from 'react'; -import { Table, Button, Modal, Form, Input, Space, message, Card, Row, Col, Collapse, Tabs, Badge, Tooltip, Radio } from 'antd'; -import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, ReloadOutlined, SyncOutlined, - EyeOutlined, CodeOutlined, BuildOutlined, HistoryOutlined, FolderOutlined } from '@ant-design/icons'; +import React, { useEffect, useState, useMemo } from 'react'; +import { + Card, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Badge } from '@/components/ui/badge'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { useToast } from '@/components/ui/use-toast'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import { + RefreshCw, + Search, + Loader2, + Server, + FolderGit2, + Code2, + Plus, + Edit, + Trash2, + History, + GitBranch, + ExternalLink, +} from 'lucide-react'; import type { JenkinsDTO, JenkinsQuery, JenkinsViewDTO, JenkinsJobDTO, JenkinsBuildDTO } from './types'; import * as jenkinsService from './service'; import SyncStatusDrawer from './components/SyncStatusDrawer'; import dayjs from 'dayjs'; -const { Panel } = Collapse; -const { TabPane } = Tabs; - -interface JenkinsDetailDTO extends JenkinsDTO { - views?: { - viewName: string; - viewUrl: string; - description?: string; - }[]; - jobs?: { - jobName: string; - jobUrl: string; - lastBuildStatus?: string; - lastBuildTime?: string; - }[]; - builds?: { - buildNumber: number; - buildStatus: string; - startTime: string; - duration: number; - triggerCause?: string; - }[]; -} - const JenkinsPage: React.FC = () => { - const [jenkinsList, setJenkinsList] = useState([]); - const [loading, setLoading] = useState(false); - const [syncLoading, setSyncLoading] = useState(false); - const [selectedJenkins, setSelectedJenkins] = useState(); - const [syncStatusVisible, setSyncStatusVisible] = useState(false); + const { toast } = useToast(); + + // Jenkins 实例列表 + const [jenkinsList, setJenkinsList] = useState([]); + const [selectedJenkins, setSelectedJenkins] = useState(); + + // 视图、任务、构建数据 + const [viewData, setViewData] = useState([]); + const [jobData, setJobData] = useState([]); + const [buildData, setBuildData] = useState([]); + + // 选中状态 + const [selectedView, setSelectedView] = useState(); + const [selectedJob, setSelectedJob] = useState(); + + // 加载状态 + const [loading, setLoading] = useState({ + jenkins: false, + views: false, + jobs: false, + builds: false, + }); + + // 同步状态 + const [syncing, setSyncing] = useState({ + all: false, + views: false, + jobs: false, + builds: false, + }); + + // 搜索过滤 + const [jenkinsSearch, setJenkinsSearch] = useState(''); + const [viewSearch] = useState(''); + const [jobSearch, setJobSearch] = useState(''); + const [buildSearch, setBuildSearch] = useState(''); + + // 弹窗状态 const [modalVisible, setModalVisible] = useState(false); - const [editingJenkins, setEditingJenkins] = useState(null); - const [form] = Form.useForm(); - const [searchForm] = Form.useForm(); - const [viewLoading, setViewLoading] = useState>({}); - const [viewData, setViewData] = useState>({}); - const [jobLoading, setJobLoading] = useState>({}); - const [jobData, setJobData] = useState>({}); - const [buildLoading, setBuildLoading] = useState>({}); - const [buildData, setBuildData] = useState>({}); - const [viewPagination, setViewPagination] = useState>({}); - const [jobPagination, setJobPagination] = useState>({}); - const [buildPagination, setBuildPagination] = useState>({}); - const [selectedRow, setSelectedRow] = useState(); - const [selectedView, setSelectedView] = useState(); + const [editingJenkins, setEditingJenkins] = useState(null); + const [syncStatusVisible, setSyncStatusVisible] = useState(false); + + // 表单数据 + const [formData, setFormData] = useState({ + name: '', + url: '', + username: '', + password: '', + remark: '', + }); - const DEFAULT_PAGE_SIZE = 10; - - const fetchJenkinsList = async (params?: JenkinsQuery) => { + // 加载 Jenkins 列表 + const loadJenkinsList = async (params?: JenkinsQuery) => { + setLoading((prev) => ({ ...prev, jenkins: true })); try { - setLoading(true); const response = await jenkinsService.getJenkinsList(params); - setJenkinsList(response || []); + if (response && Array.isArray(response)) { + setJenkinsList(response); + } else { + setJenkinsList([]); + } } catch (error) { - console.error('获取Jenkins列表失败:', error); - message.error('获取Jenkins列表失败'); + toast({ + variant: 'destructive', + title: '加载失败', + description: '加载 Jenkins 列表失败', + }); + setJenkinsList([]); } finally { - setLoading(false); + setLoading((prev) => ({ ...prev, jenkins: false })); } }; - useEffect(() => { - fetchJenkinsList(); - }, []); - - const handleSearch = async () => { - const values = await searchForm.validateFields(); - fetchJenkinsList(values); + // 加载视图列表 + const loadViews = async (jenkinsId: number) => { + setLoading((prev) => ({ ...prev, views: true })); + try { + const response = await jenkinsService.getJenkinsViews(jenkinsId); + if (response && Array.isArray(response)) { + setViewData(response); + } else { + setViewData([]); + } + } catch (error) { + toast({ + variant: 'destructive', + title: '加载失败', + description: '加载视图列表失败', + }); + setViewData([]); + } finally { + setLoading((prev) => ({ ...prev, views: false })); + } }; - const handleReset = () => { - searchForm.resetFields(); - fetchJenkinsList(); + // 加载任务列表 + const loadJobs = async (jenkinsId: number, viewName?: string) => { + setLoading((prev) => ({ ...prev, jobs: true })); + try { + const response = await jenkinsService.getJenkinsJobs(jenkinsId); + if (response && Array.isArray(response)) { + const filteredJobs = viewName + ? response.filter((job) => (job as any).viewName === viewName) + : response; + setJobData(filteredJobs); + } else { + setJobData([]); + } + } catch (error) { + toast({ + variant: 'destructive', + title: '加载失败', + description: '加载任务列表失败', + }); + setJobData([]); + } finally { + setLoading((prev) => ({ ...prev, jobs: false })); + } }; + // 加载构建列表 + const loadBuilds = async (jenkinsId: number, jobId: number) => { + setLoading((prev) => ({ ...prev, builds: true })); + try { + const response = await jenkinsService.getJenkinsBuilds(jenkinsId, jobId); + if (response && Array.isArray(response)) { + setBuildData(response); + } else { + setBuildData([]); + } + } catch (error) { + toast({ + variant: 'destructive', + title: '加载失败', + description: '加载构建历史失败', + }); + setBuildData([]); + } finally { + setLoading((prev) => ({ ...prev, builds: false })); + } + }; + + // 选中 Jenkins + const handleSelectJenkins = (jenkins: JenkinsDTO) => { + setSelectedJenkins(jenkins); + setSelectedView(undefined); + setSelectedJob(undefined); + setJobData([]); + setBuildData([]); + loadViews(jenkins.id); + }; + + // 选中视图 + const handleSelectView = (view: JenkinsViewDTO) => { + if (!selectedJenkins) return; + setSelectedView(view); + setSelectedJob(undefined); + setBuildData([]); + loadJobs(selectedJenkins.id, view.viewName); + }; + + // 选中任务 + const handleSelectJob = (job: JenkinsJobDTO) => { + if (!selectedJenkins) return; + setSelectedJob(job); + loadBuilds(selectedJenkins.id, job.id); + }; + + // 同步 + const handleSync = async (id: number, type: 'ALL' | 'VIEW' | 'JOB' | 'BUILD') => { + const syncKey = type === 'ALL' ? 'all' : type === 'VIEW' ? 'views' : type === 'JOB' ? 'jobs' : 'builds'; + setSyncing((prev) => ({ ...prev, [syncKey]: true })); + try { + await jenkinsService.sync(id, type); + toast({ + title: '同步任务已启动', + description: '正在后台同步数据,请稍后手动刷新查看结果', + }); + } catch (error: any) { + if (error.response?.data?.message === '该Jenkins已有同步任务正在运行中') { + toast({ + title: '同步中', + description: '该Jenkins已有同步任务正在运行中', + }); + } else { + toast({ + variant: 'destructive', + title: '同步失败', + description: '同步任务启动失败', + }); + } + } finally { + setSyncing((prev) => ({ ...prev, [syncKey]: false })); + } + }; + + // 新增/编辑 const handleAdd = () => { setEditingJenkins(null); - form.resetFields(); - form.setFieldsValue({ - sort: 0 + setFormData({ + name: '', + url: '', + username: '', + password: '', + remark: '', }); setModalVisible(true); }; - const handleEdit = (record: JenkinsDetailDTO) => { + const handleEdit = (record: JenkinsDTO) => { setEditingJenkins(record); - form.setFieldsValue(record); + setFormData({ + name: record.name, + url: record.url, + username: record.username, + password: record.password || '', + remark: record.remark || '', + }); setModalVisible(true); }; const handleDelete = async (id: number) => { - Modal.confirm({ - title: '确认删除', - content: '确定要删除这个Jenkins配置吗?', - onOk: async () => { - try { - await jenkinsService.deleteJenkins(id); - message.success('删除成功'); - fetchJenkinsList(); - } catch (error) { - message.error('删除失败'); - } - }, - }); + if (!window.confirm('确定要删除这个Jenkins配置吗?')) return; + + try { + await jenkinsService.deleteJenkins(id); + toast({ + title: '删除成功', + description: 'Jenkins配置已删除', + }); + loadJenkinsList(); + } catch (error) { + toast({ + variant: 'destructive', + title: '删除失败', + description: '删除Jenkins配置失败', + }); + } }; const handleSubmit = async () => { + if (!formData.name || !formData.url || !formData.username || !formData.password) { + toast({ + variant: 'destructive', + title: '验证失败', + description: '请填写所有必填字段', + }); + return; + } + try { - const values = await form.validateFields(); if (editingJenkins) { - await jenkinsService.updateJenkins(editingJenkins.id, values); - message.success('更新成功'); + await jenkinsService.updateJenkins(editingJenkins.id, formData); + toast({ + title: '更新成功', + description: 'Jenkins配置已更新', + }); } else { - await jenkinsService.createJenkins(values); - message.success('创建成功'); + await jenkinsService.createJenkins(formData); + toast({ + title: '创建成功', + description: 'Jenkins配置已创建', + }); } setModalVisible(false); - fetchJenkinsList(); + loadJenkinsList(); } catch (error) { - message.error('操作失败'); + toast({ + variant: 'destructive', + title: '操作失败', + description: editingJenkins ? '更新失败' : '创建失败', + }); } }; const handleTestConnection = async () => { - try { - const values = await form.validateFields(['url', 'username', 'password']); - await jenkinsService.testConnection(values); - message.success('连接测试成功'); - } catch (error) { - message.error('连接测试失败,请检查配置'); - } - }; - - const handleSync = async (id: number, type: 'ALL' | 'VIEW' | 'JOB' | 'BUILD') => { - try { - setSelectedJenkins(id); - setSyncLoading(true); - - if (!id) { - message.error('Jenkins ID 不能为空'); - return; - } - - await jenkinsService.sync(id, type); - message.success('同步任务已开始'); - setSyncStatusVisible(true); - } catch (error: any) { - if (error.response?.data?.message === '该Jenkins已有同步任务正在运行中') { - message.warning('该Jenkins已有同步任务正在运行中'); - } else { - message.error('同步失败'); - } - } finally { - setSyncLoading(false); - setSelectedJenkins(undefined); - } - }; - - const handleDrawerClose = () => { - setSyncStatusVisible(false); - }; - - const fetchViewData = async (jenkinsId: number) => { - if (viewData[jenkinsId]) { + if (!formData.url || !formData.username || !formData.password) { + toast({ + variant: 'destructive', + title: '验证失败', + description: '请填写URL、用户名和密码', + }); return; } try { - setViewLoading(prev => ({ ...prev, [jenkinsId]: true })); - const response = await jenkinsService.getJenkinsViews(jenkinsId); - setViewData(prev => ({ ...prev, [jenkinsId]: response || [] })); + await jenkinsService.testConnection(formData); + toast({ + title: '连接成功', + description: 'Jenkins连接测试成功', + }); } catch (error) { - console.error('获取视图列表失败:', error); - message.error('获取视图列表失败'); - } finally { - setViewLoading(prev => ({ ...prev, [jenkinsId]: false })); + toast({ + variant: 'destructive', + title: '连接失败', + description: '连接测试失败,请检查配置', + }); } }; - const fetchJobData = async (jenkinsId: number) => { - if (jobData[jenkinsId]) { - return; - } - - try { - setJobLoading(prev => ({ ...prev, [jenkinsId]: true })); - const response = await jenkinsService.getJenkinsJobs(jenkinsId); - setJobData(prev => ({ ...prev, [jenkinsId]: response || [] })); - } catch (error) { - console.error('获取作业列表失败:', error); - message.error('获取作业列表失败'); - } finally { - setJobLoading(prev => ({ ...prev, [jenkinsId]: false })); - } - }; - - const fetchBuildData = async (jenkinsId: number, jobId: number) => { - const key = `${jenkinsId}-${jobId}`; - if (buildData[key]) { - return; - } - - try { - setBuildLoading(prev => ({ ...prev, [key]: true })); - const response = await jenkinsService.getJenkinsBuilds(jenkinsId, jobId); - setBuildData(prev => ({ ...prev, [key]: response || [] })); - } catch (error) { - console.error('获取构建历史失败:', error); - message.error('获取构建历史失败'); - } finally { - setBuildLoading(prev => ({ ...prev, [key]: false })); - } - }; - - const columns = [ - { - title: 'Jenkins名称', - dataIndex: 'name', - key: 'name', - width: '12%', - }, - { - title: 'Jenkins地址', - dataIndex: 'url', - key: 'url', - width: '15%', - }, - { - title: '用户名', - dataIndex: 'username', - key: 'username', - width: '8%', - }, - { - title: '最后全量同步时间', - key: 'lastAllSyncTime', - width: '15%', - render: (_, record: JenkinsDTO) => ( - record.lastAllSyncTime ? dayjs(record.lastAllSyncTime).format('YYYY-MM-DD HH:mm:ss') : '-' - ), - }, - { - title: '最后视图同步时间', - key: 'lastViewSyncTime', - width: '15%', - render: (_, record: JenkinsDTO) => ( - record.lastViewSyncTime ? dayjs(record.lastViewSyncTime).format('YYYY-MM-DD HH:mm:ss') : '-' - ), - }, - { - title: '最后作业同步时间', - key: 'lastJobSyncTime', - width: '15%', - render: (_, record: JenkinsDTO) => ( - record.lastJobSyncTime ? dayjs(record.lastJobSyncTime).format('YYYY-MM-DD HH:mm:ss') : '-' - ), - }, - { - title: '最后构建同步时间', - key: 'lastBuildSyncTime', - width: '12%', - render: (_, record: JenkinsDTO) => ( - record.lastBuildSyncTime ? dayjs(record.lastBuildSyncTime).format('YYYY-MM-DD HH:mm:ss') : '-' - ), - }, - { - title: '操作', - key: 'action', - width: '20%', - render: (_, record: JenkinsDTO) => ( - - - - - - ), - }, - ]; - - const handleExpand = async (expanded: boolean, record: JenkinsDetailDTO) => { - if (expanded) { - await Promise.all([ - fetchViewData(record.id), - fetchJobData(record.id) - ]); - - const jobs = jobData[record.id] || []; - await Promise.all( - jobs.map(job => fetchBuildData(record.id, job.id)) - ); - } - }; - - const expandedRowRender = (record: JenkinsDetailDTO) => { - return ( -
- {/* 视图选择器 */} - { - setSelectedView(e.target.value); - fetchJobData(record.id); - }} - style={{ marginBottom: 16 }} - > - 全部视图 - {viewData[record.id]?.map(view => ( - - - - {view.viewName} - - - ))} - - - {/* 作业和构建历史 */} - !selectedView || job.viewName === selectedView) - ?.map(job => ({ - label: ( - - - - {job.jobName} - {job.lastBuildStatus && ( - - )} - - - ), - key: job.id.toString(), - children: ( -
-
- - 最后构建状态: - {job.lastBuildStatus && ( - - )} - - -
- ( - - ), - }, - { - title: '开始时间', - dataIndex: 'startTime', - width: 180, - render: (time: string) => time ? dayjs(time).format('YYYY-MM-DD HH:mm:ss') : '-', - }, - { - title: '持续时间', - dataIndex: 'duration', - width: 100, - render: (duration: number) => { - const seconds = Math.floor(duration / 1000); - if (seconds < 60) return `${seconds}秒`; - const minutes = Math.floor(seconds / 60); - const remainingSeconds = seconds % 60; - return `${minutes}分${remainingSeconds}秒`; - }, - }, - { - title: '触发原因', - dataIndex: 'triggerCause', - ellipsis: true, - }, - { - title: '操作', - key: 'action', - width: 100, - fixed: 'right', - render: (_, record: JenkinsBuildDTO) => ( - - 查看详情 - - ), - } - ]} - dataSource={buildData[`${record.id}-${job.id}`] || []} - rowKey="id" - pagination={{ - size: 'small', - showSizeChanger: true, - showQuickJumper: true, - showTotal: (total) => `共 ${total} 条`, - }} - /> - - ), - })) || [] - } - /> - + // 过滤 + const filteredJenkinsList = useMemo(() => { + if (!jenkinsSearch) return jenkinsList; + return jenkinsList.filter( + (jenkins) => + jenkins.name.toLowerCase().includes(jenkinsSearch.toLowerCase()) || + (jenkins.url && jenkins.url.toLowerCase().includes(jenkinsSearch.toLowerCase())) ); - }; + }, [jenkinsList, jenkinsSearch]); + + const filteredViews = useMemo(() => { + if (!viewSearch) return viewData; + return viewData.filter((view) => + view.viewName.toLowerCase().includes(viewSearch.toLowerCase()) + ); + }, [viewData, viewSearch]); + + const filteredJobs = useMemo(() => { + if (!jobSearch) return jobData; + return jobData.filter((job) => + job.jobName.toLowerCase().includes(jobSearch.toLowerCase()) + ); + }, [jobData, jobSearch]); + + const filteredBuilds = useMemo(() => { + if (!buildSearch) return buildData; + return buildData.filter((build) => + build.buildNumber.toString().includes(buildSearch) + ); + }, [buildData, buildSearch]); + + // 初始化 + useEffect(() => { + loadJenkinsList(); + }, []); return ( -
- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + -
record.id === selectedRow?.id ? 'ant-table-row-selected' : ''} - onRow={(record) => ({ - onClick: () => setSelectedRow(record) - })} - /> - + {/* 三栏布局 */} +
+ {/* 左栏:Jenkins 实例 */} + + +
+ Jenkins 实例 + +
- setModalVisible(false)} - width={600} - destroyOnClose - footer={[ - , - , - , - ]} - > -
- - - +
+ + setJenkinsSearch(e.target.value)} + className="pl-8 h-8" + /> +
+
+ +
+ {loading.jenkins ? ( +
+ +
+ ) : filteredJenkinsList.length === 0 ? ( +
+ +

暂无 Jenkins 实例

+
+ ) : ( + filteredJenkinsList.map((jenkins) => ( +
handleSelectJenkins(jenkins)} + > +
+
+
+ + + {jenkins.name} + +
+

+ {jenkins.url} +

+ {jenkins.lastAllSyncTime && ( +

+ 最后同步:{dayjs(jenkins.lastAllSyncTime).format('MM-DD HH:mm')} +

+ )} +
+
+ + + + + +

编辑

+
+
+ + + + + +

删除

+
+
+
+
+
+ )) + )} +
+
+
- - - + {/* 中栏:视图和任务 */} + + +
+ + 任务 {selectedView && `(${filteredJobs.length})`} + + +
- - - + {/* 视图选择器 */} + {selectedJenkins && filteredViews.length > 0 && ( +
+ + {filteredViews.map((view) => ( + + ))} +
+ )} - - - +
+ + setJobSearch(e.target.value)} + className="pl-8 h-8" + /> +
+
+
+ {loading.jobs ? ( +
+ +
+ ) : !selectedJenkins ? ( +
+ +

请先选择 Jenkins 实例

+
+ ) : filteredJobs.length === 0 ? ( +
+ +

暂无任务

+
+ ) : ( +
+ {filteredJobs.map((job) => ( +
handleSelectJob(job)} + > +
+
+
+ + + {job.jobName} + + {job.lastBuildStatus && ( + + {job.lastBuildStatus} + + )} +
+ {job.description && ( +

+ {job.description} +

+ )} + {job.lastBuildTime && ( +

+ 最后构建:{dayjs(job.lastBuildTime).format('MM-DD HH:mm')} +

+ )} +
+
+
+ ))} +
+ )} +
+
- - - - - + {/* 右栏:构建历史 */} + + +
+ + 构建历史 {selectedJob && `(${filteredBuilds.length})`} + + +
+
+ + setBuildSearch(e.target.value)} + className="pl-8 h-8" + /> +
+
+
+ {loading.builds ? ( +
+ +
+ ) : !selectedJob ? ( +
+ +

请先选择任务

+
+ ) : filteredBuilds.length === 0 ? ( +
+ +

暂无构建历史

+
+ ) : ( +
+ {filteredBuilds.map((build) => ( +
+
+ +
+
+ + #{build.buildNumber} + + + {build.buildStatus || '-'} + +
+ {build.startTime && ( +

+ {dayjs(build.startTime).format('YYYY-MM-DD HH:mm:ss')} +

+ )} + {build.duration !== undefined && ( +

+ 耗时: + {(() => { + const seconds = Math.floor(build.duration / 1000); + if (seconds < 60) return `${seconds}秒`; + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + return `${minutes}分${remainingSeconds}秒`; + })()} +

+ )} + {build.triggerCause && ( +

+ {build.triggerCause} +

+ )} + {build.buildUrl && ( + e.stopPropagation()} + > + 查看详情 + + + )} +
+
+
+ ))} +
+ )} +
+
+
+ + + {/* 新增/编辑弹窗 */} + + + + {editingJenkins ? '编辑 Jenkins' : '新增 Jenkins'} + + {editingJenkins ? '修改 Jenkins 配置信息' : '填写 Jenkins 配置信息'} + + +
+
+ + setFormData({ ...formData, name: e.target.value })} + placeholder="请输入 Jenkins 名称" + /> +
+
+ + setFormData({ ...formData, url: e.target.value })} + placeholder="请输入 Jenkins 地址" + /> +
+
+ + setFormData({ ...formData, username: e.target.value })} + placeholder="请输入用户名" + /> +
+
+ + setFormData({ ...formData, password: e.target.value })} + placeholder="请输入密码或 Token" + /> +
+
+ + setFormData({ ...formData, remark: e.target.value })} + placeholder="请输入备注" + /> +
+
+ + + + + +
+
setSyncStatusVisible(false)} /> - + ); }; -export default JenkinsPage; \ No newline at end of file +export default JenkinsPage;