-
Daniel Göbel authored
#105
Daniel Göbel authored#105
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>