Refactored the LookupAddStep, LookupMatchesGrid, LookupSearchForm, and LookupWizard components to eliminate Frame components, replacing them with simpler div structures for improved readability and maintainability. Updated comments to reflect the new layout and functionality, aligning with the wizard-2 design pattern.
191 lines
5.4 KiB
TypeScript
191 lines
5.4 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react'
|
|
import { Link } from '@tanstack/react-router'
|
|
import { toast } from 'sonner'
|
|
|
|
import { Button } from '@evobgp/ui/components/button'
|
|
import { Field, FieldLabel } from '@evobgp/ui/components/field'
|
|
|
|
import { CommunitySelect } from '@/components/modules/community-select'
|
|
import { LoadingButton } from '@/components/loading-button'
|
|
import {
|
|
Alert,
|
|
AlertDescription,
|
|
AlertTitle,
|
|
} from '@/components/reui/alert'
|
|
import { SelectMenu } from '@/components/select-field'
|
|
import { ApiError, apiMutate } from '@/lib/api-client'
|
|
import type {
|
|
BgpCommunity,
|
|
LookupQueryKind,
|
|
LookupResponse,
|
|
ModuleRow,
|
|
} from '@/types/api'
|
|
|
|
/**
|
|
* Lookup wizard step 3 — module + community (bare content for single Frame).
|
|
* @see https://reui.io/preview/base/wizard-2
|
|
*/
|
|
|
|
function hostPrefixFromIp(ip: string): string {
|
|
return ip.includes(':') ? `${ip}/128` : `${ip}/32`
|
|
}
|
|
|
|
function moduleTypeForKind(kind: LookupQueryKind): ModuleRow['type'] {
|
|
return kind === 'domain' ? 'DOMAINS' : 'IP_RANGES'
|
|
}
|
|
|
|
export function LookupAddStep({
|
|
data,
|
|
modules,
|
|
communities,
|
|
onCancel,
|
|
onAdded,
|
|
}: {
|
|
data: LookupResponse
|
|
modules: ModuleRow[]
|
|
communities: BgpCommunity[]
|
|
onCancel: () => void
|
|
onAdded: () => void | Promise<void>
|
|
}) {
|
|
const wantedType = moduleTypeForKind(data.query_kind)
|
|
const eligible = useMemo(
|
|
() => modules.filter((m) => m.type === wantedType),
|
|
[modules, wantedType],
|
|
)
|
|
|
|
const [moduleId, setModuleId] = useState('')
|
|
const [communityId, setCommunityId] = useState<string | null>(null)
|
|
const [saving, setSaving] = useState(false)
|
|
|
|
useEffect(() => {
|
|
if (eligible.length === 0) {
|
|
setModuleId('')
|
|
return
|
|
}
|
|
setModuleId((prev) =>
|
|
prev && eligible.some((m) => m.id === prev) ? prev : eligible[0]!.id,
|
|
)
|
|
}, [eligible])
|
|
|
|
useEffect(() => {
|
|
const mod = eligible.find((m) => m.id === moduleId)
|
|
if (!mod) {
|
|
setCommunityId(null)
|
|
return
|
|
}
|
|
setCommunityId(mod.default_community_id ?? null)
|
|
}, [moduleId, eligible])
|
|
|
|
const moduleItems = useMemo(
|
|
() =>
|
|
eligible.map((m) => ({
|
|
value: m.id,
|
|
label: m.name,
|
|
})),
|
|
[eligible],
|
|
)
|
|
|
|
async function handleAdd() {
|
|
if (!moduleId) {
|
|
toast.error('Выберите модуль')
|
|
return
|
|
}
|
|
if (data.query_kind !== 'domain' && !communityId) {
|
|
toast.error('Укажите community')
|
|
return
|
|
}
|
|
|
|
setSaving(true)
|
|
try {
|
|
if (data.query_kind === 'domain') {
|
|
await apiMutate(`/v1/modules/${moduleId}/domain-entries`, 'POST', {
|
|
fqdn: data.normalized,
|
|
community_id: communityId,
|
|
})
|
|
toast.success('Домен добавлен')
|
|
} else {
|
|
const prefix =
|
|
data.query_kind === 'cidr'
|
|
? data.normalized
|
|
: hostPrefixFromIp(data.normalized)
|
|
await apiMutate(`/v1/modules/${moduleId}/ip-range-entries`, 'POST', {
|
|
prefix,
|
|
community_id: communityId,
|
|
})
|
|
toast.success('Префикс добавлен')
|
|
}
|
|
await onAdded()
|
|
} catch (e) {
|
|
toast.error(e instanceof ApiError ? e.message : String(e))
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
if (eligible.length === 0) {
|
|
return (
|
|
<Alert variant="warning">
|
|
<AlertTitle>Нет подходящего модуля</AlertTitle>
|
|
<AlertDescription>
|
|
Создайте модуль типа {wantedType}, затем повторите добавление.{' '}
|
|
<Button variant="link" size="sm" className="h-auto p-0" render={<Link to="/modules/new" />}>
|
|
Перейти к модулям
|
|
</Button>
|
|
</AlertDescription>
|
|
</Alert>
|
|
)
|
|
}
|
|
|
|
const valueLabel =
|
|
data.query_kind === 'domain'
|
|
? data.normalized
|
|
: data.query_kind === 'cidr'
|
|
? data.normalized
|
|
: hostPrefixFromIp(data.normalized)
|
|
|
|
return (
|
|
<div className="flex flex-col gap-6">
|
|
<div className="flex flex-col gap-1">
|
|
<h3 className="text-sm font-semibold">Добавить в списки</h3>
|
|
<p className="text-muted-foreground text-sm">
|
|
«{valueLabel}» отсутствует в списках. Выберите модуль и community.
|
|
</p>
|
|
</div>
|
|
<div className="flex flex-col gap-4">
|
|
<Field>
|
|
<FieldLabel htmlFor="lookup-add-module">Модуль ({wantedType})</FieldLabel>
|
|
<SelectMenu
|
|
id="lookup-add-module"
|
|
items={moduleItems}
|
|
value={moduleId}
|
|
placeholder="Выберите модуль"
|
|
onValueChange={(v) => {
|
|
if (v) setModuleId(v)
|
|
}}
|
|
/>
|
|
</Field>
|
|
<CommunitySelect
|
|
id="lookup-add-comm"
|
|
label="Community"
|
|
value={communityId}
|
|
onValueChange={setCommunityId}
|
|
communities={communities}
|
|
nullable={data.query_kind === 'domain'}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-wrap items-center justify-end gap-2 border-t pt-4">
|
|
<Button type="button" variant="outline" onClick={onCancel}>
|
|
Назад
|
|
</Button>
|
|
<LoadingButton
|
|
loading={saving}
|
|
onClick={() => void handleAdd()}
|
|
disabled={!moduleId || (data.query_kind !== 'domain' && !communityId)}
|
|
>
|
|
Добавить
|
|
</LoadingButton>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|