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 <cursoragent@cursor.com>
193 lines
5.5 KiB
TypeScript
193 lines
5.5 KiB
TypeScript
import { useMemo, useState } from 'react'
|
|
import { Plus, SearchIcon, ActivityIcon, Trash2 } from 'lucide-react'
|
|
|
|
import { Button } from '@evobgp/ui/components/button'
|
|
|
|
import { ConfirmDialog } from '@/components/confirm-dialog'
|
|
import { PeerFormDialog } from '@/components/network/peer-form-dialog'
|
|
import {
|
|
getPeerFilterFieldValue,
|
|
peerColumns,
|
|
peerTabFilter,
|
|
} from '@/components/network/network-peers-grid'
|
|
import type { FilterField, FilterQuery } from '@/components/reui/filters/filters-types'
|
|
import {
|
|
ResourcePage,
|
|
createTextFilterQuery,
|
|
renderSingleSelectedLabel,
|
|
type DataGridColumnDef,
|
|
} from '@/components/reui-kit'
|
|
import { useDeletePeerMutation } from '@/queries/network'
|
|
import type { PeerRow, SpeakerRow } from '@/types/api'
|
|
|
|
/**
|
|
* BGP peers list — ResourcePage (Frame + tabs + Filters + DataGrid).
|
|
* @see https://reui.io/preview/base/data-grid-filtering-2
|
|
* @see https://reui.io/docs/components/base/data-grid
|
|
*/
|
|
|
|
function peerDeleteLabel(p: PeerRow): string {
|
|
return p.name?.trim() || p.neighbor || p.id
|
|
}
|
|
|
|
const PEER_TABS = [
|
|
{ id: 'all', label: 'Все' },
|
|
{ id: 'established', label: 'Установлены' },
|
|
{ id: 'pending', label: 'Ожидание' },
|
|
{ id: 'disabled', label: 'Выключены' },
|
|
]
|
|
|
|
const SESSION_STATE_OPTIONS = [
|
|
{ value: 'Established', label: 'Установлена' },
|
|
{ value: 'Idle', label: 'Простой' },
|
|
{ value: 'Active', label: 'Поиск' },
|
|
{ value: 'Connect', label: 'Соединение' },
|
|
{ value: 'OpenSent', label: 'Open отправлен' },
|
|
{ value: 'OpenConfirm', label: 'Open подтверждён' },
|
|
]
|
|
|
|
const peerFilterFields: FilterField[] = [
|
|
{
|
|
id: 'name',
|
|
label: 'Имя',
|
|
icon: <SearchIcon className="size-3.5" aria-hidden />,
|
|
type: 'text',
|
|
placeholder: 'Поиск по имени…',
|
|
},
|
|
{
|
|
id: 'neighbor',
|
|
label: 'Сосед',
|
|
icon: <SearchIcon className="size-3.5" aria-hidden />,
|
|
type: 'text',
|
|
placeholder: 'Адрес соседа…',
|
|
},
|
|
{
|
|
id: 'session_state',
|
|
label: 'Состояние',
|
|
icon: <ActivityIcon className="size-3.5" aria-hidden />,
|
|
type: 'select',
|
|
searchable: false,
|
|
options: SESSION_STATE_OPTIONS,
|
|
renderValue: ({ values }) => renderSingleSelectedLabel(values, SESSION_STATE_OPTIONS),
|
|
},
|
|
]
|
|
|
|
interface NetworkPeersCardProps {
|
|
items: PeerRow[]
|
|
speakers: SpeakerRow[]
|
|
isLoading: boolean
|
|
isError: boolean
|
|
error: unknown
|
|
onRetry: () => void
|
|
}
|
|
|
|
export function NetworkPeersCard({
|
|
items,
|
|
speakers,
|
|
isLoading,
|
|
isError,
|
|
error,
|
|
onRetry,
|
|
}: NetworkPeersCardProps) {
|
|
const deleteMutation = useDeletePeerMutation()
|
|
const [dialogOpen, setDialogOpen] = useState(false)
|
|
const [deleteTarget, setDeleteTarget] = useState<PeerRow | null>(null)
|
|
const [filterQuery, setFilterQuery] = useState<FilterQuery>(() => createTextFilterQuery('name'))
|
|
|
|
const addButton = useMemo(
|
|
() => (
|
|
<Button size="sm" type="button" onClick={() => setDialogOpen(true)}>
|
|
<Plus />
|
|
Добавить пира
|
|
</Button>
|
|
),
|
|
[],
|
|
)
|
|
|
|
const columns = useMemo<DataGridColumnDef<PeerRow>[]>(
|
|
() => [
|
|
...peerColumns,
|
|
{
|
|
id: 'actions',
|
|
enableSorting: false,
|
|
header: () => <span className="sr-only">Действия</span>,
|
|
cell: ({ row }) => (
|
|
<div className="flex items-center justify-end">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon-sm"
|
|
type="button"
|
|
className="text-destructive"
|
|
aria-label="Удалить пира"
|
|
title="Удалить"
|
|
onClick={() => setDeleteTarget(row.original)}
|
|
>
|
|
<Trash2 className="size-3.5" />
|
|
</Button>
|
|
</div>
|
|
),
|
|
},
|
|
],
|
|
[],
|
|
)
|
|
|
|
return (
|
|
<>
|
|
<ResourcePage
|
|
title="Пиры"
|
|
description="BGP-соседи и привязка к спикерам"
|
|
tabs={PEER_TABS}
|
|
tabFilter={peerTabFilter}
|
|
filterFields={peerFilterFields}
|
|
filterQuery={filterQuery}
|
|
onFilterQueryChange={setFilterQuery}
|
|
onClearFilters={() => setFilterQuery(createTextFilterQuery('name'))}
|
|
getFilterFieldValue={getPeerFilterFieldValue}
|
|
columns={columns}
|
|
data={items}
|
|
getRowId={(row) => row.id}
|
|
isLoading={isLoading}
|
|
isError={isError}
|
|
error={error instanceof Error ? error : null}
|
|
onRetry={onRetry}
|
|
primaryAction={addButton}
|
|
pinLastColumn
|
|
emptyState={{
|
|
title: 'Нет пиров',
|
|
description: 'Добавьте первого BGP-соседа.',
|
|
action: addButton,
|
|
}}
|
|
/>
|
|
|
|
<PeerFormDialog
|
|
open={dialogOpen}
|
|
onOpenChange={setDialogOpen}
|
|
speakers={speakers}
|
|
/>
|
|
|
|
<ConfirmDialog
|
|
open={deleteTarget !== null}
|
|
onOpenChange={(open) => {
|
|
if (!open) setDeleteTarget(null)
|
|
}}
|
|
title="Удалить пира?"
|
|
description={
|
|
deleteTarget
|
|
? `${peerDeleteLabel(deleteTarget)} будет удалён. Запустится reconcile пиров и apply на спикеры.`
|
|
: undefined
|
|
}
|
|
confirmLabel="Удалить"
|
|
confirmLoadingLabel="Удаление…"
|
|
destructive
|
|
confirmLoading={deleteMutation.isPending}
|
|
onConfirm={() => {
|
|
if (!deleteTarget) return
|
|
void deleteMutation
|
|
.mutateAsync(deleteTarget.id)
|
|
.then(() => setDeleteTarget(null))
|
|
}}
|
|
/>
|
|
</>
|
|
)
|
|
}
|