Skip to content
Snippets Groups Projects
CreateParameterTranslationView.vue 20.27 KiB
<script setup lang="ts">
import { useNameStore } from "@/stores/names";
import { useWorkflowStore } from "@/stores/workflows";
import { computed, onMounted, reactive, ref, watch } from "vue";
import { DocumentationEnum, type ParameterExtension } from "@/client/workflow";
import type { ClowmInfo } from "@/types/ClowmInfo";
import ParameterInput from "@/components/parameter-schema/form-mode/ParameterInput.vue";
import BootstrapToast from "@/components/BootstrapToast.vue";
import { Toast } from "bootstrap";
import DeleteModal from "@/components/modals/DeleteModal.vue";

// Props
// =============================================================================
const props = defineProps<{
  versionId: string;
  workflowId: string;
}>();

// HTML refs
// =============================================================================
const parameterExtensionForm = ref<HTMLFormElement | null>(null);
let successToast: Toast | null = null;
let deleteToast: Toast | null = null;

// Repositories
// =============================================================================
const nameRepository = useNameStore();
const workflowRepository = useWorkflowStore();

// Reactive State
// =============================================================================
const parameterState = reactive<{
  loading: boolean;
  makingRequest: boolean;
  extension: ParameterExtension;
  resourceParametersDefault: Set<string>;
  resourceParametersMapping: Set<string>;
  mappingParameterValues: Record<string, string>;
  formValidated: boolean;
}>({
  loading: true,
  makingRequest: false,
  extension: {},
  resourceParametersDefault: new Set(),
  resourceParametersMapping: new Set(),
  mappingParameterValues: {},
  formValidated: false,
});

const parameterPools = reactive<{
  defaults: string[];
  mapping: string[];
}>({
  defaults: [],
  mapping: [],
});

// Watchers
// =============================================================================
watch(
  () =>
    workflowRepository.documentationFiles[props.versionId]?.parameter_schema,
  (newVal, old) => {
    if (newVal != old && newVal) {
      updateParameterPools(newVal);
    }
  },
);

watch(
  () => workflowRepository.documentationFiles[props.versionId]?.clowm_info,
  (newVal, old) => {
    if (newVal != old && newVal) {
      updateResourceParameters(newVal);
    }
  },
);

// Computed States
// =============================================================================
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const parameterSchema = computed<Record<string, Record<string, any>>>(() => {
  const schema =
    workflowRepository.documentationFiles[props.versionId ?? ""]
      ?.parameter_schema;
  const a = schema?.["properties"] ?? {};
  for (const group in schema?.["definitions"] ?? {}) {
    for (const param in schema?.["definitions"]?.[group]?.["properties"] ??
      {}) {
      a[param] = schema["definitions"][group]["properties"][param];
    }
  }
  return a;
});

// Functions
// =============================================================================
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function updateParameterPools(newVal?: object) {
  if (newVal) {
    const parameters = extractParameterList(newVal);
    parameterPools.defaults = parameters.slice();
    parameterPools.mapping = parameters.filter(
      (param) =>
        parameterSchema.value?.[param]?.["type"] !== "boolean" &&
        !parameterSchema.value?.[param]?.["enum"],
    );
  }
  if (
    workflowRepository.ownVersionMapping[props.versionId]?.parameter_extension
  ) {
    parameterPools.defaults = parameterPools.defaults.filter(
      (param) =>
        workflowRepository.ownVersionMapping[props.versionId]
          ?.parameter_extension?.defaults?.[param] == undefined,
    );
    parameterPools.mapping = parameterPools.mapping.filter(
      (param) =>
        workflowRepository.ownVersionMapping[props.versionId]
          ?.parameter_extension?.defaults?.[param] == undefined,
    );
  }
}

function updateResourceParameters(newVal?: ClowmInfo) {
  newVal?.resourceParameters?.forEach((param) => {
    parameterState.resourceParametersDefault.add(param);
    parameterState.resourceParametersMapping.add(param);
  });
}

function submitForm() {
  if (parameterState.extension?.mapping) {
    for (const key of Object.keys(parameterState.extension.mapping)) {
      if (Object.keys(parameterState.extension?.mapping[key]).length === 0) {
        delete parameterState.extension?.mapping[key];
        parameterPools.mapping.push(key);
      }
    }
  }
  parameterState.formValidated = true;
  if (parameterExtensionForm.value?.checkValidity()) {
    parameterState.makingRequest = true;
    workflowRepository
      .updateWorkflowExtension(
        props.workflowId,
        props.versionId,
        parameterState.extension,
      )
      .then(() => {
        successToast?.show();
      })
      .finally(() => {
        parameterState.makingRequest = false;
      });
  }
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function extractParameterList(schema: Record<string, any>): string[] {
  const groupedParameters = Object.keys(schema["definitions"] ?? {}).reduce(
    (acc: string[], val) => [
      ...acc,
      ...Object.keys(schema["definitions"][val]["properties"]),
    ],
    [],
  );
  const singleParameters = Object.keys(schema["properties"] ?? {});
  return [...groupedParameters, ...singleParameters];
}

function getParameterType(param: string): string | undefined {
  return parameterSchema.value[param]?.["type"];
}

function getParameterSchemaDefault(
  param: string,
): string | boolean | number | undefined {
  return parameterSchema.value[param]?.["default"];
}

function getParamDefault(param: string): string | boolean | number {
  switch (getParameterType(param)) {
    case "integer": {
      return getParameterSchemaDefault(param) ?? 0;
    }
    case "number": {
      return getParameterSchemaDefault(param) ?? 0;
    }
    case "boolean": {
      return getParameterSchemaDefault(param) ?? true;
    }
    case "string": {
      if (parameterState.resourceParametersDefault.has(param)) {
        return "";
      }
      return (
        getParameterSchemaDefault(param) ??
        parameterSchema.value[param]?.["enum"]?.[0] ??
        ""
      );
    }
    default: {
      return "";
    }
  }
}

function addDefaultParameter(param: string, index: number) {
  parameterState.formValidated = false;
  if (parameterState.extension.defaults == undefined) {
    parameterState.extension.defaults = {};
  }
  parameterPools.defaults.splice(index, 1);
  parameterState.extension.defaults[param] = getParamDefault(param);
}

function makeResourceParameterDefault(param: string) {
  parameterState.formValidated = false;
  parameterState.resourceParametersDefault.add(param);
  parameterState.extension.defaults![param] = "";
}

function makeResourceParameterMapping(param: string, val: string) {
  if (parameterState.extension.mapping?.[param]?.[val]) {
    parameterState.formValidated = false;
    parameterState.resourceParametersMapping.add(param);
    parameterState.extension.mapping[param][val] = "";
  }
}

function deleteDefaultParameter(param: string) {
  if (
    !workflowRepository.documentationFiles[
      props.versionId
    ]?.clowm_info?.resourceParameters?.includes(param)
  ) {
    parameterState.resourceParametersDefault.delete(param);
  }
  delete parameterState.extension.defaults?.[param];
  parameterPools.defaults.push(param);
  if (Object.keys(parameterState.extension.defaults ?? {}).length === 0) {
    parameterState.extension.defaults = undefined;
  }
}

function addMappingParameter(param: string, index: number) {
  parameterState.formValidated = false;
  if (parameterState.extension.mapping == undefined) {
    parameterState.extension.mapping = {};
  }
  if (parameterState.extension.mapping[param] == undefined) {
    parameterState.extension.mapping[param] = {};
  }
  parameterState.mappingParameterValues[param] = "";
  if (index > -1) {
    parameterPools.mapping.splice(index, 1);
  }
}

function addMappingParameterValue(param: string, val: string) {
  parameterState.formValidated = false;
  if (parameterState.extension.mapping?.[param] != undefined) {
    parameterState.extension.mapping[param][val] = getParamDefault(param) as
      | string
      | number;
    parameterState.mappingParameterValues[param] = "";
  }
}

function deleteMappingParameterValue(param: string, val: string) {
  if (parameterState.extension.mapping?.[param]?.[val] != undefined) {
    delete parameterState.extension.mapping[param][val];
  }
}

function deleteMappingParameter(param: string) {
  if (
    !workflowRepository.documentationFiles[
      props.versionId
    ]?.clowm_info?.resourceParameters?.includes(param)
  ) {
    parameterState.resourceParametersMapping.delete(param);
  }
  delete parameterState.extension.mapping?.[param];
  delete parameterState.mappingParameterValues[param];
  parameterPools.mapping.push(param);
  if (Object.keys(parameterState.extension.mapping ?? {}).length === 0) {
    parameterState.extension.mapping = undefined;
  }
}

function deleteParameterExtension() {
  parameterState.makingRequest = true;
  workflowRepository
    .deleteWorkflowExtension(props.workflowId, props.versionId)
    .then(() => {
      parameterState.extension = {};
      updateParameterPools(
        workflowRepository.documentationFiles[props.versionId]
          ?.parameter_schema,
      );
      updateResourceParameters(
        workflowRepository.documentationFiles[props.versionId]?.clowm_info,
      );
      deleteToast?.show();
    })
    .finally(() => {
      parameterState.makingRequest = false;
    });
}

// Lifecycle Events
// =============================================================================
onMounted(() => {
  successToast = new Toast("#save-parameter-extension-success-toast");
  deleteToast = new Toast("#delete-parameter-extension-success-toast");
  workflowRepository.fetchWorkflow(props.workflowId, true, () => {
    parameterState.extension =
      workflowRepository.ownVersionMapping[props.versionId]
        ?.parameter_extension ?? {};
    for (const param of Object.keys(parameterState.extension?.mapping ?? {})) {
      for (const paramOption of Object.keys(
        parameterState.extension?.mapping?.[param] ?? {},
      )) {
        if (
          typeof parameterState.extension?.mapping?.[param]?.[paramOption] ===
          "object"
        ) {
          parameterState.resourceParametersMapping.add(param);
        }
      }
    }
    workflowRepository
      .fetchWorkflowDocumentation(
        props.workflowId,
        props.versionId,
        DocumentationEnum.PARAMETER_SCHEMA,
        workflowRepository.ownVersionMapping[props.versionId]?.modes?.[0],
      )
      .then(() =>
        workflowRepository.fetchWorkflowDocumentation(
          props.workflowId,
          props.versionId,
          DocumentationEnum.CLOWM_INFO,
          workflowRepository.ownVersionMapping[props.versionId]?.modes?.[0],
        ),
      )
      .finally(() => {
        parameterState.loading = false;
        updateParameterPools(
          workflowRepository.documentationFiles[props.versionId]
            ?.parameter_schema,
        );
        updateResourceParameters(
          workflowRepository.documentationFiles[props.versionId]?.clowm_info,
        );
      });
  });
});
</script>

<template>
  <bootstrap-toast
    toast-id="save-parameter-extension-success-toast"
    color-class="success"
  >
    <template #default>Successfully saved Parameter Extension</template>
    <template #body>
      <div class="d-grid gap-2">
        <router-link
          class="btn btn-info btn-sm"
          role="button"
          :to="{
            name: 'workflow-start',
            params: {
              versionId: props.versionId,
              workflowId: props.workflowId,
            },
            query: {
              workflowModeId:
                workflowRepository.ownVersionMapping[props.versionId]
                  ?.modes?.[0],
              viewMode: 'expert',
            },
          }"
          >View
        </router-link>
      </div>
    </template>
  </bootstrap-toast>
  <bootstrap-toast
    toast-id="delete-parameter-extension-success-toast"
    color-class="success"
  >
    Successfully deleted Parameter Extension
  </bootstrap-toast>
  <delete-modal
    v-if="
      workflowRepository.ownVersionMapping[props.versionId]?.parameter_extension
    "
    modal-id="delete-parameter-extension-modal"
    :object-name-delete="`parameter extension of ${nameRepository.getName(props.workflowId)}@${nameRepository.getName(props.versionId)}`"
    @confirm-delete="deleteParameterExtension"
  />
  <div class="d-flex justify-content-between border-bottom mb-4 pb-2">
    <h2 class="w-fit">
      Add parameter metadata to
      {{ nameRepository.getName(props.workflowId) }}@{{
        nameRepository.getName(props.versionId)
      }}
    </h2>
    <div
      v-if="
        workflowRepository.ownVersionMapping[props.versionId]
          ?.parameter_extension
      "
    >
      <button
        type="button"
        class="btn btn-danger"
        data-bs-toggle="modal"
        data-bs-target="#delete-parameter-extension-modal"
        :disabled="parameterState.loading || parameterState.makingRequest"
      >
        Delete
      </button>
    </div>
  </div>
  <div v-if="parameterState.loading" class="d-flex justify-content-center">
    <div class="spinner-border" role="status">
      <span class="visually-hidden">Loading...</span>
    </div>
  </div>
  <form
    v-else
    ref="parameterExtensionForm"
    id="parameter-extension-form"
    class="mb-2"
    :class="{ 'was-validated': parameterState.formValidated }"
    novalidate
    @submit.prevent="submitForm()"
  >
    <h3>CloWM instance specific default parameters</h3>
    <div
      class="d-flex flex-wrap overflow-y-auto p-1 border border-bottom-0 rounded-top border-dashed"
      style="max-height: 30vh"
    >
      <b class="ms-1 w-100">Workflow parameters:</b>
      <template v-if="parameterPools.defaults.length > 0">
        <div
          class="w-fit border px-2 rounded cursor-pointer m-1 parameter-container"
          v-for="(param, index) in parameterPools.defaults"
          :key="param"
          @click="addDefaultParameter(param, index)"
        >
          {{ param }}
        </div>
      </template>
      <div v-else class="px-2 text-secondary m-1">
        <i>Empty</i>
      </div>
    </div>
    <table class="table table-bordered align-middle">
      <thead>
        <tr>
          <th scope="col"><b>Parameter</b></th>
          <th scope="col"><b>Value</b></th>
        </tr>
      </thead>
      <tbody v-if="parameterState.extension.defaults" id="defaultParamsTable">
        <tr
          v-for="param in Object.keys(parameterState.extension.defaults)"
          :key="param"
        >
          <td style="width: 10%">{{ param }}</td>
          <td class="d-flex justify-content-between align-items-center">
            <div class="flex-fill input-group">
              <parameter-input
                :parameter="parameterSchema[param]"
                v-model="parameterState.extension.defaults[param]"
                size-modifier="sm"
                :resource-parameter="
                  parameterState.resourceParametersDefault.has(param)
                "
                force-raw-file
              />
            </div>
            <button
              v-if="
                !parameterState.resourceParametersDefault.has(param) &&
                getParameterType(param) === 'string'
              "
              class="btn btn-primary btn-sm ms-2"
              type="button"
              @click="makeResourceParameterDefault(param)"
            >
              Resource
            </button>
            <button
              type="button"
              class="btn btn-outline-danger btn-sm ms-2"
              @click="deleteDefaultParameter(param)"
            >
              Remove
            </button>
          </td>
        </tr>
      </tbody>
    </table>
    <h3>Parameter Translation</h3>
    <div
      class="d-flex flex-wrap overflow-y-auto p-1 border rounded-top border-dashed"
      style="max-height: 30vh"
    >
      <b class="ms-1 w-100">Eligible workflow parameters:</b>
      <template v-if="parameterPools.mapping.length > 0">
        <div
          class="w-fit border px-2 rounded cursor-pointer m-1 parameter-container"
          v-for="(param, index) in parameterPools.mapping"
          :key="param"
          @click="addMappingParameter(param, index)"
        >
          {{ param }}
        </div>
      </template>
      <div v-else class="px-2 text-secondary m-1">
        <i>Empty</i>
      </div>
    </div>
    <div v-if="parameterState.extension.mapping" class="p-0">
      <div
        v-for="param in Object.keys(parameterState.extension.mapping)"
        :key="param"
        class="p-2 border border-top-0"
      >
        <div class="d-flex justify-content-between mb-2">
          <code class="fs-6 fw-bold bg-secondary-subtle rounded p-1"
            >--{{ param }}</code
          >
          <button
            type="button"
            class="btn btn-outline-danger btn-sm"
            @click="deleteMappingParameter(param)"
          >
            Remove
          </button>
        </div>
        <div class="d-flex mb-5">
          <div class="me-2">
            <button
              type="button"
              class="btn btn-primary"
              :disabled="
                parameterState.mappingParameterValues[param]?.length === 0
              "
              @click="
                addMappingParameterValue(
                  param,
                  parameterState.mappingParameterValues[param],
                )
              "
            >
              Add Option
            </button>
          </div>
          <input
            type="text"
            class="form-control flex-fill w-fit"
            v-model="parameterState.mappingParameterValues[param]"
          />
        </div>
        <template v-if="parameterState.extension.mapping[param]">
          <div
            v-for="key in Object.keys(parameterState.extension.mapping[param])"
            :key="key"
            class="mb-5 position-relative"
          >
            <code
              class="p-1 position-absolute top-0 start-0 pt-0 rounded-top border-bottom-0 border border-secondary-subtle bla"
              >{{ key }}</code
            >
            <div class="position-absolute top-0 end-0 bla">
              <span
                v-if="!parameterState.resourceParametersMapping.has(param)"
                class="p-1 me-2 rounded-top border-bottom-0 border pseudo-primary-btn border-primary-subtle cursor-pointer"
                @click="makeResourceParameterMapping(param, key)"
                >Resource</span
              >
              <span
                class="p-1 rounded-top border-bottom-0 border pseudo-danger-btn border-danger-subtle cursor-pointer"
                @click="deleteMappingParameterValue(param, key)"
                >Remove</span
              >
            </div>
            <div class="input-group">
              <parameter-input
                :parameter="parameterSchema[param]"
                force-raw-file
                required
                size-modifier="sm"
                border="secondary-subtle"
                v-model="parameterState.extension.mapping[param][key]"
                :resource-parameter="
                  parameterState.resourceParametersMapping.has(param)
                "
              />
            </div>
          </div>
        </template>
      </div>
    </div>
  </form>
  <div class="d-grid gap-2">
    <button
      type="submit"
      class="btn btn-success btn-lh mt-3"
      form="parameter-extension-form"
      :disabled="
        parameterState.loading ||
        parameterState.makingRequest ||
        Object.keys(parameterState.extension).length === 0
      "
    >
      Save
    </button>
  </div>
</template>

<style scoped>
.parameter-container:hover {
  background: var(--bs-secondary-bg-subtle);
}

.pseudo-danger-btn {
  color: var(--bs-danger);
  background-color: var(--bs-white);
}

.pseudo-danger-btn:hover {
  color: var(--bs-white);
  background-color: var(--bs-danger);
  border-color: var(--bs-danger);
}

.pseudo-primary-btn {
  color: var(--bs-primary);
  background-color: var(--bs-white);
}

.pseudo-primary-btn:hover {
  color: var(--bs-white);
  background-color: var(--bs-primary);
  border-color: var(--bs-primary);
}

.bla {
  transform: translateY(-90%) !important;
}
</style>