-
Daniel Göbel authored
#35
Daniel Göbel authored#35
PermissionModal.vue 16.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 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>