From cb94475e4043d52403505f972749b227aaf79834 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20G=C3=B6bel?= <dgoebel@techfak.uni-bielefeld.de>
Date: Fri, 22 Sep 2023 11:15:19 +0200
Subject: [PATCH] Support workflow mode in arbitrary workflow execution

#63
---
 src/client/workflow/models/WorkflowIn.ts      |   2 +-
 src/client/workflow/models/WorkflowUpdate.ts  |   4 +-
 src/client/workflow/models/WorkflowVersion.ts |   2 +-
 .../modals/ArbitraryWorkflowModal.vue         | 115 ++++++++++++-
 src/router/index.ts                           |   4 +-
 src/stores/devWorkflow.ts                     |  18 ++
 src/utils/GitRepository.ts                    |  26 +--
 src/views/workflows/ArbitraryWorkflowView.vue | 158 +++++++++++-------
 src/views/workflows/MyWorkflowsView.vue       |   2 +-
 9 files changed, 247 insertions(+), 84 deletions(-)
 create mode 100644 src/stores/devWorkflow.ts

diff --git a/src/client/workflow/models/WorkflowIn.ts b/src/client/workflow/models/WorkflowIn.ts
index 544b0e3..9367556 100644
--- a/src/client/workflow/models/WorkflowIn.ts
+++ b/src/client/workflow/models/WorkflowIn.ts
@@ -33,6 +33,6 @@ export type WorkflowIn = {
     /**
      * List of modes with alternative entrypoint the new workflow has
      */
-    modes?: (Array<WorkflowModeIn> | null);
+    modes?: Array<WorkflowModeIn>;
 };
 
diff --git a/src/client/workflow/models/WorkflowUpdate.ts b/src/client/workflow/models/WorkflowUpdate.ts
index 97c2577..6aa6f18 100644
--- a/src/client/workflow/models/WorkflowUpdate.ts
+++ b/src/client/workflow/models/WorkflowUpdate.ts
@@ -17,10 +17,10 @@ export type WorkflowUpdate = {
     /**
      * Add modes to the new workflow version
      */
-    append_modes?: (Array<WorkflowModeIn> | null);
+    append_modes?: Array<WorkflowModeIn>;
     /**
      * Delete modes for the new workflow version.
      */
-    delete_modes?: (Array<string> | null);
+    delete_modes?: Array<string>;
 };
 
diff --git a/src/client/workflow/models/WorkflowVersion.ts b/src/client/workflow/models/WorkflowVersion.ts
index 563cdc6..a4248ce 100644
--- a/src/client/workflow/models/WorkflowVersion.ts
+++ b/src/client/workflow/models/WorkflowVersion.ts
@@ -33,6 +33,6 @@ export type WorkflowVersion = {
     /**
      * Optional modes his workflow version has
      */
-    modes: (Array<string> | null);
+    modes: Array<string>;
 };
 
diff --git a/src/components/workflows/modals/ArbitraryWorkflowModal.vue b/src/components/workflows/modals/ArbitraryWorkflowModal.vue
index df01d89..5d85f01 100644
--- a/src/components/workflows/modals/ArbitraryWorkflowModal.vue
+++ b/src/components/workflows/modals/ArbitraryWorkflowModal.vue
@@ -9,6 +9,8 @@ import {
   determineGitIcon,
 } from "@/utils/GitRepository";
 import { Collapse, Modal } from "bootstrap";
+import { useArbitraryWorkflowStore } from "@/stores/devWorkflow";
+import type { WorkflowModeOut } from "@/client/workflow";
 
 const props = defineProps<{
   modalID: string;
@@ -17,9 +19,11 @@ const props = defineProps<{
 let createWorkflowModal: Modal | null = null;
 let privateRepositoryCollapse: Collapse | null = null;
 let tokenHelpCollapse: Collapse | null = null;
+let workflowModeCollapse: Collapse | null = null;
 const arbitraryWorkflowForm = ref<HTMLFormElement | undefined>(undefined);
 const workflowRepositoryElement = ref<HTMLInputElement | undefined>(undefined);
 const router = useRouter();
+const workflowStore = useArbitraryWorkflowStore();
 
 const workflow = reactive<{
   repository_url: string;
@@ -37,6 +41,19 @@ const repositoryCredentials = reactive<{
   privateRepo: false,
 });
 
+const workflowMode = reactive<{
+  mode: WorkflowModeOut;
+  modeEnabled: boolean;
+}>({
+  mode: {
+    entrypoint: "",
+    schema_path: "",
+    name: "",
+    mode_id: crypto.randomUUID(),
+  },
+  modeEnabled: false,
+});
+
 const formState = reactive<{
   loading: boolean;
   checkRepoLoading: boolean;
@@ -65,6 +82,17 @@ watch(
   },
 );
 
+watch(
+  () => workflowMode.modeEnabled,
+  (show) => {
+    if (show) {
+      workflowModeCollapse?.show();
+    } else {
+      workflowModeCollapse?.hide();
+    }
+  },
+);
+
 function modalClosed() {
   formState.validated = false;
   tokenHelpCollapse?.hide();
@@ -72,15 +100,20 @@ function modalClosed() {
 
 function viewWorkflow() {
   createWorkflowModal?.hide();
+  const wid = workflowStore.setWorkflow({
+    ...workflow,
+    name: "",
+    short_description: "",
+    modes: workflowMode.modeEnabled ? [workflowMode.mode] : [],
+    token:
+      repositoryCredentials.token.length > 0
+        ? repositoryCredentials.token
+        : undefined,
+  });
   router.push({
     name: "arbitrary-workflow",
     query: {
-      repository: encodeURI(workflow.repository_url),
-      commit_hash: workflow.git_commit_hash,
-      token:
-        repositoryCredentials.token.length > 0
-          ? encodeURIComponent(repositoryCredentials.token)
-          : undefined,
+      wid: wid,
     },
   });
 }
@@ -100,7 +133,12 @@ function checkRepository() {
           : undefined,
       );
       repo
-        .checkFilesExist(requiredRepositoryFiles([]), true)
+        .checkFilesExist(
+          requiredRepositoryFiles(
+            workflowMode.modeEnabled ? [workflowMode.mode] : [],
+          ),
+          true,
+        )
         .then(() => {
           formState.allowUpload = true;
         })
@@ -133,6 +171,9 @@ onMounted(() => {
   privateRepositoryCollapse = new Collapse("#privateRepositoryCollapse", {
     toggle: false,
   });
+  workflowModeCollapse = new Collapse("#workflowModeCollapse", {
+    toggle: false,
+  });
   tokenHelpCollapse = new Collapse("#tokenHelpCollapse", {
     toggle: false,
   });
@@ -224,7 +265,7 @@ onMounted(() => {
               aria-controls="#privateRepositoryCollapse"
             />
             <label class="form-check-label" for="privateRepositoryCheckbox">
-              Private Git Repository
+              Enable Private Git Repository
             </label>
           </div>
           <div class="collapse" id="privateRepositoryCollapse">
@@ -280,6 +321,64 @@ onMounted(() => {
             </div>
           </div>
         </div>
+        <div class="mb-3">
+          <div class="form-check fs-5">
+            <input
+              class="form-check-input"
+              type="checkbox"
+              v-model="workflowMode.modeEnabled"
+              id="workflowModeCheckbox"
+              @change="formState.allowUpload = false"
+              aria-controls="#workflowModeCollapse"
+            />
+            <label class="form-check-label" for="workflowModeCheckbox">
+              Enable Workflow Mode
+            </label>
+          </div>
+          <div class="collapse" id="workflowModeCollapse">
+            <div class="row">
+              <div class="col-6 mb-2">
+                <label for="modeEntryInput-" class="form-label"
+                  >Entrypoint</label
+                >
+                <div class="input-group">
+                  <div class="input-group-text">
+                    <font-awesome-icon icon="fa-solid fa-tag" />
+                  </div>
+                  <input
+                    type="text"
+                    class="form-control"
+                    id="modeEntryInput"
+                    maxlength="128"
+                    v-model="workflowMode.mode.entrypoint"
+                    :required="workflowMode.modeEnabled"
+                    @change="formState.allowUpload = false"
+                  />
+                </div>
+              </div>
+              <div class="col-6">
+                <label for="modeSchemaInput-" class="form-label"
+                  >Schema File</label
+                >
+                <div class="input-group">
+                  <div class="input-group-text">
+                    <font-awesome-icon icon="fa-solid fa-file-code" />
+                  </div>
+                  <input
+                    type="text"
+                    class="form-control"
+                    id="modeSchemaInput-"
+                    maxlength="128"
+                    pattern=".*\.json$"
+                    v-model="workflowMode.mode.schema_path"
+                    :required="workflowMode.modeEnabled"
+                    @change="formState.allowUpload = false"
+                  />
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
       </form>
     </template>
     <template v-slot:footer>
diff --git a/src/router/index.ts b/src/router/index.ts
index 3d0e6ef..1432328 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -58,9 +58,7 @@ const router = createRouter({
             import("../views/workflows/ArbitraryWorkflowView.vue"),
           meta: { requiresDeveloperRole: true },
           props: (route) => ({
-            repository: route.query.repository,
-            commit_hash: route.query.commit_hash,
-            token: route.query.token,
+            wid: route.query.wid,
           }),
         },
         {
diff --git a/src/stores/devWorkflow.ts b/src/stores/devWorkflow.ts
new file mode 100644
index 0000000..38d625c
--- /dev/null
+++ b/src/stores/devWorkflow.ts
@@ -0,0 +1,18 @@
+import { defineStore } from "pinia";
+import type { WorkflowIn } from "@/client/workflow";
+
+export const useArbitraryWorkflowStore = defineStore({
+  id: "arbitraryWorkflows",
+  state: () => {
+    return {
+      arbitraryWorkflows: {} as Record<string, WorkflowIn>,
+    } as { arbitraryWorkflows: Record<string, WorkflowIn> };
+  },
+  actions: {
+    setWorkflow(workflow: WorkflowIn): string {
+      const wid = crypto.randomUUID();
+      this.arbitraryWorkflows[wid] = workflow;
+      return wid;
+    },
+  },
+});
diff --git a/src/utils/GitRepository.ts b/src/utils/GitRepository.ts
index 4299afa..648010c 100644
--- a/src/utils/GitRepository.ts
+++ b/src/utils/GitRepository.ts
@@ -1,8 +1,10 @@
 import axios from "axios";
 import type { AxiosInstance, AxiosBasicCredentials } from "axios";
-import type { WorkflowModeOut } from "@/client/workflow";
+import type { WorkflowModeOut, WorkflowModeIn } from "@/client/workflow";
 
-export function requiredRepositoryFiles(modes: WorkflowModeOut[]) {
+export function requiredRepositoryFiles(
+  modes?: WorkflowModeIn[] | WorkflowModeOut[],
+): string[] {
   const list = [
     "main.nf",
     "CHANGELOG.md",
@@ -10,7 +12,7 @@ export function requiredRepositoryFiles(modes: WorkflowModeOut[]) {
     "docs/usage.md",
     "docs/output.md",
   ];
-  if (modes.length > 0) {
+  if (modes && modes.length > 0) {
     list.push(...modes.map((mode) => mode.schema_path));
   } else {
     list.push("nextflow_schema.json");
@@ -117,16 +119,20 @@ class GithubRepository extends GitRepository {
     this.repoName = pathParts[1];
     if (token) {
       this.httpClient.interceptors.request.use((req) => {
-        req.auth = {
-          password: this.token,
-          username: this.account,
-        } as AxiosBasicCredentials;
+        if (!req.url?.includes("raw")) {
+          req.auth = {
+            password: this.token,
+            username: this.account,
+          } as AxiosBasicCredentials;
+        }
         return req;
       });
     }
     this.httpClient.interceptors.request.use((req) => {
-      req.headers.setAccept("application/vnd.github.object+json");
-      req.headers["X-GitHub-Api-Version"] = "2022-11-28";
+      if (!req.url?.includes("raw")) {
+        req.headers["X-GitHub-Api-Version"] = "2022-11-28";
+        req.headers.setAccept("application/vnd.github.object+json");
+      }
       return req;
     });
   }
@@ -140,7 +146,7 @@ class GithubRepository extends GitRepository {
   }
 
   protected async downloadFileUrl(filepath: string): Promise<string> {
-    if (this.token != undefined) {
+    if (this.token == undefined) {
       return Promise.resolve(
         `https://raw.githubusercontent.com/${this.account}/${this.repoName}/${this.gitCommitHash}/${filepath}`,
       );
diff --git a/src/views/workflows/ArbitraryWorkflowView.vue b/src/views/workflows/ArbitraryWorkflowView.vue
index bc25599..ee5f05c 100644
--- a/src/views/workflows/ArbitraryWorkflowView.vue
+++ b/src/views/workflows/ArbitraryWorkflowView.vue
@@ -1,30 +1,38 @@
 <script setup lang="ts">
 import WorkflowDocumentationTabs from "@/components/workflows/WorkflowDocumentationTabs.vue";
-import { onMounted, reactive, ref } from "vue";
+import { onMounted, reactive, ref, watch } from "vue";
 import { GitRepository, requiredRepositoryFiles } from "@/utils/GitRepository";
 import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
 import ParameterSchemaFormComponent from "@/components/parameter-schema/ParameterSchemaFormComponent.vue";
 import { WorkflowExecutionService } from "@/client/workflow";
 import { useRouter } from "vue-router";
 import { Toast } from "bootstrap";
+import { useArbitraryWorkflowStore } from "@/stores/devWorkflow";
+import type { WorkflowIn } from "@/client/workflow";
 
 const props = defineProps<{
-  repository?: string;
-  commit_hash?: string;
-  token?: string;
+  wid: string;
 }>();
 
 const router = useRouter();
+const workflowStore = useArbitraryWorkflowStore();
 
 const workflowState = reactive<{
+  workflow?: WorkflowIn;
   loading: boolean;
   changelogMarkdown?: string;
   descriptionMarkdown?: string;
   usageMarkdown?: string;
   outputMarkdown?: string;
   parameterSchema?: Record<string, never>;
+  repo: GitRepository;
 }>({
   loading: true,
+  workflow: undefined,
+  repo: GitRepository.buildRepository(
+    "https://github.de/eample/example",
+    "0123456789abcdef",
+  ),
 });
 
 const workflowExecutionState = reactive<{
@@ -38,49 +46,63 @@ const workflowExecutionState = reactive<{
 const showDocumentation = ref<boolean>(true);
 let errorToast: Toast | null = null;
 
-function downloadVersionFiles(
-  repository: string,
-  commit_hash: string,
-  token?: string,
-) {
-  workflowState.loading = true;
-  const repo = GitRepository.buildRepository(repository, commit_hash, token);
-  Promise.all(
-    requiredRepositoryFiles([]).map((file) =>
-      repo.downloadFile(file).then((response) => {
-        if (file.includes("README")) {
-          workflowState.descriptionMarkdown = response.data;
-        } else if (file.includes("CHANGELOG")) {
-          workflowState.changelogMarkdown = response.data;
-        } else if (file.includes("schema")) {
-          workflowState.parameterSchema = response.data;
-        } else if (file.includes("usage")) {
-          workflowState.usageMarkdown = response.data;
-        } else if (file.includes("output")) {
-          workflowState.outputMarkdown = response.data;
-        }
-      }),
-    ),
-  ).finally(() => {
-    workflowState.loading = false;
-  });
+function downloadVersionFiles() {
+  if (workflowState.workflow) {
+    workflowState.loading = true;
+    Promise.all(
+      requiredRepositoryFiles(workflowState.workflow.modes).map((file) =>
+        workflowState.repo.downloadFile(file).then((response) => {
+          if (file.includes("README")) {
+            workflowState.descriptionMarkdown = response.data;
+          } else if (file.includes("CHANGELOG")) {
+            workflowState.changelogMarkdown = response.data;
+          } else if (file.includes("usage")) {
+            workflowState.usageMarkdown = response.data;
+          } else if (file.includes("output")) {
+            workflowState.outputMarkdown = response.data;
+          } else {
+            workflowState.parameterSchema = response.data;
+          }
+        }),
+      ),
+    ).finally(() => {
+      workflowState.loading = false;
+    });
+  }
 }
 
+watch(
+  () => workflowState.workflow,
+  (newWorkflow, oldWorkflow) => {
+    if (
+      newWorkflow &&
+      newWorkflow?.git_commit_hash !== oldWorkflow?.git_commit_hash
+    ) {
+      workflowState.repo = GitRepository.buildRepository(
+        newWorkflow.repository_url,
+        newWorkflow.git_commit_hash,
+        newWorkflow.token ?? undefined,
+      );
+      downloadVersionFiles();
+    }
+  },
+);
+
 function startWorkflow(
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   parameters: Record<string, any>,
   notes?: string,
   report_output_bucket?: string,
 ) {
-  if (props.repository && props.commit_hash) {
+  if (workflowState.workflow) {
     errorToast?.hide();
     workflowExecutionState.loading = true;
     WorkflowExecutionService.workflowExecutionStartArbitraryWorkflow({
-      git_commit_hash: props.commit_hash,
+      git_commit_hash: workflowState.workflow.git_commit_hash,
       parameters: parameters,
       report_output_bucket: report_output_bucket,
-      repository_url: props.repository,
-      token: props.token,
+      repository_url: workflowState.workflow.repository_url,
+      token: workflowState.workflow.token ?? undefined,
     })
       .then(() => {
         router.push({
@@ -102,9 +124,7 @@ function startWorkflow(
 
 onMounted(() => {
   errorToast = new Toast("#arbitraryWorkflowExecutionViewErrorToast");
-  if (props.commit_hash && props.repository) {
-    downloadVersionFiles(props.repository, props.commit_hash, props.token);
-  }
+  workflowState.workflow = workflowStore.arbitraryWorkflows[props.wid];
 });
 </script>
 
@@ -137,26 +157,38 @@ onMounted(() => {
       </div>
     </div>
   </div>
-  <div class="row m-1 border-bottom mb-4">
-    <h1 class="mb-2">Arbitrary Workflow</h1>
-  </div>
-  <h4>
-    Git Repository: <a target="_blank" :href="repository">{{ repository }}</a>
-  </h4>
-  <h4 class="mb-5">Git Commit Hash: {{ commit_hash }}</h4>
-  <div class="d-flex justify-content-center mb-5" v-if="showDocumentation">
-    <a
-      role="button"
-      href="#"
-      class="btn btn-success btn-lg mx-auto fs-4"
-      :class="{ disabled: !workflowState.parameterSchema }"
-      @click="showDocumentation = false"
-    >
-      <font-awesome-icon icon="fa-solid fa-rocket" class="me-2" />
-      <span class="align-middle">Launch</span>
-    </a>
-  </div>
-  <template v-if="repository && commit_hash">
+  <template v-if="workflowState.workflow">
+    <div class="row m-1 border-bottom mb-4">
+      <h1 class="mb-2">Arbitrary Workflow</h1>
+    </div>
+    <h4>
+      Git Repository:
+      <a target="_blank" :href="workflowState.workflow?.repository_url">{{
+        workflowState.workflow?.repository_url
+      }}</a>
+    </h4>
+    <h4 :class="{ 'mb-5': workflowState.workflow.modes!.length < 1 }">
+      Git Commit Hash: {{ workflowState.workflow?.git_commit_hash }}
+    </h4>
+    <template v-if="workflowState.workflow.modes!.length > 0">
+      <h5>Entrypoint: {{ workflowState.workflow?.modes?.[0].entrypoint }}</h5>
+      <h5 class="mb-5">
+        Schema File:
+        {{ workflowState.workflow?.modes?.[0].schema_path }}
+      </h5>
+    </template>
+    <div class="d-flex justify-content-center mb-5" v-if="showDocumentation">
+      <a
+        role="button"
+        href="#"
+        class="btn btn-success btn-lg mx-auto fs-4"
+        :class="{ disabled: !workflowState.parameterSchema }"
+        @click="showDocumentation = false"
+      >
+        <font-awesome-icon icon="fa-solid fa-rocket" class="me-2" />
+        <span class="align-middle">Launch</span>
+      </a>
+    </div>
     <workflow-documentation-tabs
       v-if="showDocumentation"
       :loading="workflowState.loading"
@@ -173,7 +205,17 @@ onMounted(() => {
       @start-workflow="startWorkflow"
     />
   </template>
-  <p v-else>Nope</p>
+  <template v-else>
+    <div class="text-center fs-1 mt-5">
+      <font-awesome-icon
+        icon="fa-solid fa-magnifying-glass"
+        class="my-5 fs-0"
+        style="color: var(--bs-secondary)"
+      />
+      <p class="my-5">Could not find your workflow.<br />Please re-enter it.</p>
+      <router-link :to="{ name: 'workflows' }" class="mt-5">Back</router-link>
+    </div>
+  </template>
 </template>
 
 <style scoped></style>
diff --git a/src/views/workflows/MyWorkflowsView.vue b/src/views/workflows/MyWorkflowsView.vue
index 882df78..8a7ecd9 100644
--- a/src/views/workflows/MyWorkflowsView.vue
+++ b/src/views/workflows/MyWorkflowsView.vue
@@ -45,7 +45,7 @@ const workflowsState = reactive<{
     version: "",
     workflow_id: "",
     git_commit_hash: "",
-    modes: null,
+    modes: [],
     icon_url: null,
     created_at: 0,
     status: Status.CREATED,
-- 
GitLab