-
Daniel Göbel authored
#49
Daniel Göbel authored#49
ListWorkflowExecutionsView.vue 12.37 KiB
<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_version_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 mb-4 justify-content-between align-items-center"
>
<h1 class="mb-2 w-fit">My Workflow Executions</h1>
<div class="w-fit">
<button
class="btn btn-light me-2 shadow-sm border"
@click="updateExecutions"
>
<font-awesome-icon icon="fa-solid fa-arrow-rotate-right" />
</button>
<router-link
:to="{ name: 'workflows' }"
class="btn btn-primary shadow-sm border"
>Start Workflow Execution</router-link
>
</div>
</div>
<table class="table 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">Notes</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: 15%">
<span class="placeholder col-4"></span>
</td>
<td class="placeholder-glow" style="width: 15%">
<span class="placeholder col-6"></span>
</td>
<td class="placeholder-glow" style="width: 15%">
<span class="placeholder col-6"></span>
</td>
<td class="placeholder-glow" style="width: 15%">
<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 border" 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>
<router-link
v-if="execution.workflow_id"
:to="{
name: 'workflow-version',
params: {
versionId: execution.workflow_version_id,
workflowId: execution.workflow_id,
},
}"
>
{{ executionsState.workflowMapping[execution.workflow_id] }}@{{
executionsState.versionMapping[execution.workflow_version_id]
}}
</router-link>
</td>
<td>
<span
class="rounded-pill py-1 px-2 text-light"
: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-truncate">
<template v-if="execution.notes">{{ execution.notes }}</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">
<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="6" class="text-center"><i>No workflow executions</i></td>
</tr>
</tbody>
</table>
</template>
<style scoped></style>