Files
cloudflare-domain-manager/apps/web/src/components/dns-records-data-grid.tsx
T
DenozordecandCursor 72a4b3403b
Build, Test, and Push CFDM Docker Image / test (push) Successful in 3m14s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Successful in 2m5s
Build, Test, and Push CFDM Docker Image / update-wiki (push) Successful in 6s
Build, Test, and Push CFDM Docker Image / create-release (push) Has been skipped
Выравнивание UI с vps-tracker: тема b2fA, ReUI DataGrid и shell
Синхронизированы тема и layout (ModeToggle, sticky header), списки переведены на DataGridCard, дашборд на компактные SectionCards.

Co-authored-by: Cursor <[email protected]>
2026-06-30 14:48:24 +07:00

189 lines
5.6 KiB
TypeScript

import { useMemo, useState } from 'react'
import { cn } from '@cfdm/ui/lib/utils'
import { Button } from '@cfdm/ui/components/button'
import { Badge } from '@cfdm/ui/components/badge'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@cfdm/ui/components/tooltip'
import { ConfirmDialog } from '@/components/confirm-dialog'
import {
DataGridCard,
columnDefFromDataGrid,
} from '@/components/data-grid-card'
import type { DataGridColumn } from '@/components/data-grid-types'
import { ListFiltersBar } from '@/components/list-filters-bar'
import { StatusBadge } from '@/components/status-badge'
import type { DnsRecord, IpHealthStatus } from '@/lib/schemas'
import { formatDate } from '@/lib/format'
interface DnsRecordsDataGridProps {
records: DnsRecord[]
onDelete: (recordId: number) => void
isDeleting?: boolean
healthByIp?: Record<string, IpHealthStatus>
search?: string
onSearchChange?: (value: string) => void
hideSearch?: boolean
}
const healthDotClass: Record<IpHealthStatus['status'], string> = {
up: 'bg-success',
degraded: 'bg-warning',
down: 'bg-destructive',
unknown: 'bg-muted-foreground/40',
}
const healthLabel: Record<IpHealthStatus['status'], string> = {
up: 'OK',
degraded: 'Деград.',
down: 'Down',
unknown: '—',
}
function IpHealthDot({ health }: { health: IpHealthStatus }) {
const tooltipParts: string[] = [`Статус: ${healthLabel[health.status]}`]
if (health.latency_ms != null) tooltipParts.push(`Задержка: ${health.latency_ms} мс`)
if (health.last_checked_at) tooltipParts.push(`Проверка: ${formatDate(health.last_checked_at)}`)
if (health.last_error) tooltipParts.push(`Ошибка: ${health.last_error}`)
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger
render={
<span
className={cn(
'inline-flex size-2 shrink-0 cursor-default rounded-full',
healthDotClass[health.status],
)}
aria-label={`Health: ${healthLabel[health.status]}`}
/>
}
/>
<TooltipContent className="whitespace-pre-line">{tooltipParts.join('\n')}</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}
export function DnsRecordsDataGrid({
records,
onDelete,
isDeleting = false,
healthByIp,
search: controlledSearch,
onSearchChange,
hideSearch = false,
}: DnsRecordsDataGridProps) {
const [internalSearch, setInternalSearch] = useState('')
const search = controlledSearch ?? internalSearch
const setSearch = onSearchChange ?? setInternalSearch
const filteredRecords = useMemo(() => {
const query = search.trim().toLowerCase()
if (!query) return records
return records.filter(
(r) =>
r.name.toLowerCase().includes(query) ||
r.content.toLowerCase().includes(query) ||
r.record_type.toLowerCase().includes(query),
)
}, [records, search])
const columns = useMemo<DataGridColumn<DnsRecord>[]>(
() => [
{
key: 'record_type',
header: 'Тип',
sortable: true,
sortValue: (row) => row.record_type,
cell: (row) => <Badge variant="outline">{row.record_type}</Badge>,
},
{
key: 'name',
header: 'Имя',
sortable: true,
sortValue: (row) => row.name,
cell: (row) => <span className="font-medium">{row.name}</span>,
},
{
key: 'content',
header: 'Значение',
sortable: true,
sortValue: (row) => row.content,
cell: (row) => {
const health = healthByIp?.[row.content]
return (
<div className="flex min-w-0 items-center gap-2">
{health ? <IpHealthDot health={health} /> : null}
<span className="truncate font-mono text-sm">{row.content}</span>
</div>
)
},
},
{
key: 'ttl',
header: 'TTL',
sortable: true,
sortValue: (row) => row.ttl,
className: 'tabular-nums',
cell: (row) => row.ttl,
},
{
key: 'sync_status',
header: 'Синхронизация',
cell: (row) => <StatusBadge status={row.sync_status} />,
},
{
key: 'actions',
header: '',
enableHiding: false,
className: 'text-right',
cell: (row) => (
<ConfirmDialog
trigger={
<Button variant="destructive" size="sm" disabled={isDeleting}>
Удалить
</Button>
}
title="Удалить DNS-запись?"
description={`Запись ${row.name} (${row.record_type}) будет удалена из зоны.`}
onConfirm={() => onDelete(row.id)}
/>
),
},
],
[healthByIp, isDeleting, onDelete],
)
return (
<DataGridCard
title="DNS-записи"
description="Записи в зоне"
columns={columnDefFromDataGrid(columns)}
data={filteredRecords}
rowId={(row) => String(row.id)}
emptyTitle="DNS-записи не найдены"
emptyDescription="Создайте запись или измените фильтры"
pinLastColumn
actions={
hideSearch ? undefined : (
<ListFiltersBar
search={{
value: search,
onChange: setSearch,
placeholder: 'Поиск по имени или значению…',
}}
shown={filteredRecords.length}
total={records.length}
showReset={Boolean(search.trim())}
onReset={() => setSearch('')}
/>
)
}
/>
)
}