Skip to content
Snippets Groups Projects
Commit 38ea8ef3 authored by Daniel Göbel's avatar Daniel Göbel
Browse files

Resolve "Create List Workflow Executions Page"

parent e9341dae
2 merge requests!84Remove development branch,!43Resolve "Create List Workflow Executions Page"
...@@ -19,12 +19,10 @@ body { ...@@ -19,12 +19,10 @@ body {
cursor: pointer; cursor: pointer;
} }
.helpTextCode > pre { pre {
border: thin solid var(--bs-secondary); backdrop-filter: brightness(1.4);
border-radius: var(--bs-border-radius); padding: .5rem;
background: var(--bs-dark);
filter: brightness(0.9);
padding: 1rem;
margin: 1em 2em;
color: var(--bs-code-color); color: var(--bs-code-color);
width: fit-content;
min-width: 50%;
} }
...@@ -33,5 +33,9 @@ export type WorkflowExecutionOut = { ...@@ -33,5 +33,9 @@ export type WorkflowExecutionOut = {
* Status of the workflow execution * Status of the workflow execution
*/ */
status: WorkflowExecutionStatus; status: WorkflowExecutionStatus;
/**
* Id of the workflow
*/
workflow_id: string;
}; };
<template> <template>
<div <span
class="align-middle" class="align-middle"
:class="icon" :class="icon"
:style="{ :style="{
color: props.fill, color: props.fill,
}" }"
></div> ></span>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
......
...@@ -7,7 +7,11 @@ const props = defineProps<{ ...@@ -7,7 +7,11 @@ const props = defineProps<{
markdown: string; markdown: string;
}>(); }>();
const converter = new showdown.Converter(); const converter = new showdown.Converter({
tables: true,
ghCodeBLocks: false,
});
converter.setFlavor("github");
const outputHtml = computed(() => { const outputHtml = computed(() => {
const dirtyHTML = converter.makeHtml(props.markdown); const dirtyHTML = converter.makeHtml(props.markdown);
return DOMPurify.sanitize(dirtyHTML); return DOMPurify.sanitize(dirtyHTML);
...@@ -18,4 +22,4 @@ const outputHtml = computed(() => { ...@@ -18,4 +22,4 @@ const outputHtml = computed(() => {
<div v-html="outputHtml"></div> <div v-html="outputHtml"></div>
</template> </template>
<style scoped></style> <style></style>
...@@ -123,7 +123,11 @@ watch( ...@@ -123,7 +123,11 @@ watch(
> >
</li> </li>
<li> <li>
<a class="dropdown-item" href="#">Executions</a> <router-link
class="dropdown-item"
:to="{ name: 'workflow-executions' }"
>My Executions</router-link
>
</li> </li>
<li <li
v-if=" v-if="
......
...@@ -8,6 +8,7 @@ import Ajv from "ajv"; ...@@ -8,6 +8,7 @@ import Ajv from "ajv";
import type { ValidateFunction } from "ajv"; import type { ValidateFunction } from "ajv";
import ParameterStringInput from "@/components/parameter-schema/form-mode/ParameterStringInput.vue"; import ParameterStringInput from "@/components/parameter-schema/form-mode/ParameterStringInput.vue";
import { Toast } from "bootstrap"; import { Toast } from "bootstrap";
import { useRouter } from "vue-router";
// Props // Props
// ============================================================================= // =============================================================================
...@@ -21,6 +22,8 @@ const props = defineProps({ ...@@ -21,6 +22,8 @@ const props = defineProps({
}, },
}); });
const router = useRouter();
// Bootstrap Elements // Bootstrap Elements
// ============================================================================= // =============================================================================
let errorToast: Toast | null = null; let errorToast: Toast | null = null;
...@@ -142,7 +145,9 @@ function startWorkflow() { ...@@ -142,7 +145,9 @@ function startWorkflow() {
report_output_bucket: formState.report_bucket, report_output_bucket: formState.report_bucket,
}) })
.then(() => { .then(() => {
console.log("Started Workflow"); router.push({
name: "workflow-executions",
});
}) })
.catch((err: ApiError) => { .catch((err: ApiError) => {
console.error(err); console.error(err);
...@@ -354,4 +359,8 @@ onMounted(() => { ...@@ -354,4 +359,8 @@ onMounted(() => {
</div> </div>
</template> </template>
<style scoped></style> <style scoped>
div.card-body {
backdrop-filter: brightness(1.2);
}
</style>
...@@ -131,6 +131,6 @@ li:hover { ...@@ -131,6 +131,6 @@ li:hover {
background: var(--bs-secondary); background: var(--bs-secondary);
} }
a:hover { a:hover {
filter: brightness(0.8); filter: brightness(1.2);
} }
</style> </style>
...@@ -176,7 +176,7 @@ watch( ...@@ -176,7 +176,7 @@ watch(
<style scoped> <style scoped>
div.card-body { div.card-body {
filter: brightness(0.9); backdrop-filter: brightness(1.2);
} }
span.cursor-pointer:hover { span.cursor-pointer:hover {
color: var(--bs-info); color: var(--bs-info);
......
...@@ -28,6 +28,12 @@ const router = createRouter({ ...@@ -28,6 +28,12 @@ const router = createRouter({
name: "s3_keys", name: "s3_keys",
component: () => import("../views/object-storage/S3KeysView.vue"), component: () => import("../views/object-storage/S3KeysView.vue"),
}, },
{
path: "workflow-executions",
name: "workflow-executions",
component: () =>
import("../views/workflows/ListWorkflowExecutionsView.vue"),
},
{ {
path: "workflows", path: "workflows",
name: "workflows", name: "workflows",
......
<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>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment