Build, Test, and Push CFDM Docker Image / test (push) Successful in 3m4s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Successful in 1m45s
Build, Test, and Push CFDM Docker Image / create-release (push) Skipped
Build, Test, and Push CFDM Docker Image / update-wiki (push) Successful in 6s
Заголовок раздела над KPI/контентом; FrameTitle text-sm больше не заменяет page-level h1. Co-authored-by: Cursor <[email protected]>
293 lines
9.8 KiB
TypeScript
293 lines
9.8 KiB
TypeScript
import { createFileRoute } from '@tanstack/react-router'
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
import { useCallback, useMemo, useState } from 'react'
|
|
import { useForm } from 'react-hook-form'
|
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
import { toast } from 'sonner'
|
|
import type { Filter } from '@/components/reui/filters'
|
|
import { api } from '@/lib/api-client'
|
|
import { createDomainSchema, type BulkUpdateDomainsInput, type CreateDomainInput } from '@/lib/schemas'
|
|
import {
|
|
bulkUpdateDomains,
|
|
domainKeys,
|
|
domainsListQueryOptions,
|
|
groupsQueryOptions,
|
|
serviceBindingKeys,
|
|
} from '@/queries'
|
|
import { PageShell } from '@/components/page-shell'
|
|
import { PageHeader } from '@/components/page-header'
|
|
import { DomainKpiCards } from '@/components/domain-kpi-cards'
|
|
import { ResourcePage } from '@/components/reui-kit'
|
|
import {
|
|
DOMAIN_TABS,
|
|
createDefaultDomainFilters,
|
|
domainFilterFieldValue,
|
|
domainTabFilter,
|
|
useDomainColumns,
|
|
useDomainFilterFields,
|
|
} from '@/components/columns/domains-columns'
|
|
import { DomainsBulkToolbar } from '@/components/domains/domains-bulk-toolbar'
|
|
import { FormSheet } from '@/components/form-sheet'
|
|
import { FormFieldSimple } from '@/components/form-field'
|
|
import { LoadingButton } from '@/components/loading-button'
|
|
import { ConfirmDialog } from '@/components/confirm-dialog'
|
|
import { Button } from '@cfdm/ui/components/button'
|
|
import { Input } from '@cfdm/ui/components/input'
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@cfdm/ui/components/select'
|
|
import { FieldGroup } from '@cfdm/ui/components/field'
|
|
import type { DomainListItem } from '@/lib/schemas'
|
|
|
|
export const Route = createFileRoute('/_auth/domains/')({
|
|
loader: ({ context: { queryClient } }) =>
|
|
Promise.all([
|
|
queryClient.ensureQueryData(domainsListQueryOptions()),
|
|
queryClient.ensureQueryData(groupsQueryOptions()),
|
|
]),
|
|
component: DomainsPage,
|
|
})
|
|
|
|
function DomainsPage() {
|
|
const [sheetOpen, setSheetOpen] = useState(false)
|
|
const [importGroupId, setImportGroupId] = useState('')
|
|
const [deleteTarget, setDeleteTarget] = useState<DomainListItem | null>(null)
|
|
const [filters, setFilters] = useState<Filter[]>(createDefaultDomainFilters)
|
|
const [activeTab, setActiveTab] = useState('all')
|
|
const queryClient = useQueryClient()
|
|
const handleSelectTab = useCallback((tabId: string) => setActiveTab(tabId), [])
|
|
|
|
const {
|
|
data: domains,
|
|
isLoading,
|
|
isError,
|
|
error,
|
|
refetch,
|
|
} = useQuery(domainsListQueryOptions())
|
|
const { data: groups } = useQuery(groupsQueryOptions())
|
|
|
|
const groupItems = useMemo(
|
|
() => [
|
|
{ label: 'Без группы', value: 'none' },
|
|
...(groups?.map((g) => ({ label: g.name, value: String(g.id) })) ?? []),
|
|
],
|
|
[groups],
|
|
)
|
|
|
|
const filterFields = useDomainFilterFields(groupItems)
|
|
|
|
const form = useForm<CreateDomainInput>({
|
|
resolver: zodResolver(createDomainSchema),
|
|
defaultValues: { zone_name: '', group_id: '' },
|
|
})
|
|
|
|
const createMutation = useMutation({
|
|
mutationFn: (body: { zone_name: string; group_id?: number }) =>
|
|
api.post('/api/v1/domains', body),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: domainKeys.all })
|
|
form.reset({ zone_name: '', group_id: importGroupId })
|
|
setSheetOpen(false)
|
|
toast.success('Домен импортирован')
|
|
},
|
|
onError: (err) => {
|
|
toast.error(err instanceof Error ? err.message : 'Не удалось импортировать домен')
|
|
},
|
|
})
|
|
|
|
const deleteMutation = useMutation({
|
|
mutationFn: (id: number) => api.delete(`/api/v1/domains/${id}`),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: domainKeys.all })
|
|
queryClient.invalidateQueries({ queryKey: serviceBindingKeys.all })
|
|
setDeleteTarget(null)
|
|
toast.success('Зона удалена')
|
|
},
|
|
onError: (err) => {
|
|
toast.error(err instanceof Error ? err.message : 'Не удалось удалить зону')
|
|
},
|
|
})
|
|
|
|
const bulkMutation = useMutation({
|
|
mutationFn: (body: BulkUpdateDomainsInput) => bulkUpdateDomains(body),
|
|
onSuccess: (data) => {
|
|
queryClient.invalidateQueries({ queryKey: domainKeys.all })
|
|
toast.success(`Обновлено: ${data.updated}`)
|
|
},
|
|
onError: (err) => {
|
|
toast.error(err instanceof Error ? err.message : 'Не удалось обновить')
|
|
},
|
|
})
|
|
|
|
const { columns } = useDomainColumns({
|
|
onRequestDelete: setDeleteTarget,
|
|
isDeleting: deleteMutation.isPending,
|
|
enableSelection: true,
|
|
})
|
|
|
|
const handleCreate = (values: CreateDomainInput) => {
|
|
createMutation.mutate({
|
|
zone_name: values.zone_name.trim(),
|
|
group_id: importGroupId ? Number(importGroupId) : undefined,
|
|
})
|
|
}
|
|
|
|
const handleImportGroupChange = (value: string | null) => {
|
|
const next = value === 'none' || !value ? '' : value
|
|
setImportGroupId(next)
|
|
form.setValue('group_id', next)
|
|
}
|
|
|
|
const primaryAction = (
|
|
<Button type="button" onClick={() => setSheetOpen(true)}>
|
|
Импортировать домен
|
|
</Button>
|
|
)
|
|
|
|
return (
|
|
<PageShell>
|
|
<PageHeader
|
|
title="Домены"
|
|
description="Импортированные зоны Cloudflare"
|
|
actions={primaryAction}
|
|
/>
|
|
<DomainKpiCards
|
|
domains={domains}
|
|
activeTab={activeTab}
|
|
isLoading={isLoading && !domains}
|
|
onSelectTab={handleSelectTab}
|
|
/>
|
|
<ResourcePage
|
|
title="Домены"
|
|
description="Импортированные зоны Cloudflare"
|
|
hideHeader
|
|
tabs={DOMAIN_TABS.map((tab) => ({ ...tab }))}
|
|
activeTab={activeTab}
|
|
onTabChange={setActiveTab}
|
|
tabFilter={domainTabFilter}
|
|
filterFields={filterFields}
|
|
filters={filters}
|
|
onFiltersChange={setFilters}
|
|
onClearFilters={() => setFilters(createDefaultDomainFilters())}
|
|
getFilterFieldValue={domainFilterFieldValue}
|
|
columns={columns}
|
|
data={domains ?? []}
|
|
getRowId={(row) => String(row.id)}
|
|
isLoading={isLoading}
|
|
isError={isError}
|
|
error={error}
|
|
onRetry={refetch}
|
|
enableRowSelection
|
|
selectionToolbar={({ selectedIds, clearSelection }) => (
|
|
<DomainsBulkToolbar
|
|
count={selectedIds.length}
|
|
isPending={bulkMutation.isPending}
|
|
groupItems={groupItems}
|
|
onAssignGroup={(groupId) => {
|
|
bulkMutation.mutate(
|
|
{ ids: selectedIds.map(Number), group_id: groupId },
|
|
{ onSuccess: () => clearSelection() },
|
|
)
|
|
}}
|
|
onSetEnvironment={(environment) => {
|
|
bulkMutation.mutate(
|
|
{ ids: selectedIds.map(Number), environment },
|
|
{ onSuccess: () => clearSelection() },
|
|
)
|
|
}}
|
|
onAddTag={(tag) => {
|
|
bulkMutation.mutate(
|
|
{ ids: selectedIds.map(Number), tags_add: [tag] },
|
|
{ onSuccess: () => clearSelection() },
|
|
)
|
|
}}
|
|
onClear={clearSelection}
|
|
/>
|
|
)}
|
|
emptyState={{
|
|
title: 'Домены не импортированы',
|
|
description: 'Импортируйте зону из аккаунта Cloudflare',
|
|
action: (
|
|
<Button type="button" onClick={() => setSheetOpen(true)}>
|
|
Импортировать домен
|
|
</Button>
|
|
),
|
|
}}
|
|
/>
|
|
|
|
<FormSheet
|
|
open={sheetOpen}
|
|
onOpenChange={setSheetOpen}
|
|
title="Импорт домена"
|
|
description="Добавить зону из аккаунта Cloudflare в менеджер"
|
|
form={form}
|
|
onSubmit={handleCreate}
|
|
footer={
|
|
<LoadingButton
|
|
type="submit"
|
|
className="w-full"
|
|
isLoading={createMutation.isPending}
|
|
loadingLabel="Импорт…"
|
|
>
|
|
Импортировать
|
|
</LoadingButton>
|
|
}
|
|
>
|
|
<FieldGroup>
|
|
<FormFieldSimple
|
|
label="Имя зоны"
|
|
htmlFor="zone_name"
|
|
error={form.formState.errors.zone_name}
|
|
>
|
|
<Input
|
|
id="zone_name"
|
|
placeholder="example.com"
|
|
{...form.register('zone_name')}
|
|
aria-invalid={!!form.formState.errors.zone_name}
|
|
/>
|
|
</FormFieldSimple>
|
|
<FormFieldSimple label="Группа" htmlFor="import_group_id">
|
|
<Select
|
|
items={groupItems}
|
|
value={importGroupId || 'none'}
|
|
onValueChange={handleImportGroupChange}
|
|
>
|
|
<SelectTrigger id="import_group_id" className="w-full">
|
|
<SelectValue placeholder="Без группы" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{groupItems.map((item) => (
|
|
<SelectItem key={item.value} value={item.value}>
|
|
{item.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</FormFieldSimple>
|
|
</FieldGroup>
|
|
</FormSheet>
|
|
|
|
<ConfirmDialog
|
|
open={deleteTarget !== null}
|
|
onOpenChange={(open) => {
|
|
if (!open) setDeleteTarget(null)
|
|
}}
|
|
title="Удалить зону?"
|
|
description={
|
|
deleteTarget
|
|
? `Зона «${deleteTarget.zone_name}» будет удалена из менеджера вместе с DNS-записями и привязками. Зона в Cloudflare не затрагивается.`
|
|
: ''
|
|
}
|
|
onConfirm={() => {
|
|
if (deleteTarget) deleteMutation.mutate(deleteTarget.id)
|
|
}}
|
|
disabled={deleteMutation.isPending}
|
|
/>
|
|
</PageShell>
|
|
)
|
|
}
|