Skip to content
Snippets Groups Projects
Verified Commit 44a1d1a4 authored by Daniel Göbel's avatar Daniel Göbel
Browse files

Add a modal where it is possible to create or edit a permission

#14
parent 6277b534
No related branches found
No related tags found
2 merge requests!22Version 1.0.0,!10Permission Modal
Pipeline #24686 passed
...@@ -15,18 +15,24 @@ defineProps<{ ...@@ -15,18 +15,24 @@ defineProps<{
aria-hidden="true" aria-hidden="true"
:data-bs-backdrop="staticBackdrop ? 'static' : null" :data-bs-backdrop="staticBackdrop ? 'static' : null"
> >
<div class="modal-dialog modal-dialog-centered text-dark"> <div
class="modal-dialog modal-dialog-centered text-dark"
style="min-width: 25%"
>
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<div class="modal-title fs-5" :id="modalLabel"> <div class="modal-title fs-5" :id="modalLabel">
<slot name="header" /> <slot name="header" />
</div> </div>
<button <div>
type="button" <slot name="extra-button" />
class="btn-close" <button
data-bs-dismiss="modal" type="button"
aria-label="Close" class="btn-close"
></button> data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<slot name="body" /> <slot name="body" />
......
...@@ -7,6 +7,7 @@ import BootstrapIcon from "@/components/BootstrapIcon.vue"; ...@@ -7,6 +7,7 @@ import BootstrapIcon from "@/components/BootstrapIcon.vue";
import fileSize from "filesize"; import fileSize from "filesize";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { Tooltip } from "bootstrap"; import { Tooltip } from "bootstrap";
import PermissionModal from "@/components/PermissionModal.vue";
// Constants // Constants
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
...@@ -311,52 +312,68 @@ watch( ...@@ -311,52 +312,68 @@ watch(
</nav> </nav>
<!-- Inputs on top --> <!-- Inputs on top -->
<!-- Search bucket text input --> <!-- Search bucket text input -->
<div class="input-group mt-2"> <div class="row">
<span class="input-group-text" id="objects-search-wrapping" <div class="col-8">
><bootstrap-icon icon="search" :width="16" :height="16" <div class="input-group mt-2">
/></span> <span class="input-group-text" id="objects-search-wrapping"
<input ><bootstrap-icon icon="search" :width="16" :height="16"
type="text" /></span>
class="form-control" <input
placeholder="Search Objects" type="text"
aria-label="Search Objects" class="form-control"
aria-describedby="objects-search-wrapping" placeholder="Search Objects"
disabled aria-label="Search Objects"
/> aria-describedby="objects-search-wrapping"
disabled
/>
</div>
</div>
<!-- Upload object button -->
<div class="col-auto">
<button
type="button"
class="btn btn-secondary me-2"
:disabled="errorLoadingObjects"
>
<bootstrap-icon icon="upload" :width="16" :height="16" fill="white" />
<span class="visually-hidden">Upload Object</span>
</button>
<!-- Add bucket permission button -->
<button
type="button"
class="btn btn-secondary m-2"
:disabled="errorLoadingObjects"
data-bs-toggle="modal"
data-bs-target="#create-permission-modal"
>
<bootstrap-icon
icon="person-plus-fill"
:width="16"
:height="16"
fill="white"
/>
<span class="visually-hidden">Add Bucket Permission</span>
</button>
<permission-modal
modalID="create-permission-modal"
modal-label="create-permission-modal-label"
:bucket-name="props.bucketName"
:sub-folders="folderStructure"
:edit-user-permission="undefined"
:readonly="false"
/>
<!-- Add folder button -->
<button
type="button"
class="btn btn-secondary m-2"
:disabled="errorLoadingObjects"
>
<bootstrap-icon icon="plus-lg" :width="16" :height="16" fill="white" />
Folder
<span class="visually-hidden">Add Folder</span>
</button>
</div>
</div> </div>
<!-- Upload object button -->
<button
type="button"
class="btn btn-secondary m-2"
:disabled="errorLoadingObjects"
>
<bootstrap-icon icon="upload" :width="16" :height="16" fill="white" />
<span class="visually-hidden">Upload Object</span>
</button>
<!-- Add bucket permission button -->
<button
type="button"
class="btn btn-secondary m-2"
:disabled="errorLoadingObjects"
>
<bootstrap-icon
icon="person-plus-fill"
:width="16"
:height="16"
fill="white"
/>
<span class="visually-hidden">Add Bucket Permission</span>
</button>
<!-- Add folder button -->
<button
type="button"
class="btn btn-secondary m-2"
:disabled="errorLoadingObjects"
>
<bootstrap-icon icon="plus-lg" :width="16" :height="16" fill="white" />
Folder
<span class="visually-hidden">Add Folder</span>
</button>
<!-- Body --> <!-- Body -->
<div class="pt-3"> <div class="pt-3">
<!-- If bucket not found --> <!-- If bucket not found -->
......
<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>
...@@ -22,4 +22,4 @@ app.use(router); ...@@ -22,4 +22,4 @@ app.use(router);
app.mount("#app"); app.mount("#app");
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
import { Modal, Collapse, Dropdown, Tooltip } from "bootstrap"; import { Modal, Collapse, Dropdown, Tooltip, Toast } from "bootstrap";
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, reactive } from "vue"; import { onMounted, reactive } from "vue";
import type { BucketOut } from "@/client"; import type { BucketOut, BucketPermission } from "@/client";
import { BucketService } from "@/client"; import { BucketService, BucketPermissionsService } from "@/client";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import BootstrapIcon from "@/components/BootstrapIcon.vue"; import BootstrapIcon from "@/components/BootstrapIcon.vue";
import CreateBucketComponent from "@/components/CreateBucketComponent.vue"; import CreateBucketComponent from "@/components/CreateBucketComponent.vue";
import BucketListItem from "@/components/BucketListItem.vue"; import BucketListItem from "@/components/BucketListItem.vue";
import { useAuthStore } from "@/stores/auth";
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const authStore = useAuthStore();
const bucketsState = reactive({ const bucketsState = reactive({
buckets: [], buckets: [],
permissions: {},
loading: true, loading: true,
} as { } as {
buckets: BucketOut[]; buckets: BucketOut[];
loading: boolean; loading: boolean;
permissions: Record<string, BucketPermission>;
}); });
function fetchBuckets() { function fetchBuckets() {
...@@ -26,6 +30,17 @@ function fetchBuckets() { ...@@ -26,6 +30,17 @@ function fetchBuckets() {
.finally(() => { .finally(() => {
bucketsState.loading = false; bucketsState.loading = false;
}); });
if (authStore.user != null) {
BucketPermissionsService.bucketPermissionsListPermissionsPerUser(
authStore.user.uid
).then((permissions) => {
const new_permissions: Record<string, BucketPermission> = {};
for (const perm of permissions) {
new_permissions[perm.bucket_name] = perm;
}
bucketsState.permissions = new_permissions;
});
}
} }
function addBucket(bucket: BucketOut) { function addBucket(bucket: BucketOut) {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment