diff --git a/src/components/BootstrapModal.vue b/src/components/BootstrapModal.vue index 09aece78d3e3388c5d47e6a3cc40887c8b008542..b03400b3ddb79ecb114aebdd355297346641ccb2 100644 --- a/src/components/BootstrapModal.vue +++ b/src/components/BootstrapModal.vue @@ -16,7 +16,7 @@ defineProps<{ :data-bs-backdrop="staticBackdrop ? 'static' : null" > <div - class="modal-dialog modal-dialog-centered text-dark" + class="modal-dialog modal-dialog-centered modal-dialog-scrollable text-dark" style="min-width: 25%" > <div class="modal-content"> diff --git a/src/components/BucketListItem.vue b/src/components/BucketListItem.vue index 69b1069b4a889885d7de1d15ea0741758188f38a..05cb78fadc59f74bbfcba88626d0c27a0c297ec8 100644 --- a/src/components/BucketListItem.vue +++ b/src/components/BucketListItem.vue @@ -14,7 +14,7 @@ const props = defineProps<{ permission: BucketPermission | undefined; }>(); -const tooltipID = Math.random().toString(16).substr(2, 8); +const randomIDSuffix = Math.random().toString(16).substr(2, 8); const emit = defineEmits<{ (e: "delete-bucket", bucketName: string): void; @@ -23,15 +23,15 @@ const emit = defineEmits<{ onMounted(() => { if (!props.loading) { - new Tooltip("#tooltip-" + tooltipID); + new Tooltip("#tooltip-" + randomIDSuffix); } }); </script> <template> <permission-modal - v-if="props.permission != null" - :modalID="'view-permission-modal' + tooltipID" + v-if="props.permission != null && props.active" + :modalID="'view-permission-modal' + randomIDSuffix" modal-label="view-permission-modal-label" :bucket-name="props.bucket.name" :sub-folders="{ subFolders: {}, files: [] }" @@ -39,7 +39,8 @@ onMounted(() => { :readonly="true" :editable="false" :deletable="true" - @permission-deleted="(bucketName) => emit('permission-deleted', bucketName)" + :back-modal-id="undefined" + @permission-deleted="(perm) => emit('permission-deleted', perm.bucket_name)" /> <div class="mt-2 mb-2"> <div @@ -77,13 +78,27 @@ onMounted(() => { :hidden="!props.active" class="ps-2 pe-2 rounded-bottom bg-light text-bg-light border border-3 border-top-0 border-primary" > + <div v-if="props.permission != null" class="ms-1 pt-1 text-info"> + Foreign Bucket + </div> <table class="table table-sm table-borderless mb-0"> <tbody> + <tr v-if="props.permission != null"> + <th scope="row" class="fw-bold">Permission</th> + <td> + <a + href="#" + data-bs-toggle="modal" + :data-bs-target="'#view-permission-modal' + randomIDSuffix" + >View</a + > + </td> + </tr> <tr> <th scope="row" class="fw-bold">Created:</th> <td> <span - :id="'tooltip-' + tooltipID" + :id="'tooltip-' + randomIDSuffix" data-bs-toggle="tooltip" :data-bs-title=" dayjs(bucket.created_at).format('DD.MM.YYYY HH:mm:ss') @@ -101,17 +116,6 @@ onMounted(() => { <th scope="row" class="fw-bold">Size:</th> <td>{{ fileSize(0) }}</td> </tr> - <tr v-if="props.permission != null"> - <th scope="row" class="fw-bold">Permission</th> - <td> - <a - href="#" - data-bs-toggle="modal" - :data-bs-target="'#view-permission-modal' + tooltipID" - >View</a - > - </td> - </tr> </tbody> </table> </div> diff --git a/src/components/BucketView.vue b/src/components/BucketView.vue index acec756a42eb46e147049f0c9704cfdca574b25e..778196eac4a7b32bcf4ac31cf8285a69e4c280b8 100644 --- a/src/components/BucketView.vue +++ b/src/components/BucketView.vue @@ -187,6 +187,11 @@ const errorLoadingObjects: ComputedRef<boolean> = computed( // ----------------------------------------------------------------------------- onMounted(() => { updateObjects(props.bucketName); + document + .querySelectorAll(".tooltip-container") + .forEach( + (tooltipTriggerEl) => new Tooltip(tooltipTriggerEl, { trigger: "hover" }) + ); }); // Functions @@ -330,18 +335,32 @@ watch( </div> </div> <!-- Upload object button --> - <div class="col-auto"> + <div id="BucketViewButtons" class="col-auto"> <button type="button" - class="btn btn-secondary me-2" + class="btn btn-secondary me-2 tooltip-container" :disabled="errorLoadingObjects" + data-bs-toggle="tooltip" + data-bs-title="Upload Object" > <bootstrap-icon icon="upload" :width="16" :height="16" fill="white" /> <span class="visually-hidden">Upload Object</span> </button> + <!-- Add folder button --> + <button + type="button" + class="btn btn-secondary m-2 tooltip-container" + :disabled="errorLoadingObjects" + data-bs-toggle="tooltip" + data-bs-title="Create Folder" + > + <bootstrap-icon icon="plus-lg" :width="16" :height="16" fill="white" /> + Folder + <span class="visually-hidden">Add Folder</span> + </button> <!-- Add bucket permission button --> <button - v-if="props.permission == null" + :hidden="props.permission != null" type="button" class="btn btn-secondary m-2" :disabled="errorLoadingObjects" @@ -365,17 +384,31 @@ watch( :editable="false" :readonly="false" :deletable="false" + :back-modal-id="undefined" /> - <!-- Add folder button --> <button + :hidden="props.permission != null" type="button" - class="btn btn-secondary m-2" + class="btn btn-secondary m-2 tooltip-container" :disabled="errorLoadingObjects" + data-bs-title="List Bucket Permission" + data-bs-toggle="modal" + data-bs-target="#permission-list-modal" > - <bootstrap-icon icon="plus-lg" :width="16" :height="16" fill="white" /> - Folder - <span class="visually-hidden">Add Folder</span> + <bootstrap-icon + icon="person-lines-fill" + :width="16" + :height="16" + fill="white" + /> + <span class="visually-hidden">View Bucket Permissions</span> </button> + <permission-list-modal + v-if="props.permission == null" + :bucket-name="props.bucketName" + :sub-folders="folderStructure" + modalID="permission-list-modal" + /> </div> </div> <!-- Body --> diff --git a/src/components/PermissionListModal.vue b/src/components/PermissionListModal.vue new file mode 100644 index 0000000000000000000000000000000000000000..655b7c44a0c4a4f2838726fb43405a6682b1323b --- /dev/null +++ b/src/components/PermissionListModal.vue @@ -0,0 +1,135 @@ +<script setup lang="ts"> +import type { BucketPermission, S3ObjectMetaInformation } from "@/client"; +import { reactive } from "vue"; +import { BucketPermissionsService } from "@/client"; +import { onBeforeMount, watch } from "vue"; +import BootstrapModal from "@/components/BootstrapModal.vue"; +import PermissionModal from "@/components/PermissionModal.vue"; +// Types +// ----------------------------------------------------------------------------- +interface S3ObjectWithFolder extends S3ObjectMetaInformation { + folder: string[]; + pseudoFileName: string; +} + +type FolderTree = { + subFolders: Record<string, FolderTree>; + files: S3ObjectWithFolder[]; +}; + +// Props +// ----------------------------------------------------------------------------- +const props = defineProps<{ + bucketName: string; + subFolders: FolderTree; + modalID: string; +}>(); + +// Reactive State +// ----------------------------------------------------------------------------- +const state = reactive({ + permissions: [], + loading: true, + currentPermission: { + bucket_name: "bucketname", + uid: "uid", + permission: "READ", + }, +} as { + permissions: BucketPermission[]; + loading: boolean; + currentPermission: BucketPermission; +}); + +const randomIDSuffix = Math.random().toString(16).substr(2, 8); + +// Watchers +// ----------------------------------------------------------------------------- +watch( + () => props.bucketName, + (newBucketName) => { + updateBucketPermissions(newBucketName); + } +); + +// Function +// ----------------------------------------------------------------------------- +function updateBucketPermissions(bucketName: string) { + state.loading = true; + BucketPermissionsService.bucketPermissionsListPermissionsPerBucket(bucketName) + .then((permissions) => { + state.permissions = permissions; + }) + .catch((err) => { + console.error(err); + }) + .finally(() => { + state.loading = false; + }); +} + +function permissionDeleted(bucketPermission: BucketPermission) { + state.permissions = state.permissions.filter( + (perm) => perm.uid != bucketPermission.uid + ); +} + +function permissionCreated(bucketPermission: BucketPermission) { + state.permissions.push(bucketPermission); +} + +function permissionEdited(bucketPermission: BucketPermission) { + const index = state.permissions.findIndex( + (perm) => perm.uid == bucketPermission.uid + ); + state.permissions[index] = bucketPermission; +} + +// Lifecycle Hooks +// ----------------------------------------------------------------------------- +onBeforeMount(() => { + updateBucketPermissions(props.bucketName); +}); +</script> + +<template> + <permission-modal + :deletable="true" + :editable="true" + :readonly="true" + :edit-user-permission="state.currentPermission" + :bucket-name="state.currentPermission.bucket_name" + :sub-folders="props.subFolders" + :back-modal-id="props.modalID" + :modalID="'permission-list-edit-modal' + randomIDSuffix" + modal-label="permission-list-edit" + @permission-deleted="permissionDeleted" + @permission-created="permissionCreated" + @permission-edited="permissionEdited" + /> + <bootstrap-modal + :modalID="props.modalID" + :static-backdrop="true" + modal-label="permission-list" + > + <template v-slot:header> Bucket Permissions </template> + <template v-slot:body> + <div class="list-group"> + <button + type="button" + class="list-group-item list-group-item-action text-truncate" + v-for="permission in state.permissions" + :key="permission.uid" + @click="state.currentPermission = permission" + data-bs-toggle="modal" + :data-bs-target="'#permission-list-edit-modal' + randomIDSuffix" + > + <span class="text-info">{{ permission.permission }}</span> + {{ permission.uid }} + </button> + </div> + </template> + </bootstrap-modal> +</template> + +<style scoped></style> diff --git a/src/components/PermissionModal.vue b/src/components/PermissionModal.vue index b85042261cc937c5a8e45168dc23d4fb10ad4a80..b8809da918f0c034e74a02659947b1f576b45955 100644 --- a/src/components/PermissionModal.vue +++ b/src/components/PermissionModal.vue @@ -1,5 +1,5 @@ <script setup lang="ts"> -import { onMounted, reactive, watch, ref, computed, defineEmits } from "vue"; +import { onMounted, reactive, watch, ref, computed } from "vue"; import BootstrapModal from "@/components/BootstrapModal.vue"; import { Modal } from "bootstrap"; import dayjs from "dayjs"; @@ -36,11 +36,12 @@ const props = defineProps<{ readonly: boolean; editable: boolean; deletable: boolean; + backModalId: string | undefined; }>(); // Variables // ----------------------------------------------------------------------------- -const toastID = Math.random().toString(16).substr(2, 8); +const randomIDSuffix = Math.random().toString(16).substr(2, 8); let permissionModal: Modal | null = null; let successToast: Toast | null = null; @@ -99,7 +100,9 @@ watch( // Events // ----------------------------------------------------------------------------- const emit = defineEmits<{ - (e: "permission-deleted", bucket_name: string): void; + (e: "permission-deleted", permission: BucketPermission): void; + (e: "permission-created", permission: BucketPermission): void; + (e: "permission-edited", permission: BucketPermission): void; }>(); // Functions @@ -189,25 +192,27 @@ function formSubmit() { formState.error = false; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const form = document.getElementById( - "permissionCreateForm" + "permissionCreateEditForm" + randomIDSuffix )! as HTMLFormElement; if (form.checkValidity()) { const tempPermission: BucketPermission = permission; if (permission.from_timestamp != null) { - tempPermission.from_timestamp = dayjs( - permission.from_timestamp - ).toISOString(); + tempPermission.from_timestamp = + permission.from_timestamp.length > 0 + ? dayjs(permission.from_timestamp).toISOString() + : undefined; } if (permission.to_timestamp != null) { - tempPermission.to_timestamp = dayjs( - permission.to_timestamp - ).toISOString(); + tempPermission.to_timestamp = + permission.to_timestamp.length > 0 + ? dayjs(permission.to_timestamp).toISOString() + : undefined; } formState.loading = true; const serverAnswerPromise = editPermission.value ? BucketPermissionsService.bucketPermissionsUpdatePermission( - permission.bucket_name, permission.uid, + permission.bucket_name, { to_timestamp: tempPermission.to_timestamp, from_timestamp: tempPermission.from_timestamp, @@ -219,7 +224,12 @@ function formSubmit() { tempPermission ); serverAnswerPromise - .then(() => { + .then((permission) => { + if (editPermission.value) { + emit("permission-edited", permission); + } else { + emit("permission-created", permission); + } permissionModal?.hide(); successToast?.show(); updatePermission(); @@ -249,7 +259,7 @@ function deletePermission(bucketName: string, uid: string) { permissionDeleted.value = true; permissionModal?.hide(); successToast?.show(); - emit("permission-deleted", bucketName); + emit("permission-deleted", permission); }) .catch(() => { formState.error = true; @@ -264,7 +274,7 @@ function deletePermission(bucketName: string, uid: string) { // ----------------------------------------------------------------------------- onMounted(() => { permissionModal = new Modal("#" + props.modalID); - successToast = new Toast("#" + "toast-" + toastID, { autohide: true }); + successToast = new Toast("#" + "toast-" + randomIDSuffix, { autohide: true }); updatePermission(); }); </script> @@ -277,7 +287,7 @@ onMounted(() => { aria-atomic="true" class="toast text-bg-success align-items-center border-0" data-bs-autohide="false" - :id="'toast-' + toastID" + :id="'toast-' + randomIDSuffix" v-on="{ 'hidden.bs.toast': toastHidden }" > <div class="d-flex"> @@ -306,7 +316,7 @@ onMounted(() => { <template v-slot:header v-if="formState.readonly" >View Permission </template> - <template v-slot:header v-else-if="props.editUserPermission != null" + <template v-slot:header v-else-if="props.editUserPermission !== undefined" >Edit Permission </template> <template v-slot:header v-else> Create new Permission </template> @@ -332,7 +342,10 @@ onMounted(() => { /> </template> <template v-slot:body> - <form @submit.prevent="formSubmit" id="permissionCreateForm"> + <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 @@ -464,12 +477,26 @@ onMounted(() => { > </template> <template v-slot:footer> - <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> + <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="permissionCreateForm" + :form="'permissionCreateEditForm' + randomIDSuffix" class="btn btn-primary" :disabled="formState.loading" v-if="!formState.readonly"