diff --git a/web/src/hooks/useApi.ts b/web/src/hooks/useApi.ts index e150ed7..a7bccf0 100644 --- a/web/src/hooks/useApi.ts +++ b/web/src/hooks/useApi.ts @@ -100,6 +100,41 @@ export function useImportZones() { onSuccess: () => qc.invalidateQueries({ queryKey: ["domains", project?.id] }), }) } +const CHECK_ALL_CONCURRENCY = 4 + +// Ручной check по всем переданным доменам. Бэкенд (handleCheck) сам пишет +// статус, поэтому после прогона достаточно перечитать список доменов. +// Сбой одной зоны не прерывает остальные — считаем успехи и ошибки. +export function useCheckAllDomains() { + const { project } = useAuth() + const qc = useQueryClient() + return useMutation({ + mutationFn: async (ids: string[]) => { + const pid = requireProjectId(project) + let ok = 0 + let failed = 0 + const queue = [...ids] + const worker = async () => { + for (let id = queue.shift(); id !== undefined; id = queue.shift()) { + try { + await api.checkDomain(pid, id) + ok++ + } catch { + failed++ + } + } + } + await Promise.all( + Array.from({ length: Math.min(CHECK_ALL_CONCURRENCY, ids.length) }, worker), + ) + return { ok, failed } + }, + onSettled: () => { + qc.invalidateQueries({ queryKey: ["domains", project?.id] }) + qc.invalidateQueries({ queryKey: ["check", project?.id] }) + }, + }) +} export function useSetDomainTemplate() { const { project } = useAuth() const qc = useQueryClient() diff --git a/web/src/pages/DomainsPage.test.tsx b/web/src/pages/DomainsPage.test.tsx index 7b9c7dd..959f3f2 100644 --- a/web/src/pages/DomainsPage.test.tsx +++ b/web/src/pages/DomainsPage.test.tsx @@ -234,3 +234,16 @@ test("счётчик показывает, сколько доменов отф expect(screen.getByText(/показано 1 из 2/i)).toBeInTheDocument() }) + +test("«Проверить все зоны» проверяет только домены с шаблоном и обновляет список", async () => { + const check = vi.spyOn(api, "checkDomain").mockResolvedValue({ diffs: [] } as never) + const user = userEvent.setup() + renderPage() + await screen.findByText("example.com.") + + await user.click(screen.getByRole("button", { name: "Проверить все зоны" })) + + await waitFor(() => expect(check).toHaveBeenCalledTimes(1)) + expect(check).toHaveBeenCalledWith(PROJECT_ID, "d2") + expect(await screen.findByText("Проверено 1")).toBeInTheDocument() +}) diff --git a/web/src/pages/DomainsPage.tsx b/web/src/pages/DomainsPage.tsx index fc2aefb..9338ca4 100644 --- a/web/src/pages/DomainsPage.tsx +++ b/web/src/pages/DomainsPage.tsx @@ -1,6 +1,6 @@ import { useState } from "react" import { Link } from "react-router-dom" -import { Inbox, Loader2, Upload } from "lucide-react" +import { GitCompareArrows, Inbox, Loader2, Upload } from "lucide-react" import { Button } from "@/components/ui/button" import { StatusBadge } from "@/components/StatusBadge" import { @@ -20,6 +20,7 @@ import { } from "@/components/ui/table" import { useAccounts, + useCheckAllDomains, useDomains, useImportZones, useSetDomainTemplate, @@ -56,6 +57,7 @@ export function DomainsPage() { const accounts = useAccounts() const templates = useTemplates() const importZones = useImportZones() + const checkAll = useCheckAllDomains() const setTemplate = useSetDomainTemplate() const accountList = accounts.data ?? [] @@ -104,6 +106,13 @@ export function DomainsPage() { importZones.mutate(selectedImportAccount) } + // Диф есть только у доменов с шаблоном; фильтры списка на набор не влияют. + const checkableIds = domainList.filter((d) => d.templateId).map((d) => d.id) + + function onCheckAll() { + checkAll.mutate(checkableIds) + } + function onTemplateChange(domainId: string, value: unknown) { const templateId = value === NO_TEMPLATE ? null : (value as string) setTemplate.mutate({ id: domainId, templateId }) @@ -146,9 +155,27 @@ export function DomainsPage() { )} Импортировать зоны + {importZones.isError && ( {importZones.error.message} )} + {checkAll.isSuccess && ( + + Проверено {checkAll.data.ok} + {checkAll.data.failed > 0 && `, ошибок ${checkAll.data.failed}`} + + )} {importZones.isSuccess && ( Создано {importZones.data.created.length}, удалено {importZones.data.removed.length}