-
Daniel Göbel authored
#90
Daniel Göbel authored#90
ParameterSchemaFormComponent.vue 15.53 KiB
<script setup lang="ts">
import { computed, ref, reactive, watch, onMounted } 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";
import { Toast } 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";
const bucketRepository = useBucketStore();
const resourceRepository = useResourceStore();
const keyRepository = useS3KeyStore();
const router = useRouter();
const route = useRoute();
// Props
// =============================================================================
const props = defineProps({
schema: {
type: Object,
},
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",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
parameters: Record<string, any>,
notes?: string,
logs_s3_path?: string,
debug_s3_path?: string,
provenance_s3_path?: string,
): void;
}>();
// Bootstrap Elements
// =============================================================================
let errorToast: 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<{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
formInput: Record<string, any>;
validated: boolean;
pipelineNotes: string;
logs_s3_path?: string;
debug_s3_path?: string;
provenance_s3_path?: string;
errorType?: string;
}>({
formInput: {},
validated: false,
pipelineNotes: "",
logs_s3_path: undefined,
debug_s3_path: undefined,
provenance_s3_path: undefined,
errorType: undefined,
});
// Computed Properties
// =============================================================================
const parameterGroups = computed<Record<string, never>>(
() => 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,
);
}
return groups;
});
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 b = Object.keys(schema["definitions"]).map((groupName) => [
groupName,
Object.fromEntries(
Object.entries(schema["definitions"][groupName]["properties"]).map(
([parameterName, parameter]) => [
parameterName,
// @ts-ignore
parameter["default"],
],
),
),
]);
formState.formInput = Object.fromEntries(b);
}
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/ban-ts-comment, @typescript-eslint/no-unused-vars */
function startWorkflow() {
errorToast?.hide();
formState.validated = true;
formState.errorType = undefined;
if (launchForm.value?.checkValidity()) {
const realInput: Record<string, any> = Object.values(
formState.formInput,
).reduce((acc, val) => {
return { ...acc, ...val };
});
const schemaValid = validateSchema(realInput);
if (!schemaValid) {
console.error(validateSchema.errors);
errorToast?.show();
} else {
emit(
"start-workflow",
realInput,
formState.pipelineNotes,
formState.logs_s3_path,
formState.debug_s3_path,
formState.provenance_s3_path,
);
}
} else {
formState.errorType = "form";
errorToast?.show();
}
}
function scroll(selectedAnchor: string) {
document.querySelector(selectedAnchor)?.scrollIntoView({
behavior: "smooth",
});
}
// Lifecycle Events
// =============================================================================
onMounted(() => {
if (props.schema) updateSchema(props.schema);
bucketRepository.fetchBuckets();
bucketRepository.fetchOwnPermissions();
keyRepository.fetchS3Keys();
resourceRepository.fetchPublicResources();
errorToast = new Toast("#workflowExecutionErrorToast");
});
</script>
<template>
<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>
<div class="row mb-5 align-items-start">
<form
v-if="props.schema"
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
: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="showHidden"
:show-optional="showOptional"
/>
</template>
<div class="card mb-3">
<h2 class="card-header" id="pipelineGeneralOptions">
<font-awesome-icon icon="fa-solid fa-gear" class="me-2" />
Pipeline Options
</h2>
<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
>
<div class="input-group">
<span
class="input-group-text border border-secondary"
id="pipelineNotes"
>
<font-awesome-icon icon="fa-solid fa-sticky-note" />
</span>
<textarea
class="form-control border border-secondary"
rows="2"
v-model="formState.pipelineNotes"
/>
</div>
<label class="mb-3" for="pipelineNotes"
>Personal notes about the pipeline execution</label
>
</div>
<div>
<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.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>
</div>
<div>
<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.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.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>
</div>
</div>
</div>
</form>
<!-- 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>
<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"
class="btn-check w-100"
name="view-mode"
id="view-mode-simple"
autocomplete="off"
:checked="props.viewMode === 'simple'"
@click="
router.replace({
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: { 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: { viewMode: 'expert' },
hash: route.hash,
})
"
/>
<label class="btn btn-outline-primary" for="view-mode-expert"
>Expert</label
>
</div>
</div>
<nav class="h-100">
<nav v-if="props.schema" class="nav">
<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)"
>
<font-awesome-icon
:icon="group.icon"
v-if="group.icon"
class="me-2"
/>
{{ group.title }}
</router-link>
</li>
<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>
</ul>
</nav>
<!-- 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>
</nav>
<div class="d-grid gap-2 mb-2">
<button
type="submit"
form="launchWorkflowForm"
class="btn btn-success btn-lg mx-2"
:disabled="props.loading || !props.schema"
>
<font-awesome-icon icon="fa-solid fa-play" class="me-2" />
Launch
</button>
</div>
</div>
</div>
</template>
<style scoped>
div.card-body {
backdrop-filter: brightness(1.2);
}
</style>