quality / commitlint (push) Skipped
quality / changes (push) Successful in 8s
quality / go (push) Skipped
quality / bird2 (push) Skipped
quality / docker-check (push) Skipped
quality / openapi (push) Successful in 22s
quality / web (push) Successful in 1m0s
CD / quality (push) Successful in 1m35s
CD / publish (push) Successful in 2m59s
Секции страниц переведены на segmented Tabs (c-tabs-9), статус грида — ToggleGroup в toolbar. Убраны календарь задач, пустые вкладки мониторинга и demo-деревья ReUI. KPI overflow как в CFDM, DetailPanel на карточке модуля, один FrameSection. Co-authored-by: Cursor <[email protected]>
290 lines
9.0 KiB
TypeScript
290 lines
9.0 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { toast } from 'sonner'
|
|
|
|
import { Button } from '@evobgp/ui/components/button'
|
|
import { Checkbox } from '@evobgp/ui/components/checkbox'
|
|
import { Field, FieldDescription, FieldLabel } from '@evobgp/ui/components/field'
|
|
import { Input } from '@evobgp/ui/components/input'
|
|
|
|
import { FormDrawer } from '@/components/form-drawer'
|
|
import { LoadingButton } from '@/components/loading-button'
|
|
import { CommunitySelect } from '@/components/modules/community-select'
|
|
import { SelectField } from '@/components/select-field'
|
|
import { dohProfileShortLabel } from '@/lib/modules/helpers'
|
|
import { dohPolicyRu, moduleTypeRu } from '@/lib/ui-labels'
|
|
import { useCreateModuleMutation } from '@/queries/modules'
|
|
import type {
|
|
BgpCommunity,
|
|
DohProfile,
|
|
DohResolverPolicy,
|
|
ModuleCreate,
|
|
ModuleRow,
|
|
ModuleType,
|
|
} from '@/types/api'
|
|
|
|
interface ModuleCreateDialogProps {
|
|
open: boolean
|
|
onOpenChange: (open: boolean) => void
|
|
communities: BgpCommunity[]
|
|
dohProfiles: DohProfile[]
|
|
onCreated?: (mod: ModuleRow) => void
|
|
}
|
|
|
|
/** @see https://reui.io/preview/base/form-7 */
|
|
/** @see https://reui.io/preview/base/sheet-8 */
|
|
|
|
const MODULE_TYPE_ITEMS: { value: ModuleType; label: string }[] = [
|
|
{ value: 'IP_RANGES', label: moduleTypeRu('IP_RANGES') },
|
|
{ value: 'AS_PREFIXES', label: moduleTypeRu('AS_PREFIXES') },
|
|
{ value: 'CDN_CIDRS', label: moduleTypeRu('CDN_CIDRS') },
|
|
{ value: 'DOMAINS', label: moduleTypeRu('DOMAINS') },
|
|
]
|
|
|
|
const DOH_POLICY_ITEMS: { value: DohResolverPolicy; label: string }[] = [
|
|
{ value: 'primary_only', label: dohPolicyRu('primary_only') },
|
|
{ value: 'failover', label: dohPolicyRu('failover') },
|
|
{ value: 'union', label: dohPolicyRu('union') },
|
|
]
|
|
|
|
export function ModuleCreateDialog({
|
|
open,
|
|
onOpenChange,
|
|
communities,
|
|
dohProfiles,
|
|
onCreated,
|
|
}: ModuleCreateDialogProps) {
|
|
const createMutation = useCreateModuleMutation()
|
|
|
|
const [type, setType] = useState<ModuleType>('IP_RANGES')
|
|
const [name, setName] = useState('')
|
|
const [enabled, setEnabled] = useState(true)
|
|
const [priority, setPriority] = useState('0')
|
|
const [refreshIntervalSec, setRefreshIntervalSec] = useState('')
|
|
const [cronExpr, setCronExpr] = useState('')
|
|
const [defaultCommunityId, setDefaultCommunityId] = useState<string | null>(null)
|
|
const [dohResolverPolicy, setDohResolverPolicy] = useState<DohResolverPolicy>('primary_only')
|
|
const [dohProfileIds, setDohProfileIds] = useState<string[]>([])
|
|
|
|
const isDomains = type === 'DOMAINS'
|
|
|
|
useEffect(() => {
|
|
if (!open) return
|
|
setType('IP_RANGES')
|
|
setName('')
|
|
setEnabled(true)
|
|
setPriority('0')
|
|
setRefreshIntervalSec('')
|
|
setCronExpr('')
|
|
setDefaultCommunityId(null)
|
|
setDohResolverPolicy('primary_only')
|
|
setDohProfileIds([])
|
|
}, [open])
|
|
|
|
function toggleDohProfile(id: string, checked: boolean) {
|
|
setDohProfileIds((prev) => {
|
|
if (checked) {
|
|
if (prev.includes(id)) return prev
|
|
return [...prev, id]
|
|
}
|
|
return prev.filter((x) => x !== id)
|
|
})
|
|
}
|
|
|
|
async function save() {
|
|
const trimmedName = name.trim()
|
|
if (!trimmedName) {
|
|
toast.error('Укажите название модуля')
|
|
return
|
|
}
|
|
|
|
const priorityNum = Number(priority)
|
|
if (!Number.isFinite(priorityNum) || !Number.isInteger(priorityNum)) {
|
|
toast.error('Приоритет должен быть целым числом')
|
|
return
|
|
}
|
|
|
|
let refresh: number | undefined
|
|
if (refreshIntervalSec.trim() !== '') {
|
|
const n = Number(refreshIntervalSec)
|
|
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 0) {
|
|
toast.error('Интервал обновления должен быть целым числом ≥ 0')
|
|
return
|
|
}
|
|
refresh = n
|
|
}
|
|
|
|
const body: ModuleCreate = {
|
|
type,
|
|
name: trimmedName,
|
|
enabled,
|
|
priority: priorityNum,
|
|
}
|
|
if (refresh !== undefined) {
|
|
body.refresh_interval_sec = refresh
|
|
}
|
|
const cron = cronExpr.trim()
|
|
if (cron) {
|
|
body.cron_expr = cron
|
|
}
|
|
if (defaultCommunityId) {
|
|
body.default_community_id = defaultCommunityId
|
|
}
|
|
if (isDomains) {
|
|
body.doh_resolver_policy = dohResolverPolicy
|
|
body.doh_profile_ids = dohProfileIds
|
|
}
|
|
|
|
try {
|
|
const created = await createMutation.mutateAsync(body)
|
|
onOpenChange(false)
|
|
onCreated?.(created)
|
|
} catch {
|
|
// toast in mutation
|
|
}
|
|
}
|
|
|
|
return (
|
|
<FormDrawer
|
|
open={open}
|
|
onOpenChange={onOpenChange}
|
|
title="Новый модуль"
|
|
description="Тип задаётся один раз. Записи добавляются на карточке модуля."
|
|
className="sm:max-w-md"
|
|
footer={
|
|
<>
|
|
<Button variant="outline" type="button" onClick={() => onOpenChange(false)}>
|
|
Отмена
|
|
</Button>
|
|
<LoadingButton type="button" loading={createMutation.isPending} onClick={() => void save()}>
|
|
Создать
|
|
</LoadingButton>
|
|
</>
|
|
}
|
|
>
|
|
<SelectField
|
|
id="mod-create-type"
|
|
label="Тип"
|
|
items={MODULE_TYPE_ITEMS}
|
|
value={type}
|
|
onValueChange={(v) => {
|
|
if (v) setType(v)
|
|
}}
|
|
/>
|
|
|
|
<Field>
|
|
<FieldLabel htmlFor="mod-create-name">Название</FieldLabel>
|
|
<Input
|
|
id="mod-create-name"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="Имя модуля"
|
|
/>
|
|
</Field>
|
|
|
|
<Field orientation="horizontal">
|
|
<div className="grid min-w-0 flex-1 gap-1 pr-2">
|
|
<FieldLabel htmlFor="mod-create-enabled">Включён</FieldLabel>
|
|
<FieldDescription>
|
|
Выключенный модуль не участвует в обновлении и применении.
|
|
</FieldDescription>
|
|
</div>
|
|
<Checkbox
|
|
id="mod-create-enabled"
|
|
checked={enabled}
|
|
onCheckedChange={(v) => setEnabled(v === true)}
|
|
/>
|
|
</Field>
|
|
|
|
<Field>
|
|
<FieldLabel htmlFor="mod-create-priority">Приоритет</FieldLabel>
|
|
<Input
|
|
id="mod-create-priority"
|
|
type="number"
|
|
value={priority}
|
|
onChange={(e) => setPriority(e.target.value)}
|
|
/>
|
|
</Field>
|
|
|
|
<Field>
|
|
<FieldLabel htmlFor="mod-create-interval">Интервал обновления (сек)</FieldLabel>
|
|
<Input
|
|
id="mod-create-interval"
|
|
type="number"
|
|
min={0}
|
|
placeholder="пусто = по умолчанию"
|
|
value={refreshIntervalSec}
|
|
onChange={(e) => setRefreshIntervalSec(e.target.value)}
|
|
/>
|
|
</Field>
|
|
|
|
<Field>
|
|
<FieldLabel htmlFor="mod-create-cron">Cron (опционально)</FieldLabel>
|
|
<Input
|
|
id="mod-create-cron"
|
|
placeholder="0 * * * *"
|
|
value={cronExpr}
|
|
onChange={(e) => setCronExpr(e.target.value)}
|
|
/>
|
|
</Field>
|
|
|
|
<CommunitySelect
|
|
id="mod-create-community"
|
|
label="Community по умолчанию"
|
|
value={defaultCommunityId}
|
|
onValueChange={setDefaultCommunityId}
|
|
communities={communities}
|
|
nullable
|
|
/>
|
|
|
|
{isDomains ? (
|
|
<>
|
|
<SelectField
|
|
id="mod-create-doh-policy"
|
|
label="Политика DoH"
|
|
items={DOH_POLICY_ITEMS}
|
|
value={dohResolverPolicy}
|
|
onValueChange={(v) => {
|
|
if (v) setDohResolverPolicy(v)
|
|
}}
|
|
/>
|
|
|
|
<Field>
|
|
<FieldLabel>DoH профили</FieldLabel>
|
|
{dohProfiles.length === 0 ? (
|
|
<p className="text-muted-foreground text-sm">Нет профилей в справочнике</p>
|
|
) : (
|
|
<div className="flex flex-col gap-2 rounded-lg border border-border p-3">
|
|
{dohProfiles.map((p) => {
|
|
const checked = dohProfileIds.includes(p.id)
|
|
return (
|
|
<label
|
|
key={p.id}
|
|
htmlFor={`mod-create-doh-${p.id}`}
|
|
className="flex cursor-pointer items-start gap-3"
|
|
>
|
|
<Checkbox
|
|
id={`mod-create-doh-${p.id}`}
|
|
checked={checked}
|
|
onCheckedChange={(v) => toggleDohProfile(p.id, v === true)}
|
|
className="mt-0.5"
|
|
/>
|
|
<span className="flex min-w-0 flex-col gap-0.5">
|
|
<span className="text-sm font-medium">
|
|
{dohProfileShortLabel(p.id, dohProfiles)}
|
|
</span>
|
|
<span className="text-muted-foreground truncate text-xs" title={p.url}>
|
|
{p.url}
|
|
</span>
|
|
</span>
|
|
</label>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
</Field>
|
|
</>
|
|
) : null}
|
|
</FormDrawer>
|
|
)
|
|
}
|