20609: Add subprogress progress bar along with required bootstrap/coreUI styles
authorStephen Smith <stephen@curii.com>
Mon, 6 Nov 2023 14:44:20 +0000 (09:44 -0500)
committerStephen Smith <stephen@curii.com>
Mon, 6 Nov 2023 14:44:20 +0000 (09:44 -0500)
Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen@curii.com>

package.json
src/components/data-explorer/data-explorer.tsx
src/components/subprocess-progress-bar/subprocess-progress-bar.tsx [new file with mode: 0644]
src/index.tsx
src/store/subprocess-panel/subprocess-panel-actions.ts
src/views/process-panel/process-panel-root.tsx
src/views/subprocess-panel/subprocess-panel-root.tsx
src/views/subprocess-panel/subprocess-panel.tsx
yarn.lock

index 35c960c4186d132d2eb477c4ee4495204f142ec4..acc2db6ae1b75e05919cbf7573076b98b9b5c3bc 100644 (file)
@@ -3,6 +3,8 @@
   "version": "0.1.0",
   "private": true,
   "dependencies": {
+    "@coreui/coreui": "next",
+    "@coreui/react": "next",
     "@date-io/date-fns": "1",
     "@fortawesome/fontawesome-svg-core": "1.2.28",
     "@fortawesome/free-solid-svg-icons": "5.13.0",
@@ -27,6 +29,7 @@
     "axios": "^0.21.1",
     "babel-core": "6.26.3",
     "babel-runtime": "6.26.0",
+    "bootstrap": "^5.3.2",
     "caniuse-lite": "1.0.30001299",
     "classnames": "2.2.6",
     "cwlts": "1.15.29",
index ad5762dfeb1bac4bda716b02ff60bdd646c6dbf0..7657ae042b9d39fbc6ef59c7dabfd28b839fc701 100644 (file)
@@ -79,6 +79,7 @@ interface DataExplorerDataProps<T> {
     actions?: React.ReactNode;
     hideSearchInput?: boolean;
     title?: React.ReactNode;
+    toolbar?: React.ReactNode;
     paperKey?: string;
     currentItemUuid: string;
     elementPath?: string;
@@ -181,6 +182,7 @@ export const DataExplorer = withStyles(styles)(
                 fetchMode,
                 currentItemUuid,
                 title,
+                toolbar,
                 doHidePanel,
                 doMaximizePanel,
                 doUnMaximizePanel,
@@ -277,6 +279,7 @@ export const DataExplorer = withStyles(styles)(
                                     <MultiselectToolbar />
                                 </Grid>
                             )}
+                            {toolbar && (toolbar)}
                         </div>
                         <Grid
                             item
diff --git a/src/components/subprocess-progress-bar/subprocess-progress-bar.tsx b/src/components/subprocess-progress-bar/subprocess-progress-bar.tsx
new file mode 100644 (file)
index 0000000..1d467ee
--- /dev/null
@@ -0,0 +1,97 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React, { useEffect, useState } from "react";
+import { StyleRulesCallback, Typography, WithStyles, withStyles } from "@material-ui/core";
+import { CProgressStacked, CProgress } from '@coreui/react';
+import { useAsyncInterval } from "common/use-async-interval";
+import { Process, isProcessRunning } from "store/processes/process";
+import { connect } from "react-redux";
+import { Dispatch } from "redux";
+import { fetchSubprocessProgress } from "store/subprocess-panel/subprocess-panel-actions";
+import { ProcessStatusFilter } from "store/resource-type-filters/resource-type-filters";
+
+type CssRules = 'progressWrapper' | 'progressStacked' ;
+
+const styles: StyleRulesCallback<CssRules> = (theme) => ({
+    progressWrapper: {
+        margin: "0 20px",
+    },
+    progressStacked: {
+        border: "1px solid gray",
+        // Override stripe color to be close to white
+        "& .progress-bar-striped": {
+            backgroundImage:
+                "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)",
+        },
+    },
+});
+
+export interface ProgressBarDataProps {
+    process: Process;
+}
+
+export interface ProgressBarActionProps {
+    fetchSubprocessProgress: (requestingContainerUuid: string) => Promise<ProgressBarData | undefined>;
+}
+
+type ProgressBarProps = ProgressBarDataProps & ProgressBarActionProps & WithStyles<CssRules>;
+
+export type ProgressBarData = {
+    [ProcessStatusFilter.COMPLETED]: number;
+    [ProcessStatusFilter.RUNNING]: number;
+    [ProcessStatusFilter.FAILED]: number;
+    [ProcessStatusFilter.QUEUED]: number;
+};
+
+const mapDispatchToProps = (dispatch: Dispatch): ProgressBarActionProps => ({
+    fetchSubprocessProgress: (requestingContainerUuid: string) => {
+        return dispatch<any>(fetchSubprocessProgress(requestingContainerUuid));
+    },
+});
+
+export const SubprocessProgressBar = connect(null, mapDispatchToProps)(withStyles(styles)(
+    ({process, classes, fetchSubprocessProgress}: ProgressBarProps) => {
+
+        const [progressData, setProgressData] = useState<ProgressBarData|undefined>(undefined);
+        const requestingContainerUuid = process.containerRequest.containerUuid;
+        const isRunning = isProcessRunning(process);
+
+        useAsyncInterval(async () => (
+            requestingContainerUuid && setProgressData(await fetchSubprocessProgress(requestingContainerUuid))
+        ), isRunning ? 5000 : null);
+
+        useEffect(() => {
+            if (!isRunning && requestingContainerUuid) {
+                fetchSubprocessProgress(requestingContainerUuid)
+                    .then(result => setProgressData(result));
+            }
+        }, [fetchSubprocessProgress, isRunning, requestingContainerUuid]);
+
+        return progressData !== undefined && getStatusTotal(progressData) > 0 ? <div className={classes.progressWrapper}>
+            <CProgressStacked className={classes.progressStacked}>
+                <CProgress height={20} color="success" title="Completed"
+                    value={getStatusPercent(progressData, ProcessStatusFilter.COMPLETED)} />
+                <CProgress height={20} color="success" title="Running" variant="striped" animated
+                    value={getStatusPercent(progressData, ProcessStatusFilter.RUNNING)} />
+                <CProgress height={20} color="danger" title="Failed"
+                    value={getStatusPercent(progressData, ProcessStatusFilter.FAILED)} />
+                <CProgress height={20} color="secondary" title="Queued" variant="striped" animated
+                    value={getStatusPercent(progressData, ProcessStatusFilter.QUEUED)} />
+            </CProgressStacked>
+            <Typography variant="body2">
+                {progressData[ProcessStatusFilter.COMPLETED]} Completed, {progressData[ProcessStatusFilter.RUNNING]} Running, {progressData[ProcessStatusFilter.FAILED]} Failed, {progressData[ProcessStatusFilter.QUEUED]} Queued
+            </Typography>
+        </div> : <></>;
+    }
+));
+
+const getStatusTotal = (progressData: ProgressBarData) =>
+    (Object.keys(progressData).reduce((accumulator, key) => (accumulator += progressData[key]), 0));
+
+/**
+ * Gets the integer percent value for process status
+ */
+const getStatusPercent = (progressData: ProgressBarData, status: keyof ProgressBarData) =>
+    (progressData[status] / getStatusTotal(progressData) * 100);
index ede257dc5d10dc89fdbeb94718bcee8d68cc10ae..ef9ff9c98693576880141b679db8aff6f675d24c 100644 (file)
@@ -91,6 +91,9 @@ import { workflowActionSet, readOnlyWorkflowActionSet } from "views-components/c
 import { storeRedirects } from "./common/redirect-to";
 import { searchResultsActionSet } from "views-components/context-menu/action-sets/search-results-action-set";
 
+import 'bootstrap/dist/css/bootstrap.min.css';
+import '@coreui/coreui/dist/css/coreui.min.css';
+
 console.log(`Starting arvados [${getBuildInfo()}]`);
 
 addMenuActionSet(ContextMenuKind.ROOT_PROJECT, rootProjectActionSet);
index b440776ce0ff6c2ddcd02fe34aa696348f907888..68ed453f1a587e5a0c75127fa2e69b18f08f3700 100644 (file)
@@ -6,6 +6,9 @@ import { Dispatch } from 'redux';
 import { RootState } from 'store/store';
 import { ServiceRepository } from 'services/services';
 import { bindDataExplorerActions } from 'store/data-explorer/data-explorer-action';
+import { FilterBuilder } from 'services/api/filter-builder';
+import { ProgressBarData } from 'components/subprocess-progress-bar/subprocess-progress-bar';
+import { ProcessStatusFilter, buildProcessStatusFilters } from 'store/resource-type-filters/resource-type-filters';
 export const SUBPROCESS_PANEL_ID = "subprocessPanel";
 export const SUBPROCESS_ATTRIBUTES_DIALOG = 'subprocessAttributesDialog';
 export const subprocessPanelActions = bindDataExplorerActions(SUBPROCESS_PANEL_ID);
@@ -14,3 +17,52 @@ export const loadSubprocessPanel = () =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(subprocessPanelActions.REQUEST_ITEMS());
     };
+
+type ProcessStatusCount = {
+    status: keyof ProgressBarData;
+    count: number;
+};
+
+export const fetchSubprocessProgress = (requestingContainerUuid: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<ProgressBarData | undefined> => {
+
+        const requestContainerStatusCount = async (fb: FilterBuilder) => {
+            return await services.containerRequestService.list({
+                limit: 0,
+                offset: 0,
+                filters: fb.getFilters(),
+            });
+        }
+
+        if (requestingContainerUuid) {
+            try {
+                const baseFilter = new FilterBuilder().addEqual('requesting_container_uuid', requestingContainerUuid).getFilters();
+
+                // Create return object
+                let result: ProgressBarData = {
+                    [ProcessStatusFilter.COMPLETED]: 0,
+                    [ProcessStatusFilter.RUNNING]: 0,
+                    [ProcessStatusFilter.FAILED]: 0,
+                    [ProcessStatusFilter.QUEUED]: 0,
+                }
+
+                // Create array of promises that returns the status associated with the item count
+                // Helps to make the requests simultaneously while preserving the association with the status key as a typed key
+                const promises = Object.keys(result).map(async (status: keyof ProgressBarData): Promise<ProcessStatusCount> => {
+                    const filter = buildProcessStatusFilters(new FilterBuilder(baseFilter), status);
+                    const count = (await requestContainerStatusCount(filter)).itemsAvailable;
+                    return {status, count};
+                });
+
+                // Simultaneously requests each status count and apply them to the return object
+                (await Promise.all(promises)).forEach((singleResult) => {
+                    result[singleResult.status] = singleResult.count;
+                });
+                return result;
+            } catch (e) {
+                return undefined;
+            }
+        } else {
+            return undefined;
+        }
+    };
index 7a24089901333e5195af66081bf14bb8369e0035..c972c0a6cf9ebf130463c72b39ee69b750970945 100644 (file)
@@ -205,7 +205,7 @@ export const ProcessPanelRoot = withStyles(styles)(
                     xs
                     maxHeight="50%"
                     data-cy="process-children">
-                    <SubprocessPanel />
+                    <SubprocessPanel process={process} />
                 </MPVPanelContent>
             </MPVContainer>
         ) : (
index 9cf1db7753e6a90c8666d5ac4fcee361dff7ce23..33a1027585dff7fab257e67af216f8879eef72ef 100644 (file)
@@ -20,6 +20,8 @@ import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
 import { StyleRulesCallback, Typography, WithStyles, withStyles } from '@material-ui/core';
 import { ArvadosTheme } from 'common/custom-theme';
 import { ProcessResource } from 'models/process';
+import { SubprocessProgressBar } from 'components/subprocess-progress-bar/subprocess-progress-bar';
+import { Process } from 'store/processes/process';
 
 type CssRules = 'iconHeader' | 'cardHeader';
 
@@ -80,6 +82,7 @@ export const subprocessPanelColumns: DataColumns<string, ProcessResource> = [
 ];
 
 export interface SubprocessPanelDataProps {
+    process: Process;
     resources: ResourcesState;
 }
 
@@ -122,5 +125,6 @@ export const SubprocessPanelRoot = (props: SubprocessPanelProps & MPVPanelProps)
         doUnMaximizePanel={props.doUnMaximizePanel}
         panelMaximized={props.panelMaximized}
         panelName={props.panelName}
-        title={<SubProcessesTitle/>} />;
+        title={<SubProcessesTitle/>}
+        toolbar={<SubprocessProgressBar process={props.process} />} />;
 };
index 0aa02d52701824b52d4f9611ebf2d5b49beee131..c52f054b0a0c31e64af3c576420bd43e7b9f27ad 100644 (file)
@@ -26,7 +26,7 @@ const mapDispatchToProps = (dispatch: Dispatch): SubprocessPanelActionProps => (
     },
 });
 
-const mapStateToProps = (state: RootState): SubprocessPanelDataProps => ({
+const mapStateToProps = (state: RootState): Omit<SubprocessPanelDataProps,'process'> => ({
     resources: state.resources,
 });
 
index f9dfa6a9546b954ed10657180b72387ff2e8c508..142694c883e667d2332e216dc57b07a248e8ea21 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
@@ -1646,6 +1646,26 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@coreui/coreui@npm:next":
+  version: 5.0.0-alpha.3
+  resolution: "@coreui/coreui@npm:5.0.0-alpha.3"
+  peerDependencies:
+    "@popperjs/core": ^2.11.8
+  checksum: 2363ad6be775c6a895a49126a5b9062ffa9ebd0bea6dfb835c1300cd122fb1cf18d85fe647a9c08a3a384caa871e761d8ffb28ea45c7872cb2b034df6527da20
+  languageName: node
+  linkType: hard
+
+"@coreui/react@npm:next":
+  version: 5.0.0-alpha.3
+  resolution: "@coreui/react@npm:5.0.0-alpha.3"
+  peerDependencies:
+    "@coreui/coreui": ^5.0.0-alpha.2
+    react: ">=17"
+    react-dom: ">=17"
+  checksum: efd333cc346307219dcf7fe183eed65305b12e71984bcb940d80a55509d7b92523082e37045bfcb8c4b334920ca185128a9f72f3e8bec69d15cad889cbeda4b4
+  languageName: node
+  linkType: hard
+
 "@csstools/convert-colors@npm:^1.4.0":
   version: 1.4.0
   resolution: "@csstools/convert-colors@npm:1.4.0"
@@ -3800,6 +3820,8 @@ __metadata:
   version: 0.0.0-use.local
   resolution: "arvados-workbench-2@workspace:."
   dependencies:
+    "@coreui/coreui": next
+    "@coreui/react": next
     "@date-io/date-fns": 1
     "@fortawesome/fontawesome-svg-core": 1.2.28
     "@fortawesome/free-solid-svg-icons": 5.13.0
@@ -3841,6 +3863,7 @@ __metadata:
     axios-mock-adapter: 1.17.0
     babel-core: 6.26.3
     babel-runtime: 6.26.0
+    bootstrap: ^5.3.2
     caniuse-lite: 1.0.30001299
     classnames: 2.2.6
     cwlts: 1.15.29
@@ -4605,6 +4628,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"bootstrap@npm:^5.3.2":
+  version: 5.3.2
+  resolution: "bootstrap@npm:5.3.2"
+  peerDependencies:
+    "@popperjs/core": ^2.11.8
+  checksum: d5580b253d121ffc137388d41da58dce8d15f1ccd574e12f28d4a08e7649ca15e95db645b2b677cb8025bccd446bff04138fc0fe64f8cba0ccc5dc004a8644cf
+  languageName: node
+  linkType: hard
+
 "brace-expansion@npm:^1.1.7":
   version: 1.1.11
   resolution: "brace-expansion@npm:1.1.11"