Update pnpm-lock.yaml to include new dependencies for @dnd-kit packages; enhance frontend documentation with MCP patterns and shadcn guidelines; refactor route handling for groups and services; implement new DataTableCard and KanbanBoard components for better domain and service management; add service binding functionality and improve DNS record management with new schemas and queries.
Build, Test, and Push CFDM Docker Image / test (push) Failing after 16h45m4s
Build, Test, and Push CFDM Docker Image / create-release (push) Has been cancelled
Build, Test, and Push CFDM Docker Image / build-and-push (push) Has been cancelled
Build, Test, and Push CFDM Docker Image / update-wiki (push) Has been cancelled

This commit is contained in:
Denozordec
2026-06-15 16:41:10 +07:00
parent b1467575c5
commit 5f7b3ac388
50 changed files with 3364 additions and 367 deletions
+112 -89
View File
@@ -6,15 +6,20 @@ import { zodResolver } from '@hookform/resolvers/zod'
import { toast } from 'sonner'
import { api } from '@/lib/api-client'
import { createDomainSchema, type CreateDomainInput } from '@/lib/schemas'
import { domainKeys, domainsListQueryOptions, groupsQueryOptions } from '@/queries'
import { buildIpsByDomainId } from '@/lib/domain-ips'
import {
domainKeys,
domainsListQueryOptions,
groupsQueryOptions,
serviceBindingsQueryOptions,
} from '@/queries'
import { PageHeader } from '@/components/page-header'
import { StatusBadge } from '@/components/status-badge'
import { DataTableCard } from '@/components/data-table-card'
import { DomainsDataTable } from '@/components/domains-data-table'
import { Button } from '@cfdm/ui/components/button'
import { Input } from '@cfdm/ui/components/input'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
@@ -32,13 +37,13 @@ import {
SelectValue,
} from '@cfdm/ui/components/select'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@cfdm/ui/components/table'
Sheet,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
} from '@cfdm/ui/components/sheet'
import { Spinner } from '@cfdm/ui/components/spinner'
export const Route = createFileRoute('/_auth/domains/')({
@@ -46,16 +51,26 @@ export const Route = createFileRoute('/_auth/domains/')({
Promise.all([
queryClient.ensureQueryData(domainsListQueryOptions()),
queryClient.ensureQueryData(groupsQueryOptions()),
queryClient.ensureQueryData(serviceBindingsQueryOptions()),
]),
component: DomainsPage,
})
function DomainsPage() {
const [groupId, setGroupId] = useState('')
const filterGroupId = groupId ? Number(groupId) : undefined
const [sheetOpen, setSheetOpen] = useState(false)
const [importGroupId, setImportGroupId] = useState('')
const [filterGroupId, setFilterGroupId] = useState('')
const listGroupId =
filterGroupId && filterGroupId !== 'none' ? Number(filterGroupId) : undefined
const queryClient = useQueryClient()
const { data: domains } = useQuery(domainsListQueryOptions(filterGroupId))
const { data: domains } = useQuery(domainsListQueryOptions(listGroupId))
const { data: groups } = useQuery(groupsQueryOptions())
const { data: bindings } = useQuery(serviceBindingsQueryOptions())
const ipsByDomainId = useMemo(
() => buildIpsByDomainId(bindings ?? []),
[bindings],
)
const groupItems = useMemo(
() => [
@@ -75,7 +90,8 @@ function DomainsPage() {
api.post('/api/v1/domains', body),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: domainKeys.all })
form.reset({ zone_name: '', group_id: groupId })
form.reset({ zone_name: '', group_id: importGroupId })
setSheetOpen(false)
toast.success('Домен импортирован')
},
onError: (err) => {
@@ -86,31 +102,89 @@ function DomainsPage() {
const handleCreate = form.handleSubmit((values) => {
createMutation.mutate({
zone_name: values.zone_name.trim(),
group_id: groupId ? Number(groupId) : undefined,
group_id: importGroupId ? Number(importGroupId) : undefined,
})
})
const handleGroupChange = (value: string | null) => {
const handleImportGroupChange = (value: string | null) => {
const next = value === 'none' || !value ? '' : value
setGroupId(next)
setImportGroupId(next)
form.setValue('group_id', next)
}
const handleFilterGroupChange = (value: string | null) => {
if (!value || value === 'all') {
setFilterGroupId('')
return
}
setFilterGroupId(value === 'none' ? 'none' : value)
}
const filteredDomains = useMemo(() => {
if (filterGroupId === 'none') {
return domains?.filter((d) => d.group_id === null) ?? []
}
return domains ?? []
}, [domains, filterGroupId])
const tableData = useMemo(
() =>
filteredDomains.map((domain) => ({
...domain,
ips: ipsByDomainId.get(domain.id) ?? [],
})),
[filteredDomains, ipsByDomainId],
)
return (
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
<div className="@container/main flex flex-col gap-4 py-4 md:gap-6 md:py-6">
<PageHeader
title="Домены"
description="Импорт и управление зонами Cloudflare"
actions={
<>
<Button variant="outline" render={<Link to="/groups" />}>
Канбан групп
</Button>
<Button onClick={() => setSheetOpen(true)}>Импортировать домен</Button>
</>
}
/>
<Card>
<CardHeader>
<CardTitle>Добавить домен</CardTitle>
<CardDescription>Импортировать зону из Cloudflare</CardDescription>
<Card className="border-dashed">
<CardHeader className="pb-3">
<CardTitle className="text-base">Быстрый обзор</CardTitle>
<CardDescription>
{tableData.length} зон ·{' '}
{tableData.filter((d) => d.ips.length > 0).length} с IP-адресами ·{' '}
{tableData.reduce((sum, d) => sum + d.service_count, 0)} привязок сервисов
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleCreate}>
<FieldGroup className="flex flex-row flex-wrap items-end gap-2">
<Field className="max-w-xs flex-1">
</Card>
<DataTableCard
title="Список доменов"
description="Импортированные зоны Cloudflare"
>
<DomainsDataTable
data={tableData}
groupFilterItems={groupItems}
groupFilterValue={filterGroupId || 'all'}
onGroupFilterChange={handleFilterGroupChange}
/>
</DataTableCard>
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
<SheetContent>
<SheetHeader>
<SheetTitle>Импорт домена</SheetTitle>
<SheetDescription>
Добавить зону из аккаунта Cloudflare в менеджер
</SheetDescription>
</SheetHeader>
<form onSubmit={handleCreate} className="flex flex-col gap-4 px-4">
<FieldGroup>
<Field>
<FieldLabel htmlFor="zone_name">Имя зоны</FieldLabel>
<Input
id="zone_name"
@@ -119,14 +193,14 @@ function DomainsPage() {
aria-invalid={!!form.formState.errors.zone_name}
/>
</Field>
<Field className="w-48">
<FieldLabel htmlFor="group_id">Группа</FieldLabel>
<Field>
<FieldLabel htmlFor="import_group_id">Группа</FieldLabel>
<Select
items={groupItems}
value={groupId || 'none'}
onValueChange={handleGroupChange}
value={importGroupId || 'none'}
onValueChange={handleImportGroupChange}
>
<SelectTrigger id="group_id" className="w-full">
<SelectTrigger id="import_group_id" className="w-full">
<SelectValue placeholder="Без группы" />
</SelectTrigger>
<SelectContent>
@@ -138,67 +212,16 @@ function DomainsPage() {
</SelectContent>
</Select>
</Field>
<Button type="submit" disabled={createMutation.isPending}>
{createMutation.isPending && (
<Spinner data-icon="inline-start" />
)}
</FieldGroup>
<SheetFooter>
<Button type="submit" disabled={createMutation.isPending} className="w-full">
{createMutation.isPending && <Spinner data-icon="inline-start" />}
{createMutation.isPending ? 'Импорт…' : 'Импортировать'}
</Button>
</FieldGroup>
</SheetFooter>
</form>
</CardContent>
</Card>
<DataTableCard title="Список доменов" description="Импортированные зоны">
<Table>
<TableHeader>
<TableRow>
<TableHead>Зона</TableHead>
<TableHead>Статус</TableHead>
<TableHead>Последняя синхронизация</TableHead>
<TableHead>Действия</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{domains?.map((d) => (
<TableRow key={d.id}>
<TableCell className="font-medium">{d.zone_name}</TableCell>
<TableCell>
<StatusBadge status={d.status} />
</TableCell>
<TableCell>{d.last_synced_at ?? '—'}</TableCell>
<TableCell>
<div className="flex gap-2">
<Button
variant="link"
className="h-auto p-0"
render={
<Link
to="/domains/$domainId"
params={{ domainId: String(d.id) }}
/>
}
>
Обзор
</Button>
<Button
variant="link"
className="h-auto p-0"
render={
<Link
to="/domains/$domainId/dns"
params={{ domainId: String(d.id) }}
/>
}
>
DNS
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</DataTableCard>
</SheetContent>
</Sheet>
</div>
)
}