From adcfd48497c3392502a5e91da61415dbe46e5ce2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20G=C3=B6bel?= <dgoebel@techfak.uni-bielefeld.de>
Date: Thu, 11 Jan 2024 11:45:24 +0100
Subject: [PATCH] Refactor Bootstrap toast for easier usage

#88
---
 src/App.vue                                   | 10 ++-
 src/assets/main.css                           |  4 --
 src/components/BootstrapToast.vue             | 60 ++++++++++++++++
 src/components/CopyToClipboardIcon.vue        | 47 +++---------
 .../object-storage/modals/CopyObjectModal.vue | 54 +++-----------
 .../modals/CreateFolderModal.vue              | 54 +++-----------
 .../object-storage/modals/PermissionModal.vue | 38 +++-------
 .../modals/UploadObjectModal.vue              | 54 +++-----------
 .../ParameterSchemaFormComponent.vue          | 41 ++++-------
 ...ourceModal.vue => CreateResourceModal.vue} |  4 +-
 src/components/resources/ResourceCard.vue     | 71 +++++++++++++++++--
 .../workflows/modals/CreateWorkflowModal.vue  | 24 ++-----
 .../modals/UpdateWorkflowCredentialsModal.vue | 27 ++-----
 .../workflows/modals/UpdateWorkflowModal.vue  | 24 ++-----
 .../modals/UpdateWorkflowVersionIconModal.vue | 28 ++------
 src/views/LoginView.vue                       | 40 +++--------
 src/views/object-storage/BucketView.vue       | 26 ++-----
 src/views/object-storage/S3KeysView.vue       | 26 ++-----
 src/views/resources/MyResourcesView.vue       |  5 +-
 src/views/workflows/ArbitraryWorkflowView.vue | 44 +++++-------
 src/views/workflows/StartWorkflowView.vue     | 44 +++++-------
 21 files changed, 275 insertions(+), 450 deletions(-)
 create mode 100644 src/components/BootstrapToast.vue
 rename src/components/resources/{createResourceModal.vue => CreateResourceModal.vue} (98%)

diff --git a/src/App.vue b/src/App.vue
index 3fa22bb..306e0d6 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -69,9 +69,17 @@ onBeforeMount(() => {
 <template>
   <NavbarTop />
   <div class="container-xxl mt-4 flex-grow-1">
+    <div
+      id="global-toast-container"
+      class="toast-container position-fixed top-toast end-0 p-3"
+    ></div>
     <router-view></router-view>
   </div>
   <FooterBottom />
 </template>
 
-<style scoped></style>
+<style scoped>
+.top-toast {
+  top: 4rem;
+}
+</style>
diff --git a/src/assets/main.css b/src/assets/main.css
index 24f661e..5197334 100644
--- a/src/assets/main.css
+++ b/src/assets/main.css
@@ -4,10 +4,6 @@
     font-size: 3.5rem;
 }
 
-.top-toast {
-    top: 4rem;
-}
-
 .w-fit {
     width: fit-content;
 }
diff --git a/src/components/BootstrapToast.vue b/src/components/BootstrapToast.vue
new file mode 100644
index 0000000..cc2f27f
--- /dev/null
+++ b/src/components/BootstrapToast.vue
@@ -0,0 +1,60 @@
+<script setup lang="ts">
+import { useSlots, computed } from "vue";
+
+const slots = useSlots();
+const props = defineProps({
+  colorClass: { type: String, required: false, default: "success" },
+  toastId: { type: String, required: true },
+});
+
+const colorClassName = computed<string>(() => {
+  return "text-bg-" + props.colorClass;
+});
+const emit = defineEmits<{
+  (e: "hidden.bs.toast"): void;
+}>();
+</script>
+
+<template>
+  <Teleport to="#global-toast-container">
+    <div
+      role="alert"
+      aria-live="assertive"
+      aria-atomic="true"
+      class="toast align-items-center border-0"
+      :class="colorClassName"
+      data-bs-autohide="true"
+      :id="props.toastId"
+      v-on="{ 'hidden.bs.toast': () => emit('hidden.bs.toast') }"
+    >
+      <div v-if="slots.body" class="toast-header" :class="colorClassName">
+        <div class="me-auto">
+          <slot></slot>
+        </div>
+        <button
+          type="button"
+          class="btn-close btn-close-white me-2 m-auto"
+          data-bs-dismiss="toast"
+          aria-label="Close"
+        ></button>
+      </div>
+      <div v-else class="d-flex">
+        <div class="toast-body">
+          <slot></slot>
+        </div>
+        <button
+          type="button"
+          class="btn-close btn-close-white me-2 m-auto"
+          data-bs-dismiss="toast"
+          aria-label="Close"
+        ></button>
+      </div>
+
+      <div class="toast-body" v-if="slots.body">
+        <slot name="body"></slot>
+      </div>
+    </div>
+  </Teleport>
+</template>
+
+<style scoped></style>
diff --git a/src/components/CopyToClipboardIcon.vue b/src/components/CopyToClipboardIcon.vue
index 9d28cba..ca9627e 100644
--- a/src/components/CopyToClipboardIcon.vue
+++ b/src/components/CopyToClipboardIcon.vue
@@ -2,6 +2,7 @@
 import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
 import { onMounted } from "vue";
 import { Toast, Tooltip } from "bootstrap";
+import BootstrapToast from "@/components/BootstrapToast.vue";
 
 const props = defineProps<{
   text: string;
@@ -33,44 +34,14 @@ onMounted(() => {
 </script>
 
 <template>
-  <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 copied to clipboard</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
-      role="alert"
-      aria-live="assertive"
-      aria-atomic="true"
-      class="toast text-bg-danger align-items-center border-0"
-      data-bs-autohide="true"
-      :id="'failToast-' + randomIDSuffix"
-    >
-      <div class="d-flex">
-        <div class="toast-body">Can't copy to clipboard</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-toast :toast-id="'successToast-' + randomIDSuffix"
+    >Successfully copied to clipboard</bootstrap-toast
+  >
+  <bootstrap-toast
+    :toast-id="'failToast-' + randomIDSuffix"
+    color-class="danger"
+    >Can't copy to clipboard
+  </bootstrap-toast>
   <span
     class="hover-info cursor-pointer"
     data-bs-toggle="tooltip"
diff --git a/src/components/object-storage/modals/CopyObjectModal.vue b/src/components/object-storage/modals/CopyObjectModal.vue
index 0687aaa..b51e17d 100644
--- a/src/components/object-storage/modals/CopyObjectModal.vue
+++ b/src/components/object-storage/modals/CopyObjectModal.vue
@@ -5,6 +5,7 @@ import { onMounted, reactive, watch } from "vue";
 import type { _Object as S3Object } from "@aws-sdk/client-s3";
 import { useBucketStore } from "@/stores/buckets";
 import { useS3ObjectStore } from "@/stores/s3objects";
+import BootstrapToast from "@/components/BootstrapToast.vue";
 
 const objectRepository = useS3ObjectStore();
 
@@ -83,49 +84,16 @@ onMounted(() => {
 </script>
 
 <template>
-  <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 copied file</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>
-  <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-danger align-items-center border-0"
-      data-bs-autohide="true"
-      :id="'errorToast-' + randomIDSuffix"
-    >
-      <div class="d-flex">
-        <div class="toast-body">
-          There has been some Error.<br />
-          Try again later
-        </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-toast :toast-id="'successToast-' + randomIDSuffix">
+    Successfully copied file
+  </bootstrap-toast>
+  <bootstrap-toast
+    :toast-id="'errorToast-' + randomIDSuffix"
+    color-class="danger"
+  >
+    There has been some Error.<br />
+    Try again later
+  </bootstrap-toast>
   <bootstrap-modal
     :modalID="modalID"
     :static-backdrop="true"
diff --git a/src/components/object-storage/modals/CreateFolderModal.vue b/src/components/object-storage/modals/CreateFolderModal.vue
index cd26d2c..12400a9 100644
--- a/src/components/object-storage/modals/CreateFolderModal.vue
+++ b/src/components/object-storage/modals/CreateFolderModal.vue
@@ -4,6 +4,7 @@ import { computed, onMounted, reactive } from "vue";
 
 import { Modal, Toast } from "bootstrap";
 import { useS3ObjectStore } from "@/stores/s3objects";
+import BootstrapToast from "@/components/BootstrapToast.vue";
 
 const objectRepository = useS3ObjectStore();
 
@@ -67,49 +68,16 @@ onMounted(() => {
 </script>
 
 <template>
-  <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 created Folder</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>
-  <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-danger align-items-center border-0"
-      data-bs-autohide="true"
-      :id="'errorToast-' + randomIDSuffix"
-    >
-      <div class="d-flex">
-        <div class="toast-body">
-          There has been some Error.<br />
-          Try again later
-        </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-toast :toast-id="'successToast-' + randomIDSuffix">
+    Successfully created Folder
+  </bootstrap-toast>
+  <bootstrap-toast
+    :toast-id="'errorToast-' + randomIDSuffix"
+    color-class="danger"
+  >
+    There has been some Error.<br />
+    Try again later
+  </bootstrap-toast>
   <bootstrap-modal
     :modalID="modalID"
     :static-backdrop="true"
diff --git a/src/components/object-storage/modals/PermissionModal.vue b/src/components/object-storage/modals/PermissionModal.vue
index 60ad031..4ce35b4 100644
--- a/src/components/object-storage/modals/PermissionModal.vue
+++ b/src/components/object-storage/modals/PermissionModal.vue
@@ -16,6 +16,7 @@ import { Permission } from "@/client/s3proxy";
 import { Toast } from "bootstrap";
 import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
 import { useBucketStore } from "@/stores/buckets";
+import BootstrapToast from "@/components/BootstrapToast.vue";
 
 // Props
 // -----------------------------------------------------------------------------
@@ -271,33 +272,16 @@ function toTimestampChanged(target?: HTMLInputElement | null) {
     :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-toast
+    :toast-id="'toast-' + randomIDSuffix"
+    v-on="{ 'hidden.bs.toast': toastHidden }"
+  >
+    Successfully
+    <template v-if="permissionDeleted">deleted</template>
+    <template v-else-if="editPermission">edited</template>
+    <template v-else>created</template>
+    Permission
+  </bootstrap-toast>
   <bootstrap-modal
     :modalID="modalID"
     :static-backdrop="true"
diff --git a/src/components/object-storage/modals/UploadObjectModal.vue b/src/components/object-storage/modals/UploadObjectModal.vue
index adafe9a..dfa6f4d 100644
--- a/src/components/object-storage/modals/UploadObjectModal.vue
+++ b/src/components/object-storage/modals/UploadObjectModal.vue
@@ -4,6 +4,7 @@ import { computed, onMounted, reactive, ref, watch } from "vue";
 import { Modal, Toast } from "bootstrap";
 import { partial } from "filesize";
 import { useS3ObjectStore } from "@/stores/s3objects";
+import BootstrapToast from "@/components/BootstrapToast.vue";
 
 const fsize = partial({ base: 2, standard: "jedec" });
 const objectRepository = useS3ObjectStore();
@@ -101,49 +102,16 @@ onMounted(() => {
 </script>
 
 <template>
-  <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 uploaded file</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>
-  <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-danger align-items-center border-0"
-      data-bs-autohide="true"
-      :id="'errorToast-' + randomIDSuffix"
-    >
-      <div class="d-flex">
-        <div class="toast-body">
-          There has been some Error.<br />
-          Try again later
-        </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-toast :toast-id="'successToast-' + randomIDSuffix">
+    Successfully uploaded file
+  </bootstrap-toast>
+  <bootstrap-toast
+    :toast-id="'errorToast-' + randomIDSuffix"
+    color-class="danger"
+  >
+    There has been some Error.<br />
+    Try again later
+  </bootstrap-toast>
   <bootstrap-modal
     :modalID="modalID"
     :static-backdrop="true"
diff --git a/src/components/parameter-schema/ParameterSchemaFormComponent.vue b/src/components/parameter-schema/ParameterSchemaFormComponent.vue
index 7546e8b..69dd3cd 100644
--- a/src/components/parameter-schema/ParameterSchemaFormComponent.vue
+++ b/src/components/parameter-schema/ParameterSchemaFormComponent.vue
@@ -8,6 +8,7 @@ import ParameterStringInput from "@/components/parameter-schema/form-mode/Parame
 import { Toast } from "bootstrap";
 import { useBucketStore } from "@/stores/buckets";
 import { useS3KeyStore } from "@/stores/s3keys";
+import BootstrapToast from "@/components/BootstrapToast.vue";
 
 const bucketRepository = useBucketStore();
 const s3KeyRepository = useS3KeyStore();
@@ -186,34 +187,18 @@ onMounted(() => {
 </script>
 
 <template>
-  <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-danger align-items-center border-0"
-      data-bs-autohide="true"
-      id="workflowExecutionErrorToast"
-    >
-      <div class="d-flex p-2 justify-content-between align-items-center">
-        <div class="toast-body">
-          <template v-if="formState.errorType === 'form'">
-            Some inputs are not valid.
-          </template>
-          <template v-else>
-            There was an error with starting the workflow execution. Look in the
-            console for more information.
-          </template>
-        </div>
-        <button
-          type="button"
-          class="btn-close btn-close-white"
-          data-bs-dismiss="toast"
-          aria-label="Close"
-        ></button>
-      </div>
-    </div>
-  </div>
+  <bootstrap-toast toast-id="workflowExecutionErrorToast" color-class="danger">
+    <template>Error starting workflow</template>
+    <template #body>
+      <template v-if="formState.errorType === 'form'">
+        Some inputs are not valid.
+      </template>
+      <template v-else>
+        There was an error with starting the workflow execution. Look in the
+        console for more information.
+      </template>
+    </template>
+  </bootstrap-toast>
   <div class="row mb-5 align-items-start">
     <form
       v-if="props.schema"
diff --git a/src/components/resources/createResourceModal.vue b/src/components/resources/CreateResourceModal.vue
similarity index 98%
rename from src/components/resources/createResourceModal.vue
rename to src/components/resources/CreateResourceModal.vue
index 27dc54c..0f4e66d 100644
--- a/src/components/resources/createResourceModal.vue
+++ b/src/components/resources/CreateResourceModal.vue
@@ -34,7 +34,7 @@ const props = defineProps<{
 let createResourceModal: Modal | null = null;
 
 onMounted(() => {
-  createResourceModal = new Modal("#" + props.modalID);
+  CreateResourceModal = new Modal("#" + props.modalID);
 });
 
 function createResource() {
@@ -49,7 +49,7 @@ function createResource() {
     resourceRepository
       .createResource(resource)
       .then(() => {
-        createResourceModal?.hide();
+        CreateResourceModal?.hide();
         resource.name = "";
         resource.description = "";
         resource.source = "";
diff --git a/src/components/resources/ResourceCard.vue b/src/components/resources/ResourceCard.vue
index f3c9140..3aa21f2 100644
--- a/src/components/resources/ResourceCard.vue
+++ b/src/components/resources/ResourceCard.vue
@@ -7,6 +7,8 @@ import {
 import { computed } from "vue";
 import dayjs from "dayjs";
 import { useAuthStore } from "@/stores/users";
+import CopyToClipboardIcon from "@/components/CopyToClipboardIcon.vue";
+import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
 
 const randomIDSuffix: string = Math.random().toString(16).substring(2, 8);
 const userRepository = useAuthStore();
@@ -14,6 +16,7 @@ const userRepository = useAuthStore();
 const props = defineProps<{
   resource: ResourceOut;
   loading: boolean;
+  extended?: boolean;
 }>();
 
 const resourceVersions = computed<ResourceVersionOut[]>(
@@ -86,14 +89,74 @@ const resourceVersions = computed<ResourceVersionOut[]>(
               :data-bs-parent="'#accordion-' + props.resource.resource_id"
             >
               <div class="accordion-body">
-                <p>
+                <div>
                   Last Updated:
                   {{
                     dayjs.unix(resourceVersion.created_at).format("DD MMM YYYY")
                   }}
-                </p>
-                <div>
-                  Nextflow Access Path: <br />{{ resourceVersion.cluster_path }}
+                </div>
+                <div
+                  v-if="
+                    resourceVersion.status === Status.SYNCHRONIZED ||
+                    resourceVersion.status === Status.LATEST
+                  "
+                  class="my-1"
+                >
+                  <label
+                    :for="
+                      'nextflow-access-path-' +
+                      resourceVersion.resource_version_id
+                    "
+                    class="form-label"
+                    >Nextflow Access Path:</label
+                  >
+                  <div class="input-group fs-4 mb-3">
+                    <input
+                      :id="
+                        'nextflow-access-path-' +
+                        resourceVersion.resource_version_id
+                      "
+                      class="form-control"
+                      type="text"
+                      :value="resourceVersion.cluster_path"
+                      aria-label="Nextflow Access Path"
+                      readonly
+                    />
+                    <span class="input-group-text"
+                      ><copy-to-clipboard-icon
+                        :text="resourceVersion.cluster_path"
+                    /></span>
+                  </div>
+                </div>
+                <div
+                  v-if="
+                    props.extended &&
+                    resourceVersion.status !== Status.S3_DELETED
+                  "
+                  class="my-1"
+                >
+                  <label
+                    :for="
+                      's3-access-path-' + resourceVersion.resource_version_id
+                    "
+                    class="form-label"
+                    >S3 Path:</label
+                  >
+                  <div class="input-group fs-4 mb-3">
+                    <input
+                      :id="
+                        's3-access-path-' + resourceVersion.resource_version_id
+                      "
+                      class="form-control"
+                      type="text"
+                      :value="resourceVersion.s3_path"
+                      aria-label="S3 Access Path"
+                      readonly
+                    />
+                    <span class="input-group-text"
+                      ><copy-to-clipboard-icon :text="resourceVersion.s3_path"
+                    /></span>
+                  </div>
                 </div>
               </div>
             </div>
diff --git a/src/components/workflows/modals/CreateWorkflowModal.vue b/src/components/workflows/modals/CreateWorkflowModal.vue
index 86bd586..7c24bca 100644
--- a/src/components/workflows/modals/CreateWorkflowModal.vue
+++ b/src/components/workflows/modals/CreateWorkflowModal.vue
@@ -17,6 +17,7 @@ import {
 import { valid } from "semver";
 import WorkflowModeTransitionGroup from "@/components/transitions/WorkflowModeTransitionGroup.vue";
 import { useWorkflowStore } from "@/stores/workflows";
+import BootstrapToast from "@/components/BootstrapToast.vue";
 
 const workflowRepository = useWorkflowStore();
 // Emitted Events
@@ -319,26 +320,9 @@ onMounted(() => {
 </script>
 
 <template>
-  <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 created Workflow</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-toast :toast-id="'successToast-' + randomIDSuffix">
+    Successfully created Workflow
+  </bootstrap-toast>
   <bootstrap-modal
     :modalID="modalID"
     :static-backdrop="true"
diff --git a/src/components/workflows/modals/UpdateWorkflowCredentialsModal.vue b/src/components/workflows/modals/UpdateWorkflowCredentialsModal.vue
index 43f057f..130f275 100644
--- a/src/components/workflows/modals/UpdateWorkflowCredentialsModal.vue
+++ b/src/components/workflows/modals/UpdateWorkflowCredentialsModal.vue
@@ -7,6 +7,7 @@ import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
 import DeleteModal from "@/components/modals/DeleteModal.vue";
 import { GitRepository } from "@/utils/GitRepository";
 import { useWorkflowStore } from "@/stores/workflows";
+import BootstrapToast from "@/components/BootstrapToast.vue";
 
 const workflowRepository = useWorkflowStore();
 // Constants
@@ -131,29 +132,9 @@ onMounted(() => {
     :back-modal-id="modalID"
     @confirm-delete="deleteCredentials()"
   />
-  <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 v-if="formState.updateCredentials" class="toast-body">
-          Successfully updated credentials
-        </div>
-        <div v-else class="toast-body">Successfully deleted credentials</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-toast :toast-id="'successToast-' + randomIDSuffix">
+    Successfully updated credentials
+  </bootstrap-toast>
   <bootstrap-modal
     :modalID="modalID"
     :static-backdrop="true"
diff --git a/src/components/workflows/modals/UpdateWorkflowModal.vue b/src/components/workflows/modals/UpdateWorkflowModal.vue
index 1b39368..78ca17f 100644
--- a/src/components/workflows/modals/UpdateWorkflowModal.vue
+++ b/src/components/workflows/modals/UpdateWorkflowModal.vue
@@ -21,6 +21,7 @@ import { valid, lte, inc } from "semver";
 import { latestVersion as calculateLatestVersion } from "@/utils/Workflow";
 import WorkflowModeTransitionGroup from "@/components/transitions/WorkflowModeTransitionGroup.vue";
 import { useWorkflowStore } from "@/stores/workflows";
+import BootstrapToast from "@/components/BootstrapToast.vue";
 
 const workflowRepository = useWorkflowStore();
 // Bootstrap Elements
@@ -307,26 +308,9 @@ onMounted(() => {
 </script>
 
 <template>
-  <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 updated Workflow</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-toast :toast-id="'successToast-' + randomIDSuffix">
+    Successfully updated Workflow
+  </bootstrap-toast>
   <bootstrap-modal
     :modalID="modalID"
     :static-backdrop="true"
diff --git a/src/components/workflows/modals/UpdateWorkflowVersionIconModal.vue b/src/components/workflows/modals/UpdateWorkflowVersionIconModal.vue
index ef52929..420dfc3 100644
--- a/src/components/workflows/modals/UpdateWorkflowVersionIconModal.vue
+++ b/src/components/workflows/modals/UpdateWorkflowVersionIconModal.vue
@@ -6,6 +6,7 @@ import { Modal, Toast } from "bootstrap";
 import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
 import DeleteModal from "@/components/modals/DeleteModal.vue";
 import { useWorkflowStore } from "@/stores/workflows";
+import BootstrapToast from "@/components/BootstrapToast.vue";
 
 const workflowRepository = useWorkflowStore();
 // Constants
@@ -140,29 +141,10 @@ onMounted(() => {
     :back-modal-id="modalID"
     @confirm-delete="deleteIcon()"
   />
-  <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 v-if="formState.uploadIcon" class="toast-body">
-          Successfully uploaded icon
-        </div>
-        <div v-else class="toast-body">Successfully deleted icon</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-toast :toast-id="'successToast-' + randomIDSuffix">
+    <div v-if="formState.uploadIcon">Successfully uploaded icon</div>
+    <div v-else>Successfully deleted icon</div>
+  </bootstrap-toast>
   <bootstrap-modal
     :modalID="modalID"
     :static-backdrop="true"
diff --git a/src/views/LoginView.vue b/src/views/LoginView.vue
index d9e1987..ef5a66c 100644
--- a/src/views/LoginView.vue
+++ b/src/views/LoginView.vue
@@ -4,6 +4,7 @@ import { useAuthStore } from "@/stores/users";
 import { useRouter, useRoute } from "vue-router";
 import { OpenAPI as AuthOpenAPI } from "@/client/auth";
 import { Toast } from "bootstrap";
+import BootstrapToast from "@/components/BootstrapToast.vue";
 
 const router = useRouter();
 const route = useRoute();
@@ -29,35 +30,16 @@ onMounted(() => {
 </script>
 
 <template>
-  <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-danger align-items-center border-0"
-      style="--bs-bg-opacity: 0.7"
-      data-bs-config="{'delay': 8000}"
-      data-bs-autohide="true"
-      id="loginErrorToast"
-    >
-      <div class="toast-header text-bg-danger">
-        <strong class="me-auto">Login Error</strong>
-        <button
-          type="button"
-          class="btn-close btn-close-white"
-          data-bs-dismiss="toast"
-          aria-label="Close"
-        ></button>
-      </div>
-      <div class="toast-body">
-        <p>
-          There has been some kind of error during the login.<br />
-          Please try again later.
-        </p>
-        <p>Error Code: {{ route.query.login_error }}</p>
-      </div>
-    </div>
-  </div>
+  <bootstrap-toast toast-id="loginErrorToast" color-class="danger">
+    <template> Login Error </template>
+    <template #body>
+      <p>
+        There has been some kind of error during the login.<br />
+        Please try again later.
+      </p>
+      <p>Error Code: {{ route.query.login_error }}</p>
+    </template>
+  </bootstrap-toast>
   <div class="position-fixed start-50 translate-middle-x text-center">
     <img
       src="/src/assets/images/clowm.svg"
diff --git a/src/views/object-storage/BucketView.vue b/src/views/object-storage/BucketView.vue
index b3bd376..9f43cbc 100644
--- a/src/views/object-storage/BucketView.vue
+++ b/src/views/object-storage/BucketView.vue
@@ -21,6 +21,7 @@ import { useAuthStore } from "@/stores/users";
 import { useBucketStore } from "@/stores/buckets";
 import { useS3ObjectStore } from "@/stores/s3objects";
 import { useS3KeyStore } from "@/stores/s3keys";
+import BootstrapToast from "@/components/BootstrapToast.vue";
 
 const authStore = useAuthStore();
 const bucketRepository = useBucketStore();
@@ -420,28 +421,9 @@ function getObjectFileName(key: string): string {
 </script>
 
 <template>
-  <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>
+  <bootstrap-toast :toast-id="'successToast-' + randomIDSuffix">
+    Successfully deleted {{ deleteObjectsState.deletedItem }}
+  </bootstrap-toast>
   <DeleteModal
     modalID="delete-object-modal"
     :object-name-delete="deleteObjectsState.potentialObjectToDelete"
diff --git a/src/views/object-storage/S3KeysView.vue b/src/views/object-storage/S3KeysView.vue
index 1221806..1735cdb 100644
--- a/src/views/object-storage/S3KeysView.vue
+++ b/src/views/object-storage/S3KeysView.vue
@@ -5,6 +5,7 @@ import { reactive, onMounted, computed } from "vue";
 import { useAuthStore } from "@/stores/users";
 import { Toast, Tooltip } from "bootstrap";
 import { useS3KeyStore } from "@/stores/s3keys";
+import BootstrapToast from "@/components/BootstrapToast.vue";
 
 const authStore = useAuthStore();
 const keyRepository = useS3KeyStore();
@@ -76,28 +77,9 @@ onMounted(() => {
 </script>
 
 <template>
-  <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="'successKeyToast'"
-    >
-      <div class="d-flex">
-        <div class="toast-body">
-          Successfully deleted S3 Key {{ keyState.deletedKey.slice(0, 5) }}...
-        </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-toast toast-id="successKeyToast">
+    Successfully deleted S3 Key {{ keyState.deletedKey.slice(0, 5) }}...
+  </bootstrap-toast>
   <div class="row m-2 border-bottom mt-4">
     <div class="col-12"></div>
     <h2 class="mb-2">S3 Keys</h2>
diff --git a/src/views/resources/MyResourcesView.vue b/src/views/resources/MyResourcesView.vue
index 3b100fc..bb6bccc 100644
--- a/src/views/resources/MyResourcesView.vue
+++ b/src/views/resources/MyResourcesView.vue
@@ -3,7 +3,7 @@ import { onMounted, reactive } from "vue";
 import { useResourceStore } from "@/stores/resources";
 import CardTransitionGroup from "@/components/transitions/CardTransitionGroup.vue";
 import ResourceCard from "@/components/resources/ResourceCard.vue";
-import CreateResourceModal from "@/components/resources/createResourceModal.vue";
+import CreateResourceModal from "@/components/resources/CreateResourceModal.vue";
 
 const resourceRepository = useResourceStore();
 
@@ -42,7 +42,8 @@ onMounted(() => {
       :key="resource.resource_id"
       :resource="resource"
       :loading="false"
-      style="min-width: 48%"
+      style="width: 48%"
+      extended
     />
   </CardTransitionGroup>
 </template>
diff --git a/src/views/workflows/ArbitraryWorkflowView.vue b/src/views/workflows/ArbitraryWorkflowView.vue
index faedb73..e31a338 100644
--- a/src/views/workflows/ArbitraryWorkflowView.vue
+++ b/src/views/workflows/ArbitraryWorkflowView.vue
@@ -10,6 +10,7 @@ import { useWorkflowStore } from "@/stores/workflows";
 import type { WorkflowIn } from "@/client/workflow";
 import { useWorkflowExecutionStore } from "@/stores/workflowExecutions";
 import ParameterSchemaFormComponent from "@/components/parameter-schema/ParameterSchemaFormComponent.vue";
+import BootstrapToast from "@/components/BootstrapToast.vue";
 
 const props = defineProps<{
   wid: string;
@@ -148,34 +149,21 @@ onMounted(() => {
 </script>
 
 <template>
-  <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-danger align-items-center border-0"
-      data-bs-autohide="true"
-      id="arbitraryWorkflowExecutionViewErrorToast"
-    >
-      <div class="d-flex p-2 justify-content-between align-items-center">
-        <div class="toast-body">
-          <template v-if="workflowExecutionState.errorType === 'limit'">
-            You have too many active workflow executions to start a new one.
-          </template>
-          <template v-else>
-            There was an error with starting the workflow execution. Look in the
-            console for more information.
-          </template>
-        </div>
-        <button
-          type="button"
-          class="btn-close btn-close-white"
-          data-bs-dismiss="toast"
-          aria-label="Close"
-        ></button>
-      </div>
-    </div>
-  </div>
+  <bootstrap-toast
+    toast-id="arbitraryWorkflowExecutionViewErrorToast"
+    color-class="danger"
+  >
+    <template> Error starting workflow </template>
+    <template #body>
+      <template v-if="workflowExecutionState.errorType === 'limit'">
+        You have too many active workflow executions to start a new one.
+      </template>
+      <template v-else>
+        There was an error with starting the workflow execution. Look in the
+        console for more information.
+      </template>
+    </template>
+  </bootstrap-toast>
   <template v-if="workflowState.workflow">
     <div class="row m-1 border-bottom mb-4">
       <h1 class="mb-2">Arbitrary Workflow</h1>
diff --git a/src/views/workflows/StartWorkflowView.vue b/src/views/workflows/StartWorkflowView.vue
index a8d84cb..d215cda 100644
--- a/src/views/workflows/StartWorkflowView.vue
+++ b/src/views/workflows/StartWorkflowView.vue
@@ -10,6 +10,7 @@ import { onMounted, ref, reactive, watch } from "vue";
 import { useRouter } from "vue-router";
 import { Toast } from "bootstrap";
 import { useWorkflowExecutionStore } from "@/stores/workflowExecutions";
+import BootstrapToast from "@/components/BootstrapToast.vue";
 
 const executionRepository = useWorkflowExecutionStore();
 const props = defineProps<{
@@ -108,34 +109,21 @@ onMounted(() => {
 </script>
 
 <template>
-  <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-danger align-items-center border-0"
-      data-bs-autohide="true"
-      id="workflowExecutionViewErrorToast"
-    >
-      <div class="d-flex p-2 justify-content-between align-items-center">
-        <div class="toast-body">
-          <template v-if="versionState.workflowExecutionError === 'limit'">
-            You have too many active workflow executions to start a new one.
-          </template>
-          <template v-else>
-            There was an error with starting the workflow execution. Look in the
-            console for more information.
-          </template>
-        </div>
-        <button
-          type="button"
-          class="btn-close btn-close-white"
-          data-bs-dismiss="toast"
-          aria-label="Close"
-        ></button>
-      </div>
-    </div>
-  </div>
+  <bootstrap-toast
+    toast-id="workflowExecutionViewErrorToast"
+    color-class="danger"
+  >
+    <template>Error starting workflow</template>
+    <template #body>
+      <template v-if="versionState.workflowExecutionError === 'limit'">
+        You have too many active workflow executions to start a new one.
+      </template>
+      <template v-else>
+        There was an error with starting the workflow execution. Look in the
+        console for more information.
+      </template>
+    </template>
+  </bootstrap-toast>
   <parameter-schema-form-component
     :workflow-version-id="versionId"
     :schema="parameterSchema"
-- 
GitLab