import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Download, ListChecks, Plus, RefreshCw, Search, Upload, X, } from "lucide-react"; import { toast } from "sonner"; import { SidebarTree } from "@/sidebar/SidebarTree"; import { HostManager } from "@/sidebar/HostManager"; import { HostShareModal } from "@/sidebar/HostShareModal"; import { Button } from "@/components/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/dropdown-menu"; import { getSSHHosts, bulkImportSSHHosts } from "@/main-axios"; import type { SSHHostWithStatus } from "@/main-axios"; import type { Host, HostFolder, TabType } from "@/types/ui-types"; export function HostsPanel({ onOpenTab, onEditHost, hostTree, loading, onEditingChange, active = true, }: { onOpenTab: (host: Host, type: TabType) => void; onEditHost: (host: Host) => void; hostTree?: HostFolder; loading?: boolean; onEditingChange?: (editing: boolean) => void; active?: boolean; }) { const { t } = useTranslation(); const [hostSearch, setHostSearch] = useState(""); const [managerEditing, setManagerEditing] = useState(false); const [selectionMode, setSelectionMode] = useState(false); const [refreshing, setRefreshing] = useState(false); const [rawHosts, setRawHosts] = useState([]); const [shareModalHost, setShareModalHost] = useState(null); const fileInputRef = useRef(null); const importOverwriteRef = useRef(false); useEffect(() => { getSSHHosts() .then(setRawHosts) .catch(() => {}); }, []); function handleEditingChange(editing: boolean) { setManagerEditing(editing); onEditingChange?.(editing); } function toggleSelectionMode() { setSelectionMode((v) => !v); } async function handleRefresh() { setRefreshing(true); try { const hosts = await getSSHHosts(); setRawHosts(hosts); window.dispatchEvent(new CustomEvent("termix:hosts-changed")); } catch { // best-effort } finally { setRefreshing(false); } } function handleExportHosts() { const data = JSON.stringify({ hosts: rawHosts }, null, 2); const blob = new Blob([data], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "termix-hosts.json"; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); toast.success(t("hosts.hostsExported")); } function handleDownloadSample() { const sample = JSON.stringify( { hosts: [ { name: "Web Server (Production)", ip: "192.168.1.100", username: "admin", authType: "password", password: "your_secure_password_here", folder: "Production", tags: ["web", "production", "nginx"], pin: true, notes: "Main production web server running Nginx", enableSsh: true, enableRdp: false, enableVnc: false, enableTelnet: false, sshPort: 22, enableTerminal: true, enableTunnel: false, enableFileManager: true, enableDocker: false, defaultPath: "/var/www", }, { name: "Database Server", ip: "192.168.1.101", username: "dbadmin", authType: "key", key: "-----BEGIN OPENSSH PRIVATE KEY-----\nYour SSH private key content here\n-----END OPENSSH PRIVATE KEY-----", keyPassword: "optional_key_passphrase", keyType: "ssh-ed25519", folder: "Production", tags: ["database", "production", "postgresql"], enableSsh: true, enableRdp: false, enableVnc: false, enableTelnet: false, sshPort: 22, enableTerminal: true, enableTunnel: true, enableFileManager: false, enableDocker: false, }, ], }, null, 2, ); const blob = new Blob([sample], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "termix-hosts-sample.json"; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } return (
{!managerEditing && (
setHostSearch(e.target.value)} placeholder={t("hosts.searchHosts")} className="flex-1 text-xs bg-transparent outline-none placeholder:text-muted-foreground/50 text-foreground min-w-0" /> {hostSearch && ( )}
{ const file = e.target.files?.[0]; if (!file) return; e.target.value = ""; try { const text = await file.text(); const parsed = JSON.parse(text); const hostsArray = Array.isArray(parsed) ? parsed : (parsed.hosts ?? []); if (!Array.isArray(hostsArray) || hostsArray.length === 0) { toast.error("No hosts found in file"); return; } if (hostsArray.length > 100) { toast.error("Cannot import more than 100 hosts at once"); return; } const normalized = hostsArray.map((h: any) => ({ ...h, port: h.port ?? h.sshPort ?? 22, enableSsh: h.enableSsh ?? h.connectionType === "ssh", enableRdp: h.enableRdp ?? h.connectionType === "rdp", enableVnc: h.enableVnc ?? h.connectionType === "vnc", enableTelnet: h.enableTelnet ?? h.connectionType === "telnet", })); const result = await bulkImportSSHHosts( normalized, importOverwriteRef.current, ); const hosts = await getSSHHosts(); setRawHosts(hosts); window.dispatchEvent(new CustomEvent("termix:hosts-changed")); const msg = [ result.success ? `${result.success} imported` : null, result.updated ? `${result.updated} updated` : null, result.failed ? `${result.failed} failed` : null, ] .filter(Boolean) .join(", "); toast.success(`Import complete: ${msg}`); } catch (err: any) { toast.error(err?.message ?? "Failed to import hosts"); } }} />
{ importOverwriteRef.current = false; fileInputRef.current?.click(); }} > {t("hosts.importSkipExisting")} { importOverwriteRef.current = true; fileInputRef.current?.click(); }} > {t("hosts.importOverwrite")} {t("hosts.exportAll")} {t("hosts.downloadSample")}
)}
setShareModalHost(host)} query={hostSearch.trim().toLowerCase()} selectionMode={selectionMode} onToggleSelectionMode={toggleSelectionMode} loading={loading} />
setShareModalHost(null)} host={shareModalHost} />
); }