Skip to content
Snippets Groups Projects
BucketView.vue 26.6 KiB
Newer Older
  • Learn to ignore specific revisions
  • <script setup lang="ts">
    import { onMounted, reactive, watch, computed } from "vue";
    
    import type {
      FolderTree,
      S3PseudoFolder,
      S3ObjectWithFolder,
    } from "@/types/PseudoFolder";
    
    import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
    
    Daniel Göbel's avatar
    Daniel Göbel committed
    import { filesize } from "filesize";
    
    import dayjs from "dayjs";
    
    import { Toast, Tooltip } from "bootstrap";
    
    import PermissionListModal from "@/components/object-storage/modals/PermissionListModal.vue";
    import UploadObjectModal from "@/components/object-storage/modals/UploadObjectModal.vue";
    import CopyObjectModal from "@/components/object-storage/modals/CopyObjectModal.vue";
    import PermissionModal from "@/components/object-storage/modals/PermissionModal.vue";
    import ObjectDetailModal from "@/components/object-storage/modals/ObjectDetailModal.vue";
    import CreateFolderModal from "@/components/object-storage/modals/CreateFolderModal.vue";
    import DeleteModal from "@/components/modals/DeleteModal.vue";
    
    import type { _Object as S3Object } from "@aws-sdk/client-s3";
    
    import { useAuthStore } from "@/stores/users";
    
    import { useBucketStore } from "@/stores/buckets";
    
    import { useS3ObjectStore } from "@/stores/s3objects";
    
    import { useS3KeyStore } from "@/stores/s3keys";
    
    
    const authStore = useAuthStore();
    
    const bucketRepository = useBucketStore();
    
    const objectRepository = useS3ObjectStore();
    const s3KeyRepository = useS3KeyStore();
    
    // Constants
    // -----------------------------------------------------------------------------
    
    
    const props = defineProps<{
      bucketName: string;
      subFolders: string[] | string;
    }>();
    
    const randomIDSuffix = Math.random().toString(16).substring(2, 8);
    
    let successToast: Toast | null = null;
    
    let refreshTimeout: NodeJS.Timeout | undefined = undefined;
    
    // Reactive State
    // -----------------------------------------------------------------------------
    
    const deleteObjectsState = reactive<{
    
      deletedItem: string;
      potentialObjectToDelete: string;
      deleteFolder: boolean;
    
    }>({
      deletedItem: "",
      potentialObjectToDelete: "",
      deleteFolder: true,
    
    const objectState = reactive<{
      loading: boolean;
      filterString: string;
      bucketNotFoundError: boolean;
      bucketPermissionError: boolean;
      editObjectKey: string;
    
      viewDetailKey: string | undefined;
    
      loading: true,
    
      filterString: "",
    
      bucketNotFoundError: false,
      bucketPermissionError: false,
    
      editObjectKey: "",
      copyObject: {
    
        Key: "",
        Size: 0,
        LastModified: new Date(),
    
      viewDetailKey: undefined,
    
    // Computed Properties
    // -----------------------------------------------------------------------------
    
    const filteredObjects = computed<(S3ObjectWithFolder | S3PseudoFolder)[]>(
      () => {
    
        return objectState.filterString.length > 0
    
          ? visibleObjects.value.filter(
              (obj) => obj.Key?.includes(objectState.filterString),
    
            )
          : visibleObjects.value;
    
    const s3Objects = computed<S3Object[]>(
      () => objectRepository.objectMapping[props.bucketName] ?? [],
    );
    
    
    const folderStructure = computed<FolderTree>(() => {
    
      /**
       * Store the entire folder structure in a bucket in a tree-like data structure
       */
      return objectsWithFolders.value.reduce(
        // Add one object after another to the folder structure
        (fTree, currentObject) => {
          // If the object is not in a sub folder, but it in the top level 'folder'
          if (currentObject.folder.length === 0) {
            fTree.files.push(currentObject);
          } else {
            // If the object is in a sub folder
            let currentFolder: FolderTree = fTree;
            // For every sub folder the object is in , navigate into the sub folder
            for (const folderName of currentObject.folder) {
              // If the sub folder doesn't exist yet, create it
              if (
                Object.keys(currentFolder.subFolders).find(
    
                  (subFolderName) => subFolderName === folderName,
    
                ) == undefined
              ) {
                currentFolder.subFolders[folderName] = {
                  subFolders: {},
                  files: [],
                };
              }
              // navigate into the sub folder
              currentFolder = currentFolder.subFolders[folderName] as FolderTree;
            }
            // Add object to the folder
            currentFolder.files.push(currentObject);
          }
          return fTree;
        },
        // Empty folder structure as initial value
        {
          subFolders: {},
          files: [],
    
    const objectsWithFolders = computed<S3ObjectWithFolder[]>(() => {
    
      /**
       * Add to the meta information from objects the pseudo filename and their pseudo folder
       * This can be inferred from the key of the object where the '/' character is the delimiter, e.g.
       * dir1/dir2/text.txt ->
       *  folder: dir1, dir2
       *  filename: text.txt
       */
    
      return s3Objects.value.map((obj) => {
        const splittedKey = obj.Key?.split("/") ?? [""];
    
        return {
          ...obj,
          pseudoFileName: splittedKey[splittedKey.length - 1],
          folder: splittedKey.slice(0, splittedKey.length - 1),
        };
      });
    });
    
    
    const currentSubFolders = computed<string[]>(() => {
    
      /**
       * Transform a single sub folder from a string to an array containing the string and
       * replace an empty string with an empty list
       */
      return props.subFolders instanceof Array
        ? props.subFolders
        : props.subFolders.length > 0
        ? [props.subFolders]
        : [];
    });
    
    
    const visibleObjects = computed<(S3ObjectWithFolder | S3PseudoFolder)[]>(() => {
      /**
       * Compute the visible objects based on the current sub folder
       */
      let currentFolder = folderStructure.value;
      // Navigate into right sub folder
      for (const subFolder of currentSubFolders.value) {
        if (currentFolder.subFolders[subFolder] == null) {
          // If sub folder doesn't exist, no object is visible
          return [];
        } else {
          currentFolder = currentFolder.subFolders[subFolder];
    
      }
      // Add all objects and sub folders from the current sub folder as visible object
      const arr = [];
      arr.push(...currentFolder.files);
      arr.push(
        ...Object.keys(currentFolder.subFolders).map((subFolderName) => {
          const folderSize = calculateFolderSize(
    
            currentFolder.subFolders[subFolderName],
    
          );
          const folderLastModified = dayjs(
    
            calculateFolderLastModified(currentFolder.subFolders[subFolderName]),
    
          return {
            name: subFolderName,
    
            parentFolder: currentSubFolders.value,
    
          } as S3PseudoFolder;
    
      return arr.filter(
        (obj) => !obj.Key?.endsWith("/") && (obj.Key?.length ?? 0) > 0,
      );
    
    const subFolderInUrl = computed<boolean>(
    
      () => currentSubFolders.value.length > 0,
    
    const errorLoadingObjects = computed<boolean>(
    
      () => objectState.bucketPermissionError || objectState.bucketNotFoundError,
    
    const writableBucket = computed<boolean>(() => {
      // Allow only upload in bucket folder with respect to permission prefix
      let prefixWritable = true;
      if (
        bucketRepository.ownPermissions[props.bucketName]?.file_prefix != undefined
      ) {
        prefixWritable =
          bucketRepository.ownPermissions[props.bucketName]?.file_prefix ===
          currentSubFolders.value.join("/") + "/";
      }
      return bucketRepository.writableBucket(props.bucketName) && prefixWritable;
    });
    
    const readableBucket = computed<boolean>(() =>
    
      bucketRepository.readableBucket(props.bucketName),
    
    // Watchers
    // -----------------------------------------------------------------------------
    watch(
      () => props.bucketName,
      (newBucketName, oldBucketName) => {
        if (oldBucketName !== newBucketName) {
    
          objectState.viewDetailKey = undefined;
    
          // If bucket is changed, update the objects
    
          objectState.bucketPermissionError = false;
          objectState.bucketNotFoundError = false;
          fetchObjects();
    
          objectState.filterString = "";
        }
      },
    );
    
    watch(
      visibleObjects,
      (visObjs) => {
        if (visObjs.length > 0) {
          // Initialise tooltips after DOM changes
          setTimeout(() => {
            document
              .querySelectorAll("span.date-tooltip")
              .forEach((tooltipTriggerEl) => new Tooltip(tooltipTriggerEl));
          }, 500);
        }
      },
      { flush: "post" },
    );
    
    
    // Lifecycle Hooks
    // -----------------------------------------------------------------------------
    
    onMounted(() => {
    
      let counter = 0;
      const onFinally = () => {
        counter++;
        if (counter > 1) {
          fetchObjects();
        }
      };
      // wait till s3keys and ownPermissions are available before fetching objects
      s3KeyRepository.fetchS3Keys(onFinally);
      bucketRepository.fetchOwnPermissions(onFinally);
    
    
      document
        .querySelectorAll(".tooltip-container")
        .forEach(
    
          (tooltipTriggerEl) => new Tooltip(tooltipTriggerEl, { trigger: "hover" }),
    
      successToast = new Toast("#successToast-" + randomIDSuffix);
    
    // Functions
    // -----------------------------------------------------------------------------
    /**
     * Calculate recursively the cumulative file size of all o objects in a folder
     * @param folder Folder to inspect
     * @returns The size of this folder in bytes
     */
    function calculateFolderSize(folder: FolderTree): number {
      let folderSize = 0;
    
      folderSize += folder.files.reduce((acc, file) => acc + (file.Size ?? 0), 0);
    
      for (const subFolderName of Object.keys(folder.subFolders)) {
        folderSize += calculateFolderSize(folder.subFolders[subFolderName]);
      }
      return folderSize;
    }
    
    /**
     * Calculate recursively when an object in a folder were modified the last time
     * @param folder Folder to inspect
     * @returns The last modified timestamp as ISO string
     */
    function calculateFolderLastModified(folder: FolderTree): string {
      let lastModified: dayjs.Dayjs;
      lastModified = folder.files
    
        .reduce(
          (acc, fileAccessed) => (fileAccessed.isAfter(acc) ? fileAccessed : acc),
    
          dayjs("2000-01-01"),
    
        );
      for (const subFolderName of Object.keys(folder.subFolders)) {
        const lastModifiedSubFolder = dayjs(
    
          calculateFolderLastModified(folder.subFolders[subFolderName]),
    
        );
        if (lastModifiedSubFolder.isAfter(lastModified)) {
          lastModified = lastModifiedSubFolder;
        }
      }
      return lastModified.toISOString();
    }
    
    /**
    
     * Fetch object from bucket with loading animation
    
      objectState.loading = true;
    
      objectState.bucketPermissionError = false;
      objectState.bucketNotFoundError = false;
    
      const prefix: string | undefined =
        bucketRepository.ownPermissions[props.bucketName]?.file_prefix ?? undefined;
      objectRepository
        .fetchS3Objects(props.bucketName, prefix, () => {
          objectState.loading = false;
    
          objectState.bucketPermissionError = false;
          objectState.bucketNotFoundError = false;
    
        })
        .catch((error) => {
    
          if (error.Code == "AccessDenied") {
    
            objectState.bucketPermissionError = true;
    
          } else {
            objectState.bucketNotFoundError = true;
    
    /**
     * Fetch the meta information about objects from a bucket
     */
    function refreshObjects() {
      clearTimeout(refreshTimeout);
      refreshTimeout = setTimeout(() => {
        fetchObjects();
      }, 500);
    }
    
    
    function isS3Object(
    
      obj: S3PseudoFolder | S3ObjectWithFolder,
    
    ): obj is S3ObjectWithFolder {
      return (obj as S3ObjectWithFolder).folder !== undefined;
    }
    
    function deleteObject(key?: string) {
      if (key == undefined) {
        return;
    
      deleteObjectsState.potentialObjectToDelete = key;
      deleteObjectsState.deleteFolder = false;
    
    /**
     * Delete an Object in the current folder
     * @param key Key of the Object
     */
    
    function confirmedDeleteObject(key: string) {
    
      objectRepository.deleteObject(props.bucketName, key).then(() => {
        const splittedKey = key.split("/");
        deleteObjectsState.deletedItem = splittedKey[splittedKey.length - 1];
        successToast?.show();
    
      });
    }
    
    /**
     * Initiate the download of the provided object
     * @param key Key of the object
     * @param bucket Bucket of the object
     */
    
    async function downloadObject(bucket: string, key?: string) {
      if (key == undefined) {
        return;
      }
      const url = await objectRepository.getPresignedUrl(bucket, key);
    
      //creating an invisible element
      const element = document.createElement("a");
      element.setAttribute("href", url);
      element.setAttribute("target", "_blank");
      document.body.appendChild(element);
      element.click();
      document.body.removeChild(element);
    }
    
    
    function deleteFolder(folderPath: string) {
    
      deleteObjectsState.potentialObjectToDelete = folderPath;
      deleteObjectsState.deleteFolder = true;
    
    /**
     * Delete a folder in the current Bucket
     * @param folderPath Path to the folder with a trailing "/", e.g. some/path/to/a/folder/
     */
    
    function confirmedDeleteFolder(folderPath: string) {
    
      objectRepository
        .deleteObjectsWithPrefix(props.bucketName, folderPath)
    
        .then(() => {
          const splittedPath = folderPath.split("/");
    
          deleteObjectsState.deletedItem = splittedPath[splittedPath.length - 2];
    
          successToast?.show();
        });
    }
    
    function getObjectFileName(key: string): string {
      const splittedKey = key.split("/");
      return splittedKey[splittedKey.length - 1];
    }
    
    </script>
    
    <template>
    
    Daniel Göbel's avatar
    Daniel Göbel committed
      <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="'successToast-' + randomIDSuffix"
        >
          <div class="d-flex">
            <div class="toast-body">
    
              Successfully deleted {{ deleteObjectsState.deletedItem }}
    
            </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>
    
      <DeleteModal
        modalID="delete-object-modal"
    
        :object-name-delete="deleteObjectsState.potentialObjectToDelete"
    
        :back-modal-id="undefined"
    
        @confirm-delete="
          deleteObjectsState.deleteFolder
            ? confirmedDeleteFolder(deleteObjectsState.potentialObjectToDelete)
            : confirmedDeleteObject(deleteObjectsState.potentialObjectToDelete)
        "
    
      <!-- Navbar Breadcrumb -->
    
      <nav aria-label="breadcrumb" class="fs-2">
        <ol class="breadcrumb">
    
          <li class="breadcrumb-item" :class="{ active: subFolderInUrl }">
    
            <router-link
    
              v-if="subFolderInUrl"
    
              :to="{
                name: 'bucket',
    
                params: { bucketName: props.bucketName, subFolders: [] },
    
              >{{ props.bucketName }}
    
            <span v-else class="text-secondary">{{ props.bucketName }}</span>
    
          </li>
          <li
            class="breadcrumb-item"
    
            v-for="(folder, index) in currentSubFolders"
    
            :key="folder"
    
            :class="{ active: index === currentSubFolders.length }"
    
          >
            <router-link
    
              v-if="index !== currentSubFolders.length - 1"
    
              :to="{
                name: 'bucket',
                params: {
    
                  bucketName: props.bucketName,
                  subFolders: currentSubFolders.slice(0, index + 1),
    
              >{{ folder }}
            </router-link>
    
            <span v-else class="text-secondary">{{ folder }}</span>
    
          </li>
        </ol>
      </nav>
    
      <!-- Inputs on top -->
      <!-- Search bucket text input -->
    
      <div class="d-flex justify-content-between align-items-center">
        <div class="flex-grow-1 me-2">
          <div class="input-group rounded shadow-sm">
    
            <span class="input-group-text" id="objects-search-wrapping"
    
              ><font-awesome-icon icon="fa-solid fa-magnifying-glass"
    
            /></span>
            <input
              type="text"
              class="form-control"
    
              placeholder="Search Files"
              aria-label="Search Files"
    
              aria-describedby="objects-search-wrapping"
    
              :disabled="errorLoadingObjects"
    
              v-model.trim="objectState.filterString"
    
            />
          </div>
        </div>
        <!-- Upload object button -->
    
        <div id="BucketViewButtons" class="">
          <button
            type="button"
            class="btn btn-light me-3 tooltip-container border shadow-sm"
            :disabled="errorLoadingObjects"
            data-bs-toggle="tooltip"
            data-bs-title="Refresh Objects"
            @click="refreshObjects"
          >
            <font-awesome-icon icon="fa-solid fa-arrow-rotate-right" />
            <span class="visually-hidden">Refresh Objects</span>
          </button>
    
            class="btn btn-light me-2 tooltip-container border shadow-sm"
    
            :disabled="errorLoadingObjects || !writableBucket"
    
            data-bs-toggle="modal"
    
            data-bs-title="Upload File"
    
            data-bs-target="#upload-object-modal"
    
            <font-awesome-icon icon="fa-solid fa-upload" />
    
            <span class="visually-hidden">Upload File</span>
    
          <upload-object-modal
            :bucket-name="props.bucketName"
            modalID="upload-object-modal"
            :key-prefix="currentSubFolders.join('/')"
            :edit-object-file-name="undefined"
          />
    
          <!-- Add folder button -->
          <button
            type="button"
    
            class="btn btn-light me-3 tooltip-container border shadow-sm"
    
            :disabled="errorLoadingObjects || !writableBucket"
    
            data-bs-toggle="modal"
    
            data-bs-title="Create Folder"
    
            data-bs-target="#create-folder-modal"
    
            <font-awesome-icon icon="fa-solid fa-plus" />
    
            Folder
            <span class="visually-hidden">Add Folder</span>
          </button>
    
          <create-folder-modal
            :bucket-name="props.bucketName"
            modalID="create-folder-modal"
            :key-prefix="currentSubFolders.join('/')"
          />
    
          <!-- Add bucket permission button -->
          <button
    
            v-if="!authStore.foreignUser"
    
            :hidden="!bucketRepository.permissionFeatureAllowed(props.bucketName)"
    
            class="btn btn-light me-2 tooltip-container border shadow-sm"
    
            :disabled="errorLoadingObjects"
            data-bs-toggle="modal"
    
            data-bs-title="Create Bucket Permission"
    
            data-bs-target="#create-permission-modal"
          >
    
            <font-awesome-icon icon="fa-solid fa-user-plus" />
    
            <span class="visually-hidden">Add Bucket Permission</span>
          </button>
          <permission-modal
    
            v-if="!authStore.foreignUser"
    
            modalID="create-permission-modal"
            :bucket-name="props.bucketName"
            :sub-folders="folderStructure"
            :edit-user-permission="undefined"
    
            :editable="false"
    
            :deletable="false"
            :back-modal-id="undefined"
    
            v-if="!authStore.foreignUser"
    
            :hidden="!bucketRepository.permissionFeatureAllowed(props.bucketName)"
    
            class="btn btn-light tooltip-container border shadow-sm"
    
            :disabled="errorLoadingObjects"
    
            data-bs-title="List Bucket Permission"
            data-bs-toggle="modal"
            data-bs-target="#permission-list-modal"
    
            <font-awesome-icon icon="fa-solid fa-users-line" />
    
            <span class="visually-hidden">View Bucket Permissions</span>
    
          <permission-list-modal
    
              bucketRepository.ownPermissions[props.bucketName] == undefined &&
    
              !authStore.foreignUser
            "
    
            :bucket-name="props.bucketName"
            :sub-folders="folderStructure"
            modalID="permission-list-modal"
          />
    
      <div class="pt-3">
    
        <!-- If bucket not found -->
    
        <div v-if="objectState.bucketNotFoundError" class="text-center fs-2 mt-5">
    
          <font-awesome-icon
            icon="fa-solid fa-magnifying-glass"
    
            class="mb-3 fs-0"
    
            style="color: var(--bs-secondary)"
          />
          <p>
            Bucket <i>{{ props.bucketName }}</i> not found
          </p>
    
        <!-- If no permission for bucket -->
    
        <div
          v-else-if="objectState.bucketPermissionError"
          class="text-center fs-2 mt-5"
        >
    
          <font-awesome-icon
            icon="fa-solid fa-folder-xmark"
    
            class="mb-3 fs-0"
    
            style="color: var(--bs-secondary)"
          />
          <p>You don't have permission for this bucket</p>
    
        <!-- Show content of bucket -->
    
        <div v-else>
    
          <!-- Table header -->
    
          <table class="table table-sm table-hover caption-top align-middle">
    
            <caption>
              Displaying
              {{
    
                objectState.loading ? 0 : filteredObjects.length
    
            </caption>
            <thead>
              <tr>
                <th scope="col">Name</th>
                <th scope="col">Last Accessed</th>
                <th scope="col">Size</th>
                <th scope="col"></th>
              </tr>
            </thead>
    
            <!-- Table body when loading the objects -->
    
            <tbody v-if="objectState.loading">
              <tr v-for="n in 5" :key="n" class="placeholder-glow">
                <th scope="row">
                  <span class="placeholder w-100 bg-secondary"></span>
                </th>
                <td><span class="placeholder w-50 bg-secondary"></span></td>
                <td><span class="placeholder w-50 bg-secondary"></span></td>
                <td></td>
              </tr>
            </tbody>
    
            <!-- Table body when no objects are in the bucket -->
            <tbody v-else-if="filteredObjects.length === 0">
    
                <td colspan="4" class="text-center fst-italic fw-light">
                  No objects to display
    
                </td>
              </tr>
            </tbody>
    
            <!-- Table body when showing objects -->
    
            <tbody v-else>
    
              <tr v-for="obj in filteredObjects" :key="obj.Key">
    
                <th scope="row" class="text-truncate">
                  <!-- Show file name if row is an object -->
                  <div v-if="isS3Object(obj)">{{ obj.pseudoFileName }}</div>
                  <!-- Show link to subfolder if row is a folder -->
                  <div v-else>
                    <router-link
                      class="text-decoration-none"
                      :to="{
                        name: 'bucket',
                        params: {
    
                          subFolders: obj.parentFolder.concat(obj.name),
                        },
                      }"
                      >{{ obj.name }}
                    </router-link>
                  </div>
                </th>
    
                <td>
                  <span
                    class="date-tooltip"
                    data-bs-toggle="tooltip"
                    :data-bs-title="
    
                      dayjs(obj.LastModified).format('DD.MM.YYYY HH:mm:ss')
    
                    >{{ dayjs(obj.LastModified).fromNow() }}</span
    
                <td>
                  {{ filesize(obj.Size ?? 0, { base: 2, standard: "jedec" }) }}
                </td>
    
                <!-- Show buttons with dropdown menu if row is an object -->
    
                <td class="text-end">
                  <div
    
                    v-if="isS3Object(obj)"
    
                    class="btn-group btn-group-sm dropdown-center dropdown-menu-start"
                  >
    
                    <!-- Download Button -->
    
                    <button
                      type="button"
                      class="btn btn-secondary"
    
                      @click="downloadObject(props.bucketName, obj.Key)"
    
                      :disabled="!readableBucket"
    
                      Download
                    </button>
                    <button
                      type="button"
                      class="btn btn-secondary dropdown-toggle dropdown-toggle-split"
                      data-bs-toggle="dropdown"
                      aria-expanded="false"
                    >
                      <span class="visually-hidden">Toggle Dropdown</span>
                    </button>
    
                    <!-- Dropdown menu -->
    
                    <ul class="dropdown-menu dropdown-menu">
    
                        <button
                          class="dropdown-item"
                          type="button"
                          data-bs-toggle="modal"
                          data-bs-target="#detail-object-modal"
    
                          @click="objectState.viewDetailKey = obj.Key"
    
                        >
                          Details
                        </button>
    
                        <button
                          class="dropdown-item"
                          type="button"
    
                          data-bs-toggle="modal"
                          data-bs-target="#edit-object-modal"
    
                          @click="objectState.editObjectKey = obj.Key ?? ''"
    
                        >
                          Edit
                        </button>
    
                        <button
                          class="dropdown-item"
                          type="button"
    
                          :disabled="!readableBucket"
    
                          data-bs-toggle="modal"
                          data-bs-target="#copy-object-modal"
                          @click="objectState.copyObject = obj"
                        >
                          Copy
                        </button>
    
                        <button
                          class="dropdown-item text-danger align-middle"
                          type="button"
    
                          data-bs-toggle="modal"
                          data-bs-target="#delete-object-modal"
    
                          <font-awesome-icon icon="fa-solid fa-trash" />
    
                          <span class="ms-1">Delete</span>
                        </button>
                      </li>
                    </ul>
                  </div>
    
                  <!-- Show delete button when row is a folder -->
                  <div v-else>
                    <button
                      type="button"
    
                      class="btn btn-danger btn-sm align-baseline"
    
                      data-bs-toggle="modal"
                      data-bs-target="#delete-object-modal"
    
                      @click="
                        deleteFolder(
    
                          obj.parentFolder.concat(['']).join('/') + obj.name + '/',
    
                      <font-awesome-icon icon="fa-solid fa-trash" class="me-2" />
    
                      <span>Delete</span>
                    </button>
                  </div>
    
                </td>
              </tr>
            </tbody>
          </table>
    
          <upload-object-modal
            :bucket-name="props.bucketName"
            modalID="edit-object-modal"
            :key-prefix="currentSubFolders.join('/')"
            :edit-object-file-name="getObjectFileName(objectState.editObjectKey)"
          />
          <copy-object-modal
    
            :src-object="objectState.copyObject"
            :src-bucket="bucketName"
    
            modalID="copy-object-modal"
          />
          <object-detail-modal
    
            :object-key="objectState.viewDetailKey"
    
            modalID="detail-object-modal"
          />
    
        </div>
      </div>
    </template>
    
    <style scoped></style>