<script setup lang="ts"> import { onMounted, reactive, watch, ref, computed } from "vue"; import BootstrapModal from "@/components/modals/BootstrapModal.vue"; import DeleteModal from "@/components/modals/DeleteModal.vue"; import SearchUserModal from "@/components/modals/SearchUserModal.vue"; import { Modal } from "bootstrap"; import dayjs from "dayjs"; import type { BucketPermissionOut, BucketPermissionIn, BucketPermissionParameters, } from "@/client/s3proxy"; import type { User } from "@/client/auth"; import type { FolderTree } from "@/types/PseudoFolder"; import type { ComputedRef, Ref } from "vue"; import { Permission, BucketPermissionService } from "@/client/s3proxy"; import { Toast } from "bootstrap"; import BootstrapIcon from "@/components/BootstrapIcon.vue"; // Props // ----------------------------------------------------------------------------- const props = defineProps<{ modalID: string; bucketName: string; subFolders: FolderTree; editUserPermission: BucketPermissionOut | undefined; readonly: boolean; editable: boolean; deletable: boolean; backModalId: string | undefined; }>(); // Variables // ----------------------------------------------------------------------------- const randomIDSuffix = Math.random().toString(16).substr(2, 8); let permissionModal: Modal | null = null; let successToast: Toast | null = null; // Reactive State // ----------------------------------------------------------------------------- const formState = reactive({ loading: false, grantee_name: "", error: false, readonly: props.readonly, } as { loading: boolean; grantee_name: string; error: boolean; readonly: boolean; }); const permission = reactive({ from_timestamp: undefined, to_timestamp: undefined, file_prefix: undefined, permission: undefined, uid: "", bucket_name: props.bucketName, } as BucketPermissionIn); const permissionDeleted: Ref<boolean> = ref(false); // Computes Properties // ----------------------------------------------------------------------------- const editPermission: ComputedRef<boolean> = computed( () => props.editUserPermission != undefined ); const possibleSubFolders: ComputedRef<string[]> = computed(() => findSubFolders(props.subFolders, []) ); const permissionUserReadonly: ComputedRef<boolean> = computed(() => { return formState.readonly || editPermission.value; }); // Watchers // ----------------------------------------------------------------------------- watch( () => props.bucketName, (newBucketName) => { updatePermission(); permission.bucket_name = newBucketName; } ); watch( () => props.editUserPermission, () => updatePermission() ); // Events // ----------------------------------------------------------------------------- const emit = defineEmits<{ (e: "permission-deleted", permission: BucketPermissionIn): void; (e: "permission-created", permission: BucketPermissionOut): void; (e: "permission-edited", permission: BucketPermissionOut): void; }>(); // Functions // ----------------------------------------------------------------------------- /** * Reset the form. Triggered when the modal is closed. */ function modalClosed() { formState.readonly = props.readonly; formState.error = false; if (editPermission.value) { updatePermission(); } } /** * Callback when the toast is hidden again. */ function toastHidden() { permissionDeleted.value = false; } /** * Check if an input should be visible based on its state * @param input Input which visibility should be determined. */ function inputVisible(input: string | undefined): boolean { return !formState.readonly || input != undefined; } /** * Update the form content */ function updatePermission() { if (props.editUserPermission != undefined) { permission.bucket_name = props.editUserPermission.bucket_name; permission.file_prefix = props.editUserPermission.file_prefix; permission.uid = props.editUserPermission.uid; formState.grantee_name = props.editUserPermission.grantee_display_name; permission.from_timestamp = props.editUserPermission.from_timestamp != null ? dayjs(props.editUserPermission.from_timestamp).format("YYYY-MM-DD") : undefined; permission.to_timestamp = props.editUserPermission.to_timestamp != null ? dayjs(props.editUserPermission.to_timestamp).format("YYYY-MM-DD") : undefined; permission.permission = props.editUserPermission.permission; } else { permission.file_prefix = undefined; permission.uid = ""; permission.from_timestamp = undefined; permission.to_timestamp = undefined; permission.permission = undefined; formState.grantee_name = ""; } } /** * Find recursively all sub folders based on the folder structure * @param currentFolder Current Folder * @param parentFolders All parent folders */ function findSubFolders( currentFolder: FolderTree, parentFolders: string[] ): string[] { const arr: string[] = []; for (const subFolder of Object.keys(currentFolder.subFolders)) { const subFolderString = (parentFolders.length > 0 ? parentFolders.join("/") + "/" : "") + subFolder + "/"; arr.push( subFolderString, ...findSubFolders( currentFolder.subFolders[subFolder], subFolderString.slice(0, subFolderString.length - 1).split("/") ) ); } return arr; } /** * Submit the form */ function formSubmit() { formState.error = false; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const form = document.getElementById( "permissionCreateEditForm" + randomIDSuffix )! as HTMLFormElement; if (form.checkValidity()) { const tempPermission: BucketPermissionIn = permission; if (permission.from_timestamp != null) { tempPermission.from_timestamp = permission.from_timestamp.length > 0 ? dayjs(permission.from_timestamp).toISOString() : undefined; } if (permission.to_timestamp != null) { tempPermission.to_timestamp = permission.to_timestamp.length > 0 ? dayjs(permission.to_timestamp).toISOString() : undefined; } formState.loading = true; const serverAnswerPromise = editPermission.value ? BucketPermissionService.bucketPermissionUpdatePermission( permission.uid, permission.bucket_name, { to_timestamp: tempPermission.to_timestamp, from_timestamp: tempPermission.from_timestamp, permission: tempPermission.permission, file_prefix: tempPermission.file_prefix, } as BucketPermissionParameters ) : BucketPermissionService.bucketPermissionCreatePermission( tempPermission ); serverAnswerPromise .then((permission: BucketPermissionOut) => { if (editPermission.value) { emit("permission-edited", permission); } else { emit("permission-created", permission); } permissionModal?.hide(); successToast?.show(); updatePermission(); }) .catch(() => { formState.error = true; }) .finally(() => { formState.loading = false; }); } } /** * Delete a permission for a bucket user combination * @param bucketName Bucket to delete * @param uid ID of grantee of the permission */ function confirmedDeletePermission(bucketName: string, uid: string) { if (!formState.loading) { formState.loading = true; BucketPermissionService.bucketPermissionDeletePermissionForBucket( bucketName, uid ) .then(() => { permissionDeleted.value = true; permissionModal?.hide(); successToast?.show(); emit("permission-deleted", permission); }) .catch(() => { formState.error = true; }) .finally(() => { formState.loading = false; }); } } function updateUser(user: User) { permission.uid = user.uid; formState.grantee_name = user.display_name; } // Lifecycle Hooks // ----------------------------------------------------------------------------- onMounted(() => { permissionModal = new Modal("#" + props.modalID); successToast = new Toast("#" + "toast-" + randomIDSuffix); updatePermission(); }); </script> <template> <DeleteModal :modalID="'delete-permission-modal' + randomIDSuffix" object-name-delete="permission" :back-modal-id="modalID" @confirm-delete=" confirmedDeletePermission(permission.bucket_name, permission.uid) " /> <SearchUserModal :modalID="'search-user-modal' + randomIDSuffix" :back-modal-id="modalID" @user-found="updateUser" /> <div class="toast-container position-fixed top-toast end-0 p-3"> <div role="alert" aria-live="assertive" aria-atomic="true" class="toast text-bg-success align-items-center border-0" data-bs-autohide="true" :id="'toast-' + randomIDSuffix" v-on="{ 'hidden.bs.toast': toastHidden }" > <div class="d-flex"> <div class="toast-body"> Successfully <span v-if="permissionDeleted">deleted</span> <span v-else-if="editPermission">edited</span> <span v-else>created</span> Permission </div> <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close" ></button> </div> </div> </div> <bootstrap-modal :modalID="modalID" :static-backdrop="true" modal-label="Permission Modal" v-on="{ 'hidden.bs.modal': modalClosed }" > <template v-slot:header v-if="formState.readonly" >View Permission </template> <template v-slot:header v-else-if="props.editUserPermission !== undefined" >Edit Permission </template> <template v-slot:header v-else> Create new Permission </template> <template v-slot:extra-button> <bootstrap-icon v-if="props.deletable" icon="trash-fill" :height="15" :width="15" fill="currentColor" class="me-2" :class="{ 'delete-icon': !formState.loading }" data-bs-toggle="modal" :data-bs-target="'#delete-permission-modal' + randomIDSuffix" /> <bootstrap-icon v-if="formState.readonly && props.editable" icon="pencil-fill" :height="15" :width="15" fill="currentColor" class="pseudo-link" @click="formState.readonly = false" /> </template> <template v-slot:body> <form @submit.prevent="formSubmit" :id="'permissionCreateEditForm' + randomIDSuffix" > <div class="mb-3 row"> <label for="bucketNameInput" class="col-2 col-form-label" >Bucket<span v-if="!formState.readonly">*</span></label > <div class="col-10"> <input type="text" readonly class="form-control-plaintext" id="bucketNameInput" required :value="permission.bucket_name" /> </div> </div> <div class="mb-3 row align-items-center d-flex"> <label for="permissionGranteeInput" class="col-2 col-form-label"> User<span v-if="!formState.readonly">*</span> </label> <div :class="{ 'col-10': permissionUserReadonly, 'col-9': !permissionUserReadonly, }" > <input type="text" class="form-control" id="permissionGranteeInput" required placeholder="Search for a user" v-model.trim="formState.grantee_name" readonly /> </div> <div v-if="!permissionUserReadonly" class="col-1"> <button type="button" class="btn btn-secondary btn-sm float-end" data-bs-toggle="modal" :data-bs-target="'#search-user-modal' + randomIDSuffix" > <bootstrap-icon icon="search" :height="13" :width="13" fill="white" /> </button> </div> </div> <div class="mb-3 row"> <label for="permissionTypeInput" class="col-2 col-form-label"> Type<span v-if="!formState.readonly">*</span> </label> <div class="col-10"> <select class="form-select text-lowercase" id="permissionTypeInput" required :disabled="formState.readonly" v-model="permission.permission" > <option disabled selected>Select one...</option> <option v-for="perm in Permission" :key="perm" :value="perm"> {{ perm.toLowerCase() }} </option> </select> </div> </div> <div class="mb-3 row"> <label for="permissionDateFromInput" class="col-2 col-form-label" v-if="inputVisible(permission.from_timestamp)" > From </label> <div class="col-4" v-if="inputVisible(permission.from_timestamp)"> <input type="date" class="form-control" id="permissionDateFromInput" :readonly="formState.readonly" :min="dayjs().format('YYYY-MM-DD')" v-model="permission.from_timestamp" /> </div> <label for="permissionDateToInput" class="col-2 col-form-label" v-if="inputVisible(permission.to_timestamp)" > To </label> <div class="col-4" v-if="inputVisible(permission.to_timestamp)"> <input type="date" class="form-control" id="permissionToFromInput" :readonly="formState.readonly" v-model="permission.to_timestamp" :min=" permission.from_timestamp != null ? dayjs(permission.from_timestamp) .add(1, 'day') .format('YYYY-MM-DD') : dayjs().add(1, 'day').format('YYYY-MM-DD') " /> </div> </div> <div class="mb-3 row align-items-center d-flex" v-if=" inputVisible(permission.file_prefix) && possibleSubFolders.length > 0 " > <label for="permissionSubFolderInput" class="col-2 col-form-label"> Subfolder </label> <div :class="{ 'col-10': formState.readonly, 'col-9': !formState.readonly, }" > <select class="form-select" id="permissionSubFolderInput" :disabled="formState.readonly" v-model="permission.file_prefix" > <option disabled selected>Select one folder...</option> <option v-for="folder in possibleSubFolders" :key="folder" :value="folder" > {{ folder }} </option> </select> </div> <div class="col-1" v-if="!formState.readonly"> <button type="button" class="btn btn-outline-danger btn-sm float-end" @click="permission.file_prefix = undefined" :hidden="permission.file_prefix == undefined" > <bootstrap-icon icon="x-lg" :height="14" :width="14" fill="currentColor" /> </button> </div> </div> </form> <span class="text-danger" v-if="formState.error" >There was some kind of error<br />Try again later</span > </template> <template v-slot:footer> <button v-if="backModalId !== undefined" type="button" class="btn btn-secondary" :data-bs-target="'#' + props.backModalId" data-bs-toggle="modal" > Back </button> <button v-else type="button" class="btn btn-secondary" data-bs-dismiss="modal" > Close </button> <button type="submit" :form="'permissionCreateEditForm' + randomIDSuffix" class="btn btn-primary" :disabled="formState.loading" v-if="!formState.readonly" > <span v-if="formState.loading" class="spinner-border spinner-border-sm" role="status" aria-hidden="true" ></span> Save </button> </template> </bootstrap-modal> </template> <style scoped> .pseudo-link { cursor: pointer; color: var(--bs-secondary); } .pseudo-link:hover { color: var(--bs-primary); } .delete-icon { color: var(--bs-secondary); cursor: pointer; } .delete-icon:hover { color: var(--bs-danger); } </style>