From 3240ba1bcef8202aa693cb745d16f9ce89d3f366 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jana=20Vlachov=C3=A1?=
<65499282+janavlachova@users.noreply.github.com>
Date: Sat, 11 Jan 2025 16:08:58 +0100
Subject: [PATCH] [studio] display db users in expanded row #1460 (#1479)
* studio - db details in db table
* studio - db users
* studio - db users
* studio - unit test
* studio - unit tests
* studio - remove comments
* studio - dbdetails stub in unit test
* studio - type fixed
---
agdb_studio/src/assets/base.css | 5 +
agdb_studio/src/assets/button.less | 2 +-
.../base/content/AgdbContent.spec.ts | 30 +++-
.../components/base/content/AgdbContent.vue | 34 ++++-
.../components/base/modal/AgdbModal.spec.ts | 16 +-
.../src/components/base/modal/AgdbModal.vue | 6 +-
.../base/table/AgdbCellMenu.spec.ts | 4 +-
.../components/base/table/AgdbCellMenu.vue | 4 +-
.../base/table/AgdbTableRow.spec.ts | 3 +-
.../components/base/table/AgdbTableRow.vue | 12 +-
.../src/components/db/DbDetails.spec.ts | 143 ++++++++++++++++++
agdb_studio/src/components/db/DbDetails.vue | 112 ++++++++++++++
agdb_studio/src/components/db/DbTable.vue | 2 +-
.../src/components/layouts/MainLayout.vue | 2 +-
.../{FadeTrasition.vue => FadeTransition.vue} | 0
agdb_studio/src/composables/content/utils.ts | 6 +-
agdb_studio/src/composables/db/dbConfig.ts | 4 +-
.../src/composables/db/dbDetails.spec.ts | 87 +++++++++++
agdb_studio/src/composables/db/dbDetails.ts | 122 +++++++++++++++
agdb_studio/src/composables/db/dbStore.ts | 7 +
.../src/composables/db/dbUsersStore.spec.ts | 69 +++++++++
.../src/composables/db/dbUsersStore.ts | 57 +++++++
.../src/composables/modal/modal.spec.ts | 18 +--
agdb_studio/src/composables/modal/modal.ts | 14 +-
.../src/composables/table/tableConfig.ts | 2 +-
agdb_studio/src/composables/table/types.ts | 4 +-
.../src/composables/table/utils.spec.ts | 1 +
agdb_studio/src/tests/apiMock.ts | 6 +
agdb_studio/src/types/asyncComponents.d.ts | 1 +
agdb_studio/src/types/base.d.ts | 17 ++-
agdb_studio/src/utils/asyncComponents.spec.ts | 9 ++
agdb_studio/src/utils/asyncComponents.ts | 13 ++
32 files changed, 760 insertions(+), 52 deletions(-)
create mode 100644 agdb_studio/src/components/db/DbDetails.spec.ts
create mode 100644 agdb_studio/src/components/db/DbDetails.vue
rename agdb_studio/src/components/transitions/{FadeTrasition.vue => FadeTransition.vue} (100%)
create mode 100644 agdb_studio/src/composables/db/dbDetails.spec.ts
create mode 100644 agdb_studio/src/composables/db/dbDetails.ts
create mode 100644 agdb_studio/src/composables/db/dbUsersStore.spec.ts
create mode 100644 agdb_studio/src/composables/db/dbUsersStore.ts
create mode 100644 agdb_studio/src/types/asyncComponents.d.ts
create mode 100644 agdb_studio/src/utils/asyncComponents.spec.ts
create mode 100644 agdb_studio/src/utils/asyncComponents.ts
diff --git a/agdb_studio/src/assets/base.css b/agdb_studio/src/assets/base.css
index 8e480440..eece8461 100644
--- a/agdb_studio/src/assets/base.css
+++ b/agdb_studio/src/assets/base.css
@@ -20,10 +20,13 @@
--text-light-2: rgba(60, 60, 60, 0.66);
--text-dark-1: var(--white);
--text-dark-2: rgba(235, 235, 235, 0.64);
+ --text-dark-3: rgba(235, 235, 235, 0.4);
--base-font: "Red Hat Display", sans-serif;
--orange: #ffa02c;
+ --red: #af2836;
+ --green: #1e7732;
}
:root {
@@ -37,6 +40,7 @@
--color-heading: var(--text-light-1);
--color-text: var(--text-light-1);
+ --color-text-muted: var(--text-light-2);
--section-gap: 160px;
}
@@ -52,6 +56,7 @@
--color-heading: var(--text-dark-1);
--color-text: var(--text-dark-2);
+ --color-text-muted: var(--text-dark-3);
}
}
diff --git a/agdb_studio/src/assets/button.less b/agdb_studio/src/assets/button.less
index a8cf61a8..c8d64492 100644
--- a/agdb_studio/src/assets/button.less
+++ b/agdb_studio/src/assets/button.less
@@ -40,7 +40,7 @@
.button-danger {
--backgroundColor: #dc3545;
--color: var(--white);
- --borderColor: #af2836;
+ --borderColor: var(--red);
}
.button-transparent {
diff --git a/agdb_studio/src/components/base/content/AgdbContent.spec.ts b/agdb_studio/src/components/base/content/AgdbContent.spec.ts
index 0e9a08c9..d8e28e28 100644
--- a/agdb_studio/src/components/base/content/AgdbContent.spec.ts
+++ b/agdb_studio/src/components/base/content/AgdbContent.spec.ts
@@ -25,7 +25,7 @@ describe("AgdbContent", () => {
],
},
{
- component: "my-component",
+ component: "my-component" as unknown as AsyncComponent,
},
{
input: {
@@ -89,4 +89,32 @@ describe("AgdbContent", () => {
const input = wrapper.find("input");
expect(input.element.matches(":focus")).toBe(true);
});
+ it("should render select input and change value", async () => {
+ const inputValue = ref("test");
+ addInput(testKey, "test", inputValue);
+ const wrapper = mount(AgdbContent, {
+ props: {
+ content: [
+ {
+ input: {
+ key: "test",
+ type: "select",
+ label: "Test input",
+ options: [
+ { value: "test", label: "Test" },
+ { value: "test2", label: "Test2" },
+ ],
+ },
+ },
+ ],
+ contentKey: testKey,
+ },
+ });
+ const select = wrapper.find("select");
+ expect(select.element.value).toBe("test");
+ expect(getInputValue(testKey, "test")).toBe("test");
+ select.element.value = "test2";
+ await select.trigger("change");
+ expect(getInputValue(testKey, "test")).toBe("test2");
+ });
});
diff --git a/agdb_studio/src/components/base/content/AgdbContent.vue b/agdb_studio/src/components/base/content/AgdbContent.vue
index d21c5f72..7f6c500b 100644
--- a/agdb_studio/src/components/base/content/AgdbContent.vue
+++ b/agdb_studio/src/components/base/content/AgdbContent.vue
@@ -7,7 +7,7 @@ const props = defineProps({
contentKey: { type: Symbol, required: true },
});
-const { getContentInputs, setInputValue } = useContentInputs();
+const { getContentInputs, setInputValue, getInputValue } = useContentInputs();
const inputs = getContentInputs(props.contentKey) ?? new Map();
const autofocusElement = ref();
@@ -35,8 +35,32 @@ onMounted(() => {
+
{
- const { showModal, hideModal } = useModal();
+ const { openModal, closeModal } = useModal();
beforeEach(() => {
- hideModal();
+ closeModal();
});
it("shows a modal when called", async () => {
@@ -15,7 +15,7 @@ describe("AgdbModal", () => {
attachTo: document.body,
});
expect(wrapper.isVisible()).toBe(false);
- showModal({
+ openModal({
header: "Test Header",
content: convertArrayOfStringsToContent(["Test Body"]),
});
@@ -26,7 +26,7 @@ describe("AgdbModal", () => {
const wrapper = mount(AgdbModal, {
attachTo: document.body,
});
- showModal({
+ openModal({
header: "Test Header",
content: convertArrayOfStringsToContent(["Test Body"]),
});
@@ -39,7 +39,7 @@ describe("AgdbModal", () => {
const wrapper = mount(AgdbModal, {
attachTo: document.body,
});
- showModal({
+ openModal({
header: "Test Header",
content: convertArrayOfStringsToContent(["Test Body"]),
});
@@ -54,7 +54,7 @@ describe("AgdbModal", () => {
const wrapper = mount(AgdbModal, {
attachTo: document.body,
});
- showModal({
+ openModal({
header: "Test Header",
content: convertArrayOfStringsToContent(["Test Body"]),
buttons: [
@@ -75,7 +75,7 @@ describe("AgdbModal", () => {
const wrapper = mount(AgdbModal, {
attachTo: document.body,
});
- showModal({
+ openModal({
header: "Test Header",
content: convertArrayOfStringsToContent(["Test Body"]),
onConfirm: () => {},
@@ -90,7 +90,7 @@ describe("AgdbModal", () => {
const wrapper = mount(AgdbModal, {
attachTo: document.body,
});
- showModal({
+ openModal({
header: "Test Header",
content: [
{
diff --git a/agdb_studio/src/components/base/modal/AgdbModal.vue b/agdb_studio/src/components/base/modal/AgdbModal.vue
index 1463b556..79cb0bb3 100644
--- a/agdb_studio/src/components/base/modal/AgdbModal.vue
+++ b/agdb_studio/src/components/base/modal/AgdbModal.vue
@@ -5,7 +5,7 @@ import AgdbContent from "../content/AgdbContent.vue";
import { KEY_MODAL } from "@/composables/modal/constants";
import { nextTick, ref, watch } from "vue";
-const { modal, buttons, hideModal, modalIsVisible } = useModal();
+const { modal, buttons, closeModal, modalIsVisible } = useModal();
const autofocusElement = ref();
@@ -27,7 +27,7 @@ watch(modalIsVisible, async () => {
- details
-
+
diff --git a/agdb_studio/src/components/db/DbDetails.spec.ts b/agdb_studio/src/components/db/DbDetails.spec.ts
new file mode 100644
index 00000000..ed1ce559
--- /dev/null
+++ b/agdb_studio/src/components/db/DbDetails.spec.ts
@@ -0,0 +1,143 @@
+import { mount } from "@vue/test-utils";
+import { describe, beforeEach, vi, it, expect } from "vitest";
+import DbDetails from "./DbDetails.vue";
+import { ref } from "vue";
+
+const { fetchDbUsers, isDbRoleType, handleRemoveUser, handleAddUser } =
+ vi.hoisted(() => {
+ return {
+ fetchDbUsers: vi.fn(),
+ isDbRoleType: vi.fn().mockReturnValue(true),
+ handleRemoveUser: vi.fn(),
+ handleAddUser: vi.fn(),
+ };
+ });
+
+vi.mock("@/composables/db/dbUsersStore", () => {
+ return {
+ useDbUsersStore: () => {
+ return {
+ fetchDbUsers,
+ isDbRoleType,
+ };
+ },
+ };
+});
+
+const canEditUsers = ref(true);
+
+vi.mock("@/composables/db/dbDetails", () => {
+ return {
+ useDbDetails: () => {
+ return {
+ users: ref([
+ {
+ username: "testUser",
+ role: "read",
+ },
+ {
+ username: "testUser2",
+ role: "write",
+ },
+ {
+ username: "testUser3",
+ role: "admin",
+ },
+ ]),
+ dbName: ref("testOwner/testDb"),
+ canEditUsers: canEditUsers,
+ handleRemoveUser,
+ handleAddUser,
+ };
+ },
+ };
+});
+
+describe("DbDetails", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ canEditUsers.value = true;
+ });
+ it("should render users", async () => {
+ const wrapper = mount(DbDetails);
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find("header").text()).toContain("testOwner/testDb");
+ const usernames = wrapper.findAll(".username");
+ expect(usernames.length).toBe(3);
+ expect(usernames[0].text()).toContain("testUser");
+ expect(usernames[1].text()).toContain("testUser2");
+ expect(usernames[2].text()).toContain("testUser3");
+
+ const roles = wrapper.findAll(".role");
+ expect(roles.length).toBe(3);
+ expect(roles[0].text()).toContain("(R)");
+ expect(roles[1].text()).toContain("(W)");
+ expect(roles[2].text()).toContain("(A)");
+ });
+
+ it("should add a user", async () => {
+ const wrapper = mount(DbDetails);
+ await wrapper.vm.$nextTick();
+ await wrapper.find(".add-button").trigger("click");
+ await wrapper.vm.$nextTick();
+
+ expect(handleAddUser).toHaveBeenCalled();
+ });
+
+ it("should remove a user", async () => {
+ const wrapper = mount(DbDetails);
+ await wrapper.vm.$nextTick();
+ await wrapper.find(".remove-button").trigger("click");
+ await wrapper.vm.$nextTick();
+
+ expect(handleRemoveUser).toHaveBeenCalled();
+ });
+
+ it("should not render add button if not admin", async () => {
+ canEditUsers.value = false;
+ const wrapper = mount(DbDetails, {
+ props: {
+ row: {
+ role: "read",
+ },
+ },
+ });
+
+ await wrapper.vm.$nextTick();
+ const addButton = wrapper.find(".add-button");
+ expect(addButton.exists()).toBe(false);
+ });
+
+ it("should not render remove button if not admin", async () => {
+ canEditUsers.value = false;
+ const wrapper = mount(DbDetails, {
+ props: {
+ row: {
+ role: "read",
+ },
+ },
+ });
+
+ await wrapper.vm.$nextTick();
+ const removeButton = wrapper.find(".remove-button");
+ expect(removeButton.exists()).toBe(false);
+ });
+
+ it("should not render remove button if user is owner", async () => {
+ const wrapper = mount(DbDetails, {
+ props: {
+ row: {
+ owner: "testUser3",
+ role: "admin",
+ db: "testDb",
+ },
+ },
+ });
+
+ await wrapper.vm.$nextTick();
+ const items = wrapper.findAll(".user-item");
+ expect(items.length).toBe(3);
+ expect(items[0].find(".remove-button").exists()).toBe(true);
+ expect(items[2].find(".remove-button").exists()).toBe(false);
+ });
+});
diff --git a/agdb_studio/src/components/db/DbDetails.vue b/agdb_studio/src/components/db/DbDetails.vue
new file mode 100644
index 00000000..cd1acdc8
--- /dev/null
+++ b/agdb_studio/src/components/db/DbDetails.vue
@@ -0,0 +1,112 @@
+
+
+
+
+
+ Database: {{ dbName }}
+
+
+
+
+ -
+ {{ user.username }}
+
+ ({{ user.role.charAt(0).toLocaleUpperCase() }})
+
+
+
+
+
+
+
+
diff --git a/agdb_studio/src/components/db/DbTable.vue b/agdb_studio/src/components/db/DbTable.vue
index a15ffc9d..56aeed17 100644
--- a/agdb_studio/src/components/db/DbTable.vue
+++ b/agdb_studio/src/components/db/DbTable.vue
@@ -13,7 +13,7 @@ const TABLE_KEY = Symbol("databases");
addTable({
name: TABLE_KEY,
columns: dbColumns,
- rowDetailsComponent: "DbTableRowDetails",
+ rowDetailsComponent: "DbDetails",
});
watchEffect(() => {
diff --git a/agdb_studio/src/components/layouts/MainLayout.vue b/agdb_studio/src/components/layouts/MainLayout.vue
index 1f79b20e..ed1e4927 100644
--- a/agdb_studio/src/components/layouts/MainLayout.vue
+++ b/agdb_studio/src/components/layouts/MainLayout.vue
@@ -4,7 +4,7 @@ import { useAuth } from "@/composables/user/auth";
import LogoIcon from "@/components/base/icons/LogoIcon.vue";
import { useAccount } from "@/composables/user/account";
import AgdbModal from "@/components/base/modal/AgdbModal.vue";
-import FadeTrasition from "@/components/transitions/FadeTrasition.vue";
+import FadeTrasition from "@/components/transitions/FadeTransition.vue";
const { logout } = useAuth();
const { username } = useAccount();
diff --git a/agdb_studio/src/components/transitions/FadeTrasition.vue b/agdb_studio/src/components/transitions/FadeTransition.vue
similarity index 100%
rename from agdb_studio/src/components/transitions/FadeTrasition.vue
rename to agdb_studio/src/components/transitions/FadeTransition.vue
diff --git a/agdb_studio/src/composables/content/utils.ts b/agdb_studio/src/composables/content/utils.ts
index b10a1a18..496c3585 100644
--- a/agdb_studio/src/composables/content/utils.ts
+++ b/agdb_studio/src/composables/content/utils.ts
@@ -1,13 +1,15 @@
-type ConvertParams = {
+export type ConvertParams = {
emphesizedWords?: string[];
};
+export const EMPHESIZED_CLASSNAME = "emphesized";
+
const emphesizeWords = (text: string, words: string[]): Paragraph[] => {
const parts = text.split(new RegExp(`(${words.join("|")})`, "g"));
return parts.map((part) => {
if (words.includes(part)) {
- return { text: part, className: "emphesized" };
+ return { text: part, className: EMPHESIZED_CLASSNAME };
}
return { text: part };
});
diff --git a/agdb_studio/src/composables/db/dbConfig.ts b/agdb_studio/src/composables/db/dbConfig.ts
index 56c862f7..f0a51705 100644
--- a/agdb_studio/src/composables/db/dbConfig.ts
+++ b/agdb_studio/src/composables/db/dbConfig.ts
@@ -7,7 +7,7 @@ import { KEY_MODAL } from "../modal/constants";
import useModal from "../modal/modal";
const { getInputValue } = useContentInputs();
-const { showModal } = useModal();
+const { openModal } = useModal();
export type DbActionProps = ActionProps;
@@ -29,7 +29,7 @@ const dbActions: Action[] = [
)
: convertArrayOfStringsToContent(["No audit logs found."]);
- showModal({
+ openModal({
header: `Audit log of ${params.owner}/${params.db}`,
content,
});
diff --git a/agdb_studio/src/composables/db/dbDetails.spec.ts b/agdb_studio/src/composables/db/dbDetails.spec.ts
new file mode 100644
index 00000000..6629bace
--- /dev/null
+++ b/agdb_studio/src/composables/db/dbDetails.spec.ts
@@ -0,0 +1,87 @@
+import { describe, beforeEach, vi, it, expect } from "vitest";
+import { useDbDetails, type DbDetailsParams } from "./dbDetails";
+import { db_user_list, db_user_add, db_user_remove } from "@/tests/apiMock";
+import { ref } from "vue";
+import { useDbUsersStore } from "./dbUsersStore";
+import useModal from "@/composables/modal/modal";
+import { useContentInputs } from "../content/inputs";
+import { KEY_MODAL } from "../modal/constants";
+
+const dbParams = ref({
+ db: "testDb",
+ owner: "testOwner",
+ role: "admin",
+});
+const { fetchDbUsers } = useDbUsersStore();
+const { modalIsVisible, onConfirm, closeModal } = useModal();
+const { setInputValue, clearAllInputs } = useContentInputs();
+
+describe("dbDetails", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ closeModal();
+ clearAllInputs();
+ });
+ it("should get user list", async () => {
+ db_user_list.mockResolvedValue({
+ data: [
+ {
+ username: "testUser",
+ role: "read",
+ },
+ {
+ username: "testOwner",
+ role: "admin",
+ },
+ ],
+ });
+ const { users } = useDbDetails(dbParams);
+ await fetchDbUsers(dbParams.value);
+ expect(users.value).toHaveLength(2);
+ expect(users.value?.[0].username).toBe("testUser");
+ });
+ it("should get db name", () => {
+ const { dbName } = useDbDetails(dbParams);
+ expect(dbName.value).toBe("testOwner/testDb");
+ });
+ it("should get can edit users", () => {
+ const { canEditUsers } = useDbDetails(dbParams);
+ expect(canEditUsers.value).toBe(true);
+ });
+ it("should remove user", () => {
+ const { handleRemoveUser } = useDbDetails(dbParams);
+ handleRemoveUser("testUser");
+ expect(modalIsVisible.value).toBe(true);
+ onConfirm.value?.();
+ expect(db_user_remove).toHaveBeenCalledOnce();
+ });
+ it("should add user", () => {
+ const { handleAddUser } = useDbDetails(dbParams);
+ handleAddUser();
+ expect(modalIsVisible.value).toBe(true);
+ setInputValue(KEY_MODAL, "username", "testUser");
+ onConfirm.value?.();
+ expect(db_user_add).toHaveBeenCalled();
+ });
+ it("should not add user if role is not admin", () => {
+ const { handleAddUser } = useDbDetails(
+ ref({ ...dbParams.value, role: "read" }),
+ );
+ handleAddUser();
+ expect(modalIsVisible.value).toBe(false);
+ });
+ it("should not remove user if role is not admin", () => {
+ const { handleRemoveUser } = useDbDetails(
+ ref({ ...dbParams.value, role: "read" }),
+ );
+ handleRemoveUser("testUser");
+ expect(modalIsVisible.value).toBe(false);
+ });
+ it("should not add user if no username", () => {
+ const { handleAddUser } = useDbDetails(dbParams);
+ handleAddUser();
+ expect(modalIsVisible.value).toBe(true);
+ onConfirm.value?.();
+ expect(db_user_add).not.toHaveBeenCalled();
+ });
+});
diff --git a/agdb_studio/src/composables/db/dbDetails.ts b/agdb_studio/src/composables/db/dbDetails.ts
new file mode 100644
index 00000000..2f93f46d
--- /dev/null
+++ b/agdb_studio/src/composables/db/dbDetails.ts
@@ -0,0 +1,122 @@
+import type { ServerDatabase } from "agdb_api/dist/openapi";
+import { useDbStore, type DbIdentification } from "./dbStore";
+import { useDbUsersStore } from "./dbUsersStore";
+import { useContentInputs } from "../content/inputs";
+import { computed, type Ref } from "vue";
+import useModal from "../modal/modal";
+import { KEY_MODAL } from "../modal/constants";
+import { EMPHESIZED_CLASSNAME } from "../content/utils";
+
+export type DbDetailsParams = DbIdentification & Pick;
+
+const { getDbUsers, fetchDbUsers, removeUser, addUser, isDbRoleType } =
+ useDbUsersStore();
+const { getInputValue } = useContentInputs();
+
+const { openModal } = useModal();
+const { getDbName } = useDbStore();
+
+export const useDbDetails = (dbParams: Ref) => {
+ const users = computed(() => {
+ return getDbUsers(dbParams.value);
+ });
+
+ const dbName = computed(() => {
+ return getDbName(dbParams.value);
+ });
+
+ const canEditUsers = computed(() => {
+ return dbParams.value.role === "admin";
+ });
+
+ const handleRemoveUser = (username: string) => {
+ if (!canEditUsers.value) {
+ return;
+ }
+ openModal({
+ header: "Remove user",
+ content: [
+ {
+ paragraph: [
+ { text: "Are you sure you want to remove user " },
+ { text: username, className: EMPHESIZED_CLASSNAME },
+ { text: " from database " },
+ { text: dbName.value, className: EMPHESIZED_CLASSNAME },
+ { text: "?" },
+ ],
+ },
+ ],
+
+ onConfirm: () => {
+ removeUser({
+ owner: dbParams.value.owner,
+ db: dbParams.value.db,
+ username: username,
+ }).then(() => {
+ fetchDbUsers(dbParams.value);
+ });
+ },
+ });
+ };
+
+ const handleAddUser = () => {
+ if (!canEditUsers.value) {
+ return;
+ }
+ openModal({
+ header: "Add user",
+ content: [
+ {
+ paragraph: [
+ { text: "Add user to database " },
+ { text: dbName.value, className: EMPHESIZED_CLASSNAME },
+ ],
+ },
+ {
+ input: {
+ key: "username",
+ label: "Username",
+ type: "text",
+ autofocus: true,
+ },
+ },
+ {
+ input: {
+ key: "role",
+ label: "Role",
+ type: "select",
+ options: [
+ { value: "admin", label: "Admin" },
+ { value: "write", label: "Read/Write" },
+ { value: "read", label: "Read Only" },
+ ],
+ defaultValue: "write",
+ },
+ },
+ ],
+ onConfirm: () => {
+ const username = getInputValue(
+ KEY_MODAL,
+ "username",
+ )?.toString();
+ const db_role = getInputValue(KEY_MODAL, "role")?.toString();
+
+ if (username?.length && db_role && isDbRoleType(db_role)) {
+ addUser({ ...dbParams.value, username, db_role }).then(
+ () => {
+ fetchDbUsers(dbParams.value);
+ },
+ );
+ }
+ },
+ });
+ };
+
+ return {
+ users,
+ dbName,
+ canEditUsers,
+ handleRemoveUser,
+ handleAddUser,
+ };
+};
diff --git a/agdb_studio/src/composables/db/dbStore.ts b/agdb_studio/src/composables/db/dbStore.ts
index 75508c2e..37530e76 100644
--- a/agdb_studio/src/composables/db/dbStore.ts
+++ b/agdb_studio/src/composables/db/dbStore.ts
@@ -25,10 +25,17 @@ const addDatabase = async ({ name, db_type }: AddDatabaseProps) => {
client.value?.db_add({ owner: username.value, db: name, db_type });
};
+export type DbIdentification = Pick;
+
+const getDbName = (db: DbIdentification) => {
+ return `${db.owner}/${db.db}`;
+};
+
export const useDbStore = () => {
return {
databases,
fetchDatabases,
addDatabase,
+ getDbName,
};
};
diff --git a/agdb_studio/src/composables/db/dbUsersStore.spec.ts b/agdb_studio/src/composables/db/dbUsersStore.spec.ts
new file mode 100644
index 00000000..e5b61258
--- /dev/null
+++ b/agdb_studio/src/composables/db/dbUsersStore.spec.ts
@@ -0,0 +1,69 @@
+import { describe, beforeEach, vi, it, expect } from "vitest";
+import { useDbUsersStore } from "./dbUsersStore";
+import { db_user_list, db_user_add, db_user_remove } from "@/tests/apiMock";
+
+const dbIdentification = {
+ db: "testDb",
+ owner: "testOwner",
+};
+const {
+ getDbUsers,
+ fetchDbUsers,
+ addUser,
+ removeUser,
+ clearDbUsers,
+ clearAllDbUsers,
+ isDbRoleType,
+} = useDbUsersStore();
+describe("dbUsers", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ clearAllDbUsers();
+ });
+ it("should fetch and get users", async () => {
+ db_user_list.mockResolvedValue({
+ data: [
+ {
+ username: "testUser",
+ role: "read",
+ },
+ {
+ username: "testOwner",
+ role: "admin",
+ },
+ ],
+ });
+ expect(getDbUsers(dbIdentification)).toBeUndefined();
+
+ await fetchDbUsers(dbIdentification);
+ expect(db_user_list).toHaveBeenCalledOnce();
+ const users = getDbUsers(dbIdentification);
+ expect(users).toHaveLength(2);
+ expect(users?.[0].username).toBe("testUser");
+ });
+ it("should add user", async () => {
+ await addUser({
+ ...dbIdentification,
+ username: "testUser",
+ db_role: "read",
+ });
+ expect(db_user_add).toHaveBeenCalledOnce();
+ });
+ it("should remove user", async () => {
+ await removeUser({
+ ...dbIdentification,
+ username: "testUser",
+ });
+ expect(db_user_remove).toHaveBeenCalledOnce();
+ });
+ it("should clear users", () => {
+ clearDbUsers(dbIdentification);
+ expect(getDbUsers(dbIdentification)).toBeUndefined();
+ });
+ it("should check role type", () => {
+ expect(isDbRoleType("read")).toBe(true);
+ expect(isDbRoleType("write")).toBe(true);
+ expect(isDbRoleType("admin")).toBe(true);
+ expect(isDbRoleType("other")).toBe(false);
+ });
+});
diff --git a/agdb_studio/src/composables/db/dbUsersStore.ts b/agdb_studio/src/composables/db/dbUsersStore.ts
new file mode 100644
index 00000000..71666227
--- /dev/null
+++ b/agdb_studio/src/composables/db/dbUsersStore.ts
@@ -0,0 +1,57 @@
+import { client } from "@/services/api.service";
+import type { DbUser, DbUserRole } from "agdb_api/dist/openapi";
+import { ref } from "vue";
+import { useDbStore, type DbIdentification } from "./dbStore";
+
+const { getDbName } = useDbStore();
+
+const dbUsers = ref(new Map());
+
+const fetchDbUsers = async (params: DbIdentification) => {
+ client.value?.db_user_list(params).then((users) => {
+ dbUsers.value.set(getDbName(params), users.data);
+ });
+};
+
+const getDbUsers = (params: DbIdentification) => {
+ return dbUsers.value.get(getDbName(params));
+};
+
+const clearDbUsers = (params: DbIdentification) => {
+ dbUsers.value.delete(getDbName(params));
+};
+
+const clearAllDbUsers = () => {
+ dbUsers.value.clear();
+};
+
+type AddUserProps = {
+ username: string;
+ db_role: DbUserRole;
+} & DbIdentification;
+const addUser = async (params: AddUserProps) => {
+ client.value?.db_user_add(params);
+};
+
+type RemoveUserProps = {
+ username: string;
+} & DbIdentification;
+const removeUser = async (params: RemoveUserProps) => {
+ client.value?.db_user_remove(params);
+};
+
+const isDbRoleType = (role: string): role is DbUserRole => {
+ return ["read", "write", "admin"].includes(role);
+};
+
+export const useDbUsersStore = () => {
+ return {
+ getDbUsers,
+ fetchDbUsers,
+ addUser,
+ removeUser,
+ clearDbUsers,
+ clearAllDbUsers,
+ isDbRoleType,
+ };
+};
diff --git a/agdb_studio/src/composables/modal/modal.spec.ts b/agdb_studio/src/composables/modal/modal.spec.ts
index cc1143e5..0b8be46f 100644
--- a/agdb_studio/src/composables/modal/modal.spec.ts
+++ b/agdb_studio/src/composables/modal/modal.spec.ts
@@ -5,27 +5,27 @@ import { useContentInputs } from "../content/inputs";
import { KEY_MODAL } from "./constants";
describe("Modal", () => {
- const { showModal, hideModal } = useModal();
+ const { openModal, closeModal } = useModal();
beforeEach(() => {
- hideModal();
+ closeModal();
});
it("shows a modal", () => {
- showModal({
+ openModal({
header: "Test Header",
content: convertArrayOfStringsToContent(["Test Body"]),
});
expect(useModal().modalIsVisible.value).toBe(true);
});
it("hides a modal", () => {
- showModal({
+ openModal({
header: "Test Header",
content: convertArrayOfStringsToContent(["Test Body"]),
});
- hideModal();
+ closeModal();
expect(useModal().modalIsVisible.value).toBe(false);
});
it("shows a modal with custom buttons", () => {
- showModal({
+ openModal({
header: "Test Header",
content: convertArrayOfStringsToContent(["Test Body"]),
buttons: [
@@ -41,7 +41,7 @@ describe("Modal", () => {
});
it("calls onConfirm when confirm button is clicked and hides the modal", () => {
const onConfirm = vi.fn();
- showModal({
+ openModal({
header: "Test Header",
content: convertArrayOfStringsToContent(["Test Body"]),
onConfirm,
@@ -51,7 +51,7 @@ describe("Modal", () => {
expect(useModal().modalIsVisible.value).toBe(false);
});
it("sets default if no header or content is provided", () => {
- showModal({});
+ openModal({});
expect(useModal().modal.header).toBe("");
expect(useModal().modal.content).toHaveLength(0);
});
@@ -59,7 +59,7 @@ describe("Modal", () => {
const { getInputValue, setInputValue } = useContentInputs();
setInputValue(KEY_MODAL, "test", "test");
expect(getInputValue(KEY_MODAL, "test")).toBe(undefined);
- showModal({
+ openModal({
header: "Test Header",
content: [
{
diff --git a/agdb_studio/src/composables/modal/modal.ts b/agdb_studio/src/composables/modal/modal.ts
index 9514b5e7..38b36df9 100644
--- a/agdb_studio/src/composables/modal/modal.ts
+++ b/agdb_studio/src/composables/modal/modal.ts
@@ -13,7 +13,7 @@ const modalIsVisible = ref(false);
const onConfirm = ref<() => void>();
-const hideModal = (): void => {
+const closeModal = (): void => {
modal.header = "";
modal.content = [];
modalIsVisible.value = false;
@@ -27,7 +27,7 @@ const buttons = computed