From 26f95683e79bf97953053154eac9894bb5c97512 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20G=C3=B6bel?= <dgoebel@techfak.uni-bielefeld.de> Date: Thu, 21 Mar 2024 14:54:44 +0100 Subject: [PATCH] Add button to delete a parameter extension #105 --- src/App.vue | 48 +++-- .../services/WorkflowVersionService.ts | 33 +++- src/stores/workflows.ts | 46 ++++- .../CreateParameterTranslationView.vue | 173 ++++++++++++++---- 4 files changed, 253 insertions(+), 47 deletions(-) diff --git a/src/App.vue b/src/App.vue index 0ba8e44..72b4a33 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,5 +1,5 @@ <script setup lang="ts"> -import { onBeforeMount } from "vue"; +import { onBeforeMount, onMounted } from "vue"; import { useCookies } from "vue3-cookies"; import { useAuthStore } from "@/stores/users"; import { useRoute, useRouter } from "vue-router"; @@ -12,12 +12,20 @@ import axios from "axios"; import { useNameStore } from "@/stores/names"; import AppHeader from "@/components/AppHeader.vue"; import AppFooter from "@/components/AppFooter.vue"; +import { useResourceStore } from "@/stores/resources"; +import { useWorkflowStore } from "@/stores/workflows"; +import { useBucketStore } from "@/stores/buckets"; +import {useS3KeyStore} from "@/stores/s3keys"; const { cookies } = useCookies(); -const store = useAuthStore(); const router = useRouter(); const route = useRoute(); +const userRepository = useAuthStore(); const nameRepository = useNameStore(); +const resourceRepository = useResourceStore(); +const workflowRepository = useWorkflowStore(); +const bucketRepository = useBucketStore(); +const s3KeyRepository = useS3KeyStore(); onBeforeMount(() => { S3ProxyOpenAPI.BASE = environment.S3PROXY_API_BASE_URL; @@ -31,7 +39,7 @@ onBeforeMount(() => { (err.response.status === 400 || err.response.status === 403) && err.response.data.detail?.includes("JWT") ) { - store.logout(); + userRepository.logout(); cookies.remove("bearer"); router.push({ name: "login", @@ -46,15 +54,15 @@ onBeforeMount(() => { return Promise.reject(err); }, ); - store.setToken(cookies.get("bearer")); + userRepository.setToken(cookies.get("bearer")); router.afterEach((to, from) => { window._paq.push(["setReferrerUrl", from.path]); window._paq.push(["deleteCustomVariables", "page"]); window._paq.push(["deleteCustomDimension", 1]); window._paq.push(["setCustomUrl", to.path]); window._paq.push(["setDocumentTitle", to.name]); - if (store.currentUID.length > 0) { - window._paq.push(["setUserId", store.currentUID]); + if (userRepository.currentUID.length > 0) { + window._paq.push(["setUserId", userRepository.currentUID]); } window._paq.push(["trackPageView"]); window._paq.push(["enableLinkTracking"]); @@ -62,7 +70,7 @@ onBeforeMount(() => { router.beforeEach(async (to) => { // make sure the user is authenticated if ( - !store.authenticated && + !userRepository.authenticated && // â—ï¸ Avoid an infinite redirect to.name !== "login" ) { @@ -73,20 +81,20 @@ onBeforeMount(() => { }; } else if ( to.meta.requiresDeveloperRole && - !(store.workflowDev || store.admin) + !(userRepository.workflowDev || userRepository.admin) ) { return { name: "dashboard" }; } else if ( to.meta.requiresReviewerRole && - !(store.rewiewer || store.admin) + !(userRepository.rewiewer || userRepository.admin) ) { return { name: "dashboard" }; } else if ( to.meta.requiresMaintainerRole && - !(store.resourceMaintainer || store.admin) + !(userRepository.resourceMaintainer || userRepository.admin) ) { return { name: "dashboard" }; - } else if (to.meta.adminRole && !store.admin) { + } else if (to.meta.adminRole && !userRepository.admin) { return { name: "dashboard" }; } else if (to.name !== "login" && to.query.return_path) { // return to original path after login @@ -95,6 +103,24 @@ onBeforeMount(() => { }); nameRepository.loadNameMapping(); }); + +onMounted(() => { + if (userRepository.authenticated) { + resourceRepository.fetchPublicResources(); + workflowRepository.fetchWorkflows(); + bucketRepository.fetchBuckets(); + s3KeyRepository.fetchS3Keys(); + if (!userRepository.foreignUser) { + bucketRepository.fetchOwnPermissions(); + } + if (userRepository.workflowDev || userRepository.admin) { + workflowRepository.fetchOwnWorkflows(); + } + if (userRepository.resourceMaintainer || userRepository.admin) { + resourceRepository.fetchOwnResources(); + } + } +}); </script> <template> diff --git a/src/client/workflow/services/WorkflowVersionService.ts b/src/client/workflow/services/WorkflowVersionService.ts index b247d41..289e210 100644 --- a/src/client/workflow/services/WorkflowVersionService.ts +++ b/src/client/workflow/services/WorkflowVersionService.ts @@ -142,7 +142,7 @@ export class WorkflowVersionService { }); } /** - * Deprecate a workflow version + * Update parameter extension of workflow version * Update the parameter extension of a workflow version. * * @@ -176,6 +176,37 @@ export class WorkflowVersionService { }, }); } + /** + * Delete parameter extension of workflow version + * Delete the parameter extension of a workflow version. + * + * + * Permission `workflow:update` required. + * @param wid ID of a workflow + * @param gitCommitHash Git commit git_commit_hash of specific version. + * @returns void + * @throws ApiError + */ + public static workflowVersionDeleteWorkflowVersionParameterExtension( + wid: string, + gitCommitHash: string, + ): CancelablePromise<void> { + return __request(OpenAPI, { + method: 'DELETE', + url: '/workflows/{wid}/versions/{git_commit_hash}/parameter-extension', + path: { + 'wid': wid, + 'git_commit_hash': gitCommitHash, + }, + errors: { + 400: `Error decoding JWT Token`, + 401: `Not authenticated`, + 403: `Not authorized`, + 404: `Entity not Found`, + 422: `Validation Error`, + }, + }); + } /** * Fetch documentation for a workflow version * Get the documentation for a specific workflow version. diff --git a/src/stores/workflows.ts b/src/stores/workflows.ts index 8f1b70c..b00d127 100644 --- a/src/stores/workflows.ts +++ b/src/stores/workflows.ts @@ -555,8 +555,7 @@ export const useWorkflowStore = defineStore({ (version) => version.workflow_version_id == version_id, ); if (versionIndex1 > -1) { - this.workflowMapping[workflow_id].versions[versionIndex1].icon_url = - null; + this.workflowMapping[workflow_id].versions[versionIndex1] = version; } } @@ -572,11 +571,52 @@ export const useWorkflowStore = defineStore({ if (versionIndex2 > -1) { this.comprehensiveWorkflowMapping[workflow_id].versions[ versionIndex2 - ].icon_url = null; + ] = version; } } return version; }); }, + deleteWorkflowExtension( + workflow_id: string, + version_id: string, + ): Promise<void> { + return WorkflowVersionService.workflowVersionDeleteWorkflowVersionParameterExtension( + workflow_id, + version_id, + ).then(() => { + // Update version in workflowMapping + if (this.workflowMapping[workflow_id] == undefined) { + this.fetchWorkflow(workflow_id, false); + } else { + const versionIndex1 = this.workflowMapping[ + workflow_id + ].versions.findIndex( + (version) => version.workflow_version_id == version_id, + ); + if (versionIndex1 > -1) { + this.workflowMapping[workflow_id].versions[ + versionIndex1 + ].parameter_extension = undefined; + } + } + + // Update version in comprehensiveWorkflowMapping + if (this.comprehensiveWorkflowMapping[workflow_id] == undefined) { + this.fetchWorkflow(workflow_id, true); + } else { + const versionIndex2 = this.comprehensiveWorkflowMapping[ + workflow_id + ].versions.findIndex( + (version) => version.workflow_version_id == version_id, + ); + if (versionIndex2 > -1) { + this.comprehensiveWorkflowMapping[workflow_id].versions[ + versionIndex2 + ].parameter_extension = undefined; + } + } + }); + }, }, }); diff --git a/src/views/workflows/CreateParameterTranslationView.vue b/src/views/workflows/CreateParameterTranslationView.vue index 7e5ee61..a2db38a 100644 --- a/src/views/workflows/CreateParameterTranslationView.vue +++ b/src/views/workflows/CreateParameterTranslationView.vue @@ -12,6 +12,7 @@ import { useResourceStore } from "@/stores/resources"; 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 // ============================================================================= @@ -24,6 +25,7 @@ const props = defineProps<{ // ============================================================================= const parameterExtensionForm = ref<HTMLFormElement | null>(null); let successToast: Toast | null = null; +let deleteToast: Toast | null = null; // Repositories // ============================================================================= @@ -35,6 +37,7 @@ const resourceRepository = useResourceStore(); // ============================================================================= const parameterState = reactive<{ loading: boolean; + makingRequest: boolean; extension: ParameterExtension_Input; resourceParametersDefault: Set<string>; resourceParametersMapping: Set<string>; @@ -42,6 +45,7 @@ const parameterState = reactive<{ formValidated: boolean; }>({ loading: true, + makingRequest: false, extension: {}, resourceParametersDefault: new Set(), resourceParametersMapping: new Set(), @@ -102,7 +106,11 @@ function updateParameterPools(newVal?: object) { if (newVal) { const parameters = extractParameterList(newVal); parameterPools.defaults = parameters.slice(); - parameterPools.mapping = parameters.slice(); + parameterPools.mapping = parameters.filter( + (param) => + parameterSchema.value?.[param]?.["type"] !== "boolean" && + !parameterSchema.value?.[param]?.["enum"], + ); } if ( workflowRepository.ownVersionMapping[props.versionId]?.parameter_extension @@ -137,6 +145,7 @@ function submitForm() { } parameterState.formValidated = true; if (parameterExtensionForm.value?.checkValidity()) { + parameterState.makingRequest = true; workflowRepository .updateWorkflowExtension( props.workflowId, @@ -145,6 +154,9 @@ function submitForm() { ) .then(() => { successToast?.show(); + }) + .finally(() => { + parameterState.makingRequest = false; }); } } @@ -222,6 +234,17 @@ function makeResourceParameterDefault(param: string) { }; } +function makeResourceParameterMapping(param: string, val: string) { + if (parameterState.extension.mapping?.[param]?.[val]) { + parameterState.formValidated = false; + parameterState.resourceParametersMapping.add(param); + parameterState.extension.mapping[param][val] = { + resource_id: "", + resource_version_id: "", + }; + } +} + function deleteDefaultParameter(param: string) { if ( !workflowRepository.documentationFiles[ @@ -284,14 +307,47 @@ function deleteMappingParameter(param: string) { } } +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"); + 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, @@ -318,13 +374,13 @@ onMounted(() => { ); }); }); - resourceRepository.fetchPublicResources(); + }); </script> <template> <bootstrap-toast - toast-id="save-parameter-extension-success" + toast-id="save-parameter-extension-success-toast" color-class="success" > <template #default>Successfully saved Parameter Extension</template> @@ -351,13 +407,43 @@ onMounted(() => { </div> </template> </bootstrap-toast> - <div class="row border-bottom mb-4"> - <h2 class="mb-2"> + <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"> @@ -446,7 +532,7 @@ onMounted(() => { 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">Workflow parameters:</b> + <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" @@ -479,25 +565,27 @@ onMounted(() => { Remove </button> </div> - <div class="d-flex mb-4"> - <button - type="button" - class="btn btn-primary me-2" - :disabled=" - parameterState.mappingParameterValues[param]?.length === 0 - " - @click=" - addMappingParameterValue( - param, - parameterState.mappingParameterValues[param], - ) - " - > - Add - </button> + <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" + class="form-control flex-fill w-fit" v-model="parameterState.mappingParameterValues[param]" /> </div> @@ -505,15 +593,21 @@ onMounted(() => { <div v-for="key in Object.keys(parameterState.extension.mapping[param])" :key="key" - class="mb-2" + class="mb-5 position-relative" > - <div class="p-0 d-flex justify-content-between mb-n1"> - <code - class="p-1 rounded-top border-bottom-0 border border-secondary-subtle" - >{{ key }}</code + <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="px-1 pb-1 rounded-top border-bottom-0 border pseudo-danger-btn border-danger-subtle cursor-pointer" + class="p-1 rounded-top border-bottom-0 border pseudo-danger-btn border-danger-subtle cursor-pointer" @click="deleteMappingParameterValue(param, key)" >Remove</span > @@ -527,6 +621,9 @@ onMounted(() => { size-modifier="sm" border="secondary-subtle" v-model="parameterState.extension.mapping[param][key]" + :resource-parameter=" + parameterState.resourceParametersMapping.has(param) + " /> </div> </div> @@ -541,6 +638,7 @@ onMounted(() => { form="parameter-extension-form" :disabled=" parameterState.loading || + parameterState.makingRequest || Object.keys(parameterState.extension).length === 0 " > @@ -565,7 +663,18 @@ onMounted(() => { border-color: var(--bs-danger); } -.mb-n1 { - margin-bottom: -0.25rem !important; +.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> -- GitLab