feat(web): api client and hooks for custom record marks
This commit is contained in:
@@ -83,6 +83,16 @@ export const api = {
|
||||
req<ChangesetResponse>(projectPath(projectId, `/domains/${id}/check`)),
|
||||
applyDomain: (projectId: string, id: string, body: ApplyRequest) =>
|
||||
req<ChangesetResponse>(projectPath(projectId, `/domains/${id}/apply`), { method: "POST", body: JSON.stringify(body) }),
|
||||
addCustom: (projectId: string, id: string, key: string) =>
|
||||
req<void>(projectPath(projectId, `/domains/${id}/customs`), {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ key }),
|
||||
}),
|
||||
removeCustom: (projectId: string, id: string, key: string) =>
|
||||
req<void>(
|
||||
projectPath(projectId, `/domains/${id}/customs?key=${encodeURIComponent(key)}`),
|
||||
{ method: "DELETE" },
|
||||
),
|
||||
domainHistory: (projectId: string, id: string) =>
|
||||
req<CheckRun[]>(projectPath(projectId, `/domains/${id}/history`)),
|
||||
|
||||
|
||||
@@ -45,10 +45,12 @@ export interface RecordView {
|
||||
desired?: string[]
|
||||
actual?: string[]
|
||||
readOnly: boolean
|
||||
custom: boolean // помечена оператором как живущая вне шаблона
|
||||
}
|
||||
export interface ChangesetResponse {
|
||||
updates: RecordView[]
|
||||
prunes: RecordView[]
|
||||
customs: RecordView[]
|
||||
readOnly: RecordView[]
|
||||
inSyncCount: number
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ import { DiffView } from "./DiffView"
|
||||
import type { ChangesetResponse } from "@/api/types"
|
||||
|
||||
const cs: ChangesetResponse = {
|
||||
updates: [{ key: "A www.example.com.", kind: "update", type: "A", name: "www.example.com.", desired: ["1.1.1.1"], actual: ["9.9.9.9"], readOnly: false }],
|
||||
prunes: [{ key: "A old.example.com.", kind: "delete", type: "A", name: "old.example.com.", actual: ["2.2.2.2"], readOnly: false }],
|
||||
readOnly: [{ key: "NS example.com.", kind: "update", type: "NS", name: "example.com.", desired: ["ns1."], actual: ["ns2."], readOnly: true }],
|
||||
updates: [{ key: "A www.example.com.", kind: "update", type: "A", name: "www.example.com.", desired: ["1.1.1.1"], actual: ["9.9.9.9"], readOnly: false, custom: false }],
|
||||
prunes: [{ key: "A old.example.com.", kind: "delete", type: "A", name: "old.example.com.", actual: ["2.2.2.2"], readOnly: false, custom: false }],
|
||||
customs: [],
|
||||
readOnly: [{ key: "NS example.com.", kind: "update", type: "NS", name: "example.com.", desired: ["ns1."], actual: ["ns2."], readOnly: true, custom: false }],
|
||||
inSyncCount: 3,
|
||||
}
|
||||
|
||||
@@ -60,9 +61,11 @@ test("renders a very long unbreakable value (DKIM key) without crashing", () =>
|
||||
desired: [longValue],
|
||||
actual: [],
|
||||
readOnly: false,
|
||||
custom: false,
|
||||
},
|
||||
],
|
||||
prunes: [],
|
||||
customs: [],
|
||||
readOnly: [],
|
||||
inSyncCount: 0,
|
||||
}
|
||||
@@ -135,11 +138,12 @@ test("select-all header checkbox calls onToggleAllUpdates(true) when clicked whi
|
||||
test("select-all header checkbox is indeterminate when only some update rows are selected", () => {
|
||||
const csWithMultipleUpdates: ChangesetResponse = {
|
||||
updates: [
|
||||
{ key: "A www.example.com.", kind: "update", type: "A", name: "www.example.com.", desired: ["1.1.1.1"], actual: ["9.9.9.9"], readOnly: false },
|
||||
{ key: "A api.example.com.", kind: "update", type: "A", name: "api.example.com.", desired: ["1.1.1.2"], actual: ["9.9.9.8"], readOnly: false },
|
||||
{ key: "A cdn.example.com.", kind: "update", type: "A", name: "cdn.example.com.", desired: ["1.1.1.3"], actual: ["9.9.9.7"], readOnly: false },
|
||||
{ key: "A www.example.com.", kind: "update", type: "A", name: "www.example.com.", desired: ["1.1.1.1"], actual: ["9.9.9.9"], readOnly: false, custom: false },
|
||||
{ key: "A api.example.com.", kind: "update", type: "A", name: "api.example.com.", desired: ["1.1.1.2"], actual: ["9.9.9.8"], readOnly: false, custom: false },
|
||||
{ key: "A cdn.example.com.", kind: "update", type: "A", name: "cdn.example.com.", desired: ["1.1.1.3"], actual: ["9.9.9.7"], readOnly: false, custom: false },
|
||||
],
|
||||
prunes: [],
|
||||
customs: [],
|
||||
readOnly: [],
|
||||
inSyncCount: 0,
|
||||
}
|
||||
|
||||
@@ -153,6 +153,36 @@ export function useApplyDomain(id: string) {
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["check", project?.id, id] }),
|
||||
})
|
||||
}
|
||||
export function useAddCustom(id: string) {
|
||||
const { project } = useAuth()
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (key: string) => {
|
||||
const pid = requireProjectId(project)
|
||||
return api.addCustom(pid, id, key)
|
||||
},
|
||||
// Дифф пересчитывается на бэкенде — инвалидируем чек, а заодно статус
|
||||
// домена в списке (custom-запись перестаёт быть дрифтом).
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ["check", project?.id, id] })
|
||||
qc.invalidateQueries({ queryKey: ["domains", project?.id] })
|
||||
},
|
||||
})
|
||||
}
|
||||
export function useRemoveCustom(id: string) {
|
||||
const { project } = useAuth()
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (key: string) => {
|
||||
const pid = requireProjectId(project)
|
||||
return api.removeCustom(pid, id, key)
|
||||
},
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ["check", project?.id, id] })
|
||||
qc.invalidateQueries({ queryKey: ["domains", project?.id] })
|
||||
},
|
||||
})
|
||||
}
|
||||
export function useDomainHistory(id: string) {
|
||||
const { project } = useAuth()
|
||||
return useQuery({
|
||||
|
||||
@@ -43,11 +43,12 @@ beforeEach(() => {
|
||||
|
||||
test("default selection: updates checked, prunes unchecked; apply sends only selected keys", async () => {
|
||||
vi.spyOn(api, "checkDomain").mockResolvedValue({
|
||||
updates: [{ key: "A a.", kind: "update", type: "A", name: "a.", desired: ["1"], actual: ["2"], readOnly: false }],
|
||||
prunes: [{ key: "A b.", kind: "delete", type: "A", name: "b.", actual: ["3"], readOnly: false }],
|
||||
updates: [{ key: "A a.", kind: "update", type: "A", name: "a.", desired: ["1"], actual: ["2"], readOnly: false, custom: false }],
|
||||
prunes: [{ key: "A b.", kind: "delete", type: "A", name: "b.", actual: ["3"], readOnly: false, custom: false }],
|
||||
customs: [],
|
||||
readOnly: [], inSyncCount: 0,
|
||||
})
|
||||
const applySpy = vi.spyOn(api, "applyDomain").mockResolvedValue({ updates: [], prunes: [], readOnly: [], inSyncCount: 0 })
|
||||
const applySpy = vi.spyOn(api, "applyDomain").mockResolvedValue({ updates: [], prunes: [], customs: [], readOnly: [], inSyncCount: 0 })
|
||||
const zoneRecordsSpy = vi.spyOn(api, "zoneRecords")
|
||||
const user = userEvent.setup()
|
||||
renderPage()
|
||||
@@ -78,11 +79,12 @@ test("default selection: updates checked, prunes unchecked; apply sends only sel
|
||||
|
||||
test("deselecting all records disables Apply", async () => {
|
||||
vi.spyOn(api, "checkDomain").mockResolvedValue({
|
||||
updates: [{ key: "A a.", kind: "update", type: "A", name: "a.", desired: ["1"], actual: ["2"], readOnly: false }],
|
||||
updates: [{ key: "A a.", kind: "update", type: "A", name: "a.", desired: ["1"], actual: ["2"], readOnly: false, custom: false }],
|
||||
prunes: [],
|
||||
customs: [],
|
||||
readOnly: [], inSyncCount: 0,
|
||||
})
|
||||
vi.spyOn(api, "applyDomain").mockResolvedValue({ updates: [], prunes: [], readOnly: [], inSyncCount: 0 })
|
||||
vi.spyOn(api, "applyDomain").mockResolvedValue({ updates: [], prunes: [], customs: [], readOnly: [], inSyncCount: 0 })
|
||||
const user = userEvent.setup()
|
||||
renderPage()
|
||||
|
||||
@@ -105,7 +107,7 @@ test("пока список доменов грузится — показан
|
||||
}),
|
||||
)
|
||||
const checkSpy = vi.spyOn(api, "checkDomain").mockResolvedValue({
|
||||
updates: [], prunes: [], readOnly: [], inSyncCount: 0,
|
||||
updates: [], prunes: [], customs: [], readOnly: [], inSyncCount: 0,
|
||||
})
|
||||
const zoneRecordsSpy = vi.spyOn(api, "zoneRecords")
|
||||
renderPage()
|
||||
|
||||
Reference in New Issue
Block a user