feat(directories): add create dialogs for communities and DoH profiles
CI / changes (push) Successful in 6s
CI / commitlint (push) Skipped
CI / go (push) Skipped
CI / bird2 (push) Skipped
CI / openapi (push) Successful in 41s
CI / web (push) Successful in 1m13s
CI / release (push) Successful in 4m35s

На странице Справочники добавлены кнопки «Добавить» и FormDrawer
для POST /v1/communities и /v1/doh-profiles. Доступ ограничен
bgp:directories:write (sessionCanWriteDirectories).

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-08-12 12:24:10 +07:00
co-authored by Cursor
parent 821f342476
commit fc4f234cc5
5 changed files with 314 additions and 11 deletions
@@ -0,0 +1,84 @@
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { Button } from '@evobgp/ui/components/button'
import { Input } from '@evobgp/ui/components/input'
import { Label } from '@evobgp/ui/components/label'
import { FormDrawer } from '@/components/form-drawer'
import { LoadingButton } from '@/components/loading-button'
import { useCreateCommunityMutation } from '@/queries/directories'
import type { BgpCommunityCreate } from '@/types/api'
interface CommunityCreateDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
}
export function CommunityCreateDialog({ open, onOpenChange }: CommunityCreateDialogProps) {
const createMutation = useCreateCommunityMutation()
const [community, setCommunity] = useState('')
const [title, setTitle] = useState('')
useEffect(() => {
if (!open) return
setCommunity('')
setTitle('')
}, [open])
async function save() {
const value = community.trim()
if (!value) {
toast.error('Укажите community')
return
}
const body: BgpCommunityCreate = { community: value }
const t = title.trim()
if (t) body.title = t
try {
await createMutation.mutateAsync(body)
onOpenChange(false)
} catch {
// toast in mutation
}
}
return (
<FormDrawer
open={open}
onOpenChange={onOpenChange}
title="Новое сообщество BGP"
description="Тег для префиксов в фильтрах BIRD"
className="sm:max-w-sm"
footer={
<>
<Button variant="outline" type="button" onClick={() => onOpenChange(false)}>
Отмена
</Button>
<LoadingButton type="button" loading={createMutation.isPending} onClick={() => void save()}>
Создать
</LoadingButton>
</>
}
>
<div className="flex flex-col gap-2">
<Label htmlFor="comm-value">Community</Label>
<Input
id="comm-value"
placeholder="65000:100"
value={community}
onChange={(e) => setCommunity(e.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="comm-title">Название (опционально)</Label>
<Input
id="comm-title"
placeholder="Отображаемое имя"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
</div>
</FormDrawer>
)
}
@@ -0,0 +1,118 @@
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { Button } from '@evobgp/ui/components/button'
import { Input } from '@evobgp/ui/components/input'
import { Label } from '@evobgp/ui/components/label'
import { FormDrawer } from '@/components/form-drawer'
import { LoadingButton } from '@/components/loading-button'
import { useCreateDohProfileMutation } from '@/queries/directories'
import type { DohProfileCreate } from '@/types/api'
interface DohProfileCreateDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
}
export function DohProfileCreateDialog({ open, onOpenChange }: DohProfileCreateDialogProps) {
const createMutation = useCreateDohProfileMutation()
const [name, setName] = useState('')
const [url, setUrl] = useState('')
const [timeoutMs, setTimeoutMs] = useState('')
useEffect(() => {
if (!open) return
setName('')
setUrl('')
setTimeoutMs('')
}, [open])
async function save() {
const trimmedUrl = url.trim()
if (!trimmedUrl) {
toast.error('Укажите URL DoH')
return
}
let urlOk = true
try {
new URL(trimmedUrl)
} catch {
urlOk = false
}
if (!urlOk) {
toast.error('Некорректный URL')
return
}
const body: DohProfileCreate = { url: trimmedUrl }
const n = name.trim()
if (n) body.name = n
if (timeoutMs.trim() !== '') {
const ms = Number(timeoutMs)
if (!Number.isFinite(ms) || !Number.isInteger(ms) || ms <= 0) {
toast.error('Timeout должен быть целым числом > 0')
return
}
body.timeout_ms = ms
}
try {
await createMutation.mutateAsync(body)
onOpenChange(false)
} catch {
// toast in mutation
}
}
return (
<FormDrawer
open={open}
onOpenChange={onOpenChange}
title="Новый DoH-профиль"
description="Резолвер DNS-over-HTTPS для доменных модулей"
className="sm:max-w-sm"
footer={
<>
<Button variant="outline" type="button" onClick={() => onOpenChange(false)}>
Отмена
</Button>
<LoadingButton type="button" loading={createMutation.isPending} onClick={() => void save()}>
Создать
</LoadingButton>
</>
}
>
<div className="flex flex-col gap-2">
<Label htmlFor="doh-name">Имя (опционально)</Label>
<Input
id="doh-name"
placeholder="Control D / AdGuard"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="doh-url">URL</Label>
<Input
id="doh-url"
type="url"
placeholder="https://dns.example/dns-query"
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="doh-timeout">Timeout, мс (опционально)</Label>
<Input
id="doh-timeout"
type="number"
min={1}
placeholder="5000"
value={timeoutMs}
onChange={(e) => setTimeoutMs(e.target.value)}
/>
</div>
</FormDrawer>
)
}