This is an automated email from the ASF dual-hosted git repository.
yasithdev pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airavata-portals.git
The following commit(s) were added to refs/heads/main by this push:
new 50e5b2259 feat(workspace): show experiment progress as a timeline
(#226)
50e5b2259 is described below
commit 50e5b2259e85e1fa21613b1f36a4a66c35bb06a5
Author: Yasith Jayawardana <[email protected]>
AuthorDate: Sun Jun 14 03:01:24 2026 -0400
feat(workspace): show experiment progress as a timeline (#226)
Render the experiment task pipeline as a vertical timeline of status nodes
— green=completed, red=failed, gray=not-started, and a blue spinner while
running — connected by a line, instead of text status badges. Rename the
section to 'Progress', drop the raw task id from the status text, and surface
it as a hover tooltip on each node.
---
.../js/components/experiment/ExperimentSummary.vue | 184 ++++++++++++++++-----
1 file changed, 146 insertions(+), 38 deletions(-)
diff --git
a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentSummary.vue
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentSummary.vue
index 295acfe43..7d7942733 100644
---
a/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentSummary.vue
+++
b/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/experiment/ExperimentSummary.vue
@@ -116,21 +116,31 @@
</td>
</tr>
<tr v-if="stages.length > 0">
- <th scope="row">Tasks</th>
+ <th scope="row">Progress</th>
<td>
- <ul class="list-unstyled mb-0">
+ <ul class="timeline list-unstyled mb-0">
<li
v-for="stage in stages"
:key="stage.taskId"
- class="d-flex align-items-start mb-2"
+ class="timeline-item"
>
- <b-badge
- :variant="stage.variant"
- class="mr-2 mt-1 text-uppercase"
- style="min-width: 6rem"
- >{{ stage.stateLabel }}</b-badge
- >
- <div>
+ <div class="timeline-marker">
+ <span
+ v-if="stage.kind === 'running'"
+ class="timeline-node timeline-node--running"
+ :title="stage.taskId"
+ >
+ <i class="fa fa-circle-notch fa-spin"></i>
+ </span>
+ <span
+ v-else
+ class="timeline-node timeline-dot"
+ :class="'timeline-dot--' + stage.kind"
+ :title="stage.taskId"
+ ></span>
+ <span class="sr-only">{{ stage.stateLabel }}</span>
+ </div>
+ <div class="timeline-content">
<strong>{{ stage.typeLabel }}</strong>
<span v-if="stage.reason" class="text-muted">
— {{ stage.reason }}</span
@@ -138,13 +148,11 @@
<small v-if="stage.time" class="text-muted
d-block">{{
stage.time
}}</small>
- <div v-if="stage.job" class="mt-1">
- <b-badge
- :variant="stage.job.variant"
- class="mr-2 text-uppercase"
- style="min-width: 6rem"
- >{{ stage.job.stateLabel }}</b-badge
- >
+ <div v-if="stage.job" class="mt-1 small">
+ <span
+ class="timeline-dot timeline-dot--inline"
+ :class="'timeline-dot--' + stage.job.kind"
+ ></span>
<span class="text-muted"
>Job {{ stage.job.name }} (ID
{{ stage.job.id }})</span
@@ -410,8 +418,8 @@ export default {
taskId: task.task_id,
typeLabel: this.taskTypeLabel(task.task_type),
stateLabel: this.taskStateLabel(stateName),
- variant: this.taskStateVariant(stateName),
- reason: latest ? latest.reason : "",
+ kind: this.taskStateKind(stateName),
+ reason: this.cleanReason(latest ? latest.reason : ""),
time:
latest && latest.time_of_state_change
? moment(latest.time_of_state_change).fromNow()
@@ -426,8 +434,8 @@ export default {
id: job.job_id,
name: job.job_name,
stateLabel: this.titleCase(jobStateName) || "Pending",
- variant: this.jobStateVariant(jobStateName),
- reason: js ? js.reason : "",
+ kind: this.jobStateKind(jobStateName),
+ reason: this.cleanReason(js ? js.reason : ""),
};
}
result.push(stage);
@@ -536,41 +544,141 @@ export default {
if (!stateName) return "Pending";
return this.titleCase(stateName.replace(/^TASK_STATE_/, ""));
},
- taskStateVariant(stateName) {
+ // Normalize task/job states to a small set of timeline "kinds" that map
to a dot
+ // color (completed=green, failed=red, canceled=yellow, pending=gray) or a
spinner
+ // (running) in the Progress timeline.
+ taskStateKind(stateName) {
switch (stateName) {
case "TASK_STATE_COMPLETED":
- return "success";
+ return "completed";
case "TASK_STATE_EXECUTING":
- return "info";
+ return "running";
case "TASK_STATE_FAILED":
- return "danger";
+ return "failed";
case "TASK_STATE_CANCELED":
- return "warning";
- case "TASK_STATE_CREATED":
- return "secondary";
+ return "canceled";
default:
- return "light";
+ // TASK_STATE_CREATED, null, or anything not yet started.
+ return "pending";
}
},
- jobStateVariant(jobStateName) {
+ jobStateKind(jobStateName) {
switch (jobStateName) {
case "COMPLETE":
- return "success";
+ return "completed";
case "ACTIVE":
- return "info";
- case "SUBMITTED":
- case "QUEUED":
- return "secondary";
+ return "running";
case "FAILED":
case "NON_CRITICAL_FAIL":
- return "danger";
+ return "failed";
case "CANCELED":
case "SUSPENDED":
- return "warning";
+ return "canceled";
default:
- return "light";
+ // SUBMITTED, QUEUED, null — waiting to start.
+ return "pending";
}
},
+ cleanReason(reason) {
+ if (!reason) return "";
+ // The server embeds the raw task id in some status reasons; the id is
surfaced via
+ // the timeline node tooltip instead, so strip it from the
human-readable text.
+ return reason
+ .replace(/\bTASK_[0-9A-Za-z-]+/g, "")
+ .replace(/\s{2,}/g, " ")
+ .trim();
+ },
},
};
</script>
+
+<style scoped>
+/* Vertical timeline for the experiment Progress (PROCESS -> TASK pipeline).
Each task is a
+ node (colored dot, or a spinner while running) connected by a vertical
line. */
+.timeline {
+ position: relative;
+}
+.timeline-item {
+ display: flex;
+ align-items: stretch;
+}
+.timeline-item:not(:last-child) {
+ padding-bottom: 0.75rem;
+}
+.timeline-marker {
+ position: relative;
+ flex: 0 0 1.25rem;
+ display: flex;
+ justify-content: center;
+}
+/* the connector line running vertically through the nodes */
+.timeline-marker::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 50%;
+ width: 2px;
+ margin-left: -1px;
+ background-color: #dee2e6;
+}
+/* trim the line so it starts/ends at the first/last node instead of
overshooting */
+.timeline-item:first-child .timeline-marker::before {
+ top: 0.7rem;
+}
+.timeline-item:last-child .timeline-marker::before {
+ bottom: auto;
+ height: 0.7rem;
+}
+.timeline-node {
+ position: relative;
+ z-index: 1;
+ margin-top: 0.2rem;
+ box-shadow: 0 0 0 2px #fff;
+}
+.timeline-dot {
+ width: 0.85rem;
+ height: 0.85rem;
+ border-radius: 50%;
+ background-color: #adb5bd;
+}
+.timeline-dot--completed {
+ background-color: #28a745;
+}
+.timeline-dot--failed {
+ background-color: #dc3545;
+}
+.timeline-dot--canceled {
+ background-color: #ffc107;
+}
+.timeline-dot--running {
+ background-color: #007bff;
+}
+.timeline-dot--pending {
+ background-color: #adb5bd;
+}
+.timeline-node--running {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 1rem;
+ height: 1rem;
+ border-radius: 50%;
+ background-color: #fff;
+ color: #007bff;
+ font-size: 0.95rem;
+}
+.timeline-content {
+ padding-left: 0.5rem;
+}
+/* small inline dot used for the nested SLURM job state */
+.timeline-dot--inline {
+ display: inline-block;
+ width: 0.6rem;
+ height: 0.6rem;
+ margin-top: 0;
+ margin-right: 0.25rem;
+ box-shadow: none;
+ vertical-align: middle;
+}
+</style>