diff --git a/reana-ui/src/pages/workflowList/components/WorkflowDetails.js b/reana-ui/src/pages/workflowList/components/WorkflowDetails.js index a0a2de3a..50b664a8 100644 --- a/reana-ui/src/pages/workflowList/components/WorkflowDetails.js +++ b/reana-ui/src/pages/workflowList/components/WorkflowDetails.js @@ -11,12 +11,12 @@ import { Icon, Popup } from "semantic-ui-react"; import PropTypes from "prop-types"; -import { WorkflowActionsPopup } from "~/components"; import { statusMapping } from "~/util"; import styles from "./WorkflowDetails.module.scss"; +import WorkflowProgressCircleBar from "~/pages/workflowList/components/WorkflowProgressCircleBar"; -export default function WorkflowDetails({ workflow, actionsOnHover = false }) { +export default function WorkflowDetails({ workflow }) { const { name, run, @@ -26,7 +26,6 @@ export default function WorkflowDetails({ workflow, actionsOnHover = false }) { friendlyCreated, friendlyStarted, friendlyFinished, - duration, completed, total, status, @@ -69,14 +68,18 @@ export default function WorkflowDetails({ workflow, actionsOnHover = false }) { </div> </div> <div className={styles["status-box"]}> - <span - className={`${styles["status"]} sui-${statusMapping[status].color}`} - > - {status} - </span>{" "} - {statusMapping[status].preposition} {duration} <div> - step {completed}/{total} + <span + className={`${styles["status"]} sui-${statusMapping[status].color}`} + > + {status} + </span>{" "} + <div> + step {completed}/{total} + </div> + </div> + <div className={styles["progressbar-container"]}> + <WorkflowProgressCircleBar workflow={workflow} /> </div> </div> </div> diff --git a/reana-ui/src/pages/workflowList/components/WorkflowDetails.module.scss b/reana-ui/src/pages/workflowList/components/WorkflowDetails.module.scss index 49ff55a5..41f51057 100644 --- a/reana-ui/src/pages/workflowList/components/WorkflowDetails.module.scss +++ b/reana-ui/src/pages/workflowList/components/WorkflowDetails.module.scss @@ -72,18 +72,15 @@ .status-box { width: 210px; flex-shrink: 0; + display: flex; + justify-content: end; + text-align: right; } - .actions { - min-width: 22px; - - &:hover { - color: darken($sepia, 30%); - } - - &.always-visible { - visibility: visible; - } + .progressbar-container { + width: 80px; + padding: 10px; + padding-top: 0; } .notebook { diff --git a/reana-ui/src/pages/workflowList/components/WorkflowProgressCircleBar.js b/reana-ui/src/pages/workflowList/components/WorkflowProgressCircleBar.js new file mode 100644 index 00000000..e894174a --- /dev/null +++ b/reana-ui/src/pages/workflowList/components/WorkflowProgressCircleBar.js @@ -0,0 +1,103 @@ +/* + -*- coding: utf-8 -*- + + This file is part of REANA. + Copyright (C) 2024 CERN. + + REANA is free software; you can redistribute it and/or modify it + under the terms of the MIT License; see LICENSE file for more details. +*/ + +import PropTypes from "prop-types"; +import styles from "./WorkflowProgressCircleBar.module.scss"; + +export default function WorkflowProgressCircleBar({ workflow }) { + const { completed, failed, running, total, status } = workflow; + + const size = 80; + const strokeWidth = 10; + const radius = size / 2 - strokeWidth; + const circumference = 2 * Math.PI * radius; + + let lengthFinishedArc = (completed / total) * circumference; + let lengthRunningArc = (running / total) * circumference; + let lengthFailedArc = (failed / total) * circumference; + // Explicitly set the size of the progress bar for workflows that + // are not running to avoid dealing with undefined number of steps + const TERMINAL_STATUSES = ["finished", "failed", "stopped"]; + const PREPARING_STATUSES = ["created", "queued", "pending"]; + if (TERMINAL_STATUSES.includes(status)) { + lengthRunningArc = 0; + } + if (PREPARING_STATUSES.includes(status)) { + lengthFinishedArc = 0; + lengthRunningArc = 0; + lengthFailedArc = 0; + } + + // The workflow could be completely restored from the cache, in which case + // the total number of steps would be 0. If the workflow is finished, we + // want to show the full progress bar as finished even in this case. + if (status === "finished") { + lengthFinishedArc = circumference; + lengthRunningArc = 0; + lengthFailedArc = 0; + } + + return ( + <div className={styles["progress-bar-container"]}> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox={`0 0 ${size} ${size}`} + width={`${size}`} + height={`${size}`} + > + <circle + cx={`${size / 2}`} + cy={`${size / 2}`} + r={`${radius}`} + className={styles["progress-bar-background"]} + strokeWidth={`${strokeWidth}`} + /> + <circle + cx={`${size / 2}`} + cy={`${size / 2}`} + r={`${radius}`} + className={`${styles["progress-bar-running"]} ${ + styles["progress-bar-workflow-status-" + status] + }`} + strokeDasharray={`${lengthRunningArc} ${circumference}`} + strokeDashoffset={`-${lengthFinishedArc}`} + strokeWidth={`${strokeWidth}`} + /> + <circle + cx={`${size / 2}`} + cy={`${size / 2}`} + r={`${radius}`} + className={`${styles["progress-bar-finished"]} ${ + styles["progress-bar-workflow-status-" + status] + }`} + strokeDasharray={`${lengthFinishedArc} ${circumference}`} + strokeDashoffset="0" + strokeWidth={`${strokeWidth}`} + /> + <circle + cx={`${size / 2}`} + cy={`${size / 2}`} + r={`${radius}`} + className={`${styles["progress-bar-failed"]} ${ + styles["progress-bar-workflow-status-" + status] + }`} + strokeDasharray={`${lengthFailedArc} ${circumference}`} + strokeDashoffset={`-${lengthFinishedArc}`} + strokeWidth={`${strokeWidth}`} + /> + </svg> + </div> + ); +} + +WorkflowProgressCircleBar.propTypes = { + workflow: PropTypes.object.isRequired, + size: PropTypes.number, +}; diff --git a/reana-ui/src/pages/workflowList/components/WorkflowProgressCircleBar.module.scss b/reana-ui/src/pages/workflowList/components/WorkflowProgressCircleBar.module.scss new file mode 100644 index 00000000..4236806b --- /dev/null +++ b/reana-ui/src/pages/workflowList/components/WorkflowProgressCircleBar.module.scss @@ -0,0 +1,41 @@ +@import "@palette"; + +.progress-bar-container { + svg { + width: 100%; + height: 100%; + transform: rotate(-90deg); + } + + circle { + fill: none; + transition: stroke-dasharray 0.35s ease; + } + + .progress-bar-running { + stroke: $sui-blue; + } + + .progress-bar-finished { + stroke: $sui-green; + } + + .progress-bar-failed { + stroke: $sui-red; + } + + .progress-bar-background, + .progress-bar-workflow-status-queued, + .progress-bar-workflow-status-pending, + .progress-bar-workflow-status-created { + stroke: $light-gray; + } + + .progress-bar-workflow-status-deleted { + stroke: $gray; + } + + .progress-bar-workflow-status-stopped { + stroke: $sui-yellow !important; + } +} diff --git a/reana-ui/src/util.js b/reana-ui/src/util.js index 619b5ef3..1416cfce 100644 --- a/reana-ui/src/util.js +++ b/reana-ui/src/util.js @@ -53,8 +53,12 @@ export function parseWorkflows(workflows) { workflow.run = info.join("."); const progress = workflow.progress.finished; const total = workflow.progress.total; + const running = workflow.progress.running; + const failed = workflow.progress.failed; workflow.completed = typeof progress === "object" ? progress.total : 0; workflow.total = total.total; + workflow.running = typeof running === "object" ? running.total : 0; + workflow.failed = typeof failed === "object" ? failed.total : 0; workflow.launcherURL = workflow.launcher_url; workflow = parseWorkflowDates(workflow);