feat: integrate ReUI components and update MCP configuration
Build, Test, and Push CFDM Docker Image / test (push) Failing after 49s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Has been skipped
Build, Test, and Push CFDM Docker Image / update-wiki (push) Has been skipped
Build, Test, and Push CFDM Docker Image / create-release (push) Has been skipped

- Added ReUI configuration to .mcp.json and .cursor/mcp.json for component integration.
- Updated pnpm-lock.yaml with new dependencies including react-phone-number-input and adjustments to existing packages.
- Enhanced SKILL.md documentation for ReUI to clarify usage and features.
- Removed unused components (ChartCard, DataGridCard, etc.) to streamline the codebase.
- Adjusted domain-related components and filters for improved functionality and UI consistency.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-07-10 00:37:40 +07:00
co-authored by Cursor
parent 59163bfab5
commit 29e57666e0
225 changed files with 21908 additions and 4476 deletions
@@ -0,0 +1,109 @@
import { useMemo } from 'react'
import type { ColumnDef } from '@tanstack/react-table'
import { GlobeIcon, SearchIcon } from 'lucide-react'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import { createFilter, type FilterFieldConfig } from '@/components/reui/filters'
import { StatusBadge } from '@/components/status-badge'
import type { Certificate } from '@/lib/schemas'
import { formatDate, formatRelative } from '@/lib/format'
export const CERT_TABS = [
{ id: 'all', label: 'Все' },
{ id: 'active', label: 'Активен' },
{ id: 'warning', label: 'Предупреждение' },
{ id: 'expired', label: 'Истёк' },
] as const
const ACTIVE_STATUSES = new Set(['active', 'ok', 'synced'])
const EXPIRED_STATUSES = new Set(['expired', 'error', 'conflict'])
export function certTabFilter(item: Certificate, tabId: string) {
if (tabId === 'active') return ACTIVE_STATUSES.has(item.status)
if (tabId === 'warning') return item.status === 'warning'
if (tabId === 'expired') return EXPIRED_STATUSES.has(item.status)
return true
}
export function createDefaultCertFilters() {
return [createFilter('hostname', 'contains', [''])]
}
export function useCertFilterFields() {
return useMemo<FilterFieldConfig[]>(
() => [
{
key: 'hostname',
label: 'Хост',
icon: <SearchIcon className="size-3.5" aria-hidden />,
type: 'text',
className: 'w-52',
placeholder: 'Поиск по хосту…',
},
],
[],
)
}
export function certFilterFieldValue(item: Certificate, field: string) {
if (field === 'hostname') {
return `${item.hostname} ${item.status}`.toLowerCase()
}
return ''
}
export function useCertificateColumns() {
return useMemo<ColumnDef<Certificate>[]>(
() => [
{
id: 'hostname',
accessorKey: 'hostname',
header: ({ column }) => (
<DataGridColumnHeader column={column} title="Хост" icon={<GlobeIcon className="size-3.5" />} />
),
cell: ({ row }) => (
<span className="truncate font-medium">{row.original.hostname}</span>
),
},
{
id: 'status',
header: 'Статус',
cell: ({ row }) => <StatusBadge status={row.original.status} />,
},
{
id: 'expires_at',
accessorKey: 'expires_at',
header: ({ column }) => (
<DataGridColumnHeader column={column} title="Истекает" />
),
cell: ({ row }) => (
<span className="text-muted-foreground tabular-nums">
{formatDate(row.original.expires_at)}
</span>
),
},
{
id: 'relative',
header: 'Срок',
cell: ({ row }) => (
<span className="text-muted-foreground tabular-nums">
{formatRelative(row.original.expires_at)}
</span>
),
},
{
id: 'last_checked_at',
accessorKey: 'last_checked_at',
header: ({ column }) => (
<DataGridColumnHeader column={column} title="Проверка" />
),
cell: ({ row }) => (
<span className="text-muted-foreground tabular-nums">
{formatDate(row.original.last_checked_at)}
</span>
),
},
],
[],
)
}
@@ -0,0 +1,210 @@
import { useMemo } from 'react'
import type { ColumnDef } from '@tanstack/react-table'
import { MoreHorizontalIcon, SearchIcon } from 'lucide-react'
import { cn } from '@cfdm/ui/lib/utils'
import { Badge } from '@/components/reui/badge'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import { createFilter, type FilterFieldConfig } from '@/components/reui/filters'
import { StatusBadge } from '@/components/status-badge'
import type { DnsRecord, IpHealthStatus } from '@/lib/schemas'
import { formatDate } from '@/lib/format'
import { Button } from '@cfdm/ui/components/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@cfdm/ui/components/dropdown-menu'
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@cfdm/ui/components/tooltip'
export const DNS_TABS = [
{ id: 'all', label: 'Все' },
{ id: 'proxied', label: 'Proxied' },
{ id: 'dns_only', label: 'DNS only' },
] as const
const DNS_TYPES = ['A', 'AAAA', 'CNAME', 'TXT', 'MX'] as const
export function dnsTabFilter(item: DnsRecord, tabId: string) {
if (tabId === 'proxied') return item.proxied === true
if (tabId === 'dns_only') return item.proxied !== true
return true
}
export function createDefaultDnsFilters() {
return [createFilter('name', 'contains', [''])]
}
export function useDnsFilterFields() {
return useMemo<FilterFieldConfig[]>(
() => [
{
key: 'name',
label: 'Имя',
icon: <SearchIcon className="size-3.5" aria-hidden />,
type: 'text',
className: 'w-52',
placeholder: 'Поиск…',
},
{
key: 'record_type',
label: 'Тип',
type: 'select',
className: 'w-[120px]',
options: DNS_TYPES.map((type) => ({ label: type, value: type })),
},
],
[],
)
}
export function dnsFilterFieldValue(item: DnsRecord, field: string) {
switch (field) {
case 'name':
return `${item.name} ${item.content}`.toLowerCase()
case 'record_type':
return item.record_type
default:
return ''
}
}
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 (
<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>
)
}
export function useDnsColumns({
onDelete,
isDeleting,
healthByIp,
}: {
onDelete: (recordId: number) => void
isDeleting?: boolean
healthByIp?: Record<string, IpHealthStatus>
}) {
return useMemo<ColumnDef<DnsRecord>[]>(
() => [
{
id: 'record_type',
accessorKey: 'record_type',
header: ({ column }) => (
<DataGridColumnHeader column={column} title="Тип" />
),
cell: ({ row }) => (
<Badge variant="outline">{row.original.record_type}</Badge>
),
},
{
id: 'name',
accessorKey: 'name',
header: ({ column }) => (
<DataGridColumnHeader column={column} title="Имя" />
),
cell: ({ row }) => (
<span className="font-medium">{row.original.name}</span>
),
},
{
id: 'content',
accessorKey: 'content',
header: ({ column }) => (
<DataGridColumnHeader column={column} title="Значение" />
),
cell: ({ row }) => {
const health = healthByIp?.[row.original.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.original.content}</span>
</div>
)
},
},
{
id: 'proxied',
header: 'Прокси',
cell: ({ row }) =>
row.original.proxied ? (
<StatusBadge status="active" label="Proxied" />
) : (
<Badge variant="outline">DNS only</Badge>
),
},
{
id: 'ttl',
accessorKey: 'ttl',
header: ({ column }) => (
<DataGridColumnHeader column={column} title="TTL" />
),
cell: ({ row }) => (
<span className="text-muted-foreground tabular-nums">{row.original.ttl}</span>
),
},
{
id: 'actions',
header: '',
enableHiding: false,
cell: ({ row }) => (
<div className="text-right">
<DropdownMenu>
<DropdownMenuTrigger
render={<Button variant="outline" size="icon" className="size-8" />}
>
<MoreHorizontalIcon />
<span className="sr-only">Действия</span>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
variant="destructive"
disabled={isDeleting}
onClick={() => onDelete(row.original.id)}
>
Удалить
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
),
},
],
[healthByIp, isDeleting, onDelete],
)
}
@@ -0,0 +1,221 @@
import { useMemo } from 'react'
import { Link } from '@tanstack/react-router'
import type { ColumnDef } from '@tanstack/react-table'
import { MoreHorizontalIcon, SearchIcon } from 'lucide-react'
import { Badge } from '@/components/reui/badge'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import { createFilter, type FilterFieldConfig } from '@/components/reui/filters'
import { StatusBadge } from '@/components/status-badge'
import type { DomainListItem } from '@/lib/schemas'
import { renderSingleSelectedLabel } from '@/components/reui-kit/filter-utils'
import { Button } from '@cfdm/ui/components/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@cfdm/ui/components/dropdown-menu'
export const DOMAIN_TABS = [
{ id: 'all', label: 'Все' },
{ id: 'with_group', label: 'С группой' },
{ id: 'without_group', label: 'Без группы' },
] as const
export function domainTabFilter(item: DomainListItem, tabId: string) {
if (tabId === 'with_group') return item.group_id != null
if (tabId === 'without_group') return item.group_id == null
return true
}
export function createDefaultDomainFilters() {
return [createFilter('zone_name', 'contains', [''])]
}
export function useDomainFilterFields(
groupOptions: { value: string; label: string }[],
) {
return useMemo<FilterFieldConfig[]>(
() => [
{
key: 'zone_name',
label: 'Зона',
icon: <SearchIcon className="size-3.5" aria-hidden />,
type: 'text',
className: 'w-52',
placeholder: 'Поиск…',
},
{
key: 'group_id',
label: 'Группа',
type: 'select',
searchable: true,
className: 'w-[168px]',
options: groupOptions,
customValueRenderer: (values) =>
renderSingleSelectedLabel(values, groupOptions),
},
],
[groupOptions],
)
}
export function domainFilterFieldValue(item: DomainListItem, field: string) {
switch (field) {
case 'zone_name':
return `${item.zone_name} ${item.group_name ?? ''}`.toLowerCase()
case 'group_id':
return item.group_id != null ? String(item.group_id) : 'none'
default:
return ''
}
}
export function useDomainColumns({
onRequestDelete,
isDeleting,
}: {
onRequestDelete: (domain: DomainListItem) => void
isDeleting?: boolean
}) {
const columns = useMemo<ColumnDef<DomainListItem>[]>(
() => [
{
id: 'zone_name',
accessorKey: 'zone_name',
header: ({ column }) => (
<DataGridColumnHeader column={column} title="Зона" />
),
cell: ({ row }) => (
<Button
variant="link"
className="h-auto p-0 font-medium"
nativeButton={false}
render={
<Link
to="/domains/$domainId"
params={{ domainId: String(row.original.id) }}
/>
}
>
{row.original.zone_name}
</Button>
),
},
{
id: 'group',
header: ({ column }) => (
<DataGridColumnHeader column={column} title="Группа" />
),
cell: ({ row }) => {
const domain = row.original
if (domain.group_id && domain.group_name) {
return (
<Button
variant="link"
className="h-auto p-0"
nativeButton={false}
render={
<Link
to="/groups/$groupId"
params={{ groupId: String(domain.group_id) }}
/>
}
>
{domain.group_name}
</Button>
)
}
return <Badge variant="outline">Без группы</Badge>
},
},
{
id: 'service_count',
accessorKey: 'service_count',
header: ({ column }) => (
<DataGridColumnHeader column={column} title="Сервисы" />
),
cell: ({ row }) => (
<Button
variant="link"
className="h-auto p-0 tabular-nums"
nativeButton={false}
render={
<Link to="/services" search={{ domainId: row.original.id }} />
}
>
{row.original.service_count}
</Button>
),
},
{
id: 'status',
header: 'Статус',
cell: ({ row }) => <StatusBadge status={row.original.status} />,
},
{
id: 'last_synced_at',
accessorKey: 'last_synced_at',
header: ({ column }) => (
<DataGridColumnHeader column={column} title="Синхронизация" />
),
cell: ({ row }) => (
<span className="text-muted-foreground tabular-nums">
{row.original.last_synced_at ?? '—'}
</span>
),
},
{
id: 'actions',
header: '',
enableHiding: false,
cell: ({ row }) => (
<div className="text-right">
<DropdownMenu>
<DropdownMenuTrigger
render={<Button variant="outline" size="icon" className="size-8" />}
>
<MoreHorizontalIcon />
<span className="sr-only">Действия</span>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
render={
<Link
to="/domains/$domainId"
params={{ domainId: String(row.original.id) }}
/>
}
>
Обзор
</DropdownMenuItem>
<DropdownMenuItem
render={
<Link
to="/domains/$domainId/dns"
params={{ domainId: String(row.original.id) }}
search={{ host: undefined }}
/>
}
>
DNS
</DropdownMenuItem>
<DropdownMenuItem
variant="destructive"
disabled={isDeleting}
onClick={() => onRequestDelete(row.original)}
>
Удалить
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
),
},
],
[isDeleting, onRequestDelete],
)
return { columns }
}
@@ -0,0 +1,99 @@
import { useMemo } from 'react'
import { Link } from '@tanstack/react-router'
import type { ColumnDef } from '@tanstack/react-table'
import { GlobeIcon, SearchIcon } from 'lucide-react'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import { createFilter, type FilterFieldConfig } from '@/components/reui/filters'
import { StatusBadge } from '@/components/status-badge'
import type { DomainListItem } from '@/lib/schemas'
import { Button } from '@cfdm/ui/components/button'
export function createDefaultGroupDomainFilters() {
return [createFilter('zone_name', 'contains', [''])]
}
export function useGroupDomainFilterFields() {
return useMemo<FilterFieldConfig[]>(
() => [
{
key: 'zone_name',
label: 'Зона',
icon: <SearchIcon className="size-3.5" aria-hidden />,
type: 'text',
className: 'w-52',
placeholder: 'Поиск…',
},
],
[],
)
}
export function groupDomainFilterFieldValue(item: DomainListItem, field: string) {
if (field === 'zone_name') return item.zone_name.toLowerCase()
return ''
}
export function useGroupDomainColumns() {
return useMemo<ColumnDef<DomainListItem>[]>(
() => [
{
id: 'zone_name',
accessorKey: 'zone_name',
header: ({ column }) => (
<DataGridColumnHeader column={column} title="Зона" icon={<GlobeIcon className="size-3.5" />} />
),
cell: ({ row }) => (
<span className="font-medium">{row.original.zone_name}</span>
),
},
{
id: 'status',
header: 'Статус',
cell: ({ row }) => <StatusBadge status={row.original.status} />,
},
{
id: 'service_count',
accessorKey: 'service_count',
header: ({ column }) => (
<DataGridColumnHeader column={column} title="Сервисы" />
),
cell: ({ row }) => (
<Button
variant="link"
className="h-auto p-0 tabular-nums"
nativeButton={false}
render={
<Link to="/services" search={{ domainId: row.original.id }} />
}
>
{row.original.service_count}
</Button>
),
},
{
id: 'actions',
header: '',
enableHiding: false,
cell: ({ row }) => (
<div className="text-right">
<Button
variant="outline"
size="sm"
nativeButton={false}
render={
<Link
to="/domains/$domainId"
params={{ domainId: String(row.original.id) }}
/>
}
>
Обзор
</Button>
</div>
),
},
],
[],
)
}
@@ -0,0 +1,166 @@
import { useMemo } from 'react'
import { Link } from '@tanstack/react-router'
import type { ColumnDef } from '@tanstack/react-table'
import { MoreHorizontalIcon, SearchIcon } from 'lucide-react'
import { Badge } from '@/components/reui/badge'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import { createFilter, type FilterFieldConfig } from '@/components/reui/filters'
import type { SubdomainTableRow } from '@/hooks/use-domain-page'
import { formatSubdomainServiceLinks } from '@/hooks/use-domain-page'
import { formatDate } from '@/lib/format'
import { Button } from '@cfdm/ui/components/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@cfdm/ui/components/dropdown-menu'
export const SUBDOMAIN_TABS = [
{ id: 'all', label: 'Все' },
{ id: 'active', label: 'Активные' },
] as const
export function subdomainTabFilter(item: SubdomainTableRow, tabId: string) {
if (tabId === 'active') return item.subdomain.enabled
return true
}
export function createDefaultSubdomainFilters() {
return [createFilter('fqdn', 'contains', [''])]
}
export function useSubdomainFilterFields() {
return useMemo<FilterFieldConfig[]>(
() => [
{
key: 'fqdn',
label: 'Поддомен',
icon: <SearchIcon className="size-3.5" aria-hidden />,
type: 'text',
className: 'w-52',
placeholder: 'Поиск…',
},
],
[],
)
}
export function subdomainFilterFieldValue(item: SubdomainTableRow, field: string) {
if (field === 'fqdn') {
return `${item.subdomain.fqdn} ${formatSubdomainServiceLinks(item.serviceLinks)}`.toLowerCase()
}
return ''
}
export function useSubdomainColumns({
domainId,
onEdit,
onDelete,
onToggleEnabled,
isDeleting,
isToggling,
}: {
domainId: string
onEdit: (row: SubdomainTableRow) => void
onDelete: (row: SubdomainTableRow) => void
onToggleEnabled: (row: SubdomainTableRow) => void
isDeleting?: boolean
isToggling?: boolean
}) {
return useMemo<ColumnDef<SubdomainTableRow>[]>(
() => [
{
id: 'fqdn',
accessorFn: (row) => row.subdomain.fqdn,
header: ({ column }) => (
<DataGridColumnHeader column={column} title="Поддомен" />
),
cell: ({ row }) => (
<span className="truncate font-mono">{row.original.subdomain.fqdn}</span>
),
},
{
id: 'status',
header: 'Статус',
cell: ({ row }) => (
<Badge variant={row.original.subdomain.enabled ? 'success' : 'outline'}>
{row.original.subdomain.enabled ? 'Активен' : 'Неактивен'}
</Badge>
),
},
{
id: 'service',
header: 'Группа / Сервис',
accessorFn: (row) => formatSubdomainServiceLinks(row.serviceLinks),
cell: ({ row }) => (
<span className="text-muted-foreground">
{formatSubdomainServiceLinks(row.original.serviceLinks)}
</span>
),
},
{
id: 'created_at',
accessorFn: (row) => row.subdomain.created_at,
header: ({ column }) => (
<DataGridColumnHeader column={column} title="Создан" />
),
cell: ({ row }) => (
<span className="text-muted-foreground tabular-nums">
{formatDate(row.original.subdomain.created_at)}
</span>
),
},
{
id: 'actions',
header: '',
enableHiding: false,
cell: ({ row }) => (
<div className="text-right">
<DropdownMenu>
<DropdownMenuTrigger
render={<Button variant="outline" size="icon" className="size-8" />}
>
<MoreHorizontalIcon />
<span className="sr-only">Действия</span>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => onEdit(row.original)}>
Редактировать
</DropdownMenuItem>
<DropdownMenuItem
disabled={isToggling}
onClick={() => onToggleEnabled(row.original)}
>
{row.original.subdomain.enabled ? 'Деактивировать' : 'Активировать'}
</DropdownMenuItem>
<DropdownMenuItem
render={
<Link
to="/domains/$domainId/dns"
params={{ domainId }}
search={{ host: row.original.subdomain.name }}
/>
}
>
DNS
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
variant="destructive"
disabled={isDeleting}
onClick={() => onDelete(row.original)}
>
Удалить
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
),
},
],
[domainId, isDeleting, isToggling, onDelete, onEdit, onToggleEnabled],
)
}