From bc3355d745a6acbbf4e43fc99a0a495008093dd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20G=C3=B6bel?= <dgoebel@techfak.uni-bielefeld.de> Date: Tue, 27 Aug 2024 13:57:56 +0000 Subject: [PATCH] Add UI to manage OTRS for workflows #145 --- src/client/services/WorkflowService.ts | 2 +- src/components/modals/ShowOtrModal.vue | 8 +- .../workflows/WorkflowWithVersionsCard.vue | 47 ++++---- .../workflows/modals/CreateWorkflowModal.vue | 19 ++-- src/stores/otrs.ts | 42 ++++--- src/stores/workflows.ts | 18 +-- src/views/object-storage/BucketsView.vue | 6 +- src/views/workflows/MyWorkflowsView.vue | 103 ++++++++++++++++-- 8 files changed, 180 insertions(+), 65 deletions(-) diff --git a/src/client/services/WorkflowService.ts b/src/client/services/WorkflowService.ts index 05e48cb..f1cc9bf 100644 --- a/src/client/services/WorkflowService.ts +++ b/src/client/services/WorkflowService.ts @@ -245,7 +245,7 @@ export class WorkflowService { * @returns OwnershipTransferRequestOut Successful Response * @throws ApiError */ - public static workflowCreateBucketOtr( + public static workflowCreateWorkflowOtr( wid: string, requestBody: OwnershipTransferRequestIn, ): CancelablePromise<OwnershipTransferRequestOut> { diff --git a/src/components/modals/ShowOtrModal.vue b/src/components/modals/ShowOtrModal.vue index ec34501..478a51d 100644 --- a/src/components/modals/ShowOtrModal.vue +++ b/src/components/modals/ShowOtrModal.vue @@ -65,13 +65,9 @@ onMounted(() => { color-class="success" > <template #default v-if="accepted"> - Successfully accepted request for {{ otr?.target_type }} - {{ otr?.target_name }} - </template> - <template #default v-else> - Successfully deleted request for {{ otr?.target_type }} - {{ otr?.target_name }} + Successfully accepted request </template> + <template #default v-else> Successfully deleted request </template> </bootstrap-toast> <delete-modal :modal-id="`delete-otr-modal-${randomIDSuffix}`" diff --git a/src/components/workflows/WorkflowWithVersionsCard.vue b/src/components/workflows/WorkflowWithVersionsCard.vue index 0e76c9d..4c5366c 100644 --- a/src/components/workflows/WorkflowWithVersionsCard.vue +++ b/src/components/workflows/WorkflowWithVersionsCard.vue @@ -5,14 +5,17 @@ import type { WorkflowVersion, } from "@/client"; import { WorkflowVersionStatus, WorkflowExecutionStatus } from "@/client"; -import { computed, onMounted, ref, watch } from "vue"; +import { computed, onMounted, ref } from "vue"; import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue"; import dayjs from "dayjs"; import { sortedVersions } from "@/utils/Workflow"; import { Tooltip } from "bootstrap"; import { useWorkflowExecutionStore } from "@/stores/workflowExecutions"; +import { useOTRStore } from "@/stores/otrs"; const workflowExecutionRepository = useWorkflowExecutionStore(); +const otrRepository = useOTRStore(); + const props = defineProps<{ workflow: WorkflowOut; loading: boolean; @@ -25,17 +28,10 @@ const emit = defineEmits<{ (e: "workflow-delete-click", workflow: WorkflowOut): void; (e: "workflow-update-credentials-click", workflow: WorkflowOut): void; (e: "workflow-update-icon-click", version: WorkflowVersion): void; + (e: "show-otr-click", workflowId: string): void; + (e: "create-otr-click", workflow: WorkflowOut): void; }>(); -watch( - () => props.loading, - (loading) => { - if (!loading && props.workflow.private) { - new Tooltip(`#tooltip-${randomIDSuffix}`); - } - }, -); - const workflowExecutions = computed<AnonymizedWorkflowExecution[]>(() => workflowExecutionRepository.anonymizedExecutions.filter( (execution) => execution.workflow_id == props.workflow.workflow_id, @@ -89,9 +85,6 @@ function uniqueUsers(executions: AnonymizedWorkflowExecution[]): number { onMounted(() => { if (!props.loading) { - if (props.workflow.private) { - new Tooltip(`#tooltip-${randomIDSuffix}`); - } document .querySelector("#workflow-card-" + randomIDSuffix) ?.querySelectorAll('[data-bs-toggle="tooltip"]') @@ -120,9 +113,14 @@ onMounted(() => { v-if="props.workflow.private" icon="fa-solid fa-lock" class="fs-5 ms-2 tooltip-private-repository" - :id="'tooltip-' + randomIDSuffix" - data-bs-toggle="tooltip" - data-bs-title="Private Git repository" + tooltip="Private Git repository" + /> + <font-awesome-icon + v-if="otrRepository.otrMapping[workflow.workflow_id]" + icon="fa-solid fa-people-arrows" + class="fs-5 ms-2 cursor-pointer hover-info" + tooltip="Ownership transfer requested" + @click="emit('show-otr-click', workflow.workflow_id)" /> </div> <div class="btn-group"> @@ -130,7 +128,7 @@ onMounted(() => { type="button" class="btn btn-outline-success" :class="{ disabled: props.loading }" - @click="emit('workflow-update-click', props.workflow)" + @click="emit('workflow-update-click', workflow)" data-bs-toggle="modal" data-bs-target="#updateWorkflowModal" > @@ -145,6 +143,17 @@ onMounted(() => { <span class="visually-hidden">Toggle Dropdown</span> </button> <ul class="dropdown-menu z-3"> + <li + v-if="otrRepository.otrMapping[workflow.workflow_id] == undefined" + > + <button + type="button" + class="dropdown-item" + @click="emit('create-otr-click', workflow)" + > + Transfer ownership + </button> + </li> <li> <a class="dropdown-item" @@ -344,8 +353,8 @@ onMounted(() => { }" > <template v-if="version.parameter_extension" - >Update</template - > + >Update + </template> <template v-else>Add</template> Parameter Translation </router-link> diff --git a/src/components/workflows/modals/CreateWorkflowModal.vue b/src/components/workflows/modals/CreateWorkflowModal.vue index a31a402..d06cf4f 100644 --- a/src/components/workflows/modals/CreateWorkflowModal.vue +++ b/src/components/workflows/modals/CreateWorkflowModal.vue @@ -335,9 +335,12 @@ function removeMode(index: number) { onMounted(() => { createWorkflowModal = new Modal("#" + props.modalId); successToast = new Toast("#successToast-" + randomIDSuffix); - privateRepositoryCollapse = new Collapse("#privateRepositoryCollapse", { - toggle: false, - }); + privateRepositoryCollapse = new Collapse( + "#createWorkflowPrivateRepositoryCheckbox", + { + toggle: false, + }, + ); tokenHelpCollapse = new Collapse("#tokenHelpCollapse", { toggle: false, }); @@ -443,7 +446,7 @@ onMounted(() => { </div> <div class="row mb-3"> <div class="col-8"> - <label for="workflowGitCommitInput" class="form-label" + <label for="createWorkflowGitCommitInput" class="form-label" >Git Commit Hash</label > <div class="input-group"> @@ -453,7 +456,7 @@ onMounted(() => { <input type="text" class="form-control text-lowercase" - id="workflowGitCommitInput" + id="createWorkflowGitCommitInput" placeholder="ba8bcd9..." required ref="workflowGitCommitHashElement" @@ -549,15 +552,15 @@ onMounted(() => { class="form-check-input" type="checkbox" v-model="repositoryCredentials.privateRepo" - id="privateRepositoryCheckbox" + id="createWorkflowPrivateRepositoryCheckbox" @change="formState.allowUpload = false" - aria-controls="#privateRepositoryCollapse" + aria-controls="#createWorkflowPrivateRepositoryCheckbox" /> <label class="form-check-label" for="privateRepositoryCheckbox"> Enable private Git Repository </label> </div> - <div class="collapse" id="privateRepositoryCollapse"> + <div class="collapse" id="createWorkflowPrivateRepositoryCheckbox"> <label for="createRepositoryTokenInput" class="form-label" >Token</label > diff --git a/src/stores/otrs.ts b/src/stores/otrs.ts index c7b1586..0b24961 100644 --- a/src/stores/otrs.ts +++ b/src/stores/otrs.ts @@ -9,6 +9,7 @@ import { } from "@/client"; import { useUserStore } from "@/stores/users"; import { useBucketStore } from "@/stores/buckets"; +import { useWorkflowStore } from "@/stores/workflows"; export const useOTRStore = defineStore({ id: "otrs", @@ -49,26 +50,41 @@ export const useOTRStore = defineStore({ } } }, - fetchBucketOtrs( + fetchOtrs( + targetType: OwnershipTypeEnum, currentOwnerId?: string, newOwnerId?: string, ): Promise<OwnershipTransferRequestOut[]> { - return BucketService.bucketListBucketOtrs(currentOwnerId, newOwnerId); + switch (targetType) { + case OwnershipTypeEnum.BUCKET: + return BucketService.bucketListBucketOtrs(currentOwnerId, newOwnerId); + case OwnershipTypeEnum.WORKFLOW: + return WorkflowService.workflowListWorkflowOtrs( + currentOwnerId, + newOwnerId, + ); + case OwnershipTypeEnum.RESOURCE: + return ResourceService.resourceListResourceOtrs( + currentOwnerId, + newOwnerId, + ); + } }, - fetchOwnBucketOtrs( + fetchOwnOtrs( + targetType: OwnershipTypeEnum, onFinally?: () => void, ): Promise<OwnershipTransferRequestOut[]> { - if (this.__otrLoaded[OwnershipTypeEnum.BUCKET]) { + if (this.__otrLoaded[targetType]) { onFinally?.(); } const userRepo = useUserStore(); return Promise.all([ - this.fetchBucketOtrs(userRepo.currentUID), - this.fetchBucketOtrs(undefined, userRepo.currentUID), + this.fetchOtrs(targetType, userRepo.currentUID), + this.fetchOtrs(targetType, undefined, userRepo.currentUID), ]) .then((otrs) => otrs.flat()) .then((otrs) => { - this.__filterOTRMapping(OwnershipTypeEnum.BUCKET); + this.__filterOTRMapping(targetType); for (const otr of otrs) { this.otrMapping[otr.target_id] = otr; } @@ -81,7 +97,7 @@ export const useOTRStore = defineStore({ return otrs; }) .finally(() => { - this.__otrLoaded[OwnershipTypeEnum.BUCKET] = true; + this.__otrLoaded[targetType] = true; onFinally?.(); }); }, @@ -95,10 +111,10 @@ export const useOTRStore = defineStore({ return bucket.name; }); case OwnershipTypeEnum.WORKFLOW: - return useBucketStore() - .acceptBucketOtr(otr) - .then((bucket) => { - return bucket.name; + return useWorkflowStore() + .acceptWorkflowOtr(otr) + .then((workflow) => { + return workflow.workflow_id; }); case OwnershipTypeEnum.RESOURCE: return useBucketStore() @@ -139,7 +155,7 @@ export const useOTRStore = defineStore({ case OwnershipTypeEnum.BUCKET: return BucketService.bucketCreateBucketOtr(targetId, otr); case OwnershipTypeEnum.WORKFLOW: - return WorkflowService.workflowCreateBucketOtr(targetId, otr); + return WorkflowService.workflowCreateWorkflowOtr(targetId, otr); case OwnershipTypeEnum.RESOURCE: return ResourceService.resourceCreateResourceOtr(targetId, otr); } diff --git a/src/stores/workflows.ts b/src/stores/workflows.ts index 9b71a1d..aee87d8 100644 --- a/src/stores/workflows.ts +++ b/src/stores/workflows.ts @@ -2,6 +2,7 @@ import { defineStore } from "pinia"; import type { Body_Workflow_Version_upload_workflow_version_icon, IconUpdateOut, + OwnershipTransferRequestOut, ParameterExtension, WorkflowCredentialsIn, WorkflowIn, @@ -90,15 +91,6 @@ export const useWorkflowStore = defineStore({ __addNameToMapping(key: string, value: string) { useNameStore().addNameToMapping(key, value); }, - fetchWorkflowVersion(wid: string, vid: string): Promise<WorkflowVersion> { - return WorkflowVersionService.workflowVersionGetWorkflowVersion( - vid, - wid, - ).then((version) => { - this.__addNameToMapping(version.workflow_version_id, version.version); - return version; - }); - }, fetchWorkflows(onFinally?: () => void): Promise<WorkflowOut[]> { if (Object.keys(this.workflowMapping).length > 0) { onFinally?.(); @@ -637,5 +629,13 @@ export const useWorkflowStore = defineStore({ } }); }, + acceptWorkflowOtr(otr: OwnershipTransferRequestOut): Promise<WorkflowOut> { + return WorkflowService.workflowAcceptWorkflowOtr(otr.target_id).then( + (workflow) => { + this.fetchWorkflow(workflow.workflow_id, true); + return workflow; + }, + ); + }, }, }); diff --git a/src/views/object-storage/BucketsView.vue b/src/views/object-storage/BucketsView.vue index af5200f..3ddb701 100644 --- a/src/views/object-storage/BucketsView.vue +++ b/src/views/object-storage/BucketsView.vue @@ -54,7 +54,7 @@ let detailModal: Modal | null = null; let refreshTimeout: NodeJS.Timeout | undefined = undefined; function fetchBuckets() { - otrRepository.fetchOwnBucketOtrs(); + otrRepository.fetchOwnOtrs(OwnershipTypeEnum.BUCKET); bucketRepository.fetchOwnPermissions(); bucketRepository .fetchOwnBuckets(() => { @@ -190,7 +190,9 @@ onMounted(() => { style="font-size: 0.8em" > {{ otrForNewBuckets.length }} - <span class="visually-hidden">unread messages</span> + <span class="visually-hidden" + >open bucket ownership transfer requests</span + > </span> </button> <button diff --git a/src/views/workflows/MyWorkflowsView.vue b/src/views/workflows/MyWorkflowsView.vue index 7ccfeb4..09ddb60 100644 --- a/src/views/workflows/MyWorkflowsView.vue +++ b/src/views/workflows/MyWorkflowsView.vue @@ -2,6 +2,8 @@ import { computed, onMounted, reactive } from "vue"; import { NextflowVersion, + type OwnershipTransferRequestOut, + OwnershipTypeEnum, type WorkflowOut, type WorkflowVersion, WorkflowVersionStatus, @@ -17,18 +19,43 @@ import { useWorkflowStore } from "@/stores/workflows"; import { useWorkflowExecutionStore } from "@/stores/workflowExecutions"; import { environment } from "@/environment"; import ArbitraryWorkflowModal from "@/components/workflows/modals/ArbitraryWorkflowModal.vue"; +import OtrModal from "@/components/modals/ShowOtrModal.vue"; +import ListOtrsModal from "@/components/modals/ListOtrsModal.vue"; +import { useOTRStore } from "@/stores/otrs"; +import { useUserStore } from "@/stores/users"; +import { Modal, Tooltip } from "bootstrap"; +import { useSettingsStore } from "@/stores/settings"; +import CreateOtrModal from "@/components/modals/CreateOtrModal.vue"; const workflowRepository = useWorkflowStore(); const workflowExecutionRepository = useWorkflowExecutionStore(); +const otrRepository = useOTRStore(); +const userRepository = useUserStore(); +const settingsStore = useSettingsStore(); + +let showOtrModalInstance: Modal | null = null; +let createOtrModalInstance: Modal | null = null; const workflowsState = reactive<{ loading: boolean; updateWorkflow: WorkflowOut; potentialWorkflowDelete?: WorkflowOut; updateIconVersion: WorkflowVersion; + showOtrTarget: string; + createOtrTarget: WorkflowOut; }>({ loading: true, potentialWorkflowDelete: undefined, + showOtrTarget: "", + createOtrTarget: { + short_description: "", + name: "", + versions: [], + repository_url: "", + workflow_id: "", + developer_id: "", + private: false, + }, updateWorkflow: { short_description: "", name: "", @@ -67,6 +94,12 @@ const sortedWorkflows = computed<WorkflowOut[]>(() => { return temp; }); +const otrForNewWorkflows = computed<OwnershipTransferRequestOut[]>(() => + otrRepository.workflowOtrs.filter( + (otr) => otr.new_owner_uid === userRepository.currentUID, + ), +); + function workflowUpdateClicked(workflow: WorkflowOut) { workflowsState.updateWorkflow = workflow; } @@ -91,15 +124,44 @@ function updateIconClicked(version: WorkflowVersion) { workflowsState.updateIconVersion = version; } +function showOtrModal(workflowId: string) { + workflowsState.showOtrTarget = workflowId; + showOtrModalInstance?.show(); +} + +function showCreateOtrModal(workflow: WorkflowOut) { + workflowsState.createOtrTarget = workflow; + createOtrModalInstance?.show(); +} + onMounted(() => { + showOtrModalInstance = Modal.getOrCreateInstance("#view-workflow-otr-modal"); + createOtrModalInstance = Modal.getOrCreateInstance( + "#create-workflow-otr-modal", + ); workflowRepository.fetchOwnWorkflows(() => { workflowsState.loading = false; }); + otrRepository.fetchOwnOtrs(OwnershipTypeEnum.WORKFLOW); workflowExecutionRepository.fetchExecutionsForDevStatistics(); + new Tooltip("#showWorkflowOtrsButtons"); }); </script> <template> + <otr-modal + :otr-target-id="workflowsState.showOtrTarget" + modalId="view-workflow-otr-modal" + /> + <list-otrs-modal + :otrs="otrForNewWorkflows" + :otr-type="OwnershipTypeEnum.WORKFLOW" + modal-id="list-workflow-otrs-modal" + /> + <create-otr-modal + modal-id="create-workflow-otr-modal" + :target="workflowsState.createOtrTarget" + /> <create-workflow-modal modal-id="createWorkflowModal" /> <update-workflow-modal :workflow="workflowsState.updateWorkflow" @@ -131,13 +193,38 @@ onMounted(() => { class="row border-bottom mb-4 justify-content-between align-items-center pb-2 pe-2" > <h2 class="w-fit">My Workflows</h2> - <button - class="btn btn-lg btn-primary w-fit" - data-bs-toggle="modal" - data-bs-target="#createWorkflowModal" - > - Create - </button> + <div class="w-fit"> + <button + id="showWorkflowOtrsButtons" + :hidden="otrForNewWorkflows.length === 0" + class="btn border btn-lg shadow-sm position-relative me-3" + :class="{ + 'btn-light': settingsStore.lightThemeActive, + 'btn-secondary': settingsStore.darkThemeActive, + }" + data-bs-title="Ownership transfer requests" + data-bs-target="#list-workflow-otrs-modal" + data-bs-toggle="modal" + > + Requests + <span + class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger" + style="font-size: 0.8em" + > + {{ otrForNewWorkflows.length }} + <span class="visually-hidden" + >open workflow ownership transfer requests</span + > + </span> + </button> + <button + class="btn btn-lg btn-primary w-fit" + data-bs-toggle="modal" + data-bs-target="#createWorkflowModal" + > + Create + </button> + </div> </div> <div v-if="environment.DEV_SYSTEM" class="d-grid gap-2 col-4 mx-auto"> <button @@ -164,6 +251,8 @@ onMounted(() => { @workflow-update-credentials-click="workflowUpdateClicked" @workflow-delete-click="workflowDeleteClicked" @workflow-update-icon-click="iconUpdateClicked" + @create-otr-click="showCreateOtrModal" + @show-otr-click="showOtrModal" /> </card-transition-group> <div v-else class="text-center mt-5 fs-2"> -- GitLab