21224: merge stable Arvados-DCO-1.1-Signed-off-by: Lisa Knox <lisa.knox@curii.com>
[arvados.git] / services / workbench2 / src / components / subprocess-progress-bar / subprocess-progress-bar.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import React, { useEffect, useState } from "react";
6 import { StyleRulesCallback, Tooltip, WithStyles, withStyles } from "@material-ui/core";
7 import { CProgressStacked, CProgress } from '@coreui/react';
8 import { useAsyncInterval } from "common/use-async-interval";
9 import { Process, isProcessRunning } from "store/processes/process";
10 import { connect } from "react-redux";
11 import { Dispatch } from "redux";
12 import { fetchSubprocessProgress } from "store/subprocess-panel/subprocess-panel-actions";
13 import { ProcessStatusFilter } from "store/resource-type-filters/resource-type-filters";
14
15 type CssRules = 'progressWrapper' | 'progressStacked';
16
17 const styles: StyleRulesCallback<CssRules> = (theme) => ({
18     progressWrapper: {
19         margin: "28px 0 0",
20         flexGrow: 1,
21         flexBasis: "100px",
22     },
23     progressStacked: {
24         border: "1px solid gray",
25         height: "10px",
26         // Override stripe color to be close to white
27         "& .progress-bar-striped": {
28             backgroundImage:
29                 "linear-gradient(45deg,rgba(255,255,255,.80) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.80) 50%,rgba(255,255,255,.80) 75%,transparent 75%,transparent)",
30         },
31     },
32 });
33
34 export interface ProgressBarDataProps {
35     process: Process;
36 }
37
38 export interface ProgressBarActionProps {
39     fetchSubprocessProgress: (requestingContainerUuid: string) => Promise<ProgressBarData | undefined>;
40 }
41
42 type ProgressBarProps = ProgressBarDataProps & ProgressBarActionProps & WithStyles<CssRules>;
43
44 export type ProgressBarData = {
45     [ProcessStatusFilter.COMPLETED]: number;
46     [ProcessStatusFilter.RUNNING]: number;
47     [ProcessStatusFilter.FAILED]: number;
48     [ProcessStatusFilter.QUEUED]: number;
49 };
50
51 const mapDispatchToProps = (dispatch: Dispatch): ProgressBarActionProps => ({
52     fetchSubprocessProgress: (requestingContainerUuid: string) => {
53         return dispatch<any>(fetchSubprocessProgress(requestingContainerUuid));
54     },
55 });
56
57 export const SubprocessProgressBar = connect(null, mapDispatchToProps)(withStyles(styles)(
58     ({ process, classes, fetchSubprocessProgress }: ProgressBarProps) => {
59
60         const [progressData, setProgressData] = useState<ProgressBarData | undefined>(undefined);
61         const requestingContainerUuid = process.containerRequest.containerUuid;
62         const isRunning = isProcessRunning(process);
63
64         useAsyncInterval(async () => (
65             requestingContainerUuid && setProgressData(await fetchSubprocessProgress(requestingContainerUuid))
66         ), isRunning ? 5000 : null);
67
68         useEffect(() => {
69             if (!isRunning && requestingContainerUuid) {
70                 fetchSubprocessProgress(requestingContainerUuid)
71                     .then(result => setProgressData(result));
72             }
73         }, [fetchSubprocessProgress, isRunning, requestingContainerUuid]);
74
75         let tooltip = "";
76         if (progressData) {
77             let total = 0;
78             [ProcessStatusFilter.COMPLETED,
79             ProcessStatusFilter.RUNNING,
80             ProcessStatusFilter.FAILED,
81             ProcessStatusFilter.QUEUED].forEach(psf => {
82                 if (progressData[psf] > 0) {
83                     if (tooltip.length > 0) { tooltip += ", "; }
84                     tooltip += `${progressData[psf]} ${psf}`;
85                     total += progressData[psf];
86                 }
87             });
88             if (total > 0) {
89                 if (tooltip.length > 0) { tooltip += ", "; }
90                 tooltip += `${total} Total`;
91             }
92         }
93
94         return progressData !== undefined && getStatusTotal(progressData) > 0 ? <div className={classes.progressWrapper}>
95             <Tooltip title={tooltip}>
96                 <CProgressStacked className={classes.progressStacked}>
97                     <CProgress height={10} color="success"
98                         value={getStatusPercent(progressData, ProcessStatusFilter.COMPLETED)} />
99                     <CProgress height={10} color="success" variant="striped"
100                         value={getStatusPercent(progressData, ProcessStatusFilter.RUNNING)} />
101                     <CProgress height={10} color="danger"
102                         value={getStatusPercent(progressData, ProcessStatusFilter.FAILED)} />
103                     <CProgress height={10} color="secondary" variant="striped"
104                         value={getStatusPercent(progressData, ProcessStatusFilter.QUEUED)} />
105                 </CProgressStacked>
106             </Tooltip>
107         </div> : <></>;
108     }
109 ));
110
111 const getStatusTotal = (progressData: ProgressBarData) =>
112     (Object.keys(progressData).reduce((accumulator, key) => (accumulator += progressData[key]), 0));
113
114 /**
115  * Gets the integer percent value for process status
116  */
117 const getStatusPercent = (progressData: ProgressBarData, status: keyof ProgressBarData) =>
118     (progressData[status] / getStatusTotal(progressData) * 100);