Skip to content
Snippets Groups Projects
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>