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.
355 lines
14 KiB
TypeScript
355 lines
14 KiB
TypeScript
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>
|
||
)
|
||
}
|