247 lines
9.6 KiB
TypeScript
247 lines
9.6 KiB
TypeScript
"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 { 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<TrafficFlowOverlayResult | null>(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")
|
|
toast.success(`wg-flow на ${res.address}`)
|
|
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 (
|
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
<SheetContent
|
|
side="right"
|
|
className="flex w-full flex-col gap-0 overflow-hidden p-0 sm:max-w-2xl"
|
|
>
|
|
<SheetHeader className="shrink-0 gap-1 border-b px-5 pt-5 pb-4 pr-12">
|
|
<SheetTitle className="text-base font-semibold tracking-tight">
|
|
Подключить jump-host
|
|
</SheetTitle>
|
|
<SheetDescription>
|
|
Создаст wg-flow на MikroTik (сервер, listen 13232) и выдаст готовый bash для Linux-хоста Docker MM (клиент).
|
|
</SheetDescription>
|
|
</SheetHeader>
|
|
<div className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto px-5 py-4">
|
|
<FormField label="Jump-host" required>
|
|
<Select
|
|
value={serverId || undefined}
|
|
onValueChange={(v) => handleServerChange(String(v ?? ""))}
|
|
>
|
|
<SelectTrigger aria-label="Jump-host" className="h-9 w-full min-w-0">
|
|
<SelectValue>{selectedLabel}</SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent align="start" className="min-w-(--anchor-width)">
|
|
{jumpHosts.map((s) => (
|
|
<SelectItem key={s.id} value={String(s.id)}>
|
|
{s.name || s.host} ({s.host})
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</FormField>
|
|
<FormField
|
|
label="Публичный IP или DNS jump-host"
|
|
required
|
|
hint="Куда хост MM (wg-quick) стучится по UDP 13232. Не контейнер backend."
|
|
>
|
|
<Input aria-label="Публичный IP или DNS jump-host"
|
|
className="font-mono"
|
|
value={endpoint}
|
|
onChange={(e) => setEndpoint(e.target.value)}
|
|
placeholder="jh.example.com"
|
|
autoComplete="off"
|
|
/>
|
|
</FormField>
|
|
<FormField
|
|
label="GRE slow-path (IPFIX inner)"
|
|
hint="allow-fast-path=no на GRE этого JH. Inner YouTube попадёт в Traffic Flow, но вырастет CPU. По умолчанию выкл."
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<Switch aria-label="Выключить FastPath на GRE" checked={disableGreFastPath} onCheckedChange={setDisableGreFastPath} />
|
|
<span className="text-sm text-muted-foreground">Выключить FastPath на GRE</span>
|
|
</div>
|
|
</FormField>
|
|
{result ? (
|
|
<div className="flex min-h-0 flex-col gap-4">
|
|
<Alert variant="success">
|
|
<InfoIcon />
|
|
<AlertTitle>Linux-хост /opt/cdn-mm</AlertTitle>
|
|
<AlertDescription>
|
|
Скопируйте вкладку Linux и выполните от root. WG — клиент к JH:13232; контейнер слушает только WG-IP:4739, не 0.0.0.0. Ключ не кладите в git.
|
|
</AlertDescription>
|
|
</Alert>
|
|
<ul className="flex flex-col gap-1.5">
|
|
{result.steps.map((s) => (
|
|
<li key={s} className="flex items-start gap-2 text-sm">
|
|
<CheckIcon className="mt-0.5 size-3.5 shrink-0 text-success" />
|
|
<span>{s}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
{formats.length > 0 && active ? (
|
|
<div className="flex min-h-0 flex-col gap-3">
|
|
<Tabs
|
|
value={tab}
|
|
onValueChange={(v) => {
|
|
setTab(String(v))
|
|
setCopied(false)
|
|
}}
|
|
className="shrink-0 gap-0"
|
|
>
|
|
<TabsList className="h-9 w-full">
|
|
{formats.map((f) => (
|
|
<TabsTrigger key={f.id} value={f.id} className="flex-1 px-2 text-xs sm:text-sm">
|
|
{f.label}
|
|
</TabsTrigger>
|
|
))}
|
|
</TabsList>
|
|
</Tabs>
|
|
<Frame dense className="flex min-h-0 flex-col overflow-hidden">
|
|
<FramePanel className="relative flex min-h-0 flex-col overflow-hidden p-0">
|
|
<ScrollArea className="h-full min-h-0 max-h-[min(52vh,22rem)]">
|
|
<CodeBlock code={active.code} className="whitespace-pre break-normal" />
|
|
</ScrollArea>
|
|
</FramePanel>
|
|
</Frame>
|
|
<div className="flex items-center justify-end gap-2">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => downloadText(active.filename, active.code)}
|
|
>
|
|
<DownloadIcon className="size-3.5" />
|
|
Файл
|
|
</Button>
|
|
<Button type="button" size="sm" onClick={handleCopy}>
|
|
{copied ? <CheckIcon className="size-3.5" /> : <CopyIcon className="size-3.5" />}
|
|
{copied ? "Скопировано" : "Копировать"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
<SheetFooter className="shrink-0 flex-row items-center justify-between gap-3 border-t px-5 py-3.5 sm:flex-row">
|
|
<SheetClose render={<Button type="button" variant="outline" className="shrink-0" />}>
|
|
Закрыть
|
|
</SheetClose>
|
|
<Button disabled={!canSubmit} onClick={() => { void handleSubmit() }}>
|
|
{busy ? "Подключение…" : "Подключить"}
|
|
</Button>
|
|
</SheetFooter>
|
|
</SheetContent>
|
|
</Sheet>
|
|
)
|
|
}
|
|
|
|
export { FlowOverlaySheet }
|