Skip to content
Snippets Groups Projects
Verified Commit cb94475e authored by Daniel Göbel's avatar Daniel Göbel
Browse files

Support workflow mode in arbitrary workflow execution

#63
parent f45ab60f
No related branches found
No related tags found
1 merge request!57Resolve "Support Workflow Modes"
......@@ -33,6 +33,6 @@ export type WorkflowIn = {
/**
* List of modes with alternative entrypoint the new workflow has
*/
modes?: (Array<WorkflowModeIn> | null);
modes?: Array<WorkflowModeIn>;
};
......@@ -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>;
};
......@@ -33,6 +33,6 @@ export type WorkflowVersion = {
/**
* Optional modes his workflow version has
*/
modes: (Array<string> | null);
modes: Array<string>;
};
......@@ -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>
......
......@@ -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,
}),
},
{
......
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;
},
},
});
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}`,
);
......
<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>
......@@ -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,
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment