import { useState, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { cn } from "@/lib/utils"; 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 { useServers, useServerMutations } from "@/hooks/api"; import { ServerStatsStrip } from "./ServerStatsStrip"; import { useConfirm } from "@/components/ui/confirm-dialog"; import { toast } from "sonner"; import { CloudServerImport } from "./CloudServerImport"; import { TerminalDialog } from "./TerminalDialog"; import { RdpDialog } from "./RdpDialog"; import { ServerFileBrowser } from "./ServerFileBrowser"; import type { JsonData } from "@/types/common"; import { ExcelServerImport } from "./ExcelServerImport"; import { ServerForm, EMPTY_FORM, type ServerFormData } from "./ServerForm"; import { getTags } from "./serverHelpers"; import { ServerTableRow } from "./ServerTableRow"; import { ServerDetailSheet } from "./ServerDetailSheet"; import { usePermission } from "@/hooks/usePermission"; import { PageShell } from "@/components/layout/PageShell"; import { ContentPanel } from "@/components/layout/ContentPanel"; import { Server } from "lucide-react"; export function ServerManager() { const { t } = useTranslation(); const confirm = useConfirm(); const { hasPermission } = usePermission(); 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; username?: string; hasAuth?: boolean; } | null>(null); const [rdpOpen, setRdpOpen] = useState(false); const [rdpServer, setRdpServer] = useState<{ id: number; name: string; ip: string; } | null>(null); // 文件浏览 Dialog 状态 const [fileBrowserOpen, setFileBrowserOpen] = useState(false); const [fileBrowserServer, setFileBrowserServer] = useState<{ id: number; name: string; } | null>(null); const [detailServer, setDetailServer] = useState(null); // 分页状态 const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(10); const queryParams = useMemo(() => { const params: Record = { page, page_size: pageSize }; if (searchQuery) params.keyword = searchQuery; if (statusFilter && statusFilter !== "all") params.status = statusFilter; return params; }, [page, pageSize, searchQuery, statusFilter]); const { data, isLoading: loading, isFetching, refetch, } = useServers(queryParams); const { createMutation, updateMutation, deleteMutation, batchDeleteMutation, } = useServerMutations(); const servers = data?.list ?? []; const total = data?.total ?? 0; const getServerDisplayName = (server: JsonData) => server.alias || server.cloud_instance_name || server.hostname || server.ip || ""; // 详情抽屉内打开终端/RDP/文件时先关闭抽屉,避免同 z-index 的 Sheet 遮挡弹窗 // Logan.Li const closeDetailSheet = () => setDetailServer(null); const openTerminal = (server: JsonData) => { closeDetailSheet(); setTerminalServer({ id: server.id, name: getServerDisplayName(server), ip: server.ip, username: server.username || "root", hasAuth: server.has_auth ?? true, }); setTerminalOpen(true); }; const openRdp = (server: JsonData) => { closeDetailSheet(); setRdpServer({ id: server.id, name: getServerDisplayName(server), ip: server.ip }); setRdpOpen(true); }; const openFileBrowser = (server: JsonData) => { closeDetailSheet(); setFileBrowserServer({ id: server.id, name: getServerDisplayName(server) }); setFileBrowserOpen(true); }; // 搜索/筛选变化时重置到第1页 const handleSearch = (val: string) => { setSearchQuery(val); setPage(1); }; const handleStatusFilter = (val: string) => { setStatusFilter(val); setPage(1); }; // 导出 CSV const toggleSelectAll = () => { if (selectedServers.length === servers.length) { setSelectedServers([]); } else { setSelectedServers(servers.map((s) => String(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 name = server ? getServerDisplayName(server) : id; const ok = await confirm({ title: t("server.deleteTitle"), description: t("server.deleteConfirm", { name, ip: server?.ip || "-", }), confirmText: t("common.delete"), variant: "destructive", }); if (!ok) return; try { await deleteMutation.mutateAsync(id); toast.success(t("server.deleted")); } catch (e) { console.error("[ServerManager] delete failed:", e); toast.error(t("server.deleteFailed")); } }; // 批量删除 const handleBatchDelete = async () => { if (selectedServers.length === 0) return; const ok = await confirm({ title: t("server.batchDeleteTitle"), description: t("server.batchDeleteConfirm", { count: selectedServers.length, }), confirmText: t("server.batchDelete"), variant: "destructive", }); if (!ok) return; try { await batchDeleteMutation.mutateAsync( selectedServers.map((id) => Number(id)), ); toast.success( t("server.deletedCount", { count: selectedServers.length }), ); setSelectedServers([]); } catch (e) { console.error("[ServerManager] batch delete failed:", e); toast.error(t("server.batchDeleteFailed")); } }; // 打开创建表单 const openCreateForm = () => { setEditingId(null); setFormData(EMPTY_FORM); setDetailsOpen(false); setFormOpen(true); }; // 打开编辑表单 const openEditForm = (server: JsonData) => { setEditingId(server.id); setFormData({ hostname: server.hostname || "", alias: server.alias || "", ip: server.ip || "", public_ip: server.public_ip || "", port: String(server.port || 22), username: server.username || "root", auth_type: server.auth_type || "password", os_type: server.os_type || (server.protocol === "rdp" ? "windows" : "linux"), protocol: server.protocol || (server.os_type === "windows" ? "rdp" : "ssh"), rdp_port: String(server.rdp_port || 3389), 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, alias: formData.alias, ip: formData.ip, public_ip: formData.public_ip || undefined, port: Number(formData.port) || 22, os_type: formData.os_type || "linux", protocol: formData.protocol || "ssh", rdp_port: Number(formData.rdp_port) || 3389, 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 updateMutation.mutateAsync({ id: editingId, data: payload }); } else { await createMutation.mutateAsync(payload); } toast.success(editingId ? t("server.updated") : t("server.created")); setFormOpen(false); } catch (e) { console.error("[ServerManager] save failed:", e); toast.error(t("server.saveFailed")); } finally { setFormSaving(false); } }; // 导出 CSV — 将当前服务器列表导出为 CSV 文件 // Logan.Li const handleExport = () => { if (servers.length === 0) { toast.error(t("server.noExportData")); return; } const headers = [ t("server.csv.name"), t("server.csv.hostname"), t("server.csv.ip"), t("server.csv.publicIp"), t("server.csv.port"), t("server.csv.os"), t("server.csv.cpu"), t("server.csv.memory"), t("server.csv.disk"), t("server.csv.status"), t("server.csv.tags"), ]; const rows = servers.map((s) => { const memGB = s.memory ? Math.round(Number(s.memory) / 1024) : 0; const tags = getTags(s).join(";"); return [ getServerDisplayName(s), 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(t("server.exported", { count: servers.length })); }; const colSpanCount = 8; const tableToolbar = ( <>
handleSearch(e.target.value)} className="pl-9 h-9 dg-input" />
{selectedServers.length > 0 && (
{t("server.selected", { count: selectedServers.length })} {hasPermission("server:delete") && ( )}
)} ); return ( } metrics={} actions={ <> {hasPermission("server:create") && ( {t("server.importMethod")} setCloudImportOpen(true)}> {t("server.cloudImport")} {t("server.cloudImportHint")} setExcelImportOpen(true)}> {t("server.csvImport")} {t("server.csvImportHint")} )} {hasPermission("server:create") && ( )} } >
0 } onCheckedChange={toggleSelectAll} /> {t("server.assetName")} {t("server.ipAddress")} {t("server.publicIp")} {t("common.status")} {t("server.config")} {t("server.tags")} {t("common.actions")} {loading ? ( {t("common.loading")} ) : servers.length === 0 ? ( {t("server.noData")} ) : ( servers.map((server) => ( )) )}
{/* 分页 */}
{t("server.perPage")} {t("server.items")} {t("server.totalItems", { count: total })}
{page} / {Math.max(1, Math.ceil(total / pageSize))}
{/* 创建/编辑服务器 Dialog */} {/* Import Dialogs */} { setCloudImportOpen(false); refetch(); }} /> { setExcelImportOpen(false); refetch(); }} /> {/* 终端 Dialog */} { if (!v) setDetailServer(null); }} server={ detailServer ? servers.find((s) => String(s.id) === String(detailServer.id)) || detailServer : null } onOpenTerminal={openTerminal} onOpenRdp={openRdp} onOpenFiles={openFileBrowser} onOpenEdit={(s) => { setDetailServer(null); openEditForm(s); }} /> {/* 文件浏览 Dialog */}
); }