[arvados-workbench2] created: 2.7.0-197-gdcf2e835

git repository hosting git at public.arvados.org
Tue Nov 14 14:08:46 UTC 2023


        at  dcf2e835b33e926073dad1f636cf92a95493ca0b (commit)


commit dcf2e835b33e926073dad1f636cf92a95493ca0b
Author: Stephen Smith <stephen at curii.com>
Date:   Tue Nov 14 09:07:08 2023 -0500

    20609: Change subprocess progress bar to combine relevant process statuses
    
    eg. Queued segment = Queued + OnHold, Failed segment = Failed + Cancelled
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/components/subprocess-progress-bar/subprocess-progress-bar.test.tsx b/src/components/subprocess-progress-bar/subprocess-progress-bar.test.tsx
index 83737178..bd8603f9 100644
--- a/src/components/subprocess-progress-bar/subprocess-progress-bar.test.tsx
+++ b/src/components/subprocess-progress-bar/subprocess-progress-bar.test.tsx
@@ -73,8 +73,14 @@ describe("<SubprocessProgressBar />", () => {
         statusResponse = {
             [ProcessStatusFilter.COMPLETED]: 100,
             [ProcessStatusFilter.RUNNING]: 200,
-            [ProcessStatusFilter.FAILED]: 300,
-            [ProcessStatusFilter.QUEUED]: 400,
+
+            // Combined into failed segment
+            [ProcessStatusFilter.FAILED]: 200,
+            [ProcessStatusFilter.CANCELLED]: 100,
+
+            // Combined into queued segment
+            [ProcessStatusFilter.QUEUED]: 300,
+            [ProcessStatusFilter.ONHOLD]: 100,
         };
 
         services.containerRequestService.list = createMockListFunc(process.containerRequest.containerUuid);
@@ -88,12 +94,14 @@ describe("<SubprocessProgressBar />", () => {
         });
         await progressBar.update();
 
-        // expects 4 subprocess status list requests
+        // expects 6 subprocess status list requests
         const expectedFilters = [
             ProcessStatusFilter.COMPLETED,
             ProcessStatusFilter.RUNNING,
             ProcessStatusFilter.FAILED,
+            ProcessStatusFilter.CANCELLED,
             ProcessStatusFilter.QUEUED,
+            ProcessStatusFilter.ONHOLD,
         ].map((state) =>
             buildProcessStatusFilters(
                 new FilterBuilder().addEqual(
@@ -128,8 +136,12 @@ describe("<SubprocessProgressBar />", () => {
         statusResponse = {
             [ProcessStatusFilter.COMPLETED]: 50,
             [ProcessStatusFilter.RUNNING]: 55,
-            [ProcessStatusFilter.FAILED]: 60,
-            [ProcessStatusFilter.QUEUED]: 335,
+
+            [ProcessStatusFilter.FAILED]: 30,
+            [ProcessStatusFilter.CANCELLED]: 30,
+
+            [ProcessStatusFilter.QUEUED]: 235,
+            [ProcessStatusFilter.ONHOLD]: 100,
         };
 
         services.containerRequestService.list = createMockListFunc(process.containerRequest.containerUuid);
diff --git a/src/store/subprocess-panel/subprocess-panel-actions.ts b/src/store/subprocess-panel/subprocess-panel-actions.ts
index 68ed453f..a67dd1f4 100644
--- a/src/store/subprocess-panel/subprocess-panel-actions.ts
+++ b/src/store/subprocess-panel/subprocess-panel-actions.ts
@@ -18,11 +18,35 @@ export const loadSubprocessPanel = () =>
         dispatch(subprocessPanelActions.REQUEST_ITEMS());
     };
 
-type ProcessStatusCount = {
+/**
+ * Holds a ProgressBarData status type and process count result
+ */
+type ProcessStatusBarCount = {
     status: keyof ProgressBarData;
     count: number;
 };
 
+/**
+ * Associates each of the limited progress bar segment types with an array of
+ * ProcessStatusFilterTypes to be combined when displayed
+ */
+type ProcessStatusMap = Record<keyof ProgressBarData, ProcessStatusFilter[]>;
+
+const statusMap: ProcessStatusMap = {
+        [ProcessStatusFilter.COMPLETED]: [ProcessStatusFilter.COMPLETED],
+        [ProcessStatusFilter.RUNNING]: [ProcessStatusFilter.RUNNING],
+        [ProcessStatusFilter.FAILED]: [ProcessStatusFilter.FAILED, ProcessStatusFilter.CANCELLED],
+        [ProcessStatusFilter.QUEUED]: [ProcessStatusFilter.QUEUED, ProcessStatusFilter.ONHOLD],
+};
+
+/**
+ * Utility type to hold a pair of associated progress bar status and process status
+ */
+type ProgressBarStatusPair = {
+    barStatus: keyof ProcessStatusMap;
+    processStatus: ProcessStatusFilter;
+};
+
 export const fetchSubprocessProgress = (requestingContainerUuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<ProgressBarData | undefined> => {
 
@@ -48,15 +72,20 @@ export const fetchSubprocessProgress = (requestingContainerUuid: string) =>
 
                 // 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};
-                });
+                const promises = (Object.keys(statusMap) as Array<keyof ProcessStatusMap>)
+                    // Split statusMap into pairs of progress bar status and process status
+                    .reduce((acc, curr) => [...acc, ...statusMap[curr].map(processStatus => ({barStatus: curr, processStatus}))], [] as ProgressBarStatusPair[])
+                    .map(async (statusPair: ProgressBarStatusPair): Promise<ProcessStatusBarCount> => {
+                        // For each status pair, request count and return bar status and count
+                        const { barStatus, processStatus } = statusPair;
+                        const filter = buildProcessStatusFilters(new FilterBuilder(baseFilter), processStatus);
+                        const count = (await requestContainerStatusCount(filter)).itemsAvailable;
+                        return {status: barStatus, count};
+                    });
 
                 // Simultaneously requests each status count and apply them to the return object
                 (await Promise.all(promises)).forEach((singleResult) => {
-                    result[singleResult.status] = singleResult.count;
+                    result[singleResult.status] += singleResult.count;
                 });
                 return result;
             } catch (e) {

commit 356d030ba429f793d4dd5d9997395cb0a7125514
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Nov 13 10:42:44 2023 -0500

    20609: Move subprocess progress bar between title and headermenu
    
    Also make multi select toolbar show up in header or below header depending on
    whether there is a panel title / progress bar
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/components/data-explorer/data-explorer.tsx b/src/components/data-explorer/data-explorer.tsx
index 7657ae04..8f566192 100644
--- a/src/components/data-explorer/data-explorer.tsx
+++ b/src/components/data-explorer/data-explorer.tsx
@@ -17,15 +17,20 @@ import { CloseIcon, IconType, MaximizeIcon, UnMaximizeIcon, MoreVerticalIcon } f
 import { PaperProps } from "@material-ui/core/Paper";
 import { MPVPanelProps } from "components/multi-panel-view/multi-panel-view";
 
-type CssRules = "searchBox" | "headerMenu" | "toolbar" | "footer" | "root" | "moreOptionsButton" | "title" | "dataTable" | "container";
+type CssRules = "titleWrapper" | "searchBox" | "headerMenu" | "toolbar" | "footer" | "root" | "moreOptionsButton" | "title" | "dataTable" | "container";
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    titleWrapper: {
+        display: "flex",
+        justifyContent: "space-between",
+    },
     searchBox: {
         paddingBottom: 0,
     },
     toolbar: {
         paddingTop: 0,
         paddingRight: theme.spacing.unit,
+        paddingLeft: "10px",
     },
     footer: {
         overflow: "auto",
@@ -41,6 +46,8 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         paddingLeft: theme.spacing.unit * 2,
         paddingTop: theme.spacing.unit * 2,
         fontSize: "18px",
+        flexGrow: 0,
+        paddingRight: "10px",
     },
     dataTable: {
         height: "100%",
@@ -50,11 +57,9 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         height: "100%",
     },
     headerMenu: {
-        width: "100%",
-        float: "right",
-        display: "flex",
-        flexDirection: "row-reverse",
-        justifyContent: "space-between",
+        marginLeft: "auto",
+        flexBasis: "initial",
+        flexGrow: 0,
     },
 });
 
@@ -79,7 +84,7 @@ interface DataExplorerDataProps<T> {
     actions?: React.ReactNode;
     hideSearchInput?: boolean;
     title?: React.ReactNode;
-    toolbar?: React.ReactNode;
+    progressBar?: React.ReactNode;
     paperKey?: string;
     currentItemUuid: string;
     elementPath?: string;
@@ -114,6 +119,8 @@ export const DataExplorer = withStyles(styles)(
             prevRoute: "",
         };
 
+        multiSelectToolbarInTitle = !this.props.title && !this.props.progressBar;
+
         componentDidUpdate(prevProps: DataExplorerProps<T>) {
             const currentRefresh = this.props.currentRefresh || "";
             const currentRoute = this.props.currentRoute || "";
@@ -182,7 +189,7 @@ export const DataExplorer = withStyles(styles)(
                 fetchMode,
                 currentItemUuid,
                 title,
-                toolbar,
+                progressBar,
                 doHidePanel,
                 doMaximizePanel,
                 doUnMaximizePanel,
@@ -206,7 +213,7 @@ export const DataExplorer = withStyles(styles)(
                         wrap="nowrap"
                         className={classes.container}
                     >
-                        <div>
+                        <div className={classes.titleWrapper}>
                             {title && (
                                 <Grid
                                     item
@@ -216,6 +223,8 @@ export const DataExplorer = withStyles(styles)(
                                     {title}
                                 </Grid>
                             )}
+                            {!!progressBar && progressBar}
+                            {this.multiSelectToolbarInTitle && <MultiselectToolbar />}
                             {(!hideColumnSelector || !hideSearchInput || !!actions) && (
                                 <Grid
                                     className={classes.headerMenu}
@@ -223,25 +232,27 @@ export const DataExplorer = withStyles(styles)(
                                     xs
                                 >
                                     <Toolbar className={classes.toolbar}>
-                                        {!hideSearchInput && (
-                                            <div className={classes.searchBox}>
-                                                {!hideSearchInput && (
-                                                    <SearchInput
-                                                        label={searchLabel}
-                                                        value={searchValue}
-                                                        selfClearProp={""}
-                                                        onSearch={onSearch}
-                                                    />
-                                                )}
-                                            </div>
-                                        )}
-                                        {actions}
-                                        {!hideColumnSelector && (
-                                            <ColumnSelector
-                                                columns={columns}
-                                                onColumnToggle={onColumnToggle}
-                                            />
-                                        )}
+                                        <Grid container justify="space-between" wrap="nowrap" alignItems="center">
+                                            {!hideSearchInput && (
+                                                <div className={classes.searchBox}>
+                                                    {!hideSearchInput && (
+                                                        <SearchInput
+                                                            label={searchLabel}
+                                                            value={searchValue}
+                                                            selfClearProp={""}
+                                                            onSearch={onSearch}
+                                                        />
+                                                    )}
+                                                </div>
+                                            )}
+                                            {actions}
+                                            {!hideColumnSelector && (
+                                                <ColumnSelector
+                                                    columns={columns}
+                                                    onColumnToggle={onColumnToggle}
+                                                />
+                                            )}
+                                        </Grid>
                                         {doUnMaximizePanel && panelMaximized && (
                                             <Tooltip
                                                 title={`Unmaximize ${panelName || "panel"}`}
@@ -276,11 +287,10 @@ export const DataExplorer = withStyles(styles)(
                                             </Tooltip>
                                         )}
                                     </Toolbar>
-                                    <MultiselectToolbar />
                                 </Grid>
                             )}
-                            {toolbar && (toolbar)}
                         </div>
+                        {!this.multiSelectToolbarInTitle && <MultiselectToolbar />}
                         <Grid
                             item
                             xs="auto"
diff --git a/src/components/multiselect-toolbar/MultiselectToolbar.tsx b/src/components/multiselect-toolbar/MultiselectToolbar.tsx
index 3d8ae0c3..4eff8885 100644
--- a/src/components/multiselect-toolbar/MultiselectToolbar.tsx
+++ b/src/components/multiselect-toolbar/MultiselectToolbar.tsx
@@ -26,6 +26,7 @@ type CssRules = "root" | "button";
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
         display: "flex",
+        flexShrink: 0,
         flexDirection: "row",
         width: 0,
         padding: 0,
diff --git a/src/components/subprocess-progress-bar/subprocess-progress-bar.tsx b/src/components/subprocess-progress-bar/subprocess-progress-bar.tsx
index 1d467eea..07178e79 100644
--- a/src/components/subprocess-progress-bar/subprocess-progress-bar.tsx
+++ b/src/components/subprocess-progress-bar/subprocess-progress-bar.tsx
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import React, { useEffect, useState } from "react";
-import { StyleRulesCallback, Typography, WithStyles, withStyles } from "@material-ui/core";
+import { StyleRulesCallback, Tooltip, 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";
@@ -16,10 +16,13 @@ type CssRules = 'progressWrapper' | 'progressStacked' ;
 
 const styles: StyleRulesCallback<CssRules> = (theme) => ({
     progressWrapper: {
-        margin: "0 20px",
+        margin: "25px 0 0",
+        flexGrow: 1,
+        flexBasis: "100px",
     },
     progressStacked: {
         border: "1px solid gray",
+        height: "10px",
         // Override stripe color to be close to white
         "& .progress-bar-striped": {
             backgroundImage:
@@ -71,18 +74,23 @@ export const SubprocessProgressBar = connect(null, mapDispatchToProps)(withStyle
 
         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)} />
+                <Tooltip title={`${progressData[ProcessStatusFilter.COMPLETED]} Completed`}>
+                    <CProgress height={10} color="success"
+                        value={getStatusPercent(progressData, ProcessStatusFilter.COMPLETED)} />
+                </Tooltip>
+                <Tooltip title={`${progressData[ProcessStatusFilter.RUNNING]} Running`}>
+                    <CProgress height={10} color="success" variant="striped"
+                        value={getStatusPercent(progressData, ProcessStatusFilter.RUNNING)} />
+                </Tooltip>
+                <Tooltip title={`${progressData[ProcessStatusFilter.FAILED]} Failed`}>
+                    <CProgress height={10} color="danger"
+                        value={getStatusPercent(progressData, ProcessStatusFilter.FAILED)} />
+                </Tooltip>
+                <Tooltip title={`${progressData[ProcessStatusFilter.QUEUED]} Queued`}>
+                    <CProgress height={10} color="secondary" variant="striped"
+                        value={getStatusPercent(progressData, ProcessStatusFilter.QUEUED)} />
+                </Tooltip>
             </CProgressStacked>
-            <Typography variant="body2">
-                {progressData[ProcessStatusFilter.COMPLETED]} Completed, {progressData[ProcessStatusFilter.RUNNING]} Running, {progressData[ProcessStatusFilter.FAILED]} Failed, {progressData[ProcessStatusFilter.QUEUED]} Queued
-            </Typography>
         </div> : <></>;
     }
 ));
diff --git a/src/views/subprocess-panel/subprocess-panel-root.tsx b/src/views/subprocess-panel/subprocess-panel-root.tsx
index 33a10275..dd5229bb 100644
--- a/src/views/subprocess-panel/subprocess-panel-root.tsx
+++ b/src/views/subprocess-panel/subprocess-panel-root.tsx
@@ -126,5 +126,5 @@ export const SubprocessPanelRoot = (props: SubprocessPanelProps & MPVPanelProps)
         panelMaximized={props.panelMaximized}
         panelName={props.panelName}
         title={<SubProcessesTitle/>}
-        toolbar={<SubprocessProgressBar process={props.process} />} />;
+        progressBar={<SubprocessProgressBar process={props.process} />} />;
 };

commit acbdf56bc7c678796cc4a8d0627ab66fd1edf37f
Author: Stephen Smith <stephen at curii.com>
Date:   Tue Nov 7 15:52:22 2023 -0500

    20609: Update momentjs
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/package.json b/package.json
index fd9cdf78..d21d0675 100644
--- a/package.json
+++ b/package.json
@@ -50,7 +50,7 @@
     "material-ui-pickers": "^2.2.4",
     "mem": "4.0.0",
     "mime": "^3.0.0",
-    "moment": "2.29.1",
+    "moment": "^2.29.4",
     "parse-duration": "0.4.4",
     "prop-types": "15.7.2",
     "query-string": "6.9.0",
diff --git a/yarn.lock b/yarn.lock
index f1fc857f..ef7b9d31 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3888,7 +3888,7 @@ __metadata:
     material-ui-pickers: ^2.2.4
     mem: 4.0.0
     mime: ^3.0.0
-    moment: 2.29.1
+    moment: ^2.29.4
     node-sass: ^9.0.0
     node-sass-chokidar: ^2.0.0
     parse-duration: 0.4.4
@@ -12612,14 +12612,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"moment at npm:2.29.1":
-  version: 2.29.1
-  resolution: "moment at npm:2.29.1"
-  checksum: 1e14d5f422a2687996be11dd2d50c8de3bd577c4a4ca79ba5d02c397242a933e5b941655de6c8cb90ac18f01cc4127e55b4a12ae3c527a6c0a274e455979345e
-  languageName: node
-  linkType: hard
-
-"moment at npm:^2.27.0":
+"moment at npm:^2.27.0, moment at npm:^2.29.4":
   version: 2.29.4
   resolution: "moment at npm:2.29.4"
   checksum: 0ec3f9c2bcba38dc2451b1daed5daded747f17610b92427bebe1d08d48d8b7bdd8d9197500b072d14e326dd0ccf3e326b9e3d07c5895d3d49e39b6803b76e80e

commit 72c70bed4eb3098a92a0deb07841a0b46d9df5bf
Author: Stephen Smith <stephen at curii.com>
Date:   Tue Nov 7 15:52:05 2023 -0500

    20609: Update caniuse
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/yarn.lock b/yarn.lock
index 7b837aab..f1fc857f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5135,17 +5135,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"caniuse-lite at npm:^1.0.0, caniuse-lite at npm:^1.0.30000981, caniuse-lite at npm:^1.0.30001035, caniuse-lite at npm:^1.0.30001109":
-  version: 1.0.30001486
-  resolution: "caniuse-lite at npm:1.0.30001486"
-  checksum: 5e8c2ba2679e4ad17dea6d2761a6449b814441bfeac81af6cc9d58af187df6af4b79b27befcbfc4a557e720b21c0399a7d1911c8705922e38938dcc0f40b5d4b
-  languageName: node
-  linkType: hard
-
-"caniuse-lite at npm:^1.0.30001541":
-  version: 1.0.30001543
-  resolution: "caniuse-lite at npm:1.0.30001543"
-  checksum: 1a65c8b0b93913b6241c7d66e1e1f3ea0f194f7e140eefe500512641c2eb4df285991ec9869a1ba2856ea6f6d21e9f3d7bcd91971b5fb1721e3fa0390feec6f1
+"caniuse-lite at npm:^1.0.0, caniuse-lite at npm:^1.0.30000981, caniuse-lite at npm:^1.0.30001035, caniuse-lite at npm:^1.0.30001109, caniuse-lite at npm:^1.0.30001541":
+  version: 1.0.30001561
+  resolution: "caniuse-lite at npm:1.0.30001561"
+  checksum: 949829fe037e23346595614e01d362130245920503a12677f2506ce68e1240360113d6383febed41e8aa38cd0f5fd9c69c21b0af65a71c0246d560db489f1373
   languageName: node
   linkType: hard
 

commit 82646dcb3d8f2497de1a33d2250101749526662f
Author: Stephen Smith <stephen at curii.com>
Date:   Tue Nov 7 15:50:37 2023 -0500

    20609: Add unit tests for subprocess progress bar, includes node/dom upgrade to 16.14.x
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/cypress/integration/project.spec.js b/cypress/integration/project.spec.js
index a8663d86..e6113821 100644
--- a/cypress/integration/project.spec.js
+++ b/cypress/integration/project.spec.js
@@ -564,7 +564,7 @@ describe("Project tests", function () {
         );
     });
 
-    it.only("sorts displayed items correctly", () => {
+    it("sorts displayed items correctly", () => {
         cy.loginAs(activeUser);
 
         cy.get('[data-cy=project-panel] button[title="Select columns"]').click();
diff --git a/package.json b/package.json
index acc2db6a..fd9cdf78 100644
--- a/package.json
+++ b/package.json
@@ -54,11 +54,11 @@
     "parse-duration": "0.4.4",
     "prop-types": "15.7.2",
     "query-string": "6.9.0",
-    "react": "16.8.6",
+    "react": "16.14.0",
     "react-copy-to-clipboard": "5.0.3",
     "react-dnd": "5.0.0",
     "react-dnd-html5-backend": "5.0.1",
-    "react-dom": "16.8.6",
+    "react-dom": "16.14.0",
     "react-dropzone": "5.1.1",
     "react-highlight-words": "0.14.0",
     "react-idle-timer": "4.3.6",
diff --git a/src/components/subprocess-progress-bar/subprocess-progress-bar.test.tsx b/src/components/subprocess-progress-bar/subprocess-progress-bar.test.tsx
new file mode 100644
index 00000000..83737178
--- /dev/null
+++ b/src/components/subprocess-progress-bar/subprocess-progress-bar.test.tsx
@@ -0,0 +1,153 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from "react";
+import { configure, mount } from "enzyme";
+import { ServiceRepository, createServices } from "services/services";
+import { configureStore } from "store/store";
+import { createBrowserHistory } from "history";
+import { mockConfig } from 'common/config';
+import { ApiActions } from "services/api/api-actions";
+import Axios from "axios";
+import MockAdapter from "axios-mock-adapter";
+import { Process } from "store/processes/process";
+import { ContainerState } from "models/container";
+import Adapter from "enzyme-adapter-react-16";
+import { SubprocessProgressBar } from "./subprocess-progress-bar";
+import { Provider } from "react-redux";
+import { FilterBuilder } from 'services/api/filter-builder';
+import { ProcessStatusFilter, buildProcessStatusFilters } from 'store/resource-type-filters/resource-type-filters';
+import {act} from "react-dom/test-utils";
+
+configure({ adapter: new Adapter() });
+
+describe("<SubprocessProgressBar />", () => {
+    const axiosInst = Axios.create({ headers: {} });
+    const axiosMock = new MockAdapter(axiosInst);
+
+    let store;
+    let services: ServiceRepository;
+    const config: any = {};
+    const actions: ApiActions = {
+        progressFn: (id: string, working: boolean) => { },
+        errorFn: (id: string, message: string) => { }
+    };
+    let statusResponse = {
+        [ProcessStatusFilter.COMPLETED]: 0,
+        [ProcessStatusFilter.RUNNING]: 0,
+        [ProcessStatusFilter.FAILED]: 0,
+        [ProcessStatusFilter.QUEUED]: 0,
+    };
+
+    const createMockListFunc = (uuid: string) => jest.fn(async (args) => {
+        const baseFilter = new FilterBuilder().addEqual('requesting_container_uuid', uuid).getFilters();
+
+        const filterResponses = Object.keys(statusResponse)
+            .map(status => ({filters: buildProcessStatusFilters(new FilterBuilder(baseFilter), status).getFilters(), value: statusResponse[status]}));
+
+        const matchedFilter = filterResponses.find(response => response.filters === args.filters);
+        if (matchedFilter) {
+            return { itemsAvailable: matchedFilter.value };
+        } else {
+            return { itemsAvailable: 0 };
+        }
+    });
+
+    beforeEach(() => {
+        services = createServices(mockConfig({}), actions, axiosInst);
+        store = configureStore(createBrowserHistory(), services, config);
+    });
+
+    it("requests subprocess progress stats for stopped processes and displays progress", async () => {
+        // when
+        const process = {
+            container: {
+                state: ContainerState.COMPLETE,
+            },
+            containerRequest: {
+                containerUuid: 'zzzzz-dz642-000000000000000',
+            },
+        } as Process;
+
+        statusResponse = {
+            [ProcessStatusFilter.COMPLETED]: 100,
+            [ProcessStatusFilter.RUNNING]: 200,
+            [ProcessStatusFilter.FAILED]: 300,
+            [ProcessStatusFilter.QUEUED]: 400,
+        };
+
+        services.containerRequestService.list = createMockListFunc(process.containerRequest.containerUuid);
+
+        let progressBar;
+        await act(async () => {
+            progressBar = mount(
+                <Provider store={store}>
+                    <SubprocessProgressBar process={process} />
+                </Provider>);
+        });
+        await progressBar.update();
+
+        // expects 4 subprocess status list requests
+        const expectedFilters = [
+            ProcessStatusFilter.COMPLETED,
+            ProcessStatusFilter.RUNNING,
+            ProcessStatusFilter.FAILED,
+            ProcessStatusFilter.QUEUED,
+        ].map((state) =>
+            buildProcessStatusFilters(
+                new FilterBuilder().addEqual(
+                    "requesting_container_uuid",
+                    process.containerRequest.containerUuid
+                ),
+                state
+            ).getFilters()
+        );
+
+        expectedFilters.forEach((filter) => {
+            expect(services.containerRequestService.list).toHaveBeenCalledWith({limit: 0, offset: 0, filters: filter});
+        });
+
+        // Verify progress bar with correct degment widths
+        ['10%', '20%', '30%', '40%'].forEach((value, i) => {
+            const styles = progressBar.find('.progress').at(i).props().style;
+            expect(styles).toHaveProperty('width', value);
+        });
+    });
+
+    it("dislays correct progress bar widths with different values", async () => {
+        const process = {
+            container: {
+                state: ContainerState.COMPLETE,
+            },
+            containerRequest: {
+                containerUuid: 'zzzzz-dz642-000000000000001',
+            },
+        } as Process;
+
+        statusResponse = {
+            [ProcessStatusFilter.COMPLETED]: 50,
+            [ProcessStatusFilter.RUNNING]: 55,
+            [ProcessStatusFilter.FAILED]: 60,
+            [ProcessStatusFilter.QUEUED]: 335,
+        };
+
+        services.containerRequestService.list = createMockListFunc(process.containerRequest.containerUuid);
+
+        let progressBar;
+        await act(async () => {
+            progressBar = mount(
+                <Provider store={store}>
+                    <SubprocessProgressBar process={process} />
+                </Provider>);
+        });
+        await progressBar.update();
+
+        // Verify progress bar with correct degment widths
+        ['10%', '11%', '12%', '67%'].forEach((value, i) => {
+            const styles = progressBar.find('.progress').at(i).props().style;
+            expect(styles).toHaveProperty('width', value);
+        });
+    });
+
+});
diff --git a/src/store/tree-picker/tree-picker-actions.test.ts b/src/store/tree-picker/tree-picker-actions.test.ts
index 9622282c..7a55503e 100644
--- a/src/store/tree-picker/tree-picker-actions.test.ts
+++ b/src/store/tree-picker/tree-picker-actions.test.ts
@@ -23,10 +23,7 @@ describe('tree-picker-actions', () => {
 
     let store: RootStore;
     let services: ServiceRepository;
-    const config: any = {
-
-
-    };
+    const config: any = {};
     const actions: ApiActions = {
         progressFn: (id: string, working: boolean) => { },
         errorFn: (id: string, message: string) => { }
diff --git a/yarn.lock b/yarn.lock
index 142694c8..7b837aab 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3894,11 +3894,11 @@ __metadata:
     parse-duration: 0.4.4
     prop-types: 15.7.2
     query-string: 6.9.0
-    react: 16.8.6
+    react: 16.14.0
     react-copy-to-clipboard: 5.0.3
     react-dnd: 5.0.0
     react-dnd-html5-backend: 5.0.1
-    react-dom: 16.8.6
+    react-dom: 16.14.0
     react-dropzone: 5.1.1
     react-highlight-words: 0.14.0
     react-idle-timer: 4.3.6
@@ -15349,17 +15349,17 @@ __metadata:
   languageName: node
   linkType: hard
 
-"react-dom at npm:16.8.6":
-  version: 16.8.6
-  resolution: "react-dom at npm:16.8.6"
+"react-dom at npm:16.14.0":
+  version: 16.14.0
+  resolution: "react-dom at npm:16.14.0"
   dependencies:
     loose-envify: ^1.1.0
     object-assign: ^4.1.1
     prop-types: ^15.6.2
-    scheduler: ^0.13.6
+    scheduler: ^0.19.1
   peerDependencies:
-    react: ^16.0.0
-  checksum: 7f8ebd8523eb4a14a1439efa009d020abc0529da25d0de251a4f3d5b3781061f6b30d72425f5fe944317850997efc6c1d667e99b1fd70172f30a976a00008bf6
+    react: ^16.14.0
+  checksum: 5a5c49da0f106b2655a69f96c622c347febcd10532db391c262b26aec225b235357d9da1834103457683482ab1b229af7a50f6927a6b70e53150275e31785544
   languageName: node
   linkType: hard
 
@@ -15683,15 +15683,14 @@ __metadata:
   languageName: node
   linkType: hard
 
-"react at npm:16.8.6":
-  version: 16.8.6
-  resolution: "react at npm:16.8.6"
+"react at npm:16.14.0":
+  version: 16.14.0
+  resolution: "react at npm:16.14.0"
   dependencies:
     loose-envify: ^1.1.0
     object-assign: ^4.1.1
     prop-types: ^15.6.2
-    scheduler: ^0.13.6
-  checksum: 8dfdbec9af6999c2cfb33a9389995c6401daba732e1ee7e0a4920d28fd2e8e6b0fde99dfe4b8e2f81efc4a962c92656e3e79e221323449e55850232163f15ff4
+  checksum: 8484f3ecb13414526f2a7412190575fc134da785c02695eb92bb6028c930bfe1c238d7be2a125088fec663cc7cda0a3623373c46807cf2c281f49c34b79881ac
   languageName: node
   linkType: hard
 
@@ -16696,16 +16695,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"scheduler at npm:^0.13.6":
-  version: 0.13.6
-  resolution: "scheduler at npm:0.13.6"
-  dependencies:
-    loose-envify: ^1.1.0
-    object-assign: ^4.1.1
-  checksum: c82c705f6d0d6df87b26bf2cca33f427e91889438c0435ade3ee7f41860eda4dd7f3171ca2d93e8fe9431f3bd831ca0e267a401a0296e4b14de05e389f82d320
-  languageName: node
-  linkType: hard
-
 "scheduler at npm:^0.19.1":
   version: 0.19.1
   resolution: "scheduler at npm:0.19.1"

commit 9eca8f9b0755eaeb1104a8e699a463f0ac127040
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Nov 6 09:44:20 2023 -0500

    20609: Add subprogress progress bar along with required bootstrap/coreUI styles
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/package.json b/package.json
index 35c960c4..acc2db6a 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/components/data-explorer/data-explorer.tsx b/src/components/data-explorer/data-explorer.tsx
index ad5762df..7657ae04 100644
--- a/src/components/data-explorer/data-explorer.tsx
+++ b/src/components/data-explorer/data-explorer.tsx
@@ -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
index 00000000..1d467eea
--- /dev/null
+++ b/src/components/subprocess-progress-bar/subprocess-progress-bar.tsx
@@ -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);
diff --git a/src/index.tsx b/src/index.tsx
index ede257dc..ef9ff9c9 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -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);
diff --git a/src/store/subprocess-panel/subprocess-panel-actions.ts b/src/store/subprocess-panel/subprocess-panel-actions.ts
index b440776c..68ed453f 100644
--- a/src/store/subprocess-panel/subprocess-panel-actions.ts
+++ b/src/store/subprocess-panel/subprocess-panel-actions.ts
@@ -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;
+        }
+    };
diff --git a/src/views/process-panel/process-panel-root.tsx b/src/views/process-panel/process-panel-root.tsx
index 7a240899..c972c0a6 100644
--- a/src/views/process-panel/process-panel-root.tsx
+++ b/src/views/process-panel/process-panel-root.tsx
@@ -205,7 +205,7 @@ export const ProcessPanelRoot = withStyles(styles)(
                     xs
                     maxHeight="50%"
                     data-cy="process-children">
-                    <SubprocessPanel />
+                    <SubprocessPanel process={process} />
                 </MPVPanelContent>
             </MPVContainer>
         ) : (
diff --git a/src/views/subprocess-panel/subprocess-panel-root.tsx b/src/views/subprocess-panel/subprocess-panel-root.tsx
index 9cf1db77..33a10275 100644
--- a/src/views/subprocess-panel/subprocess-panel-root.tsx
+++ b/src/views/subprocess-panel/subprocess-panel-root.tsx
@@ -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} />} />;
 };
diff --git a/src/views/subprocess-panel/subprocess-panel.tsx b/src/views/subprocess-panel/subprocess-panel.tsx
index 0aa02d52..c52f054b 100644
--- a/src/views/subprocess-panel/subprocess-panel.tsx
+++ b/src/views/subprocess-panel/subprocess-panel.tsx
@@ -26,7 +26,7 @@ const mapDispatchToProps = (dispatch: Dispatch): SubprocessPanelActionProps => (
     },
 });
 
-const mapStateToProps = (state: RootState): SubprocessPanelDataProps => ({
+const mapStateToProps = (state: RootState): Omit<SubprocessPanelDataProps,'process'> => ({
     resources: state.resources,
 });
 
diff --git a/yarn.lock b/yarn.lock
index f9dfa6a9..142694c8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1646,6 +1646,26 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@coreui/coreui at npm:next":
+  version: 5.0.0-alpha.3
+  resolution: "@coreui/coreui at npm:5.0.0-alpha.3"
+  peerDependencies:
+    "@popperjs/core": ^2.11.8
+  checksum: 2363ad6be775c6a895a49126a5b9062ffa9ebd0bea6dfb835c1300cd122fb1cf18d85fe647a9c08a3a384caa871e761d8ffb28ea45c7872cb2b034df6527da20
+  languageName: node
+  linkType: hard
+
+"@coreui/react at npm:next":
+  version: 5.0.0-alpha.3
+  resolution: "@coreui/react at 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 at npm:^1.4.0":
   version: 1.4.0
   resolution: "@csstools/convert-colors at npm:1.4.0"
@@ -3800,6 +3820,8 @@ __metadata:
   version: 0.0.0-use.local
   resolution: "arvados-workbench-2 at 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 at npm:^5.3.2":
+  version: 5.3.2
+  resolution: "bootstrap at npm:5.3.2"
+  peerDependencies:
+    "@popperjs/core": ^2.11.8
+  checksum: d5580b253d121ffc137388d41da58dce8d15f1ccd574e12f28d4a08e7649ca15e95db645b2b677cb8025bccd446bff04138fc0fe64f8cba0ccc5dc004a8644cf
+  languageName: node
+  linkType: hard
+
 "brace-expansion at npm:^1.1.7":
   version: 1.1.11
   resolution: "brace-expansion at npm:1.1.11"

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list