增加审批组件
This commit is contained in:
parent
7c49715172
commit
ffbad526ac
@ -118,12 +118,15 @@ const DataTablePagination: React.FC<DataTablePaginationProps> = ({
|
||||
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<DataTablePaginationProps> = ({
|
||||
if (startPage > 1) {
|
||||
pages.push(
|
||||
<PaginationItem key={1}>
|
||||
<PaginationLink onClick={() => onPageChange(1)}>1</PaginationLink>
|
||||
<PaginationLink
|
||||
isActive={currentPage === 1}
|
||||
onClick={() => onPageChange(0)}
|
||||
>
|
||||
1
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
);
|
||||
if (startPage > 2) {
|
||||
@ -151,8 +159,8 @@ const DataTablePagination: React.FC<DataTablePaginationProps> = ({
|
||||
pages.push(
|
||||
<PaginationItem key={i}>
|
||||
<PaginationLink
|
||||
isActive={i === pageIndex}
|
||||
onClick={() => onPageChange(i)}
|
||||
isActive={i === currentPage}
|
||||
onClick={() => onPageChange(i - 1)}
|
||||
>
|
||||
{i}
|
||||
</PaginationLink>
|
||||
@ -171,7 +179,10 @@ const DataTablePagination: React.FC<DataTablePaginationProps> = ({
|
||||
}
|
||||
pages.push(
|
||||
<PaginationItem key={pageCount}>
|
||||
<PaginationLink onClick={() => onPageChange(pageCount)}>
|
||||
<PaginationLink
|
||||
isActive={currentPage === pageCount}
|
||||
onClick={() => onPageChange(pageCount - 1)}
|
||||
>
|
||||
{pageCount}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
@ -186,15 +197,15 @@ const DataTablePagination: React.FC<DataTablePaginationProps> = ({
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
onClick={() => 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")}
|
||||
/>
|
||||
</PaginationItem>
|
||||
{renderPageNumbers()}
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
onClick={() => 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")}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
|
||||
@ -344,9 +344,10 @@ const JenkinsManager: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 flex flex-col h-screen">
|
||||
<TooltipProvider>
|
||||
<div className="flex flex-col h-full p-6 gap-4">
|
||||
{/* 页面标题 */}
|
||||
<div className="flex items-center justify-between mb-6 flex-shrink-0">
|
||||
<div className="flex items-center justify-between flex-shrink-0">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-foreground">Jenkins 管理</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
@ -403,7 +404,7 @@ const JenkinsManager: React.FC = () => {
|
||||
</div>
|
||||
</CardHeader>
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="p-2 space-y-1">
|
||||
<div className="p-2 space-y-1 min-h-[calc(100vh-280px)]">
|
||||
{loading.views ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
@ -434,26 +435,24 @@ const JenkinsManager: React.FC = () => {
|
||||
</Badge>
|
||||
)}
|
||||
{view.viewUrl && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<a
|
||||
href={view.viewUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<Button variant="ghost" size="icon" className="h-5 w-5">
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</Button>
|
||||
</a>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>在 Jenkins 中查看</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<a
|
||||
href={view.viewUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<Button variant="ghost" size="icon" className="h-5 w-5">
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</Button>
|
||||
</a>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>在 Jenkins 中查看</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
{view.description && (
|
||||
@ -532,26 +531,24 @@ const JenkinsManager: React.FC = () => {
|
||||
</Badge>
|
||||
)}
|
||||
{job.jobUrl && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<a
|
||||
href={job.jobUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6">
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</Button>
|
||||
</a>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>在 Jenkins 中查看</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<a
|
||||
href={job.jobUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6">
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</Button>
|
||||
</a>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>在 Jenkins 中查看</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -564,25 +561,23 @@ const JenkinsManager: React.FC = () => {
|
||||
<div className="flex items-center gap-3 text-xs">
|
||||
{job.lastBuildStatus && getBuildStatusBadge(job.lastBuildStatus)}
|
||||
{job.healthReportScore !== undefined && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center gap-1">
|
||||
<Activity className="h-3 w-3 text-muted-foreground" />
|
||||
<Progress
|
||||
value={job.healthReportScore}
|
||||
className="w-16 h-2"
|
||||
/>
|
||||
<span className="text-muted-foreground">
|
||||
{job.healthReportScore}%
|
||||
</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
健康度: {job.healthReportScore}%
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center gap-1">
|
||||
<Activity className="h-3 w-3 text-muted-foreground" />
|
||||
<Progress
|
||||
value={job.healthReportScore}
|
||||
className="w-16 h-2"
|
||||
/>
|
||||
<span className="text-muted-foreground">
|
||||
{job.healthReportScore}%
|
||||
</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
健康度: {job.healthReportScore}%
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -658,25 +653,23 @@ const JenkinsManager: React.FC = () => {
|
||||
{getBuildStatusBadge(build.buildStatus)}
|
||||
</div>
|
||||
{build.buildUrl && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<a
|
||||
href={build.buildUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6">
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</Button>
|
||||
</a>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>在 Jenkins 中查看</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<a
|
||||
href={build.buildUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6">
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</Button>
|
||||
</a>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>在 Jenkins 中查看</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -701,6 +694,7 @@ const JenkinsManager: React.FC = () => {
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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<JobEditDialogProps> = ({
|
||||
}) => {
|
||||
const { toast } = useToast();
|
||||
const [submitting, setSubmitting] = React.useState(false);
|
||||
// 保存原始的 Cron 表达式,用于判断是否需要更新调度器
|
||||
const [originalCronExpression, setOriginalCronExpression] = React.useState<string>('');
|
||||
|
||||
const form = useForm<ScheduleJobRequest>({
|
||||
defaultValues: {
|
||||
@ -69,7 +71,8 @@ const JobEditDialog: React.FC<JobEditDialogProps> = ({
|
||||
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<JobEditDialogProps> = ({
|
||||
alertEmail: job.alertEmail || '',
|
||||
});
|
||||
} else {
|
||||
// 新建模式
|
||||
// 新建模式 - 重置原始 Cron 表达式
|
||||
setOriginalCronExpression('');
|
||||
form.reset({
|
||||
jobName: '',
|
||||
jobDescription: '',
|
||||
@ -110,11 +114,35 @@ const JobEditDialog: React.FC<JobEditDialogProps> = ({
|
||||
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({
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user