Обновлены переводы на русский язык для компонентов, включая карточки мониторинга, сетевые панели и настройки. Исправлены описания и метки для улучшения пользовательского интерфейса, а также добавлены новые элементы для поддержки локализации в компонентах, таких как DataGrid и ResourcePage.
179 lines
5.2 KiB
TypeScript
179 lines
5.2 KiB
TypeScript
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'
|
||
|
||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||
import { SpeakerFormDialog } from '@/components/network/speaker-form-dialog'
|
||
import {
|
||
getSpeakerFilterFieldValue,
|
||
speakerColumns,
|
||
speakerTabFilter,
|
||
} from '@/components/network/network-speakers-grid'
|
||
import {
|
||
createFilter,
|
||
type Filter,
|
||
type FilterFieldConfig,
|
||
} from '@/components/reui/filters'
|
||
import { ResourcePage, renderSingleSelectedLabel } from '@/components/reui-kit'
|
||
import { useDeleteSpeakerMutation } from '@/queries/network'
|
||
import type { SpeakerRow } from '@/types/api'
|
||
|
||
/**
|
||
* BGP speakers list — ResourcePage (Frame + tabs + Filters + DataGrid).
|
||
* @see https://reui.io/preview/base/data-grid-filtering-2
|
||
*/
|
||
|
||
function speakerDeleteLabel(s: SpeakerRow): string {
|
||
return s.agent_domain ?? s.endpoint ?? s.id
|
||
}
|
||
|
||
const SPEAKER_TABS = [
|
||
{ id: 'all', label: 'Все' },
|
||
{ id: 'online', label: 'В сети' },
|
||
{ id: 'offline', label: 'Не в сети' },
|
||
]
|
||
|
||
const ROLE_OPTIONS = [
|
||
{ value: 'primary', label: 'Основной' },
|
||
{ value: 'secondary', label: 'Резервный' },
|
||
{ value: 'speaker', label: 'Спикер' },
|
||
]
|
||
|
||
function createDefaultSpeakerFilters(): Filter[] {
|
||
return [createFilter('endpoint', 'contains', [''])]
|
||
}
|
||
|
||
const speakerFilterFields: FilterFieldConfig[] = [
|
||
{
|
||
key: 'endpoint',
|
||
label: 'Конечная точка',
|
||
icon: <SearchIcon className="size-3.5" aria-hidden />,
|
||
type: 'text',
|
||
className: 'w-52',
|
||
placeholder: 'Адрес агента…',
|
||
},
|
||
{
|
||
key: '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),
|
||
},
|
||
]
|
||
|
||
interface NetworkSpeakersCardProps {
|
||
items: SpeakerRow[]
|
||
isLoading: boolean
|
||
isError: boolean
|
||
error: unknown
|
||
onRetry: () => void
|
||
}
|
||
|
||
export function NetworkSpeakersCard({
|
||
items,
|
||
isLoading,
|
||
isError,
|
||
error,
|
||
onRetry,
|
||
}: NetworkSpeakersCardProps) {
|
||
const deleteMutation = useDeleteSpeakerMutation()
|
||
const [dialogOpen, setDialogOpen] = useState(false)
|
||
const [deleteTarget, setDeleteTarget] = useState<SpeakerRow | null>(null)
|
||
const [filters, setFilters] = useState<Filter[]>(createDefaultSpeakerFilters)
|
||
|
||
const addButton = useMemo(
|
||
() => (
|
||
<Button size="sm" type="button" onClick={() => setDialogOpen(true)}>
|
||
<Plus />
|
||
Добавить спикера
|
||
</Button>
|
||
),
|
||
[],
|
||
)
|
||
|
||
const columns = useMemo<ColumnDef<SpeakerRow, unknown>[]>(
|
||
() => [
|
||
...speakerColumns,
|
||
{
|
||
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="BIRD-агенты на нодах арендатора"
|
||
tabs={SPEAKER_TABS}
|
||
tabFilter={speakerTabFilter}
|
||
filterFields={speakerFilterFields}
|
||
filters={filters}
|
||
onFiltersChange={setFilters}
|
||
onClearFilters={() => setFilters(createDefaultSpeakerFilters())}
|
||
getFilterFieldValue={getSpeakerFilterFieldValue}
|
||
columns={columns}
|
||
data={items}
|
||
getRowId={(row) => row.id}
|
||
isLoading={isLoading}
|
||
isError={isError}
|
||
error={error instanceof Error ? error : null}
|
||
onRetry={onRetry}
|
||
primaryAction={addButton}
|
||
emptyState={{
|
||
title: 'Нет спикеров',
|
||
description: 'Добавьте первого BIRD-агента на ноде.',
|
||
action: addButton,
|
||
}}
|
||
/>
|
||
|
||
<SpeakerFormDialog open={dialogOpen} onOpenChange={setDialogOpen} />
|
||
|
||
<ConfirmDialog
|
||
open={deleteTarget !== null}
|
||
onOpenChange={(open) => {
|
||
if (!open) setDeleteTarget(null)
|
||
}}
|
||
title="Удалить спикера?"
|
||
description={
|
||
deleteTarget
|
||
? `${speakerDeleteLabel(deleteTarget)} будет удалён. Привязки пиров к этому спикеру сбросятся.`
|
||
: undefined
|
||
}
|
||
confirmLabel="Удалить"
|
||
confirmLoadingLabel="Удаление…"
|
||
destructive
|
||
confirmLoading={deleteMutation.isPending}
|
||
onConfirm={() => {
|
||
if (!deleteTarget) return
|
||
void deleteMutation.mutateAsync(deleteTarget.id).then(() => setDeleteTarget(null))
|
||
}}
|
||
/>
|
||
</>
|
||
)
|
||
}
|