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