服务器拆分接口

This commit is contained in:
dengqichen 2025-12-09 11:10:21 +08:00
parent 44de0ca028
commit 81a11c4594
6 changed files with 269 additions and 13 deletions

View File

@ -15,6 +15,7 @@ import {
Tags, Tags,
Tag, Tag,
FileText, FileText,
RefreshCw,
} from 'lucide-react'; } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
@ -36,11 +37,13 @@ interface ServerCardProps {
onEdit: (server: ServerResponse) => void; onEdit: (server: ServerResponse) => void;
onDelete: (server: ServerResponse) => void; onDelete: (server: ServerResponse) => void;
onSSHConnect: (server: ServerResponse) => void; onSSHConnect: (server: ServerResponse) => void;
onCollectHardware: (server: ServerResponse) => void;
isTesting?: boolean; isTesting?: boolean;
isCollecting?: boolean;
getOsIcon: (osType?: string) => React.ReactNode; 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 [expanded, setExpanded] = React.useState(false);
const formatTime = (time?: string) => { const formatTime = (time?: string) => {
@ -252,7 +255,7 @@ export const ServerCard: React.FC<ServerCardProps> = ({ server, onTest, onEdit,
size="icon" size="icon"
className="h-7 w-7" className="h-7 w-7"
onClick={() => onTest(server)} onClick={() => onTest(server)}
disabled={isTesting} disabled={isTesting || isCollecting}
> >
{isTesting ? ( {isTesting ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" /> <Loader2 className="h-3.5 w-3.5 animate-spin" />
@ -263,6 +266,25 @@ export const ServerCard: React.FC<ServerCardProps> = ({ server, onTest, onEdit,
</TooltipTrigger> </TooltipTrigger>
<TooltipContent></TooltipContent> <TooltipContent></TooltipContent>
</Tooltip> </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> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button

View File

@ -37,9 +37,9 @@ import {
} from '@/components/ui/collapsible'; } from '@/components/ui/collapsible';
import type { ServerResponse, ServerCategoryResponse } from '../types'; import type { ServerResponse, ServerCategoryResponse } from '../types';
import { OsType, OsTypeLabels, AuthType, AuthTypeLabels, ServerStatus } 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 { 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 { interface ServerEditDialogProps {
open: boolean; open: boolean;
@ -61,6 +61,9 @@ export const ServerEditDialog: React.FC<ServerEditDialogProps> = ({
const [showPassphrase, setShowPassphrase] = useState(false); const [showPassphrase, setShowPassphrase] = useState(false);
const [tagInput, setTagInput] = useState(''); const [tagInput, setTagInput] = useState('');
const [systemInfoOpen, setSystemInfoOpen] = useState(true); 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 isEdit = !!server?.id;
const form = useForm<ServerFormValues>({ const form = useForm<ServerFormValues>({
@ -150,6 +153,129 @@ export const ServerEditDialog: React.FC<ServerEditDialogProps> = ({
} }
}, [server, open, form]); }, [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) => { const onSubmit = async (values: ServerFormValues) => {
try { try {
setLoading(true); setLoading(true);
@ -558,6 +684,41 @@ export const ServerEditDialog: React.FC<ServerEditDialogProps> = ({
</> </>
)} )}
</div> </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> </div>
<Separator /> <Separator />

View File

@ -8,6 +8,7 @@ import {
AlertCircle, AlertCircle,
HelpCircle, HelpCircle,
Terminal, Terminal,
RefreshCw,
} from 'lucide-react'; } from 'lucide-react';
import { import {
Table, Table,
@ -36,7 +37,9 @@ interface ServerTableProps {
onEdit: (server: ServerResponse) => void; onEdit: (server: ServerResponse) => void;
onDelete: (server: ServerResponse) => void; onDelete: (server: ServerResponse) => void;
onSSHConnect: (server: ServerResponse) => void; onSSHConnect: (server: ServerResponse) => void;
onCollectHardware: (server: ServerResponse) => void;
isTesting?: (serverId: number) => boolean; isTesting?: (serverId: number) => boolean;
isCollecting?: (serverId: number) => boolean;
getOsIcon: (osType?: string) => React.ReactNode; getOsIcon: (osType?: string) => React.ReactNode;
} }
@ -46,7 +49,9 @@ export const ServerTable: React.FC<ServerTableProps> = ({
onEdit, onEdit,
onDelete, onDelete,
onSSHConnect, onSSHConnect,
onCollectHardware,
isTesting, isTesting,
isCollecting,
getOsIcon, getOsIcon,
}) => { }) => {
const formatTime = (time?: string) => { const formatTime = (time?: string) => {
@ -155,7 +160,7 @@ export const ServerTable: React.FC<ServerTableProps> = ({
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => onTest(server)} onClick={() => onTest(server)}
disabled={isTesting?.(server.id)} disabled={isTesting?.(server.id) || isCollecting?.(server.id)}
className="h-8 w-8 p-0" className="h-8 w-8 p-0"
> >
{isTesting?.(server.id) ? ( {isTesting?.(server.id) ? (
@ -167,6 +172,24 @@ export const ServerTable: React.FC<ServerTableProps> = ({
</TooltipTrigger> </TooltipTrigger>
<TooltipContent></TooltipContent> <TooltipContent></TooltipContent>
</Tooltip> </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> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button

View File

@ -36,7 +36,7 @@ import { DataTablePagination } from '@/components/ui/pagination';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import type { ServerResponse, ServerCategoryResponse, ServerStatus, OsType } from './types'; import type { ServerResponse, ServerCategoryResponse, ServerStatus, OsType } from './types';
import { ServerStatusLabels, OsTypeLabels } 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 { CategoryManageDialog } from './components/CategoryManageDialog';
import { AlertRuleManageDialog } from './components/AlertRuleManageDialog'; import { AlertRuleManageDialog } from './components/AlertRuleManageDialog';
import { ServerEditDialog } from './components/ServerEditDialog'; import { ServerEditDialog } from './components/ServerEditDialog';
@ -79,6 +79,7 @@ const ServerList: React.FC = () => {
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [serverToDelete, setServerToDelete] = useState<ServerResponse | null>(null); const [serverToDelete, setServerToDelete] = useState<ServerResponse | null>(null);
const [testingServerId, setTestingServerId] = useState<number | null>(null); const [testingServerId, setTestingServerId] = useState<number | null>(null);
const [collectingServerId, setCollectingServerId] = useState<number | null>(null);
const [statsData, setStatsData] = useState<{ const [statsData, setStatsData] = useState<{
total: number; total: number;
online: number; online: number;
@ -167,7 +168,7 @@ const ServerList: React.FC = () => {
} }
}; };
// 测试连接 // 测试连接(轻量级)
const handleTestConnection = async (server: ServerResponse) => { const handleTestConnection = async (server: ServerResponse) => {
setTestingServerId(server.id); setTestingServerId(server.id);
try { try {
@ -175,10 +176,8 @@ const ServerList: React.FC = () => {
if (result.connected) { if (result.connected) {
toast({ toast({
title: '连接成功', title: '连接成功',
description: `${result.hostname} - ${result.osVersion} (响应时间: ${result.responseTime}ms)`, description: `响应时间: ${result.responseTime}ms`,
}); });
// 测试连接成功后刷新数据
await loadServers();
} else { } else {
toast({ toast({
variant: 'destructive', 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) => { const handleEdit = (server: ServerResponse) => {
setEditingServer(server); setEditingServer(server);
@ -532,7 +554,9 @@ const ServerList: React.FC = () => {
onEdit={handleEdit} onEdit={handleEdit}
onDelete={handleDelete} onDelete={handleDelete}
onSSHConnect={handleSSHConnect} onSSHConnect={handleSSHConnect}
onCollectHardware={handleCollectHardware}
isTesting={testingServerId === server.id} isTesting={testingServerId === server.id}
isCollecting={collectingServerId === server.id}
getOsIcon={getOsIcon} getOsIcon={getOsIcon}
/> />
))} ))}
@ -548,7 +572,9 @@ const ServerList: React.FC = () => {
onEdit={handleEdit} onEdit={handleEdit}
onDelete={handleDelete} onDelete={handleDelete}
onSSHConnect={handleSSHConnect} onSSHConnect={handleSSHConnect}
onCollectHardware={handleCollectHardware}
isTesting={(serverId) => testingServerId === serverId} isTesting={(serverId) => testingServerId === serverId}
isCollecting={(serverId) => collectingServerId === serverId}
getOsIcon={getOsIcon} getOsIcon={getOsIcon}
/> />
)} )}

View File

@ -6,6 +6,7 @@ import type {
ServerResponse, ServerResponse,
ServerRequest, ServerRequest,
ServerConnectionTestResult, ServerConnectionTestResult,
ServerHardwareCollectionResult,
AlertRuleResponse, AlertRuleResponse,
AlertRuleQuery, AlertRuleQuery,
AlertRuleRequest, AlertRuleRequest,
@ -115,12 +116,19 @@ export const batchDeleteServers = (ids: number[]) =>
request.post<void>(`${SERVER_URL}/batch-delete`, { ids }); request.post<void>(`${SERVER_URL}/batch-delete`, { ids });
/** /**
* *
* *
*/ */
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`);
/**
*
* hostnameCPU
*/
export const collectServerHardware = (id: number) =>
request.post<ServerHardwareCollectionResult>(`${SERVER_URL}/${id}/collect-hardware`);
// ==================== 告警规则 ==================== // ==================== 告警规则 ====================
/** /**

View File

@ -239,7 +239,7 @@ export const AuthTypeLabels: Record<AuthType, { label: string; description: stri
}; };
/** /**
* *
*/ */
export interface ServerConnectionTestResult { export interface ServerConnectionTestResult {
/** 是否连接成功 */ /** 是否连接成功 */
@ -250,6 +250,20 @@ export interface ServerConnectionTestResult {
connectTime: string; connectTime: string;
/** 响应时间(ms) */ /** 响应时间(ms) */
responseTime: number; responseTime: number;
}
/**
*
*/
export interface ServerHardwareCollectionResult {
/** 是否连接成功 */
connected: boolean;
/** 错误信息(连接失败时返回) */
errorMessage: string | null;
/** 连接时间 */
connectTime: string;
/** 响应时间(ms) */
responseTime: number;
/** 主机名 */ /** 主机名 */
hostname: string; hostname: string;
/** 操作系统版本 */ /** 操作系统版本 */
@ -260,6 +274,8 @@ export interface ServerConnectionTestResult {
memorySize: number; memorySize: number;
/** 磁盘大小(GB) */ /** 磁盘大小(GB) */
diskSize: number; diskSize: number;
/** 磁盘详细信息 */
diskInfo?: DiskInfo[];
} }
// ==================== 告警规则 ==================== // ==================== 告警规则 ====================