<script setup lang="ts"> import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue"; import { onMounted, reactive, computed } from "vue"; import type { WorkflowExecutionOut } from "@/client/workflow"; import { useAuthStore } from "@/stores/auth"; import { WorkflowExecutionService, WorkflowExecutionStatus, WorkflowService, WorkflowVersionService, } from "@/client/workflow"; import dayjs from "dayjs"; import DeleteModal from "@/components/modals/DeleteModal.vue"; const userStore = useAuthStore(); const executionsState = reactive<{ workflowMapping: Record<string, string>; versionMapping: Record<string, string>; workflowExecutions: WorkflowExecutionOut[]; loading: boolean; mappingLoading: boolean; executionToDelete?: WorkflowExecutionOut; }>({ workflowMapping: {}, versionMapping: {}, workflowExecutions: [], loading: true, mappingLoading: true, executionToDelete: undefined, }); const statusToColorMapping = { PENDING: "bg-warning", SCHEDULED: "bg-warning", RUNNING: "bg-info", CANCELED: "bg-danger", SUCCESS: "bg-success", ERROR: "bg-danger", }; const statusToIconMapping = { PENDING: "fa-solid fa-circle-pause", SCHEDULED: "fa-solid fa-circle-pause", RUNNING: "fa-solid fa-circle-play", CANCELED: "fa-solid fa-circle-xmark", SUCCESS: "fa-solid fa-circle-check", ERROR: "fa-solid fa-circle-xmark", }; const sortedExecutions = computed<WorkflowExecutionOut[]>(() => { const tempList = [...executionsState.workflowExecutions]; tempList.sort((a, b) => { // sort by start time descending return dayjs(a.start_time).isBefore(dayjs(b.start_time)) ? 1 : -1; }); return tempList; }); const deleteModalString = computed<string>(() => { if (executionsState.executionToDelete === undefined) { return ""; } else if ( !executionsState.executionToDelete.workflow_version_id || !executionsState.executionToDelete.workflow_id ) { return `Workflow Execution from ${dayjs( executionsState.executionToDelete.start_time ).format("DD.MM.YYYY HH:mm")}`; } else { return `Workflow Execution ${ executionsState.workflowMapping[ executionsState.executionToDelete.workflow_id ] }@${ executionsState.versionMapping[ executionsState.executionToDelete.workflow_version_id ] } from ${dayjs(executionsState.executionToDelete.start_time).format( "DD.MM.YYYY HH:mm" )}`; } }); // Functions // ----------------------------------------------------------------------------- function updateExecutions() { const listExecutionsPromise = WorkflowExecutionService.workflowExecutionListWorkflowExecutions( userStore.currentUID ) .then((executions) => { executionsState.workflowExecutions = executions; return executions; }) .finally(() => { executionsState.loading = false; }); listExecutionsPromise // construct mapping from workflow id to workflow name .then((executions) => Promise.all( // group all calls to the API executions .filter((execution) => execution.workflow_id) // filter undefined workflows .filter( (execution) => !executionsState.workflowMapping[execution.workflow_id] ) .filter( // filter unique workflows (execution, index, array) => array.findIndex( (val) => val.workflow_id === execution.workflow_id ) === index ) .map((execution) => WorkflowService.workflowGetWorkflow(execution.workflow_id) ) ) ) .then((workflows) => workflows.forEach((workflow) => { executionsState.workflowMapping[workflow.workflow_id] = workflow.name; }) ) .finally(() => { executionsState.mappingLoading = false; }); listExecutionsPromise // construct mapping from version id to clear text version .then((executions) => Promise.all( // group all calls to the API executions .filter( // filter undefined workflow versions (execution) => execution.workflow_id && execution.workflow_version_id ) .filter( // filter already seen workflow versions (version) => !executionsState.versionMapping[version.workflow_id] ) .filter( // filter unique workflow versions (execution, index, array) => array.findIndex( (val) => val.workflow_version_id === execution.workflow_version_id ) === index ) .map((execution) => WorkflowVersionService.workflowVersionGetWorkflowVersion( execution.workflow_version_id, execution.workflow_id ) ) ) ) .then((versions) => versions.forEach((version) => { executionsState.versionMapping[version.git_commit_hash] = version.version; }) ); } function workflowExecutionDeletable(status: WorkflowExecutionStatus): boolean { return [ WorkflowExecutionStatus.ERROR, WorkflowExecutionStatus.CANCELED, WorkflowExecutionStatus.SUCCESS, ].includes(status); } function workflowExecutionCancable(status: WorkflowExecutionStatus): boolean { return [ WorkflowExecutionStatus.RUNNING, WorkflowExecutionStatus.PENDING, WorkflowExecutionStatus.SCHEDULED, ].includes(status); } function deleteWorkflowExecution(executionId?: string) { if (executionId) { WorkflowExecutionService.workflowExecutionDeleteWorkflowExecution( executionId ).then(() => { executionsState.workflowExecutions = executionsState.workflowExecutions.filter( (execution) => execution.execution_id !== executionId ); }); } } function cancelWorkflowExecution(executionId: string) { WorkflowExecutionService.workflowExecutionCancelWorkflowExecution( executionId ).then(() => { const index = executionsState.workflowExecutions.findIndex( (execution) => execution.execution_id === executionId ); if (index > -1) { executionsState.workflowExecutions[index].status = WorkflowExecutionStatus.CANCELED; executionsState.workflowExecutions[index].end_time = dayjs().toISOString(); } }); } onMounted(() => { updateExecutions(); }); </script> <template> <delete-modal modal-i-d="deleteWorkflowExecutionModal" :object-name-delete="deleteModalString" @confirm-delete=" deleteWorkflowExecution(executionsState.executionToDelete?.execution_id) " /> <div class="row m-2 border-bottom border-light mb-4 justify-content-between align-items-center" > <h1 class="mb-2 text-light w-fit">My Workflow Executions</h1> <router-link :to="{ name: 'workflows' }" class="btn btn-primary w-fit" >Start Workflow Execution</router-link > </div> <table class="table table-dark table-striped table-hover caption-top align-middle" > <caption> Displaying {{ executionsState.workflowExecutions.length }} Workflow Execution </caption> <thead> <tr> <th scope="col">Workflow</th> <th scope="col">Status</th> <th scope="col">Started</th> <th scope="col">Ended</th> <th scope="col"></th> </tr> </thead> <tbody> <template v-if="executionsState.loading"> <tr v-for="n in 5" :key="n"> <td class="placeholder-glow w-25"> <span class="placeholder col-6"></span> </td> <td class="placeholder-glow" style="width: 20%"> <span class="placeholder col-4"></span> </td> <td class="placeholder-glow" style="width: 20%"> <span class="placeholder col-6"></span> </td> <td class="placeholder-glow" style="width: 20%"> <span class="placeholder col-6"></span> </td> <td class="text-end"> <div class="btn-group btn-group-sm dropdown-center dropdown-menu-start" > <button type="button" class="btn btn-secondary" disabled> Details </button> <button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" disabled > <span class="visually-hidden">Toggle Dropdown</span> </button> </div> </td> </tr> </template> <template v-else-if="executionsState.workflowExecutions.length > 0"> <tr v-for="execution in sortedExecutions" :key="execution.execution_id"> <td v-if="executionsState.mappingLoading" class="placeholder-glow w-25" > <span class="placeholder col-6"></span> </td> <td v-else> <span>{{ executionsState.workflowMapping[execution.workflow_id] }}</span> <span >@{{ executionsState.versionMapping[execution.workflow_version_id] }}</span > </td> <td> <span class="rounded-pill py-1 px-2" :class="statusToColorMapping[execution.status]" ><font-awesome-icon class="me-2" :icon="statusToIconMapping[execution.status]" />{{ execution.status }}</span > </td> <td>{{ dayjs(execution.start_time).format("DD.MM.YYYY HH:mm") }}</td> <td> <template v-if="execution.end_time"> {{ dayjs(execution.end_time).format("DD.MM.YYYY HH:mm") }} </template> <template v-else> - </template> </td> <td class="text-end"> <div class="btn-group btn-group-sm dropdown-center dropdown-menu-start" > <button type="button" class="btn btn-secondary">Details</button> <button type="button" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false" > <span class="visually-hidden">Toggle Dropdown</span> </button> <ul class="dropdown-menu dropdown-menu-dark"> <li v-if="workflowExecutionCancable(execution.status)"> <button class="dropdown-item text-danger align-middle" type="button" @click="cancelWorkflowExecution(execution.execution_id)" > <font-awesome-icon icon="fa-solid fa-ban" /> <span class="ms-1">Cancel</span> </button> </li> <li v-if="workflowExecutionDeletable(execution.status)"> <button class="dropdown-item text-danger align-middle" type="button" data-bs-toggle="modal" data-bs-target="#deleteWorkflowExecutionModal" @click="executionsState.executionToDelete = execution" > <font-awesome-icon icon="fa-solid fa-trash" /> <span class="ms-1">Delete</span> </button> </li> </ul> </div> </td> </tr> </template> <tr v-else> <td colspan="5" class="text-center"><i>No workflow executions</i></td> </tr> </tbody> </table> </template> <style scoped></style>