"use client" import { useMemo, useState } from "react" import { toast } from "sonner" import { FormField } from "@/components/form-kit" import { Alert, AlertDescription, AlertTitle } from "@/components/reui/alert" import { Frame, FramePanel } from "@/components/reui/frame" import { CodeBlock, downloadText } from "@/components/reui-kit/code-export-sheet" import { Button } from "@/components/ui/button" import { Switch } from "@/components/ui/switch" import { Input } from "@/components/ui/input" import { ScrollArea } from "@/components/ui/scroll-area" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetFooter, SheetClose, } from "@/components/ui/sheet" import { FlowHostPanel } from "./flow-host-panel" import { applyTrafficFlowOverlay } from "@/shared/api/traffic-flow" import type { ServerRead } from "@mmapp/contracts/servers" import type { TrafficFlowOverlayResult } from "@mmapp/contracts/traffic-flow" import { CheckIcon, CopyIcon, DownloadIcon, InfoIcon, } from "lucide-react" function FlowOverlaySheet({ open, onOpenChange, servers, backendUrl, onDone, }: { open: boolean onOpenChange: (v: boolean) => void servers: ServerRead[] backendUrl: string onDone?: (result: TrafficFlowOverlayResult) => void }) { const jumpHosts = useMemo( () => servers.filter((s) => s.enabled && s.type === "jump-host"), [servers], ) const [serverId, setServerId] = useState("") const [endpoint, setEndpoint] = useState("") const [busy, setBusy] = useState(false) const [result, setResult] = useState(null) const [copied, setCopied] = useState(false) const [tab, setTab] = useState("linux") const [disableGreFastPath, setDisableGreFastPath] = useState(false) const [resetContext, setResetContext] = useState({ open: false, jumpHosts }) if (resetContext.open !== open || resetContext.jumpHosts !== jumpHosts) { setResetContext({ open, jumpHosts }) if (open) { setResult(null) setCopied(false) setTab("linux") setDisableGreFastPath(false) const first = jumpHosts[0] const nextId = first ? String(first.id) : "" setServerId(nextId) setEndpoint(first?.host ?? "") } } function handleServerChange(id: string) { setServerId(id) const selected = jumpHosts.find((s) => String(s.id) === id) if (selected) setEndpoint(selected.host) } const formats = result?.hostFiles ?? [] const active = formats.find((f) => f.id === tab) ?? formats[0] const selectedHost = jumpHosts.find((s) => String(s.id) === serverId) const selectedLabel = selectedHost ? `${selectedHost.name || selectedHost.host} (${selectedHost.host})` : "Выберите сервер…" const canSubmit = Boolean(serverId && endpoint.trim()) && !busy async function handleSubmit() { if (!serverId || !endpoint.trim()) return setBusy(true) try { const res = await applyTrafficFlowOverlay(backendUrl, serverId, endpoint.trim(), { disableGreFastPath, }) setResult(res) setTab(res.hostFiles[0]?.id ?? "linux") if (res.ok) toast.success(`wg-flow на ${res.address}`) else toast.warning(res.hostError || "MikroTik настроен; требуется применение на хосте") onDone?.(res) } catch (e) { toast.error(e instanceof Error ? e.message : "Не удалось подключить JH") } finally { setBusy(false) } } function handleCopy() { const code = active?.code ?? "" if (!code) return void navigator.clipboard.writeText(code).then(() => { setCopied(true) toast.success("Скопировано") setTimeout(() => setCopied(false), 1800) }) } return ( Подключить jump-host Настроит wg-flow и IPFIX на MikroTik. Если агент Docker установлен, автоматически применит пир на Linux-хосте MM.
setEndpoint(e.target.value)} placeholder="jh.example.com" autoComplete="off" />
Выключить FastPath на GRE
{result ? (
{result.stage === "applied" ? "Настройки применены" : result.stage === "host-pending" ? "MikroTik настроен, хост ожидает применения" : "Установка агента на Linux-хосте"} {result.hostError || (result.stage === "applied" ? "Пир добавлен без перезапуска WireGuard. Ожидайте handshake и поступления IPFIX в панели NetFlow." : "Выполните установщик из вкладки «Агент Docker» от root на сервере MM. Он однократно пересоздаст backend. Экспорт содержит приватный ключ — храните его как секрет.")}
    {result.steps.map((s) => (
  • {s}
  • ))}
{formats.length > 0 && active ? (
{ setTab(String(v)) setCopied(false) }} className="shrink-0 gap-0" > {formats.map((f) => ( {f.label} ))}
) : null}
) : null}
}> Закрыть
) } export { FlowOverlaySheet }