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