告警规则表单优化:数字输入框使用本地状态管理,解决清空后回退到默认值问题
This commit is contained in:
parent
a5dda91f20
commit
c1e0763472
@ -40,7 +40,7 @@ export const Terminal: React.FC<TerminalProps> = ({
|
|||||||
const [showSearch, setShowSearch] = useState(false);
|
const [showSearch, setShowSearch] = useState(false);
|
||||||
const [currentTheme, setCurrentTheme] = useState('dark');
|
const [currentTheme, setCurrentTheme] = useState('dark');
|
||||||
const [auditShown, setAuditShown] = useState(false);
|
const [auditShown, setAuditShown] = useState(false);
|
||||||
const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>('disconnected'); // 初始状态,会被实例状态覆盖
|
const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>('connecting'); // 初始状态为连接中,符合正常生命周期
|
||||||
const [errorMessage, setErrorMessage] = useState<string>('');
|
const [errorMessage, setErrorMessage] = useState<string>('');
|
||||||
const [connectedTime, setConnectedTime] = useState<Date | null>(null);
|
const [connectedTime, setConnectedTime] = useState<Date | null>(null);
|
||||||
|
|
||||||
@ -103,10 +103,11 @@ export const Terminal: React.FC<TerminalProps> = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 延迟连接(仅在未连接时)
|
// 延迟连接(仅在未连接或需要连接时)
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
const currentState = instance.getState();
|
const currentState = instance.getState();
|
||||||
if (currentState.status === 'disconnected' || currentState.status === 'error') {
|
// 在 connecting、disconnected 或 error 状态下需要建立连接
|
||||||
|
if (currentState.status === 'connecting' || currentState.status === 'disconnected' || currentState.status === 'error') {
|
||||||
instance.connect();
|
instance.connect();
|
||||||
}
|
}
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export type ErrorCallback = (error: string) => void;
|
|||||||
* 抽象连接策略基类
|
* 抽象连接策略基类
|
||||||
*/
|
*/
|
||||||
export abstract class BaseConnectionStrategy {
|
export abstract class BaseConnectionStrategy {
|
||||||
protected status: ConnectionStatus = 'disconnected';
|
protected status: ConnectionStatus = 'connecting'; // 初始状态为connecting,符合打开终端时的正常生命周期
|
||||||
protected statusListeners: Set<StatusChangeCallback> = new Set();
|
protected statusListeners: Set<StatusChangeCallback> = new Set();
|
||||||
protected messageListeners: Set<MessageCallback> = new Set();
|
protected messageListeners: Set<MessageCallback> = new Set();
|
||||||
protected errorListeners: Set<ErrorCallback> = new Set();
|
protected errorListeners: Set<ErrorCallback> = new Set();
|
||||||
|
|||||||
@ -3,7 +3,22 @@ import * as React from "react"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||||
({ className, type, ...props }, ref) => {
|
({ className, type, onChange, ...props }, ref) => {
|
||||||
|
// 为 number 类型的输入框提供智能处理,避免空值被转换为 0
|
||||||
|
const handleChange = React.useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (type === 'number' && onChange) {
|
||||||
|
const value = e.target.value;
|
||||||
|
// 当输入为空时,将 value 设置为空字符串,防止外部 Number() 转换为 0
|
||||||
|
// 外部应该判断空字符串并转为 undefined/null
|
||||||
|
onChange(e);
|
||||||
|
} else {
|
||||||
|
onChange?.(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[type, onChange]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
@ -12,6 +27,7 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
|||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
onChange={handleChange}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -51,12 +51,8 @@ const DeleteDialog: React.FC<DeleteDialogProps> = ({
|
|||||||
onSuccess();
|
onSuccess();
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// 错误提示统一由全局 request 拦截器处理,这里仅记录日志
|
||||||
console.error('删除失败:', error);
|
console.error('删除失败:', error);
|
||||||
toast({
|
|
||||||
title: '删除失败',
|
|
||||||
description: error instanceof Error ? error.message : '未知错误',
|
|
||||||
variant: 'destructive'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2, Search, Check, ChevronDown } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@ -31,10 +31,13 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { useToast } from '@/components/ui/use-toast';
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
import type { AlertRuleResponse, ServerResponse, AlertRuleRequest } from '../types';
|
import type { AlertRuleResponse, ServerResponse, AlertRuleRequest } from '../types';
|
||||||
import { AlertType, AlertTypeLabels } from '../types';
|
import { AlertType, AlertTypeLabels } from '../types';
|
||||||
import { alertRuleFormSchema, type AlertRuleFormValues } from '../schema';
|
import { alertRuleFormSchema, type AlertRuleFormValues } from '../schema';
|
||||||
import { createAlertRule, updateAlertRule, getServers } from '../service';
|
import { createAlertRule, updateAlertRule, getServerList } from '../service';
|
||||||
|
|
||||||
interface AlertRuleFormDialogProps {
|
interface AlertRuleFormDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -52,6 +55,8 @@ export const AlertRuleFormDialog: React.FC<AlertRuleFormDialogProps> = ({
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [servers, setServers] = useState<ServerResponse[]>([]);
|
const [servers, setServers] = useState<ServerResponse[]>([]);
|
||||||
|
const [serverSearchValue, setServerSearchValue] = useState('');
|
||||||
|
const [serverPopoverOpen, setServerPopoverOpen] = useState(false);
|
||||||
|
|
||||||
const form = useForm<AlertRuleFormValues>({
|
const form = useForm<AlertRuleFormValues>({
|
||||||
resolver: zodResolver(alertRuleFormSchema),
|
resolver: zodResolver(alertRuleFormSchema),
|
||||||
@ -76,8 +81,8 @@ export const AlertRuleFormDialog: React.FC<AlertRuleFormDialogProps> = ({
|
|||||||
|
|
||||||
const loadServers = async () => {
|
const loadServers = async () => {
|
||||||
try {
|
try {
|
||||||
const result = await getServers({ pageNum: 0, size: 1000 });
|
const result = await getServerList();
|
||||||
setServers(result?.content || []);
|
setServers(result || []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载服务器列表失败:', error);
|
console.error('加载服务器列表失败:', error);
|
||||||
}
|
}
|
||||||
@ -168,26 +173,101 @@ export const AlertRuleFormDialog: React.FC<AlertRuleFormDialogProps> = ({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="col-span-2">
|
<FormItem className="col-span-2">
|
||||||
<FormLabel>规则范围</FormLabel>
|
<FormLabel>规则范围</FormLabel>
|
||||||
<Select
|
<Popover open={serverPopoverOpen} onOpenChange={setServerPopoverOpen}>
|
||||||
value={field.value === null ? 'global' : field.value.toString()}
|
<PopoverTrigger asChild>
|
||||||
onValueChange={(value) => {
|
<FormControl>
|
||||||
field.onChange(value === 'global' ? null : Number(value));
|
<Button
|
||||||
}}
|
variant="outline"
|
||||||
>
|
role="combobox"
|
||||||
<FormControl>
|
className={cn(
|
||||||
<SelectTrigger>
|
'w-full justify-between',
|
||||||
<SelectValue placeholder="选择规则范围" />
|
field.value === null && 'text-muted-foreground'
|
||||||
</SelectTrigger>
|
)}
|
||||||
</FormControl>
|
>
|
||||||
<SelectContent>
|
{field.value === null ? (
|
||||||
<SelectItem value="global">全局规则(适用于所有服务器)</SelectItem>
|
'全局规则(适用于所有服务器)'
|
||||||
{servers.map((server) => (
|
) : (() => {
|
||||||
<SelectItem key={server.id} value={server.id.toString()}>
|
const server = servers.find(s => s.id === field.value);
|
||||||
专属规则 - {server.serverName} ({server.hostIp})
|
return server ? `专属规则 - ${server.serverName} (${server.hostIp})` : '选择规则范围';
|
||||||
</SelectItem>
|
})()}
|
||||||
))}
|
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</SelectContent>
|
</Button>
|
||||||
</Select>
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0">
|
||||||
|
<div className="flex items-center border-b px-3">
|
||||||
|
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
<input
|
||||||
|
placeholder="搜索服务器..."
|
||||||
|
className="flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground"
|
||||||
|
value={serverSearchValue}
|
||||||
|
onChange={(e) => setServerSearchValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ScrollArea className="h-[300px]">
|
||||||
|
<div className="p-1">
|
||||||
|
{/* 全局规则选项 */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground',
|
||||||
|
field.value === null && 'bg-accent text-accent-foreground'
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
field.onChange(null);
|
||||||
|
setServerSearchValue('');
|
||||||
|
setServerPopoverOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex-1 truncate">
|
||||||
|
全局规则(适用于所有服务器)
|
||||||
|
</div>
|
||||||
|
{field.value === null && (
|
||||||
|
<Check className="ml-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 服务器列表 */}
|
||||||
|
{(() => {
|
||||||
|
const filteredServers = servers.filter(server =>
|
||||||
|
server.serverName.toLowerCase().includes(serverSearchValue.toLowerCase()) ||
|
||||||
|
server.hostIp.toLowerCase().includes(serverSearchValue.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
if (filteredServers.length === 0 && serverSearchValue) {
|
||||||
|
return (
|
||||||
|
<div className="p-4 text-center text-sm text-muted-foreground">
|
||||||
|
未找到服务器
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredServers.map((server) => (
|
||||||
|
<div
|
||||||
|
key={server.id}
|
||||||
|
className={cn(
|
||||||
|
'relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground',
|
||||||
|
server.id === field.value && 'bg-accent text-accent-foreground'
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
field.onChange(server.id);
|
||||||
|
setServerSearchValue('');
|
||||||
|
setServerPopoverOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex-1 truncate">
|
||||||
|
<span className="font-medium">专属规则 - {server.serverName}</span>
|
||||||
|
<span className="text-muted-foreground"> ({server.hostIp})</span>
|
||||||
|
</div>
|
||||||
|
{server.id === field.value && (
|
||||||
|
<Check className="ml-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
全局规则适用于所有服务器,专属规则只对指定服务器生效(会覆盖同类型的全局规则)
|
全局规则适用于所有服务器,专属规则只对指定服务器生效(会覆盖同类型的全局规则)
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
@ -248,85 +328,167 @@ export const AlertRuleFormDialog: React.FC<AlertRuleFormDialogProps> = ({
|
|||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="durationMinutes"
|
name="durationMinutes"
|
||||||
render={({ field }) => (
|
render={({ field }) => {
|
||||||
<FormItem>
|
const [localValue, setLocalValue] = React.useState<string>(
|
||||||
<FormLabel>
|
field.value?.toString() ?? ''
|
||||||
持续时长(分钟) <span className="text-destructive">*</span>
|
);
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
// 同步外部值变化到本地状态
|
||||||
<Input
|
React.useEffect(() => {
|
||||||
type="number"
|
setLocalValue(field.value?.toString() ?? '');
|
||||||
min="1"
|
}, [field.value]);
|
||||||
{...field}
|
|
||||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
return (
|
||||||
/>
|
<FormItem>
|
||||||
</FormControl>
|
<FormLabel>
|
||||||
<FormDescription>
|
持续时长(分钟) <span className="text-destructive">*</span>
|
||||||
触发告警前需要持续超过阈值的时间
|
</FormLabel>
|
||||||
</FormDescription>
|
<FormControl>
|
||||||
<FormMessage />
|
<Input
|
||||||
</FormItem>
|
type="number"
|
||||||
)}
|
min="1"
|
||||||
|
value={localValue}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setLocalValue(value);
|
||||||
|
// 只有在有效值时才更新表单
|
||||||
|
if (value !== '') {
|
||||||
|
field.onChange(Number(value));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
// 失焦时,如果为空则设置为undefined触发验证
|
||||||
|
if (localValue === '') {
|
||||||
|
field.onChange(undefined);
|
||||||
|
}
|
||||||
|
field.onBlur();
|
||||||
|
}}
|
||||||
|
name={field.name}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
触发告警前需要持续超过阈值的时间
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 警告阈值 */}
|
{/* 警告阈值 */}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="warningThreshold"
|
name="warningThreshold"
|
||||||
render={({ field }) => (
|
render={({ field }) => {
|
||||||
<FormItem>
|
const currentAlertType = form.watch('alertType');
|
||||||
<FormLabel>
|
const isServerStatus = currentAlertType === AlertType.SERVER_STATUS;
|
||||||
警告阈值 <span className="text-destructive">*</span>
|
const [localValue, setLocalValue] = React.useState<string>(
|
||||||
</FormLabel>
|
field.value?.toString() ?? ''
|
||||||
<FormControl>
|
);
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Input
|
// 同步外部值变化到本地状态
|
||||||
type="number"
|
React.useEffect(() => {
|
||||||
min="0"
|
setLocalValue(field.value?.toString() ?? '');
|
||||||
max="100"
|
}, [field.value]);
|
||||||
step="0.1"
|
|
||||||
{...field}
|
return (
|
||||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
<FormItem>
|
||||||
className="flex-1"
|
<FormLabel>
|
||||||
/>
|
警告阈值 <span className="text-destructive">*</span>
|
||||||
<span className="text-sm text-muted-foreground">
|
</FormLabel>
|
||||||
{AlertTypeLabels[form.watch('alertType')]?.unit || '%'}
|
<FormControl>
|
||||||
</span>
|
<div className="flex items-center gap-2">
|
||||||
</div>
|
<Input
|
||||||
</FormControl>
|
type="number"
|
||||||
<FormMessage />
|
min="0"
|
||||||
</FormItem>
|
max="100"
|
||||||
)}
|
step={isServerStatus ? "1" : "0.1"}
|
||||||
|
value={localValue}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setLocalValue(value);
|
||||||
|
// 只有在有效值时才更新表单
|
||||||
|
if (value !== '') {
|
||||||
|
field.onChange(Number(value));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
// 失焦时,如果为空则设置为undefined触发验证
|
||||||
|
if (localValue === '') {
|
||||||
|
field.onChange(undefined);
|
||||||
|
}
|
||||||
|
field.onBlur();
|
||||||
|
}}
|
||||||
|
name={field.name}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{AlertTypeLabels[currentAlertType]?.unit || '%'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 严重阈值 */}
|
{/* 严重阈值 */}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="criticalThreshold"
|
name="criticalThreshold"
|
||||||
render={({ field }) => (
|
render={({ field }) => {
|
||||||
<FormItem>
|
const currentAlertType = form.watch('alertType');
|
||||||
<FormLabel>
|
const isServerStatus = currentAlertType === AlertType.SERVER_STATUS;
|
||||||
严重阈值 <span className="text-destructive">*</span>
|
const [localValue, setLocalValue] = React.useState<string>(
|
||||||
</FormLabel>
|
field.value?.toString() ?? ''
|
||||||
<FormControl>
|
);
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Input
|
// 同步外部值变化到本地状态
|
||||||
type="number"
|
React.useEffect(() => {
|
||||||
min="0"
|
setLocalValue(field.value?.toString() ?? '');
|
||||||
max="100"
|
}, [field.value]);
|
||||||
step="0.1"
|
|
||||||
{...field}
|
return (
|
||||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
<FormItem>
|
||||||
className="flex-1"
|
<FormLabel>
|
||||||
/>
|
严重阈值 <span className="text-destructive">*</span>
|
||||||
<span className="text-sm text-muted-foreground">
|
</FormLabel>
|
||||||
{AlertTypeLabels[form.watch('alertType')]?.unit || '%'}
|
<FormControl>
|
||||||
</span>
|
<div className="flex items-center gap-2">
|
||||||
</div>
|
<Input
|
||||||
</FormControl>
|
type="number"
|
||||||
<FormMessage />
|
min="0"
|
||||||
</FormItem>
|
max="100"
|
||||||
)}
|
step={isServerStatus ? "1" : "0.1"}
|
||||||
|
value={localValue}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setLocalValue(value);
|
||||||
|
// 只有在有效值时才更新表单
|
||||||
|
if (value !== '') {
|
||||||
|
field.onChange(Number(value));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
// 失焦时,如果为空则设置为undefined触发验证
|
||||||
|
if (localValue === '') {
|
||||||
|
field.onChange(undefined);
|
||||||
|
}
|
||||||
|
field.onBlur();
|
||||||
|
}}
|
||||||
|
name={field.name}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{AlertTypeLabels[currentAlertType]?.unit || '%'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 是否启用 */}
|
{/* 是否启用 */}
|
||||||
|
|||||||
@ -59,6 +59,7 @@ const ServerList: React.FC = () => {
|
|||||||
const [categories, setCategories] = useState<ServerCategoryResponse[]>([]);
|
const [categories, setCategories] = useState<ServerCategoryResponse[]>([]);
|
||||||
const [servers, setServers] = useState<ServerResponse[]>([]);
|
const [servers, setServers] = useState<ServerResponse[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [pageLoading, setPageLoading] = useState(false); // 分页加载状态
|
||||||
const [pageIndex, setPageIndex] = useState(0);
|
const [pageIndex, setPageIndex] = useState(0);
|
||||||
const [pageSize] = useState(10);
|
const [pageSize] = useState(10);
|
||||||
const [totalElements, setTotalElements] = useState(0);
|
const [totalElements, setTotalElements] = useState(0);
|
||||||
@ -142,10 +143,15 @@ const ServerList: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 加载服务器列表
|
// 加载服务器列表
|
||||||
const loadServers = async (silent: boolean = false) => {
|
const loadServers = async (silent: boolean = false, isPagination: boolean = false) => {
|
||||||
// silent 为 true 时不显示全局 loading 状态(用于操作后的静默刷新)
|
// silent 为 true 时不显示全局 loading 状态(用于操作后的静默刷新)
|
||||||
|
// isPagination 为 true 时只显示分页 loading,不显示全局 loading
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
setLoading(true);
|
if (isPagination) {
|
||||||
|
setPageLoading(true);
|
||||||
|
} else {
|
||||||
|
setLoading(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
@ -184,6 +190,7 @@ const ServerList: React.FC = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
setPageLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -335,10 +342,21 @@ const ServerList: React.FC = () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听筛选条件、分页和视图模式变化
|
// 监听筛选条件和视图模式变化(使用全局loading)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadServers();
|
loadServers(false, false); // 筛选加载,使用全局loading
|
||||||
}, [pageIndex, pageSize, selectedCategoryId, selectedStatus, selectedOsType, viewMode]);
|
// 筛选条件变化时重置分页到第一页
|
||||||
|
setPageIndex(0);
|
||||||
|
}, [pageSize, selectedCategoryId, selectedStatus, selectedOsType, viewMode]);
|
||||||
|
|
||||||
|
// 监听分页变化(单独处理,使用pageLoading)
|
||||||
|
// 注意:这个useEffect只处理用户主动翻页的情况
|
||||||
|
useEffect(() => {
|
||||||
|
if (viewMode === 'table' && pageIndex > 0) {
|
||||||
|
// pageIndex > 0 说明是用户翻页操作,使用pageLoading
|
||||||
|
loadServers(false, true);
|
||||||
|
}
|
||||||
|
}, [pageIndex]);
|
||||||
|
|
||||||
// 使用statsData作为统计数据
|
// 使用statsData作为统计数据
|
||||||
const stats = statsData;
|
const stats = statsData;
|
||||||
@ -607,18 +625,28 @@ const ServerList: React.FC = () => {
|
|||||||
|
|
||||||
{/* 表格视图 */}
|
{/* 表格视图 */}
|
||||||
{!loading && filteredServers.length > 0 && viewMode === 'table' && (
|
{!loading && filteredServers.length > 0 && viewMode === 'table' && (
|
||||||
<ServerTable
|
<div className="relative">
|
||||||
servers={filteredServers}
|
{pageLoading && (
|
||||||
loading={loading}
|
<div className="absolute inset-0 bg-background/80 backdrop-blur-sm z-10 flex items-center justify-center">
|
||||||
onTest={handleTestConnection}
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
onEdit={handleEdit}
|
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||||
onDelete={handleDelete}
|
<span>加载中...</span>
|
||||||
onSSHConnect={handleSSHConnect}
|
</div>
|
||||||
onCollectHardware={handleCollectHardware}
|
</div>
|
||||||
isTesting={(serverId) => testingServerId === serverId}
|
)}
|
||||||
isCollecting={(serverId) => collectingServerId === serverId}
|
<ServerTable
|
||||||
getOsIcon={getOsIcon}
|
servers={filteredServers}
|
||||||
/>
|
loading={loading}
|
||||||
|
onTest={handleTestConnection}
|
||||||
|
onEdit={handleEdit}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
onSSHConnect={handleSSHConnect}
|
||||||
|
onCollectHardware={handleCollectHardware}
|
||||||
|
isTesting={(serverId) => testingServerId === serverId}
|
||||||
|
isCollecting={(serverId) => collectingServerId === serverId}
|
||||||
|
getOsIcon={getOsIcon}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 分页(仅列表模式) */}
|
{/* 分页(仅列表模式) */}
|
||||||
|
|||||||
@ -101,6 +101,24 @@ export const alertRuleFormSchema = z.object({
|
|||||||
path: ['criticalThreshold'],
|
path: ['criticalThreshold'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 服务器状态告警的阈值必须是整数
|
||||||
|
if (data.alertType === AlertType.SERVER_STATUS) {
|
||||||
|
if (!Number.isInteger(data.warningThreshold)) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: 'custom',
|
||||||
|
message: '服务器状态告警的警告阈值必须是整数',
|
||||||
|
path: ['warningThreshold'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!Number.isInteger(data.criticalThreshold)) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: 'custom',
|
||||||
|
message: '服务器状态告警的严重阈值必须是整数',
|
||||||
|
path: ['criticalThreshold'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export type AlertRuleFormValues = z.infer<typeof alertRuleFormSchema>;
|
export type AlertRuleFormValues = z.infer<typeof alertRuleFormSchema>;
|
||||||
|
|||||||
@ -292,6 +292,8 @@ export enum AlertType {
|
|||||||
DISK = 'DISK',
|
DISK = 'DISK',
|
||||||
/** 网络流量 */
|
/** 网络流量 */
|
||||||
NETWORK = 'NETWORK',
|
NETWORK = 'NETWORK',
|
||||||
|
/** 服务器状态 */
|
||||||
|
SERVER_STATUS = 'SERVER_STATUS',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -366,5 +368,6 @@ export const AlertTypeLabels: Record<AlertType, { label: string; unit: string; d
|
|||||||
[AlertType.MEMORY]: { label: '内存使用率', unit: '%', description: '内存使用率告警' },
|
[AlertType.MEMORY]: { label: '内存使用率', unit: '%', description: '内存使用率告警' },
|
||||||
[AlertType.DISK]: { label: '磁盘使用率', unit: '%', description: '磁盘使用率告警' },
|
[AlertType.DISK]: { label: '磁盘使用率', unit: '%', description: '磁盘使用率告警' },
|
||||||
[AlertType.NETWORK]: { label: '网络流量', unit: 'MB/s', description: '网络流量告警' },
|
[AlertType.NETWORK]: { label: '网络流量', unit: 'MB/s', description: '网络流量告警' },
|
||||||
|
[AlertType.SERVER_STATUS]: { label: '服务器状态', unit: '次', description: '服务器连接状态告警' },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -124,7 +124,6 @@ export const HttpRequestNodeDefinition: ConfigurableNodeDefinition = {
|
|||||||
title: "超时时间(毫秒)",
|
title: "超时时间(毫秒)",
|
||||||
description: "请求超时时间,0表示不限制",
|
description: "请求超时时间,0表示不限制",
|
||||||
minimum: 1000,
|
minimum: 1000,
|
||||||
maximum: 300000,
|
|
||||||
default: 30000
|
default: 30000
|
||||||
},
|
},
|
||||||
responseBodyType: {
|
responseBodyType: {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user