Compare commits

...
2 Commits
Author SHA1 Message Date
Denozordec d14550cda1 refactor(lookup): replace Alert components with LookupStatusBanner in LookupWizard
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
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
Denozordec 632adaa63f refactor(lookup): simplify layout by removing Frame components
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
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
6 changed files with 205 additions and 168 deletions
@@ -12,14 +12,6 @@ import {
AlertDescription, AlertDescription,
AlertTitle, AlertTitle,
} from '@/components/reui/alert' } from '@/components/reui/alert'
import {
Frame,
FrameDescription,
FrameFooter,
FrameHeader,
FramePanel,
FrameTitle,
} from '@/components/reui/frame'
import { SelectMenu } from '@/components/select-field' import { SelectMenu } from '@/components/select-field'
import { ApiError, apiMutate } from '@/lib/api-client' import { ApiError, apiMutate } from '@/lib/api-client'
import type { import type {
@@ -30,9 +22,8 @@ import type {
} from '@/types/api' } from '@/types/api'
/** /**
* Lookup wizard step 3 — choose module + community and create entry. * Lookup wizard step 3 — module + community (bare content for single Frame).
* @see https://reui.io/preview/base/wizard-2 * @see https://reui.io/preview/base/wizard-2
* @see https://reui.io/docs/components/base/frame
*/ */
function hostPrefixFromIp(ip: string): string { function hostPrefixFromIp(ip: string): string {
@@ -153,14 +144,14 @@ export function LookupAddStep({
: hostPrefixFromIp(data.normalized) : hostPrefixFromIp(data.normalized)
return ( return (
<Frame spacing="sm" className="w-full"> <div className="flex flex-col gap-6">
<FrameHeader> <div className="flex flex-col gap-1">
<FrameTitle>Добавить в списки</FrameTitle> <h3 className="text-sm font-semibold">Добавить в списки</h3>
<FrameDescription> <p className="text-muted-foreground text-sm">
«{valueLabel}» отсутствует в списках. Выберите модуль и community. «{valueLabel}» отсутствует в списках. Выберите модуль и community.
</FrameDescription> </p>
</FrameHeader> </div>
<FramePanel className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<Field> <Field>
<FieldLabel htmlFor="lookup-add-module">Модуль ({wantedType})</FieldLabel> <FieldLabel htmlFor="lookup-add-module">Модуль ({wantedType})</FieldLabel>
<SelectMenu <SelectMenu
@@ -181,8 +172,8 @@ export function LookupAddStep({
communities={communities} communities={communities}
nullable={data.query_kind === 'domain'} nullable={data.query_kind === 'domain'}
/> />
</FramePanel> </div>
<FrameFooter className="flex flex-wrap items-center justify-end gap-2"> <div className="flex flex-wrap items-center justify-end gap-2 border-t pt-4">
<Button type="button" variant="outline" onClick={onCancel}> <Button type="button" variant="outline" onClick={onCancel}>
Назад Назад
</Button> </Button>
@@ -193,7 +184,7 @@ export function LookupAddStep({
> >
Добавить Добавить
</LoadingButton> </LoadingButton>
</FrameFooter> </div>
</Frame> </div>
) )
} }
@@ -4,7 +4,6 @@ import { useNavigate } from '@tanstack/react-router'
import { CategoryBadge } from '@/components/category-badge' import { CategoryBadge } from '@/components/category-badge'
import { DataGridPrimaryCell } from '@/components/data-grid-cell' import { DataGridPrimaryCell } from '@/components/data-grid-cell'
import { FrameDataGrid } from '@/components/reui-kit'
import { DataGridSection } from '@/components/data-grid-shell' import { DataGridSection } from '@/components/data-grid-shell'
import { Badge } from '@/components/reui/badge' import { Badge } from '@/components/reui/badge'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header' import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
@@ -12,7 +11,8 @@ import { useClientDataGrid } from '@/hooks/use-client-data-grid'
import type { LookupMatch } from '@/types/api' import type { LookupMatch } from '@/types/api'
/** /**
* Lookup matches grid — data-grid-filtering-2 pattern. * Lookup matches grid — bare section for wizard-2 single Frame.
* @see https://reui.io/preview/base/wizard-2
* @see https://reui.io/preview/base/data-grid-filtering-2 * @see https://reui.io/preview/base/data-grid-filtering-2
* @see https://reui.io/docs/components/base/badge * @see https://reui.io/docs/components/base/badge
*/ */
@@ -113,10 +113,13 @@ export function LookupMatchesGrid({
}) })
return ( return (
<FrameDataGrid <div className="flex flex-col gap-3">
title="Совпадения" <div className="flex flex-col gap-px">
description="Entries и snapshots · клик по строке открывает модуль" <h3 className="text-sm font-semibold">Совпадения</h3>
> <p className="text-muted-foreground text-sm">
Entries и snapshots · клик по строке открывает модуль
</p>
</div>
<DataGridSection <DataGridSection
table={table} table={table}
recordCount={filteredCount} recordCount={filteredCount}
@@ -129,6 +132,6 @@ export function LookupMatchesGrid({
void navigate({ to: '/modules/$moduleId', params: { moduleId: row.module_id } }) void navigate({ to: '/modules/$moduleId', params: { moduleId: row.module_id } })
} }
/> />
</FrameDataGrid> </div>
) )
} }
@@ -8,17 +8,10 @@ import {
InputGroupAddon, InputGroupAddon,
InputGroupInput, InputGroupInput,
} from '@evobgp/ui/components/input-group' } from '@evobgp/ui/components/input-group'
import {
Frame,
FrameDescription,
FrameHeader,
FramePanel,
FrameTitle,
} from '@/components/reui/frame'
/** /**
* Lookup search form — Frame + InputGroup (form-7 pattern). * Lookup search fields — bare form for wizard-2 single-Frame surface.
* @see https://reui.io/preview/base/form-7 * @see https://reui.io/preview/base/wizard-2
* @see https://reui.io/docs/components/base/frame * @see https://reui.io/docs/components/base/frame
*/ */
export function LookupSearchForm({ export function LookupSearchForm({
@@ -40,37 +33,27 @@ export function LookupSearchForm({
} }
return ( return (
<Frame spacing="sm" className="w-full"> <form onSubmit={handleSubmit} className="flex flex-col gap-4 sm:flex-row sm:items-end">
<FrameHeader> <Field className="min-w-0 flex-1">
<FrameTitle>Проверка списка</FrameTitle> <FieldLabel htmlFor="lookup-q">IP, CIDR или домен</FieldLabel>
<FrameDescription> <InputGroup>
IP, CIDR или FQDN поиск в entries и материализованных snapshots с community. <InputGroupAddon align="inline-start">
</FrameDescription> <Search aria-hidden />
</FrameHeader> </InputGroupAddon>
<FramePanel> <InputGroupInput
<form onSubmit={handleSubmit} className="flex flex-col gap-4 sm:flex-row sm:items-end"> id="lookup-q"
<Field className="min-w-0 flex-1"> name="q"
<FieldLabel htmlFor="lookup-q">IP, CIDR или домен</FieldLabel> value={value}
<InputGroup> onChange={(e) => setValue(e.target.value)}
<InputGroupAddon align="inline-start"> placeholder="8.8.8.8, 203.0.113.0/24 или example.com"
<Search aria-hidden /> autoComplete="off"
</InputGroupAddon> autoFocus
<InputGroupInput />
id="lookup-q" </InputGroup>
name="q" </Field>
value={value} <Button type="submit" disabled={isPending || !value.trim()}>
onChange={(e) => setValue(e.target.value)} Проверить
placeholder="8.8.8.8, 203.0.113.0/24 или example.com" </Button>
autoComplete="off" </form>
autoFocus
/>
</InputGroup>
</Field>
<Button type="submit" disabled={isPending || !value.trim()}>
Проверить
</Button>
</form>
</FramePanel>
</Frame>
) )
} }
@@ -0,0 +1,74 @@
import { CircleAlert, CircleCheck, ShieldAlert } from 'lucide-react'
import { Badge } from '@/components/reui/badge'
import {
Alert,
AlertDescription,
AlertTitle,
} from '@/components/reui/alert'
/**
* Lookup result status plaque — ReUI Alert + Badge.
* @see https://reui.io/docs/components/base/alert
* @see https://reui.io/preview/base/components/c-alert-6
* @see https://reui.io/docs/components/base/badge
*/
export function LookupStatusBanner({
status,
normalized,
matchCount = 0,
}: {
status: 'found' | 'missing' | 'missing_denied'
normalized: string
matchCount?: number
}) {
if (status === 'found') {
return (
<Alert variant="success" className="items-start py-3">
<CircleCheck aria-hidden />
<AlertTitle className="line-clamp-none flex flex-wrap items-center gap-2">
Найдено
<Badge variant="success-light" size="sm" radius="full">
в списках
</Badge>
</AlertTitle>
<AlertDescription>
«{normalized}» {matchCount} совп. в entries / snapshots.
</AlertDescription>
</Alert>
)
}
if (status === 'missing_denied') {
return (
<Alert variant="warning" className="items-start py-3">
<ShieldAlert aria-hidden />
<AlertTitle className="line-clamp-none flex flex-wrap items-center gap-2">
Не найдено
<Badge variant="warning-light" size="sm" radius="full">
нет прав
</Badge>
</AlertTitle>
<AlertDescription>
«{normalized}» отсутствует в списках. Для добавления нужно право{' '}
<span className="font-medium text-foreground">bgp:modules:write</span>.
</AlertDescription>
</Alert>
)
}
return (
<Alert variant="warning" className="items-start py-3">
<CircleAlert aria-hidden />
<AlertTitle className="line-clamp-none flex flex-wrap items-center gap-2">
Не найдено
<Badge variant="warning-light" size="sm" radius="full">
можно добавить
</Badge>
</AlertTitle>
<AlertDescription>
«{normalized}» отсутствует в entries и snapshots. Добавить запись в модуль?
</AlertDescription>
</Alert>
)
}
@@ -1,12 +1,13 @@
import { useEffect, useState } from 'react' import { useEffect, useState, type ReactNode } from 'react'
import { useQuery, useQueryClient } from '@tanstack/react-query' import { useQuery, useQueryClient } from '@tanstack/react-query'
import { Check, Plus, Search, ShieldAlert } from 'lucide-react' import { Check, Search } from 'lucide-react'
import { Button } from '@evobgp/ui/components/button' import { Button } from '@evobgp/ui/components/button'
import { LookupAddStep } from '@/components/lookup/lookup-add-step' import { LookupAddStep } from '@/components/lookup/lookup-add-step'
import { LookupMatchesGrid } from '@/components/lookup/lookup-matches-grid' import { LookupMatchesGrid } from '@/components/lookup/lookup-matches-grid'
import { LookupSearchForm } from '@/components/lookup/lookup-search-form' import { LookupSearchForm } from '@/components/lookup/lookup-search-form'
import { LookupStatusBanner } from '@/components/lookup/lookup-status-banner'
import { EmptyState } from '@/components/empty-state' import { EmptyState } from '@/components/empty-state'
import { QueryState } from '@/components/query-state' import { QueryState } from '@/components/query-state'
import { import {
@@ -29,7 +30,6 @@ import {
import { import {
Frame, Frame,
FrameDescription, FrameDescription,
FrameFooter,
FrameHeader, FrameHeader,
FramePanel, FramePanel,
FrameTitle, FrameTitle,
@@ -42,8 +42,7 @@ import { lookupKeys, lookupQueryOptions } from '@/queries/lookup'
import { modulesListQueryOptions } from '@/queries/modules' import { modulesListQueryOptions } from '@/queries/modules'
/** /**
* Lookup membership wizard — check → result → optional add. * Lookup membership wizard — one Frame (wizard-2), all content inside.
* DNA: wizard-2 · surface frame · stepper · FrameFooter CTAs.
* @see https://reui.io/preview/base/wizard-2 * @see https://reui.io/preview/base/wizard-2
* @see https://reui.io/docs/components/base/stepper * @see https://reui.io/docs/components/base/stepper
* @see https://reui.io/docs/components/base/frame * @see https://reui.io/docs/components/base/frame
@@ -55,6 +54,14 @@ const STEP_RESULT = 2
const STEP_ADD = 3 const STEP_ADD = 3
const STEP_DONE = 4 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({ export function LookupWizard({
q, q,
onSubmitQuery, onSubmitQuery,
@@ -109,13 +116,14 @@ export function LookupWizard({
return ( return (
<div className="flex flex-col gap-4 md:gap-6"> <div className="flex flex-col gap-4 md:gap-6">
<Frame spacing="sm" className="w-full"> <Frame spacing="sm" className="w-full">
<FrameHeader> <FramePanel className="flex flex-col gap-6">
<FrameTitle>Мастер проверки</FrameTitle> <FrameHeader className="px-0 py-0">
<FrameDescription> <FrameTitle>Мастер проверки</FrameTitle>
Проверка IP / CIDR / домена в списках, затем при необходимости добавление. <FrameDescription>
</FrameDescription> Проверка IP / CIDR / домена в списках, затем при необходимости добавление.
</FrameHeader> </FrameDescription>
<FramePanel> </FrameHeader>
<Stepper <Stepper
value={step} value={step}
onValueChange={setStep} onValueChange={setStep}
@@ -184,21 +192,21 @@ export function LookupWizard({
<StepperPanel> <StepperPanel>
<StepperContent value={STEP_QUERY}> <StepperContent value={STEP_QUERY}>
<LookupSearchForm <div className="flex flex-col gap-6">
key={`form-${trimmed}`} <LookupSearchForm
initialQuery={trimmed} key={`form-${trimmed}`}
isPending={lookupQ.isFetching} initialQuery={trimmed}
onSubmit={onSubmitQuery} isPending={lookupQ.isFetching}
/> onSubmit={onSubmitQuery}
{!trimmed ? ( />
<div className="mt-4"> {!trimmed ? (
<EmptyState <EmptyState
icon={<Search className="size-8" />} icon={<Search className="size-8" />}
title="Введите IP, CIDR или домен" title="Введите IP, CIDR или домен"
description="Например 8.8.8.8, 203.0.113.0/24 или example.com — проверка по entries и snapshots." description="Например 8.8.8.8, 203.0.113.0/24 или example.com — проверка по entries и snapshots."
/> />
</div> ) : null}
) : null} </div>
</StepperContent> </StepperContent>
<StepperContent value={STEP_RESULT}> <StepperContent value={STEP_RESULT}>
@@ -228,75 +236,59 @@ export function LookupWizard({
{result.matched ? ( {result.matched ? (
<> <>
<Alert variant="success"> <LookupStatusBanner
<Check className="size-4" /> status="found"
<AlertTitle>Уже есть в списках</AlertTitle> normalized={result.normalized}
<AlertDescription> matchCount={result.match_count}
«{result.normalized}» найден в entries и/или snapshots />
({result.match_count} совпад.).
</AlertDescription>
</Alert>
<LookupMatchesGrid <LookupMatchesGrid
items={result.matches} items={result.matches}
isLoading={lookupQ.isFetching} isLoading={lookupQ.isFetching}
/> />
<Frame spacing="sm" className="w-full"> <WizardActions>
<FrameFooter className="flex flex-wrap items-center justify-end gap-2"> <Button
<Button type="button"
type="button" variant="outline"
variant="outline" onClick={resetToQuery}
onClick={resetToQuery} >
> Новая проверка
Новая проверка </Button>
</Button> </WizardActions>
</FrameFooter>
</Frame>
</> </>
) : canWrite ? ( ) : canWrite ? (
<> <>
<Alert variant="info"> <LookupStatusBanner
<Plus className="size-4" /> status="missing"
<AlertTitle>Не найдено в списках</AlertTitle> normalized={result.normalized}
<AlertDescription> />
«{result.normalized}» отсутствует. Добавить запись? <WizardActions>
</AlertDescription> <Button
</Alert> type="button"
<Frame spacing="sm" className="w-full"> variant="outline"
<FrameFooter className="flex flex-wrap items-center justify-end gap-2"> onClick={resetToQuery}
<Button >
type="button" Новая проверка
variant="outline" </Button>
onClick={resetToQuery} <Button type="button" onClick={() => goToAdd()}>
> Добавить
Новая проверка </Button>
</Button> </WizardActions>
<Button type="button" onClick={() => goToAdd()}>
Добавить
</Button>
</FrameFooter>
</Frame>
</> </>
) : ( ) : (
<> <>
<Alert variant="warning"> <LookupStatusBanner
<ShieldAlert className="size-4" /> status="missing_denied"
<AlertTitle>Не найдено</AlertTitle> normalized={result.normalized}
<AlertDescription> />
«{result.normalized}» отсутствует в списках. У вас нет <WizardActions>
права добавлять записи (нужно bgp:modules:write). <Button
</AlertDescription> type="button"
</Alert> variant="outline"
<Frame spacing="sm" className="w-full"> onClick={resetToQuery}
<FrameFooter className="flex flex-wrap items-center justify-end gap-2"> >
<Button Новая проверка
type="button" </Button>
variant="outline" </WizardActions>
onClick={resetToQuery}
>
Новая проверка
</Button>
</FrameFooter>
</Frame>
</> </>
)} )}
</div> </div>
@@ -329,9 +321,9 @@ export function LookupWizard({
<StepperContent value={STEP_DONE}> <StepperContent value={STEP_DONE}>
<div className="flex flex-col gap-4 md:gap-6"> <div className="flex flex-col gap-4 md:gap-6">
<Alert variant="success"> <Alert variant="success" className="items-start py-3">
<Check className="size-4" /> <Check aria-hidden />
<AlertTitle>Запись добавлена</AlertTitle> <AlertTitle className="line-clamp-none">Запись добавлена</AlertTitle>
<AlertDescription> <AlertDescription>
Повторная проверка обновлена. Можно посмотреть совпадения или Повторная проверка обновлена. Можно посмотреть совпадения или
начать новый запрос. начать новый запрос.
@@ -343,20 +335,14 @@ export function LookupWizard({
isLoading={lookupQ.isFetching} isLoading={lookupQ.isFetching}
/> />
) : null} ) : null}
<Frame spacing="sm" className="w-full"> <WizardActions>
<FrameFooter className="flex flex-wrap items-center justify-end gap-2"> <Button type="button" variant="outline" onClick={resetToQuery}>
<Button Новая проверка
type="button" </Button>
variant="outline" <Button type="button" onClick={() => setStep(STEP_RESULT)}>
onClick={resetToQuery} К результату
> </Button>
Новая проверка </WizardActions>
</Button>
<Button type="button" onClick={() => setStep(STEP_RESULT)}>
К результату
</Button>
</FrameFooter>
</Frame>
</div> </div>
</StepperContent> </StepperContent>
</StepperPanel> </StepperPanel>
File diff suppressed because one or more lines are too long