服务器拆分接口
This commit is contained in:
parent
44de0ca028
commit
81a11c4594
@ -15,6 +15,7 @@ import {
|
||||
Tags,
|
||||
Tag,
|
||||
FileText,
|
||||
RefreshCw,
|
||||
} from 'lucide-react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@ -36,11 +37,13 @@ interface ServerCardProps {
|
||||
onEdit: (server: ServerResponse) => void;
|
||||
onDelete: (server: ServerResponse) => void;
|
||||
onSSHConnect: (server: ServerResponse) => void;
|
||||
onCollectHardware: (server: ServerResponse) => void;
|
||||
isTesting?: boolean;
|
||||
isCollecting?: boolean;
|
||||
getOsIcon: (osType?: string) => React.ReactNode;
|
||||
}
|
||||
|
||||
export const ServerCard: React.FC<ServerCardProps> = ({ server, onTest, onEdit, onDelete, onSSHConnect, isTesting, getOsIcon }) => {
|
||||
export const ServerCard: React.FC<ServerCardProps> = ({ server, onTest, onEdit, onDelete, onSSHConnect, onCollectHardware, isTesting, isCollecting, getOsIcon }) => {
|
||||
const [expanded, setExpanded] = React.useState(false);
|
||||
|
||||
const formatTime = (time?: string) => {
|
||||
@ -252,7 +255,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ server, onTest, onEdit,
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={() => onTest(server)}
|
||||
disabled={isTesting}
|
||||
disabled={isTesting || isCollecting}
|
||||
>
|
||||
{isTesting ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
@ -263,6 +266,25 @@ export const ServerCard: React.FC<ServerCardProps> = ({ server, onTest, onEdit,
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>测试连接</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={() => onCollectHardware(server)}
|
||||
disabled={isTesting || isCollecting}
|
||||
>
|
||||
{isCollecting ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>刷新硬件信息</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
|
||||
@ -37,9 +37,9 @@ import {
|
||||
} from '@/components/ui/collapsible';
|
||||
import type { ServerResponse, ServerCategoryResponse } from '../types';
|
||||
import { OsType, OsTypeLabels, AuthType, AuthTypeLabels, ServerStatus } from '../types';
|
||||
import { createServer, updateServer, getServerCategories } from '../service';
|
||||
import { createServer, updateServer, getServerCategories, testServerConnection, collectServerHardware } from '../service';
|
||||
import { serverFormSchema, type ServerFormValues } from '../schema';
|
||||
import { Eye, EyeOff, X, Plus, ChevronDown } from 'lucide-react';
|
||||
import { Eye, EyeOff, X, Plus, ChevronDown, Loader2, CheckCircle } from 'lucide-react';
|
||||
|
||||
interface ServerEditDialogProps {
|
||||
open: boolean;
|
||||
@ -61,6 +61,9 @@ export const ServerEditDialog: React.FC<ServerEditDialogProps> = ({
|
||||
const [showPassphrase, setShowPassphrase] = useState(false);
|
||||
const [tagInput, setTagInput] = useState('');
|
||||
const [systemInfoOpen, setSystemInfoOpen] = useState(true);
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [collecting, setCollecting] = useState(false);
|
||||
const [testSuccess, setTestSuccess] = useState(false);
|
||||
const isEdit = !!server?.id;
|
||||
|
||||
const form = useForm<ServerFormValues>({
|
||||
@ -150,6 +153,129 @@ export const ServerEditDialog: React.FC<ServerEditDialogProps> = ({
|
||||
}
|
||||
}, [server, open, form]);
|
||||
|
||||
// 测试连接并采集硬件信息
|
||||
const handleTestAndCollect = async () => {
|
||||
// 验证必填的连接信息
|
||||
const hostIp = form.getValues('hostIp');
|
||||
const sshPort = form.getValues('sshPort');
|
||||
const sshUser = form.getValues('sshUser');
|
||||
const authType = form.getValues('authType');
|
||||
const sshPassword = form.getValues('sshPassword');
|
||||
const sshPrivateKey = form.getValues('sshPrivateKey');
|
||||
|
||||
if (!hostIp || !sshPort || !sshUser) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '缺少必填信息',
|
||||
description: '请先填写IP地址、SSH端口和用户名',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (authType === AuthType.PASSWORD && !sshPassword) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '缺少密码',
|
||||
description: '请填写SSH密码',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (authType === AuthType.KEY && !sshPrivateKey) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '缺少私钥',
|
||||
description: '请填写SSH私钥',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是新增服务器,需要先创建临时服务器记录
|
||||
let serverId = server?.id;
|
||||
|
||||
if (!serverId) {
|
||||
// 新增服务器:先保存基础信息
|
||||
try {
|
||||
setLoading(true);
|
||||
const tempServer = await createServer({
|
||||
serverName: form.getValues('serverName') || '临时服务器',
|
||||
hostIp,
|
||||
sshPort,
|
||||
sshUser,
|
||||
authType,
|
||||
sshPassword,
|
||||
sshPrivateKey,
|
||||
sshPassphrase: form.getValues('sshPassphrase'),
|
||||
categoryId: form.getValues('categoryId'),
|
||||
osType: form.getValues('osType'),
|
||||
description: form.getValues('description'),
|
||||
});
|
||||
serverId = tempServer.id;
|
||||
toast({
|
||||
title: '服务器已保存',
|
||||
description: '正在测试连接...',
|
||||
});
|
||||
} catch (error: any) {
|
||||
return;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 步骤1:测试连接
|
||||
setTesting(true);
|
||||
setTestSuccess(false);
|
||||
try {
|
||||
const testResult = await testServerConnection(serverId);
|
||||
if (!testResult.connected) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '连接失败',
|
||||
description: testResult.errorMessage || '无法连接到服务器',
|
||||
});
|
||||
setTesting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
toast({
|
||||
title: '连接成功',
|
||||
description: `响应时间: ${testResult.responseTime}ms`,
|
||||
});
|
||||
setTestSuccess(true);
|
||||
|
||||
// 步骤2:自动采集硬件信息
|
||||
setTesting(false);
|
||||
setCollecting(true);
|
||||
|
||||
const hardwareResult = await collectServerHardware(serverId);
|
||||
|
||||
// 自动填充表单
|
||||
form.setValue('hostname', hardwareResult.hostname);
|
||||
form.setValue('osVersion', hardwareResult.osVersion);
|
||||
form.setValue('cpuCores', hardwareResult.cpuCores);
|
||||
form.setValue('memorySize', hardwareResult.memorySize);
|
||||
form.setValue('diskSize', hardwareResult.diskSize);
|
||||
|
||||
toast({
|
||||
title: '硬件信息采集成功',
|
||||
description: `${hardwareResult.hostname} - CPU: ${hardwareResult.cpuCores}核 内存: ${hardwareResult.memorySize}GB`,
|
||||
});
|
||||
|
||||
// 展开系统信息区域
|
||||
setSystemInfoOpen(true);
|
||||
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '操作失败',
|
||||
description: error.response?.data?.message || '无法完成操作',
|
||||
});
|
||||
} finally {
|
||||
setTesting(false);
|
||||
setCollecting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (values: ServerFormValues) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
@ -558,6 +684,41 @@ export const ServerEditDialog: React.FC<ServerEditDialogProps> = ({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 测试连接按钮 */}
|
||||
<div className="mt-4 flex items-center gap-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleTestAndCollect}
|
||||
disabled={testing || collecting || loading}
|
||||
className="flex-1"
|
||||
>
|
||||
{testing ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
测试连接中...
|
||||
</>
|
||||
) : collecting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
采集硬件信息中...
|
||||
</>
|
||||
) : testSuccess ? (
|
||||
<>
|
||||
<CheckCircle className="mr-2 h-4 w-4 text-green-500" />
|
||||
测试连接并采集信息
|
||||
</>
|
||||
) : (
|
||||
'测试连接并采集信息'
|
||||
)}
|
||||
</Button>
|
||||
{testSuccess && !isEdit && (
|
||||
<span className="text-xs text-green-600 dark:text-green-400">
|
||||
✓ 连接成功,硬件信息已采集
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
AlertCircle,
|
||||
HelpCircle,
|
||||
Terminal,
|
||||
RefreshCw,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
Table,
|
||||
@ -36,7 +37,9 @@ interface ServerTableProps {
|
||||
onEdit: (server: ServerResponse) => void;
|
||||
onDelete: (server: ServerResponse) => void;
|
||||
onSSHConnect: (server: ServerResponse) => void;
|
||||
onCollectHardware: (server: ServerResponse) => void;
|
||||
isTesting?: (serverId: number) => boolean;
|
||||
isCollecting?: (serverId: number) => boolean;
|
||||
getOsIcon: (osType?: string) => React.ReactNode;
|
||||
}
|
||||
|
||||
@ -46,7 +49,9 @@ export const ServerTable: React.FC<ServerTableProps> = ({
|
||||
onEdit,
|
||||
onDelete,
|
||||
onSSHConnect,
|
||||
onCollectHardware,
|
||||
isTesting,
|
||||
isCollecting,
|
||||
getOsIcon,
|
||||
}) => {
|
||||
const formatTime = (time?: string) => {
|
||||
@ -155,7 +160,7 @@ export const ServerTable: React.FC<ServerTableProps> = ({
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onTest(server)}
|
||||
disabled={isTesting?.(server.id)}
|
||||
disabled={isTesting?.(server.id) || isCollecting?.(server.id)}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
{isTesting?.(server.id) ? (
|
||||
@ -167,6 +172,24 @@ export const ServerTable: React.FC<ServerTableProps> = ({
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>测试连接</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onCollectHardware(server)}
|
||||
disabled={isTesting?.(server.id) || isCollecting?.(server.id)}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
{isCollecting?.(server.id) ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>刷新硬件信息</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
|
||||
@ -36,7 +36,7 @@ import { DataTablePagination } from '@/components/ui/pagination';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import type { ServerResponse, ServerCategoryResponse, ServerStatus, OsType } from './types';
|
||||
import { ServerStatusLabels, OsTypeLabels } from './types';
|
||||
import { getServers, getServerCategories, deleteServer, testServerConnection } from './service';
|
||||
import { getServers, getServerCategories, deleteServer, testServerConnection, collectServerHardware } from './service';
|
||||
import { CategoryManageDialog } from './components/CategoryManageDialog';
|
||||
import { AlertRuleManageDialog } from './components/AlertRuleManageDialog';
|
||||
import { ServerEditDialog } from './components/ServerEditDialog';
|
||||
@ -79,6 +79,7 @@ const ServerList: React.FC = () => {
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [serverToDelete, setServerToDelete] = useState<ServerResponse | null>(null);
|
||||
const [testingServerId, setTestingServerId] = useState<number | null>(null);
|
||||
const [collectingServerId, setCollectingServerId] = useState<number | null>(null);
|
||||
const [statsData, setStatsData] = useState<{
|
||||
total: number;
|
||||
online: number;
|
||||
@ -167,7 +168,7 @@ const ServerList: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 测试连接
|
||||
// 测试连接(轻量级)
|
||||
const handleTestConnection = async (server: ServerResponse) => {
|
||||
setTestingServerId(server.id);
|
||||
try {
|
||||
@ -175,10 +176,8 @@ const ServerList: React.FC = () => {
|
||||
if (result.connected) {
|
||||
toast({
|
||||
title: '连接成功',
|
||||
description: `${result.hostname} - ${result.osVersion} (响应时间: ${result.responseTime}ms)`,
|
||||
description: `响应时间: ${result.responseTime}ms`,
|
||||
});
|
||||
// 测试连接成功后刷新数据
|
||||
await loadServers();
|
||||
} else {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
@ -197,6 +196,29 @@ const ServerList: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新硬件信息
|
||||
const handleCollectHardware = async (server: ServerResponse) => {
|
||||
setCollectingServerId(server.id);
|
||||
try {
|
||||
const result = await collectServerHardware(server.id);
|
||||
toast({
|
||||
title: '硬件信息已更新',
|
||||
description: `${result.hostname} - CPU: ${result.cpuCores}核 内存: ${result.memorySize}GB`,
|
||||
});
|
||||
// 刷新服务器列表
|
||||
await loadServers();
|
||||
await loadStats();
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: '采集失败',
|
||||
description: error.response?.data?.message || '无法采集硬件信息',
|
||||
});
|
||||
} finally {
|
||||
setCollectingServerId(null);
|
||||
}
|
||||
};
|
||||
|
||||
// 编辑服务器
|
||||
const handleEdit = (server: ServerResponse) => {
|
||||
setEditingServer(server);
|
||||
@ -532,7 +554,9 @@ const ServerList: React.FC = () => {
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
onSSHConnect={handleSSHConnect}
|
||||
onCollectHardware={handleCollectHardware}
|
||||
isTesting={testingServerId === server.id}
|
||||
isCollecting={collectingServerId === server.id}
|
||||
getOsIcon={getOsIcon}
|
||||
/>
|
||||
))}
|
||||
@ -548,7 +572,9 @@ const ServerList: React.FC = () => {
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
onSSHConnect={handleSSHConnect}
|
||||
onCollectHardware={handleCollectHardware}
|
||||
isTesting={(serverId) => testingServerId === serverId}
|
||||
isCollecting={(serverId) => collectingServerId === serverId}
|
||||
getOsIcon={getOsIcon}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -6,6 +6,7 @@ import type {
|
||||
ServerResponse,
|
||||
ServerRequest,
|
||||
ServerConnectionTestResult,
|
||||
ServerHardwareCollectionResult,
|
||||
AlertRuleResponse,
|
||||
AlertRuleQuery,
|
||||
AlertRuleRequest,
|
||||
@ -115,12 +116,19 @@ export const batchDeleteServers = (ids: number[]) =>
|
||||
request.post<void>(`${SERVER_URL}/batch-delete`, { ids });
|
||||
|
||||
/**
|
||||
* 测试服务器连接
|
||||
* 返回连接测试结果,包含连接状态和服务器信息
|
||||
* 测试服务器连接(轻量级)
|
||||
* 仅验证连接是否成功,不采集硬件信息
|
||||
*/
|
||||
export const testServerConnection = (id: number) =>
|
||||
request.post<ServerConnectionTestResult>(`${SERVER_URL}/${id}/test-connection`);
|
||||
|
||||
/**
|
||||
* 采集服务器硬件信息
|
||||
* 采集并更新服务器硬件信息(hostname、CPU、内存、磁盘等)
|
||||
*/
|
||||
export const collectServerHardware = (id: number) =>
|
||||
request.post<ServerHardwareCollectionResult>(`${SERVER_URL}/${id}/collect-hardware`);
|
||||
|
||||
// ==================== 告警规则 ====================
|
||||
|
||||
/**
|
||||
|
||||
@ -239,7 +239,7 @@ export const AuthTypeLabels: Record<AuthType, { label: string; description: stri
|
||||
};
|
||||
|
||||
/**
|
||||
* 服务器连接测试结果
|
||||
* 服务器连接测试结果(轻量级,仅测试连通性)
|
||||
*/
|
||||
export interface ServerConnectionTestResult {
|
||||
/** 是否连接成功 */
|
||||
@ -250,6 +250,20 @@ export interface ServerConnectionTestResult {
|
||||
connectTime: string;
|
||||
/** 响应时间(ms) */
|
||||
responseTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务器硬件信息采集结果
|
||||
*/
|
||||
export interface ServerHardwareCollectionResult {
|
||||
/** 是否连接成功 */
|
||||
connected: boolean;
|
||||
/** 错误信息(连接失败时返回) */
|
||||
errorMessage: string | null;
|
||||
/** 连接时间 */
|
||||
connectTime: string;
|
||||
/** 响应时间(ms) */
|
||||
responseTime: number;
|
||||
/** 主机名 */
|
||||
hostname: string;
|
||||
/** 操作系统版本 */
|
||||
@ -260,6 +274,8 @@ export interface ServerConnectionTestResult {
|
||||
memorySize: number;
|
||||
/** 磁盘大小(GB) */
|
||||
diskSize: number;
|
||||
/** 磁盘详细信息 */
|
||||
diskInfo?: DiskInfo[];
|
||||
}
|
||||
|
||||
// ==================== 告警规则 ====================
|
||||
|
||||
Loading…
Reference in New Issue
Block a user