增加审批组件
This commit is contained in:
parent
7c49715172
commit
ffbad526ac
@ -118,12 +118,15 @@ const DataTablePagination: React.FC<DataTablePaginationProps> = ({
|
|||||||
pageCount,
|
pageCount,
|
||||||
onPageChange,
|
onPageChange,
|
||||||
}) => {
|
}) => {
|
||||||
|
// 将 0-based 的 pageIndex 转换为 1-based 的显示页码
|
||||||
|
const currentPage = pageIndex + 1;
|
||||||
|
|
||||||
const renderPageNumbers = () => {
|
const renderPageNumbers = () => {
|
||||||
const pages = [];
|
const pages = [];
|
||||||
const maxVisiblePages = 5;
|
const maxVisiblePages = 5;
|
||||||
const halfMaxVisiblePages = Math.floor(maxVisiblePages / 2);
|
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);
|
let endPage = Math.min(pageCount, startPage + maxVisiblePages - 1);
|
||||||
|
|
||||||
if (endPage - startPage + 1 < maxVisiblePages) {
|
if (endPage - startPage + 1 < maxVisiblePages) {
|
||||||
@ -134,7 +137,12 @@ const DataTablePagination: React.FC<DataTablePaginationProps> = ({
|
|||||||
if (startPage > 1) {
|
if (startPage > 1) {
|
||||||
pages.push(
|
pages.push(
|
||||||
<PaginationItem key={1}>
|
<PaginationItem key={1}>
|
||||||
<PaginationLink onClick={() => onPageChange(1)}>1</PaginationLink>
|
<PaginationLink
|
||||||
|
isActive={currentPage === 1}
|
||||||
|
onClick={() => onPageChange(0)}
|
||||||
|
>
|
||||||
|
1
|
||||||
|
</PaginationLink>
|
||||||
</PaginationItem>
|
</PaginationItem>
|
||||||
);
|
);
|
||||||
if (startPage > 2) {
|
if (startPage > 2) {
|
||||||
@ -151,8 +159,8 @@ const DataTablePagination: React.FC<DataTablePaginationProps> = ({
|
|||||||
pages.push(
|
pages.push(
|
||||||
<PaginationItem key={i}>
|
<PaginationItem key={i}>
|
||||||
<PaginationLink
|
<PaginationLink
|
||||||
isActive={i === pageIndex}
|
isActive={i === currentPage}
|
||||||
onClick={() => onPageChange(i)}
|
onClick={() => onPageChange(i - 1)}
|
||||||
>
|
>
|
||||||
{i}
|
{i}
|
||||||
</PaginationLink>
|
</PaginationLink>
|
||||||
@ -171,7 +179,10 @@ const DataTablePagination: React.FC<DataTablePaginationProps> = ({
|
|||||||
}
|
}
|
||||||
pages.push(
|
pages.push(
|
||||||
<PaginationItem key={pageCount}>
|
<PaginationItem key={pageCount}>
|
||||||
<PaginationLink onClick={() => onPageChange(pageCount)}>
|
<PaginationLink
|
||||||
|
isActive={currentPage === pageCount}
|
||||||
|
onClick={() => onPageChange(pageCount - 1)}
|
||||||
|
>
|
||||||
{pageCount}
|
{pageCount}
|
||||||
</PaginationLink>
|
</PaginationLink>
|
||||||
</PaginationItem>
|
</PaginationItem>
|
||||||
@ -186,15 +197,15 @@ const DataTablePagination: React.FC<DataTablePaginationProps> = ({
|
|||||||
<PaginationContent>
|
<PaginationContent>
|
||||||
<PaginationItem>
|
<PaginationItem>
|
||||||
<PaginationPrevious
|
<PaginationPrevious
|
||||||
onClick={() => onPageChange(Math.max(1, pageIndex - 1))}
|
onClick={() => onPageChange(Math.max(0, pageIndex - 1))}
|
||||||
className={cn(pageIndex <= 1 && "pointer-events-none opacity-50")}
|
className={cn(pageIndex <= 0 && "pointer-events-none opacity-50")}
|
||||||
/>
|
/>
|
||||||
</PaginationItem>
|
</PaginationItem>
|
||||||
{renderPageNumbers()}
|
{renderPageNumbers()}
|
||||||
<PaginationItem>
|
<PaginationItem>
|
||||||
<PaginationNext
|
<PaginationNext
|
||||||
onClick={() => onPageChange(Math.min(pageCount, pageIndex + 1))}
|
onClick={() => onPageChange(Math.min(pageCount - 1, pageIndex + 1))}
|
||||||
className={cn(pageIndex >= pageCount && "pointer-events-none opacity-50")}
|
className={cn(pageIndex >= pageCount - 1 && "pointer-events-none opacity-50")}
|
||||||
/>
|
/>
|
||||||
</PaginationItem>
|
</PaginationItem>
|
||||||
</PaginationContent>
|
</PaginationContent>
|
||||||
|
|||||||
@ -344,9 +344,10 @@ const JenkinsManager: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-foreground">Jenkins 管理</h1>
|
<h1 className="text-3xl font-bold text-foreground">Jenkins 管理</h1>
|
||||||
<p className="text-muted-foreground mt-2">
|
<p className="text-muted-foreground mt-2">
|
||||||
@ -403,7 +404,7 @@ const JenkinsManager: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<ScrollArea className="flex-1">
|
<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 ? (
|
{loading.views ? (
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||||
@ -434,26 +435,24 @@ const JenkinsManager: React.FC = () => {
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{view.viewUrl && (
|
{view.viewUrl && (
|
||||||
<TooltipProvider>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<a
|
||||||
<a
|
href={view.viewUrl}
|
||||||
href={view.viewUrl}
|
target="_blank"
|
||||||
target="_blank"
|
rel="noopener noreferrer"
|
||||||
rel="noopener noreferrer"
|
onClick={(e) => e.stopPropagation()}
|
||||||
onClick={(e) => e.stopPropagation()}
|
className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
>
|
||||||
>
|
<Button variant="ghost" size="icon" className="h-5 w-5">
|
||||||
<Button variant="ghost" size="icon" className="h-5 w-5">
|
<ExternalLink className="h-3 w-3" />
|
||||||
<ExternalLink className="h-3 w-3" />
|
</Button>
|
||||||
</Button>
|
</a>
|
||||||
</a>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent>
|
||||||
<TooltipContent>
|
<p>在 Jenkins 中查看</p>
|
||||||
<p>在 Jenkins 中查看</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{view.description && (
|
{view.description && (
|
||||||
@ -532,26 +531,24 @@ const JenkinsManager: React.FC = () => {
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{job.jobUrl && (
|
{job.jobUrl && (
|
||||||
<TooltipProvider>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<a
|
||||||
<a
|
href={job.jobUrl}
|
||||||
href={job.jobUrl}
|
target="_blank"
|
||||||
target="_blank"
|
rel="noopener noreferrer"
|
||||||
rel="noopener noreferrer"
|
onClick={(e) => e.stopPropagation()}
|
||||||
onClick={(e) => e.stopPropagation()}
|
className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
>
|
||||||
>
|
<Button variant="ghost" size="icon" className="h-6 w-6">
|
||||||
<Button variant="ghost" size="icon" className="h-6 w-6">
|
<ExternalLink className="h-3 w-3" />
|
||||||
<ExternalLink className="h-3 w-3" />
|
</Button>
|
||||||
</Button>
|
</a>
|
||||||
</a>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent>
|
||||||
<TooltipContent>
|
<p>在 Jenkins 中查看</p>
|
||||||
<p>在 Jenkins 中查看</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -564,25 +561,23 @@ const JenkinsManager: React.FC = () => {
|
|||||||
<div className="flex items-center gap-3 text-xs">
|
<div className="flex items-center gap-3 text-xs">
|
||||||
{job.lastBuildStatus && getBuildStatusBadge(job.lastBuildStatus)}
|
{job.lastBuildStatus && getBuildStatusBadge(job.lastBuildStatus)}
|
||||||
{job.healthReportScore !== undefined && (
|
{job.healthReportScore !== undefined && (
|
||||||
<TooltipProvider>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<div className="flex items-center gap-1">
|
||||||
<div className="flex items-center gap-1">
|
<Activity className="h-3 w-3 text-muted-foreground" />
|
||||||
<Activity className="h-3 w-3 text-muted-foreground" />
|
<Progress
|
||||||
<Progress
|
value={job.healthReportScore}
|
||||||
value={job.healthReportScore}
|
className="w-16 h-2"
|
||||||
className="w-16 h-2"
|
/>
|
||||||
/>
|
<span className="text-muted-foreground">
|
||||||
<span className="text-muted-foreground">
|
{job.healthReportScore}%
|
||||||
{job.healthReportScore}%
|
</span>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent>
|
||||||
<TooltipContent>
|
健康度: {job.healthReportScore}%
|
||||||
健康度: {job.healthReportScore}%
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -658,25 +653,23 @@ const JenkinsManager: React.FC = () => {
|
|||||||
{getBuildStatusBadge(build.buildStatus)}
|
{getBuildStatusBadge(build.buildStatus)}
|
||||||
</div>
|
</div>
|
||||||
{build.buildUrl && (
|
{build.buildUrl && (
|
||||||
<TooltipProvider>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<a
|
||||||
<a
|
href={build.buildUrl}
|
||||||
href={build.buildUrl}
|
target="_blank"
|
||||||
target="_blank"
|
rel="noopener noreferrer"
|
||||||
rel="noopener noreferrer"
|
className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
>
|
||||||
>
|
<Button variant="ghost" size="icon" className="h-6 w-6">
|
||||||
<Button variant="ghost" size="icon" className="h-6 w-6">
|
<ExternalLink className="h-3 w-3" />
|
||||||
<ExternalLink className="h-3 w-3" />
|
</Button>
|
||||||
</Button>
|
</a>
|
||||||
</a>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent>
|
||||||
<TooltipContent>
|
<p>在 Jenkins 中查看</p>
|
||||||
<p>在 Jenkins 中查看</p>
|
</TooltipContent>
|
||||||
</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -701,6 +694,7 @@ const JenkinsManager: React.FC = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import { Switch } from '@/components/ui/switch';
|
|||||||
import { useToast } from '@/components/ui/use-toast';
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
import { createScheduleJob, updateScheduleJob } from '../service';
|
import { createScheduleJob, updateScheduleJob, updateJobCron } from '../service';
|
||||||
import type { ScheduleJobResponse, ScheduleJobRequest, JobCategoryResponse } from '../types';
|
import type { ScheduleJobResponse, ScheduleJobRequest, JobCategoryResponse } from '../types';
|
||||||
|
|
||||||
interface JobEditDialogProps {
|
interface JobEditDialogProps {
|
||||||
@ -46,6 +46,8 @@ const JobEditDialog: React.FC<JobEditDialogProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [submitting, setSubmitting] = React.useState(false);
|
const [submitting, setSubmitting] = React.useState(false);
|
||||||
|
// 保存原始的 Cron 表达式,用于判断是否需要更新调度器
|
||||||
|
const [originalCronExpression, setOriginalCronExpression] = React.useState<string>('');
|
||||||
|
|
||||||
const form = useForm<ScheduleJobRequest>({
|
const form = useForm<ScheduleJobRequest>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@ -69,7 +71,8 @@ const JobEditDialog: React.FC<JobEditDialogProps> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
if (job) {
|
if (job) {
|
||||||
// 编辑模式
|
// 编辑模式 - 保存原始的 Cron 表达式
|
||||||
|
setOriginalCronExpression(job.cronExpression);
|
||||||
form.reset({
|
form.reset({
|
||||||
jobName: job.jobName,
|
jobName: job.jobName,
|
||||||
jobDescription: job.jobDescription || '',
|
jobDescription: job.jobDescription || '',
|
||||||
@ -86,7 +89,8 @@ const JobEditDialog: React.FC<JobEditDialogProps> = ({
|
|||||||
alertEmail: job.alertEmail || '',
|
alertEmail: job.alertEmail || '',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 新建模式
|
// 新建模式 - 重置原始 Cron 表达式
|
||||||
|
setOriginalCronExpression('');
|
||||||
form.reset({
|
form.reset({
|
||||||
jobName: '',
|
jobName: '',
|
||||||
jobDescription: '',
|
jobDescription: '',
|
||||||
@ -110,11 +114,35 @@ const JobEditDialog: React.FC<JobEditDialogProps> = ({
|
|||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
if (job) {
|
if (job) {
|
||||||
|
// 更新任务
|
||||||
await updateScheduleJob(job.id, values);
|
await updateScheduleJob(job.id, values);
|
||||||
toast({
|
|
||||||
title: '更新成功',
|
// 检查 Cron 表达式是否变化
|
||||||
description: `任务 "${values.jobName}" 已更新`,
|
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 {
|
} else {
|
||||||
await createScheduleJob(values);
|
await createScheduleJob(values);
|
||||||
toast({
|
toast({
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user