Skip to content
Snippets Groups Projects
BucketView.vue 29.3 KiB
Newer Older
  • Learn to ignore specific revisions
  • <script setup lang="ts">
    import { onMounted, reactive, watch, computed } from "vue";
    
    import type { ComputedRef } from "vue";
    
    import type {
      S3ObjectMetaInformation,
      BucketPermissionOut,
    } from "@/client/s3proxy";
    
    import type {
      FolderTree,
      S3PseudoFolder,
      S3ObjectWithFolder,
    } from "@/types/PseudoFolder";
    
    import { ObjectService } from "@/client/s3proxy";
    
    import BootstrapIcon from "@/components/BootstrapIcon.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 {
      S3Client,
      DeleteObjectCommand,
      DeleteObjectsCommand,
      GetObjectCommand,
    } from "@aws-sdk/client-s3";
    import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
    import { awsAuthMiddlewareOptions } from "@aws-sdk/middleware-signing";
    import { useAuthStore } from "@/stores/auth";
    
    import { useBucketStore } from "@/stores/buckets";
    
    import { environment } from "@/environment";
    
    
    const authStore = useAuthStore();
    
    const bucketRepository = useBucketStore();
    
    
    const middleware = [
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      (next) => async (args) => {
    
        args.request.headers["host"] = environment.S3_URL.split("://")[1];
    
        return await next(args);
      },
      {
        relation: "before",
        toMiddleware: awsAuthMiddlewareOptions?.name ?? "impossible",
      },
    ];
    
    let client = new S3Client({
      region: "us-east-1",
    
      forcePathStyle: true,
      credentials: {
        accessKeyId: authStore.s3key?.access_key ?? "",
        secretAccessKey: authStore.s3key?.secret_key ?? "",
      },
    });
    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore
    client.middlewareStack.addRelativeTo(middleware[0], middleware[1]);
    
    // If S3 Key changes
    authStore.$onAction(({ name, args }) => {
      if (name === "setS3Key") {
        if (args[0] === null) {
          console.error("There are no S3 Keys");
        } else {
          client = new S3Client({
            region: "us-east-1",
    
            forcePathStyle: true,
            credentials: {
              accessKeyId: args[0].access_key,
              secretAccessKey: args[0].secret_key,
            },
          });
          // eslint-disable-next-line @typescript-eslint/ban-ts-comment
          // @ts-ignore
          client.middlewareStack.addRelativeTo(middleware[0], middleware[1]);
        }
      }
    });
    
    // Constants
    // -----------------------------------------------------------------------------
    
    
    const props = defineProps<{
      bucketName: string;
      subFolders: string[] | string;
    
      permission: BucketPermissionOut | undefined;
    
    const randomIDSuffix = Math.random().toString(16).substr(2, 8);
    let successToast: Toast | null = null;
    
    // Reactive State
    // -----------------------------------------------------------------------------
    
    
    const deleteObjectsState = reactive({
      deletedItem: "",
      potentialObjectToDelete: "",
      deleteFolder: true,
    } as {
      deletedItem: string;
      potentialObjectToDelete: string;
      deleteFolder: boolean;
    });
    
    
    const objectState = reactive({
      objects: [],
      loading: true,
    
      filterString: "",
    
      bucketNotFoundError: false,
      bucketPermissionError: false,
    
      createdPermission: undefined,
    
      editObjectKey: "",
      copyObject: {
        key: "",
        size: 0,
        bucket: "",
        last_modified: "2022-01-01",
    
        content_type: "text/plain",
    
      },
      viewDetailObject: {
        key: "",
        size: 0,
        bucket: "",
        last_modified: "2022-01-01",
    
        content_type: "text/plain",
    
    } as {
      objects: S3ObjectMetaInformation[];
      loading: boolean;
    
      filterString: string;
    
      bucketNotFoundError: boolean;
      bucketPermissionError: boolean;
    
      createdPermission: undefined | BucketPermissionOut;
    
      editObjectKey: string;
      copyObject: S3ObjectMetaInformation;
      viewDetailObject: S3ObjectMetaInformation;
    
    // Watcher
    // -----------------------------------------------------------------------------
    
      () => props.bucketName,
      (newBucketName, oldBucketName) => {
        if (oldBucketName !== newBucketName) {
    
          // If bucket is changed, update the objects
    
          objectState.filterString = "";
    
    // Computed Properties
    // -----------------------------------------------------------------------------
    
    const filteredObjects: ComputedRef<(S3ObjectWithFolder | S3PseudoFolder)[]> =
      computed(() => {
        return objectState.filterString.length > 0
          ? visibleObjects.value.filter((obj) =>
              obj.key.includes(objectState.filterString)
            )
          : visibleObjects.value;
      });
    
    
    const folderStructure: ComputedRef<FolderTree> = computed(() => {
      /**
       * 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: [],
        } as FolderTree
      );
    });
    
    const objectsWithFolders: ComputedRef<S3ObjectWithFolder[]> = computed(() => {
      /**
       * 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 objectState.objects.map((obj) => {
        const splittedKey = obj.key.split("/");
        return {
          ...obj,
          pseudoFileName: splittedKey[splittedKey.length - 1],
          folder: splittedKey.slice(0, splittedKey.length - 1),
        };
      });
    });
    
    
    const currentSubFolders: ComputedRef<string[]> = computed(() => {
      /**
       * 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: ComputedRef<(S3ObjectWithFolder | S3PseudoFolder)[]> =
      computed(() => {
        /**
         * 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])
            ).toISOString();
            return {
              name: subFolderName,
              size: folderSize,
              key: subFolderName,
              parentFolder: currentSubFolders.value,
              last_modified: folderLastModified,
            } as S3PseudoFolder;
          })
        );
    
        return arr.filter((obj) => !obj.key.endsWith(".s3keep"));
    
    const subFolderInUrl: ComputedRef<boolean> = computed(
    
      () => currentSubFolders.value.length > 0
    
    );
    const errorLoadingObjects: ComputedRef<boolean> = computed(
      () => objectState.bucketPermissionError || objectState.bucketNotFoundError
    );
    
    const writableBucket: ComputedRef<boolean> = computed(() =>
      bucketRepository.writableBucket(props.bucketName)
    
    const readableBucket: ComputedRef<boolean> = computed(() =>
      bucketRepository.readableBucket(props.bucketName)
    );
    
    // Lifecycle Hooks
    // -----------------------------------------------------------------------------
    
    onMounted(() => {
    
      updateObjects(props.bucketName);
    
      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);
      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
        .map((f) => dayjs(f.last_modified))
        .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();
    }
    
    /**
     * Load the meta information about objects from a bucket
     * @param bucketName Name of a bucket
     */
    function updateObjects(bucketName: string) {
      objectState.bucketNotFoundError = false;
      objectState.bucketPermissionError = false;
    
      objectState.loading = true;
    
      ObjectService.objectGetBucketObjects(bucketName)
    
        .then((objs) => {
          objectState.objects = objs;
        })
        .catch((error) => {
          if (error.status === 404) {
    
            objectState.bucketNotFoundError = true;
    
          } else if (error.status == 403) {
    
            objectState.bucketPermissionError = true;
    
          }
        })
        .finally(() => {
          objectState.loading = false;
        });
    }
    
    
    function isS3Object(
      obj: S3PseudoFolder | S3ObjectWithFolder
    ): obj is S3ObjectWithFolder {
      return (obj as S3ObjectWithFolder).folder !== undefined;
    }
    
    /**
     * callback function when an object has been uploaded
     * @param newObject Uploaded object
     */
    function objectUploaded(newObject: S3ObjectMetaInformation) {
    
      bucketRepository.fetchBucket(newObject.bucket);
      const index = objectState.objects.findIndex(
        (obj) => obj.key === newObject.key
      );
    
      if (index > -1) {
        objectState.objects[index] = newObject;
      } else {
        objectState.objects.push(newObject);
      }
    }
    
    /**
     * callback function when an object has been copied
     * @param copiedObject Uploaded object
     */
    function objectCopied(copiedObject: S3ObjectMetaInformation) {
    
      bucketRepository.fetchBucket(copiedObject.bucket);
    
      if (copiedObject.bucket === props.bucketName) {
        objectState.objects.push(copiedObject);
      }
    }
    
    
    function deleteObject(key: string) {
    
      deleteObjectsState.potentialObjectToDelete = key;
      deleteObjectsState.deleteFolder = false;
    
    /**
     * Delete an Object in the current folder
     * @param key Key of the Object
     */
    
    function confirmedDeleteObject(key: string) {
    
      const command = new DeleteObjectCommand({
        Bucket: props.bucketName,
        Key: key,
      });
      client
        .send(command)
        .then(() => {
    
          bucketRepository.fetchBucket(props.bucketName);
    
          const splittedKey = key.split("/");
    
          deleteObjectsState.deletedItem = splittedKey[splittedKey.length - 1];
    
          successToast?.show();
          objectState.objects = objectState.objects.filter(
            (obj) => obj.key !== key
          );
        })
        .catch((err) => {
          console.error(err);
        });
    }
    
    /**
     * Initiate the download of the provided object
     * @param key Key of the object
     * @param bucket Bucket of the object
     */
    async function downloadObject(key: string, bucket: string) {
      const command = new GetObjectCommand({
        Bucket: bucket,
        Key: key,
      });
      const url = await getSignedUrl(client, command, { expiresIn: 30 });
      //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) {
    
      const command = new DeleteObjectsCommand({
        Bucket: props.bucketName,
        Delete: {
          Objects: objectState.objects
            .filter((obj) => obj.key.startsWith(folderPath))
            .map((obj) => {
              return { Key: obj.key };
            }),
        },
      });
      client
        .send(command)
        .then(() => {
    
          bucketRepository.fetchBucket(props.bucketName);
    
          const splittedPath = folderPath.split("/");
    
          deleteObjectsState.deletedItem = splittedPath[splittedPath.length - 2];
    
          successToast?.show();
          objectState.objects = objectState.objects.filter(
            (obj) => !obj.key.startsWith(folderPath)
          );
        })
        .catch((err) => {
          console.error(err);
        });
    }
    
    function getObjectFileName(key: string): string {
      const splittedKey = key.split("/");
      return splittedKey[splittedKey.length - 1];
    }
    
    
    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" }
    );
    
    </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: [] },
    
            <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="row">
        <div class="col-8">
          <div class="input-group mt-2">
            <span class="input-group-text" id="objects-search-wrapping"
              ><bootstrap-icon icon="search" :width="16" :height="16"
            /></span>
            <input
              type="text"
              class="form-control"
              placeholder="Search Objects"
              aria-label="Search Objects"
              aria-describedby="objects-search-wrapping"
    
              :disabled="errorLoadingObjects"
    
              v-model.trim="objectState.filterString"
    
            />
          </div>
        </div>
        <!-- Upload object button -->
    
        <div id="BucketViewButtons" class="col-auto">
    
            class="btn btn-secondary me-2 tooltip-container"
    
            :disabled="errorLoadingObjects || !writableBucket"
    
            data-bs-toggle="modal"
    
            data-bs-title="Upload Object"
    
            data-bs-target="#upload-object-modal"
    
          >
            <bootstrap-icon icon="upload" :width="16" :height="16" fill="white" />
            <span class="visually-hidden">Upload Object</span>
          </button>
    
          <upload-object-modal
            :bucket-name="props.bucketName"
            :s3-client="client"
            modalID="upload-object-modal"
            :key-prefix="currentSubFolders.join('/')"
            :edit-object-file-name="undefined"
            @object-created="objectUploaded"
          />
    
          <!-- Add folder button -->
          <button
            type="button"
            class="btn btn-secondary m-2 tooltip-container"
    
            :disabled="errorLoadingObjects || !writableBucket"
    
            data-bs-toggle="modal"
    
            data-bs-title="Create Folder"
    
            data-bs-target="#create-folder-modal"
    
          >
            <bootstrap-icon icon="plus-lg" :width="16" :height="16" fill="white" />
            Folder
            <span class="visually-hidden">Add Folder</span>
          </button>
    
          <create-folder-modal
            :bucket-name="props.bucketName"
            :s3-client="client"
            modalID="create-folder-modal"
            :key-prefix="currentSubFolders.join('/')"
            @folder-created="objectUploaded"
          />
    
          <!-- Add bucket permission button -->
          <button
    
            v-if="!authStore.foreignUser"
    
            :hidden="!bucketRepository.permissionFeatureAllowed(props.bucketName)"
    
            class="btn btn-secondary m-2 tooltip-container"
    
            :disabled="errorLoadingObjects"
            data-bs-toggle="modal"
    
            data-bs-title="Create Bucket Permission"
    
            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
    
            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"
            @permission-created="
              (newPermission) => (objectState.createdPermission = newPermission)
            "
    
            v-if="!authStore.foreignUser"
    
            :hidden="!bucketRepository.permissionFeatureAllowed(props.bucketName)"
    
            class="btn btn-secondary m-2 tooltip-container"
    
            :disabled="errorLoadingObjects"
    
            data-bs-title="List Bucket Permission"
            data-bs-toggle="modal"
            data-bs-target="#permission-list-modal"
    
            <bootstrap-icon
              icon="person-lines-fill"
              :width="16"
              :height="16"
              fill="white"
            />
            <span class="visually-hidden">View Bucket Permissions</span>
    
          <permission-list-modal
    
            v-if="
              bucketRepository.getBucketPermission(props.bucketName) == null &&
              !authStore.foreignUser
            "
    
            :bucket-name="props.bucketName"
            :sub-folders="folderStructure"
            modalID="permission-list-modal"
            :add-permission="objectState.createdPermission"
          />
    
      <div class="pt-3">
    
        <!-- If bucket not found -->
    
        <div v-if="objectState.bucketNotFoundError" class="text-center fs-2 mt-5">
          <bootstrap-icon
            icon="search"
            class="mb-3"
            :width="64"
            :height="64"
            style="color: var(--bs-secondary)"
            fill="currentColor"
          />
          <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"
        >
          <bootstrap-icon
            icon="folder-x"
            class="mb-3"
            :width="64"
            :height="64"
            style="color: var(--bs-secondary)"
            fill="currentColor"
          />
          <p>You don't have permission for this bucket</p>
    
        <!-- Show content of bucket -->
    
        <div v-else>
    
          <!-- Table header -->
    
          <table
            class="table table-dark table-striped table-hover caption-top align-middle"
          >
            <caption>
              Displaying
              {{
    
                objectState.loading ? 0 : filteredObjects.length
    
              }}
              Objects
            </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.last_modified).format('DD.MM.YYYY HH:mm:ss')
                    "
                    >{{ dayjs(obj.last_modified).fromNow() }}</span
                  >
                </td>
    
    Daniel Göbel's avatar
    Daniel Göbel committed
                <td>{{ filesize(obj.size) }}</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(obj.key, props.bucketName)"
    
                      :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-dark">
                      <li>
    
                        <button
                          class="dropdown-item"
                          type="button"
                          data-bs-toggle="modal"
                          data-bs-target="#detail-object-modal"
                          @click="objectState.viewDetailObject = obj"
                        >
                          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"
    
                          @click="deleteObject(obj.key)"
    
                          data-bs-toggle="modal"
                          data-bs-target="#delete-object-modal"
    
                          <bootstrap-icon
                            icon="trash-fill"
                            class="text-danger"
                            :width="13"
                            :height="13"
                            fill="currentColor"
                          />
                          <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-middle"
    
                      data-bs-toggle="modal"
                      data-bs-target="#delete-object-modal"
    
                      @click="
                        deleteFolder(
                          obj.parentFolder.concat(['']).join('/') + obj.name + '/'
                        )
                      "
    
                    >
                      <bootstrap-icon
                        icon="trash-fill"
                        class="text-danger me-2"
                        :width="12"
                        :height="12"
                        fill="white"
                      />
                      <span>Delete</span>
                    </button>
                  </div>
    
                </td>
              </tr>
            </tbody>
          </table>
    
          <upload-object-modal
            :bucket-name="props.bucketName"
            :s3-client="client"
            modalID="edit-object-modal"
            :key-prefix="currentSubFolders.join('/')"
            :edit-object-file-name="getObjectFileName(objectState.editObjectKey)"
            @object-created="objectUploaded"
          />
          <copy-object-modal
            :source-object="objectState.copyObject"
            :s3-client="client"
            modalID="copy-object-modal"
            @object-copied="objectCopied"
          />
          <object-detail-modal
            :s3-object="objectState.viewDetailObject"
            modalID="detail-object-modal"
          />
    
        </div>
      </div>
    </template>
    
    <style scoped></style>