From 2ba00971acbcb2c0f89ff214fbe4b44e67d1ff86 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20G=C3=B6bel?= <dgoebel@techfak.uni-bielefeld.de>
Date: Thu, 12 Oct 2023 14:20:37 +0200
Subject: [PATCH] Use object repository in parameter schema form

#71
---
 .../ParameterSchemaFormComponent.vue          | 44 +++++++++++--------
 .../form-mode/ParameterStringInput.vue        | 27 ++++++------
 src/stores/s3keys.ts                          |  4 +-
 src/views/LoginView.vue                       | 12 ++---
 src/views/object-storage/BucketView.vue       | 14 +++---
 src/views/object-storage/S3KeysView.vue       |  2 +-
 .../workflows/ListWorkflowExecutionsView.vue  | 18 +++++++-
 src/views/workflows/WorkflowView.vue          |  2 +-
 8 files changed, 74 insertions(+), 49 deletions(-)

diff --git a/src/components/parameter-schema/ParameterSchemaFormComponent.vue b/src/components/parameter-schema/ParameterSchemaFormComponent.vue
index ae1a09d..fea414f 100644
--- a/src/components/parameter-schema/ParameterSchemaFormComponent.vue
+++ b/src/components/parameter-schema/ParameterSchemaFormComponent.vue
@@ -6,6 +6,11 @@ import Ajv from "ajv";
 import type { ValidateFunction } from "ajv";
 import ParameterStringInput from "@/components/parameter-schema/form-mode/ParameterStringInput.vue";
 import { Toast } from "bootstrap";
+import { useBucketStore } from "@/stores/buckets";
+import { useS3KeyStore } from "@/stores/s3keys";
+
+const bucketRepository = useBucketStore();
+const s3KeyRepository = useS3KeyStore();
 
 // Props
 // =============================================================================
@@ -165,6 +170,9 @@ function startWorkflow() {
 // =============================================================================
 onMounted(() => {
   if (props.schema) updateSchema(props.schema);
+  bucketRepository.fetchBuckets();
+  bucketRepository.fetchOwnPermissions();
+  s3KeyRepository.fetchS3Keys();
   errorToast = new Toast("#workflowExecutionErrorToast");
 });
 </script>
@@ -208,6 +216,18 @@ onMounted(() => {
       @submit.prevent="startWorkflow"
       novalidate
     >
+      <template v-for="(group, groupName) in parameterGroups" :key="groupName">
+        <parameter-group-form
+          :modelValue="formState.formInput[groupName]"
+          @update:model-value="
+            (newValue) => (formState.formInput[groupName] = newValue)
+          "
+          v-if="formState.formInput[groupName]"
+          :parameter-group-name="groupName"
+          :parameter-group="group"
+          :showHidden="formState.showHidden"
+        />
+      </template>
       <div class="card mb-3">
         <h2 class="card-header" id="pipelineGeneralOptions">
           <font-awesome-icon icon="fa-solid fa-gear" class="me-2" />
@@ -256,18 +276,6 @@ onMounted(() => {
           </label>
         </div>
       </div>
-      <template v-for="(group, groupName) in parameterGroups" :key="groupName">
-        <parameter-group-form
-          :modelValue="formState.formInput[groupName]"
-          @update:model-value="
-            (newValue) => (formState.formInput[groupName] = newValue)
-          "
-          v-if="formState.formInput[groupName]"
-          :parameter-group-name="groupName"
-          :parameter-group="group"
-          :showHidden="formState.showHidden"
-        />
-      </template>
     </form>
     <!-- Loading card -->
     <div v-else class="col-9">
@@ -317,12 +325,6 @@ onMounted(() => {
       <nav class="h-100">
         <nav v-if="props.schema" class="nav">
           <ul class="ps-0">
-            <li class="nav-link">
-              <a href="#pipelineGeneralOptions">
-                <font-awesome-icon icon="fa-solid fa-gear" class="me-2" />
-                General Pipeline Options
-              </a>
-            </li>
             <li
               class="nav-link"
               v-for="group in navParameterGroups"
@@ -337,6 +339,12 @@ onMounted(() => {
                 {{ group.title }}</a
               >
             </li>
+            <li class="nav-link">
+              <a href="#pipelineGeneralOptions">
+                <font-awesome-icon icon="fa-solid fa-gear" class="me-2" />
+                General Pipeline Options
+              </a>
+            </li>
           </ul>
           <div class="mx-auto mb-3">
             <input
diff --git a/src/components/parameter-schema/form-mode/ParameterStringInput.vue b/src/components/parameter-schema/form-mode/ParameterStringInput.vue
index 7eca836..cbfa064 100644
--- a/src/components/parameter-schema/form-mode/ParameterStringInput.vue
+++ b/src/components/parameter-schema/form-mode/ParameterStringInput.vue
@@ -1,9 +1,10 @@
 <script setup lang="ts">
 import { computed, watch, ref, onMounted, reactive } from "vue";
 import { useBucketStore } from "@/stores/buckets";
-import { ObjectService } from "@/client/s3proxy";
+import { useS3ObjectStore } from "@/stores/s3objects";
 
 const bucketRepository = useBucketStore();
+const s3objectRepository = useS3ObjectStore();
 
 const props = defineProps({
   parameter: {
@@ -38,8 +39,6 @@ const s3Path = reactive<{
   key: undefined,
 });
 
-const keysInBucket = ref<string[]>([]);
-
 watch(defaultValue, (newVal, oldVal) => {
   if (newVal != oldVal && newVal != undefined) {
     emit("update:modelValue", newVal);
@@ -72,13 +71,18 @@ const stringInput = ref<HTMLInputElement | undefined>(undefined);
 const format = computed<string | undefined>(() => props.parameter["format"]);
 
 const filesInBucket = computed<string[]>(() =>
-  keysInBucket.value.filter((obj) => !obj.endsWith("/")),
+  (s3objectRepository.objectMapping[s3Path.bucket ?? ""] ?? [])
+    .filter((obj) => !obj.Key?.endsWith("/"))
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    .map((obj) => obj.Key!),
 );
 
 const foldersInBucket = computed<string[]>(() =>
-  keysInBucket.value
+  (s3objectRepository.objectMapping[s3Path.bucket ?? ""] ?? [])
+    .filter((obj) => obj.Key != undefined)
     .map((obj) => {
-      const parts = obj.split("/");
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      const parts = obj.Key!.split("/");
       return parts
         .slice(0, parts.length - 1)
         .map((part, index) =>
@@ -126,17 +130,14 @@ const helpTextPresent = computed<boolean>(() => props.parameter["help_text"]);
 
 function updateKeysInBucket(bucketName?: string) {
   if (bucketName != null) {
-    ObjectService.objectGetBucketObjects(bucketName).then((objs) => {
-      keysInBucket.value = objs.map((obj) => obj.key);
-    });
-  } else {
-    keysInBucket.value = [];
+    s3objectRepository.fetchS3Objects(
+      bucketName,
+      bucketRepository.ownPermissions[bucketName]?.file_prefix ?? undefined,
+    );
   }
 }
 
 onMounted(() => {
-  bucketRepository.fetchBuckets();
-  bucketRepository.fetchOwnPermissions();
   if (format.value) {
     s3Path.key = defaultValue.value;
   }
diff --git a/src/stores/s3keys.ts b/src/stores/s3keys.ts
index c174a6e..915426f 100644
--- a/src/stores/s3keys.ts
+++ b/src/stores/s3keys.ts
@@ -22,11 +22,11 @@ export const useS3KeyStore = defineStore({
     },
   },
   actions: {
-    fetchS3Keys(uid: string, onFinally?: () => void): Promise<S3Key[]> {
+    fetchS3Keys(onFinally?: () => void): Promise<S3Key[]> {
       if (this.keys.length > 0) {
         onFinally?.();
       }
-      return S3KeyService.s3KeyGetUserKeys(uid)
+      return S3KeyService.s3KeyGetUserKeys(useAuthStore().currentUID)
         .then((keys) => {
           const s3ObjectRepository = useS3ObjectStore();
           s3ObjectRepository.updateS3Client(keys[0]);
diff --git a/src/views/LoginView.vue b/src/views/LoginView.vue
index 20c717d..2f0d18a 100644
--- a/src/views/LoginView.vue
+++ b/src/views/LoginView.vue
@@ -64,7 +64,7 @@ onMounted(() => {
       class="img-fluid mb-3"
       width="128"
       height="128"
-      alt="..."
+      alt="CloWM Logo"
     />
     <h1>
       <span class="blue fw-bold">Clo</span><span class="red fw-bold">W</span
@@ -98,7 +98,7 @@ onMounted(() => {
           <img
             src="/src/assets/images/nfdi.svg"
             alt="NFDI4Microbiota Logo"
-            height="70"
+            height="50"
           />
         </a>
       </div>
@@ -108,25 +108,25 @@ onMounted(() => {
           <img
             src="/src/assets/images/denbi.svg"
             alt="de.NBI Logo"
-            height="70"
+            height="50"
           />
         </a>
       </div>
       <div class="border rounded p-4 icon text-center">
         <h4 class="mb-4">Hosted By</h4>
         <a href="https://bibi.uni-bielefeld.de/">
-          <img src="/src/assets/images/bibi.png" alt="BiBi Logo" height="70" />
+          <img src="/src/assets/images/bibi.png" alt="BiBi Logo" height="50" />
         </a>
       </div>
       <div class="border rounded p-4 icon text-center">
         <h4 class="mb-4">Funded By</h4>
-        <img src="/src/assets/images/dfg.png" alt="DFG Logo" height="70" />
+        <img src="/src/assets/images/dfg.png" alt="DFG Logo" height="50" />
       </div>
       <div class="border rounded p-4 icon text-center">
         <img
           src="/src/assets/images/unibi.svg"
           alt="Bielefeld University Logo"
-          height="70"
+          height="50"
         />
       </div>
     </div>
diff --git a/src/views/object-storage/BucketView.vue b/src/views/object-storage/BucketView.vue
index 5966934..39d238f 100644
--- a/src/views/object-storage/BucketView.vue
+++ b/src/views/object-storage/BucketView.vue
@@ -267,7 +267,7 @@ onMounted(() => {
     }
   };
   // wait till s3keys and ownPermissions are available before fetching objects
-  s3KeyRepository.fetchS3Keys(authStore.currentUID, onFinally);
+  s3KeyRepository.fetchS3Keys(onFinally);
   bucketRepository.fetchOwnPermissions(onFinally);
 
   document
@@ -488,9 +488,9 @@ function getObjectFileName(key: string): string {
   </nav>
   <!-- Inputs on top -->
   <!-- Search bucket text input -->
-  <div class="row">
-    <div class="col-5 me-auto">
-      <div class="input-group mt-2 rounded shadow-sm">
+  <div class="d-flex justify-content-between align-items-center">
+    <div class="flex-grow-1 me-2">
+      <div class="input-group rounded shadow-sm">
         <span class="input-group-text" id="objects-search-wrapping"
           ><font-awesome-icon icon="fa-solid fa-magnifying-glass"
         /></span>
@@ -506,10 +506,10 @@ function getObjectFileName(key: string): string {
       </div>
     </div>
     <!-- Upload object button -->
-    <div id="BucketViewButtons" class="col-auto">
+    <div id="BucketViewButtons" class="">
       <button
         type="button"
-        class="btn btn-light me-4 tooltip-container border shadow-sm"
+        class="btn btn-light me-3 tooltip-container border shadow-sm"
         :disabled="errorLoadingObjects"
         data-bs-toggle="tooltip"
         data-bs-title="Refresh Objects"
@@ -538,7 +538,7 @@ function getObjectFileName(key: string): string {
       <!-- Add folder button -->
       <button
         type="button"
-        class="btn btn-light me-4 tooltip-container border shadow-sm"
+        class="btn btn-light me-3 tooltip-container border shadow-sm"
         :disabled="errorLoadingObjects || !writableBucket"
         data-bs-toggle="modal"
         data-bs-title="Create Folder"
diff --git a/src/views/object-storage/S3KeysView.vue b/src/views/object-storage/S3KeysView.vue
index a9072b8..ffade65 100644
--- a/src/views/object-storage/S3KeysView.vue
+++ b/src/views/object-storage/S3KeysView.vue
@@ -26,7 +26,7 @@ const allowKeyDeletion = computed<boolean>(() => keyRepository.keys.length > 1);
 
 function fetchKeys() {
   keyRepository
-    .fetchS3Keys(authStore.currentUID, () => (keyState.initialLoading = false))
+    .fetchS3Keys(() => (keyState.initialLoading = false))
     .then((keys) => {
       if (keyState.activeKey >= keys.length) {
         keyState.activeKey = keys.length - 1;
diff --git a/src/views/workflows/ListWorkflowExecutionsView.vue b/src/views/workflows/ListWorkflowExecutionsView.vue
index 5144545..691068c 100644
--- a/src/views/workflows/ListWorkflowExecutionsView.vue
+++ b/src/views/workflows/ListWorkflowExecutionsView.vue
@@ -1,6 +1,6 @@
 <script setup lang="ts">
 import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
-import { onMounted, reactive, computed } from "vue";
+import { onMounted, reactive, computed, onUnmounted } from "vue";
 import type { WorkflowExecutionOut } from "@/client/workflow";
 import { WorkflowExecutionStatus } from "@/client/workflow";
 import dayjs from "dayjs";
@@ -13,6 +13,7 @@ const workflowRepository = useWorkflowStore();
 const executionRepository = useWorkflowExecutionStore();
 
 let refreshTimeout: NodeJS.Timeout | undefined = undefined;
+let intervalId: NodeJS.Timer | undefined = undefined;
 
 const executionsState = reactive<{
   loading: boolean;
@@ -115,11 +116,26 @@ function cancelWorkflowExecution(executionId: string) {
   executionRepository.cancelExecution(executionId);
 }
 
+function refreshRunningWorkflowExecution() {
+  Promise.all(
+    executionRepository.executions
+      .filter((execution) => workflowExecutionCancelable(execution.status))
+      .map((execution) =>
+        executionRepository.fetchExecution(execution.execution_id),
+      ),
+  );
+}
+
 onMounted(() => {
   workflowRepository.fetchWorkflows();
   updateExecutions();
+  intervalId = setInterval(refreshRunningWorkflowExecution, 5000);
   new Tooltip("#refreshExecutionsButton");
 });
+
+onUnmounted(() => {
+  clearInterval(intervalId);
+});
 </script>
 
 <template>
diff --git a/src/views/workflows/WorkflowView.vue b/src/views/workflows/WorkflowView.vue
index ceea0ba..4b66830 100644
--- a/src/views/workflows/WorkflowView.vue
+++ b/src/views/workflows/WorkflowView.vue
@@ -255,7 +255,7 @@ onMounted(() => {
       v-if="activeVersionModeIds.length > 0"
       class="row align-items-center mb-3 fs-5"
     >
-      <label class="col-sm-1 col-form-label">Mode:</label>
+      <label class="col-sm-1 col-form-label"><b>Mode:</b></label>
       <div class="col-sm-11">
         <select class="form-select w-fit" v-model="workflowState.activeModeId">
           <option
-- 
GitLab