diff --git a/frontend/src/components/Terminal/Terminal.tsx b/frontend/src/components/Terminal/Terminal.tsx index 6d28da0a..1470115c 100644 --- a/frontend/src/components/Terminal/Terminal.tsx +++ b/frontend/src/components/Terminal/Terminal.tsx @@ -40,7 +40,7 @@ export const Terminal: React.FC = ({ const [showSearch, setShowSearch] = useState(false); const [currentTheme, setCurrentTheme] = useState('dark'); const [auditShown, setAuditShown] = useState(false); - const [connectionStatus, setConnectionStatus] = useState('disconnected'); // 初始状态,会被实例状态覆盖 + const [connectionStatus, setConnectionStatus] = useState('connecting'); // 初始状态为连接中,符合正常生命周期 const [errorMessage, setErrorMessage] = useState(''); const [connectedTime, setConnectedTime] = useState(null); @@ -103,10 +103,11 @@ export const Terminal: React.FC = ({ } }); - // 延迟连接(仅在未连接时) + // 延迟连接(仅在未连接或需要连接时) const timer = setTimeout(() => { 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(); } }, 300); diff --git a/frontend/src/components/Terminal/strategies/BaseConnectionStrategy.ts b/frontend/src/components/Terminal/strategies/BaseConnectionStrategy.ts index 97a0bc83..a396bfae 100644 --- a/frontend/src/components/Terminal/strategies/BaseConnectionStrategy.ts +++ b/frontend/src/components/Terminal/strategies/BaseConnectionStrategy.ts @@ -22,7 +22,7 @@ export type ErrorCallback = (error: string) => void; * 抽象连接策略基类 */ export abstract class BaseConnectionStrategy { - protected status: ConnectionStatus = 'disconnected'; + protected status: ConnectionStatus = 'connecting'; // 初始状态为connecting,符合打开终端时的正常生命周期 protected statusListeners: Set = new Set(); protected messageListeners: Set = new Set(); protected errorListeners: Set = new Set(); diff --git a/frontend/src/components/ui/input.tsx b/frontend/src/components/ui/input.tsx index 35a99e51..3d0abcd8 100644 --- a/frontend/src/components/ui/input.tsx +++ b/frontend/src/components/ui/input.tsx @@ -3,7 +3,22 @@ import * as React from "react" import { cn } from "@/lib/utils" const Input = React.forwardRef>( - ({ className, type, ...props }, ref) => { + ({ className, type, onChange, ...props }, ref) => { + // 为 number 类型的输入框提供智能处理,避免空值被转换为 0 + const handleChange = React.useCallback( + (e: React.ChangeEvent) => { + if (type === 'number' && onChange) { + const value = e.target.value; + // 当输入为空时,将 value 设置为空字符串,防止外部 Number() 转换为 0 + // 外部应该判断空字符串并转为 undefined/null + onChange(e); + } else { + onChange?.(e); + } + }, + [type, onChange] + ); + return ( >( className )} ref={ref} + onChange={handleChange} {...props} /> ) diff --git a/frontend/src/pages/Deploy/Application/List/components/DeleteDialog.tsx b/frontend/src/pages/Deploy/Application/List/components/DeleteDialog.tsx index f72e3f88..355884ce 100644 --- a/frontend/src/pages/Deploy/Application/List/components/DeleteDialog.tsx +++ b/frontend/src/pages/Deploy/Application/List/components/DeleteDialog.tsx @@ -51,12 +51,8 @@ const DeleteDialog: React.FC = ({ onSuccess(); onOpenChange(false); } catch (error) { + // 错误提示统一由全局 request 拦截器处理,这里仅记录日志 console.error('删除失败:', error); - toast({ - title: '删除失败', - description: error instanceof Error ? error.message : '未知错误', - variant: 'destructive' - }); } }; diff --git a/frontend/src/pages/Resource/Server/List/components/AlertRuleFormDialog.tsx b/frontend/src/pages/Resource/Server/List/components/AlertRuleFormDialog.tsx index 56430486..7929e02d 100644 --- a/frontend/src/pages/Resource/Server/List/components/AlertRuleFormDialog.tsx +++ b/frontend/src/pages/Resource/Server/List/components/AlertRuleFormDialog.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; -import { Loader2 } from 'lucide-react'; +import { Loader2, Search, Check, ChevronDown } from 'lucide-react'; import { Dialog, DialogContent, @@ -31,10 +31,13 @@ import { SelectValue, } from '@/components/ui/select'; 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 { AlertType, AlertTypeLabels } from '../types'; import { alertRuleFormSchema, type AlertRuleFormValues } from '../schema'; -import { createAlertRule, updateAlertRule, getServers } from '../service'; +import { createAlertRule, updateAlertRule, getServerList } from '../service'; interface AlertRuleFormDialogProps { open: boolean; @@ -52,6 +55,8 @@ export const AlertRuleFormDialog: React.FC = ({ const { toast } = useToast(); const [loading, setLoading] = useState(false); const [servers, setServers] = useState([]); + const [serverSearchValue, setServerSearchValue] = useState(''); + const [serverPopoverOpen, setServerPopoverOpen] = useState(false); const form = useForm({ resolver: zodResolver(alertRuleFormSchema), @@ -76,8 +81,8 @@ export const AlertRuleFormDialog: React.FC = ({ const loadServers = async () => { try { - const result = await getServers({ pageNum: 0, size: 1000 }); - setServers(result?.content || []); + const result = await getServerList(); + setServers(result || []); } catch (error) { console.error('加载服务器列表失败:', error); } @@ -168,26 +173,101 @@ export const AlertRuleFormDialog: React.FC = ({ render={({ field }) => ( 规则范围 - + + + + + + + +
+ + setServerSearchValue(e.target.value)} + /> +
+ +
+ {/* 全局规则选项 */} +
{ + field.onChange(null); + setServerSearchValue(''); + setServerPopoverOpen(false); + }} + > +
+ 全局规则(适用于所有服务器) +
+ {field.value === null && ( + + )} +
+ + {/* 服务器列表 */} + {(() => { + const filteredServers = servers.filter(server => + server.serverName.toLowerCase().includes(serverSearchValue.toLowerCase()) || + server.hostIp.toLowerCase().includes(serverSearchValue.toLowerCase()) + ); + + if (filteredServers.length === 0 && serverSearchValue) { + return ( +
+ 未找到服务器 +
+ ); + } + + return filteredServers.map((server) => ( +
{ + field.onChange(server.id); + setServerSearchValue(''); + setServerPopoverOpen(false); + }} + > +
+ 专属规则 - {server.serverName} + ({server.hostIp}) +
+ {server.id === field.value && ( + + )} +
+ )); + })()} +
+
+
+
全局规则适用于所有服务器,专属规则只对指定服务器生效(会覆盖同类型的全局规则) @@ -248,85 +328,167 @@ export const AlertRuleFormDialog: React.FC = ({ ( - - - 持续时长(分钟) * - - - field.onChange(Number(e.target.value))} - /> - - - 触发告警前需要持续超过阈值的时间 - - - - )} + render={({ field }) => { + const [localValue, setLocalValue] = React.useState( + field.value?.toString() ?? '' + ); + + // 同步外部值变化到本地状态 + React.useEffect(() => { + setLocalValue(field.value?.toString() ?? ''); + }, [field.value]); + + return ( + + + 持续时长(分钟) * + + + { + 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} + /> + + + 触发告警前需要持续超过阈值的时间 + + + + ); + }} /> {/* 警告阈值 */} ( - - - 警告阈值 * - - -
- field.onChange(Number(e.target.value))} - className="flex-1" - /> - - {AlertTypeLabels[form.watch('alertType')]?.unit || '%'} - -
-
- -
- )} + render={({ field }) => { + const currentAlertType = form.watch('alertType'); + const isServerStatus = currentAlertType === AlertType.SERVER_STATUS; + const [localValue, setLocalValue] = React.useState( + field.value?.toString() ?? '' + ); + + // 同步外部值变化到本地状态 + React.useEffect(() => { + setLocalValue(field.value?.toString() ?? ''); + }, [field.value]); + + return ( + + + 警告阈值 * + + +
+ { + 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" + /> + + {AlertTypeLabels[currentAlertType]?.unit || '%'} + +
+
+ +
+ ); + }} /> {/* 严重阈值 */} ( - - - 严重阈值 * - - -
- field.onChange(Number(e.target.value))} - className="flex-1" - /> - - {AlertTypeLabels[form.watch('alertType')]?.unit || '%'} - -
-
- -
- )} + render={({ field }) => { + const currentAlertType = form.watch('alertType'); + const isServerStatus = currentAlertType === AlertType.SERVER_STATUS; + const [localValue, setLocalValue] = React.useState( + field.value?.toString() ?? '' + ); + + // 同步外部值变化到本地状态 + React.useEffect(() => { + setLocalValue(field.value?.toString() ?? ''); + }, [field.value]); + + return ( + + + 严重阈值 * + + +
+ { + 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" + /> + + {AlertTypeLabels[currentAlertType]?.unit || '%'} + +
+
+ +
+ ); + }} /> {/* 是否启用 */} diff --git a/frontend/src/pages/Resource/Server/List/index.tsx b/frontend/src/pages/Resource/Server/List/index.tsx index 7c27a58d..1568f1be 100644 --- a/frontend/src/pages/Resource/Server/List/index.tsx +++ b/frontend/src/pages/Resource/Server/List/index.tsx @@ -59,6 +59,7 @@ const ServerList: React.FC = () => { const [categories, setCategories] = useState([]); const [servers, setServers] = useState([]); const [loading, setLoading] = useState(false); + const [pageLoading, setPageLoading] = useState(false); // 分页加载状态 const [pageIndex, setPageIndex] = useState(0); const [pageSize] = useState(10); 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 状态(用于操作后的静默刷新) + // isPagination 为 true 时只显示分页 loading,不显示全局 loading if (!silent) { - setLoading(true); + if (isPagination) { + setPageLoading(true); + } else { + setLoading(true); + } } try { const params = { @@ -184,6 +190,7 @@ const ServerList: React.FC = () => { } finally { if (!silent) { setLoading(false); + setPageLoading(false); } } }; @@ -335,10 +342,21 @@ const ServerList: React.FC = () => { ); }); - // 监听筛选条件、分页和视图模式变化 + // 监听筛选条件和视图模式变化(使用全局loading) useEffect(() => { - loadServers(); - }, [pageIndex, pageSize, selectedCategoryId, selectedStatus, selectedOsType, viewMode]); + loadServers(false, false); // 筛选加载,使用全局loading + // 筛选条件变化时重置分页到第一页 + setPageIndex(0); + }, [pageSize, selectedCategoryId, selectedStatus, selectedOsType, viewMode]); + + // 监听分页变化(单独处理,使用pageLoading) + // 注意:这个useEffect只处理用户主动翻页的情况 + useEffect(() => { + if (viewMode === 'table' && pageIndex > 0) { + // pageIndex > 0 说明是用户翻页操作,使用pageLoading + loadServers(false, true); + } + }, [pageIndex]); // 使用statsData作为统计数据 const stats = statsData; @@ -607,18 +625,28 @@ const ServerList: React.FC = () => { {/* 表格视图 */} {!loading && filteredServers.length > 0 && viewMode === 'table' && ( - testingServerId === serverId} - isCollecting={(serverId) => collectingServerId === serverId} - getOsIcon={getOsIcon} - /> +
+ {pageLoading && ( +
+
+
+ 加载中... +
+
+ )} + testingServerId === serverId} + isCollecting={(serverId) => collectingServerId === serverId} + getOsIcon={getOsIcon} + /> +
)} {/* 分页(仅列表模式) */} diff --git a/frontend/src/pages/Resource/Server/List/schema.ts b/frontend/src/pages/Resource/Server/List/schema.ts index 125a6437..4bab13f5 100644 --- a/frontend/src/pages/Resource/Server/List/schema.ts +++ b/frontend/src/pages/Resource/Server/List/schema.ts @@ -101,6 +101,24 @@ export const alertRuleFormSchema = z.object({ 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; diff --git a/frontend/src/pages/Resource/Server/List/types.ts b/frontend/src/pages/Resource/Server/List/types.ts index 2e908b67..c4594636 100644 --- a/frontend/src/pages/Resource/Server/List/types.ts +++ b/frontend/src/pages/Resource/Server/List/types.ts @@ -292,6 +292,8 @@ export enum AlertType { DISK = 'DISK', /** 网络流量 */ NETWORK = 'NETWORK', + /** 服务器状态 */ + SERVER_STATUS = 'SERVER_STATUS', } /** @@ -366,5 +368,6 @@ export const AlertTypeLabels: Record