增加服务器阈值规则
This commit is contained in:
parent
e298dcf83c
commit
c36ee0808c
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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="确定"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -7,6 +7,7 @@ import {
|
||||
Activity,
|
||||
AlertCircle,
|
||||
HelpCircle,
|
||||
Terminal,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
Table,
|
||||
@ -34,6 +35,7 @@ interface ServerTableProps {
|
||||
onTest: (server: ServerResponse) => void;
|
||||
onEdit: (server: ServerResponse) => void;
|
||||
onDelete: (server: ServerResponse) => void;
|
||||
onSSHConnect: (server: ServerResponse) => void;
|
||||
isTesting?: (serverId: number) => boolean;
|
||||
getOsIcon: (osType?: string) => React.ReactNode;
|
||||
}
|
||||
@ -43,6 +45,7 @@ export const ServerTable: React.FC<ServerTableProps> = ({
|
||||
onTest,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onSSHConnect,
|
||||
isTesting,
|
||||
getOsIcon,
|
||||
}) => {
|
||||
@ -74,7 +77,6 @@ export const ServerTable: React.FC<ServerTableProps> = ({
|
||||
<TableHead width="100px">内存(GB)</TableHead>
|
||||
<TableHead width="100px">磁盘(GB)</TableHead>
|
||||
<TableHead width="120px">SSH认证</TableHead>
|
||||
<TableHead width="140px">分类</TableHead>
|
||||
<TableHead width="150px">最后连接</TableHead>
|
||||
<TableHead sticky width="140px" className="text-right">操作</TableHead>
|
||||
</TableRow>
|
||||
@ -82,7 +84,7 @@ export const ServerTable: React.FC<ServerTableProps> = ({
|
||||
<TableBody>
|
||||
{servers.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={11} className="text-center py-8">
|
||||
<TableCell colSpan={10} className="text-center py-8">
|
||||
<span className="text-muted-foreground">暂无服务器数据</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@ -129,16 +131,24 @@ export const ServerTable: React.FC<ServerTableProps> = ({
|
||||
{server.authType === 'PASSWORD' ? '密码' : '密钥'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{server.categoryName || '未分类'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-muted-foreground">{formatTime(server.lastConnectTime)}</span>
|
||||
</TableCell>
|
||||
<TableCell sticky className="text-right">
|
||||
<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>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
RotateCcw,
|
||||
Grid3x3,
|
||||
List,
|
||||
Bell,
|
||||
} from 'lucide-react';
|
||||
import { Card, CardHeader, CardContent } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
@ -37,6 +38,7 @@ import type { ServerResponse, ServerCategoryResponse, ServerStatus, OsType } fro
|
||||
import { ServerStatusLabels, OsTypeLabels } from './types';
|
||||
import { getServers, getServerCategories, deleteServer, testServerConnection } from './service';
|
||||
import { CategoryManageDialog } from './components/CategoryManageDialog';
|
||||
import { AlertRuleManageDialog } from './components/AlertRuleManageDialog';
|
||||
import { ServerEditDialog } from './components/ServerEditDialog';
|
||||
import { ServerCard } from './components/ServerCard';
|
||||
import { ServerTable } from './components/ServerTable';
|
||||
@ -71,6 +73,7 @@ const ServerList: React.FC = () => {
|
||||
const [tempOsType, setTempOsType] = useState<OsType | undefined>();
|
||||
|
||||
const [categoryDialogOpen, setCategoryDialogOpen] = useState(false);
|
||||
const [alertRuleDialogOpen, setAlertRuleDialogOpen] = useState(false);
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||
const [editingServer, setEditingServer] = useState<ServerResponse | null>(null);
|
||||
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 items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">服务器管理</h1>
|
||||
<p className="text-muted-foreground mt-1">管理和监控服务器资源,支持SSH连接和分类管理</p>
|
||||
</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">
|
||||
@ -470,6 +455,23 @@ const ServerList: React.FC = () => {
|
||||
<Search className="h-4 w-4 mr-1.5" />
|
||||
查询
|
||||
</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>
|
||||
@ -545,6 +547,7 @@ const ServerList: React.FC = () => {
|
||||
onTest={handleTestConnection}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
onSSHConnect={handleSSHConnect}
|
||||
isTesting={(serverId) => testingServerId === serverId}
|
||||
getOsIcon={getOsIcon}
|
||||
/>
|
||||
@ -581,6 +584,13 @@ const ServerList: React.FC = () => {
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
|
||||
{/* 告警规则管理对话框 */}
|
||||
<AlertRuleManageDialog
|
||||
open={alertRuleDialogOpen}
|
||||
onOpenChange={setAlertRuleDialogOpen}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
|
||||
{/* 服务器编辑对话框 */}
|
||||
<ServerEditDialog
|
||||
open={editDialogOpen}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 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>;
|
||||
|
||||
|
||||
@ -6,6 +6,9 @@ import type {
|
||||
ServerResponse,
|
||||
ServerRequest,
|
||||
ServerConnectionTestResult,
|
||||
AlertRuleResponse,
|
||||
AlertRuleQuery,
|
||||
AlertRuleRequest,
|
||||
} from './types';
|
||||
import type { ServerFormValues } from './schema';
|
||||
import type { Page } from '@/types/base';
|
||||
@ -13,6 +16,7 @@ import type { Page } from '@/types/base';
|
||||
// API 基础路径
|
||||
const CATEGORY_URL = '/api/v1/server-category';
|
||||
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) =>
|
||||
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}`);
|
||||
|
||||
|
||||
@ -262,3 +262,93 @@ export interface ServerConnectionTestResult {
|
||||
diskSize: number;
|
||||
}
|
||||
|
||||
// ==================== 告警规则 ====================
|
||||
|
||||
/**
|
||||
* 告警类型枚举
|
||||
*/
|
||||
export enum AlertType {
|
||||
/** CPU使用率 */
|
||||
CPU = 'CPU',
|
||||
/** 内存使用率 */
|
||||
MEMORY = 'MEMORY',
|
||||
/** 磁盘使用率 */
|
||||
DISK = 'DISK',
|
||||
/** 网络流量 */
|
||||
NETWORK = 'NETWORK',
|
||||
}
|
||||
|
||||
/**
|
||||
* 告警规则响应类型
|
||||
*/
|
||||
export interface AlertRuleResponse extends BaseResponse {
|
||||
/** 服务器ID(null表示全局规则) */
|
||||
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 {
|
||||
/** 服务器ID(null表示全局规则) */
|
||||
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: '网络流量告警' },
|
||||
};
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user