<script setup lang="ts"> import { onMounted, reactive, watch, computed } from "vue"; import BootstrapModal from "@/components/BootstrapModal.vue"; import { Modal } from "bootstrap"; import dayjs from "dayjs"; import type { BucketPermission, S3ObjectMetaInformation, BucketPermissionParameters, } from "@/client"; import type { ComputedRef } from "vue"; import { PermissionEnum, BucketPermissionsService } from "@/client"; import { Toast } from "bootstrap"; import BootstrapIcon from "@/components/BootstrapIcon.vue"; // Types // ----------------------------------------------------------------------------- interface S3ObjectWithFolder extends S3ObjectMetaInformation { folder: string[]; pseudoFileName: string; } type FolderTree = { subFolders: Record<string, FolderTree>; files: S3ObjectWithFolder[]; }; // Props // ----------------------------------------------------------------------------- const props = defineProps<{ modalID: string; modalLabel: string; bucketName: string; subFolders: FolderTree; editUserPermission: BucketPermission | undefined; readonly: boolean; }>(); // Variables // ----------------------------------------------------------------------------- const toastID = Math.random().toString(16).substr(2, 8); let createPermissionModal: Modal | null = null; let successToast: Toast | null = null; // Reactive State // ----------------------------------------------------------------------------- const formState = reactive({ loading: false, error: false, readonly: props.readonly, } as { loading: boolean; error: boolean; readonly: boolean; }); const permission = reactive({ from_timestamp: undefined, to_timestamp: undefined, file_prefix: undefined, permission: undefined, uid: "", bucket_name: props.bucketName, } as BucketPermission); // 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() ); // Functions // ----------------------------------------------------------------------------- /** * Reset the form. Triggered when the modal is closed. */ function modalClosed() { formState.readonly = props.readonly; formState.error = false; if (editPermission.value) { updatePermission(); } } /** * Check if a 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; 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; } } /** * 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( "permissionCreateForm" )! as HTMLFormElement; if (form.checkValidity()) { const tempPermission: BucketPermission = permission; if (permission.from_timestamp != null) { tempPermission.from_timestamp = dayjs( permission.from_timestamp ).toISOString(); } if (permission.to_timestamp != null) { tempPermission.to_timestamp = dayjs( permission.to_timestamp ).toISOString(); } formState.loading = true; const serverAnswerPromise = editPermission.value ? BucketPermissionsService.bucketPermissionsUpdatePermission( permission.bucket_name, permission.uid, { to_timestamp: tempPermission.to_timestamp, from_timestamp: tempPermission.from_timestamp, permission: tempPermission.permission, file_prefix: tempPermission.file_prefix, } as BucketPermissionParameters ) : BucketPermissionsService.bucketPermissionsCreatePermission( tempPermission ); serverAnswerPromise .then(() => { createPermissionModal?.hide(); successToast?.show(); }) .catch((err) => { formState.error = true; console.error(err); }) .finally(() => { formState.loading = false; }); } } // Lifecycle Hooks // ----------------------------------------------------------------------------- onMounted(() => { createPermissionModal = new Modal("#" + props.modalID); successToast = new Toast("#" + "toast-" + toastID, { autohide: true }); }); </script> <template> <div class="toast-container position-fixed top-0 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="false" :id="'toast-' + toastID" > <div class="d-flex"> <div class="toast-body"> Successfully <span v-if="editPermission">created</span> <span v-else>edited</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="modalLabel" v-on="{ 'hidden.bs.modal': modalClosed }" > <template v-slot:header> Create new Permission </template> <template v-slot:extra-button v-if="formState.readonly"> <bootstrap-icon 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="permissionCreateForm"> <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"> <label for="permissionGranteeInput" class="col-2 col-form-label"> User<span v-if="!formState.readonly">*</span> </label> <div class="col-10"> <input type="text" :class="{ 'form-control-plaintext': permissionUserReadonly, 'form-control': !permissionUserReadonly, }" id="permissionGranteeInput" required minlength="3" maxlength="64" placeholder="Grantee of the permission" v-model.trim="permission.uid" :readonly="permissionUserReadonly" /> </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 PermissionEnum" :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" v-if=" inputVisible(permission.file_prefix) && possibleSubFolders.length > 0 " > <label for="permissionSubFolderInput" class="col-2 col-form-label"> Subfolder </label> <div class="col-10"> <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> </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 type="button" class="btn btn-secondary" data-bs-dismiss="modal"> Close </button> <button type="submit" form="permissionCreateForm" 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); } </style>