From cab64046a9efe0f8f6464f31d844e6759308b240 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20G=C3=B6bel?= <dgoebel@techfak.uni-bielefeld.de>
Date: Fri, 3 Mar 2023 17:24:08 +0100
Subject: [PATCH] Add basic list workflow page with cards

#36
/spent 3h 25m 2023-03-03
---
 src/App.vue                                   |   2 +-
 src/components/BootstrapIcon.vue              |   6 +-
 src/components/NavbarTop.vue                  |  28 ++---
 src/components/modals/SearchUserModal.vue     |  12 +-
 .../object-storage/BucketListItem.vue         |   6 -
 src/components/object-storage/BucketView.vue  |  48 ++------
 src/components/object-storage/S3KeyView.vue   |   4 +-
 .../object-storage/modals/PermissionModal.vue |  20 +---
 src/components/workflows/WorkflowCard.vue     | 104 ++++++++++++++++++
 src/router/index.ts                           |   5 +
 src/stores/workflows.ts                       |  47 ++++++++
 src/views/object-storage/BucketsView.vue      |  21 ++--
 src/views/object-storage/S3KeysView.vue       |   4 +-
 src/views/workflows/ListWorkflowsView.vue     |  60 ++++++++++
 14 files changed, 265 insertions(+), 102 deletions(-)
 create mode 100644 src/components/workflows/WorkflowCard.vue
 create mode 100644 src/stores/workflows.ts
 create mode 100644 src/views/workflows/ListWorkflowsView.vue

diff --git a/src/App.vue b/src/App.vue
index d6b5c68..405ef89 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -34,7 +34,7 @@ onBeforeMount(() => {
 
 <template>
   <NavbarTop />
-  <div class="container">
+  <div class="container mt-4">
     <router-view></router-view>
   </div>
 </template>
diff --git a/src/components/BootstrapIcon.vue b/src/components/BootstrapIcon.vue
index 2722a66..7c57717 100644
--- a/src/components/BootstrapIcon.vue
+++ b/src/components/BootstrapIcon.vue
@@ -14,9 +14,9 @@ import iconPath from "bootstrap-icons/bootstrap-icons.svg";
 
 const props = defineProps({
   icon: { type: String, required: true },
-  width: { type: Number, default: 24, required: false },
-  height: { type: Number, default: 24, required: false },
-  fill: { type: String, default: "black", required: false },
+  width: { type: String, default: "1em", required: false },
+  height: { type: String, default: "1em", required: false },
+  fill: { type: String, default: "currentColor", required: false },
 });
 </script>
 
diff --git a/src/components/NavbarTop.vue b/src/components/NavbarTop.vue
index 81a6b2e..949d38e 100644
--- a/src/components/NavbarTop.vue
+++ b/src/components/NavbarTop.vue
@@ -21,6 +21,9 @@ const activeRoute = ref("");
 const objectStorageActive: ComputedRef<boolean> = computed(
   () => activeRoute.value == "buckets" || activeRoute.value == "s3_keys"
 );
+const workflowActive: ComputedRef<boolean> = computed(
+  () => activeRoute.value == "workflows"
+);
 
 watch(
   () => route.name,
@@ -28,10 +31,8 @@ watch(
     if (typeof to === "string") {
       if (to.startsWith("bucket")) {
         activeRoute.value = "buckets";
-      } else if (to.startsWith("s3_keys")) {
-        activeRoute.value = "s3_keys";
       } else {
-        activeRoute.value = "";
+        activeRoute.value = to;
       }
     } else {
       activeRoute.value = "";
@@ -41,7 +42,9 @@ watch(
 </script>
 
 <template>
-  <header class="navbar navbar-expand-lg navbar-dark bd-navbar sticky-top">
+  <header
+    class="navbar navbar-expand-lg navbar-dark bd-navbar bg-dark sticky-top border-bottom border-secondary"
+  >
     <nav class="container-xxl bd-gutter flex-wrap flex-lg-nowrap text-light">
       <button
         class="navbar-toggler"
@@ -85,18 +88,12 @@ watch(
             </a>
             <ul class="dropdown-menu dropdown-menu-dark">
               <li>
-                <router-link
-                  class="dropdown-item"
-                  :to="{ name: 'buckets' }"
-                  :class="{ active: activeRoute === 'buckets' }"
+                <router-link class="dropdown-item" :to="{ name: 'buckets' }"
                   >Buckets</router-link
                 >
               </li>
               <li>
-                <router-link
-                  class="dropdown-item"
-                  :to="{ name: 's3_keys' }"
-                  :class="{ active: activeRoute === 's3_keys' }"
+                <router-link class="dropdown-item" :to="{ name: 's3_keys' }"
                   >S3 Keys</router-link
                 >
               </li>
@@ -107,6 +104,7 @@ watch(
           <li class="nav-item dropdown">
             <a
               class="nav-link dropdown-toggle"
+              :class="{ active: workflowActive }"
               href="#"
               role="button"
               data-bs-toggle="dropdown"
@@ -116,7 +114,9 @@ watch(
             </a>
             <ul class="dropdown-menu dropdown-menu-dark">
               <li>
-                <a class="dropdown-item" href="#">Workflows</a>
+                <router-link class="dropdown-item" :to="{ name: 'workflows' }"
+                  >Workflows</router-link
+                >
               </li>
               <li>
                 <a class="dropdown-item" href="#">Executions</a>
@@ -134,7 +134,7 @@ watch(
           aria-expanded="false"
         >
           <strong class="me-2">{{ store.user.display_name }}</strong>
-          <bootstrap-icon icon="person-circle" fill="white" />
+          <bootstrap-icon icon="person-circle" class="fs-4" />
         </a>
         <ul
           class="dropdown-menu dropdown-menu-dark text-small shadow"
diff --git a/src/components/modals/SearchUserModal.vue b/src/components/modals/SearchUserModal.vue
index e17b35e..658f095 100644
--- a/src/components/modals/SearchUserModal.vue
+++ b/src/components/modals/SearchUserModal.vue
@@ -82,7 +82,7 @@ function searchUser(name: string) {
     <template v-slot:body>
       <div class="input-group mt-2 mb-4">
         <span class="input-group-text" id="objects-search-wrapping"
-          ><bootstrap-icon icon="search" :width="16" :height="16"
+          ><bootstrap-icon icon="search"
         /></span>
         <input
           class="form-control"
@@ -100,10 +100,9 @@ function searchUser(name: string) {
         <bootstrap-icon
           icon="x-lg"
           class="mb-2"
-          :width="56"
-          :height="56"
+          width="56"
+          height="56"
           style="color: var(--bs-danger)"
-          fill="currentColor"
         /><br />
         <span class="text-danger"
           >There seems to be an error<br />Try again later</span
@@ -126,10 +125,9 @@ function searchUser(name: string) {
         <bootstrap-icon
           icon="search"
           class="mb-2"
-          :width="56"
-          :height="56"
+          width="56"
+          height="56"
           style="color: var(--bs-secondary)"
-          fill="currentColor"
         /><br />
         <span v-if="formState.searchString.length > 2"
           >Could not find any Users</span
diff --git a/src/components/object-storage/BucketListItem.vue b/src/components/object-storage/BucketListItem.vue
index 1412a89..bec5afd 100644
--- a/src/components/object-storage/BucketListItem.vue
+++ b/src/components/object-storage/BucketListItem.vue
@@ -92,9 +92,6 @@ onMounted(() => {
             v-if="props.active && permission == null && props.deletable"
             icon="trash-fill"
             class="delete-icon me-2"
-            :width="16"
-            :height="16"
-            fill="currentColor"
             @click="emit('delete-bucket', bucket.name)"
           />
           <bootstrap-icon
@@ -103,9 +100,6 @@ onMounted(() => {
             :data-bs-target="'#view-bucket-details-modal' + randomIDSuffix"
             v-if="props.active"
             icon="info-circle-fill"
-            :width="16"
-            :height="16"
-            fill="currentColor"
           />
         </div>
       </router-link>
diff --git a/src/components/object-storage/BucketView.vue b/src/components/object-storage/BucketView.vue
index 86e21c8..7bd9e5e 100644
--- a/src/components/object-storage/BucketView.vue
+++ b/src/components/object-storage/BucketView.vue
@@ -580,7 +580,7 @@ watch(
     <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"
+          ><bootstrap-icon icon="search"
         /></span>
         <input
           type="text"
@@ -603,7 +603,7 @@ watch(
         data-bs-title="Upload Object"
         data-bs-target="#upload-object-modal"
       >
-        <bootstrap-icon icon="upload" :width="16" :height="16" fill="white" />
+        <bootstrap-icon icon="upload" fill="white" />
         <span class="visually-hidden">Upload Object</span>
       </button>
       <upload-object-modal
@@ -623,7 +623,7 @@ watch(
         data-bs-title="Create Folder"
         data-bs-target="#create-folder-modal"
       >
-        <bootstrap-icon icon="plus-lg" :width="16" :height="16" fill="white" />
+        <bootstrap-icon icon="plus-lg" />
         Folder
         <span class="visually-hidden">Add Folder</span>
       </button>
@@ -645,12 +645,7 @@ watch(
         data-bs-title="Create Bucket Permission"
         data-bs-target="#create-permission-modal"
       >
-        <bootstrap-icon
-          icon="person-plus-fill"
-          :width="16"
-          :height="16"
-          fill="white"
-        />
+        <bootstrap-icon icon="person-plus-fill" />
         <span class="visually-hidden">Add Bucket Permission</span>
       </button>
       <permission-modal
@@ -677,12 +672,7 @@ watch(
         data-bs-toggle="modal"
         data-bs-target="#permission-list-modal"
       >
-        <bootstrap-icon
-          icon="person-lines-fill"
-          :width="16"
-          :height="16"
-          fill="white"
-        />
+        <bootstrap-icon icon="person-lines-fill" />
         <span class="visually-hidden">View Bucket Permissions</span>
       </button>
       <permission-list-modal
@@ -704,10 +694,9 @@ watch(
       <bootstrap-icon
         icon="search"
         class="mb-3"
-        :width="64"
-        :height="64"
+        width="64"
+        height="64"
         style="color: var(--bs-secondary)"
-        fill="currentColor"
       />
       <p>
         Bucket <i>{{ props.bucketName }}</i> not found
@@ -721,10 +710,9 @@ watch(
       <bootstrap-icon
         icon="folder-x"
         class="mb-3"
-        :width="64"
-        :height="64"
+        width="64"
+        height="64"
         style="color: var(--bs-secondary)"
-        fill="currentColor"
       />
       <p>You don't have permission for this bucket</p>
     </div>
@@ -869,13 +857,7 @@ watch(
                       data-bs-target="#delete-object-modal"
                       :disabled="!writableBucket"
                     >
-                      <bootstrap-icon
-                        icon="trash-fill"
-                        class="text-danger"
-                        :width="13"
-                        :height="13"
-                        fill="currentColor"
-                      />
+                      <bootstrap-icon icon="trash-fill" />
                       <span class="ms-1">Delete</span>
                     </button>
                   </li>
@@ -885,7 +867,7 @@ watch(
               <div v-else>
                 <button
                   type="button"
-                  class="btn btn-danger btn-sm align-middle"
+                  class="btn btn-danger btn-sm align-baseline"
                   :disabled="!writableBucket"
                   data-bs-toggle="modal"
                   data-bs-target="#delete-object-modal"
@@ -895,13 +877,7 @@ watch(
                     )
                   "
                 >
-                  <bootstrap-icon
-                    icon="trash-fill"
-                    class="text-danger me-2"
-                    :width="12"
-                    :height="12"
-                    fill="white"
-                  />
+                  <bootstrap-icon icon="trash-fill" class="me-2" />
                   <span>Delete</span>
                 </button>
               </div>
diff --git a/src/components/object-storage/S3KeyView.vue b/src/components/object-storage/S3KeyView.vue
index 1f29e7e..7985316 100644
--- a/src/components/object-storage/S3KeyView.vue
+++ b/src/components/object-storage/S3KeyView.vue
@@ -60,9 +60,7 @@ function deleteKeyTrigger() {
       @click="visibleSecret = !visibleSecret"
     >
       <bootstrap-icon
-        :width="18"
-        :height="18"
-        fill="white"
+        class="fs-5"
         :icon="visibleSecret ? 'eye' : 'eye-slash'"
       />
     </button>
diff --git a/src/components/object-storage/modals/PermissionModal.vue b/src/components/object-storage/modals/PermissionModal.vue
index ddc2899..0c61ba9 100644
--- a/src/components/object-storage/modals/PermissionModal.vue
+++ b/src/components/object-storage/modals/PermissionModal.vue
@@ -336,9 +336,6 @@ onMounted(() => {
       <bootstrap-icon
         v-if="props.deletable"
         icon="trash-fill"
-        :height="15"
-        :width="15"
-        fill="currentColor"
         class="me-2"
         :class="{ 'delete-icon': !formState.loading }"
         data-bs-toggle="modal"
@@ -347,9 +344,6 @@ onMounted(() => {
       <bootstrap-icon
         v-if="formState.readonly && props.editable"
         icon="pencil-fill"
-        :height="15"
-        :width="15"
-        fill="currentColor"
         class="pseudo-link"
         @click="formState.readonly = false"
       />
@@ -401,12 +395,7 @@ onMounted(() => {
               data-bs-toggle="modal"
               :data-bs-target="'#search-user-modal' + randomIDSuffix"
             >
-              <bootstrap-icon
-                icon="search"
-                :height="13"
-                :width="13"
-                fill="white"
-              />
+              <bootstrap-icon icon="search" />
             </button>
           </div>
         </div>
@@ -510,12 +499,7 @@ onMounted(() => {
               @click="permission.file_prefix = undefined"
               :hidden="permission.file_prefix == undefined"
             >
-              <bootstrap-icon
-                icon="x-lg"
-                :height="14"
-                :width="14"
-                fill="currentColor"
-              />
+              <bootstrap-icon icon="x-lg" />
             </button>
           </div>
         </div>
diff --git a/src/components/workflows/WorkflowCard.vue b/src/components/workflows/WorkflowCard.vue
new file mode 100644
index 0000000..09826b6
--- /dev/null
+++ b/src/components/workflows/WorkflowCard.vue
@@ -0,0 +1,104 @@
+<script setup lang="ts">
+import type { WorkflowOut, WorkflowVersionReduced } from "@/client/workflow";
+import BootstrapIcon from "@/components/BootstrapIcon.vue";
+import dayjs from "dayjs";
+import { onMounted, ref, computed } from "vue";
+import type { Ref, ComputedRef } from "vue";
+import { Tooltip } from "bootstrap";
+import { useWorkflowStore } from "@/stores/workflows";
+
+const props = defineProps<{
+  workflow: WorkflowOut;
+  loading: boolean;
+}>();
+const workflowRepository = useWorkflowStore();
+
+const randomIDSuffix: string = Math.random().toString(16).substr(2, 8);
+const truncateDescription: Ref<boolean> = ref(true);
+const latestVersion: ComputedRef<WorkflowVersionReduced | undefined> = computed(
+  () =>
+    props.loading
+      ? undefined
+      : workflowRepository.latestVersion(props.workflow.workflow_id)
+);
+
+onMounted(() => {
+  if (!props.loading) {
+    new Tooltip("#creationDate-" + randomIDSuffix);
+  }
+});
+</script>
+
+<template>
+  <div
+    class="card-hover border border-secondary card text-bg-dark m-3 align-self-center"
+  >
+    <div class="card-body">
+      <h3 class="card-title">
+        <div v-if="props.loading" class="placeholder-glow">
+          <span class="placeholder col-6"></span>
+        </div>
+        <a v-else href="#">{{ props.workflow.name }}</a>
+      </h3>
+      <p class="card-text" :class="{ 'text-truncate': truncateDescription }">
+        <span v-if="props.loading" class="placeholder-glow"
+          ><span class="placeholder col-12"></span
+        ></span>
+        <span
+          v-else
+          @click="truncateDescription = false"
+          :class="{
+            'pointer-cursor': truncateDescription,
+          }"
+          >{{ props.workflow.short_description }}</span
+        >
+      </p>
+      <div class="d-flex justify-content-between mb-0">
+        <div v-if="props.loading" class="placeholder-glow w-50">
+          <span class="placeholder placeholder-lg w-50 bg-success"></span>
+        </div>
+        <a
+          v-else
+          :href="
+            workflow.repository_url + '/tree/' + latestVersion?.git_commit_hash
+          "
+          target="_blank"
+          class="btn btn-outline-success"
+          role="button"
+        >
+          <bootstrap-icon class="fs-5" icon="tag-fill" />
+          {{ latestVersion?.version }}
+        </a>
+        <div v-if="props.loading" class="placeholder-glow w-25">
+          <span class="placeholder w-100 bg-secondary"></span>
+        </div>
+        <span
+          v-else
+          :id="'creationDate-' + randomIDSuffix"
+          data-bs-toggle="tooltip"
+          class="align-self-end text-secondary"
+          :data-bs-title="
+            dayjs(workflow.versions[0].created_at).format('DD.MM.YYYY HH:mm:ss')
+          "
+        >
+          {{ dayjs(latestVersion?.created_at).fromNow() }}
+        </span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.card-hover {
+  transition: transform 0.3s ease-out;
+  width: 47%;
+}
+
+.card-hover:hover {
+  transform: translate(0, -5px);
+}
+
+.pointer-cursor {
+  cursor: pointer;
+}
+</style>
diff --git a/src/router/index.ts b/src/router/index.ts
index 9dbda65..41c8d18 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -29,6 +29,11 @@ const router = createRouter({
           name: "s3_keys",
           component: () => import("../views/object-storage/S3KeysView.vue"),
         },
+        {
+          path: "workflows",
+          name: "workflows",
+          component: () => import("../views/workflows/ListWorkflowsView.vue"),
+        },
       ],
     },
     {
diff --git a/src/stores/workflows.ts b/src/stores/workflows.ts
new file mode 100644
index 0000000..a15e6c1
--- /dev/null
+++ b/src/stores/workflows.ts
@@ -0,0 +1,47 @@
+import { defineStore } from "pinia";
+import { WorkflowService } from "@/client/workflow";
+import type { WorkflowVersionReduced } from "@/client/workflow";
+import type { WorkflowOut } from "@/client/workflow";
+
+export const useWorkflowStore = defineStore({
+  id: "workflows",
+  state: () =>
+    ({
+      workflows: [],
+    } as {
+      workflows: WorkflowOut[];
+    }),
+  getters: {
+    latestVersion(): (
+      workflowId: string
+    ) => WorkflowVersionReduced | undefined {
+      return (workflowId) => {
+        const workflow = this.workflows.find(
+          (w) => workflowId == w.workflow_id
+        );
+        return workflow?.versions[
+          Math.max(workflow?.versions?.length - 1 ?? 0, 0)
+        ];
+      };
+    },
+  },
+  actions: {
+    fetchWorkflows(
+      onFulfilled:
+        | ((workflows: WorkflowOut[]) => void)
+        | null
+        | undefined = null,
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      onRejected: ((reason: any) => void) | null | undefined = null,
+      onFinally: (() => void) | null | undefined = null
+    ) {
+      WorkflowService.workflowListWorkflows()
+        .then((workflows) => {
+          this.workflows = workflows;
+          onFulfilled?.(workflows);
+        })
+        .catch(onRejected)
+        .finally(onFinally);
+    },
+  },
+});
diff --git a/src/views/object-storage/BucketsView.vue b/src/views/object-storage/BucketsView.vue
index 877761c..15fc387 100644
--- a/src/views/object-storage/BucketsView.vue
+++ b/src/views/object-storage/BucketsView.vue
@@ -71,7 +71,7 @@ onMounted(() => {
     modalID="create-bucket-modal"
     v-if="!authStore.foreignUser"
   />
-  <div class="row m-2 border-bottom border-light mt-4">
+  <div class="row m-2 border-bottom border-light">
     <div class="col-12"></div>
     <h1 class="mb-2 text-light">Buckets</h1>
   </div>
@@ -83,7 +83,7 @@ onMounted(() => {
           class="btn btn-light"
           @click.stop.prevent="fetchBuckets"
         >
-          <bootstrap-icon icon="arrow-clockwise" />
+          <bootstrap-icon icon="arrow-clockwise" class="fs-5" />
           <span class="visually-hidden">Refresh Buckets</span>
         </button>
         <button
@@ -93,13 +93,13 @@ onMounted(() => {
           data-bs-toggle="modal"
           data-bs-target="#create-bucket-modal"
         >
-          <bootstrap-icon icon="plus-lg" />
+          <bootstrap-icon icon="plus-lg" class="fs-5" />
           <span class="visually-hidden">Create Bucket</span>
         </button>
       </div>
       <div class="input-group flex-nowrap mt-2">
         <span class="input-group-text" id="buckets-search-wrapping"
-          ><bootstrap-icon icon="search" :width="16" :height="16"
+          ><bootstrap-icon icon="search"
         /></span>
         <input
           type="text"
@@ -131,10 +131,9 @@ onMounted(() => {
             <bootstrap-icon
               icon="search"
               class="mb-2"
-              :width="56"
-              :height="56"
+              width="56"
+              height="56"
               style="color: var(--bs-secondary)"
-              fill="currentColor"
             />
             <br />
             Could not find any Buckets
@@ -145,8 +144,7 @@ onMounted(() => {
             v-for="n in 5"
             :key="n"
             :active="false"
-            :loading="true"
-            :permission="undefined"
+            loading
             :deletable="false"
             :bucket="{
               name: '',
@@ -169,10 +167,9 @@ onMounted(() => {
         <bootstrap-icon
           icon="hand-index-thumb-fill"
           class="mb-5"
-          :width="64"
-          :height="64"
+          width="64"
+          height="64"
           style="color: var(--bs-secondary)"
-          fill="currentColor"
         />
         <p>Click on a bucket to browse its content</p>
       </div>
diff --git a/src/views/object-storage/S3KeysView.vue b/src/views/object-storage/S3KeysView.vue
index d234f32..b0d1c10 100644
--- a/src/views/object-storage/S3KeysView.vue
+++ b/src/views/object-storage/S3KeysView.vue
@@ -118,11 +118,11 @@ onMounted(() => {
           class="btn btn-light"
           @click="refreshKeys(authStore.user?.uid ?? 'impossible')"
         >
-          <bootstrap-icon icon="arrow-clockwise" />
+          <bootstrap-icon icon="arrow-clockwise" class="fs-5" />
           <span class="visually-hidden">Refresh S3 Keys</span>
         </button>
         <button type="button" class="btn btn-light" @click="createKey">
-          <bootstrap-icon icon="plus-lg" />
+          <bootstrap-icon icon="plus-lg" class="fs-5" />
           <span class="visually-hidden">Create S3 Key</span>
         </button>
       </div>
diff --git a/src/views/workflows/ListWorkflowsView.vue b/src/views/workflows/ListWorkflowsView.vue
new file mode 100644
index 0000000..8ea7602
--- /dev/null
+++ b/src/views/workflows/ListWorkflowsView.vue
@@ -0,0 +1,60 @@
+<script setup lang="ts">
+import { computed, onMounted, reactive } from "vue";
+import type { ComputedRef } from "vue";
+import { useWorkflowStore } from "@/stores/workflows";
+import type { WorkflowOut } from "@/client/workflow";
+import WorkflowCard from "@/components/workflows/WorkflowCard.vue";
+
+const workflowRepository = useWorkflowStore();
+
+const workflowsState = reactive({
+  loading: true,
+  filterString: "",
+} as {
+  loading: boolean;
+  filterString: string;
+});
+
+const filteredWorkflows: ComputedRef<WorkflowOut[]> = computed(() => {
+  return workflowsState.filterString.length > 0
+    ? workflowRepository.workflows.filter((workflow) =>
+        workflow.name.includes(workflowsState.filterString)
+      )
+    : workflowRepository.workflows;
+});
+
+onMounted(() => {
+  workflowRepository.fetchWorkflows(
+    null,
+    null,
+    () => (workflowsState.loading = false)
+  );
+});
+</script>
+
+<template>
+  <div v-if="!workflowsState.loading" class="d-flex flex-wrap">
+    <workflow-card
+      v-for="workflow in filteredWorkflows"
+      :key="workflow.workflow_id"
+      :workflow="workflow"
+      :loading="false"
+    />
+  </div>
+  <div v-else class="d-flex flex-wrap">
+    <workflow-card
+      v-for="workflow in 6"
+      :key="workflow"
+      :workflow="{
+        name: '',
+        short_description: '',
+        repository_url: '',
+        workflow_id: '',
+        versions: [],
+      }"
+      loading
+    />
+  </div>
+</template>
+
+<style scoped></style>
-- 
GitLab