diff --git a/src/components/workflows/modals/ParameterModal.vue b/src/components/workflows/modals/ParameterModal.vue new file mode 100644 index 0000000000000000000000000000000000000000..36c328afdb03600eb1ac4dc0e3417d983c1093c3 --- /dev/null +++ b/src/components/workflows/modals/ParameterModal.vue @@ -0,0 +1,184 @@ +<script setup lang="ts"> +import BootstrapModal from "@/components/modals/BootstrapModal.vue"; +import { computed, onMounted, reactive, watch } from "vue"; +import { useWorkflowExecutionStore } from "@/stores/workflowExecutions"; +import type { RouteParamsRaw } from "vue-router"; +import { Modal } from "bootstrap"; +import { useRouter } from "vue-router"; +import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue"; +import { useWorkflowStore } from "@/stores/workflows"; + +const workflowRepository = useWorkflowStore(); +const executionRepository = useWorkflowExecutionStore(); +const router = useRouter(); + +let parameterModal: Modal | null = null; + +const props = defineProps<{ + modalID: string; + executionId?: string; +}>(); + +const parameterState = reactive<{ + loading: boolean; + error: boolean; +}>({ + loading: false, + error: false, +}); + +const workflowName = computed<string>(() => { + if (props.executionId) { + const execution = executionRepository.executionMapping[props.executionId]; + if ( + execution?.workflow_id != undefined && + execution?.workflow_version_id != undefined + ) { + return ( + workflowRepository.getName(execution.workflow_id) + + "@" + + workflowRepository.getName(execution.workflow_version_id) + ); + } + } + return ""; +}); + +function fetchWorkflowExecutionParameters(executionId?: string) { + parameterState.error = false; + if (executionId != undefined) { + parameterState.loading = true; + executionRepository + .fetchExecutionParameters(executionId) + .catch(() => { + parameterState.error = true; + }) + .finally(() => { + parameterState.loading = false; + }); + } +} + +function handleBucketLinkClick(s3String: string) { + parameterModal?.hide(); + router.push({ + name: "bucket", + params: getS3LinkParameters(s3String), + }); +} + +function getS3LinkParameters(s3String: string): RouteParamsRaw { + const pathComponents = s3String.slice(5).split("/"); + const s3File = + pathComponents.length > 1 && + pathComponents[pathComponents.length - 1].includes("."); + return { + bucketName: pathComponents[0], + subFolders: pathComponents.slice(1, s3File ? -1 : undefined), + }; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function isBucketLink(value: any): boolean { + if (typeof value === "string") { + return value.startsWith("s3://"); + } + return false; +} + +watch( + () => props.executionId, + (newId, oldId) => { + if (newId != oldId) { + fetchWorkflowExecutionParameters(newId); + } + }, +); + +onMounted(() => { + fetchWorkflowExecutionParameters(props.executionId); + parameterModal = Modal.getOrCreateInstance("#" + props.modalID); +}); +</script> + +<template> + <bootstrap-modal + :modalID="modalID" + :static-backdrop="false" + modal-label="Workflow Execution Parameters Modal" + > + <template v-slot:header + >Workflow Execution Parameters + <b> + {{ workflowName }} + </b> + </template> + <template v-slot:body> + <div v-if="parameterState.error" class="text-center fs-4 mt-5"> + <font-awesome-icon + icon="fa-solid fa-magnifying-glass" + class="mb-3 fs-0" + style="color: var(--bs-secondary)" + /> + <p> + Workflow Execution <i>{{ props.executionId }}</i> not found + </p> + </div> + <table v-else-if="props.executionId" class="table table-hover"> + <caption class="placeholder-glow"> + <span v-if="parameterState.loading" class="placeholder col-1"></span> + <template v-else> + {{ + Object.keys(executionRepository.parameters[props.executionId]) + .length + }} + </template> + Parameters + </caption> + <tbody> + <template v-if="parameterState.loading"> + <tr v-for="n in 6" :key="n"> + <th scope="row" style="width: 20%" class="placeholder-glow"> + <div class="placeholder col-12"></div> + </th> + <td class="placeholder-glow"> + <div class="placeholder col-8"></div> + </td> + </tr> + </template> + <template v-else> + <tr + v-for="(value, name) in executionRepository.parameters[ + props.executionId + ]" + :key="name" + > + <th scope="row" style="width: 10%" class="text-end"> + <b>{{ name }}</b> + </th> + <td> + <router-link + v-if="isBucketLink(value)" + :to="{ + name: 'bucket', + params: getS3LinkParameters(value), + }" + @click.prevent="handleBucketLinkClick(value)" + >{{ value }} + </router-link> + <template v-else>{{ value }}</template> + </td> + </tr> + </template> + </tbody> + </table> + </template> + <template v-slot:footer> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> + Close + </button> + </template> + </bootstrap-modal> +</template> + +<style scoped></style> diff --git a/src/stores/workflowExecutions.ts b/src/stores/workflowExecutions.ts index 87b5d37bb550a8e91291e76533cdf02e95da1c3d..9633272e7dac04c2705022eea3cc81a7368638d3 100644 --- a/src/stores/workflowExecutions.ts +++ b/src/stores/workflowExecutions.ts @@ -12,6 +12,7 @@ import { } from "@/client/workflow"; import { useAuthStore } from "@/stores/users"; import dayjs from "dayjs"; +import { set, get } from "idb-keyval"; export const useWorkflowExecutionStore = defineStore({ id: "workflow-executions", @@ -19,9 +20,12 @@ export const useWorkflowExecutionStore = defineStore({ ({ executionMapping: {}, anonymizedExecutions: [], + parameters: {}, }) as { executionMapping: Record<string, WorkflowExecutionOut>; anonymizedExecutions: AnonymizedWorkflowExecution[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + parameters: Record<string, Record<string, any>>; }, getters: { executions(): WorkflowExecutionOut[] { @@ -79,6 +83,31 @@ export const useWorkflowExecutionStore = defineStore({ }) .finally(onFinally); }, + fetchExecutionParameters( + executionId: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): Promise<Record<string, any>> { + if (Object.keys(this.parameters).includes(executionId)) { + return Promise.resolve(this.parameters[executionId]); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return get<Record<string, any>>(executionId) + .then((parameters) => { + if (parameters != undefined) { + return parameters; + } + return WorkflowExecutionService.workflowExecutionGetWorkflowExecutionParams( + executionId, + ).then((parameters) => { + set(executionId, parameters); + return parameters; + }); + }) + .then((parameters) => { + this.parameters[executionId] = parameters; + return parameters; + }); + }, deleteExecution(executionId: string): Promise<void> { return WorkflowExecutionService.workflowExecutionDeleteWorkflowExecution( executionId, diff --git a/src/views/workflows/ListWorkflowExecutionsView.vue b/src/views/workflows/ListWorkflowExecutionsView.vue index bda5cfd1668d35c5437596f87e8cbca8847ff743..ea395aee68394923ecbcd377ad6df8b38d15ca12 100644 --- a/src/views/workflows/ListWorkflowExecutionsView.vue +++ b/src/views/workflows/ListWorkflowExecutionsView.vue @@ -7,6 +7,7 @@ import { Tooltip } from "bootstrap"; import DeleteModal from "@/components/modals/DeleteModal.vue"; import { useWorkflowStore } from "@/stores/workflows"; import { useWorkflowExecutionStore } from "@/stores/workflowExecutions"; +import ParameterModal from "@/components/workflows/modals/ParameterModal.vue"; const workflowRepository = useWorkflowStore(); const executionRepository = useWorkflowExecutionStore(); @@ -17,9 +18,11 @@ let intervalId: NodeJS.Timer | undefined = undefined; const executionsState = reactive<{ loading: boolean; executionToDelete?: WorkflowExecutionOut; + executionParameters?: string; }>({ loading: true, executionToDelete: undefined, + executionParameters: undefined, }); const statusToColorMapping = { @@ -133,6 +136,10 @@ onUnmounted(() => { deleteWorkflowExecution(executionsState.executionToDelete?.execution_id) " /> + <parameter-modal + modal-i-d="workflowExecutionParameterModal" + :execution-id="executionsState.executionParameters" + /> <div class="row m-2 border-bottom mb-4 justify-content-between align-items-center" > @@ -258,6 +265,20 @@ onUnmounted(() => { <span class="visually-hidden">Toggle Dropdown</span> </button> <ul class="dropdown-menu dropdown-menu"> + <li> + <button + class="dropdown-item align-middle" + type="button" + data-bs-toggle="modal" + data-bs-target="#workflowExecutionParameterModal" + @click=" + executionsState.executionParameters = + execution.execution_id + " + > + <span class="ms-1">Parameters</span> + </button> + </li> <li v-if="workflowExecutionCancelable(execution)"> <button class="dropdown-item text-danger align-middle"