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`)),
|
req<ChangesetResponse>(projectPath(projectId, `/domains/${id}/check`)),
|
||||||
applyDomain: (projectId: string, id: string, body: ApplyRequest) =>
|
applyDomain: (projectId: string, id: string, body: ApplyRequest) =>
|
||||||
req<ChangesetResponse>(projectPath(projectId, `/domains/${id}/apply`), { method: "POST", body: JSON.stringify(body) }),
|
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) =>
|
domainHistory: (projectId: string, id: string) =>
|
||||||
req<CheckRun[]>(projectPath(projectId, `/domains/${id}/history`)),
|
req<CheckRun[]>(projectPath(projectId, `/domains/${id}/history`)),
|
||||||
|
|
||||||
|
|||||||
@@ -45,10 +45,12 @@ export interface RecordView {
|
|||||||
desired?: string[]
|
desired?: string[]
|
||||||
actual?: string[]
|
actual?: string[]
|
||||||
readOnly: boolean
|
readOnly: boolean
|
||||||
|
custom: boolean // помечена оператором как живущая вне шаблона
|
||||||
}
|
}
|
||||||
export interface ChangesetResponse {
|
export interface ChangesetResponse {
|
||||||
updates: RecordView[]
|
updates: RecordView[]
|
||||||
prunes: RecordView[]
|
prunes: RecordView[]
|
||||||
|
customs: RecordView[]
|
||||||
readOnly: RecordView[]
|
readOnly: RecordView[]
|
||||||
inSyncCount: number
|
inSyncCount: number
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import { DiffView } from "./DiffView"
|
|||||||
import type { ChangesetResponse } from "@/api/types"
|
import type { ChangesetResponse } from "@/api/types"
|
||||||
|
|
||||||
const cs: ChangesetResponse = {
|
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 }],
|
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 }],
|
prunes: [{ key: "A old.example.com.", kind: "delete", type: "A", name: "old.example.com.", actual: ["2.2.2.2"], readOnly: false, custom: false }],
|
||||||
readOnly: [{ key: "NS example.com.", kind: "update", type: "NS", name: "example.com.", desired: ["ns1."], actual: ["ns2."], readOnly: true }],
|
customs: [],
|
||||||
|
readOnly: [{ key: "NS example.com.", kind: "update", type: "NS", name: "example.com.", desired: ["ns1."], actual: ["ns2."], readOnly: true, custom: false }],
|
||||||
inSyncCount: 3,
|
inSyncCount: 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,9 +61,11 @@ test("renders a very long unbreakable value (DKIM key) without crashing", () =>
|
|||||||
desired: [longValue],
|
desired: [longValue],
|
||||||
actual: [],
|
actual: [],
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
|
custom: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
prunes: [],
|
prunes: [],
|
||||||
|
customs: [],
|
||||||
readOnly: [],
|
readOnly: [],
|
||||||
inSyncCount: 0,
|
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", () => {
|
test("select-all header checkbox is indeterminate when only some update rows are selected", () => {
|
||||||
const csWithMultipleUpdates: ChangesetResponse = {
|
const csWithMultipleUpdates: ChangesetResponse = {
|
||||||
updates: [
|
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 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 },
|
{ 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 },
|
{ 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: [],
|
prunes: [],
|
||||||
|
customs: [],
|
||||||
readOnly: [],
|
readOnly: [],
|
||||||
inSyncCount: 0,
|
inSyncCount: 0,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,6 +153,36 @@ export function useApplyDomain(id: string) {
|
|||||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["check", project?.id, id] }),
|
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) {
|
export function useDomainHistory(id: string) {
|
||||||
const { project } = useAuth()
|
const { project } = useAuth()
|
||||||
return useQuery({
|
return useQuery({
|
||||||
|
|||||||
@@ -43,11 +43,12 @@ beforeEach(() => {
|
|||||||
|
|
||||||
test("default selection: updates checked, prunes unchecked; apply sends only selected keys", async () => {
|
test("default selection: updates checked, prunes unchecked; apply sends only selected keys", async () => {
|
||||||
vi.spyOn(api, "checkDomain").mockResolvedValue({
|
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: [{ key: "A b.", kind: "delete", type: "A", name: "b.", actual: ["3"], readOnly: false }],
|
prunes: [{ key: "A b.", kind: "delete", type: "A", name: "b.", actual: ["3"], readOnly: false, custom: false }],
|
||||||
|
customs: [],
|
||||||
readOnly: [], inSyncCount: 0,
|
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 zoneRecordsSpy = vi.spyOn(api, "zoneRecords")
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
renderPage()
|
renderPage()
|
||||||
@@ -78,11 +79,12 @@ test("default selection: updates checked, prunes unchecked; apply sends only sel
|
|||||||
|
|
||||||
test("deselecting all records disables Apply", async () => {
|
test("deselecting all records disables Apply", async () => {
|
||||||
vi.spyOn(api, "checkDomain").mockResolvedValue({
|
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: [],
|
prunes: [],
|
||||||
|
customs: [],
|
||||||
readOnly: [], inSyncCount: 0,
|
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()
|
const user = userEvent.setup()
|
||||||
renderPage()
|
renderPage()
|
||||||
|
|
||||||
@@ -105,7 +107,7 @@ test("пока список доменов грузится — показан
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const checkSpy = vi.spyOn(api, "checkDomain").mockResolvedValue({
|
const checkSpy = vi.spyOn(api, "checkDomain").mockResolvedValue({
|
||||||
updates: [], prunes: [], readOnly: [], inSyncCount: 0,
|
updates: [], prunes: [], customs: [], readOnly: [], inSyncCount: 0,
|
||||||
})
|
})
|
||||||
const zoneRecordsSpy = vi.spyOn(api, "zoneRecords")
|
const zoneRecordsSpy = vi.spyOn(api, "zoneRecords")
|
||||||
renderPage()
|
renderPage()
|
||||||
|
|||||||
Reference in New Issue
Block a user