fix(ui): подтверждать сохранение пользователей IPsec

This commit is contained in:
Denozordec committed 2026-09-29 19:15:15 +07:00
1 parent f9413d5100
commit 86ecf764ff
7 files changed
+135 -25

No files matched your search

+42 -12
View File
@@ -1,9 +1,10 @@
"use client"
import { useEffect, useMemo, useState } from "react"
import { useEffect, useMemo, useRef, useState } from "react"
import { IPSEC_MIN_PASSPHRASE, type IpsecClientDto } from "@mmapp/contracts/ipsec"
import { FormField, FormToggle, SectionTitle } from "@/components/form-kit"
import { FormField, SectionTitle } from "@/components/form-kit"
import { Button } from "@/components/ui/button"
import { Switch } from "@/components/ui/switch"
import { Input } from "@/components/ui/input"
import {
Sheet, SheetContent, SheetHeader, SheetTitle,
@@ -63,11 +64,19 @@ function IpsecUserSheet({
onSubmit: (form: IpsecUserFormState) => void | Promise<void>
}) {
const [form, setForm] = useState<IpsecUserFormState>(defaultIpsecUserForm)
const lock = useRef(false)
const opened = useRef(false)
const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)
const pending = busy || submitting
const set = <K extends keyof IpsecUserFormState>(k: K, v: IpsecUserFormState[K]) =>
setForm((f) => ({ ...f, [k]: v }))
useEffect(() => {
if (!open) return
if (!open) { opened.current = false; return }
if (opened.current) return
opened.current = true
queueMicrotask(() => setError(null))
if (editing) {
const edit = editing
queueMicrotask(() => setForm({
@@ -89,7 +98,7 @@ function IpsecUserSheet({
const canSubmit = useMemo(() => {
if (!editing && !form.serverId) return false
if (!form.name.trim()) return false
if (!editing && form.authMethod === "pre-shared-key" && form.psk.trim().length < 8) return false
if (form.authMethod === "pre-shared-key" && (!editing || form.psk.trim()) && form.psk.trim().length < 8) return false
if (form.useStaticIp && form.staticIp.trim() && !/^\d{1,3}(?:\.\d{1,3}){3}$/.test(form.staticIp.trim())) return false
// RouterOS отклоняет export-passphrase короче 8 символов
const passphrase = form.passphrase.trim()
@@ -97,9 +106,19 @@ function IpsecUserSheet({
return true
}, [form, editing])
const submit = async () => {
if (!canSubmit || busy || lock.current) return
lock.current = true
setSubmitting(true)
setError(null)
try { await onSubmit(form) }
catch (error) { setError(error instanceof Error ? error.message : "Не удалось сохранить клиента") }
finally { lock.current = false; setSubmitting(false) }
}
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent side="right" className="w-full sm:max-w-md flex flex-col gap-0 p-0">
<Sheet open={open} onOpenChange={value => { if (!lock.current && !busy) onOpenChange(value) }}>
<SheetContent showCloseButton={!pending} 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>{editing ? `Клиент «${editing.name}»` : "Новый клиент IKEv2"}</SheetTitle>
<SheetDescription>
@@ -110,14 +129,16 @@ function IpsecUserSheet({
</SheetHeader>
<div className="flex-1 overflow-y-auto px-6 py-5 flex flex-col gap-5">
<fieldset disabled={pending} className="flex flex-col gap-5">
<div className="flex flex-col gap-4">
<SectionTitle>Основные</SectionTitle>
{!editing ? (
<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]"
aria-label="Сервер"
value={form.serverId}
onChange={(e) => set("serverId", e.target.value)}
onChange={(e) => setForm(f => ({ ...f, serverId: e.target.value, peerName: "" }))}
>
<option value="">Выберите сервер…</option>
{servers.map((s) => (
@@ -132,6 +153,7 @@ function IpsecUserSheet({
<FormField label="Peer (сервер)" hint="К какому peer привязать identity">
<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]"
aria-label="Peer (сервер)"
value={form.peerName}
onChange={(e) => set("peerName", e.target.value)}
>
@@ -149,6 +171,7 @@ function IpsecUserSheet({
<FormField label="Имя клиента" required hint={editing ? undefined : "CN сертификата и отображаемое имя"}>
<Input
placeholder="alice"
aria-label="Имя клиента"
value={form.name}
onChange={(e) => set("name", e.target.value)}
/>
@@ -187,6 +210,7 @@ function IpsecUserSheet({
<Input
className="font-mono"
type="password"
aria-label="Secret (PSK)"
value={form.psk}
onChange={(e) => set("psk", e.target.value)}
/>
@@ -196,6 +220,7 @@ function IpsecUserSheet({
<Input
className="font-mono"
placeholder="alice"
aria-label="Remote ID"
value={form.remoteId}
onChange={(e) => set("remoteId", e.target.value)}
/>
@@ -215,6 +240,7 @@ function IpsecUserSheet({
<Input
className="font-mono"
placeholder="например MySecret123"
aria-label="Пароль архива .p12"
value={form.passphrase}
onChange={(e) => set("passphrase", e.target.value)}
/>
@@ -233,31 +259,35 @@ function IpsecUserSheet({
{" "}Статический IP создаёт персональную identity на peer.
</p>
</div>
<FormToggle checked={form.useStaticIp} onChange={(v) => set("useStaticIp", v)} />
<Switch aria-label="Статический IP" disabled={pending} checked={form.useStaticIp} onCheckedChange={(v) => set("useStaticIp", v)} />
</div>
{form.useStaticIp && (
<FormField label="IP клиента" hint="Например 10.77.0.10">
<Input
className="font-mono"
placeholder={freeIpHint ?? "10.77.0.10"}
aria-label="IP клиента"
value={form.staticIp}
onChange={(e) => set("staticIp", e.target.value)}
/>
</FormField>
)}
</div>
</fieldset>
{pending && <p role="status" className="text-sm text-muted-foreground">Ожидание результата сохранения…</p>}
{error && <p role="alert" className="text-sm text-destructive">{error}</p>}
</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 render={<Button variant="outline" className="flex-1" disabled={pending} />}>
Отмена
</SheetClose>
<Button
className="flex-1"
disabled={!canSubmit || busy}
onClick={() => void onSubmit(form)}
disabled={!canSubmit || pending}
onClick={() => void submit()}
>
{busy ? (editing ? "Сохранение…" : "Создание…") : editing ? "Сохранить" : "Создать клиента"}
{pending ? (editing ? "Сохранение…" : "Создание…") : editing ? "Сохранить" : "Создать клиента"}
</Button>
</SheetFooter>
</SheetContent>