From 9617b88ab80383f1873cc4a48bd730294d0218a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jana=20Vlachov=C3=A1?= <65499282+janavlachova@users.noreply.github.com> Date: Fri, 3 Jan 2025 12:29:55 +0100 Subject: [PATCH] [studio] print audit to modal window #1449 (#1451) studio - print audit log and display db name in header --- .../base/table/AgdbCellMenu.spec.ts | 104 +++++++++++++++++- .../components/base/table/AgdbCellMenu.vue | 9 +- .../src/composables/db/dbConfig.spec.ts | 59 +++++++++- agdb_studio/src/composables/db/dbConfig.ts | 38 ++++++- agdb_studio/src/tests/apiMock.ts | 2 +- agdb_studio/src/types/base.d.ts | 1 + 6 files changed, 205 insertions(+), 8 deletions(-) diff --git a/agdb_studio/src/components/base/table/AgdbCellMenu.spec.ts b/agdb_studio/src/components/base/table/AgdbCellMenu.spec.ts index b9e71fed..0cbc4101 100644 --- a/agdb_studio/src/components/base/table/AgdbCellMenu.spec.ts +++ b/agdb_studio/src/components/base/table/AgdbCellMenu.spec.ts @@ -10,6 +10,7 @@ const { fetchDatabases } = vi.hoisted(() => { fetchDatabases: vi.fn(), }; }); +const { modalIsVisible, onConfirm, modal, hideModal } = useModal(); vi.mock("@/composables/db/dbStore", () => { return { @@ -23,6 +24,7 @@ vi.mock("@/composables/db/dbStore", () => { describe("AgdbCellMenu", () => { beforeEach(() => { vi.clearAllMocks(); + hideModal(); }); it("should open and close on click", async () => { const wrapper = mount(AgdbCellMenu, { @@ -85,8 +87,10 @@ describe("AgdbCellMenu", () => { }); it("should open the modal on click when confirmation is required", async () => { const deleteAction = vi.fn(); + const question = "Are you sure you want to delete this database?"; + const header = "Delete Database"; const deleteConfirmation = convertArrayOfStringsToContent([ - "Are you sure you want to delete this database?", + question, "This will permanently delete all data.", ]); const wrapper = mount(AgdbCellMenu, { @@ -97,6 +101,7 @@ describe("AgdbCellMenu", () => { label: "Delete", action: deleteAction, confirmation: deleteConfirmation, + confirmationHeader: header, }, ], }, @@ -124,10 +129,11 @@ describe("AgdbCellMenu", () => { await action.trigger("click"); await wrapper.vm.$nextTick(); expect(wrapper.find(".content").exists()).toBe(false); - const { modalIsVisible, onConfirm } = useModal(); expect(modalIsVisible.value).toBe(true); onConfirm.value?.(); expect(deleteAction).toHaveBeenCalledOnce(); + expect(modal.content[0].paragraph?.at(0)?.text).toBe(question); + expect(modal.header).toBe(header); }); it("should not close the dropdown if item has no action", async () => { const wrapper = mount(AgdbCellMenu, { @@ -159,4 +165,98 @@ describe("AgdbCellMenu", () => { await wrapper.vm.$nextTick(); expect(wrapper.find(".content").exists()).toBe(true); }); + + it("should use header function if provided", async () => { + const deleteAction = vi.fn(); + const question = "Are you sure you want to delete this database?"; + const header = vi.fn().mockReturnValue("Test Header"); + const deleteConfirmation = convertArrayOfStringsToContent([ + question, + "This will permanently delete all data.", + ]); + const wrapper = mount(AgdbCellMenu, { + props: { + actions: [ + { + key: "delete", + label: "Delete", + action: deleteAction, + confirmation: deleteConfirmation, + confirmationHeader: header, + }, + ], + }, + global: { + provide: { + [INJECT_KEY_ROW]: { + value: { + role: "admin", + owner: "admin", + db: "test", + db_type: "memory", + size: 2656, + backup: 0, + }, + }, + }, + }, + }); + const trigger = wrapper.find(".trigger"); + expect(wrapper.find(".content").exists()).toBe(false); + trigger.trigger("click"); + await wrapper.vm.$nextTick(); + expect(wrapper.find(".content").isVisible()).toBe(true); + const action = wrapper.find(".menu-item[data-key=delete]"); + await action.trigger("click"); + await wrapper.vm.$nextTick(); + expect(wrapper.find(".content").exists()).toBe(false); + expect(header).toHaveBeenCalled(); + expect(modal.content[0].paragraph?.at(0)?.text).toBe(question); + expect(modal.header).toBe("Test Header"); + }); + it("should set the header to the default if no header function is provided", async () => { + const deleteAction = vi.fn(); + const question = "Are you sure you want to delete this database?"; + const deleteConfirmation = convertArrayOfStringsToContent([ + question, + "This will permanently delete all data.", + ]); + const wrapper = mount(AgdbCellMenu, { + props: { + actions: [ + { + key: "delete", + label: "Delete", + action: deleteAction, + confirmation: deleteConfirmation, + }, + ], + }, + global: { + provide: { + [INJECT_KEY_ROW]: { + value: { + role: "admin", + owner: "admin", + db: "test", + db_type: "memory", + size: 2656, + backup: 0, + }, + }, + }, + }, + }); + const trigger = wrapper.find(".trigger"); + expect(wrapper.find(".content").exists()).toBe(false); + trigger.trigger("click"); + await wrapper.vm.$nextTick(); + expect(wrapper.find(".content").isVisible()).toBe(true); + const action = wrapper.find(".menu-item[data-key=delete]"); + await action.trigger("click"); + await wrapper.vm.$nextTick(); + expect(wrapper.find(".content").exists()).toBe(false); + expect(modal.content[0].paragraph?.at(0)?.text).toBe(question); + expect(modal.header).toBe("Confirm action"); + }); }); diff --git a/agdb_studio/src/components/base/table/AgdbCellMenu.vue b/agdb_studio/src/components/base/table/AgdbCellMenu.vue index 6a2580f9..48ebb064 100644 --- a/agdb_studio/src/components/base/table/AgdbCellMenu.vue +++ b/agdb_studio/src/components/base/table/AgdbCellMenu.vue @@ -34,7 +34,14 @@ const mapActions = (actions: Action[]): Action[] => { : action.confirmation ? ({ event }: ActionProps) => showModal({ - header: "Confirm action", + header: action.confirmationHeader + ? typeof action.confirmationHeader === + "function" + ? action.confirmationHeader({ + params: row?.value, + }) + : action.confirmationHeader + : "Confirm action", content: action.confirmation, onConfirm: () => runAction({ event, params: undefined }), diff --git a/agdb_studio/src/composables/db/dbConfig.spec.ts b/agdb_studio/src/composables/db/dbConfig.spec.ts index 1118afd8..4399e44e 100644 --- a/agdb_studio/src/composables/db/dbConfig.spec.ts +++ b/agdb_studio/src/composables/db/dbConfig.spec.ts @@ -1,5 +1,10 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; -import { dbActions, dbColumns } from "./dbConfig"; +import { + dbActions, + dbColumns, + getConfirmationHeaderFn, + type DbActionProps, +} from "./dbConfig"; import { db_backup, db_restore, @@ -15,9 +20,12 @@ import { import { useContentInputs } from "../content/inputs"; import { KEY_MODAL } from "../modal/constants"; import { ref } from "vue"; +import useModal from "../modal/modal"; const { addInput, setInputValue, clearAllInputs } = useContentInputs(); +const { modalIsVisible, modal } = useModal(); + describe("dbConfig", () => { describe("dbColumns", () => { it("should have correct db columns", () => { @@ -108,5 +116,54 @@ describe("dbConfig", () => { expect(api).not.toHaveBeenCalled(); clearAllInputs(); }); + + it("should print the empty audit log", async () => { + const action = dbActions.find((action) => action.key === "audit"); + const params = { db: "test_db", owner: "test_owner" }; + await action?.action({ params }); + expect(db_audit).toHaveBeenCalledWith(params); + + expect(modalIsVisible.value).toBe(true); + expect(modal.header).toBe("Audit log of test_owner/test_db"); + expect(modal.content).toHaveLength(1); + }); + it("should print the audit log", async () => { + const action = dbActions.find((action) => action.key === "audit"); + const params = { db: "test_db", owner: "test_owner" }; + db_audit.mockResolvedValueOnce({ + data: [ + { + timestamp: "123", + user: "test_user", + query: "test_query", + }, + { + timestamp: "456", + user: "test_user2", + query: "test_query2", + }, + ], + }); + await action?.action({ params }); + expect(db_audit).toHaveBeenCalledWith(params); + + expect(modalIsVisible.value).toBe(true); + expect(modal.header).toBe("Audit log of test_owner/test_db"); + expect(modal.content).toHaveLength(2); + expect(modal.content[0].paragraph?.at(0)?.text).toBe( + "123 | test_user | test_query", + ); + expect(modal.content[1].paragraph?.at(0)?.text).toBe( + "456 | test_user2 | test_query2", + ); + }); + }); + describe("getConfirmationHeaderFn", () => { + it("should return correct header", () => { + const header = getConfirmationHeaderFn({ + params: { db: "test_db", owner: "test_owner" }, + } as unknown as DbActionProps); + expect(header).toBe("Confirm action for test_owner/test_db"); + }); }); }); diff --git a/agdb_studio/src/composables/db/dbConfig.ts b/agdb_studio/src/composables/db/dbConfig.ts index ab684cd1..bca36321 100644 --- a/agdb_studio/src/composables/db/dbConfig.ts +++ b/agdb_studio/src/composables/db/dbConfig.ts @@ -4,17 +4,35 @@ import { dateFormatter } from "@/composables/table/utils"; import { convertArrayOfStringsToContent } from "@/composables/content/utils"; import { useContentInputs } from "../content/inputs"; import { KEY_MODAL } from "../modal/constants"; +import useModal from "../modal/modal"; const { getInputValue } = useContentInputs(); +const { showModal } = useModal(); + +export type DbActionProps = ActionProps; + +const getConfirmationHeaderFn = ({ params }: DbActionProps) => + `Confirm action for ${params.owner}/${params.db}`; -type DbActionProps = ActionProps; const dbActions: Action[] = [ { key: "audit", label: "Audit", action: ({ params }: DbActionProps) => client.value?.db_audit(params).then((res) => { - console.log(res.data); + const content = res.data.length + ? convertArrayOfStringsToContent( + res.data.map( + (item) => + `${item.timestamp} | ${item.user} | ${item.query}`, + ), + ) + : convertArrayOfStringsToContent(["No audit logs found."]); + + showModal({ + header: `Audit log of ${params.owner}/${params.db}`, + content, + }); }), }, { @@ -30,6 +48,7 @@ const dbActions: Action[] = [ "This will swap the existing backup with the current db.", ]), ], + confirmationHeader: getConfirmationHeaderFn, }, { key: "clear", @@ -47,6 +66,7 @@ const dbActions: Action[] = [ ], { emphesizedWords: ["clear", "all"] }, ), + confirmationHeader: getConfirmationHeaderFn, }, { key: "db", @@ -60,6 +80,7 @@ const dbActions: Action[] = [ ], { emphesizedWords: ["clear", "database"] }, ), + confirmationHeader: getConfirmationHeaderFn, }, { key: "audit", @@ -72,6 +93,7 @@ const dbActions: Action[] = [ ], { emphesizedWords: ["clear", "audit"] }, ), + confirmationHeader: getConfirmationHeaderFn, }, { key: "backup", @@ -84,6 +106,7 @@ const dbActions: Action[] = [ ], { emphesizedWords: ["clear", "backup"] }, ), + confirmationHeader: getConfirmationHeaderFn, }, ], }, @@ -102,6 +125,7 @@ const dbActions: Action[] = [ ], { emphesizedWords: ["convert", "memory"] }, ), + confirmationHeader: getConfirmationHeaderFn, }, { key: "file", @@ -114,6 +138,7 @@ const dbActions: Action[] = [ ], { emphesizedWords: ["convert", "file"] }, ), + confirmationHeader: getConfirmationHeaderFn, }, { key: "mapped", @@ -126,6 +151,7 @@ const dbActions: Action[] = [ ], { emphesizedWords: ["convert", "mapped"] }, ), + confirmationHeader: getConfirmationHeaderFn, }, ], }, @@ -152,6 +178,7 @@ const dbActions: Action[] = [ }, }, ], + confirmationHeader: getConfirmationHeaderFn, }, { key: "delete", @@ -167,6 +194,7 @@ const dbActions: Action[] = [ { emphesizedWords: ["all data"] }, ), ], + confirmationHeader: getConfirmationHeaderFn, }, { key: "optimize", @@ -177,6 +205,7 @@ const dbActions: Action[] = [ ["Are you sure you want to optimize this database?"], { emphesizedWords: ["optimize"] }, ), + confirmationHeader: getConfirmationHeaderFn, }, { @@ -190,6 +219,7 @@ const dbActions: Action[] = [ ], { emphesizedWords: ["remove"] }, ), + confirmationHeader: getConfirmationHeaderFn, }, { key: "rename", @@ -214,6 +244,7 @@ const dbActions: Action[] = [ }, }, ], + confirmationHeader: getConfirmationHeaderFn, }, { key: "restore", @@ -226,6 +257,7 @@ const dbActions: Action[] = [ ], { emphesizedWords: ["restore"] }, ), + confirmationHeader: getConfirmationHeaderFn, }, ]; @@ -247,4 +279,4 @@ const dbColumns = [ }, ]; -export { dbActions, dbColumns }; +export { dbActions, dbColumns, getConfirmationHeaderFn }; diff --git a/agdb_studio/src/tests/apiMock.ts b/agdb_studio/src/tests/apiMock.ts index 058d78a7..ac64ef71 100644 --- a/agdb_studio/src/tests/apiMock.ts +++ b/agdb_studio/src/tests/apiMock.ts @@ -11,7 +11,7 @@ export const db_convert = vi.fn(); export const db_remove = vi.fn(); export const db_delete = vi.fn(); export const db_optimize = vi.fn(); -export const db_audit = vi.fn().mockResolvedValue([]); +export const db_audit = vi.fn().mockResolvedValue({ data: [] }); export const db_copy = vi.fn(); export const db_rename = vi.fn(); diff --git a/agdb_studio/src/types/base.d.ts b/agdb_studio/src/types/base.d.ts index bc591603..7c252b5e 100644 --- a/agdb_studio/src/types/base.d.ts +++ b/agdb_studio/src/types/base.d.ts @@ -34,4 +34,5 @@ type Action = { action?: ActionFn; actions?: Action[]; confirmation?: Content[]; + confirmationHeader?: string | ActionFn; };