import { useState } from "react"; import { useTranslation } from "react-i18next"; import { Server, RefreshCw, CheckSquare, Square, Download } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from "@/components/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/select"; import { discoverProxmoxGuests, bulkImportSSHHosts, getSSHHosts, } from "@/main-axios"; import type { SSHHostWithStatus } from "@/main-axios"; import type { ProxmoxGuest } from "@/types/proxmox"; import { resolveProxmoxImportAuth } from "./proxmox-import-auth"; interface ProxmoxDiscoverDialogProps { open: boolean; onClose: () => void; hosts: SSHHostWithStatus[]; onHostsChanged: (hosts: SSHHostWithStatus[]) => void; /** Pre-select a specific host and skip the host picker */ preselectedHostId?: number; /** Credential to use for imported hosts */ defaultCredentialId?: number | null; /** Auth type to use for imported hosts */ defaultAuthType?: string; /** Username from the default credential */ defaultUsername?: string; } export function ProxmoxDiscoverDialog({ open, onClose, hosts, onHostsChanged, preselectedHostId, defaultCredentialId, defaultAuthType, defaultUsername, }: ProxmoxDiscoverDialogProps) { const { t } = useTranslation(); const [selectedHostId, setSelectedHostId] = useState( preselectedHostId ? String(preselectedHostId) : "", ); const [discovering, setDiscovering] = useState(false); const [guests, setGuests] = useState(null); const [discoveredCredentialId, setDiscoveredCredentialId] = useState< number | null >(null); const [selected, setSelected] = useState>(new Set()); const [importing, setImporting] = useState(false); // When opened from the dropdown (no preselectedHostId), only show Proxmox-enabled hosts const sshHosts = hosts.filter( (h) => !("isFolder" in h) && h.enableProxmox === true, ); // The Proxmox host the discovery runs against — imported guests are grouped // into a folder named after it. Use the same id resolution as discovery so // it also works when launched directly from a host action (preselectedHostId). const effectiveHostId = preselectedHostId != null ? String(preselectedHostId) : selectedHostId; const sourceHost = hosts.find( (h) => !("isFolder" in h) && String(h.id) === effectiveHostId, ); const importFolder = sourceHost?.name || "Proxmox"; function reset() { if (!preselectedHostId) setSelectedHostId(""); setGuests(null); setDiscoveredCredentialId(null); setSelected(new Set()); setDiscovering(false); setImporting(false); } async function handleDiscover() { const hostId = preselectedHostId ?? (selectedHostId ? Number(selectedHostId) : null); if (!hostId) return; setDiscovering(true); setGuests(null); setDiscoveredCredentialId(null); setSelected(new Set()); try { const result = await discoverProxmoxGuests(hostId); setGuests(result.guests); setDiscoveredCredentialId(result.credentialId ?? null); setSelected( new Set( result.guests .filter((g) => g.status === "running") .map((g) => g.vmid), ), ); } catch (err: any) { toast.error(err?.message ?? t("hosts.proxmoxDiscoveryFailed")); } finally { setDiscovering(false); } } async function handleImport() { if (!guests || selected.size === 0) return; setImporting(true); try { // Prefer explicitly configured credential, then fall back to the host's own credential const credId = defaultCredentialId ?? discoveredCredentialId; const importAuth = resolveProxmoxImportAuth(defaultAuthType, credId); const toImport = guests .filter((g) => selected.has(g.vmid)) .map((g) => ({ name: g.name, ip: g.ip ?? "0.0.0.0", port: g.connectionType === "rdp" ? 3389 : 22, username: defaultUsername ?? "root", folder: importFolder, ...importAuth, enableTerminal: g.connectionType !== "rdp", enableFileManager: g.connectionType !== "rdp", enableTunnel: g.connectionType !== "rdp", enableSsh: g.connectionType !== "rdp", enableRdp: g.connectionType === "rdp", enableDocker: g.enableDocker, connectionType: g.connectionType, tags: ["proxmox", g.type, g.node], proxmoxConfig: { source: { source: "proxmox", sourceHostId: Number(effectiveHostId), node: g.node, vmid: g.vmid, type: g.type, lastSeenAt: new Date().toISOString(), lastStatus: g.status, missingSince: null, }, }, })); const result = await bulkImportSSHHosts(toImport, false); const updated = await getSSHHosts(); onHostsChanged(updated); window.dispatchEvent(new CustomEvent("termix:hosts-changed")); const msg = [ result.success ? t("hosts.proxmoxResultImported", { count: result.success }) : null, result.updated ? t("hosts.proxmoxResultUpdated", { count: result.updated }) : null, result.failed ? t("hosts.proxmoxResultFailed", { count: result.failed }) : null, ] .filter(Boolean) .join(", "); toast.success(t("hosts.proxmoxImportComplete", { summary: msg })); if (result.failed === 0) { reset(); onClose(); } } catch (err: any) { toast.error(err?.message ?? t("hosts.proxmoxImportFailed")); } finally { setImporting(false); } } const nodeGroups = guests ? guests.reduce( (acc, g) => { if (!acc[g.node]) acc[g.node] = []; acc[g.node].push(g); return acc; }, {} as Record, ) : {}; const canDiscover = preselectedHostId != null || !!selectedHostId; return ( { if (!v) { reset(); onClose(); } }} > {t("hosts.proxmoxImportTitle")} {t("hosts.docsLink")}
{/* Host selector — hidden when launched from a specific host */} {!preselectedHostId && (
)} {/* When launched from a specific host, show Discover directly */} {preselectedHostId && !guests && ( )} {/* Guest list */} {guests !== null && (
{t("hosts.proxmoxGuestsSelected", { count: guests.length, selected: selected.size, })}
·
{guests.length === 0 ? (

{t("hosts.proxmoxNoGuests")}

) : (
{Object.entries(nodeGroups).map(([node, nodeGuests]) => (
{node}
{nodeGuests.map((g) => ( ))}
))}
)}
)}
{guests !== null && selected.size > 0 && ( )}
); }