-
Daniel Göbel authored
#88
Daniel Göbel authored#88
PermissionModal.vue 15.91 KiB
<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 { Permission } from "@/client/s3proxy";
import { Toast } from "bootstrap";
import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
import { useBucketStore } from "@/stores/buckets";
import BootstrapToast from "@/components/BootstrapToast.vue";
// Props
// -----------------------------------------------------------------------------
const props = defineProps<{
modalID: string;
bucketName: string;
subFolders: FolderTree;
editUserPermission?: BucketPermissionOut;
readonly: boolean;
editable: boolean;
deletable: boolean;
backModalId?: string;
}>();
const bucketRepository = useBucketStore();
const emit = defineEmits<{ (e: "permission-deleted"): void }>();
// Variables
// -----------------------------------------------------------------------------
const randomIDSuffix = Math.random().toString(16).substring(2, 8);
let permissionModal: Modal | null = null;
let successToast: Toast | null = null;
// Reactive State
// -----------------------------------------------------------------------------
// eslint-disable-next-line vue/no-setup-props-destructure
const formState = reactive<{
loading: boolean;
grantee_name: string;
error: boolean;
readonly: boolean;
}>({
loading: false,
grantee_name: "",
error: false,
readonly: props.readonly,
});
// eslint-disable-next-line vue/no-setup-props-destructure
const permission = reactive<BucketPermissionIn>({
from_timestamp: undefined,
to_timestamp: undefined,
file_prefix: undefined,
permission: undefined,
uid: "",
bucket_name: props.bucketName,
});
const permissionDeleted = ref<boolean>(false);
const permissionForm = ref<HTMLFormElement | undefined>(undefined);
// Computes Properties
// -----------------------------------------------------------------------------
const editPermission = computed<boolean>(
() => props.editUserPermission != undefined,
);
const possibleSubFolders = computed<string[]>(() =>
findSubFolders(props.subFolders, []),
);
const permissionUserReadonly = computed<boolean>(() => {
return formState.readonly || editPermission.value;
});
// Watchers
// -----------------------------------------------------------------------------
watch(
() => props.bucketName,
(newBucketName) => {
updateLocalPermission();
permission.bucket_name = newBucketName;
},
);
watch(
() => props.editUserPermission,
() => updateLocalPermission(),
);
// Functions
// -----------------------------------------------------------------------------
/**
* Reset the form. Triggered when the modal is closed.
*/
function modalClosed() {
formState.readonly = props.readonly;
formState.error = false;
if (editPermission.value) {
updateLocalPermission();
}
}
/**
* 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?: number | string | null): boolean {
return !formState.readonly || input != undefined;
}
/**
* Update the form content
*/
function updateLocalPermission() {
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;
permission.to_timestamp = props.editUserPermission.to_timestamp;
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 +
(subFolder.endsWith("/") ? "" : "/");
arr.push(
subFolderString,
...findSubFolders(
currentFolder.subFolders[subFolder],
subFolderString.slice(0, subFolderString.length - 1).split("/"),
),
);
}
return arr;
}
/**
* Submit the form
*/
function formSubmit() {
formState.error = false;
if (permissionForm.value?.checkValidity()) {
formState.loading = true;
const serverAnswerPromise = editPermission.value
? bucketRepository.updateBucketPermission(
permission.bucket_name,
permission.uid,
{
to_timestamp: permission.to_timestamp,
from_timestamp: permission.from_timestamp,
permission: permission.permission,
file_prefix: permission.file_prefix,
} as BucketPermissionParameters,
)
: bucketRepository.createBucketPermission(permission);
serverAnswerPromise
.then(() => {
permissionModal?.hide();
successToast?.show();
updateLocalPermission();
})
.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;
bucketRepository
.deleteBucketPermission(bucketName, uid)
.then(() => {
permissionDeleted.value = true;
emit("permission-deleted");
permissionModal?.hide();
successToast?.show();
})
.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);
updateLocalPermission();
});
function fromTimestampChanged(target?: HTMLInputElement | null) {
permission.from_timestamp = target?.value
? dayjs(target?.value).unix()
: undefined;
}
function toTimestampChanged(target?: HTMLInputElement | null) {
permission.to_timestamp = target?.value
? dayjs(target?.value).unix()
: undefined;
}
</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"
/>
<bootstrap-toast
:toast-id="'toast-' + randomIDSuffix"
v-on="{ 'hidden.bs.toast': toastHidden }"
>
Successfully
<template v-if="permissionDeleted">deleted</template>
<template v-else-if="editPermission">edited</template>
<template v-else>created</template>
Permission
</bootstrap-toast>
<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 Bucket Permission</template>
<template v-slot:extra-button>
<font-awesome-icon
v-if="props.deletable"
icon="fa-solid fa-trash"
class="me-2 cursor-pointer"
:class="{ 'delete-icon': !formState.loading }"
data-bs-toggle="modal"
:data-bs-target="'#delete-permission-modal' + randomIDSuffix"
/>
<font-awesome-icon
v-if="formState.readonly && props.editable"
icon="fa-solid fa-pen"
class="pseudo-link cursor-pointer"
@click="formState.readonly = false"
/>
</template>
<template v-slot:body>
<form
@submit.prevent="formSubmit"
:id="'permissionCreateEditForm' + randomIDSuffix"
ref="permissionForm"
>
<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"
>
<font-awesome-icon icon="fa-solid fa-magnifying-glass" />
</button>
</div>
</div>
<div class="mb-3 row">
<label for="permissionTypeInput" class="col-3 col-form-label">
Permission Type<span v-if="!formState.readonly">*</span>
</label>
<div class="col-9">
<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')"
:value="
permission.from_timestamp
? dayjs.unix(permission.from_timestamp).format('YYYY-MM-DD')
: undefined
"
@input="
(event) =>
fromTimestampChanged(event.target as HTMLInputElement)
"
/>
</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="permissionDateToInput"
:readonly="formState.readonly"
:min="
permission.from_timestamp != null
? dayjs
.unix(permission.from_timestamp)
.add(1, 'day')
.format('YYYY-MM-DD')
: dayjs().add(1, 'day').format('YYYY-MM-DD')
"
:value="
permission.to_timestamp
? dayjs.unix(permission.to_timestamp).format('YYYY-MM-DD')
: undefined
"
@input="
(event) => toTimestampChanged(event.target as HTMLInputElement)
"
/>
</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"
>
<font-awesome-icon icon="fa-solid fa-x" />
</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 {
color: var(--bs-secondary);
}
.pseudo-link:hover {
color: var(--bs-link-hover-color);
}
.delete-icon {
color: var(--bs-secondary) !important;
}
.delete-icon:hover {
color: var(--bs-danger) !important;
}
</style>