Files
EvoBGP/apps/web/src/components/lookup/lookup-add-step.tsx
T
Denozordec 632adaa63f
CI / changes (push) Successful in 6s
CI / commitlint (push) Skipped
CI / go (push) Skipped
CI / bird2 (push) Skipped
CI / openapi (push) Successful in 46s
CI / web (push) Successful in 1m23s
CI / release (push) Successful in 4m16s
refactor(lookup): simplify layout by removing Frame components
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.
2026-07-31 15:21:16 +07:00

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>
)
}