增加服务器阈值规则

This commit is contained in:
dengqichen 2025-12-08 17:02:05 +08:00
parent e298dcf83c
commit c36ee0808c
7 changed files with 869 additions and 29 deletions

View File

@ -0,0 +1,394 @@
import React, { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Loader2 } from 'lucide-react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogBody,
DialogFooter,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Switch } from '@/components/ui/switch';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
FormDescription,
} from '@/components/ui/form';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { useToast } from '@/components/ui/use-toast';
import type { AlertRuleResponse, ServerResponse, AlertRuleRequest } from '../types';
import { AlertType, AlertTypeLabels } from '../types';
import { alertRuleFormSchema, type AlertRuleFormValues } from '../schema';
import { createAlertRule, updateAlertRule, getServers } from '../service';
interface AlertRuleFormDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
rule: AlertRuleResponse | null;
onSuccess?: () => void;
}
export const AlertRuleFormDialog: React.FC<AlertRuleFormDialogProps> = ({
open,
onOpenChange,
rule,
onSuccess,
}) => {
const { toast } = useToast();
const [loading, setLoading] = useState(false);
const [servers, setServers] = useState<ServerResponse[]>([]);
const form = useForm<AlertRuleFormValues>({
resolver: zodResolver(alertRuleFormSchema),
defaultValues: {
serverId: null,
ruleName: '',
alertType: AlertType.CPU,
warningThreshold: 75,
criticalThreshold: 90,
durationMinutes: 5,
enabled: true,
description: '',
},
});
// 加载服务器列表
useEffect(() => {
if (open) {
loadServers();
}
}, [open]);
const loadServers = async () => {
try {
const result = await getServers({ pageNum: 0, size: 1000 });
setServers(result?.content || []);
} catch (error) {
console.error('加载服务器列表失败:', error);
}
};
// 编辑时初始化表单
useEffect(() => {
if (open && rule) {
form.reset({
serverId: rule.serverId,
ruleName: rule.ruleName,
alertType: rule.alertType,
warningThreshold: rule.warningThreshold,
criticalThreshold: rule.criticalThreshold,
durationMinutes: rule.durationMinutes,
enabled: rule.enabled,
description: rule.description || '',
});
} else if (open && !rule) {
form.reset({
serverId: null,
ruleName: '',
alertType: AlertType.CPU,
warningThreshold: 75,
criticalThreshold: 90,
durationMinutes: 5,
enabled: true,
description: '',
});
}
}, [open, rule, form]);
const onSubmit = async (values: AlertRuleFormValues) => {
setLoading(true);
try {
const requestData: AlertRuleRequest = {
serverId: values.serverId ?? null,
ruleName: values.ruleName!,
alertType: values.alertType!,
warningThreshold: values.warningThreshold!,
criticalThreshold: values.criticalThreshold!,
durationMinutes: values.durationMinutes!,
enabled: values.enabled!,
description: values.description,
};
if (rule) {
await updateAlertRule(rule.id, requestData);
toast({
title: '更新成功',
description: `告警规则"${values.ruleName}"已更新`,
});
} else {
await createAlertRule(requestData);
toast({
title: '创建成功',
description: `告警规则"${values.ruleName}"已创建`,
});
}
onSuccess?.();
onOpenChange(false);
} catch (error: any) {
toast({
variant: 'destructive',
title: rule ? '更新失败' : '创建失败',
description: error.response?.data?.message || '操作失败',
});
} finally {
setLoading(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{rule ? '编辑告警规则' : '新增告警规则'}</DialogTitle>
</DialogHeader>
<DialogBody>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
{/* 规则范围 */}
<FormField
control={form.control}
name="serverId"
render={({ field }) => (
<FormItem className="col-span-2">
<FormLabel></FormLabel>
<Select
value={field.value === null ? 'global' : field.value.toString()}
onValueChange={(value) => {
field.onChange(value === 'global' ? null : Number(value));
}}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="选择规则范围" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="global"></SelectItem>
{servers.map((server) => (
<SelectItem key={server.id} value={server.id.toString()}>
- {server.serverName} ({server.hostIp})
</SelectItem>
))}
</SelectContent>
</Select>
<FormDescription>
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{/* 规则名称 */}
<FormField
control={form.control}
name="ruleName"
render={({ field }) => (
<FormItem className="col-span-2">
<FormLabel>
<span className="text-destructive">*</span>
</FormLabel>
<FormControl>
<Input placeholder="例如全局CPU告警规则" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* 告警类型 */}
<FormField
control={form.control}
name="alertType"
render={({ field }) => (
<FormItem>
<FormLabel>
<span className="text-destructive">*</span>
</FormLabel>
<Select
value={field.value}
onValueChange={field.onChange}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="选择告警类型" />
</SelectTrigger>
</FormControl>
<SelectContent>
{Object.entries(AlertTypeLabels).map(([key, value]) => (
<SelectItem key={key} value={key}>
{value.label} ({value.unit})
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{/* 持续时长 */}
<FormField
control={form.control}
name="durationMinutes"
render={({ field }) => (
<FormItem>
<FormLabel>
() <span className="text-destructive">*</span>
</FormLabel>
<FormControl>
<Input
type="number"
min="1"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
<FormDescription>
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{/* 警告阈值 */}
<FormField
control={form.control}
name="warningThreshold"
render={({ field }) => (
<FormItem>
<FormLabel>
<span className="text-destructive">*</span>
</FormLabel>
<FormControl>
<div className="flex items-center gap-2">
<Input
type="number"
min="0"
max="100"
step="0.1"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
className="flex-1"
/>
<span className="text-sm text-muted-foreground">
{AlertTypeLabels[form.watch('alertType')]?.unit || '%'}
</span>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* 严重阈值 */}
<FormField
control={form.control}
name="criticalThreshold"
render={({ field }) => (
<FormItem>
<FormLabel>
<span className="text-destructive">*</span>
</FormLabel>
<FormControl>
<div className="flex items-center gap-2">
<Input
type="number"
min="0"
max="100"
step="0.1"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
className="flex-1"
/>
<span className="text-sm text-muted-foreground">
{AlertTypeLabels[form.watch('alertType')]?.unit || '%'}
</span>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* 是否启用 */}
<FormField
control={form.control}
name="enabled"
render={({ field }) => (
<FormItem className="col-span-2 flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base"></FormLabel>
<FormDescription>
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
{/* 描述 */}
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem className="col-span-2">
<FormLabel></FormLabel>
<FormControl>
<Textarea
placeholder="输入规则描述..."
rows={3}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={loading}
>
</Button>
<Button type="submit" disabled={loading}>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{rule ? '更新' : '创建'}
</Button>
</DialogFooter>
</form>
</Form>
</DialogBody>
</DialogContent>
</Dialog>
);
};

View File

@ -0,0 +1,262 @@
import React, { useState, useEffect, useRef } from 'react';
import { Plus, Edit, Trash2, AlertTriangle, Bell } from 'lucide-react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogBody,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { useToast } from '@/components/ui/use-toast';
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import type { AlertRuleResponse, AlertRuleQuery } from '../types';
import { AlertType, AlertTypeLabels } from '../types';
import {
getAlertRulePage,
deleteAlertRule,
} from '../service';
import { PaginatedTable, type ColumnDef, type SearchFieldDef, type PaginatedTableRef } from '@/components/ui/paginated-table';
import { AlertRuleFormDialog } from './AlertRuleFormDialog';
interface AlertRuleManageDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess?: () => void;
}
export const AlertRuleManageDialog: React.FC<AlertRuleManageDialogProps> = ({
open,
onOpenChange,
onSuccess,
}) => {
const { toast } = useToast();
const tableRef = useRef<PaginatedTableRef<AlertRuleResponse>>(null);
const [formDialogOpen, setFormDialogOpen] = useState(false);
const [editingRule, setEditingRule] = useState<AlertRuleResponse | null>(null);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [ruleToDelete, setRuleToDelete] = useState<AlertRuleResponse | null>(null);
// 刷新列表
const handleSuccess = () => {
tableRef.current?.refresh();
onSuccess?.();
};
// 新建规则
const handleCreate = () => {
setEditingRule(null);
setFormDialogOpen(true);
};
// 编辑规则
const handleEdit = (rule: AlertRuleResponse) => {
setEditingRule(rule);
setFormDialogOpen(true);
};
// 删除规则
const handleDelete = (rule: AlertRuleResponse) => {
setRuleToDelete(rule);
setDeleteDialogOpen(true);
};
const confirmDelete = async () => {
if (!ruleToDelete) return;
await deleteAlertRule(ruleToDelete.id);
};
// 搜索字段定义
const searchFields: SearchFieldDef[] = [
{ key: 'ruleName', type: 'input', placeholder: '规则名称', width: 'w-[180px]' },
{
key: 'alertType',
type: 'select',
placeholder: '告警类型',
width: 'w-[140px]',
options: Object.entries(AlertTypeLabels).map(([key, value]) => ({
label: value.label,
value: key,
})),
},
{
key: 'enabled',
type: 'select',
placeholder: '状态',
width: 'w-[120px]',
options: [
{ label: '启用', value: 'true' },
{ label: '禁用', value: 'false' },
],
},
];
// 列定义
const columns: ColumnDef<AlertRuleResponse>[] = [
{ key: 'id', title: 'ID', dataIndex: 'id', width: '80px' },
{ key: 'ruleName', title: '规则名称', dataIndex: 'ruleName', width: '200px' },
{
key: 'ruleScope',
title: '规则范围',
width: '150px',
render: (_, record) => (
<Badge variant={record.serverId ? 'default' : 'secondary'}>
{record.serverId ? `专属规则 (${record.serverName || `ID:${record.serverId}`})` : '全局规则'}
</Badge>
),
},
{
key: 'alertType',
title: '告警类型',
width: '120px',
render: (_, record) => {
const typeInfo = AlertTypeLabels[record.alertType];
return (
<div className="flex items-center gap-2">
<AlertTriangle className="h-4 w-4 text-orange-500" />
<span>{typeInfo.label}</span>
</div>
);
},
},
{
key: 'thresholds',
title: '阈值',
width: '200px',
render: (_, record) => {
const unit = AlertTypeLabels[record.alertType].unit;
return (
<div className="space-y-1 text-sm">
<div className="flex items-center gap-2">
<Badge variant="outline" className="bg-yellow-50 text-yellow-700 border-yellow-300">
</Badge>
<span>{record.warningThreshold}{unit}</span>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline" className="bg-red-50 text-red-700 border-red-300">
</Badge>
<span>{record.criticalThreshold}{unit}</span>
</div>
</div>
);
},
},
{
key: 'durationMinutes',
title: '持续时长',
width: '100px',
render: (_, record) => `${record.durationMinutes} 分钟`,
},
{
key: 'enabled',
title: '状态',
width: '100px',
render: (_, record) => (
<Badge variant={record.enabled ? 'default' : 'secondary'}>
{record.enabled ? '启用' : '禁用'}
</Badge>
),
},
{
key: 'description',
title: '描述',
dataIndex: 'description',
width: '200px',
render: (value) => value || '-',
},
{
key: 'actions',
title: '操作',
width: '150px',
sticky: true,
render: (_, record) => (
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleEdit(record)}
>
<Edit className="h-4 w-4 mr-1" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDelete(record)}
className="text-destructive hover:text-destructive"
>
<Trash2 className="h-4 w-4 mr-1" />
</Button>
</div>
),
},
];
// 工具栏
const toolbar = (
<Button onClick={handleCreate}>
<Plus className="h-4 w-4 mr-2" />
</Button>
);
return (
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[90vw] max-h-[85vh]">
<DialogHeader>
<div className="flex items-center gap-2">
<Bell className="h-5 w-5 text-primary" />
<DialogTitle></DialogTitle>
</div>
</DialogHeader>
<DialogBody className="overflow-hidden">
<PaginatedTable<AlertRuleResponse, AlertRuleQuery>
ref={tableRef}
fetchFn={getAlertRulePage}
columns={columns}
searchFields={searchFields}
toolbar={toolbar}
rowKey="id"
minWidth="1400px"
emptyText="暂无告警规则"
/>
</DialogBody>
</DialogContent>
</Dialog>
{/* 规则编辑对话框 */}
<AlertRuleFormDialog
open={formDialogOpen}
onOpenChange={setFormDialogOpen}
rule={editingRule}
onSuccess={handleSuccess}
/>
{/* 删除确认对话框 */}
<ConfirmDialog
open={deleteDialogOpen}
onOpenChange={setDeleteDialogOpen}
title="确认删除"
description={`确定要删除告警规则"${ruleToDelete?.ruleName}"吗?此操作无法撤销。`}
item={ruleToDelete}
onConfirm={confirmDelete}
onSuccess={() => {
toast({
title: '删除成功',
description: `告警规则"${ruleToDelete?.ruleName}"已删除`,
});
setRuleToDelete(null);
handleSuccess();
}}
variant="destructive"
confirmText="确定"
/>
</>
);
};

View File

@ -7,6 +7,7 @@ import {
Activity, Activity,
AlertCircle, AlertCircle,
HelpCircle, HelpCircle,
Terminal,
} from 'lucide-react'; } from 'lucide-react';
import { import {
Table, Table,
@ -34,6 +35,7 @@ interface ServerTableProps {
onTest: (server: ServerResponse) => void; onTest: (server: ServerResponse) => void;
onEdit: (server: ServerResponse) => void; onEdit: (server: ServerResponse) => void;
onDelete: (server: ServerResponse) => void; onDelete: (server: ServerResponse) => void;
onSSHConnect: (server: ServerResponse) => void;
isTesting?: (serverId: number) => boolean; isTesting?: (serverId: number) => boolean;
getOsIcon: (osType?: string) => React.ReactNode; getOsIcon: (osType?: string) => React.ReactNode;
} }
@ -43,6 +45,7 @@ export const ServerTable: React.FC<ServerTableProps> = ({
onTest, onTest,
onEdit, onEdit,
onDelete, onDelete,
onSSHConnect,
isTesting, isTesting,
getOsIcon, getOsIcon,
}) => { }) => {
@ -74,7 +77,6 @@ export const ServerTable: React.FC<ServerTableProps> = ({
<TableHead width="100px">(GB)</TableHead> <TableHead width="100px">(GB)</TableHead>
<TableHead width="100px">(GB)</TableHead> <TableHead width="100px">(GB)</TableHead>
<TableHead width="120px">SSH认证</TableHead> <TableHead width="120px">SSH认证</TableHead>
<TableHead width="140px"></TableHead>
<TableHead width="150px"></TableHead> <TableHead width="150px"></TableHead>
<TableHead sticky width="140px" className="text-right"></TableHead> <TableHead sticky width="140px" className="text-right"></TableHead>
</TableRow> </TableRow>
@ -82,7 +84,7 @@ export const ServerTable: React.FC<ServerTableProps> = ({
<TableBody> <TableBody>
{servers.length === 0 ? ( {servers.length === 0 ? (
<TableRow> <TableRow>
<TableCell colSpan={11} className="text-center py-8"> <TableCell colSpan={10} className="text-center py-8">
<span className="text-muted-foreground"></span> <span className="text-muted-foreground"></span>
</TableCell> </TableCell>
</TableRow> </TableRow>
@ -129,16 +131,24 @@ export const ServerTable: React.FC<ServerTableProps> = ({
{server.authType === 'PASSWORD' ? '密码' : '密钥'} {server.authType === 'PASSWORD' ? '密码' : '密钥'}
</Badge> </Badge>
</TableCell> </TableCell>
<TableCell>
<Badge variant="secondary" className="text-xs">
{server.categoryName || '未分类'}
</Badge>
</TableCell>
<TableCell> <TableCell>
<span className="text-sm text-muted-foreground">{formatTime(server.lastConnectTime)}</span> <span className="text-sm text-muted-foreground">{formatTime(server.lastConnectTime)}</span>
</TableCell> </TableCell>
<TableCell sticky className="text-right"> <TableCell sticky className="text-right">
<div className="flex items-center justify-end gap-1"> <div className="flex items-center justify-end gap-1">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={() => onSSHConnect(server)}
className="h-8 w-8 p-0"
>
<Terminal className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>SSH连接</TooltipContent>
</Tooltip>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button

View File

@ -13,6 +13,7 @@ import {
RotateCcw, RotateCcw,
Grid3x3, Grid3x3,
List, List,
Bell,
} from 'lucide-react'; } from 'lucide-react';
import { Card, CardHeader, CardContent } from '@/components/ui/card'; import { Card, CardHeader, CardContent } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
@ -37,6 +38,7 @@ import type { ServerResponse, ServerCategoryResponse, ServerStatus, OsType } fro
import { ServerStatusLabels, OsTypeLabels } from './types'; import { ServerStatusLabels, OsTypeLabels } from './types';
import { getServers, getServerCategories, deleteServer, testServerConnection } from './service'; import { getServers, getServerCategories, deleteServer, testServerConnection } from './service';
import { CategoryManageDialog } from './components/CategoryManageDialog'; import { CategoryManageDialog } from './components/CategoryManageDialog';
import { AlertRuleManageDialog } from './components/AlertRuleManageDialog';
import { ServerEditDialog } from './components/ServerEditDialog'; import { ServerEditDialog } from './components/ServerEditDialog';
import { ServerCard } from './components/ServerCard'; import { ServerCard } from './components/ServerCard';
import { ServerTable } from './components/ServerTable'; import { ServerTable } from './components/ServerTable';
@ -71,6 +73,7 @@ const ServerList: React.FC = () => {
const [tempOsType, setTempOsType] = useState<OsType | undefined>(); const [tempOsType, setTempOsType] = useState<OsType | undefined>();
const [categoryDialogOpen, setCategoryDialogOpen] = useState(false); const [categoryDialogOpen, setCategoryDialogOpen] = useState(false);
const [alertRuleDialogOpen, setAlertRuleDialogOpen] = useState(false);
const [editDialogOpen, setEditDialogOpen] = useState(false); const [editDialogOpen, setEditDialogOpen] = useState(false);
const [editingServer, setEditingServer] = useState<ServerResponse | null>(null); const [editingServer, setEditingServer] = useState<ServerResponse | null>(null);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
@ -282,28 +285,10 @@ const ServerList: React.FC = () => {
{/* 顶部区域 - 统计和标题 */} {/* 顶部区域 - 统计和标题 */}
<div className="flex-shrink-0 space-y-4 p-6"> <div className="flex-shrink-0 space-y-4 p-6">
{/* 标题栏 */} {/* 标题栏 */}
<div className="flex items-center justify-between">
<div> <div>
<h1 className="text-3xl font-bold"></h1> <h1 className="text-3xl font-bold"></h1>
<p className="text-muted-foreground mt-1">SSH连接和分类管理</p> <p className="text-muted-foreground mt-1">SSH连接和分类管理</p>
</div> </div>
<div className="flex items-center gap-2">
<Button variant="outline" onClick={() => setCategoryDialogOpen(true)}>
<Settings className="h-4 w-4 mr-2" />
</Button>
<Button
onClick={() => {
setEditingServer(null);
setEditDialogOpen(true);
}}
className="bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700"
>
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
{/* 统计卡片 - 更紧凑的布局 */} {/* 统计卡片 - 更紧凑的布局 */}
<div className="grid gap-3 md:grid-cols-4"> <div className="grid gap-3 md:grid-cols-4">
@ -470,6 +455,23 @@ const ServerList: React.FC = () => {
<Search className="h-4 w-4 mr-1.5" /> <Search className="h-4 w-4 mr-1.5" />
</Button> </Button>
<Button variant="outline" onClick={() => setAlertRuleDialogOpen(true)}>
<Bell className="h-4 w-4 mr-2" />
</Button>
<Button variant="outline" onClick={() => setCategoryDialogOpen(true)}>
<Settings className="h-4 w-4 mr-2" />
</Button>
<Button
onClick={() => {
setEditingServer(null);
setEditDialogOpen(true);
}}
>
<Plus className="h-4 w-4 mr-2" />
</Button>
</div> </div>
</div> </div>
</div> </div>
@ -545,6 +547,7 @@ const ServerList: React.FC = () => {
onTest={handleTestConnection} onTest={handleTestConnection}
onEdit={handleEdit} onEdit={handleEdit}
onDelete={handleDelete} onDelete={handleDelete}
onSSHConnect={handleSSHConnect}
isTesting={(serverId) => testingServerId === serverId} isTesting={(serverId) => testingServerId === serverId}
getOsIcon={getOsIcon} getOsIcon={getOsIcon}
/> />
@ -581,6 +584,13 @@ const ServerList: React.FC = () => {
onSuccess={handleSuccess} onSuccess={handleSuccess}
/> />
{/* 告警规则管理对话框 */}
<AlertRuleManageDialog
open={alertRuleDialogOpen}
onOpenChange={setAlertRuleDialogOpen}
onSuccess={handleSuccess}
/>
{/* 服务器编辑对话框 */} {/* 服务器编辑对话框 */}
<ServerEditDialog <ServerEditDialog
open={editDialogOpen} open={editDialogOpen}

View File

@ -1,5 +1,5 @@
import { z } from 'zod'; import { z } from 'zod';
import { OsType, AuthType, ServerStatus } from './types'; import { OsType, AuthType, ServerStatus, AlertType } from './types';
/** /**
* *
@ -73,3 +73,35 @@ export const serverFormSchema = z.object({
export type ServerCategoryFormValues = z.infer<typeof serverCategoryFormSchema>; export type ServerCategoryFormValues = z.infer<typeof serverCategoryFormSchema>;
export type ServerFormValues = z.infer<typeof serverFormSchema>; export type ServerFormValues = z.infer<typeof serverFormSchema>;
/**
*
*/
export const alertRuleFormSchema = z.object({
serverId: z.union([z.number(), z.null()]).default(null),
ruleName: z.string().min(1, '规则名称不能为空').max(100, '规则名称不能超过100个字符'),
alertType: z.nativeEnum(AlertType, {
errorMap: () => ({ message: '请选择告警类型' })
}),
warningThreshold: z.number()
.min(0, '警告阈值不能小于0')
.max(100, '警告阈值不能大于100'),
criticalThreshold: z.number()
.min(0, '严重阈值不能小于0')
.max(100, '严重阈值不能大于100'),
durationMinutes: z.number()
.min(1, '持续时长至少为1分钟'),
enabled: z.boolean().default(true),
description: z.string().max(500, '描述不能超过500个字符').optional(),
}).superRefine((data, ctx) => {
// 严重阈值必须大于警告阈值
if (data.criticalThreshold <= data.warningThreshold) {
ctx.addIssue({
code: 'custom',
message: '严重阈值必须大于警告阈值',
path: ['criticalThreshold'],
});
}
});
export type AlertRuleFormValues = z.infer<typeof alertRuleFormSchema>;

View File

@ -6,6 +6,9 @@ import type {
ServerResponse, ServerResponse,
ServerRequest, ServerRequest,
ServerConnectionTestResult, ServerConnectionTestResult,
AlertRuleResponse,
AlertRuleQuery,
AlertRuleRequest,
} from './types'; } from './types';
import type { ServerFormValues } from './schema'; import type { ServerFormValues } from './schema';
import type { Page } from '@/types/base'; import type { Page } from '@/types/base';
@ -13,6 +16,7 @@ import type { Page } from '@/types/base';
// API 基础路径 // API 基础路径
const CATEGORY_URL = '/api/v1/server-category'; const CATEGORY_URL = '/api/v1/server-category';
const SERVER_URL = '/api/v1/server'; const SERVER_URL = '/api/v1/server';
const ALERT_RULE_URL = '/api/v1/server/alert-rule';
// ==================== 服务器分类 ==================== // ==================== 服务器分类 ====================
@ -117,3 +121,41 @@ export const batchDeleteServers = (ids: number[]) =>
export const testServerConnection = (id: number) => export const testServerConnection = (id: number) =>
request.post<ServerConnectionTestResult>(`${SERVER_URL}/${id}/test-connection`); request.post<ServerConnectionTestResult>(`${SERVER_URL}/${id}/test-connection`);
// ==================== 告警规则 ====================
/**
*
*/
export const getAlertRules = (params?: AlertRuleQuery) =>
request.get<AlertRuleResponse[]>(`${ALERT_RULE_URL}/list`, { params });
/**
*
*/
export const getAlertRulePage = (params?: AlertRuleQuery) =>
request.get<Page<AlertRuleResponse>>(`${ALERT_RULE_URL}/page`, { params });
/**
*
*/
export const getAlertRule = (id: number) =>
request.get<AlertRuleResponse>(`${ALERT_RULE_URL}/${id}`);
/**
*
*/
export const createAlertRule = (data: AlertRuleRequest) =>
request.post<AlertRuleResponse>(ALERT_RULE_URL, data);
/**
*
*/
export const updateAlertRule = (id: number, data: AlertRuleRequest) =>
request.put<AlertRuleResponse>(`${ALERT_RULE_URL}/${id}`, data);
/**
*
*/
export const deleteAlertRule = (id: number) =>
request.delete<void>(`${ALERT_RULE_URL}/${id}`);

View File

@ -262,3 +262,93 @@ export interface ServerConnectionTestResult {
diskSize: number; diskSize: number;
} }
// ==================== 告警规则 ====================
/**
*
*/
export enum AlertType {
/** CPU使用率 */
CPU = 'CPU',
/** 内存使用率 */
MEMORY = 'MEMORY',
/** 磁盘使用率 */
DISK = 'DISK',
/** 网络流量 */
NETWORK = 'NETWORK',
}
/**
*
*/
export interface AlertRuleResponse extends BaseResponse {
/** 服务器IDnull表示全局规则 */
serverId: number | null;
/** 规则名称 */
ruleName: string;
/** 告警类型 */
alertType: AlertType;
/** 警告阈值 */
warningThreshold: number;
/** 严重阈值 */
criticalThreshold: number;
/** 持续时长(分钟) */
durationMinutes: number;
/** 描述 */
description?: string;
/** 服务器名称(关联数据) */
serverName?: string;
}
/**
*
*/
export interface AlertRuleQuery {
/** 服务器ID */
serverId?: number | null;
/** 规则名称 */
ruleName?: string;
/** 告警类型 */
alertType?: AlertType;
/** 是否启用 */
enabled?: boolean;
/** 页码从0开始 */
page?: number;
/** 每页数量 */
size?: number;
/** 排序例如id,desc */
sort?: string;
}
/**
*
*/
export interface AlertRuleRequest {
/** 服务器IDnull表示全局规则 */
serverId: number | null;
/** 规则名称 */
ruleName: string;
/** 告警类型 */
alertType: AlertType;
/** 警告阈值 */
warningThreshold: number;
/** 严重阈值 */
criticalThreshold: number;
/** 持续时长(分钟) */
durationMinutes: number;
/** 是否启用 */
enabled: boolean;
/** 描述 */
description?: string;
}
/**
*
*/
export const AlertTypeLabels: Record<AlertType, { label: string; unit: string; description: string }> = {
[AlertType.CPU]: { label: 'CPU使用率', unit: '%', description: 'CPU使用率告警' },
[AlertType.MEMORY]: { label: '内存使用率', unit: '%', description: '内存使用率告警' },
[AlertType.DISK]: { label: '磁盘使用率', unit: '%', description: '磁盘使用率告警' },
[AlertType.NETWORK]: { label: '网络流量', unit: 'MB/s', description: '网络流量告警' },
};