Files
EvoBGP/apps/web/src/components/lookup/lookup-wizard.tsx
T
Denozordec d14550cda1
CI / changes (push) Successful in 7s
CI / commitlint (push) Skipped
CI / go (push) Skipped
CI / bird2 (push) Skipped
CI / openapi (push) Successful in 46s
CI / web (push) Successful in 1m12s
CI / release (push) Successful in 4m11s
refactor(lookup): replace Alert components with LookupStatusBanner in LookupWizard
Updated the LookupWizard component to enhance readability and maintainability by replacing Alert components with a new LookupStatusBanner component. This change simplifies the status display logic for matched and unmatched results, improving the user experience during the lookup process.
2026-07-31 15:53:10 +07:00

355 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState, type ReactNode } from 'react'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { Check, Search } from 'lucide-react'
import { Button } from '@evobgp/ui/components/button'
import { LookupAddStep } from '@/components/lookup/lookup-add-step'
import { LookupMatchesGrid } from '@/components/lookup/lookup-matches-grid'
import { LookupSearchForm } from '@/components/lookup/lookup-search-form'
import { LookupStatusBanner } from '@/components/lookup/lookup-status-banner'
import { EmptyState } from '@/components/empty-state'
import { QueryState } from '@/components/query-state'
import {
Alert,
AlertDescription,
AlertTitle,
} from '@/components/reui/alert'
import {
Stepper,
StepperContent,
StepperDescription,
StepperIndicator,
StepperItem,
StepperNav,
StepperPanel,
StepperSeparator,
StepperTitle,
StepperTrigger,
} from '@/components/reui/stepper'
import {
Frame,
FrameDescription,
FrameHeader,
FramePanel,
FrameTitle,
} from '@/components/reui/frame'
import { TableSkeleton } from '@/components/skeletons'
import { sessionCanWriteModules } from '@/lib/auth'
import { authSessionQueryOptions } from '@/queries/auth'
import { directoriesCommunitiesQueryOptions } from '@/queries/directories'
import { lookupKeys, lookupQueryOptions } from '@/queries/lookup'
import { modulesListQueryOptions } from '@/queries/modules'
/**
* Lookup membership wizard — one Frame (wizard-2), all content inside.
* @see https://reui.io/preview/base/wizard-2
* @see https://reui.io/docs/components/base/stepper
* @see https://reui.io/docs/components/base/frame
* @see https://reui.io/preview/base/data-grid-filtering-2
*/
const STEP_QUERY = 1
const STEP_RESULT = 2
const STEP_ADD = 3
const STEP_DONE = 4
function WizardActions({ children }: { children: ReactNode }) {
return (
<div className="flex flex-wrap items-center justify-end gap-2 border-t pt-4">
{children}
</div>
)
}
export function LookupWizard({
q,
onSubmitQuery,
}: {
q: string
onSubmitQuery: (next: string) => void
}) {
const queryClient = useQueryClient()
const trimmed = q.trim()
const lookupQ = useQuery(lookupQueryOptions(trimmed))
const sessionQ = useQuery(authSessionQueryOptions())
const canWrite = sessionCanWriteModules(sessionQ.data)
const [step, setStep] = useState(trimmed ? STEP_RESULT : STEP_QUERY)
const [offerAdd, setOfferAdd] = useState(false)
const loadCatalog = step === STEP_ADD || step === STEP_DONE
const modulesQ = useQuery({
...modulesListQueryOptions(),
enabled: loadCatalog,
})
const communitiesQ = useQuery({
...directoriesCommunitiesQueryOptions(),
enabled: loadCatalog,
})
useEffect(() => {
setOfferAdd(false)
setStep(trimmed ? STEP_RESULT : STEP_QUERY)
}, [trimmed])
function resetToQuery() {
setOfferAdd(false)
setStep(STEP_QUERY)
onSubmitQuery('')
}
function goToAdd() {
setOfferAdd(true)
setStep(STEP_ADD)
}
async function handleAdded() {
setStep(STEP_DONE)
await queryClient.invalidateQueries({ queryKey: lookupKeys.query(trimmed) })
await lookupQ.refetch()
}
const data = lookupQ.data
const notFound = data != null && !data.matched
return (
<div className="flex flex-col gap-4 md:gap-6">
<Frame spacing="sm" className="w-full">
<FramePanel className="flex flex-col gap-6">
<FrameHeader className="px-0 py-0">
<FrameTitle>Мастер проверки</FrameTitle>
<FrameDescription>
Проверка IP / CIDR / домена в списках, затем при необходимости добавление.
</FrameDescription>
</FrameHeader>
<Stepper
value={step}
onValueChange={setStep}
className="flex flex-col gap-6"
indicators={{
completed: <Check className="size-4" />,
}}
>
<StepperNav>
<StepperItem step={STEP_QUERY} completed={step > STEP_QUERY}>
<StepperTrigger className="flex flex-col items-start gap-1 sm:flex-row sm:items-center sm:gap-2">
<StepperIndicator>{STEP_QUERY}</StepperIndicator>
<div className="flex flex-col items-start">
<StepperTitle>Запрос</StepperTitle>
<StepperDescription className="hidden sm:block">
IP, CIDR или FQDN
</StepperDescription>
</div>
</StepperTrigger>
<StepperSeparator className="max-sm:hidden" />
</StepperItem>
<StepperItem
step={STEP_RESULT}
completed={step > STEP_RESULT}
disabled={!trimmed}
>
<StepperTrigger className="flex flex-col items-start gap-1 sm:flex-row sm:items-center sm:gap-2">
<StepperIndicator>{STEP_RESULT}</StepperIndicator>
<div className="flex flex-col items-start">
<StepperTitle>Результат</StepperTitle>
<StepperDescription className="hidden sm:block">
Есть в списках?
</StepperDescription>
</div>
</StepperTrigger>
<StepperSeparator className="max-sm:hidden" />
</StepperItem>
<StepperItem
step={STEP_ADD}
completed={step > STEP_ADD}
disabled={!offerAdd && step !== STEP_ADD && step !== STEP_DONE}
>
<StepperTrigger className="flex flex-col items-start gap-1 sm:flex-row sm:items-center sm:gap-2">
<StepperIndicator>{STEP_ADD}</StepperIndicator>
<div className="flex flex-col items-start">
<StepperTitle>Добавление</StepperTitle>
<StepperDescription className="hidden sm:block">
Модуль и community
</StepperDescription>
</div>
</StepperTrigger>
<StepperSeparator className="max-sm:hidden" />
</StepperItem>
<StepperItem step={STEP_DONE} completed={step === STEP_DONE}>
<StepperTrigger className="flex flex-col items-start gap-1 sm:flex-row sm:items-center sm:gap-2">
<StepperIndicator>{STEP_DONE}</StepperIndicator>
<div className="flex flex-col items-start">
<StepperTitle>Готово</StepperTitle>
<StepperDescription className="hidden sm:block">
Подтверждение
</StepperDescription>
</div>
</StepperTrigger>
</StepperItem>
</StepperNav>
<StepperPanel>
<StepperContent value={STEP_QUERY}>
<div className="flex flex-col gap-6">
<LookupSearchForm
key={`form-${trimmed}`}
initialQuery={trimmed}
isPending={lookupQ.isFetching}
onSubmit={onSubmitQuery}
/>
{!trimmed ? (
<EmptyState
icon={<Search className="size-8" />}
title="Введите IP, CIDR или домен"
description="Например 8.8.8.8, 203.0.113.0/24 или example.com — проверка по entries и snapshots."
/>
) : null}
</div>
</StepperContent>
<StepperContent value={STEP_RESULT}>
{!trimmed ? (
<EmptyState
icon={<Search className="size-8" />}
title="Сначала выполните проверку"
description="Вернитесь к шагу «Запрос» и укажите значение."
/>
) : (
<QueryState
data={lookupQ.data}
isLoading={lookupQ.isLoading}
isError={lookupQ.isError}
error={lookupQ.error}
onRetry={() => void lookupQ.refetch()}
skeleton={<TableSkeleton rows={5} />}
>
{(result) => (
<div className="flex flex-col gap-4 md:gap-6">
<LookupSearchForm
key={`result-form-${trimmed}`}
initialQuery={trimmed}
isPending={lookupQ.isFetching}
onSubmit={onSubmitQuery}
/>
{result.matched ? (
<>
<LookupStatusBanner
status="found"
normalized={result.normalized}
matchCount={result.match_count}
/>
<LookupMatchesGrid
items={result.matches}
isLoading={lookupQ.isFetching}
/>
<WizardActions>
<Button
type="button"
variant="outline"
onClick={resetToQuery}
>
Новая проверка
</Button>
</WizardActions>
</>
) : canWrite ? (
<>
<LookupStatusBanner
status="missing"
normalized={result.normalized}
/>
<WizardActions>
<Button
type="button"
variant="outline"
onClick={resetToQuery}
>
Новая проверка
</Button>
<Button type="button" onClick={() => goToAdd()}>
Добавить
</Button>
</WizardActions>
</>
) : (
<>
<LookupStatusBanner
status="missing_denied"
normalized={result.normalized}
/>
<WizardActions>
<Button
type="button"
variant="outline"
onClick={resetToQuery}
>
Новая проверка
</Button>
</WizardActions>
</>
)}
</div>
)}
</QueryState>
)}
</StepperContent>
<StepperContent value={STEP_ADD}>
{data && notFound ? (
<LookupAddStep
data={data}
modules={modulesQ.data?.items ?? []}
communities={communitiesQ.data?.items ?? []}
onCancel={() => {
setOfferAdd(false)
setStep(STEP_RESULT)
}}
onAdded={handleAdded}
/>
) : (
<Alert variant="warning">
<AlertTitle>Добавление недоступно</AlertTitle>
<AlertDescription>
Сначала выполните проверку для значения, которого ещё нет в списках.
</AlertDescription>
</Alert>
)}
</StepperContent>
<StepperContent value={STEP_DONE}>
<div className="flex flex-col gap-4 md:gap-6">
<Alert variant="success" className="items-start py-3">
<Check aria-hidden />
<AlertTitle className="line-clamp-none">Запись добавлена</AlertTitle>
<AlertDescription>
Повторная проверка обновлена. Можно посмотреть совпадения или
начать новый запрос.
</AlertDescription>
</Alert>
{lookupQ.data?.matched ? (
<LookupMatchesGrid
items={lookupQ.data.matches}
isLoading={lookupQ.isFetching}
/>
) : null}
<WizardActions>
<Button type="button" variant="outline" onClick={resetToQuery}>
Новая проверка
</Button>
<Button type="button" onClick={() => setStep(STEP_RESULT)}>
К результату
</Button>
</WizardActions>
</div>
</StepperContent>
</StepperPanel>
</Stepper>
</FramePanel>
</Frame>
</div>
)
}