Newer
Older
import { computed, ref, reactive, watch, onMounted, type PropType } from "vue";
import ParameterGroupForm from "@/components/parameter-schema/form-mode/ParameterGroupForm.vue";
import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
import Ajv from "ajv";
import type { ValidateFunction } from "ajv";
import ParameterStringInput from "@/components/parameter-schema/form-mode/ParameterStringInput.vue";

Daniel Göbel
committed
import { Toast, Tooltip } from "bootstrap";
import { useBucketStore } from "@/stores/buckets";
import { useS3KeyStore } from "@/stores/s3keys";
import BootstrapToast from "@/components/BootstrapToast.vue";
import { useResourceStore } from "@/stores/resources";
import { useRoute, useRouter } from "vue-router";
import type { ClowmInfo } from "@/types/ClowmInfo";

Daniel Göbel
committed
import UploadParameterFileModal from "@/components/parameter-schema/UploadParameterFileModal.vue";
import type {
TemporaryParams,

Daniel Göbel
committed
WorkflowParameters,
} from "@/types/WorkflowParameters";
import { useWorkflowExecutionStore } from "@/stores/workflowExecutions";
const bucketRepository = useBucketStore();
const resourceRepository = useResourceStore();
const keyRepository = useS3KeyStore();

Daniel Göbel
committed
const executionRepository = useWorkflowExecutionStore();
const router = useRouter();
const route = useRoute();
// Props
// =============================================================================
const props = defineProps({
schema: {
type: Object,
},
clowmInfo: {
type: Object as PropType<ClowmInfo>,
required: false,
},
loading: {
type: Boolean,
},
allowNotes: {
type: Boolean,
viewMode: {
type: String,
default: "simple",
validator(value: string) {
return ["simple", "advanced", "expert"].includes(value);
},
},
const emit = defineEmits<{
(
e: "start-workflow",

Daniel Göbel
committed
parameters: WorkflowParameters,
metaParameters: WorkflowMetaParameters,
// Bootstrap Elements
// =============================================================================
let errorToast: Toast | null = null;

Daniel Göbel
committed
let parameterLoadToast: Toast | null = null;
// Types
// =============================================================================
type ParameterGroup = {
group: string;
title: string;
icon?: string;
};
// JSON Schema package
// =============================================================================
const schemaCompiler = new Ajv({
strict: false,
});
let validateSchema: ValidateFunction;
// Reactive State
// =============================================================================
const launchForm = ref<HTMLFormElement | null>(null);
const formState = reactive<{

Daniel Göbel
committed
formInput: WorkflowParameters;
metaParameters: WorkflowMetaParameters;
errorType?: string;
}>({
formInput: {},
validated: false,
metaParameters: {
logs_s3_path: undefined,
debug_s3_path: undefined,
provenance_s3_path: undefined,
notes: undefined,
},
errorType: undefined,
});
// Computed Properties
// =============================================================================
const parameterGroups = computed<Record<string, never>>(() => {
if (Object.keys(props.schema?.["properties"] ?? {}).length > 0) {
return {
...props.schema?.["definitions"],
ungrouped_parameters: {
title: "Ungrouped Parameters",
properties: props.schema?.["properties"],
type: "object",
},
};
}
return props.schema?.["definitions"];
});
// Create a list with the names of all parameter groups
const navParameterGroups = computed<ParameterGroup[]>(() => {
let groups = Object.keys(parameterGroups.value).map((group) => {
return {
group: group,
title: parameterGroups.value[group]["title"],
icon: parameterGroups.value[group]["fa_icon"],
};
if (!showHidden.value) {
groups = groups.filter(
// filter all groups that have only hidden parameters
(group) =>
Object.keys(parameterGroups.value[group.group]["properties"]).filter(
(key) =>
!parameterGroups.value[group.group]["properties"][key]["hidden"],
).length > 0,
);
}
if (!showOptional.value) {
groups = groups.filter(
// filter all groups that have no required parameter
(group) =>
(
(parameterGroups.value[group.group]["required"] as Array<string>) ??
[]
).length > 0,
);
}
const showHidden = computed<boolean>(() => props.viewMode === "expert");
const showOptional = computed<boolean>(() => props.viewMode !== "simple");
// Watchers
// =============================================================================
watch(
() => props.schema,
(newValue) => {
if (newValue) {
updateSchema(newValue);
}
);
// Functions
// =============================================================================
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/ban-ts-comment, @typescript-eslint/no-unused-vars */
function updateSchema(schema: Record<string, any>) {
validateSchema = schemaCompiler.compile(schema);
const groupedParameters = Object.keys(parameterGroups.value).map(
(groupName) =>
Object.fromEntries(
Object.entries(parameterGroups.value[groupName]["properties"]).map(
([parameterName, parameter]) => [
parameterName,
// @ts-ignore
parameter["default"],
],
),

Daniel Göbel
committed
);
formState.formInput = groupedParameters.reduce((acc, val) => {

Daniel Göbel
committed
return { ...acc, ...val };
});
loadParameters(executionRepository.popTemporaryParameters());
// delete parameters that are strings and have a length of 0
for (const paramName of Object.keys(formState.formInput)) {
const param = formState.formInput[paramName];
if (typeof param === "string" && param?.trim().length === 0) {
delete formState.formInput[paramName];
}
}
if (launchForm.value?.checkValidity()) {

Daniel Göbel
committed
const schemaValid = validateSchema(formState.formInput);
if (!schemaValid) {
console.error(validateSchema.errors);
errorToast?.show();
} else {
emit("start-workflow", formState.formInput, formState.metaParameters);
} else {
formState.errorType = "form";
errorToast?.show();

Daniel Göbel
committed
function loadParameters(tempParams?: TemporaryParams) {
if (tempParams) {
for (const param in tempParams.params) {
if (param in formState.formInput) {
formState.formInput[param] = tempParams.params[param];
}
}
formState.metaParameters = tempParams.metaParams;

Daniel Göbel
committed
if (Object.keys(tempParams?.params ?? {}).length > 0) {
parameterLoadToast?.show();
}
}
}
function scroll(selectedAnchor: string) {
document.querySelector(selectedAnchor)?.scrollIntoView({
behavior: "smooth",
});
}
// Lifecycle Events
// =============================================================================
onMounted(() => {
if (props.schema) updateSchema(props.schema);
if (props.clowmInfo?.exampleParameters)
Tooltip.getOrCreateInstance("#exampleDataButton");
bucketRepository.fetchBuckets();
bucketRepository.fetchOwnPermissions();
keyRepository.fetchS3Keys();
resourceRepository.fetchPublicResources();
errorToast = new Toast("#workflowExecutionErrorToast");

Daniel Göbel
committed
parameterLoadToast = new Toast("#workflowExecutionParameterLoadToast");
});
</script>
<template>

Daniel Göbel
committed
<bootstrap-toast
toast-id="workflowExecutionParameterLoadToast"
color-class="success"
>
Successfully loaded parameters
</bootstrap-toast>
<bootstrap-toast toast-id="workflowExecutionErrorToast" color-class="danger">
<template #default>Error starting workflow</template>
<template #body>
<template v-if="formState.errorType === 'form'">
Some inputs are not valid.
</template>
<template v-else>
There was an error with starting the workflow execution. Look in the
console for more information.
</template>
</template>
</bootstrap-toast>

Daniel Göbel
committed
<upload-parameter-file-modal
modal-id="parameterUploadModal"
@parameters-uploaded="
(params: WorkflowParameters) =>
loadParameters({
params: params,
metaParams: {},
})
"
/>
<div class="row align-items-start">
class="col-9"
id="launchWorkflowForm"
ref="launchForm"
:class="{ 'was-validated': formState.validated }"
@submit.prevent="startWorkflow"
novalidate
<template v-for="(group, groupName) in parameterGroups" :key="groupName">
<parameter-group-form
v-model="formState.formInput"

Daniel Göbel
committed
v-if="formState.formInput"
:parameter-group-name="groupName"
:parameter-group="group"
:showHidden="showHidden"
:show-optional="showOptional"
:resource-parameters="props.clowmInfo?.resourceParameters"
/>
</template>
<div class="card mb-3">
<h3 class="card-header" id="pipelineGeneralOptions">
<font-awesome-icon icon="fa-solid fa-gear" class="me-2" />
Pipeline Options
<div class="card-body">
<h5 class="card-title">
General Options about the pipeline execution
</h5>
<div v-if="props.allowNotes" :hidden="!showOptional">
<code
class="bg-secondary-subtle p-2 rounded-top border border-secondary"
>--notes</code
>
<span
class="input-group-text border border-secondary"
id="pipelineNotes"
>
<font-awesome-icon icon="fa-solid fa-sticky-note" />
class="form-control border border-secondary"
rows="2"
v-model="formState.metaParameters.notes"
<label class="mb-3" for="pipelineNotes"
>Personal notes about the pipeline execution</label
>
<code
class="bg-secondary-subtle p-2 rounded-top border border-secondary"
>--logs_s3_path</code
>
<div class="input-group">
<span class="input-group-text border border-secondary">
<font-awesome-icon icon="fa-solid fa-folder" />
</span>
<parameter-string-input
parameter-name="logs_s3_path"
v-model="formState.metaParameters.logs_s3_path"
:parameter="{
format: 'directory-path',
type: 'string',
}"
remove-advanced
/>
</div>
<label class="mb-3" for="logsS3Path">
Directory in bucket where to save Nextflow log and reports
</label>
<code
class="bg-secondary-subtle p-2 rounded-top border border-secondary"
>--provenance_s3_path</code
>
<div class="input-group">
<span class="input-group-text border border-secondary">
<font-awesome-icon icon="fa-solid fa-folder" />
</span>
<parameter-string-input
parameter-name="provenance_s3_path"
v-model="formState.metaParameters.provenance_s3_path"
:parameter="{
format: 'directory-path',
type: 'string',
}"
remove-advanced
/>
</div>
<label class="mb-3" for="provenanceS3Path">
Directory in bucket where to save provenance information about the
workflow execution
</label>
</div>
<div :hidden="!showHidden">
<code
class="bg-secondary-subtle p-2 rounded-top border border-secondary"
>--debug_s3_path</code
>
<div class="input-group">
<span class="input-group-text border border-secondary">
<font-awesome-icon icon="fa-solid fa-folder" />
</span>
<parameter-string-input
parameter-name="debug_s3_path"
v-model="formState.metaParameters.debug_s3_path"
:parameter="{
format: 'directory-path',
type: 'string',
}"
remove-advanced
/>
</div>
<label class="mb-3" for="debugS3Path">
Directory in bucket where to save debug information about the
workflow execution
</label>
<!-- Loading card -->
<div v-else class="col-9">
<div class="card mb-3">
<h2 class="card-header placeholder-glow">
<span class="placeholder col-6"></span>
</h2>
<div class="card-body">
<h5 class="card-title placeholder-glow">
<span class="placeholder col-5"> </span>
</h5>
<template v-for="n in 4" :key="n">
<div class="placeholder-glow fs-5">
<span class="placeholder w-100"> </span>
</div>
<div class="mb-3 placeholder-glow">
<span class="placeholder col-3"> </span>
</div>
</template>
</div>
</div>
</div>
class="col-3 sticky-top border shadow-sm rounded-1 px-0"
style="top: 70px !important; max-height: calc(100vh - 150px)"
>
<h5 class="mx-3 mt-2">Parameter View</h5>
<div class="mx-2">
<div
class="btn-group my-1 w-100"
role="group"
aria-label="Basic radio toggle button group"
<input
type="radio"

Daniel Göbel
committed
class="btn-check"
name="view-mode"
id="view-mode-simple"
autocomplete="off"
:checked="props.viewMode === 'simple'"
@click="
router.replace({
query: { ...route.query, viewMode: 'simple' },
hash: route.hash,
})
"
/>
<label class="btn btn-outline-primary" for="view-mode-simple"
>Simple</label
>
<input
type="radio"
class="btn-check"
name="view-mode"
id="view-mode-advanced"
autocomplete="off"
:checked="props.viewMode === 'advanced'"
@click="
router.replace({
query: { ...route.query, viewMode: 'advanced' },
hash: route.hash,
})
"
/>
<label class="btn btn-outline-primary" for="view-mode-advanced"
>Advanced</label
>
<input
type="radio"
class="btn-check"
name="view-mode"
id="view-mode-expert"
autocomplete="off"
:checked="props.viewMode === 'expert'"
@click="
router.replace({
query: { ...route.query, viewMode: 'expert' },
hash: route.hash,
})
"
/>
<label class="btn btn-outline-primary" for="view-mode-expert"
>Expert</label
>
</div>
<ul class="ps-0">
<li
class="nav-link"
v-for="group in navParameterGroups"
:key="group.group"
>
<router-link
:to="{ hash: '#' + group.group, query: route.query }"
replace
@click="scroll('#' + group.group)"
>
:icon="group.icon"
v-if="group.icon"
class="me-2"
{{ group.title }}
</router-link>
<li class="nav-link">
<router-link
:to="{ hash: '#pipelineGeneralOptions', query: route.query }"
replace
@click="scroll('#pipelineGeneralOptions')"
>
<font-awesome-icon icon="fa-solid fa-gear" class="me-2" />
General Pipeline Options
</router-link>
</li>
<!-- Loading nav links -->
<div v-else class="placeholder-glow ps-3 pt-3">
<span
v-for="n in 5"
:key="n"
class="placeholder col-8 mt-2 mb-3"
></span>
</div>

Daniel Göbel
committed
<div class="d-grid gap-2 mb-2 px-2">
<button
type="button"
class="btn btn-primary"
v-if="props.clowmInfo?.exampleParameters"
data-bs-toggle="tooltip"
id="exampleDataButton"
data-bs-title="Load example parameters/data for this workflow"
@click="
loadParameters({
params: props.clowmInfo?.exampleParameters,
metaParams: {},
})
"
>
Try it out
</button>
<button
class="btn btn-primary"
data-bs-toggle="modal"
data-bs-target="#parameterUploadModal"
>
<font-awesome-icon icon="fa-solid fa-upload" class="me-2" />
Upload Parameters
</button>
<button
type="submit"
form="launchWorkflowForm"

Daniel Göbel
committed
class="btn btn-success btn-lg"
:disabled="props.loading || !props.schema"
>
<font-awesome-icon icon="fa-solid fa-play" class="me-2" />
Launch
</button>
</div>
</div>
</div>
</template>

Daniel Göbel
committed
<style>
.was-validated *:invalid {
border-color: var(--bs-form-invalid-border-color) !important;
background: var(--bs-danger-bg-subtle) !important;