Skip to content
Snippets Groups Projects
ParameterSchemaFormComponent.vue 17.9 KiB
Newer Older
  • Learn to ignore specific revisions
  • <script setup lang="ts">
    
    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";
    
    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";
    
    import UploadParameterFileModal from "@/components/parameter-schema/UploadParameterFileModal.vue";
    import type {
      TemporaryParams,
    
      WorkflowMetaParameters,
    
      WorkflowParameters,
    } from "@/types/WorkflowParameters";
    import { useWorkflowExecutionStore } from "@/stores/workflowExecutions";
    
    
    const bucketRepository = useBucketStore();
    
    const resourceRepository = useResourceStore();
    const keyRepository = useS3KeyStore();
    
    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",
    
        metaParameters: WorkflowMetaParameters,
    
    // Bootstrap Elements
    // =============================================================================
    let errorToast: Toast | null = null;
    
    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<{
    
      validated: boolean;
    
      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"],
        };
    
        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 groupedParameters = Object.keys(parameterGroups.value).map(
        (groupName) =>
          Object.fromEntries(
            Object.entries(parameterGroups.value[groupName]["properties"]).map(
              ([parameterName, parameter]) => [
                parameterName,
                // @ts-ignore
                parameter["default"],
              ],
            ),
    
      formState.formInput = groupedParameters.reduce((acc, val) => {
    
        return { ...acc, ...val };
      });
      loadParameters(executionRepository.popTemporaryParameters());
    
    function startWorkflow() {
    
      errorToast?.hide();
    
      formState.validated = true;
    
      formState.errorType = undefined;
    
      // 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()) {
    
        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();
    
    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;
    
        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");
    
      parameterLoadToast = new Toast("#workflowExecutionParameterLoadToast");
    
      <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>
    
      <upload-parameter-file-modal
        modal-id="parameterUploadModal"
        @parameters-uploaded="
          (params: WorkflowParameters) =>
            loadParameters({
              params: params,
              metaParams: {},
            })
        "
      />
    
      <div class="row align-items-start">
    
          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
    
              v-model="formState.formInput"
    
              :parameter-group-name="groupName"
              :parameter-group="group"
    
              :showHidden="showHidden"
              :show-optional="showOptional"
    
              :resource-parameters="props.clowmInfo?.resourceParameters"
    
            <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
                >
    
                <div class="input-group">
    
                  <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>
    
            </div>
          </div>
        </form>
    
        <!-- Loading card -->
        <div v-else class="col-9">
    
            <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"
    
                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>
    
          </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>
    
                  <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
    
            <!-- 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>
    
          <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"
    
              :disabled="props.loading || !props.schema"
            >
              <font-awesome-icon icon="fa-solid fa-play" class="me-2" />
              Launch
            </button>
          </div>
    
    <style>
    .was-validated *:invalid {
      border-color: var(--bs-form-invalid-border-color) !important;
      background: var(--bs-danger-bg-subtle) !important;