CI / changes (push) Successful in 12s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 1m6s
CI / go (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / release (push) Successful in 4m27s
Updated multiple components to improve user interface consistency by replacing traditional badge implementations with the new CategoryBadge and DataGridPrimaryCell components. Enhanced the StatusBadge component to support additional status variants and integrated it across various grids, including AccessApiKeysGrid, DashboardRecentJobsGrid, and OperationsJobsGrid. This refactor streamlines the presentation of data and improves the overall user experience across the application.
260 lines
8.3 KiB
TypeScript
260 lines
8.3 KiB
TypeScript
import { ColumnDef } from '@tanstack/react-table'
|
|
import { Pencil, Trash2 } from 'lucide-react'
|
|
import { useMemo } from 'react'
|
|
|
|
import { Button } from '@evobgp/ui/components/button'
|
|
|
|
import { CategoryBadge } from '@/components/category-badge'
|
|
import { DataGridMutedCell, DataGridPrimaryCell } from '@/components/data-grid-cell'
|
|
import { DataGridSection } from '@/components/data-grid-shell'
|
|
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
|
|
import { formatDateTime } from '@/lib/modules/display'
|
|
import { communityLabel } from '@/lib/modules/helpers'
|
|
import { useClientDataGrid } from '@/hooks/use-client-data-grid'
|
|
import type {
|
|
AsEntry,
|
|
BgpCommunity,
|
|
CdnSource,
|
|
DomainEntry,
|
|
IpRangeEntry,
|
|
ModuleRow,
|
|
} from '@/types/api'
|
|
|
|
type DeleteTarget =
|
|
| { kind: 'domain'; entry: DomainEntry }
|
|
| { kind: 'ip-range'; entry: IpRangeEntry }
|
|
| { kind: 'cdn'; entry: CdnSource }
|
|
| { kind: 'as'; entry: AsEntry }
|
|
|
|
function RowActions({ onEdit, onDelete }: { onEdit: () => void; onDelete: () => void }) {
|
|
return (
|
|
<div className="flex justify-end gap-1">
|
|
<Button variant="ghost" size="icon-sm" type="button" onClick={onEdit} aria-label="Редактировать">
|
|
<Pencil className="size-3.5" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon-sm"
|
|
type="button"
|
|
className="text-destructive"
|
|
onClick={onDelete}
|
|
aria-label="Удалить"
|
|
>
|
|
<Trash2 className="size-3.5" />
|
|
</Button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function ModuleEntriesGrid({
|
|
mod,
|
|
rows,
|
|
communities,
|
|
onEdit,
|
|
onDelete,
|
|
isLoading = false,
|
|
}: {
|
|
mod: ModuleRow
|
|
rows: Record<string, unknown>[]
|
|
communities: BgpCommunity[]
|
|
onEdit: (target: DeleteTarget) => void
|
|
onDelete: (target: DeleteTarget) => void
|
|
isLoading?: boolean
|
|
}) {
|
|
const columns = useMemo(() => {
|
|
if (mod.type === 'DOMAINS') {
|
|
return [
|
|
{
|
|
accessorKey: 'fqdn',
|
|
header: ({ column }: { column: { id: string } }) => (
|
|
<DataGridColumnHeader column={column as never} title="FQDN" />
|
|
),
|
|
cell: ({ row }: { row: { original: DomainEntry } }) => (
|
|
<DataGridPrimaryCell title={row.original.fqdn} accent="mono" />
|
|
),
|
|
},
|
|
{
|
|
id: 'community',
|
|
header: 'Community',
|
|
cell: ({ row }: { row: { original: DomainEntry } }) => (
|
|
<span className="text-sm text-muted-foreground">
|
|
{communityLabel(row.original.community_id, communities)}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'actions',
|
|
enableSorting: false,
|
|
header: () => null,
|
|
cell: ({ row }: { row: { original: DomainEntry } }) => (
|
|
<RowActions
|
|
onEdit={() => onEdit({ kind: 'domain', entry: row.original })}
|
|
onDelete={() => onDelete({ kind: 'domain', entry: row.original })}
|
|
/>
|
|
),
|
|
},
|
|
] as ColumnDef<DomainEntry>[]
|
|
}
|
|
|
|
if (mod.type === 'IP_RANGES') {
|
|
return [
|
|
{
|
|
accessorKey: 'prefix',
|
|
header: ({ column }: { column: { id: string } }) => (
|
|
<DataGridColumnHeader column={column as never} title="Префикс (CIDR)" />
|
|
),
|
|
cell: ({ row }: { row: { original: IpRangeEntry } }) => (
|
|
<DataGridPrimaryCell title={row.original.prefix} accent="mono" />
|
|
),
|
|
},
|
|
{
|
|
id: 'community',
|
|
header: 'Community',
|
|
cell: ({ row }: { row: { original: IpRangeEntry } }) => (
|
|
<span className="text-sm text-muted-foreground">
|
|
{communityLabel(row.original.community_id, communities)}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'actions',
|
|
enableSorting: false,
|
|
header: () => null,
|
|
cell: ({ row }: { row: { original: IpRangeEntry } }) => (
|
|
<RowActions
|
|
onEdit={() => onEdit({ kind: 'ip-range', entry: row.original })}
|
|
onDelete={() => onDelete({ kind: 'ip-range', entry: row.original })}
|
|
/>
|
|
),
|
|
},
|
|
] as ColumnDef<IpRangeEntry>[]
|
|
}
|
|
|
|
if (mod.type === 'CDN_CIDRS') {
|
|
return [
|
|
{
|
|
accessorKey: 'url',
|
|
header: ({ column }: { column: { id: string } }) => (
|
|
<DataGridColumnHeader column={column as never} title="URL" />
|
|
),
|
|
cell: ({ row }: { row: { original: CdnSource } }) => (
|
|
<DataGridPrimaryCell title={row.original.url} accent="mono" className="max-w-xs" />
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'source_kind',
|
|
header: 'Тип',
|
|
cell: ({ row }: { row: { original: CdnSource } }) => (
|
|
<CategoryBadge>{row.original.source_kind}</CategoryBadge>
|
|
),
|
|
},
|
|
{
|
|
id: 'community',
|
|
header: 'Community',
|
|
cell: ({ row }: { row: { original: CdnSource } }) => (
|
|
<span className="text-sm text-muted-foreground">
|
|
{communityLabel(row.original.community_id, communities)}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'last_refreshed_at',
|
|
accessorFn: (row: CdnSource) => row.last_refreshed_at ?? '',
|
|
header: ({ column }: { column: { id: string } }) => (
|
|
<DataGridColumnHeader column={column as never} title="Обновлено" />
|
|
),
|
|
cell: ({ row }: { row: { original: CdnSource } }) => (
|
|
<DataGridMutedCell>{formatDateTime(row.original.last_refreshed_at)}</DataGridMutedCell>
|
|
),
|
|
},
|
|
{
|
|
id: 'actions',
|
|
enableSorting: false,
|
|
header: () => null,
|
|
cell: ({ row }: { row: { original: CdnSource } }) => (
|
|
<RowActions
|
|
onEdit={() => onEdit({ kind: 'cdn', entry: row.original })}
|
|
onDelete={() => onDelete({ kind: 'cdn', entry: row.original })}
|
|
/>
|
|
),
|
|
},
|
|
] as ColumnDef<CdnSource>[]
|
|
}
|
|
|
|
return [
|
|
{
|
|
accessorKey: 'asn',
|
|
header: ({ column }: { column: { id: string } }) => (
|
|
<DataGridColumnHeader column={column as never} title="ASN" />
|
|
),
|
|
cell: ({ row }: { row: { original: AsEntry } }) => (
|
|
<span className="font-mono text-sm">{row.original.asn}</span>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'asn_name',
|
|
header: 'Имя',
|
|
cell: ({ row }: { row: { original: AsEntry } }) => (
|
|
<span className="text-sm">{row.original.asn_name ?? '—'}</span>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'prefix_count',
|
|
header: 'Префиксов',
|
|
cell: ({ row }: { row: { original: AsEntry } }) => (
|
|
<span className="font-mono text-sm">{row.original.prefix_count ?? '—'}</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'community',
|
|
header: 'Community',
|
|
cell: ({ row }: { row: { original: AsEntry } }) => (
|
|
<span className="text-sm text-muted-foreground">
|
|
{communityLabel(row.original.community_id, communities)}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
id: 'actions',
|
|
enableSorting: false,
|
|
header: () => null,
|
|
cell: ({ row }: { row: { original: AsEntry } }) => (
|
|
<RowActions
|
|
onEdit={() => onEdit({ kind: 'as', entry: row.original })}
|
|
onDelete={() => onDelete({ kind: 'as', entry: row.original })}
|
|
/>
|
|
),
|
|
},
|
|
] as ColumnDef<AsEntry>[]
|
|
}, [communities, mod.type, onDelete, onEdit])
|
|
|
|
type RowType = DomainEntry | IpRangeEntry | CdnSource | AsEntry
|
|
const data = rows as unknown as RowType[]
|
|
|
|
const { table, globalFilter, setGlobalFilter, filteredCount } = useClientDataGrid({
|
|
data,
|
|
columns: columns as ColumnDef<RowType>[],
|
|
getSearchText: (row) => {
|
|
const r = row as Record<string, unknown>
|
|
return Object.values(r)
|
|
.filter((v) => typeof v === 'string' || typeof v === 'number')
|
|
.join(' ')
|
|
},
|
|
getRowId: (row) => row.id,
|
|
})
|
|
|
|
return (
|
|
<DataGridSection
|
|
table={table}
|
|
recordCount={filteredCount}
|
|
isLoading={isLoading}
|
|
emptyMessage="Нет записей"
|
|
searchValue={globalFilter}
|
|
onSearchChange={setGlobalFilter}
|
|
searchPlaceholder="Поиск записей…"
|
|
/>
|
|
)
|
|
}
|
|
|
|
export type { DeleteTarget as ModuleEntryDeleteTarget }
|