Compare commits

...
2 Commits
Author SHA1 Message Date
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
Denozordec cc80858952 refactor(lookup): remove LookupSummaryKpi component and update LookupWizard
CI / changes (push) Successful in 6s
CI / commitlint (push) Skipped
CI / go (push) Skipped
CI / bird2 (push) Skipped
CI / openapi (push) Successful in 47s
CI / web (push) Successful in 1m8s
CI / release (push) Successful in 4m19s
Deleted the LookupSummaryKpi component to streamline the lookup functionality. Updated the LookupWizard component to remove references to the deleted component and adjusted the layout to incorporate FrameFooter for improved user interaction. Modified API type documentation to reflect changes in the lookup membership wizard.
2026-07-31 13:42:02 +07:00
7 changed files with 129 additions and 231 deletions
@@ -12,14 +12,6 @@ import {
AlertDescription,
AlertTitle,
} from '@/components/reui/alert'
import {
Frame,
FrameDescription,
FrameFooter,
FrameHeader,
FramePanel,
FrameTitle,
} from '@/components/reui/frame'
import { SelectMenu } from '@/components/select-field'
import { ApiError, apiMutate } from '@/lib/api-client'
import type {
@@ -30,9 +22,8 @@ import type {
} 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/docs/components/base/frame
*/
function hostPrefixFromIp(ip: string): string {
@@ -153,14 +144,14 @@ export function LookupAddStep({
: hostPrefixFromIp(data.normalized)
return (
<Frame spacing="sm" className="w-full">
<FrameHeader>
<FrameTitle>Добавить в списки</FrameTitle>
<FrameDescription>
<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.
</FrameDescription>
</FrameHeader>
<FramePanel className="flex flex-col gap-4">
</p>
</div>
<div className="flex flex-col gap-4">
<Field>
<FieldLabel htmlFor="lookup-add-module">Модуль ({wantedType})</FieldLabel>
<SelectMenu
@@ -181,8 +172,8 @@ export function LookupAddStep({
communities={communities}
nullable={data.query_kind === 'domain'}
/>
</FramePanel>
<FrameFooter className="flex flex-wrap items-center justify-end gap-2">
</div>
<div className="flex flex-wrap items-center justify-end gap-2 border-t pt-4">
<Button type="button" variant="outline" onClick={onCancel}>
Назад
</Button>
@@ -193,7 +184,7 @@ export function LookupAddStep({
>
Добавить
</LoadingButton>
</FrameFooter>
</Frame>
</div>
</div>
)
}
@@ -4,7 +4,6 @@ import { useNavigate } from '@tanstack/react-router'
import { CategoryBadge } from '@/components/category-badge'
import { DataGridPrimaryCell } from '@/components/data-grid-cell'
import { FrameDataGrid } from '@/components/reui-kit'
import { DataGridSection } from '@/components/data-grid-shell'
import { Badge } from '@/components/reui/badge'
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'
/**
* 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/docs/components/base/badge
*/
@@ -113,10 +113,13 @@ export function LookupMatchesGrid({
})
return (
<FrameDataGrid
title="Совпадения"
description="Entries и snapshots · клик по строке открывает модуль"
>
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-px">
<h3 className="text-sm font-semibold">Совпадения</h3>
<p className="text-muted-foreground text-sm">
Entries и snapshots · клик по строке открывает модуль
</p>
</div>
<DataGridSection
table={table}
recordCount={filteredCount}
@@ -129,6 +132,6 @@ export function LookupMatchesGrid({
void navigate({ to: '/modules/$moduleId', params: { moduleId: row.module_id } })
}
/>
</FrameDataGrid>
</div>
)
}
@@ -8,17 +8,10 @@ import {
InputGroupAddon,
InputGroupInput,
} from '@evobgp/ui/components/input-group'
import {
Frame,
FrameDescription,
FrameHeader,
FramePanel,
FrameTitle,
} from '@/components/reui/frame'
/**
* Lookup search form — Frame + InputGroup (form-7 pattern).
* @see https://reui.io/preview/base/form-7
* Lookup search fields — bare form for wizard-2 single-Frame surface.
* @see https://reui.io/preview/base/wizard-2
* @see https://reui.io/docs/components/base/frame
*/
export function LookupSearchForm({
@@ -40,37 +33,27 @@ export function LookupSearchForm({
}
return (
<Frame spacing="sm" className="w-full">
<FrameHeader>
<FrameTitle>Проверка списка</FrameTitle>
<FrameDescription>
IP, CIDR или FQDN поиск в entries и материализованных snapshots с community.
</FrameDescription>
</FrameHeader>
<FramePanel>
<form onSubmit={handleSubmit} className="flex flex-col gap-4 sm:flex-row sm:items-end">
<Field className="min-w-0 flex-1">
<FieldLabel htmlFor="lookup-q">IP, CIDR или домен</FieldLabel>
<InputGroup>
<InputGroupAddon align="inline-start">
<Search aria-hidden />
</InputGroupAddon>
<InputGroupInput
id="lookup-q"
name="q"
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="8.8.8.8, 203.0.113.0/24 или example.com"
autoComplete="off"
autoFocus
/>
</InputGroup>
</Field>
<Button type="submit" disabled={isPending || !value.trim()}>
Проверить
</Button>
</form>
</FramePanel>
</Frame>
<form onSubmit={handleSubmit} className="flex flex-col gap-4 sm:flex-row sm:items-end">
<Field className="min-w-0 flex-1">
<FieldLabel htmlFor="lookup-q">IP, CIDR или домен</FieldLabel>
<InputGroup>
<InputGroupAddon align="inline-start">
<Search aria-hidden />
</InputGroupAddon>
<InputGroupInput
id="lookup-q"
name="q"
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="8.8.8.8, 203.0.113.0/24 или example.com"
autoComplete="off"
autoFocus
/>
</InputGroup>
</Field>
<Button type="submit" disabled={isPending || !value.trim()}>
Проверить
</Button>
</form>
)
}
@@ -1,62 +0,0 @@
import { Globe, Layers, ListChecks, Radar } from 'lucide-react'
import { KpiStatGrid, type KpiStatItem } from '@/components/reui-kit'
import type { LookupResponse } from '@/types/api'
/**
* Lookup summary KPI — stats-12 via KpiStatGrid.
* @see https://reui.io/preview/base/stats-12
*/
export function LookupSummaryKpi({ data }: { data: LookupResponse }) {
const entryCount = data.matches.filter((m) => m.layer === 'entry').length
const snapshotCount = data.matches.filter((m) => m.layer === 'snapshot').length
const resolvedCount = data.resolved_ips?.length ?? 0
const items: KpiStatItem[] = [
{
id: 'matched',
label: 'Результат',
value: data.matched ? 'Найдено' : 'Не найдено',
hint: data.normalized,
icon: <Radar aria-hidden />,
iconClassName: data.matched
? 'bg-success text-success-foreground [&_svg]:text-success-foreground'
: 'bg-muted text-muted-foreground [&_svg]:text-muted-foreground',
variant: data.matched ? 'default' : 'warning',
},
{
id: 'entry',
label: 'Слой entry',
value: entryCount,
hint: 'сырые списки',
icon: <ListChecks aria-hidden />,
iconClassName: 'bg-info text-info-foreground [&_svg]:text-info-foreground',
},
{
id: 'snapshot',
label: 'Слой snapshot',
value: snapshotCount,
hint: 'материализация',
icon: <Layers aria-hidden />,
iconClassName: 'bg-focus text-focus-foreground [&_svg]:text-focus-foreground',
},
]
if (data.query_kind === 'domain') {
items.push({
id: 'resolved',
label: 'DNS IP',
value: resolvedCount,
hint: resolvedCount > 0 ? data.resolved_ips?.slice(0, 3).join(', ') : 'нет A/AAAA',
icon: <Globe aria-hidden />,
iconClassName: 'bg-primary text-primary-foreground [&_svg]:text-primary-foreground',
})
}
return (
<KpiStatGrid
items={items}
aria-label={`Запрос: ${data.query_kind} · ${data.query}`}
/>
)
}
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'
import { useEffect, useState, type ReactNode } from 'react'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { Check, Plus, Search, ShieldAlert } from 'lucide-react'
@@ -7,12 +7,10 @@ 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 { LookupSummaryKpi } from '@/components/lookup/lookup-summary-kpi'
import { EmptyState } from '@/components/empty-state'
import { QueryState } from '@/components/query-state'
import {
Alert,
AlertAction,
AlertDescription,
AlertTitle,
} from '@/components/reui/alert'
@@ -35,7 +33,7 @@ import {
FramePanel,
FrameTitle,
} from '@/components/reui/frame'
import { SectionCardsSkeleton, TableSkeleton } from '@/components/skeletons'
import { TableSkeleton } from '@/components/skeletons'
import { sessionCanWriteModules } from '@/lib/auth'
import { authSessionQueryOptions } from '@/queries/auth'
import { directoriesCommunitiesQueryOptions } from '@/queries/directories'
@@ -43,11 +41,10 @@ import { lookupKeys, lookupQueryOptions } from '@/queries/lookup'
import { modulesListQueryOptions } from '@/queries/modules'
/**
* Lookup membership wizard — check → result → optional add.
* DNA: wizard-2 · surface frame · stepper.
* 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/preview/base/stats-12
* @see https://reui.io/docs/components/base/frame
* @see https://reui.io/preview/base/data-grid-filtering-2
*/
@@ -56,6 +53,14 @@ 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,
@@ -110,13 +115,14 @@ export function LookupWizard({
return (
<div className="flex flex-col gap-4 md:gap-6">
<Frame spacing="sm" className="w-full">
<FrameHeader>
<FrameTitle>Мастер проверки</FrameTitle>
<FrameDescription>
Проверка IP / CIDR / домена в списках, затем при необходимости добавление.
</FrameDescription>
</FrameHeader>
<FramePanel>
<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}
@@ -185,21 +191,21 @@ export function LookupWizard({
<StepperPanel>
<StepperContent value={STEP_QUERY}>
<LookupSearchForm
key={`form-${trimmed}`}
initialQuery={trimmed}
isPending={lookupQ.isFetching}
onSubmit={onSubmitQuery}
/>
{!trimmed ? (
<div className="mt-4">
<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."
/>
</div>
) : null}
) : null}
</div>
</StepperContent>
<StepperContent value={STEP_RESULT}>
@@ -216,12 +222,7 @@ export function LookupWizard({
isError={lookupQ.isError}
error={lookupQ.error}
onRetry={() => void lookupQ.refetch()}
skeleton={
<div className="flex flex-col gap-4 md:gap-6">
<SectionCardsSkeleton />
<TableSkeleton rows={5} />
</div>
}
skeleton={<TableSkeleton rows={5} />}
>
{(result) => (
<div className="flex flex-col gap-4 md:gap-6">
@@ -231,7 +232,6 @@ export function LookupWizard({
isPending={lookupQ.isFetching}
onSubmit={onSubmitQuery}
/>
<LookupSummaryKpi data={result} />
{result.matched ? (
<>
@@ -242,66 +242,63 @@ export function LookupWizard({
«{result.normalized}» найден в entries и/или snapshots
({result.match_count} совпад.).
</AlertDescription>
<AlertAction>
<Button
type="button"
variant="outline"
size="xs"
onClick={resetToQuery}
>
Новая проверка
</Button>
</AlertAction>
</Alert>
<LookupMatchesGrid
items={result.matches}
isLoading={lookupQ.isFetching}
/>
<WizardActions>
<Button
type="button"
variant="outline"
onClick={resetToQuery}
>
Новая проверка
</Button>
</WizardActions>
</>
) : canWrite ? (
<Alert variant="info">
<Plus className="size-4" />
<AlertTitle>Не найдено в списках</AlertTitle>
<AlertDescription>
«{result.normalized}» отсутствует. Добавить запись?
</AlertDescription>
<AlertAction>
<>
<Alert variant="info">
<Plus className="size-4" />
<AlertTitle>Не найдено в списках</AlertTitle>
<AlertDescription>
«{result.normalized}» отсутствует. Добавить запись?
</AlertDescription>
</Alert>
<WizardActions>
<Button
type="button"
size="xs"
onClick={() => void goToAdd()}
variant="outline"
onClick={resetToQuery}
>
Новая проверка
</Button>
<Button type="button" onClick={() => goToAdd()}>
Добавить
</Button>
<Button
type="button"
variant="outline"
size="xs"
onClick={resetToQuery}
>
Новая проверка
</Button>
</AlertAction>
</Alert>
</WizardActions>
</>
) : (
<Alert variant="warning">
<ShieldAlert className="size-4" />
<AlertTitle>Не найдено</AlertTitle>
<AlertDescription>
«{result.normalized}» отсутствует в списках. У вас нет
права добавлять записи (нужно bgp:modules:write).
</AlertDescription>
<AlertAction>
<>
<Alert variant="warning">
<ShieldAlert className="size-4" />
<AlertTitle>Не найдено</AlertTitle>
<AlertDescription>
«{result.normalized}» отсутствует в списках. У вас нет
права добавлять записи (нужно bgp:modules:write).
</AlertDescription>
</Alert>
<WizardActions>
<Button
type="button"
variant="outline"
size="xs"
onClick={resetToQuery}
>
Новая проверка
</Button>
</AlertAction>
</Alert>
</WizardActions>
</>
)}
</div>
)}
@@ -340,35 +337,21 @@ export function LookupWizard({
Повторная проверка обновлена. Можно посмотреть совпадения или
начать новый запрос.
</AlertDescription>
<AlertAction>
<Button
type="button"
size="xs"
onClick={() => setStep(STEP_RESULT)}
>
К результату
</Button>
<Button
type="button"
variant="outline"
size="xs"
onClick={resetToQuery}
>
Новая проверка
</Button>
</AlertAction>
</Alert>
{lookupQ.data ? (
<>
<LookupSummaryKpi data={lookupQ.data} />
{lookupQ.data.matched ? (
<LookupMatchesGrid
items={lookupQ.data.matches}
isLoading={lookupQ.isFetching}
/>
) : null}
</>
{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>
+1 -1
View File
@@ -162,7 +162,7 @@ export type BgpCommunityPatch = Partial<BgpCommunityCreate>
export type CommunitiesResponse = Page<BgpCommunity>
// ---- Lookup (GET /v1/lookup) ----
/** @see https://reui.io/preview/base/stats-12 — KPI summary on /lookup */
/** @see https://reui.io/preview/base/wizard-2 — lookup membership wizard */
export type LookupQueryKind = 'ip' | 'domain' | 'cidr'
export type LookupLayer = 'entry' | 'snapshot'
export type LookupMatchKind = 'ip_range' | 'domain' | 'prefix'
File diff suppressed because one or more lines are too long