From 1c68f0a82975def56c3fea4249d77f385a581ab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20G=C3=B6bel?= <dgoebel@techfak.uni-bielefeld.de> Date: Wed, 22 May 2024 12:21:01 +0000 Subject: [PATCH] Add modal to register new users #119 --- .gitlab-ci.yml | 2 +- src/components/AppHeader.vue | 10 +- src/components/admin/CreateUserModal.vue | 171 +++++++++++++++++++++++ src/stores/users.ts | 8 +- src/views/admin/AdminUsersView.vue | 14 +- 5 files changed, 194 insertions(+), 11 deletions(-) create mode 100644 src/components/admin/CreateUserModal.vue diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f340faa..4527b2d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -37,7 +37,7 @@ build: .build-container-job: stage: deploy image: - name: gcr.io/kaniko-project/executor:v1.21.1-debug + name: gcr.io/kaniko-project/executor:v1.23.0-debug entrypoint: [ "" ] dependencies: [ ] cache: [ ] diff --git a/src/components/AppHeader.vue b/src/components/AppHeader.vue index 78d7f01..e879949 100644 --- a/src/components/AppHeader.vue +++ b/src/components/AppHeader.vue @@ -2,7 +2,6 @@ import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue"; import { useUserStore } from "@/stores/users"; import { useRoute } from "vue-router"; -import { useCookies } from "vue3-cookies"; import { watch, ref, computed } from "vue"; import BootstrapModal from "@/components/modals/BootstrapModal.vue"; import { OpenAPI } from "@/client"; @@ -10,12 +9,10 @@ import CopyToClipboardIcon from "@/components/CopyToClipboardIcon.vue"; import dayjs from "dayjs"; const userRepository = useUserStore(); -const { cookies } = useCookies(); const route = useRoute(); function logout() { userRepository.logout(); - cookies.remove("bearer"); } const activeRoute = ref(""); @@ -299,13 +296,12 @@ watch( <hr class="dropdown-divider" /> </li> <li> - <router-link - :to="{ name: 'login' }" + <a class="dropdown-item" @click="logout" + :href="OpenAPI.BASE + '/auth/logout'" + >Sign out</a > - Sign out - </router-link> </li> </ul> </div> diff --git a/src/components/admin/CreateUserModal.vue b/src/components/admin/CreateUserModal.vue new file mode 100644 index 0000000..3556d8d --- /dev/null +++ b/src/components/admin/CreateUserModal.vue @@ -0,0 +1,171 @@ +<script setup lang="ts"> +import { RoleEnum, type UserIn, type UserOutExtended } from "@/client"; +import { onMounted, reactive, ref } from "vue"; +import { Modal, Toast } from "bootstrap"; +import BootstrapModal from "@/components/modals/BootstrapModal.vue"; +import { useUserStore } from "@/stores/users"; +import BootstrapToast from "@/components/BootstrapToast.vue"; + +let modal: Modal | null = null; +const createUserForm = ref<HTMLFormElement | undefined>(undefined); +let successToast: Toast | undefined; +let errorToast: Toast | undefined; + +const userRepository = useUserStore(); + +const formState = reactive<{ + loading: boolean; + user: UserIn; + errorMessage?: string; + registeredUserName: string; + validated: boolean; +}>({ + loading: false, + errorMessage: "", + validated: false, + registeredUserName: "", + user: { + display_name: "", + email: "", + roles: [RoleEnum.USER], + }, +}); + +const props = defineProps<{ + modalId: string; +}>(); + +const emit = defineEmits<{ + (e: "user-created", user: UserOutExtended): void; +}>(); + +function createUser() { + formState.validated = true; + if (createUserForm.value?.checkValidity()) { + formState.loading = true; + formState.registeredUserName = formState.user.display_name; + userRepository + .createUser(formState.user) + .then((user) => { + emit("user-created", user); + formState.validated = false; + formState.user = { + display_name: "", + email: "", + roles: [RoleEnum.USER], + }; + successToast?.show(); + modal?.hide(); + }) + .catch((err) => { + formState.errorMessage = err.body?.detail; + errorToast?.show(); + }) + .finally(() => { + formState.loading = false; + }); + } +} + +onMounted(() => { + modal = Modal.getOrCreateInstance(`#${props.modalId}`); + successToast = new Toast("#create-user-success-toast"); + errorToast = new Toast("#create-user-error-toast"); +}); +</script> + +<template> + <bootstrap-toast toast-id="create-user-success-toast"> + Successfully registered user {{ formState.registeredUserName }} + </bootstrap-toast> + <bootstrap-toast toast-id="create-user-error-toast" color-class="danger"> + <template #default + >Couldn't regsiter user + {{ formState.registeredUserName }} + </template> + <template #body>Error: {{ formState.errorMessage }}</template> + </bootstrap-toast> + <bootstrap-modal + :modalId="props.modalId" + :static-backdrop="true" + modal-label="Create user" + > + <template #header>Register a user</template> + <template #body> + <form + id="create-user-form" + @submit.prevent="createUser()" + :class="{ 'was-validated': formState.validated }" + ref="createUserForm" + novalidate + > + <div class="mb-3"> + <label for="create-user-name" class="form-label">Name</label> + <input + type="text" + class="form-control" + id="create-user-name" + minlength="3" + maxlength="256" + required + placeholder="John Doe" + v-model="formState.user.display_name" + /> + <div class="invalid-feedback">Please choose a name.</div> + </div> + <div class="mb-3"> + <label for="create-user-email" class="form-label" + >Email address</label + > + <input + type="email" + class="form-control" + id="create-user-email" + required + placeholder="name@example.com" + minlength="3" + maxlength="256" + v-model="formState.user.email" + /> + <div class="invalid-feedback"> + Please provide a valid email address. + </div> + </div> + <div class="mb-3"> + <div class="mb-1">Roles:</div> + <div + class="form-check" + v-for="role in Object.values(RoleEnum)" + :key="role" + > + <input + class="form-check-input" + type="checkbox" + :value="role" + :id="`create-user-role-${role}`" + v-model="formState.user.roles" + /> + <label class="form-check-label" :for="`create-user-role-${role}`"> + {{ role.toUpperCase() }} + </label> + </div> + </div> + </form> + </template> + <template #footer> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> + Close + </button> + <button + type="submit" + form="create-user-form" + class="btn btn-primary" + :disabled="formState.loading" + > + Save + </button> + </template> + </bootstrap-modal> +</template> + +<style scoped></style> diff --git a/src/stores/users.ts b/src/stores/users.ts index 8c4ca11..6cb49f2 100644 --- a/src/stores/users.ts +++ b/src/stores/users.ts @@ -1,5 +1,5 @@ import { defineStore } from "pinia"; -import type { UserOutExtended, UserOut } from "@/client"; +import type { UserOutExtended, UserOut, UserIn } from "@/client"; import { OpenAPI, UserService, RoleEnum } from "@/client"; import { useWorkflowExecutionStore } from "@/stores/workflowExecutions"; import { useBucketStore } from "@/stores/buckets"; @@ -113,6 +113,12 @@ export const useUserStore = defineStore({ roles: roles, }); }, + createUser(userIn: UserIn): Promise<UserOutExtended> { + return UserService.userCreateUser(userIn).then((user) => { + useNameStore().addNameToMapping(user.uid, user.display_name); + return user; + }); + }, searchUser(searchString: string): Promise<UserOut[]> { return UserService.userSearchUsers(searchString).then((users) => { const nameStore = useNameStore(); diff --git a/src/views/admin/AdminUsersView.vue b/src/views/admin/AdminUsersView.vue index 9dde3ff..e390a01 100644 --- a/src/views/admin/AdminUsersView.vue +++ b/src/views/admin/AdminUsersView.vue @@ -5,6 +5,7 @@ import { RoleEnum, type UserOutExtended } from "@/client"; import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue"; import BootstrapToast from "@/components/BootstrapToast.vue"; import { Toast } from "bootstrap"; +import CreateUserModal from "@/components/admin/CreateUserModal.vue"; const userRepository = useUserStore(); type RoleList = RoleEnum[]; @@ -96,10 +97,19 @@ onMounted(() => { </template> <template #body>Error: {{ userState.errorMessage }}</template> </bootstrap-toast> + <create-user-modal modal-id="create-user-modal" /> <div - class="row border-bottom mb-4 justify-content-between align-items-center" + class="row border-bottom mb-4 justify-content-between align-items-center pb-2 pe-2" > - <h2>Manage Users</h2> + <h2 class="w-fit">Manage Users</h2> + <button + type="button" + class="btn btn-primary w-fit" + data-bs-toggle="modal" + data-bs-target="#create-user-modal" + > + Register User + </button> </div> <form @submit.prevent="searchUsers" id="admin-user-search-form"> <div class="d-flex justify-content-evenly align-content-center"> -- GitLab