Docker images / prepare-release (push) Successful in 7s
Docker images / backend-image (push) Successful in 1m36s
Docker images / frontend-image (push) Successful in 2m45s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 43s
Docker images / publish-release (push) Successful in 10s
Updated the TerminalPage to include a new ServerTileRail for server selection and added QuickCmds for quick command execution. Enhanced the WireGuardPage with new tabs for interface and peer management, improved server selection handling, and added support for displaying peer details. Refactored data grids to support compact server views and improved empty state handling. Introduced new hooks for managing server states and improved user experience across components.
234 lines
8.5 KiB
TypeScript
234 lines
8.5 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useMemo, useState } from "react"
|
|
import { FormField, FormToggle, SectionTitle } from "@/components/form-kit"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Input } from "@/components/ui/input"
|
|
import {
|
|
Sheet, SheetContent, SheetHeader, SheetTitle,
|
|
SheetDescription, SheetFooter, SheetClose,
|
|
} from "@/components/ui/sheet"
|
|
import { ChevronDownIcon, ChevronRightIcon } from "lucide-react"
|
|
|
|
export type WgCreateFormState = {
|
|
serverId: string
|
|
name: string
|
|
listenPort: string
|
|
mtu: string
|
|
comment: string
|
|
address: string
|
|
enabled: boolean
|
|
showAdvanced: boolean
|
|
peerEnabled: boolean
|
|
peerPublicKey: string
|
|
peerAllowedIps: string
|
|
peerEndpoint: string
|
|
peerKeepalive: string
|
|
peerComment: string
|
|
}
|
|
|
|
export const defaultWgCreateForm = (): WgCreateFormState => ({
|
|
serverId: "",
|
|
name: "",
|
|
listenPort: "13231",
|
|
mtu: "1420",
|
|
comment: "",
|
|
address: "",
|
|
enabled: true,
|
|
showAdvanced: false,
|
|
peerEnabled: false,
|
|
peerPublicKey: "",
|
|
peerAllowedIps: "",
|
|
peerEndpoint: "",
|
|
peerKeepalive: "25",
|
|
peerComment: "",
|
|
})
|
|
|
|
type ServerOption = { id: string; name: string; host: string }
|
|
|
|
function WgCreateSheet({
|
|
open,
|
|
onOpenChange,
|
|
servers,
|
|
busy,
|
|
defaultServerId,
|
|
onSubmit,
|
|
}: {
|
|
open: boolean
|
|
onOpenChange: (v: boolean) => void
|
|
servers: ServerOption[]
|
|
busy?: boolean
|
|
defaultServerId?: string
|
|
onSubmit: (form: WgCreateFormState) => void | Promise<void>
|
|
}) {
|
|
const [form, setForm] = useState<WgCreateFormState>(defaultWgCreateForm)
|
|
const set = <K extends keyof WgCreateFormState>(k: K, v: WgCreateFormState[K]) =>
|
|
setForm((f) => ({ ...f, [k]: v }))
|
|
|
|
useEffect(() => {
|
|
if (!open) return
|
|
setForm({ ...defaultWgCreateForm(), serverId: defaultServerId ?? "" })
|
|
}, [open, defaultServerId])
|
|
|
|
const canSubmit = useMemo(() => {
|
|
return Boolean(form.serverId && form.name.trim() && form.listenPort)
|
|
}, [form.serverId, form.name, form.listenPort])
|
|
|
|
return (
|
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
<SheetContent side="right" className="w-full sm:max-w-md flex flex-col gap-0 p-0">
|
|
<SheetHeader className="px-6 pt-6 pb-4 border-b shrink-0">
|
|
<SheetTitle>Быстрый туннель WireGuard</SheetTitle>
|
|
<SheetDescription>
|
|
Создать интерфейс на выбранном MikroTik (ключи сгенерирует RouterOS)
|
|
</SheetDescription>
|
|
</SheetHeader>
|
|
|
|
<div className="flex-1 overflow-y-auto px-6 py-5 flex flex-col gap-5">
|
|
<div className="flex flex-col gap-4">
|
|
<SectionTitle>Основные</SectionTitle>
|
|
<FormField label="Сервер" required>
|
|
<select
|
|
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]"
|
|
value={form.serverId}
|
|
onChange={(e) => set("serverId", e.target.value)}
|
|
>
|
|
<option value="">Выберите сервер…</option>
|
|
{servers.map((s) => (
|
|
<option key={s.id} value={s.id}>
|
|
{s.name} ({s.host})
|
|
</option>
|
|
))}
|
|
</select>
|
|
</FormField>
|
|
<FormField label="Имя интерфейса" required hint="Например wg-msk-spb">
|
|
<Input
|
|
className="font-mono"
|
|
placeholder="wg0"
|
|
value={form.name}
|
|
onChange={(e) => set("name", e.target.value)}
|
|
/>
|
|
</FormField>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<FormField label="Listen port" required>
|
|
<Input
|
|
className="font-mono"
|
|
value={form.listenPort}
|
|
onChange={(e) => set("listenPort", e.target.value)}
|
|
/>
|
|
</FormField>
|
|
<FormField label="MTU">
|
|
<Input
|
|
className="font-mono"
|
|
value={form.mtu}
|
|
onChange={(e) => set("mtu", e.target.value)}
|
|
/>
|
|
</FormField>
|
|
</div>
|
|
<FormField label="Комментарий">
|
|
<Input
|
|
value={form.comment}
|
|
onChange={(e) => set("comment", e.target.value)}
|
|
placeholder="MSK → SPB overlay"
|
|
/>
|
|
</FormField>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium">Включён</p>
|
|
<p className="text-xs text-muted-foreground">disabled=no на роутере</p>
|
|
</div>
|
|
<FormToggle checked={form.enabled} onChange={(v) => set("enabled", v)} />
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
className="flex items-center gap-1.5 text-sm font-medium text-muted-foreground hover:text-foreground"
|
|
onClick={() => set("showAdvanced", !form.showAdvanced)}
|
|
>
|
|
{form.showAdvanced ? <ChevronDownIcon className="size-4" /> : <ChevronRightIcon className="size-4" />}
|
|
Дополнительно
|
|
</button>
|
|
|
|
{form.showAdvanced && (
|
|
<div className="flex flex-col gap-4">
|
|
<FormField label="IP на интерфейсе" hint="/ip address add, например 10.210.0.1/30">
|
|
<Input
|
|
className="font-mono"
|
|
placeholder="10.210.0.1/30"
|
|
value={form.address}
|
|
onChange={(e) => set("address", e.target.value)}
|
|
/>
|
|
</FormField>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium">Добавить первого пира</p>
|
|
<p className="text-xs text-muted-foreground">Сразу после создания интерфейса</p>
|
|
</div>
|
|
<FormToggle checked={form.peerEnabled} onChange={(v) => set("peerEnabled", v)} />
|
|
</div>
|
|
|
|
{form.peerEnabled && (
|
|
<div className="flex flex-col gap-3 rounded-lg border border-border p-3">
|
|
<FormField label="Public key пира" required>
|
|
<Input
|
|
className="font-mono text-xs"
|
|
value={form.peerPublicKey}
|
|
onChange={(e) => set("peerPublicKey", e.target.value)}
|
|
/>
|
|
</FormField>
|
|
<FormField label="Allowed IPs" required hint="Через запятую">
|
|
<Input
|
|
className="font-mono"
|
|
placeholder="10.210.0.2/32"
|
|
value={form.peerAllowedIps}
|
|
onChange={(e) => set("peerAllowedIps", e.target.value)}
|
|
/>
|
|
</FormField>
|
|
<FormField label="Endpoint" hint="host:port">
|
|
<Input
|
|
className="font-mono"
|
|
placeholder="1.2.3.4:13231"
|
|
value={form.peerEndpoint}
|
|
onChange={(e) => set("peerEndpoint", e.target.value)}
|
|
/>
|
|
</FormField>
|
|
<FormField label="Keepalive (сек)">
|
|
<Input
|
|
className="font-mono"
|
|
value={form.peerKeepalive}
|
|
onChange={(e) => set("peerKeepalive", e.target.value)}
|
|
/>
|
|
</FormField>
|
|
<FormField label="Комментарий пира">
|
|
<Input
|
|
value={form.peerComment}
|
|
onChange={(e) => set("peerComment", e.target.value)}
|
|
/>
|
|
</FormField>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<SheetFooter className="px-6 py-4 border-t shrink-0 flex-row gap-2">
|
|
<SheetClose render={<Button variant="outline" className="flex-1" disabled={busy} />}>
|
|
Отмена
|
|
</SheetClose>
|
|
<Button
|
|
className="flex-1"
|
|
disabled={!canSubmit || busy}
|
|
onClick={() => void onSubmit(form)}
|
|
>
|
|
{busy ? "Создание…" : "Создать туннель"}
|
|
</Button>
|
|
</SheetFooter>
|
|
</SheetContent>
|
|
</Sheet>
|
|
)
|
|
}
|
|
|
|
export { WgCreateSheet }
|