feat(web): upgrade lists to ReUI Table v9 and EventCalendar
quality / commitlint (push) Skipped
quality / changes (push) Successful in 9s
quality / docker-check (push) Skipped
quality / openapi (push) Successful in 39s
quality / web (push) Successful in 1m29s
quality / go (push) Successful in 1m3s
quality / bird2 (push) Successful in 15s
CD / quality (push) Successful in 3m43s
CD / publish (push) Failing after 8m29s

Единый kitDataGridTableLayout и ResourcePage/FrameDataGrid на всех списках.
Календарь задач переведён на EventCalendar, lookup — на cascader, у операций появился вид доски.
Удалены settings-7 и самописные DataGridSection/toolbar.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-08-23 01:38:56 +07:00
co-authored by Cursor
parent 927e27640a
commit d56405af7b
157 changed files with 39067 additions and 6110 deletions
@@ -1,24 +1,23 @@
import { useMemo, useState } from 'react'
import type { ColumnDef } from '@tanstack/react-table'
import { CheckIcon, SearchIcon, XIcon } from 'lucide-react'
import { CheckIcon, Network, SearchIcon, XIcon } from 'lucide-react'
import { Button } from '@evobgp/ui/components/button'
import { Input } from '@evobgp/ui/components/input'
import { Label } from '@evobgp/ui/components/label'
import { ConfirmDialog } from '@/components/confirm-dialog'
import { DataGridPrimaryCell } from '@/components/data-grid-cell'
import { DataGridMonoCell, DataGridNameCell } from '@/components/data-grid-cell'
import { FormDrawer } from '@/components/form-drawer'
import { LoadingButton } from '@/components/loading-button'
import { SelectField } from '@/components/select-field'
import { StatusBadge } from '@/components/status-badge'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import type { FilterField, FilterQuery } from '@/components/reui/filters/filters-types'
import {
createFilter,
type Filter,
type FilterFieldConfig,
} from '@/components/reui/filters'
import { ResourcePage } from '@/components/reui-kit'
ResourcePage,
createTextFilterQuery,
type DataGridColumnDef,
} from '@/components/reui-kit'
import { bgpSessionStateRu } from '@/lib/ui-labels'
import {
useApproveDiscoveredPeerMutation,
@@ -40,17 +39,12 @@ function speakerLabel(s: SpeakerRow): string {
return s.agent_domain ?? s.endpoint ?? `${s.id.slice(0, 8)}…`
}
function createDefaultFilters(): Filter[] {
return [createFilter('neighbor', 'contains', [''])]
}
const filterFields: FilterFieldConfig[] = [
const filterFields: FilterField[] = [
{
key: 'neighbor',
id: 'neighbor',
label: 'Сосед',
icon: <SearchIcon className="size-3.5" aria-hidden />,
type: 'text',
className: 'w-48',
placeholder: 'IP или ID соседа…',
},
]
@@ -74,7 +68,9 @@ export function NetworkDiscoveredPeersCard({
}: NetworkDiscoveredPeersCardProps) {
const approveMutation = useApproveDiscoveredPeerMutation()
const rejectMutation = useRejectDiscoveredPeerMutation()
const [filters, setFilters] = useState<Filter[]>(createDefaultFilters)
const [filterQuery, setFilterQuery] = useState<FilterQuery>(() =>
createTextFilterQuery('neighbor'),
)
const [approveTarget, setApproveTarget] = useState<PeerDiscoveryRow | null>(null)
const [rejectTarget, setRejectTarget] = useState<PeerDiscoveryRow | null>(null)
const [name, setName] = useState('')
@@ -88,7 +84,7 @@ export function NetworkDiscoveredPeersCard({
[speakers],
)
const columns = useMemo<ColumnDef<PeerDiscoveryRow, unknown>[]>(
const columns = useMemo<DataGridColumnDef<PeerDiscoveryRow>[]>(
() => [
{
id: 'neighbor_id',
@@ -97,10 +93,10 @@ export function NetworkDiscoveredPeersCard({
<DataGridColumnHeader column={column} title="ID соседа" />
),
cell: ({ row }) => (
<DataGridPrimaryCell
<DataGridNameCell
icon={Network}
title={row.original.neighbor_id || '—'}
subtitle={row.original.neighbor}
accent="mono"
/>
),
meta: { headerTitle: 'ID соседа' },
@@ -109,7 +105,7 @@ export function NetworkDiscoveredPeersCard({
accessorKey: 'remote_asn',
header: ({ column }) => <DataGridColumnHeader column={column} title="ASN" />,
cell: ({ row }) => (
<span className="font-mono text-xs">{row.original.remote_asn || '—'}</span>
<DataGridMonoCell className="text-xs">{row.original.remote_asn || '—'}</DataGridMonoCell>
),
meta: { headerTitle: 'ASN' },
},
@@ -186,9 +182,9 @@ export function NetworkDiscoveredPeersCard({
title="На одобрение"
description="Новые BGP-клиенты, подключившиеся к dynamic listener (карантин без export)"
filterFields={filterFields}
filters={filters}
onFiltersChange={setFilters}
onClearFilters={() => setFilters(createDefaultFilters())}
filterQuery={filterQuery}
onFilterQueryChange={setFilterQuery}
onClearFilters={() => setFilterQuery(createTextFilterQuery('neighbor'))}
getFilterFieldValue={getFilterFieldValue}
columns={columns}
data={items}
@@ -197,6 +193,7 @@ export function NetworkDiscoveredPeersCard({
isError={isError}
error={error instanceof Error ? error : null}
onRetry={onRetry}
pinLastColumn
emptyState={{
title: 'Нет ожидающих пиров',
description:
@@ -1,5 +1,4 @@
import { useMemo, useState } from 'react'
import type { ColumnDef } from '@tanstack/react-table'
import { Plus, SearchIcon, ActivityIcon, Trash2 } from 'lucide-react'
import { Button } from '@evobgp/ui/components/button'
@@ -11,12 +10,13 @@ import {
peerColumns,
peerTabFilter,
} from '@/components/network/network-peers-grid'
import type { FilterField, FilterQuery } from '@/components/reui/filters/filters-types'
import {
createFilter,
type Filter,
type FilterFieldConfig,
} from '@/components/reui/filters'
import { ResourcePage, renderSingleSelectedLabel } from '@/components/reui-kit'
ResourcePage,
createTextFilterQuery,
renderSingleSelectedLabel,
type DataGridColumnDef,
} from '@/components/reui-kit'
import { useDeletePeerMutation } from '@/queries/network'
import type { PeerRow, SpeakerRow } from '@/types/api'
@@ -46,37 +46,29 @@ const SESSION_STATE_OPTIONS = [
{ value: 'OpenConfirm', label: 'Open подтверждён' },
]
function createDefaultPeerFilters(): Filter[] {
return [createFilter('name', 'contains', [''])]
}
const peerFilterFields: FilterFieldConfig[] = [
const peerFilterFields: FilterField[] = [
{
key: 'name',
id: 'name',
label: 'Имя',
icon: <SearchIcon className="size-3.5" aria-hidden />,
type: 'text',
className: 'w-48',
placeholder: 'Поиск по имени…',
},
{
key: 'neighbor',
id: 'neighbor',
label: 'Сосед',
icon: <SearchIcon className="size-3.5" aria-hidden />,
type: 'text',
className: 'w-48',
placeholder: 'Адрес соседа…',
},
{
key: 'session_state',
id: 'session_state',
label: 'Состояние',
icon: <ActivityIcon className="size-3.5" aria-hidden />,
type: 'select',
searchable: false,
className: 'w-[160px]',
options: SESSION_STATE_OPTIONS,
customValueRenderer: (values) =>
renderSingleSelectedLabel(values, SESSION_STATE_OPTIONS),
renderValue: ({ values }) => renderSingleSelectedLabel(values, SESSION_STATE_OPTIONS),
},
]
@@ -100,7 +92,7 @@ export function NetworkPeersCard({
const deleteMutation = useDeletePeerMutation()
const [dialogOpen, setDialogOpen] = useState(false)
const [deleteTarget, setDeleteTarget] = useState<PeerRow | null>(null)
const [filters, setFilters] = useState<Filter[]>(createDefaultPeerFilters)
const [filterQuery, setFilterQuery] = useState<FilterQuery>(() => createTextFilterQuery('name'))
const addButton = useMemo(
() => (
@@ -112,7 +104,7 @@ export function NetworkPeersCard({
[],
)
const columns = useMemo<ColumnDef<PeerRow, unknown>[]>(
const columns = useMemo<DataGridColumnDef<PeerRow>[]>(
() => [
...peerColumns,
{
@@ -147,9 +139,9 @@ export function NetworkPeersCard({
tabs={PEER_TABS}
tabFilter={peerTabFilter}
filterFields={peerFilterFields}
filters={filters}
onFiltersChange={setFilters}
onClearFilters={() => setFilters(createDefaultPeerFilters())}
filterQuery={filterQuery}
onFilterQueryChange={setFilterQuery}
onClearFilters={() => setFilterQuery(createTextFilterQuery('name'))}
getFilterFieldValue={getPeerFilterFieldValue}
columns={columns}
data={items}
@@ -159,6 +151,7 @@ export function NetworkPeersCard({
error={error instanceof Error ? error : null}
onRetry={onRetry}
primaryAction={addButton}
pinLastColumn
emptyState={{
title: 'Нет пиров',
description: 'Добавьте первого BGP-соседа.',
@@ -1,22 +1,23 @@
import type { ColumnDef } from '@tanstack/react-table'
import { Share2 } from 'lucide-react'
import { CategoryBadge } from '@/components/category-badge'
import { DataGridPrimaryCell } from '@/components/data-grid-cell'
import { DataGridMonoCell, DataGridNameCell } from '@/components/data-grid-cell'
import { StatusBadge } from '@/components/status-badge'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import type { DataGridColumnDef } from '@/components/reui-kit'
import { bgpSessionStateRu } from '@/lib/ui-labels'
import type { PeerRow } from '@/types/api'
export const peerColumns: ColumnDef<PeerRow, unknown>[] = [
export const peerColumns: DataGridColumnDef<PeerRow>[] = [
{
id: 'name',
accessorFn: (row) => row.name ?? row.neighbor,
header: ({ column }) => <DataGridColumnHeader column={column} title="Имя" />,
cell: ({ row }) => (
<DataGridPrimaryCell
<DataGridNameCell
icon={Share2}
title={row.original.name ?? row.original.neighbor}
subtitle={row.original.name ? row.original.neighbor : undefined}
accent="primary"
/>
),
meta: { headerTitle: 'Имя' },
@@ -24,16 +25,14 @@ export const peerColumns: ColumnDef<PeerRow, unknown>[] = [
{
accessorKey: 'neighbor',
header: ({ column }) => <DataGridColumnHeader column={column} title="Адрес соседа" />,
cell: ({ row }) => (
<DataGridPrimaryCell title={row.original.neighbor} accent="mono" />
),
cell: ({ row }) => <DataGridMonoCell>{row.original.neighbor}</DataGridMonoCell>,
meta: { headerTitle: 'Адрес соседа' },
},
{
accessorKey: 'remote_asn',
header: ({ column }) => <DataGridColumnHeader column={column} title="ASN" />,
cell: ({ row }) => (
<span className="font-mono text-xs">{row.original.remote_asn ?? '—'}</span>
<DataGridMonoCell className="text-xs">{row.original.remote_asn ?? '—'}</DataGridMonoCell>
),
meta: { headerTitle: 'ASN' },
},
@@ -1,5 +1,4 @@
import { useMemo, useState } from 'react'
import type { ColumnDef } from '@tanstack/react-table'
import { Plus, SearchIcon, TagIcon, Trash2 } from 'lucide-react'
import { Button } from '@evobgp/ui/components/button'
@@ -11,12 +10,13 @@ import {
speakerColumns,
speakerTabFilter,
} from '@/components/network/network-speakers-grid'
import type { FilterField, FilterQuery } from '@/components/reui/filters/filters-types'
import {
createFilter,
type Filter,
type FilterFieldConfig,
} from '@/components/reui/filters'
import { ResourcePage, renderSingleSelectedLabel } from '@/components/reui-kit'
ResourcePage,
createTextFilterQuery,
renderSingleSelectedLabel,
type DataGridColumnDef,
} from '@/components/reui-kit'
import { useDeleteSpeakerMutation } from '@/queries/network'
import type { SpeakerRow } from '@/types/api'
@@ -40,29 +40,22 @@ const ROLE_OPTIONS = [
{ value: 'master', label: 'Мастер' },
]
function createDefaultSpeakerFilters(): Filter[] {
return [createFilter('endpoint', 'contains', [''])]
}
const speakerFilterFields: FilterFieldConfig[] = [
const speakerFilterFields: FilterField[] = [
{
key: 'endpoint',
id: 'endpoint',
label: 'Конечная точка',
icon: <SearchIcon className="size-3.5" aria-hidden />,
type: 'text',
className: 'w-52',
placeholder: 'Адрес агента…',
},
{
key: 'role',
id: 'role',
label: 'Роль',
icon: <TagIcon className="size-3.5" aria-hidden />,
type: 'select',
searchable: false,
className: 'w-[140px]',
options: ROLE_OPTIONS,
customValueRenderer: (values) =>
renderSingleSelectedLabel(values, ROLE_OPTIONS),
renderValue: ({ values }) => renderSingleSelectedLabel(values, ROLE_OPTIONS),
},
]
@@ -84,7 +77,9 @@ export function NetworkSpeakersCard({
const deleteMutation = useDeleteSpeakerMutation()
const [dialogOpen, setDialogOpen] = useState(false)
const [deleteTarget, setDeleteTarget] = useState<SpeakerRow | null>(null)
const [filters, setFilters] = useState<Filter[]>(createDefaultSpeakerFilters)
const [filterQuery, setFilterQuery] = useState<FilterQuery>(() =>
createTextFilterQuery('endpoint'),
)
const addButton = useMemo(
() => (
@@ -96,7 +91,7 @@ export function NetworkSpeakersCard({
[],
)
const columns = useMemo<ColumnDef<SpeakerRow, unknown>[]>(
const columns = useMemo<DataGridColumnDef<SpeakerRow>[]>(
() => [
...speakerColumns,
{
@@ -131,9 +126,9 @@ export function NetworkSpeakersCard({
tabs={SPEAKER_TABS}
tabFilter={speakerTabFilter}
filterFields={speakerFilterFields}
filters={filters}
onFiltersChange={setFilters}
onClearFilters={() => setFilters(createDefaultSpeakerFilters())}
filterQuery={filterQuery}
onFilterQueryChange={setFilterQuery}
onClearFilters={() => setFilterQuery(createTextFilterQuery('endpoint'))}
getFilterFieldValue={getSpeakerFilterFieldValue}
columns={columns}
data={items}
@@ -143,6 +138,7 @@ export function NetworkSpeakersCard({
error={error instanceof Error ? error : null}
onRetry={onRetry}
primaryAction={addButton}
pinLastColumn
emptyState={{
title: 'Нет спикеров',
description: 'Добавьте первого BIRD-агента на ноде.',
@@ -1,19 +1,24 @@
import type { ColumnDef } from '@tanstack/react-table'
import { ServerCog } from 'lucide-react'
import { CategoryBadge } from '@/components/category-badge'
import { DataGridPrimaryCell } from '@/components/data-grid-cell'
import { DataGridMonoCell, DataGridNameCell } from '@/components/data-grid-cell'
import { StatusBadge } from '@/components/status-badge'
import { Badge } from '@/components/reui/badge'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import type { DataGridColumnDef } from '@/components/reui-kit'
import { speakerOnlineLabel, speakerRoleRu } from '@/lib/ui-labels'
import type { SpeakerRow } from '@/types/api'
export const speakerColumns: ColumnDef<SpeakerRow, unknown>[] = [
export const speakerColumns: DataGridColumnDef<SpeakerRow>[] = [
{
accessorKey: 'endpoint',
header: ({ column }) => <DataGridColumnHeader column={column} title="Конечная точка" />,
cell: ({ row }) => (
<DataGridPrimaryCell title={row.original.endpoint} accent="mono" />
<DataGridNameCell
icon={ServerCog}
title={row.original.agent_domain ?? row.original.endpoint}
subtitle={row.original.agent_domain ? row.original.endpoint : undefined}
/>
),
meta: { headerTitle: 'Конечная точка' },
},
@@ -51,9 +56,9 @@ export const speakerColumns: ColumnDef<SpeakerRow, unknown>[] = [
const live = row.original.live
if (!live) return '—'
return (
<span className="text-xs">
<DataGridMonoCell className="text-xs">
{live.bgp_established ?? 0} / {live.bgp_sessions_total ?? 0}
</span>
</DataGridMonoCell>
)
},
meta: { headerTitle: 'BGP' },
@@ -63,7 +68,7 @@ export const speakerColumns: ColumnDef<SpeakerRow, unknown>[] = [
export function getSpeakerFilterFieldValue(item: SpeakerRow, field: string): unknown {
switch (field) {
case 'endpoint':
return item.endpoint
return `${item.endpoint} ${item.agent_domain ?? ''}`
case 'role':
return item.role
default: