From 6b21bc8740e614801c0174ee77c777c4a04d331b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20G=C3=B6bel?= <dgoebel@techfak.uni-bielefeld.de> Date: Wed, 7 Feb 2024 16:51:18 +0100 Subject: [PATCH] Rerun whole workflow execution #94 --- src/components/modals/BootstrapModal.vue | 4 +- .../ParameterSchemaFormComponent.vue | 69 ++++++--- .../UploadParameterFileModal.vue | 11 +- .../workflows/modals/ParameterModal.vue | 131 ++++++++++++++--- src/stores/workflowExecutions.ts | 29 +++- src/types/WorkflowParameters.ts | 16 +++ src/views/workflows/ArbitraryWorkflowView.vue | 4 +- src/views/workflows/ListWorkflowsView.vue | 4 +- src/views/workflows/ReviewWorkflowsView.vue | 28 +++- src/views/workflows/StartWorkflowView.vue | 4 +- src/views/workflows/WorkflowVersionView.vue | 16 ++- src/views/workflows/WorkflowView.vue | 132 ++++++++---------- 12 files changed, 312 insertions(+), 136 deletions(-) create mode 100644 src/types/WorkflowParameters.ts diff --git a/src/components/modals/BootstrapModal.vue b/src/components/modals/BootstrapModal.vue index bb49405..7f3a55e 100644 --- a/src/components/modals/BootstrapModal.vue +++ b/src/components/modals/BootstrapModal.vue @@ -5,9 +5,11 @@ const props = defineProps<{ modalId: string; modalLabel: string; staticBackdrop?: boolean; - sizeModifier?: string; // https://getbootstrap.com/docs/5.3/components/modal/#optional-sizes, e.g. sm, lg and xl + sizeModifier?: sizeModifierType; // https://getbootstrap.com/docs/5.3/components/modal/#optional-sizes, e.g. sm, lg and xl }>(); +type sizeModifierType = "sm" | "lg" | "xl"; + const modalSizeClass = computed<string>(() => { if (props.sizeModifier == undefined) { return ""; diff --git a/src/components/parameter-schema/ParameterSchemaFormComponent.vue b/src/components/parameter-schema/ParameterSchemaFormComponent.vue index e0721ac..5d79f31 100644 --- a/src/components/parameter-schema/ParameterSchemaFormComponent.vue +++ b/src/components/parameter-schema/ParameterSchemaFormComponent.vue @@ -5,7 +5,7 @@ import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue"; import Ajv from "ajv"; import type { ValidateFunction } from "ajv"; import ParameterStringInput from "@/components/parameter-schema/form-mode/ParameterStringInput.vue"; -import { Toast } from "bootstrap"; +import { Toast, Tooltip } from "bootstrap"; import { useBucketStore } from "@/stores/buckets"; import { useS3KeyStore } from "@/stores/s3keys"; import BootstrapToast from "@/components/BootstrapToast.vue"; @@ -13,10 +13,16 @@ import { useResourceStore } from "@/stores/resources"; import { useRoute, useRouter } from "vue-router"; import type { ClowmInfo } from "@/types/ClowmInfo"; import UploadParameterFileModal from "@/components/parameter-schema/UploadParameterFileModal.vue"; +import type { + TemporaryParams, + WorkflowParameters, +} from "@/types/WorkflowParameters"; +import { useWorkflowExecutionStore } from "@/stores/workflowExecutions"; const bucketRepository = useBucketStore(); const resourceRepository = useResourceStore(); const keyRepository = useS3KeyStore(); +const executionRepository = useWorkflowExecutionStore(); const router = useRouter(); const route = useRoute(); @@ -48,8 +54,7 @@ const props = defineProps({ const emit = defineEmits<{ ( e: "start-workflow", - // eslint-disable-next-line @typescript-eslint/no-explicit-any - parameters: Record<string, any>, + parameters: WorkflowParameters, notes?: string, logs_s3_path?: string, debug_s3_path?: string, @@ -60,6 +65,7 @@ const emit = defineEmits<{ // Bootstrap Elements // ============================================================================= let errorToast: Toast | null = null; +let parameterLoadToast: Toast | null = null; // Types // ============================================================================= @@ -82,7 +88,7 @@ let validateSchema: ValidateFunction; const launchForm = ref<HTMLFormElement | null>(null); const formState = reactive<{ - formInput: Record<string, number | boolean | string | undefined>; + formInput: WorkflowParameters; validated: boolean; pipelineNotes: string; logs_s3_path?: string; @@ -170,14 +176,13 @@ function updateSchema(schema: Record<string, any>) { formState.formInput = b.reduce((acc, val) => { return { ...acc, ...val }; }); + loadParameters(executionRepository.popTemporaryParameters()); } -/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/ban-ts-comment, @typescript-eslint/no-unused-vars */ function startWorkflow() { errorToast?.hide(); formState.validated = true; formState.errorType = undefined; - console.log(formState.formInput); if (launchForm.value?.checkValidity()) { const schemaValid = validateSchema(formState.formInput); @@ -200,15 +205,20 @@ function startWorkflow() { } } -function loadParameters( - params?: Record<string, number | boolean | string | undefined>, -) { - if (params) { - for (const param in params) { +function loadParameters(tempParams?: TemporaryParams) { + if (tempParams) { + for (const param in tempParams.params) { if (param in formState.formInput) { - formState.formInput[param] = params[param]; + formState.formInput[param] = tempParams.params[param]; } } + formState.pipelineNotes = tempParams.metaParams.notes ?? ""; + formState.logs_s3_path = tempParams.metaParams.logs_s3_path; + formState.provenance_s3_path = tempParams.metaParams.provenance_s3_path; + formState.debug_s3_path = tempParams.metaParams.debug_s3_path; + if (Object.keys(tempParams?.params ?? {}).length > 0) { + parameterLoadToast?.show(); + } } } @@ -222,15 +232,23 @@ function scroll(selectedAnchor: string) { // ============================================================================= onMounted(() => { if (props.schema) updateSchema(props.schema); + if (props.clowmInfo) Tooltip.getOrCreateInstance("#exampleDataButton"); bucketRepository.fetchBuckets(); bucketRepository.fetchOwnPermissions(); keyRepository.fetchS3Keys(); resourceRepository.fetchPublicResources(); errorToast = new Toast("#workflowExecutionErrorToast"); + parameterLoadToast = new Toast("#workflowExecutionParameterLoadToast"); }); </script> <template> + <bootstrap-toast + toast-id="workflowExecutionParameterLoadToast" + color-class="success" + > + Successfully loaded parameters + </bootstrap-toast> <bootstrap-toast toast-id="workflowExecutionErrorToast" color-class="danger"> <template #default>Error starting workflow</template> <template #body> @@ -245,7 +263,13 @@ onMounted(() => { </bootstrap-toast> <upload-parameter-file-modal modal-id="parameterUploadModal" - @parameters-uploaded="loadParameters" + @parameters-uploaded=" + (params: WorkflowParameters) => + loadParameters({ + params: params, + metaParams: {}, + }) + " /> <div class="row mb-5 align-items-start"> <form @@ -506,9 +530,17 @@ onMounted(() => { type="button" class="btn btn-primary" v-if="props.clowmInfo?.exampleParameters" - @click="loadParameters(props.clowmInfo?.exampleParameters)" + data-bs-toggle="tooltip" + id="exampleDataButton" + data-bs-title="Load example parameters/data for this workflow" + @click=" + loadParameters({ + params: props.clowmInfo?.exampleParameters, + metaParams: {}, + }) + " > - Test this workflow + Try it out </button> <button class="btn btn-primary" @@ -532,8 +564,9 @@ onMounted(() => { </div> </template> -<style scoped> -div.card-body { - backdrop-filter: brightness(1.2); +<style> +.was-validated *:invalid { + border-color: var(--bs-form-invalid-border-color) !important; + background: var(--bs-danger-bg-subtle) !important; } </style> diff --git a/src/components/parameter-schema/UploadParameterFileModal.vue b/src/components/parameter-schema/UploadParameterFileModal.vue index db24fe3..fef305d 100644 --- a/src/components/parameter-schema/UploadParameterFileModal.vue +++ b/src/components/parameter-schema/UploadParameterFileModal.vue @@ -1,6 +1,7 @@ <script setup lang="ts"> import BootstrapModal from "@/components/modals/BootstrapModal.vue"; import { reactive, ref } from "vue"; +import type { WorkflowParameters } from "@/types/WorkflowParameters"; const props = defineProps<{ modalId: string; @@ -18,16 +19,9 @@ const randomIDSuffix = Math.random().toString(16).substring(2, 8); const parameterFileInput = ref<HTMLInputElement | undefined>(undefined); const emit = defineEmits<{ - ( - e: "parameters-uploaded", - params: Record<string, string | number | boolean | undefined>, - ): void; + (e: "parameters-uploaded", params: WorkflowParameters): void; }>(); -function modalClosed() { - console.log("Modal closed"); -} - function emitParameters() { if (parameterState.params != undefined) { emit("parameters-uploaded", parameterState.params); @@ -66,7 +60,6 @@ function fileChange() { :modalId="props.modalId" :static-backdrop="true" modal-label="Confirm Delete Modal" - v-on="{ 'hidden.bs.modal': modalClosed }" size-modifier="lg" > <template #header>Upload Parameter File</template> diff --git a/src/components/workflows/modals/ParameterModal.vue b/src/components/workflows/modals/ParameterModal.vue index c457cfb..31c33ca 100644 --- a/src/components/workflows/modals/ParameterModal.vue +++ b/src/components/workflows/modals/ParameterModal.vue @@ -2,15 +2,18 @@ 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 type { RouteLocationRaw, RouteParamsRaw } from "vue-router"; import { Modal } from "bootstrap"; import { useRouter } from "vue-router"; import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue"; -import type { WorkflowExecutionOut } from "@/client/workflow"; +import type { WorkflowExecutionOut, WorkflowVersion } from "@/client/workflow"; import { useNameStore } from "@/stores/names"; +import { useWorkflowStore } from "@/stores/workflows"; +import type { WorkflowParameters } from "@/types/WorkflowParameters"; const nameRepository = useNameStore(); const executionRepository = useWorkflowExecutionStore(); +const workflowRepository = useWorkflowStore(); const router = useRouter(); let parameterModal: Modal | null = null; @@ -35,6 +38,24 @@ const execution = computed<WorkflowExecutionOut | undefined>(() => { return executionRepository.executionMapping[props.executionId] ?? undefined; }); +const workflowVersion = computed<WorkflowVersion | undefined>( + () => + workflowRepository.versionMapping[ + execution.value?.workflow_version_id ?? "" + ], +); + +watch(execution, (newVal, oldVal) => { + if ( + newVal != undefined && + newVal.execution_id != oldVal?.execution_id && + newVal.workflow_id != undefined && + workflowRepository.workflowMapping[newVal.workflow_id] == undefined + ) { + workflowRepository.fetchWorkflow(newVal.workflow_id); + } +}); + const logs_s3_path = computed<string | undefined>( () => execution.value?.logs_s3_path ?? undefined, ); @@ -44,9 +65,11 @@ const debug_s3_path = computed<string | undefined>( const provenance_s3_path = computed<string | undefined>( () => execution.value?.provenance_s3_path ?? undefined, ); +const notes = computed<string | undefined>( + () => execution.value?.notes ?? undefined, +); -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const parameters = computed<Record<string, any> | undefined>(() => { +const parameters = computed<WorkflowParameters | undefined>(() => { if (props.executionId == undefined) { return undefined; } @@ -95,12 +118,23 @@ function fetchWorkflowExecutionParameters(executionId?: string) { } } -function handleBucketLinkClick(s3String: string) { +function handleLinkClick(route: RouteLocationRaw) { parameterModal?.hide(); - router.push({ - name: "bucket", - params: getS3LinkParameters(s3String), - }); + router.push(route); +} + +function saveParameters() { + if (parameters.value != undefined) { + executionRepository.pushTemporaryParameters(parameters.value, { + logs_s3_path: logs_s3_path?.value?.split("/")?.slice(0, -1)?.join("/"), + debug_s3_path: debug_s3_path?.value?.split("/")?.slice(0, -1)?.join("/"), + provenance_s3_path: provenance_s3_path?.value + ?.split("/") + ?.slice(0, -1) + ?.join("/"), + notes: notes.value, + }); + } } function getS3LinkParameters(s3String: string): RouteParamsRaw { @@ -199,7 +233,12 @@ onMounted(() => { name: 'bucket', params: getS3LinkParameters(logs_s3_path), }" - @click.prevent="handleBucketLinkClick(logs_s3_path)" + @click.prevent=" + handleLinkClick({ + name: 'bucket', + params: getS3LinkParameters(logs_s3_path), + }) + " >{{ logs_s3_path }} </router-link> </td> @@ -219,7 +258,12 @@ onMounted(() => { name: 'bucket', params: getS3LinkParameters(provenance_s3_path), }" - @click.prevent="handleBucketLinkClick(provenance_s3_path)" + @click.prevent=" + handleLinkClick({ + name: 'bucket', + params: getS3LinkParameters(provenance_s3_path), + }) + " >{{ provenance_s3_path }} </router-link> </td> @@ -234,7 +278,12 @@ onMounted(() => { name: 'bucket', params: getS3LinkParameters(debug_s3_path), }" - @click.prevent="handleBucketLinkClick(debug_s3_path)" + @click.prevent=" + handleLinkClick({ + name: 'bucket', + params: getS3LinkParameters(debug_s3_path), + }) + " >{{ debug_s3_path }} </router-link> </td> @@ -245,14 +294,31 @@ onMounted(() => { </th> <td> <router-link - v-if="isBucketLink(value)" + v-if="typeof value === 'string' && isBucketLink(value)" :to="{ name: 'bucket', params: getS3LinkParameters(value), }" - @click.prevent="handleBucketLinkClick(value)" + @click.prevent=" + handleLinkClick({ + name: 'bucket', + params: getS3LinkParameters(value), + }) + " >{{ value }} </router-link> + <a + v-else-if=" + typeof value === 'string' && value.startsWith('http') + " + :href="value" + target="_blank" + >{{ value }} + <font-awesome-icon + icon="fa-solid fa-arrow-up-right-from-square" + class="ms-1" + /> + </a> <template v-else>{{ value }}</template> </td> </tr> @@ -261,6 +327,39 @@ onMounted(() => { </table> </template> <template v-slot:footer> + <router-link + v-if="workflowVersion" + class="btn btn-primary" + :class="{ + disabled: parameterState.loading || parameterState.error, + }" + @click.prevent=" + saveParameters(); + handleLinkClick({ + name: 'workflow-start', + params: { + versionId: workflowVersion.workflow_version_id, + workflowId: workflowVersion.workflow_id, + }, + query: { + workflowModeId: execution?.mode_id, + }, + }); + " + :to="{ + name: 'workflow-start', + params: { + versionId: workflowVersion.workflow_version_id, + workflowId: workflowVersion.workflow_id, + }, + query: { + workflowModeId: execution?.mode_id, + }, + }" + > + Rerun Execution + <font-awesome-icon icon="fa-solid fa-arrow-rotate-right" class="me2" /> + </router-link> <a class="btn btn-primary" role="button" @@ -275,8 +374,8 @@ onMounted(() => { :aria-disabled="parameterState.loading || parameterState.error" > Download Parameters - <font-awesome-icon icon="fa-solid fa-download" class="ms-1" - /></a> + <font-awesome-icon icon="fa-solid fa-download" class="ms-1" /> + </a> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> Close </button> diff --git a/src/stores/workflowExecutions.ts b/src/stores/workflowExecutions.ts index 9633272..e90bd1d 100644 --- a/src/stores/workflowExecutions.ts +++ b/src/stores/workflowExecutions.ts @@ -13,6 +13,11 @@ import { import { useAuthStore } from "@/stores/users"; import dayjs from "dayjs"; import { set, get } from "idb-keyval"; +import type { + WorkflowParameters, + WorkflowMetaParameters, + TemporaryParams, +} from "@/types/WorkflowParameters"; export const useWorkflowExecutionStore = defineStore({ id: "workflow-executions", @@ -21,11 +26,14 @@ export const useWorkflowExecutionStore = defineStore({ executionMapping: {}, anonymizedExecutions: [], parameters: {}, + __temporaryParameters: undefined, + __temporaryMetaParameters: undefined, }) as { executionMapping: Record<string, WorkflowExecutionOut>; anonymizedExecutions: AnonymizedWorkflowExecution[]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - parameters: Record<string, Record<string, any>>; + parameters: Record<string, WorkflowParameters>; + __temporaryParameters?: WorkflowParameters; + __temporaryMetaParameters?: Record<string, string>; }, getters: { executions(): WorkflowExecutionOut[] { @@ -33,6 +41,23 @@ export const useWorkflowExecutionStore = defineStore({ }, }, actions: { + pushTemporaryParameters( + params: WorkflowParameters, + metaParams?: WorkflowMetaParameters, + ) { + this.__temporaryParameters = params; + this.__temporaryMetaParameters = metaParams ?? {}; + }, + popTemporaryParameters(): TemporaryParams { + const params = this.__temporaryParameters; + this.__temporaryParameters = undefined; + const metaParams = this.__temporaryMetaParameters; + this.__temporaryMetaParameters = undefined; + return { + metaParams: metaParams ?? {}, + params: params ?? {}, + }; + }, fetchExecutionsForDevStatistics( onFinally?: () => void, ): Promise<AnonymizedWorkflowExecution[]> { diff --git a/src/types/WorkflowParameters.ts b/src/types/WorkflowParameters.ts new file mode 100644 index 0000000..0fd1398 --- /dev/null +++ b/src/types/WorkflowParameters.ts @@ -0,0 +1,16 @@ +export type WorkflowParameters = Record< + string, + string | number | boolean | undefined +>; + +export type WorkflowMetaParameters = { + logs_s3_path?: string; + debug_s3_path?: string; + provenance_s3_path?: string; + notes?: string; +}; + +export type TemporaryParams = { + params: WorkflowParameters; + metaParams: WorkflowMetaParameters; +}; diff --git a/src/views/workflows/ArbitraryWorkflowView.vue b/src/views/workflows/ArbitraryWorkflowView.vue index c739aa5..a32fd64 100644 --- a/src/views/workflows/ArbitraryWorkflowView.vue +++ b/src/views/workflows/ArbitraryWorkflowView.vue @@ -11,6 +11,7 @@ import type { WorkflowIn } from "@/client/workflow"; import { useWorkflowExecutionStore } from "@/stores/workflowExecutions"; import ParameterSchemaFormComponent from "@/components/parameter-schema/ParameterSchemaFormComponent.vue"; import BootstrapToast from "@/components/BootstrapToast.vue"; +import type { WorkflowParameters } from "@/types/WorkflowParameters"; const props = defineProps<{ wid: string; @@ -92,8 +93,7 @@ watch( ); function startWorkflow( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - parameters: Record<string, any>, + parameters: WorkflowParameters, notes?: string, logs_s3_path?: string, debug_s3_path?: string, diff --git a/src/views/workflows/ListWorkflowsView.vue b/src/views/workflows/ListWorkflowsView.vue index ac39acf..56ccb5a 100644 --- a/src/views/workflows/ListWorkflowsView.vue +++ b/src/views/workflows/ListWorkflowsView.vue @@ -28,7 +28,9 @@ const filterFunctionMapping: Record< (a: WorkflowOut, b: WorkflowOut) => boolean > = { name: (a: WorkflowOut, b: WorkflowOut) => - workflowsState.sortDesc ? a.name > b.name : a.name < b.name, + workflowsState.sortDesc + ? a.name.toLowerCase() > b.name.toLowerCase() + : a.name.toLowerCase() < b.name.toLowerCase(), release: (a: WorkflowOut, b: WorkflowOut) => { const a_date = dayjs.unix(a.versions[0].created_at); const b_date = dayjs.unix(b.versions[0].created_at); diff --git a/src/views/workflows/ReviewWorkflowsView.vue b/src/views/workflows/ReviewWorkflowsView.vue index 1ff20c4..8507574 100644 --- a/src/views/workflows/ReviewWorkflowsView.vue +++ b/src/views/workflows/ReviewWorkflowsView.vue @@ -7,6 +7,8 @@ import { sortedVersions } from "@/utils/Workflow"; import { useWorkflowStore } from "@/stores/workflows"; import { useAuthStore } from "@/stores/users"; import { useNameStore } from "@/stores/names"; +import dayjs from "dayjs"; +import { Tooltip } from "bootstrap"; const workflowRepository = useWorkflowStore(); const nameRepository = useNameStore(); @@ -42,6 +44,17 @@ onMounted(() => { .fetchReviewableWorkflows(() => { workflowsState.loading = false; }) + .then((workflows) => { + setTimeout(() => { + document + .querySelector("#reviewTable") + ?.querySelectorAll('[data-bs-toggle="tooltip"]') + ?.forEach((tooltipTriggerEl) => + Tooltip.getOrCreateInstance(tooltipTriggerEl), + ); + }, 1000); + return workflows; + }) .then((workflows) => workflows.map((workflow) => workflow.developer_id).filter(isDefined), ) @@ -60,6 +73,7 @@ onMounted(() => { </div> <table class="table table-striped mx-auto" + id="reviewTable" v-else-if="workflowRepository.reviewableWorkflows.length > 0" > <thead class="fs-5"> @@ -96,9 +110,6 @@ onMounted(() => { <tr> <td colspan="4" class="px-5"> <table class="table mb-0"> - <colgroup> - <col span="1" style="width: 1%" /> - </colgroup> <tbody class="text-center"> <tr v-for="version in sortedVersions(workflow.versions)" @@ -113,6 +124,17 @@ onMounted(() => { /> </td> <td class="text-start">{{ version.version }}</td> + <td> + <span + data-bs-toggle="tooltip" + :data-bs-title=" + dayjs + .unix(version?.created_at) + .format('DD.MM.YYYY HH:mm:ss') + " + >{{ dayjs.unix(version.created_at).fromNow() }}</span + > + </td> <td>{{ version.status }}</td> <td> <a diff --git a/src/views/workflows/StartWorkflowView.vue b/src/views/workflows/StartWorkflowView.vue index 0d419d4..df2f89e 100644 --- a/src/views/workflows/StartWorkflowView.vue +++ b/src/views/workflows/StartWorkflowView.vue @@ -8,6 +8,7 @@ import { Toast } from "bootstrap"; import { useWorkflowExecutionStore } from "@/stores/workflowExecutions"; import BootstrapToast from "@/components/BootstrapToast.vue"; import { useWorkflowStore } from "@/stores/workflows"; +import type { WorkflowParameters } from "@/types/WorkflowParameters"; const executionRepository = useWorkflowExecutionStore(); const workflowRepository = useWorkflowStore(); @@ -67,8 +68,7 @@ function downloadParameterSchema() { } function startWorkflow( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - parameters: Record<string, any>, + parameters: WorkflowParameters, notes?: string, logs_s3_path?: string, debug_s3_path?: string, diff --git a/src/views/workflows/WorkflowVersionView.vue b/src/views/workflows/WorkflowVersionView.vue index 072ba90..b130b67 100644 --- a/src/views/workflows/WorkflowVersionView.vue +++ b/src/views/workflows/WorkflowVersionView.vue @@ -36,12 +36,16 @@ watch( () => props.workflowModeId, (newModeId, oldModeId) => { if (newModeId !== oldModeId) { - workflowRepository.fetchWorkflowDocumentation( - props.workflowId, - props.versionId, - DocumentationEnum.PARAMETER_SCHEMA, - newModeId, - ); + workflowRepository + .fetchWorkflowDocumentation( + props.workflowId, + props.versionId, + DocumentationEnum.PARAMETER_SCHEMA, + newModeId, + ) + .then((schema) => { + versionState.parameterSchema = schema; + }); } }, ); diff --git a/src/views/workflows/WorkflowView.vue b/src/views/workflows/WorkflowView.vue index b6d573f..8ad2714 100644 --- a/src/views/workflows/WorkflowView.vue +++ b/src/views/workflows/WorkflowView.vue @@ -38,16 +38,12 @@ const route = useRoute(); // ============================================================================= const workflowState = reactive<{ loading: boolean; - activeVersionId: string; initialOpen: boolean; stats: WorkflowStatistic[]; - activeModeId?: string; }>({ loading: true, - activeVersionId: props.versionId ?? "", initialOpen: true, stats: [], - activeModeId: props.workflowModeId, }); // Watchers @@ -61,67 +57,6 @@ watch( }, ); -watch( - () => props.versionId, - (newWorkflowId) => { - workflowState.activeVersionId = newWorkflowId ?? ""; - }, -); - -watch( - () => props.workflowModeId, - (newModeId) => { - if (newModeId) { - workflowState.activeModeId = newModeId; - } - }, -); - -watch( - () => workflowState.activeVersionId, - (newVersionId, oldVersionId) => { - if ( - newVersionId && - newVersionId !== oldVersionId && - route.name !== "workflow-start" - ) { - // If mode does not exist in other version, select another mode - if ( - activeVersionModeIds.value.length > 0 && - activeVersionModeIds.value.findIndex( - (modeId) => modeId === workflowState.activeModeId, - ) == -1 - ) { - workflowState.activeModeId = activeVersionModeIds.value[0]; - } else if (activeVersionModeIds.value.length == 0) { - // If new version does not has any modes, then set mode id to none - workflowState.activeModeId = undefined; - } - router.push({ - name: "workflow-version", - params: { versionId: newVersionId }, - query: { - tab: route.query.tab, - workflowModeId: workflowState.activeModeId, - }, - }); - } - }, -); - -watch( - () => workflowState.activeModeId, - (newModeId, oldModeId) => { - if (newModeId != oldModeId) { - router.push({ - name: route.name ?? undefined, - params: { versionId: workflowState.activeVersionId }, - query: { tab: route.query.tab, workflowModeId: newModeId }, - }); - } - }, -); - // Computed Properties // ============================================================================= const workflow = computed<WorkflowOut | undefined>(() => @@ -139,7 +74,7 @@ const latestVersion = computed<WorkflowVersion | undefined>(() => ); const activeVersion = computed<WorkflowVersion | undefined>(() => workflow.value?.versions.find( - (w) => w.workflow_version_id === workflowState.activeVersionId, + (w) => w.workflow_version_id === props.versionId, ), ); @@ -198,13 +133,9 @@ function updateWorkflow(workflowId: string) { workflowState.initialOpen = false; }) .then((workflow) => { - if (!workflowState.initialOpen || !route.params.versionId) { - workflowState.activeVersionId = - workflow.versions[workflow.versions.length - 1].workflow_version_id; - } else { - workflowState.activeVersionId = route.params.versionId as string; + if (props.versionId == undefined) { + updateVersion(workflow.versions[0]?.workflow_version_id); } - workflowState.activeModeId = activeVersionModeIds.value[0] ?? undefined; }); WorkflowService.workflowGetWorkflowStatistics(workflowId).then((stats) => { @@ -212,6 +143,52 @@ function updateWorkflow(workflowId: string) { }); } +function updateVersion(workflowVersionId?: string) { + if (workflowVersionId) { + const possibleModes = getModesForVersion(workflowVersionId); + let modeId: string | undefined = undefined; + if ( + (props.workflowModeId == undefined && possibleModes.length > 0) || // next version needs a mode + (props.workflowId != undefined && // next version has not the current mode + possibleModes.length > 0 && + !possibleModes.includes(props.workflowId)) + ) { + modeId = possibleModes[0]; + } else if ( + // next version has the same mode as the current mode + props.workflowId != undefined && + possibleModes.length > 0 && + possibleModes.includes(props.workflowId) + ) { + modeId = props.workflowModeId; + } + router.push({ + params: { + ...route.params, + versionId: workflowVersionId, + }, + query: { ...route.query, workflowModeId: modeId }, + }); + } +} + +function updateMode(modeId?: string) { + router.replace({ + params: { + ...route.params, + }, + query: { ...route.query, workflowModeId: modeId }, + }); +} + +function getModesForVersion(workflowVersionId: string): string[] { + return ( + workflow.value?.versions.find( + (w) => w.workflow_version_id === workflowVersionId, + )?.modes ?? [] + ); +} + function deprecateCurrentWorkflowVersion() { if (props.versionId) { workflowRepository.deprecateWorkflowVersion( @@ -272,7 +249,7 @@ onMounted(() => { <select id="workflowModeSelect" class="form-select w-fit" - v-model="workflowState.activeModeId" + @change="updateMode(($event?.target as HTMLSelectElement)?.value)" > <option v-for="modeId of activeVersionModeIds" @@ -326,7 +303,7 @@ onMounted(() => { workflowId: props.workflowId, }, query: { - workflowModeId: workflowState.activeModeId, + workflowModeId: props.workflowModeId, viewMode: 'simple', }, }" @@ -346,12 +323,15 @@ onMounted(() => { class="form-select form-select-sm" aria-label="Workflow version selection" aria-describedby="workflow-version-wrapping" - v-model="workflowState.activeVersionId" + @change=" + updateVersion(($event?.target as HTMLSelectElement)?.value) + " > <option v-for="version in sortedVersions(workflow.versions)" :key="version.workflow_version_id" :value="version.workflow_version_id" + :selected="version.workflow_version_id === props.versionId" > {{ version.version }} </option> @@ -365,7 +345,7 @@ onMounted(() => { class="text-secondary text-decoration-none mx-auto w-fit p-0" > <font-awesome-icon :icon="gitIcon" class="me-1" /> - <span class="align-middle"> {{ workflow.repository_url }}</span> + <span class="align-middle"> {{ workflow?.repository_url }}</span> <font-awesome-icon icon="fa-solid fa-arrow-up-right-from-square" class="ms-1" -- GitLab