From 945d827eb87fb6a304efd288b0376ce17f21f32b Mon Sep 17 00:00:00 2001 From: dengqichen Date: Wed, 29 Oct 2025 17:55:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=9B=A2=E9=98=9F=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/Deploy/GitManager/List/index.tsx | 13 + .../Deploy/JenkinsManager/List/index.tsx | 1168 +++++++++-------- .../ScheduleJob/List/components/Dashboard.tsx | 441 +++++++ .../List/components/JobLogDialog.tsx | 419 +++--- .../pages/Deploy/ScheduleJob/List/index.tsx | 86 +- .../pages/Deploy/ScheduleJob/List/service.ts | 35 +- .../pages/Deploy/ScheduleJob/List/types.ts | 32 + 7 files changed, 1472 insertions(+), 722 deletions(-) create mode 100644 frontend/src/pages/Deploy/ScheduleJob/List/components/Dashboard.tsx diff --git a/frontend/src/pages/Deploy/GitManager/List/index.tsx b/frontend/src/pages/Deploy/GitManager/List/index.tsx index 81e37ce7..c2cce655 100644 --- a/frontend/src/pages/Deploy/GitManager/List/index.tsx +++ b/frontend/src/pages/Deploy/GitManager/List/index.tsx @@ -477,6 +477,12 @@ const GitManager: React.FC = () => { {group.name} + {group.projectCount !== undefined && group.projectCount > 0 && ( + + {group.projectCount} + + )} + {group.webUrl && ( @@ -739,6 +745,13 @@ const GitManager: React.FC = () => { )} + {project.branchCount !== undefined && project.branchCount > 0 && ( + + + {project.branchCount} 分支 + + + )} {project.lastActivityAt && ( diff --git a/frontend/src/pages/Deploy/JenkinsManager/List/index.tsx b/frontend/src/pages/Deploy/JenkinsManager/List/index.tsx index 27870a51..90c791a8 100644 --- a/frontend/src/pages/Deploy/JenkinsManager/List/index.tsx +++ b/frontend/src/pages/Deploy/JenkinsManager/List/index.tsx @@ -1,560 +1,666 @@ 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 } from 'lucide-react'; +import { + RefreshCw, + Layers, + Box, + Activity, + Search, + CheckCircle, + XCircle, + AlertTriangle, + Loader2, + ExternalLink, + GitBranch, + Calendar +} from 'lucide-react'; import { - Card, - CardContent, - CardHeader, - CardTitle, + Card, + CardContent, + CardHeader, + CardTitle, } from '@/components/ui/card'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table'; +import { ScrollArea } from '@/components/ui/scroll-area'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, } from '@/components/ui/select'; import { Badge } from '@/components/ui/badge'; import { useToast } from '@/components/ui/use-toast'; import { Progress } from '@/components/ui/progress'; -import type { JenkinsInstance, JenkinsInstanceDTO } from './types'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; +import type { JenkinsInstance, JenkinsInstanceDTO, JenkinsViewDTO, JenkinsJobDTO } from './types'; import { getJenkinsInstances, getJenkinsInstance, syncViews, syncJobs, syncBuilds } from './service'; +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime'; +import 'dayjs/locale/zh-cn'; + +dayjs.extend(relativeTime); +dayjs.locale('zh-cn'); const JenkinsManagerList: React.FC = () => { - const [jenkinsList, setJenkinsList] = useState([]); - const [currentJenkinsId, setCurrentJenkinsId] = useState(); - const [instanceDetails, setInstanceDetails] = useState(); - const [loading, setLoading] = useState(false); - const [currentView, setCurrentView] = useState(); - const [syncing, setSyncing] = useState>({ - views: false, - jobs: false, - builds: false, - all: false, - }); - - // 搜索和过滤状态 - const [searchKeyword, setSearchKeyword] = useState(''); - const [statusFilter, setStatusFilter] = useState('all'); + const [jenkinsList, setJenkinsList] = useState([]); + const [selectedInstanceId, setSelectedInstanceId] = useState(); + const [instanceDetails, setInstanceDetails] = useState(); + + // 三栏选中状态 + const [selectedView, setSelectedView] = useState(null); + const [selectedJob, setSelectedJob] = useState(null); + + // 加载状态 + const [loading, setLoading] = useState({ + views: false, + jobs: false, + builds: false, + }); + + // 同步状态 + const [syncing, setSyncing] = useState({ + all: false, + views: false, + jobs: false, + builds: false, + }); + + // 搜索状态 + const [viewSearch, setViewSearch] = useState(''); + const [jobSearch, setJobSearch] = useState(''); + const [buildSearch, setBuildSearch] = useState(''); - const { toast } = useToast(); + const { toast } = useToast(); - // 获取 Jenkins 实例列表 - const loadJenkinsList = async () => { - try { - const data = await getJenkinsInstances(); - setJenkinsList(data); - if (!currentJenkinsId && data.length > 0) { - setCurrentJenkinsId(String(data[0].id)); - } - } catch (error) { - toast({ - variant: 'destructive', - title: '获取 Jenkins 实例列表失败', - duration: 3000, - }); - } - }; - - // 获取 Jenkins 实例详情 - const loadInstanceDetails = async () => { - if (!currentJenkinsId) return; - setLoading(true); - try { - const data = await getJenkinsInstance(currentJenkinsId); - setInstanceDetails(data); - // 设置默认视图 - if (data.jenkinsViewList.length > 0 && !currentView) { - setCurrentView(data.jenkinsViewList[0].id); - } - } catch (error) { - toast({ - variant: 'destructive', - title: '获取实例详情失败', - duration: 3000, - }); - } finally { - setLoading(false); - } - }; - - // 切换 Jenkins 实例 - const handleJenkinsChange = (id: string) => { - setCurrentJenkinsId(id); - setInstanceDetails(undefined); - setCurrentView(undefined); - setSearchKeyword(''); - setStatusFilter('all'); - }; - - // 单个同步 - const handleSync = async (type: 'views' | 'jobs' | 'builds') => { - if (!currentJenkinsId) return; - setSyncing((prev) => ({ ...prev, [type]: true })); - try { - switch (type) { - case 'views': - await syncViews(currentJenkinsId); - break; - case 'jobs': - await syncJobs(currentJenkinsId); - break; - case 'builds': - await syncBuilds(currentJenkinsId); - break; - } - await loadInstanceDetails(); - toast({ - title: '同步成功', - duration: 2000, - }); - } catch (error) { - toast({ - variant: 'destructive', - title: '同步失败', - duration: 3000, - }); - } finally { - setSyncing((prev) => ({ ...prev, [type]: false })); - } - }; - - // 全部同步 - const handleSyncAll = async () => { - if (!currentJenkinsId) return; - setSyncing((prev) => ({ ...prev, all: true })); - try { - await Promise.all([ - syncViews(currentJenkinsId), - syncJobs(currentJenkinsId), - syncBuilds(currentJenkinsId), - ]); - await loadInstanceDetails(); - toast({ - title: '全部同步成功', - duration: 2000, - }); - } catch (error) { - toast({ - variant: 'destructive', - title: '同步失败', - description: '部分数据同步失败,请重试', - duration: 3000, - }); - } finally { - setSyncing((prev) => ({ ...prev, all: false })); - } - }; - - useEffect(() => { - loadJenkinsList(); - }, []); - - useEffect(() => { - if (currentJenkinsId) { - loadInstanceDetails(); - } - }, [currentJenkinsId]); - - const formatTime = (time: string | null | undefined) => { - if (!time) return 'Never'; - return time; - }; - - // 获取构建状态信息 - const getBuildStatusInfo = (status: string) => { - const statusMap: Record = { - SUCCESS: { - variant: 'default', - icon: , - label: '成功', - }, - FAILURE: { - variant: 'destructive', - icon: , - label: '失败', - }, - UNSTABLE: { - variant: 'secondary', - icon: , - label: '不稳定', - }, - ABORTED: { - variant: 'outline', - icon: , - label: '已中止', - }, - RUNNING: { - variant: 'outline', - icon: , - label: '构建中', - }, + // 获取 Jenkins 实例列表 + const loadJenkinsList = async () => { + try { + const data = await getJenkinsInstances(); + setJenkinsList(data); + if (!selectedInstanceId && data.length > 0) { + setSelectedInstanceId(data[0].id); + } + } catch (error) { + toast({ + variant: 'destructive', + title: '获取 Jenkins 实例列表失败', + duration: 3000, + }); + } }; - return statusMap[status] || { - variant: 'outline' as const, - icon: , - label: status || '未知', + + // 获取 Jenkins 实例详情 + const loadInstanceDetails = async () => { + if (!selectedInstanceId) return; + setLoading((prev) => ({ ...prev, views: true })); + try { + const data = await getJenkinsInstance(String(selectedInstanceId)); + setInstanceDetails(data); + + // 自动选择第一个 View + if (data.jenkinsViewList.length > 0 && !selectedView) { + setSelectedView(data.jenkinsViewList[0]); + } + } catch (error) { + toast({ + variant: 'destructive', + title: '获取实例详情失败', + duration: 3000, + }); + } finally { + setLoading((prev) => ({ ...prev, views: false })); + } }; - }; - // 获取健康度颜色 - 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'; - }; + // 切换 Jenkins 实例 + const handleInstanceChange = (id: string) => { + setSelectedInstanceId(Number(id)); + setInstanceDetails(undefined); + setSelectedView(null); + setSelectedJob(null); + setViewSearch(''); + setJobSearch(''); + setBuildSearch(''); + }; - // 过滤和搜索 Jobs - const filteredJobs = useMemo(() => { - if (!instanceDetails) return []; - - let jobs = instanceDetails.jenkinsJobList; - - // 按 View 过滤 - if (currentView) { - jobs = jobs.filter((job) => job.viewId === currentView); - } - - // 按搜索关键词过滤 - if (searchKeyword) { - jobs = jobs.filter((job) => - job.jobName.toLowerCase().includes(searchKeyword.toLowerCase()) || - job.description?.toLowerCase().includes(searchKeyword.toLowerCase()) - ); - } - - // 按状态过滤 - if (statusFilter !== 'all') { - jobs = jobs.filter((job) => job.lastBuildStatus === statusFilter); - } - - return jobs; - }, [instanceDetails, currentView, searchKeyword, statusFilter]); + // 选择 View + const handleSelectView = (view: JenkinsViewDTO) => { + setSelectedView(view); + setSelectedJob(null); + }; + + // 选择 Job + const handleSelectJob = (job: JenkinsJobDTO) => { + setSelectedJob(job); + // TODO: 加载 Builds 列表 + }; + + // 同步 Views + const handleSyncViews = async () => { + if (!selectedInstanceId) return; + setSyncing((prev) => ({ ...prev, views: true })); + try { + await syncViews(String(selectedInstanceId)); + toast({ + title: '同步成功', + description: 'Views 同步任务已启动', + duration: 2000, + }); + } catch (error) { + toast({ + variant: 'destructive', + title: '同步失败', + duration: 3000, + }); + } finally { + setSyncing((prev) => ({ ...prev, views: false })); + } + }; + + // 同步 Jobs + const handleSyncJobs = async () => { + if (!selectedInstanceId) return; + setSyncing((prev) => ({ ...prev, jobs: true })); + try { + await syncJobs(String(selectedInstanceId)); + toast({ + title: '同步成功', + description: 'Jobs 同步任务已启动', + duration: 2000, + }); + } catch (error) { + toast({ + variant: 'destructive', + title: '同步失败', + duration: 3000, + }); + } finally { + setSyncing((prev) => ({ ...prev, jobs: false })); + } + }; + + // 同步 Builds + const handleSyncBuilds = async () => { + if (!selectedInstanceId) return; + setSyncing((prev) => ({ ...prev, builds: true })); + try { + await syncBuilds(String(selectedInstanceId)); + toast({ + title: '同步成功', + description: 'Builds 同步任务已启动', + duration: 2000, + }); + } catch (error) { + toast({ + variant: 'destructive', + title: '同步失败', + duration: 3000, + }); + } finally { + setSyncing((prev) => ({ ...prev, builds: false })); + } + }; + + // 全部同步 + const handleSyncAll = async () => { + if (!selectedInstanceId) return; + setSyncing((prev) => ({ ...prev, all: true })); + try { + await Promise.all([ + syncViews(String(selectedInstanceId)), + syncJobs(String(selectedInstanceId)), + syncBuilds(String(selectedInstanceId)), + ]); + toast({ + title: '全部同步任务已启动', + description: '请稍后刷新页面查看同步结果', + duration: 2000, + }); + } catch (error) { + toast({ + variant: 'destructive', + title: '同步失败', + description: '部分数据同步失败,请重试', + duration: 3000, + }); + } finally { + setSyncing((prev) => ({ ...prev, all: false })); + } + }; + + // 格式化时间 + const formatTime = (time?: string | null) => { + if (!time) return '-'; + const diff = dayjs().diff(dayjs(time), 'day'); + if (diff > 7) { + return dayjs(time).format('YYYY-MM-DD HH:mm'); + } + return dayjs(time).fromNow(); + }; + + // 获取构建状态信息 + const getBuildStatusInfo = (status: string) => { + const statusMap: Record = { + SUCCESS: { + variant: 'success', + icon: , + label: '成功', + }, + FAILURE: { + variant: 'destructive', + icon: , + label: '失败', + }, + UNSTABLE: { + variant: 'secondary', + icon: , + label: '不稳定', + }, + ABORTED: { + variant: 'outline', + icon: , + label: '已中止', + }, + RUNNING: { + variant: 'outline', + icon: , + label: '构建中', + }, + }; + return statusMap[status] || { + variant: 'outline' as const, + icon: , + label: status || '未知', + }; + }; + + // 获取健康度颜色 + const getHealthColor = (score: number) => { + if (score >= 80) return 'bg-green-500'; + if (score >= 60) return 'bg-green-400'; + if (score >= 40) return 'bg-yellow-400'; + if (score >= 20) return 'bg-orange-400'; + return 'bg-red-500'; + }; + + // 过滤 Views + const filteredViews = useMemo(() => { + if (!instanceDetails) return []; + if (!viewSearch) return instanceDetails.jenkinsViewList; + return instanceDetails.jenkinsViewList.filter((view) => + view.viewName.toLowerCase().includes(viewSearch.toLowerCase()) || + (view.description && view.description.toLowerCase().includes(viewSearch.toLowerCase())) + ); + }, [instanceDetails, viewSearch]); + + // 过滤 Jobs(基于选中的 View) + const filteredJobs = useMemo(() => { + if (!instanceDetails || !selectedView) return []; + let jobs = instanceDetails.jenkinsJobList.filter( + (job) => job.viewId === selectedView.id + ); + if (jobSearch) { + jobs = jobs.filter((job) => + job.jobName.toLowerCase().includes(jobSearch.toLowerCase()) || + (job.description && job.description.toLowerCase().includes(jobSearch.toLowerCase())) + ); + } + return jobs; + }, [instanceDetails, selectedView, jobSearch]); + + useEffect(() => { + loadJenkinsList(); + }, []); + + useEffect(() => { + if (selectedInstanceId) { + loadInstanceDetails(); + } + }, [selectedInstanceId]); + + // 空状态:没有实例 + if (jenkinsList.length === 0) { + return ( + +
+

Jenkins 管理

+
+ + + +

暂无 Jenkins 实例

+

+ 请先在外部服务管理中添加 Jenkins 系统 +

+ +
+
+
+ ); + } - // 空状态:没有实例 - if (jenkinsList.length === 0) { return ( - -
-

Jenkins 三方管理

-
- - - -

暂无 Jenkins 实例

-

- 请先在外部服务管理中添加 Jenkins 系统 -

- -
-
-
- ); - } + + + {/* 页面标题和实例选择 */} +
+

Jenkins 管理

+
+ + +
+
- return ( - - {/* 页面标题 */} -
-

Jenkins 三方管理

-
- - -
-
- - {/* 统计卡片 */} -
- - - Views -
- - -
-
- -
{instanceDetails?.totalViews || 0}
-

- 最后同步: {formatTime(instanceDetails?.lastSyncViewsTime)} -

-
-
- - - - Jobs -
- - -
-
- -
{instanceDetails?.totalJobs || 0}
-

- 最后同步: {formatTime(instanceDetails?.lastSyncJobsTime)} -

-
-
- - - - Builds -
- - -
-
- -
{instanceDetails?.totalBuilds || 0}
-

- 最后同步: {formatTime(instanceDetails?.lastSyncBuildsTime)} -

-
-
-
- - {/* View 切换 */} - {instanceDetails && instanceDetails.jenkinsViewList.length > 0 && ( - - -
- {instanceDetails.jenkinsViewList.map((view) => ( - - ))} -
-
-
- )} - - {/* 搜索和过滤 */} - {instanceDetails && instanceDetails.jenkinsViewList.length > 0 && ( - -
-
-
- - setSearchKeyword(e.target.value)} - className="pl-9" - /> -
- - -
-
-
- )} - - {/* Jobs 表格 */} - {loading ? ( - - - - - - ) : instanceDetails && instanceDetails.jenkinsViewList.length > 0 ? ( - - - - {instanceDetails.jenkinsViewList.find((v) => v.id === currentView)?.viewName || 'Jobs'} 列表 - - - -
- - - - Job 名称 - 描述 - 最后构建 - 状态 - 健康度 - 构建时间 - 操作 - - - - {filteredJobs.length === 0 ? ( - - -
- -

暂无任务

+ {/* 三栏布局 */} +
+ {/* 左栏:Views 列表 */} + + +
+ + Views {instanceDetails && `(${filteredViews.length})`} + + +
+
+ + setViewSearch(e.target.value)} + className="pl-8 h-8" + /> +
+
+
+ {loading.views ? ( +
+ +
+ ) : filteredViews.length === 0 ? ( +
+ +

暂无视图

+
+ ) : ( +
+ {filteredViews.map((view) => ( +
handleSelectView(view)} + > +
+
+
+ + + {view.viewName} + +
+ {view.description && ( +

+ {view.description} +

+ )} +
+ {view.viewUrl && ( + + + e.stopPropagation()} + className="flex-shrink-0" + > + + + + +

在 Jenkins 中查看

+
+
+ )} +
+
+ ))} +
+ )}
- - - ) : ( - filteredJobs.map((job) => { - const statusInfo = getBuildStatusInfo(job.lastBuildStatus); - return ( - - -
- {job.jobName} + + + {/* 中栏:Jobs 列表 */} + + +
+ + Jobs {selectedView && `(${filteredJobs.length})`} + +
- - - {job.description || '-'} - - - #{job.lastBuildNumber} - - - - {statusInfo.icon} - {statusInfo.label} - - - -
- - {job.healthReportScore}% +
+ + setJobSearch(e.target.value)} + className="pl-8 h-8" + />
- - - {formatTime(job.lastBuildTime)} - - - - - - ); - }) - )} - -
-
-
-
- ) : ( - - - -

暂无视图数据

-

- 请点击上方的同步按钮获取 Jenkins 数据 -

- -
-
- )} -
- ); + +
+ {loading.jobs ? ( +
+ +
+ ) : !selectedView ? ( +
+ +

请先选择视图

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

暂无任务

+
+ ) : ( +
+ {filteredJobs.map((job) => { + const statusInfo = getBuildStatusInfo(job.lastBuildStatus); + return ( +
handleSelectJob(job)} + > +
+
+
+ + + {job.jobName} + +
+ {job.description && ( +

+ {job.description} +

+ )} +
+ + {statusInfo.icon} + {statusInfo.label} + + + + #{job.lastBuildNumber} + +
+ + + {job.healthReportScore}% + +
+
+ {job.lastBuildTime && ( +
+ + {formatTime(job.lastBuildTime)} +
+ )} +
+ {job.jobUrl && ( + + + e.stopPropagation()} + className="flex-shrink-0" + > + + + + +

在 Jenkins 中查看

+
+
+ )} +
+
+ ); + })} +
+ )} +
+ + + {/* 右栏:Builds 列表(待实现) */} + + +
+ Builds + +
+
+ + setBuildSearch(e.target.value)} + className="pl-8 h-8" + /> +
+
+
+ {!selectedJob ? ( +
+ +

请先选择任务

+
+ ) : ( +
+ +

构建列表待实现

+

等待接口定义

+
+ )} +
+
+ +
+
+ ); }; export default JenkinsManagerList; diff --git a/frontend/src/pages/Deploy/ScheduleJob/List/components/Dashboard.tsx b/frontend/src/pages/Deploy/ScheduleJob/List/components/Dashboard.tsx new file mode 100644 index 00000000..6a852887 --- /dev/null +++ b/frontend/src/pages/Deploy/ScheduleJob/List/components/Dashboard.tsx @@ -0,0 +1,441 @@ +import React, { useState, useEffect } from 'react'; +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Progress } from '@/components/ui/progress'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { + Activity, + CheckCircle2, + XCircle, + Pause, + Clock, + Loader2, + TrendingUp, + AlertCircle, + Calendar, +} from 'lucide-react'; +import { getDashboard, getScheduleJobs } from '../service'; +import type { DashboardResponse, ScheduleJobResponse, LogStatus } from '../types'; +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime'; +import 'dayjs/locale/zh-cn'; + +dayjs.extend(relativeTime); +dayjs.locale('zh-cn'); + +const Dashboard: React.FC = () => { + const [data, setData] = useState(null); + const [allJobs, setAllJobs] = useState([]); + const [loading, setLoading] = useState(true); + + // 加载仪表盘数据 + const loadData = async () => { + try { + const [dashboardData, jobsData] = await Promise.all([ + getDashboard(), + getScheduleJobs({ pageNum: 0, pageSize: 100 }) + ]); + setData(dashboardData); + setAllJobs(jobsData?.content || []); + } catch (error) { + console.error('加载仪表盘数据失败:', error); + } finally { + setLoading(false); + } + }; + + // 初始加载和5秒轮询 + useEffect(() => { + loadData(); + const interval = setInterval(loadData, 5000); + return () => clearInterval(interval); + }, []); + + // 获取日志状态徽章 + const getLogStatusBadge = (status: LogStatus) => { + const statusMap: Record = { + SUCCESS: { variant: 'success', text: '成功', icon: CheckCircle2 }, + FAIL: { variant: 'destructive', text: '失败', icon: XCircle }, + TIMEOUT: { variant: 'secondary', text: '超时', icon: Clock }, + }; + const info = statusMap[status]; + const Icon = info.icon; + return ( + + + {info.text} + + ); + }; + + // 按状态分组任务 + const groupedJobs = React.useMemo(() => { + return { + running: allJobs.filter(j => data?.runningJobs.some(r => r.jobId === j.id)), + paused: allJobs.filter(j => j.status === 'PAUSED'), + idle: allJobs.filter(j => j.status === 'ENABLED' && !data?.runningJobs.some(r => r.jobId === j.id)), + disabled: allJobs.filter(j => j.status === 'DISABLED'), + }; + }, [allJobs, data]); + + // 获取即将执行的任务(按下次执行时间排序) + const upcomingJobs = React.useMemo(() => { + return allJobs + .filter(j => j.nextExecuteTime && j.status === 'ENABLED') + .sort((a, b) => { + if (!a.nextExecuteTime || !b.nextExecuteTime) return 0; + return dayjs(a.nextExecuteTime).valueOf() - dayjs(b.nextExecuteTime).valueOf(); + }) + .slice(0, 10); + }, [allJobs]); + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+ {/* 任务概览统计卡片 */} +
+ + + 任务总数 + + + +
{data?.summary.total || 0}
+

所有定时任务

+
+
+ + + + 正在执行 + + + +
{data?.summary.running || 0}
+

运行中的任务

+
+
+ + + + 已启用 + + + +
{data?.summary.enabled || 0}
+

活跃的任务

+
+
+ + + + 已暂停 + + + +
{data?.summary.paused || 0}
+

暂停的任务

+
+
+ + + + 已禁用 + + + +
{data?.summary.disabled || 0}
+

停用的任务

+
+
+
+ + {/* 正在执行的任务 */} + {data && data.runningJobs.length > 0 && ( + + + + + 正在执行的任务 ({data.runningJobs.length}) + + + + {data.runningJobs.map((job) => ( +
+
+
+

{job.jobName}

+

+ 开始时间: {dayjs(job.startTime).format('YYYY-MM-DD HH:mm:ss')} +

+
+ + {job.progress}% + +
+ + {job.message && ( +

+ + {job.message} +

+ )} +
+ ))} +
+
+ )} + + {/* 多视图 Tabs */} + + + + + 时间线视图 + + + + 状态分组 + + + + 最近日志 + + + + {/* 时间线视图 */} + + + + 任务执行时间线 +

+ 即将执行的任务按时间顺序排列 +

+
+ + {upcomingJobs.length === 0 ? ( +
+ +

暂无即将执行的任务

+
+ ) : ( +
+ {/* 时间线 */} +
+ + {upcomingJobs.map((job, index) => ( +
+ {/* 时间点 */} +
+ +
+
+ {dayjs(job.nextExecuteTime).format('HH:mm')} +
+
+
+
+

{job.jobName}

+

+ {dayjs(job.nextExecuteTime).fromNow()} +

+
+ + {index === 0 && data?.runningJobs.some(r => r.jobId === job.id) ? ( + + + 执行中 + + ) : ( + + + 待执行 + + )} + +
+
+
+
+ ))} +
+ )} + + + + + {/* 状态分组视图 */} + +
+ {/* 运行中 */} + + + +
+ 运行中 ({groupedJobs.running.length}) + + + + {groupedJobs.running.length === 0 ? ( +

无正在运行的任务

+ ) : ( +
+ {groupedJobs.running.map((job) => { + const runningJob = data?.runningJobs.find(r => r.jobId === job.id); + return ( +
+
+

{job.jobName}

+ {runningJob && ( + + )} +
+ {runningJob && ( + {runningJob.progress}% + )} +
+ ); + })} +
+ )} +
+ + + {/* 已暂停 */} + + + + + 已暂停 ({groupedJobs.paused.length}) + + + + {groupedJobs.paused.length === 0 ? ( +

无暂停的任务

+ ) : ( +
+ {groupedJobs.paused.map((job) => ( +
+

{job.jobName}

+

+ 上次: {job.lastExecuteTime ? dayjs(job.lastExecuteTime).fromNow() : '-'} +

+
+ ))} +
+ )} +
+
+ + {/* 空闲中 */} + + + + + 空闲中 ({groupedJobs.idle.length}) + + + + {groupedJobs.idle.length === 0 ? ( +

无空闲任务

+ ) : ( +
+ {groupedJobs.idle.map((job) => ( +
+

{job.jobName}

+

+ 下次: {job.nextExecuteTime ? dayjs(job.nextExecuteTime).format('MM-DD HH:mm') : '-'} +

+
+ ))} +
+ )} +
+
+ + {/* 已禁用 */} + + + + + 已禁用 ({groupedJobs.disabled.length}) + + + + {groupedJobs.disabled.length === 0 ? ( +

无禁用的任务

+ ) : ( +
+ {groupedJobs.disabled.map((job) => ( +
+

{job.jobName}

+

已停用

+
+ ))} +
+ )} +
+
+
+ + + {/* 最近日志 */} + + + + 最近执行日志 (10条) + + + {!data || data.recentLogs.length === 0 ? ( +
+ +

暂无执行日志

+
+ ) : ( +
+ {data.recentLogs.map((log) => ( +
+
+
+

{log.jobName}

+

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

+
+ {getLogStatusBadge(log.status)} +
+ {log.resultMessage && ( +

+ {log.resultMessage} +

+ )} + {log.exceptionInfo && ( +

+ {log.exceptionInfo} +

+ )} +
+ ))} +
+ )} +
+
+
+ +
+ ); +}; + +export default Dashboard; + diff --git a/frontend/src/pages/Deploy/ScheduleJob/List/components/JobLogDialog.tsx b/frontend/src/pages/Deploy/ScheduleJob/List/components/JobLogDialog.tsx index cb88f237..5b52be99 100644 --- a/frontend/src/pages/Deploy/ScheduleJob/List/components/JobLogDialog.tsx +++ b/frontend/src/pages/Deploy/ScheduleJob/List/components/JobLogDialog.tsx @@ -4,37 +4,37 @@ import { DialogContent, DialogHeader, DialogTitle, + DialogBody, } from '@/components/ui/dialog'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table'; +import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { DataTablePagination } from '@/components/ui/pagination'; import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { Separator } from '@/components/ui/separator'; import { FileText, Loader2, - Search, CheckCircle2, XCircle, Clock, Server, Calendar, AlertCircle, + Timer, + Code, + Info, + Activity, + RefreshCw, } from 'lucide-react'; import { useToast } from '@/components/ui/use-toast'; import { getJobLogs } from '../service'; import type { ScheduleJobResponse, ScheduleJobLogResponse, ScheduleJobLogQuery, LogStatus } from '../types'; import type { Page } from '@/types/base'; import dayjs from 'dayjs'; -import { Textarea } from '@/components/ui/textarea'; +import { cn } from '@/lib/utils'; interface JobLogDialogProps { open: boolean; @@ -131,193 +131,260 @@ const JobLogDialog: React.FC = ({ return ( - + - + 执行日志 - {job?.jobName} -
+ {/* 筛选栏 */} -
- -
{/* 日志列表 */} -
- - - - 执行时间 - 完成时间 - 耗时 - 状态 - 服务器IP - 结果消息 - 操作 - - - - {loading ? ( - - - - - - ) : data && data.content.length > 0 ? ( - data.content.map((log) => ( - +
+ +

加载中...

+
+ + ) : data && data.content.length > 0 ? ( +
+ {/* 左侧:日志列表 */} +
+ + {data.content.map((log) => ( + setSelectedLog(log)} > - -
- - {dayjs(log.executeTime).format('YYYY-MM-DD HH:mm:ss')} -
-
- - {log.finishTime ? dayjs(log.finishTime).format('YYYY-MM-DD HH:mm:ss') : '-'} - - - {formatDuration(log.duration)} - - {getStatusBadge(log.status)} - - {log.serverIp ? ( -
- - {log.serverIp} + +
+
+ {getStatusBadge(log.status)} + + {formatDuration(log.duration)} +
- ) : ( - '-' - )} - - - {log.resultMessage || '-'} - - - - - - )) - ) : ( - - - 暂无执行日志 - - +
+ + {dayjs(log.executeTime).format('MM-DD HH:mm:ss')} +
+
+ +
+ {log.serverIp && ( +
+ + {log.serverIp} +
+ )} + {log.resultMessage && ( +

+ {log.resultMessage} +

+ )} +
+
+ + ))} + + + {/* 分页 */} + {data && data.content.length > 0 && ( + setQuery({ ...query, pageNum: pageIndex })} + /> )} - -
-
- - {/* 分页 */} - {data && data.content.length > 0 && ( - setQuery({ ...query, pageNum: pageIndex })} - /> - )} - - {/* 日志详情 */} - {selectedLog && ( -
-
-

- - 执行详情 -

-
-
-
- Bean名称: - {selectedLog.beanName} -
-
- 方法名称: - {selectedLog.methodName} -
-
- 服务器主机: - {selectedLog.serverHost || '-'} -
-
- 服务器IP: - {selectedLog.serverIp || '-'} -
+ {/* 右侧:详情面板 */} +
+ {selectedLog ? ( + +
+
+

+ + 执行详情 +

+ {getStatusBadge(selectedLog.status)} +
+ + + + {/* 基本信息 */} +
+
+ +
+

执行时间

+

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

+
+
+ + {selectedLog.finishTime && ( +
+ +
+

完成时间

+

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

+
+
+ )} + +
+ +
+

执行耗时

+

+ {formatDuration(selectedLog.duration)} +

+
+
+ + {selectedLog.serverIp && ( +
+ +
+

服务器信息

+

{selectedLog.serverIp}

+ {selectedLog.serverHost && ( +

{selectedLog.serverHost}

+ )} +
+
+ )} +
+ + + + {/* 技术信息 */} +
+
+ +
+

Bean名称

+ + {selectedLog.beanName} + +
+
+ +
+ +
+

方法名称

+ + {selectedLog.methodName} + +
+
+ + {selectedLog.methodParams && ( +
+

+ + 方法参数 +

+
+                                                            {selectedLog.methodParams}
+                                                        
+
+ )} +
+ + {/* 结果信息 */} + {selectedLog.resultMessage && ( + <> + +
+

+ + 结果消息 +

+
+ {selectedLog.resultMessage} +
+
+ + )} + + {/* 异常信息 */} + {selectedLog.exceptionInfo && ( + <> + +
+

+ + 异常信息 +

+
+                                                            {selectedLog.exceptionInfo}
+                                                        
+
+ + )} +
+
+ ) : ( +
+
+ +

+ 选择左侧日志查看详情 +

+
+
+ )} +
+
+ ) : ( +
+
+ +

暂无执行日志

- - {selectedLog.methodParams && ( -
- -