import { useState, useEffect, useRef } from 'react'; import { cn } from '@/lib/utils'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Checkbox } from '@/components/ui/checkbox'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Search, Plus, Trash2, Download, Upload, Cloud, FileSpreadsheet, RefreshCw, ChevronLeft, ChevronRight, } from 'lucide-react'; import { serverApi } from '@/api'; import { ServerStatsStrip } from './ServerStatsStrip'; import { useConfirm } from '@/components/ui/confirm-dialog'; import { toast } from 'sonner'; import { useUserStore } from '@/store'; import { CloudServerImport } from './CloudServerImport'; import { TerminalDialog } from './TerminalDialog'; import { ServerFileBrowser } from './ServerFileBrowser'; import { ExcelServerImport } from './ExcelServerImport'; import { ServerForm, EMPTY_FORM, type ServerFormData } from './ServerForm'; import { getTags } from './serverHelpers'; import { ServerTableRow } from './ServerTableRow'; import { usePermission } from '@/hooks/usePermission'; export function ServerManager() { const confirm = useConfirm(); const { hasPermission } = usePermission(); const [servers, setServers] = useState([]); const [loading, setLoading] = useState(true); const [selectedServers, setSelectedServers] = useState([]); const [searchQuery, setSearchQuery] = useState(''); const [statusFilter, setStatusFilter] = useState('all'); const [cloudImportOpen, setCloudImportOpen] = useState(false); const [excelImportOpen, setExcelImportOpen] = useState(false); // 创建/编辑表单状态 const [formOpen, setFormOpen] = useState(false); const [editingId, setEditingId] = useState(null); const [formData, setFormData] = useState(EMPTY_FORM); const [formSaving, setFormSaving] = useState(false); const [detailsOpen, setDetailsOpen] = useState(false); // 终端 Dialog 状态 const [terminalOpen, setTerminalOpen] = useState(false); const [terminalServer, setTerminalServer] = useState<{ id: number; name: string; ip: string; hasAuth?: boolean } | null>(null); // 文件浏览 Dialog 状态 const [fileBrowserOpen, setFileBrowserOpen] = useState(false); const [fileBrowserServer, setFileBrowserServer] = useState<{ id: number; name: string } | null>(null); // 分页状态 const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(10); const [total, setTotal] = useState(0); const openTerminal = ( server: any) => { setTerminalServer({ id: server.id, name: server.hostname, ip: server.ip, hasAuth: server.has_auth ?? true }); setTerminalOpen(true); }; const openFileBrowser = ( server: any) => { setFileBrowserServer({ id: server.id, name: server.hostname }); setFileBrowserOpen(true); }; const loadData = async (kw?: string, st?: string) => { try { setLoading(true); const q = kw ?? searchQuery; const s = st ?? statusFilter; const params: Record = { page, page_size: pageSize }; if (q) params.keyword = q; if (s && s !== 'all') params.status = s; const res: Record = await serverApi.getServers(params); setServers(res?.list || []); setTotal(res?.total || 0); } catch (e) { console.error('加载服务器列表失败:', e); setServers([]); } finally { setLoading(false); } }; // 搜索/筛选变化时重置到第1页 const handleSearch = (val: string) => { setSearchQuery(val); setPage(1); }; const handleStatusFilter = (val: string) => { setStatusFilter(val); setPage(1); }; useEffect(() => { loadData(); }, [page, pageSize]); // WebSocket 实时接收 Agent 指标推送 // Logan.Li const wsRef = useRef(null); const reconnectTimerRef = useRef | null>(null); useEffect(() => { const token = useUserStore.getState().token; if (!token) return; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/ws/metrics?token=${token}`; const connect = () => { if (wsRef.current?.readyState === WebSocket.OPEN) return; const ws = new WebSocket(wsUrl); wsRef.current = ws; ws.onopen = () => { // WebSocket 已连接,静默接收指标推送 }; ws.onmessage = (event) => { try { const msg = JSON.parse(event.data); if (msg.type === 'metrics_update') { setServers((prev) => prev.map((s) => { // 通过 hostname 匹配(Agent 上报时使用 hostname) if (s.hostname === msg.hostname) { return { ...s, cpu_usage: msg.cpu_usage, memory_usage: msg.memory_usage, disk_usage: msg.disk_usage, status: msg.status, tcp_established: msg.tcp_established, tcp_time_wait: msg.tcp_time_wait, net_rx_bps: msg.net_rx_bps, net_tx_bps: msg.net_tx_bps, disk_read_bps: msg.disk_read_bps, disk_write_bps: msg.disk_write_bps, }; } return s; }) ); } } catch { // 忽略非 JSON 消息 } }; ws.onclose = () => { // 连接断开,3 秒后重连 wsRef.current = null; reconnectTimerRef.current = setTimeout(connect, 3000); }; ws.onerror = (err) => { console.error('[MetricsWS] 连接错误:', err); ws.close(); }; }; connect(); return () => { if (reconnectTimerRef.current) clearTimeout(reconnectTimerRef.current); if (wsRef.current) { wsRef.current.onclose = null; // 阻止自动重连 wsRef.current.close(); wsRef.current = null; } }; }, []); // 导出 CSV const toggleSelectAll = () => { if (selectedServers.length === servers.length) { setSelectedServers([]); } else { setSelectedServers(servers.map((s) => s.id)); } }; const toggleSelect = (id: string) => { if (selectedServers.includes(id)) { setSelectedServers(selectedServers.filter((s) => s !== id)); } else { setSelectedServers([...selectedServers, id]); } }; const handleDelete = async (id: string) => { const server = servers.find((s) => String(s.id) === String(id)); const ok = await confirm({ title: '删除服务器', description: `确定删除「${server?.hostname || id}」吗?此操作不可撤销。`, confirmText: '删除', variant: 'destructive', }); if (!ok) return; try { await serverApi.deleteServer(id); toast.success('服务器已删除'); loadData(); } catch (e) { console.error('删除服务器失败:', e); toast.error('删除失败'); } }; // 批量删除 const handleBatchDelete = async () => { if (selectedServers.length === 0) return; const ok = await confirm({ title: '批量删除服务器', description: `确定删除选中的 ${selectedServers.length} 台服务器吗?此操作不可撤销。`, confirmText: '批量删除', variant: 'destructive', }); if (!ok) return; try { await serverApi.batchDelete(selectedServers.map((id) => Number(id))); toast.success(`已删除 ${selectedServers.length} 台服务器`); setSelectedServers([]); loadData(); } catch (e) { console.error('批量删除失败:', e); toast.error('批量删除失败'); } }; // 打开创建表单 const openCreateForm = () => { setEditingId(null); setFormData(EMPTY_FORM); setDetailsOpen(false); setFormOpen(true); }; // 打开编辑表单 const openEditForm = ( server: any) => { setEditingId(server.id); setFormData({ hostname: server.hostname || '', ip: server.ip || '', public_ip: server.public_ip || '', port: String(server.port || 22), username: server.username || 'root', auth_type: server.auth_type || 'password', password: '', ssh_key: '', credential_id: null as number | null, os: server.os || '', cpu: String(server.cpu || ''), memory: server.memory ? String(Math.round(server.memory / 1024)) : '', // MB → GB disk: String(server.disk || ''), status: server.status || 'running', tags: server.tags || '', description: server.description || '', }); setDetailsOpen(false); setFormOpen(true); }; // 保存表单 const handleSave = async () => { if (!formData.hostname || !formData.ip) return; try { setFormSaving(true); const payload: Record = { hostname: formData.hostname, ip: formData.ip, public_ip: formData.public_ip || undefined, port: Number(formData.port) || 22, username: formData.username || 'root', auth_type: formData.auth_type || 'password', os: formData.os, cpu: Number(formData.cpu) || 0, memory: (Number(formData.memory) || 0) * 1024, // GB → MB disk: Number(formData.disk) || 0, status: formData.status, tags: formData.tags || undefined, description: formData.description || undefined, }; // 凭证关联 — 下拉选择后自动关联,无需手输密码 if (formData.credential_id) { payload.credential_id = formData.credential_id; } else { // 手动输入模式:仅填了密码才提交 if (formData.password) { payload.password = formData.password; } if (formData.ssh_key) { payload.ssh_key = formData.ssh_key; } } if (editingId) { await serverApi.updateServer(editingId, payload); } else { await serverApi.createServer(payload); } toast.success(editingId ? '服务器已更新' : '服务器已创建'); setFormOpen(false); loadData(); } catch (e) { console.error('保存服务器失败:', e); toast.error('保存失败,请重试'); } finally { setFormSaving(false); } }; // 导出 CSV — 将当前服务器列表导出为 CSV 文件 // Logan.Li const handleExport = () => { if (servers.length === 0) { toast.error('没有可导出的数据'); return; } const headers = ['主机名', 'IP地址', '公网IP', '端口', '操作系统', 'CPU(核)', '内存(GB)', '磁盘(GB)', '状态', '标签']; const rows = servers.map((s) => { const memGB = s.memory ? Math.round(s.memory / 1024) : 0; const tags = getTags(s).join(';'); return [s.hostname || '', s.ip || '', s.public_ip || '', s.port || 22, s.os || '', s.cpu || 0, memGB, s.disk || 0, s.status || '', tags]; }); const csv = [headers, ...rows] .map((row) => row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(',')) .join('\n'); const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `servers_${new Date().toISOString().slice(0, 10)}.csv`; a.click(); URL.revokeObjectURL(url); toast.success(`已导出 ${servers.length} 台服务器`); }; const colSpanCount = 8; return (
{/* Page Header */}

服务器管理

管理云平台服务器资产

{hasPermission('server:create') && ( 选择导入方式 setCloudImportOpen(true)} > 云主机导入 从云平台同步 setExcelImportOpen(true)} > CSV导入 hostname + IP 即可 )} {hasPermission('server:create') && ( )}
{/* Stats Cards */} {/* Filters & Table */} {/* Toolbar */}
handleSearch(e.target.value)} className="pl-9 h-9 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]" />
{selectedServers.length > 0 && (
已选择 {selectedServers.length} 项 {hasPermission('server:delete') && ( )}
)}
0 } onCheckedChange={toggleSelectAll} className="border-[var(--dg-border)]" /> 主机名 IP地址 公网IP 状态 配置 使用率 标签 操作 {loading ? ( 加载中... ) : servers.length === 0 ? ( 暂无服务器数据 ) : ( servers.map((server) => ( )) )}
{/* 分页 */}
每页 条 共 {total} 条
{page} / {Math.max(1, Math.ceil(total / pageSize))}
{/* 创建/编辑服务器 Dialog */} {/* Import Dialogs */} { setCloudImportOpen(false); loadData(); }} /> { setExcelImportOpen(false); loadData(); }} /> {/* 终端 Dialog */} {/* 文件浏览 Dialog */}
); }