Merge branch '21925-progress-bar-useeffect-fix' into main. Closes #21925
[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 { fetchProcessProgressBarStatus, isProcess } from "store/subprocess-panel/subprocess-panel-actions";
13 import { ProcessStatusFilter, serializeOnlyProcessTypeFilters } from "store/resource-type-filters/resource-type-filters";
14 import { ProjectResource } from "models/project";
15 import { getDataExplorer } from "store/data-explorer/data-explorer-reducer";
16 import { RootState } from "store/store";
17 import { ProcessResource } from "models/process";
18 import { getDataExplorerColumnFilters } from "store/data-explorer/data-explorer-middleware-service";
19 import { ProjectPanelRunColumnNames } from "views/project-panel/project-panel-run";
20 import { DataColumns } from "components/data-table/data-table";
21
22 type CssRules = 'progressStacked';
23
24 const styles: StyleRulesCallback<CssRules> = (theme) => ({
25     progressStacked: {
26         border: "1px solid gray",
27         height: "10px",
28         // Override stripe color to be close to white
29         "& .progress-bar-striped": {
30             backgroundImage:
31                 "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)",
32         },
33     },
34 });
35
36 export interface ProgressBarDataProps {
37     parentResource: Process | ProjectResource | undefined;
38     dataExplorerId?: string;
39     typeFilter?: string;
40 }
41
42 export interface ProgressBarActionProps {
43     fetchProcessProgressBarStatus: (parentResourceUuid: string, typeFilter?: string) => Promise<ProgressBarStatus | undefined>;
44 }
45
46 type ProgressBarProps = ProgressBarDataProps & ProgressBarActionProps & WithStyles<CssRules>;
47
48 export type ProgressBarCounts = {
49     [ProcessStatusFilter.COMPLETED]: number;
50     [ProcessStatusFilter.RUNNING]: number;
51     [ProcessStatusFilter.FAILED]: number;
52     [ProcessStatusFilter.QUEUED]: number;
53 };
54
55 export type ProgressBarStatus = {
56     counts: ProgressBarCounts;
57     shouldPollProject: boolean;
58 };
59
60 const mapStateToProps = (state: RootState, props: ProgressBarDataProps) => {
61     let typeFilter: string | undefined = undefined;
62
63     if (props.dataExplorerId) {
64         const dataExplorerState = getDataExplorer(state.dataExplorer, props.dataExplorerId);
65         const columns = dataExplorerState.columns as DataColumns<string, ProcessResource>;
66         typeFilter = serializeOnlyProcessTypeFilters(getDataExplorerColumnFilters(columns, ProjectPanelRunColumnNames.TYPE));
67     }
68
69     return { typeFilter };
70 };
71
72 const mapDispatchToProps = (dispatch: Dispatch): ProgressBarActionProps => ({
73     fetchProcessProgressBarStatus: (parentResourceUuid: string, typeFilter?: string) => {
74         return dispatch<any>(fetchProcessProgressBarStatus(parentResourceUuid, typeFilter));
75     },
76 });
77
78 export const SubprocessProgressBar = connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(
79     ({ parentResource, typeFilter, classes, fetchProcessProgressBarStatus }: ProgressBarProps) => {
80
81         const [progressCounts, setProgressData] = useState<ProgressBarCounts | undefined>(undefined);
82         const [shouldPollProject, setShouldPollProject] = useState<boolean>(false);
83         const shouldPollProcess = isProcess(parentResource) ? isProcessRunning(parentResource) : false;
84
85         // Should polling be active based on container status
86         // or result of aggregated project process contents
87         const shouldPoll = shouldPollProject || shouldPollProcess;
88
89         const parentUuid = parentResource
90             ? isProcess(parentResource)
91                 ? parentResource.containerRequest.uuid
92                 : parentResource.uuid
93             : "";
94
95         // Runs periodically whenever polling should be happeing
96         // Either when the workflow is running (shouldPollProcess) or when the
97         //   project contains steps in an active state (shouldPollProject)
98         useAsyncInterval(async () => {
99             if (parentUuid) {
100                 fetchProcessProgressBarStatus(parentUuid, typeFilter)
101                     .then(result => {
102                         if (result) {
103                             setProgressData(result.counts);
104                             setShouldPollProject(result.shouldPollProject);
105                         }
106                     });
107             }
108         }, shouldPoll ? 5000 : null);
109
110         // Runs fetch on first load for processes and projects, except when
111         //   process is running since polling will be enabled by shouldPoll.
112         // Project polling starts false so this is still needed for project
113         //   initial load to set shouldPollProject and kick off shouldPoll
114         // Watches shouldPollProcess but not shouldPollProject
115         //   * This runs a final fetch when process ends & is updated through
116         //     websocket / store
117         //   * We ignore shouldPollProject entirely since it changes to false
118         //     as a result of a fetch so the data is already up to date
119         useEffect(() => {
120             if (!shouldPollProcess && parentUuid) {
121                 fetchProcessProgressBarStatus(parentUuid, typeFilter)
122                     .then(result => {
123                         if (result) {
124                             setProgressData(result.counts);
125                             setShouldPollProject(result.shouldPollProject);
126                         }
127                     });
128             }
129         }, [fetchProcessProgressBarStatus, shouldPollProcess, parentUuid, typeFilter]);
130
131         let tooltip = "";
132         if (progressCounts) {
133             let total = 0;
134             [ProcessStatusFilter.COMPLETED,
135             ProcessStatusFilter.RUNNING,
136             ProcessStatusFilter.FAILED,
137             ProcessStatusFilter.QUEUED].forEach(psf => {
138                 if (progressCounts[psf] > 0) {
139                     if (tooltip.length > 0) { tooltip += ", "; }
140                     tooltip += `${progressCounts[psf]} ${psf}`;
141                     total += progressCounts[psf];
142                 }
143             });
144             if (total > 0) {
145                 if (tooltip.length > 0) { tooltip += ", "; }
146                 tooltip += `${total} Total`;
147             }
148         }
149
150         return progressCounts !== undefined && getStatusTotal(progressCounts) > 0 ? <Tooltip title={tooltip}>
151             <CProgressStacked className={classes.progressStacked}>
152                 <CProgress height={10} color="success"
153                     value={getStatusPercent(progressCounts, ProcessStatusFilter.COMPLETED)} />
154                 <CProgress height={10} color="success" variant="striped"
155                     value={getStatusPercent(progressCounts, ProcessStatusFilter.RUNNING)} />
156                 <CProgress height={10} color="danger"
157                     value={getStatusPercent(progressCounts, ProcessStatusFilter.FAILED)} />
158                 <CProgress height={10} color="secondary" variant="striped"
159                     value={getStatusPercent(progressCounts, ProcessStatusFilter.QUEUED)} />
160             </CProgressStacked>
161         </Tooltip> : <></>;
162     }
163 ));
164
165 const getStatusTotal = (progressCounts: ProgressBarCounts) =>
166     (Object.keys(progressCounts).reduce((accumulator, key) => (accumulator += progressCounts[key]), 0));
167
168 /**
169  * Gets the integer percent value for process status
170  */
171 const getStatusPercent = (progressCounts: ProgressBarCounts, status: keyof ProgressBarCounts) =>
172     (progressCounts[status] / getStatusTotal(progressCounts) * 100);