21128: Merge branch 'main' into 21128-toolbar-context-menu
authorLisa Knox <lisaknox83@gmail.com>
Mon, 18 Dec 2023 16:42:25 +0000 (11:42 -0500)
committerLisa Knox <lisaknox83@gmail.com>
Mon, 18 Dec 2023 19:06:57 +0000 (14:06 -0500)
refs #21128

Arvados-DCO-1.1-Signed-off-by: Lisa Knox <lisa.knox@curii.com>

34 files changed:
Makefile
cypress/integration/process.spec.js
cypress/integration/side-panel.spec.js
package.json
public/arrow-to-left.png [deleted file]
public/arrow-to-right.png [deleted file]
public/collapseLHS-New.svg [deleted file]
public/mui-start-icon.svg [new file with mode: 0644]
src/components/data-explorer/data-explorer.tsx
src/components/icon/icon.tsx
src/components/multiselect-toolbar/MultiselectToolbar.tsx
src/components/subprocess-progress-bar/subprocess-progress-bar.test.tsx [new file with mode: 0644]
src/components/subprocess-progress-bar/subprocess-progress-bar.tsx [new file with mode: 0644]
src/components/tree/tree.tsx
src/index.tsx
src/store/favorites/favorites-actions.ts
src/store/navigation/navigation-action.ts
src/store/process-logs-panel/process-logs-panel-actions.ts
src/store/public-favorites/public-favorites-actions.ts
src/store/side-panel-tree/side-panel-tree-actions.ts
src/store/subprocess-panel/subprocess-panel-actions.ts
src/store/tree-picker/tree-picker-actions.test.ts
src/store/workbench/workbench-actions.ts
src/views-components/data-explorer/renderers.tsx
src/views-components/side-panel-toggle/side-panel-toggle.tsx
src/views-components/side-panel-tree/side-panel-tree.tsx
src/views-components/side-panel/side-panel-collapsed.tsx [new file with mode: 0644]
src/views-components/side-panel/side-panel.tsx
src/views/process-panel/process-panel-root.tsx
src/views/shared-with-me-panel/shared-with-me-panel.tsx
src/views/subprocess-panel/subprocess-panel-root.tsx
src/views/subprocess-panel/subprocess-panel.tsx
src/views/workbench/workbench.tsx
yarn.lock

index 0220adadc2993fea8a3d16896da79d4fbbb4d026..c7a9cbfb8ea514e40017a0ae77515c54d6afe065 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -151,8 +151,11 @@ packages-in-docker: check-arvados-directory workbench2-build-image
        docker run --env ci="true" \
                --env ARVADOS_DIRECTORY=/tmp/arvados \
                --env APP_NAME=${APP_NAME} \
+               --env VERSION="${VERSION}" \
                --env ITERATION=${ITERATION} \
                --env TARGETS="${TARGETS}" \
+               --env MAINTAINER="${MAINTAINER}" \
+               --env DESCRIPTION="${DESCRIPTION}" \
                -w="/tmp/workbench2" \
                -t -v ${WORKSPACE}:/tmp/workbench2 \
                -v ${ARVADOS_DIRECTORY}:/tmp/arvados workbench2-build:latest \
index 7f77815b15aa25a3eacf7d487868f1cc51296b9c..9ea026b9906511c297cb6367370b8c08955f8869 100644 (file)
@@ -599,6 +599,53 @@ describe("Process tests", function () {
             });
         });
 
+        it("preserves original ordering of lines within the same log type", function () {
+            const crName = "test_container_request";
+            createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
+                cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [
+                    // Should come first
+                    "2023-07-18T20:14:46.000000000Z A out 1",
+                    // Comes fourth in a contiguous block
+                    "2023-07-18T20:14:48.128642814Z A out 2",
+                    "2023-07-18T20:14:48.128642814Z X out 3",
+                    "2023-07-18T20:14:48.128642814Z A out 4",
+                ]).as("stdout");
+
+                cy.appendLog(adminUser.token, containerRequest.uuid, "stderr.txt", [
+                    // Comes second
+                    "2023-07-18T20:14:47.000000000Z Z err 1",
+                    // Comes third in a contiguous block
+                    "2023-07-18T20:14:48.128642814Z B err 2",
+                    "2023-07-18T20:14:48.128642814Z C err 3",
+                    "2023-07-18T20:14:48.128642814Z Y err 4",
+                    "2023-07-18T20:14:48.128642814Z Z err 5",
+                    "2023-07-18T20:14:48.128642814Z A err 6",
+                ]).as("stderr");
+
+                cy.loginAs(activeUser);
+                cy.goToPath(`/processes/${containerRequest.uuid}`);
+                cy.get("[data-cy=process-details]").should("contain", crName);
+                cy.get("[data-cy=process-logs]").should("contain", "No logs yet");
+
+                cy.getAll("@stdout", "@stderr").then(() => {
+                    // Switch to All logs
+                    cy.get("[data-cy=process-logs-filter]").click();
+                    cy.get("body").contains("li", "All logs").click();
+                    // Verify sorted logs
+                    cy.get("[data-cy=process-logs] pre").eq(0).should("contain", "2023-07-18T20:14:46.000000000Z A out 1");
+                    cy.get("[data-cy=process-logs] pre").eq(1).should("contain", "2023-07-18T20:14:47.000000000Z Z err 1");
+                    cy.get("[data-cy=process-logs] pre").eq(2).should("contain", "2023-07-18T20:14:48.128642814Z B err 2");
+                    cy.get("[data-cy=process-logs] pre").eq(3).should("contain", "2023-07-18T20:14:48.128642814Z C err 3");
+                    cy.get("[data-cy=process-logs] pre").eq(4).should("contain", "2023-07-18T20:14:48.128642814Z Y err 4");
+                    cy.get("[data-cy=process-logs] pre").eq(5).should("contain", "2023-07-18T20:14:48.128642814Z Z err 5");
+                    cy.get("[data-cy=process-logs] pre").eq(6).should("contain", "2023-07-18T20:14:48.128642814Z A err 6");
+                    cy.get("[data-cy=process-logs] pre").eq(7).should("contain", "2023-07-18T20:14:48.128642814Z A out 2");
+                    cy.get("[data-cy=process-logs] pre").eq(8).should("contain", "2023-07-18T20:14:48.128642814Z X out 3");
+                    cy.get("[data-cy=process-logs] pre").eq(9).should("contain", "2023-07-18T20:14:48.128642814Z A out 4");
+                });
+            });
+        });
+
         it("correctly generates sniplines", function () {
             const SNIPLINE = `================ ✀ ================ ✀ ========= Some log(s) were skipped ========= ✀ ================ ✀ ================`;
             const crName = "test_container_request";
index 92181150edddd2f07c2e67a56e63ec03327732ce..d6ac754d0a4d0da815b51c7ee4ef7564b43b1e9d 100644 (file)
@@ -135,4 +135,41 @@ describe('Side panel tests', function() {
             });
         });
     });
+
+    it('collapses and un-collapses', () => {
+
+        cy.loginAs(activeUser)
+        cy.get('[data-cy=side-panel-tree]').should('exist')
+        cy.get('[data-cy=side-panel-toggle]').click()
+        cy.get('[data-cy=side-panel-tree]').should('not.exist')
+        cy.get('[data-cy=side-panel-collapsed]').should('exist')
+        cy.get('[data-cy=side-panel-toggle]').click()
+        cy.get('[data-cy=side-panel-tree]').should('exist')
+        cy.get('[data-cy=side-panel-collapsed]').should('not.exist')
+    })
+
+    it('can navigate from collapsed panel', () => {
+
+        const collapsedCategories = {
+            'shared-with-me': '/shared-with-me',
+            'public-favorites': '/public-favorites',
+            'my-favorites': '/favorites',
+            'groups': '/groups',
+            'all-processes': '/all_processes',
+            'trash': '/trash',
+            'shell-access': '/virtual-machines-user',
+            'home-projects': `/projects/${activeUser.user.uuid}`,
+        }
+
+        cy.loginAs(activeUser)
+        cy.get('[data-cy=side-panel-tree]').should('exist')
+        cy.get('[data-cy=side-panel-toggle]').click()
+        cy.get('[data-cy=side-panel-collapsed]').should('exist')
+
+        for (const cat in collapsedCategories) {
+            cy.get(`[data-cy=collapsed-${cat}]`).should('exist').click()
+            cy.url().should('include', collapsedCategories[cat])
+        }
+    })
 })
+
index 35c960c4186d132d2eb477c4ee4495204f142ec4..abb204907be5b5ac12aa25c994d4473cb18a2800 100644 (file)
@@ -3,6 +3,8 @@
   "version": "0.1.0",
   "private": true,
   "dependencies": {
+    "@coreui/coreui": "^4.3.2",
+    "@coreui/react": "^4.11.0",
     "@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",
     "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",
-    "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/public/arrow-to-left.png b/public/arrow-to-left.png
deleted file mode 100644 (file)
index 262c148..0000000
Binary files a/public/arrow-to-left.png and /dev/null differ
diff --git a/public/arrow-to-right.png b/public/arrow-to-right.png
deleted file mode 100644 (file)
index 8205c21..0000000
Binary files a/public/arrow-to-right.png and /dev/null differ
diff --git a/public/collapseLHS-New.svg b/public/collapseLHS-New.svg
deleted file mode 100644 (file)
index ce2eac8..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
-   version="1.1"
-   id="svg148"
-   width="300"
-   height="300"
-   viewBox="0 0 300 300"
-   xml:space="preserve"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:svg="http://www.w3.org/2000/svg"><defs
-     id="defs152" /><g
-     id="g154"><g
-       id="g6337"><path
-         style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:22.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
-         d="m 191.30938,11.567958 0.0193,275.898262"
-         id="path400" /><path
-         style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:22.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
-         d="m 202.57626,149.50744 -89.79939,0.0193"
-         id="path400-3" /><path
-         style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:59.0707;stroke-dasharray:none;stroke-opacity:1"
-         id="path4546"
-         d="M 81.113348,90.153499 -22.761723,90.479332 28.893633,0.35796487 Z"
-         transform="matrix(0,0.5047589,0.28743877,-0.01237225,93.434122,136.22641)" /></g></g></svg>
diff --git a/public/mui-start-icon.svg b/public/mui-start-icon.svg
new file mode 100644 (file)
index 0000000..3140cc3
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M80-240v-480h80v480H80Zm560 0-57-56 144-144H240v-80h487L584-664l56-56 240 240-240 240Z"/></svg>
\ No newline at end of file
index f9c62a09d7d5fee64ac38fc9521e700213f26e1e..27e46d584962c8d3e1cb1ca536b21ab1b4577ecf 100644 (file)
@@ -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" | 'subProcessTitle' | "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,15 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         paddingLeft: theme.spacing.unit * 2,
         paddingTop: theme.spacing.unit * 2,
         fontSize: "18px",
+        paddingRight: "10px",
+    },
+    subProcessTitle: {
+        display: "inline-block",
+        paddingLeft: theme.spacing.unit * 2,
+        paddingTop: theme.spacing.unit * 2,
+        fontSize: "18px",
+        flexGrow: 0,
+        paddingRight: "10px",
     },
     dataTable: {
         height: "100%",
@@ -50,11 +64,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,6 +91,7 @@ interface DataExplorerDataProps<T> {
     actions?: React.ReactNode;
     hideSearchInput?: boolean;
     title?: React.ReactNode;
+    progressBar?: React.ReactNode;
     paperKey?: string;
     currentItemUuid: string;
     elementPath?: string;
@@ -113,6 +126,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 || "";
@@ -180,7 +195,9 @@ export const DataExplorer = withStyles(styles)(
                 paperKey,
                 fetchMode,
                 currentItemUuid,
+                currentRoute,
                 title,
+                progressBar,
                 doHidePanel,
                 doMaximizePanel,
                 doUnMaximizePanel,
@@ -204,16 +221,18 @@ export const DataExplorer = withStyles(styles)(
                         wrap="nowrap"
                         className={classes.container}
                     >
-                        <div>
+                        <div className={classes.titleWrapper} style={currentRoute?.includes('search-results') || !!progressBar ? {marginBottom: '-20px'} : {}}>
                             {title && (
                                 <Grid
                                     item
                                     xs
-                                    className={classes.title}
+                                    className={!!progressBar ? classes.subProcessTitle : classes.title}
                                 >
                                     {title}
                                 </Grid>
                             )}
+                            {!!progressBar && progressBar}
+                            {this.multiSelectToolbarInTitle && <MultiselectToolbar />}
                             {(!hideColumnSelector || !hideSearchInput || !!actions) && (
                                 <Grid
                                     className={classes.headerMenu}
@@ -221,25 +240,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"}`}
@@ -274,14 +295,15 @@ export const DataExplorer = withStyles(styles)(
                                             </Tooltip>
                                         )}
                                     </Toolbar>
-                                    <MultiselectToolbar />
                                 </Grid>
                             )}
                         </div>
+                        {!this.multiSelectToolbarInTitle && <MultiselectToolbar />}
                         <Grid
                             item
                             xs="auto"
                             className={classes.dataTable}
+                            style={currentRoute?.includes('search-results')  || !!progressBar ? {marginTop: '-10px'} : {}}
                         >
                             <DataTable
                                 columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
index 4168e2a2f44e4921c6f066dcf2e1ec5c9fe0c25f..2dd97c1663777cc5d64a7540351d8d8dfeef5b52 100644 (file)
@@ -164,6 +164,13 @@ export const CheckboxMultipleBlankOutline: IconType = (props: any) => (
     </SvgIcon>
 );
 
+//https://pictogrammers.com/library/mdi/icon/console/
+export const TerminalIcon: IconType = (props: any) => (
+    <SvgIcon {...props}>
+        <path d="M20,19V7H4V19H20M20,3A2,2 0 0,1 22,5V19A2,2 0 0,1 20,21H4A2,2 0 0,1 2,19V5C2,3.89 2.9,3 4,3H20M13,17V15H18V17H13M9.58,13L5.57,9H8.4L11.7,12.3C12.09,12.69 12.09,13.33 11.7,13.72L8.42,17H5.59L9.58,13Z" />
+    </SvgIcon>
+)
+
 export type IconType = React.SFC<{ className?: string; style?: object }>;
 
 export const AddIcon: IconType = props => <Add {...props} />;
index d9024cd15244296f9b4f1c4c08ac115a275eb76a..30d5bd7912f1d232d7687ea847d842506841fb55 100644 (file)
@@ -42,6 +42,7 @@ type CssRules = "root" | "transition" | "button" | "iconContainer";
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
         display: "flex",
+        flexShrink: 0,
         flexDirection: "row",
         width: 0,
         height: '2.7rem',
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 (file)
index 0000000..bd8603f
--- /dev/null
@@ -0,0 +1,165 @@
+// 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,
+
+            // 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);
+
+        let progressBar;
+        await act(async () => {
+            progressBar = mount(
+                <Provider store={store}>
+                    <SubprocessProgressBar process={process} />
+                </Provider>);
+        });
+        await progressBar.update();
+
+        // 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(
+                    "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]: 30,
+            [ProcessStatusFilter.CANCELLED]: 30,
+
+            [ProcessStatusFilter.QUEUED]: 235,
+            [ProcessStatusFilter.ONHOLD]: 100,
+        };
+
+        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/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..b21d879
--- /dev/null
@@ -0,0 +1,105 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React, { useEffect, useState } from "react";
+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";
+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: "28px 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:
+                "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}>
+                <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>
+        </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 28f1966aa173264b627c03a75651b39bb4ccb2c9..11a9540290cc21eb9aa3995f93124608775f0254 100644 (file)
@@ -5,7 +5,7 @@
 import React, { useCallback, useState } from 'react';
 import { List, ListItem, ListItemIcon, Checkbox, Radio, Collapse } from "@material-ui/core";
 import { StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core/styles';
-import { CollectionIcon, DefaultIcon, DirectoryIcon, FileIcon, ProjectIcon, FilterGroupIcon, FreezeIcon } from 'components/icon/icon';
+import { CollectionIcon, DefaultIcon, DirectoryIcon, FileIcon, ProjectIcon, ProcessIcon, FilterGroupIcon, FreezeIcon } from 'components/icon/icon';
 import { ReactElement } from "react";
 import CircularProgress from '@material-ui/core/CircularProgress';
 import classnames from "classnames";
@@ -14,6 +14,7 @@ import { ArvadosTheme } from 'common/custom-theme';
 import { SidePanelRightArrowIcon } from '../icon/icon';
 import { ResourceKind } from 'models/resource';
 import { GroupClass } from 'models/group';
+import { SidePanelTreeCategory } from 'store/side-panel-tree/side-panel-tree-actions';
 
 type CssRules = 'list'
     | 'listItem'
@@ -27,7 +28,8 @@ type CssRules = 'list'
     | 'checkbox'
     | 'childItem'
     | 'childItemIcon'
-    | 'frozenIcon';
+    | 'frozenIcon'
+    | 'indentSpacer';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     list: {
@@ -45,9 +47,10 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         color: theme.palette.grey["700"],
         height: '14px',
         width: '14px',
+        marginBottom: '0.4rem',
     },
     toggableIcon: {
-        fontSize: '14px'
+        fontSize: '14px',
     },
     renderContainer: {
         flex: 1
@@ -89,6 +92,9 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         color: theme.palette.grey["600"],
         marginLeft: '10px',
     },
+    indentSpacer: {
+        width: '0.25rem'
+    }
 });
 
 export enum TreeItemStatus {
@@ -99,6 +105,7 @@ export enum TreeItemStatus {
 
 export interface TreeItem<T> {
     data: T;
+    depth?: number;
     id: string;
     open: boolean;
     active: boolean;
@@ -155,6 +162,10 @@ const getActionAndId = (event: any, initAction: string | undefined = undefined)
     return [action, id];
 };
 
+const isInFavoritesTree = (item: TreeItem<any>): boolean => {
+    return item.id === SidePanelTreeCategory.FAVORITES || item.id === SidePanelTreeCategory.PUBLIC_FAVORITES;
+}
+
 interface FlatTreeProps {
     it: TreeItem<any>;
     levelIndentation: number;
@@ -177,7 +188,7 @@ const FLAT_TREE_ACTIONS = {
     toggleActive: 'TOGGLE_ACTIVE',
 };
 
-const ItemIcon = React.memo(({ type, kind, active, groupClass, classes }: any) => {
+const ItemIcon = React.memo(({ type, kind, headKind, active, groupClass, classes }: any) => {
     let Icon = ProjectIcon;
 
     if (groupClass === GroupClass.FILTER) {
@@ -198,10 +209,14 @@ const ItemIcon = React.memo(({ type, kind, active, groupClass, classes }: any) =
     }
 
     if (kind) {
+        if(kind === ResourceKind.LINK && headKind) kind = headKind;
         switch (kind) {
             case ResourceKind.COLLECTION:
                 Icon = CollectionIcon;
                 break;
+            case ResourceKind.CONTAINER_REQUEST:
+                Icon = ProcessIcon;
+                break;
             default:
                 break;
         }
@@ -240,11 +255,14 @@ const FlatTree = (props: FlatTreeProps) =>
                 .map((item: any) => <div key={item.id} data-id={item.id}
                     className={classnames(props.classes.childItem, { [props.classes.active]: item.active })}
                     style={{ paddingLeft: `${item.depth * props.levelIndentation}px` }}>
-                    <i data-action={FLAT_TREE_ACTIONS.toggleOpen} className={props.classes.toggableIconContainer}>
-                        <ListItemIcon className={props.getToggableIconClassNames(item.open, item.active)}>
-                            {props.getProperArrowAnimation(item.status, item.items!)}
-                        </ListItemIcon>
-                    </i>
+                    {isInFavoritesTree(props.it) ? 
+                        <div className={props.classes.indentSpacer} />
+                        :
+                        <i data-action={FLAT_TREE_ACTIONS.toggleOpen} className={props.classes.toggableIconContainer}>
+                            <ListItemIcon className={props.getToggableIconClassNames(item.open, item.active)}>
+                                {props.getProperArrowAnimation(item.status, item.items!)}
+                            </ListItemIcon> 
+                        </i>}
                     {props.showSelection(item) && !props.useRadioButtons &&
                         <Checkbox
                             checked={item.selected}
@@ -258,7 +276,7 @@ const FlatTree = (props: FlatTreeProps) =>
                             color="primary" />}
                     <div data-action={FLAT_TREE_ACTIONS.toggleActive} className={props.classes.renderContainer} ref={item.active ? props.selectedRef : undefined}>
                         <span style={{ display: 'flex', alignItems: 'center' }}>
-                            <ItemIcon type={item.data.type} active={item.active} kind={item.data.kind} groupClass={item.data.kind === ResourceKind.GROUP ? item.data.groupClass : ''} classes={props.classes} />
+                            <ItemIcon type={item.data.type} active={item.active} kind={item.data.kind} headKind={item.data.headKind || null} groupClass={item.data.kind === ResourceKind.GROUP ? item.data.groupClass : ''} classes={props.classes} />
                             <span style={{ fontSize: '0.875rem' }}>
                                 {item.data.name}
                             </span>
@@ -326,6 +344,9 @@ export const Tree = withStyles(styles)(
         const { levelIndentation = 20, itemRightPadding = 20 } = props;
         return <List className={list}>
             {items && items.map((it: TreeItem<T>, idx: number) => {
+                if (isInFavoritesTree(it) && it.open === true && it.items && it.items.length) {
+                    it = { ...it, items: it.items.filter(item => item.depth && item.depth < 3) }
+                }
                 return <div key={`item/${level}/${it.id}`}>
                     <ListItem button className={listItem}
                         style={{
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 a1667326324a7b38485acbc5d9f0e3e6743ab869..da454ed77dbc9561116cf43c6de5fc25ae8edc95 100644 (file)
@@ -12,6 +12,7 @@ import { ServiceRepository } from "services/services";
 import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
 import { MultiSelectMenuActionNames } from "views-components/multiselect-toolbar/ms-menu-actions";
 import { addDisabledButton, removeDisabledButton } from "store/multiselect/multiselect-actions";
+import { loadFavoritesTree} from "store/side-panel-tree/side-panel-tree-actions";
 
 export const favoritesActions = unionize({
     TOGGLE_FAVORITE: ofType<{ resourceUuid: string }>(),
@@ -55,6 +56,7 @@ export const toggleFavorite = (resource: { uuid: string; name: string }) =>
                 }));
                 dispatch<any>(removeDisabledButton(MultiSelectMenuActionNames.ADD_TO_FAVORITES))
                 dispatch(progressIndicatorActions.STOP_WORKING("toggleFavorite"));
+                dispatch<any>(loadFavoritesTree())
             })
             .catch((e: any) => {
                 dispatch(progressIndicatorActions.STOP_WORKING("toggleFavorite"));
index 61720b56abfd5bbba55266a08fd95dd63fff8de8..55112fb0ae44ab153e89ce41058205d5fb0d5ca6 100644 (file)
@@ -69,6 +69,9 @@ export const navigateTo = (uuid: string) => async (dispatch: Dispatch, getState:
         case SidePanelTreeCategory.ALL_PROCESSES:
             dispatch(navigateToAllProcesses);
             return;
+        case SidePanelTreeCategory.SHELL_ACCESS:
+            dispatch(navigateToUserVirtualMachines)
+            return;
         case USERS_PANEL_LABEL:
             dispatch(navigateToUsers);
             return;
index 4e52431eebadc9a05b240ee530cd29e3390569e4..88b56a2c324379c2b9be44c859da7ad9e05eb28e 100644 (file)
@@ -33,6 +33,12 @@ type FileWithProgress = {
     lastByte: number;
 }
 
+type SortableLine = {
+    logType: LogEventType,
+    timestamp: string;
+    contents: string;
+}
+
 export type ProcessLogsPanelAction = UnionOf<typeof processLogsPanelActions>;
 
 export const setProcessLogsPanelFilter = (filter: string) =>
@@ -257,18 +263,61 @@ const mergeMultilineLoglines = (logFragments: LogFragment[]) => (
  * @returns string[] of merged and sorted log lines
  */
 const mergeSortLogFragments = (logFragments: LogFragment[]): string[] => {
-    const sortableLines = fragmentsToLines(logFragments
-        .filter((fragment) => (!NON_SORTED_LOG_TYPES.includes(fragment.logType))));
+    const sortableFragments = logFragments
+        .filter((fragment) => (!NON_SORTED_LOG_TYPES.includes(fragment.logType)));
 
     const nonSortableLines = fragmentsToLines(logFragments
         .filter((fragment) => (NON_SORTED_LOG_TYPES.includes(fragment.logType)))
         .sort((a, b) => (a.logType.localeCompare(b.logType))));
 
-    return [...nonSortableLines, ...sortableLines.sort(sortLogLines)]
+    return [...nonSortableLines, ...sortLogFragments(sortableFragments)];
 };
 
-const sortLogLines = (a: string, b: string) => {
-    return a.localeCompare(b);
+/**
+ * Performs merge and sort of input log fragment lines
+ * @param logFragments set of sortable log fragments to be merged and sorted
+ * @returns A string array containing all lines, sorted by timestamp and
+ *          preserving line ordering and type grouping when timestamps match
+ */
+const sortLogFragments = (logFragments: LogFragment[]): string[] => {
+    const linesWithType: SortableLine[] = logFragments
+        // Map each logFragment into an array of SortableLine
+        .map((fragment: LogFragment): SortableLine[] => (
+            fragment.contents.map((singleLine: string) => {
+                const timestampMatch = singleLine.match(LOG_TIMESTAMP_PATTERN);
+                const timestamp = timestampMatch && timestampMatch[0] ? timestampMatch[0] : "";
+                return {
+                    logType: fragment.logType,
+                    timestamp: timestamp,
+                    contents: singleLine,
+                };
+            })
+        // Merge each array of SortableLine into single array
+        )).reduce((acc: SortableLine[], lines: SortableLine[]) => (
+            [...acc, ...lines]
+        ), [] as SortableLine[]);
+
+    return linesWithType
+        .sort(sortableLineSortFunc)
+        .map(lineWithType => lineWithType.contents);
+};
+
+/**
+ * Sort func to sort lines
+ *   Preserves original ordering of lines from the same source
+ *   Stably orders lines of differing type but same timestamp
+ *     (produces a block of same-timestamped lines of one type before a block
+ *     of same timestamped lines of another type for readability)
+ *   Sorts all other lines by contents (ie by timestamp)
+ */
+const sortableLineSortFunc = (a: SortableLine, b: SortableLine) => {
+    if (a.logType === b.logType) {
+        return 0;
+    } else if (a.timestamp === b.timestamp) {
+        return a.logType.localeCompare(b.logType);
+    } else {
+        return a.contents.localeCompare(b.contents);
+    }
 };
 
 const fragmentsToLines = (fragments: LogFragment[]): string[] => (
index 5bf5078d30a9a198d77f3e6fd0f40923a168c9db..0f8ed6c2611c72e55fc769a2d5f4b7ab3d4c6408 100644 (file)
@@ -11,6 +11,7 @@ import { ServiceRepository } from "services/services";
 import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
 import { addDisabledButton, removeDisabledButton } from "store/multiselect/multiselect-actions";
 import { MultiSelectMenuActionNames } from "views-components/multiselect-toolbar/ms-menu-actions";
+import { loadPublicFavoritesTree } from "store/side-panel-tree/side-panel-tree-actions";
 
 export const publicFavoritesActions = unionize({
     TOGGLE_PUBLIC_FAVORITE: ofType<{ resourceUuid: string }>(),
@@ -52,6 +53,7 @@ export const togglePublicFavorite = (resource: { uuid: string; name: string }) =
                 }));
                 dispatch<any>(removeDisabledButton(MultiSelectMenuActionNames.ADD_TO_PUBLIC_FAVORITES))
                 dispatch(progressIndicatorActions.STOP_WORKING("togglePublicFavorite"));
+                dispatch<any>(loadPublicFavoritesTree())
             })
             .catch((e: any) => {
                 dispatch(progressIndicatorActions.STOP_WORKING("togglePublicFavorite"));
index f6015fbfa0c9d4f6201373eb4bed1f5c71708a05..44dfe869389fc5598b5fb3a205ebd36390b3037d 100644 (file)
@@ -14,22 +14,23 @@ import { getNodeAncestors, getNodeAncestorsIds, getNode, TreeNode, initTreeNode,
 import { ProjectResource } from 'models/project';
 import { OrderBuilder } from 'services/api/order-builder';
 import { ResourceKind } from 'models/resource';
-import { GroupContentsResourcePrefix } from 'services/groups-service/groups-service';
-import { GroupClass } from 'models/group';
 import { CategoriesListReducer } from 'common/plugintypes';
 import { pluginConfig } from 'plugins';
+import { LinkClass } from 'models/link';
 
 export enum SidePanelTreeCategory {
     PROJECTS = 'Home Projects',
-    SHARED_WITH_ME = 'Shared with me',
-    PUBLIC_FAVORITES = 'Public Favorites',
     FAVORITES = 'My Favorites',
-    TRASH = 'Trash',
+    PUBLIC_FAVORITES = 'Public Favorites',
+    SHARED_WITH_ME = 'Shared with me',
     ALL_PROCESSES = 'All Processes',
+    SHELL_ACCESS = 'Shell Access',
     GROUPS = 'Groups',
+    TRASH = 'Trash',
 }
 
 export const SIDE_PANEL_TREE = 'sidePanelTree';
+const SIDEPANEL_TREE_NODE_LIMIT = 50
 
 export const getSidePanelTree = (treePicker: TreePicker) =>
     getTreePicker<ProjectResource | string>(SIDE_PANEL_TREE)(treePicker);
@@ -48,11 +49,12 @@ export const getSidePanelTreeBranch = (uuid: string) => (treePicker: TreePicker)
 
 let SIDE_PANEL_CATEGORIES: string[] = [
     SidePanelTreeCategory.PROJECTS,
-    SidePanelTreeCategory.SHARED_WITH_ME,
-    SidePanelTreeCategory.PUBLIC_FAVORITES,
     SidePanelTreeCategory.FAVORITES,
-    SidePanelTreeCategory.GROUPS,
+    SidePanelTreeCategory.PUBLIC_FAVORITES,
+    SidePanelTreeCategory.SHARED_WITH_ME,
     SidePanelTreeCategory.ALL_PROCESSES,
+    SidePanelTreeCategory.SHELL_ACCESS,
+    SidePanelTreeCategory.GROUPS,
     SidePanelTreeCategory.TRASH
 ];
 
@@ -81,7 +83,7 @@ export const initSidePanelTree = () =>
             nodes
         }));
         SIDE_PANEL_CATEGORIES.forEach(category => {
-            if (category !== SidePanelTreeCategory.PROJECTS && category !== SidePanelTreeCategory.SHARED_WITH_ME) {
+                if (category !== SidePanelTreeCategory.PROJECTS && category !== SidePanelTreeCategory.FAVORITES && category !== SidePanelTreeCategory.PUBLIC_FAVORITES ) {
                 dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
                     id: category,
                     pickerId: SIDE_PANEL_TREE,
@@ -95,8 +97,10 @@ export const loadSidePanelTreeProjects = (projectUuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const treePicker = getTreePicker(SIDE_PANEL_TREE)(getState().treePicker);
         const node = treePicker ? getNode(projectUuid)(treePicker) : undefined;
-        if (projectUuid === SidePanelTreeCategory.SHARED_WITH_ME) {
-            await dispatch<any>(loadSharedRoot);
+        if (projectUuid === SidePanelTreeCategory.PUBLIC_FAVORITES) {
+            await dispatch<any>(loadPublicFavoritesTree());
+        } else if (projectUuid === SidePanelTreeCategory.FAVORITES) {
+            await dispatch<any>(loadFavoritesTree());
         } else if (node || projectUuid !== '') {
             await dispatch<any>(loadProject(projectUuid));
         }
@@ -110,10 +114,13 @@ const loadProject = (projectUuid: string) =>
                 .addEqual('owner_uuid', projectUuid)
                 .getFilters(),
             order: new OrderBuilder<ProjectResource>()
-                .addAsc('name')
-                .getOrder()
+                .addDesc('createdAt')
+                .getOrder(),
+            limit: SIDEPANEL_TREE_NODE_LIMIT,
         };
+
         const { items } = await services.projectService.list(params);
+        
         dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
             id: projectUuid,
             pickerId: SIDE_PANEL_TREE,
@@ -122,28 +129,58 @@ const loadProject = (projectUuid: string) =>
         dispatch(resourcesActions.SET_RESOURCES(items));
     };
 
-const loadSharedRoot = async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-    dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: SidePanelTreeCategory.SHARED_WITH_ME, pickerId: SIDE_PANEL_TREE }));
+export const loadFavoritesTree = () => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: SidePanelTreeCategory.FAVORITES, pickerId: SIDE_PANEL_TREE }));
+
+    const params = {
+        filters: new FilterBuilder()
+            .addEqual('link_class', LinkClass.STAR)
+            .addEqual('tail_uuid', getUserUuid(getState()))
+            .addEqual('tail_kind', ResourceKind.USER)
+            .getFilters(),
+        order: new OrderBuilder<ProjectResource>().addDesc('createdAt').getOrder(),
+        limit: SIDEPANEL_TREE_NODE_LIMIT,
+    };
+
+    const { items } = await services.linkService.list(params);
+
+    dispatch(
+        treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
+            id: SidePanelTreeCategory.FAVORITES,
+            pickerId: SIDE_PANEL_TREE,
+            nodes: items.map(item => initTreeNode({ id: item.headUuid, value: item })),
+        })
+    );
+
+    dispatch(resourcesActions.SET_RESOURCES(items));
+};
+
+export const loadPublicFavoritesTree = () => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id: SidePanelTreeCategory.PUBLIC_FAVORITES, pickerId: SIDE_PANEL_TREE }));
+
+    const uuidPrefix = getState().auth.config.uuidPrefix;
+    const publicProjectUuid = `${uuidPrefix}-j7d0g-publicfavorites`;
+    const typeFilters = [ResourceKind.COLLECTION, ResourceKind.CONTAINER_REQUEST, ResourceKind.GROUP, ResourceKind.WORKFLOW];
 
     const params = {
-        filters: `[${new FilterBuilder()
-            .addIsA('uuid', ResourceKind.PROJECT)
-            .addIn('group_class', [GroupClass.PROJECT, GroupClass.FILTER])
-            .addDistinct('uuid', getState().auth.config.uuidPrefix + '-j7d0g-publicfavorites')
-            .getFilters()}]`,
-        order: new OrderBuilder<ProjectResource>()
-            .addAsc('name', GroupContentsResourcePrefix.PROJECT)
-            .getOrder(),
-        limit: 1000
+        filters: new FilterBuilder()
+            .addEqual('link_class', LinkClass.STAR)
+            .addEqual('owner_uuid', publicProjectUuid)
+            .addIsA('head_uuid', typeFilters)
+            .getFilters(),
+        order: new OrderBuilder<ProjectResource>().addDesc('createdAt').getOrder(),
+        limit: SIDEPANEL_TREE_NODE_LIMIT,
     };
 
-    const { items } = await services.groupsService.shared(params);
+    const { items } = await services.linkService.list(params);
 
-    dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
-        id: SidePanelTreeCategory.SHARED_WITH_ME,
-        pickerId: SIDE_PANEL_TREE,
-        nodes: items.map(item => initTreeNode({ id: item.uuid, value: item })),
-    }));
+    dispatch(
+        treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
+            id: SidePanelTreeCategory.PUBLIC_FAVORITES,
+            pickerId: SIDE_PANEL_TREE,
+            nodes: items.map(item => initTreeNode({ id: item.headUuid, value: item })),
+        })
+    );
 
     dispatch(resourcesActions.SET_RESOURCES(items));
 };
@@ -152,9 +189,9 @@ export const activateSidePanelTreeItem = (id: string) =>
     async (dispatch: Dispatch, getState: () => RootState) => {
         const node = getSidePanelTreeNode(id)(getState().treePicker);
         if (node && !node.active) {
-            dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ id, pickerId: SIDE_PANEL_TREE }));
-        }
-        if (!isSidePanelTreeCategory(id)) {
+        dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ id, pickerId: SIDE_PANEL_TREE }));
+    }
+    if (!isSidePanelTreeCategory(id)) {
             await dispatch<any>(activateSidePanelTreeProject(id));
         }
     };
@@ -180,18 +217,11 @@ export const activateSidePanelTreeBranch = (id: string) =>
         const userUuid = getUserUuid(getState());
         if (!userUuid) { return; }
         const ancestors = await services.ancestorsService.ancestors(id, userUuid);
-        const isShared = ancestors.every(({ uuid }) => uuid !== userUuid);
-        if (isShared) {
-            await dispatch<any>(loadSidePanelTreeProjects(SidePanelTreeCategory.SHARED_WITH_ME));
-        }
         for (const ancestor of ancestors) {
             await dispatch<any>(loadSidePanelTreeProjects(ancestor.uuid));
         }
         dispatch(treePickerActions.EXPAND_TREE_PICKER_NODES({
-            ids: [
-                ...(isShared ? [SidePanelTreeCategory.SHARED_WITH_ME] : []),
-                ...ancestors.map(ancestor => ancestor.uuid)
-            ],
+            ids: ancestors.map(ancestor => ancestor.uuid),
             pickerId: SIDE_PANEL_TREE
         }));
         dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ id, pickerId: SIDE_PANEL_TREE }));
@@ -203,7 +233,7 @@ export const toggleSidePanelTreeItemCollapse = (id: string) =>
         if (node && node.status === TreeNodeStatus.INITIAL) {
             await dispatch<any>(loadSidePanelTreeProjects(node.id));
         }
-        dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId: SIDE_PANEL_TREE }));
+            dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId: SIDE_PANEL_TREE }));
     };
 
 export const expandSidePanelTreeItem = (id: string) =>
index b440776ce0ff6c2ddcd02fe34aa696348f907888..a67dd1f436441fa1cbf9d7ec4abc46c06910adc1 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,81 @@ export const loadSubprocessPanel = () =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(subprocessPanelActions.REQUEST_ITEMS());
     };
+
+/**
+ * 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> => {
+
+        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(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;
+                });
+                return result;
+            } catch (e) {
+                return undefined;
+            }
+        } else {
+            return undefined;
+        }
+    };
index 9622282c4fcb02414401006eb3e357b9b2569d22..7a55503e20f7b2968ea05b3678591a58e262f1bd 100644 (file)
@@ -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) => { }
index 72b818fd0f8256200fdc10a74fa03bfca86e01cc..188dba05689edf9be63c7da67ed6c35c2db1df55 100644 (file)
@@ -50,6 +50,7 @@ import { trashPanelColumns } from "views/trash-panel/trash-panel";
 import { loadTrashPanel, trashPanelActions } from "store/trash-panel/trash-panel-action";
 import { loadProcessPanel } from "store/process-panel/process-panel-actions";
 import { loadSharedWithMePanel, sharedWithMePanelActions } from "store/shared-with-me-panel/shared-with-me-panel-actions";
+import { sharedWithMePanelColumns } from "views/shared-with-me-panel/shared-with-me-panel";
 import { CopyFormDialogData } from "store/copy-dialog/copy-dialog";
 import { workflowPanelActions } from "store/workflow-panel/workflow-panel-actions";
 import { loadSshKeysPanel } from "store/auth/auth-action-ssh";
@@ -140,7 +141,7 @@ export const loadWorkbench = () => async (dispatch: Dispatch, getState: () => Ro
             })
         );
         dispatch(trashPanelActions.SET_COLUMNS({ columns: trashPanelColumns }));
-        dispatch(sharedWithMePanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
+        dispatch(sharedWithMePanelActions.SET_COLUMNS({ columns: sharedWithMePanelColumns }));
         dispatch(workflowPanelActions.SET_COLUMNS({ columns: workflowPanelColumns }));
         dispatch(
             searchResultsPanelActions.SET_FETCH_MODE({
index 13f0a6d4513bfe24ab2e4e046334e87c2ac78d2e..059aad4344fad0f49d55349f79e076a3f92d92e9 100644 (file)
@@ -917,6 +917,30 @@ const _resourceWithName = withStyles(
     );
 });
 
+const _resourceWithNameLink = withStyles(
+    {},
+    { withTheme: true }
+)((props: { uuid: string; userFullname: string; dispatch: Dispatch; theme: ArvadosTheme }) => {
+    const { uuid, userFullname, dispatch, theme } = props;
+    if (!userFullname) {
+        dispatch<any>(loadResource(uuid, false));
+    }
+
+    return (
+        <Typography
+            style={{ color: theme.palette.primary.main, cursor: 'pointer' }}
+            inline
+            noWrap
+            onClick={() => dispatch<any>(navigateTo(uuid))}
+        >
+            {userFullname ? userFullname : uuid}
+        </Typography>
+    )
+});
+
+
+export const ResourceOwnerWithNameLink = ownerFromResourceId(_resourceWithNameLink);
+
 export const ResourceOwnerWithName = ownerFromResourceId(_resourceWithName);
 
 export const ResourceWithName = userFromID(_resourceWithName);
index 9984886b8ac0369d5dbbf296caedc58d2afcbde4..47d34216cc58959678d7e4a7463e98c92348cf69 100644 (file)
@@ -20,22 +20,23 @@ const SidePanelToggle = (props: collapseButtonProps) => {
         root: {
             width: `${COLLAPSE_ICON_SIZE}px`,
             height: `${COLLAPSE_ICON_SIZE}px`,
-            marginTop: '0.4rem'
+            marginTop: '0.4rem',
+            marginLeft: '0.7rem',
+            paddingTop: '1rem',
+            paddingRight: '1rem'
         },
         icon: {
-            height: '1.5rem',
-            width: '3rem',
-            opacity: '0.6',
+            opacity: '0.5',
         },
     }
 
     return <Tooltip disableFocusListener title="Toggle Side Panel">
-        <IconButton style={collapseButtonIconStyles.root} onClick={() => { props.toggleSidePanel(props.isCollapsed) }}>
+        <IconButton data-cy="side-panel-toggle" style={collapseButtonIconStyles.root} onClick={() => { props.toggleSidePanel(props.isCollapsed) }}>
             <div>
                 {props.isCollapsed ?
-                    <img style={{ ...collapseButtonIconStyles.icon, transform: "rotate(180deg)" }} src='/collapseLHS-New.svg#svgView(preserveAspectRatio(none))' alt='expand button' />
+                    <img style={{...collapseButtonIconStyles.icon, marginLeft:'0.25rem'}} src='/mui-start-icon.svg' alt='an arrow pointing right'/>
                     :
-                    <img style={{ ...collapseButtonIconStyles.icon, }} src='/collapseLHS-New.svg#svgView(preserveAspectRatio(none))' alt='collapse button' />}
+                    <img style={{ ...collapseButtonIconStyles.icon, transform: "rotate(180deg)"}} src='/mui-start-icon.svg' alt='an arrow pointing right'/>}
             </div>
         </IconButton>
     </Tooltip>
index 6814a31eb1b8a5b0459ed0609b0dcc9792e4d0d0..19ab3184af88e2f042afb7ac1b02c98bcbfa877e 100644 (file)
@@ -9,7 +9,7 @@ import { TreePicker, TreePickerProps } from "../tree-picker/tree-picker";
 import { TreeItem } from "components/tree/tree";
 import { ProjectResource } from "models/project";
 import { ListItemTextIcon } from "components/list-item-text-icon/list-item-text-icon";
-import { ProcessIcon, ProjectIcon, FilterGroupIcon, FavoriteIcon, ProjectsIcon, ShareMeIcon, TrashIcon, PublicFavoriteIcon, GroupsIcon } from 'components/icon/icon';
+import { ProcessIcon, ProjectIcon, FilterGroupIcon, FavoriteIcon, ProjectsIcon, ShareMeIcon, TrashIcon, PublicFavoriteIcon, GroupsIcon, TerminalIcon } from 'components/icon/icon';
 import { activateSidePanelTreeItem, toggleSidePanelTreeItemCollapse, SIDE_PANEL_TREE, SidePanelTreeCategory } from 'store/side-panel-tree/side-panel-tree-actions';
 import { openSidePanelContextMenu } from 'store/context-menu/context-menu-actions';
 import { noop } from 'lodash';
@@ -64,7 +64,7 @@ const getProjectPickerIcon = (item: TreeItem<ProjectResource | string>) =>
         ? getSidePanelIcon(item.data)
         : (item.data && item.data.groupClass === GroupClass.FILTER)
             ? FilterGroupIcon
-            : ProjectIcon;
+            : ProjectsIcon;
 
 export const getSidePanelIcon = (category: string) => {
     switch (category) {
@@ -82,6 +82,8 @@ export const getSidePanelIcon = (category: string) => {
             return ProcessIcon;
         case SidePanelTreeCategory.GROUPS:
             return GroupsIcon;
+        case SidePanelTreeCategory.SHELL_ACCESS:
+            return TerminalIcon
         default:
             return ProjectIcon;
     }
diff --git a/src/views-components/side-panel/side-panel-collapsed.tsx b/src/views-components/side-panel/side-panel-collapsed.tsx
new file mode 100644 (file)
index 0000000..d2f5cfe
--- /dev/null
@@ -0,0 +1,159 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React, { ReactElement } from 'react'
+import { connect } from 'react-redux'
+import { ProjectsIcon, ProcessIcon, FavoriteIcon, ShareMeIcon, TrashIcon, PublicFavoriteIcon, GroupsIcon } from 'components/icon/icon'
+import { TerminalIcon } from 'components/icon/icon'
+import { IconButton, List, ListItem, Tooltip } from '@material-ui/core'
+import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles'
+import { ArvadosTheme } from 'common/custom-theme'
+import { navigateTo } from 'store/navigation/navigation-action'
+import { RootState } from 'store/store'
+import { Dispatch } from 'redux'
+import {
+    navigateToSharedWithMe,
+    navigateToPublicFavorites,
+    navigateToFavorites,
+    navigateToGroups,
+    navigateToAllProcesses,
+    navigateToTrash,
+} from 'store/navigation/navigation-action'
+import { navigateToUserVirtualMachines } from 'store/navigation/navigation-action'
+import { RouterAction } from 'react-router-redux'
+import { User } from 'models/user'
+
+type CssRules = 'button' | 'unselected' | 'selected'
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    button: {
+        width: '40px',
+        height: '40px',
+        paddingLeft: '-2rem',
+        marginLeft: '-0.6rem',
+        marginBottom: '-1rem'
+    },
+    unselected: {
+        color: theme.customs.colors.grey600,
+    },
+    selected: {
+        color: theme.palette.primary.main,
+    },
+})
+
+enum SidePanelCollapsedCategory {
+    PROJECTS = 'Home Projects',
+    FAVORITES = 'My Favorites',
+    PUBLIC_FAVORITES = 'Public Favorites',
+    SHARED_WITH_ME = 'Shared with me',
+    ALL_PROCESSES = 'All Processes',
+    SHELL_ACCESS = 'Shell Access',
+    GROUPS = 'Groups',
+    TRASH = 'Trash',
+}
+
+type TCollapsedCategory = {
+    name: SidePanelCollapsedCategory
+    icon: ReactElement
+    navTarget: RouterAction | ''
+}
+
+const sidePanelCollapsedCategories: TCollapsedCategory[] = [
+    {
+        name: SidePanelCollapsedCategory.PROJECTS,
+        icon: <ProjectsIcon />,
+        navTarget: '',
+    },
+    {
+        name: SidePanelCollapsedCategory.FAVORITES,
+        icon: <FavoriteIcon />,
+        navTarget: navigateToFavorites,
+    },
+    {
+        name: SidePanelCollapsedCategory.PUBLIC_FAVORITES,
+        icon: <PublicFavoriteIcon />,
+        navTarget: navigateToPublicFavorites,
+    },
+    {
+        name: SidePanelCollapsedCategory.SHARED_WITH_ME,
+        icon: <ShareMeIcon />,
+        navTarget: navigateToSharedWithMe,
+    },
+    {
+        name: SidePanelCollapsedCategory.ALL_PROCESSES,
+        icon: <ProcessIcon />,
+        navTarget: navigateToAllProcesses,
+    },
+    {
+        name: SidePanelCollapsedCategory.SHELL_ACCESS,
+        icon: <TerminalIcon />,
+        navTarget: navigateToUserVirtualMachines,
+    },
+    {
+        name: SidePanelCollapsedCategory.GROUPS,
+        icon: <GroupsIcon style={{marginLeft: '2px', scale: '85%'}}/>,
+        navTarget: navigateToGroups,
+    },
+    {
+        name: SidePanelCollapsedCategory.TRASH,
+        icon: <TrashIcon />,
+        navTarget: navigateToTrash,
+    },
+]
+
+type SidePanelCollapsedProps = {
+    user: User;
+    selectedPath: string;
+    navToHome: (uuid: string) => void;
+    navTo: (navTarget: RouterAction | '') => void;
+};
+
+const mapStateToProps = ({auth, properties }: RootState) => {
+        return {
+            user: auth.user,
+            selectedPath: properties.breadcrumbs
+                ? properties.breadcrumbs[0].label !== 'Virtual Machines'
+                ? properties.breadcrumbs[0].label
+                : SidePanelCollapsedCategory.SHELL_ACCESS
+                : SidePanelCollapsedCategory.PROJECTS,
+        }
+}
+
+const mapDispatchToProps = (dispatch: Dispatch) => {
+    return {
+        navToHome: (navTarget) => dispatch<any>(navigateTo(navTarget)),
+        navTo: (navTarget) => dispatch<any>(navTarget),
+    }
+}
+
+export const SidePanelCollapsed = withStyles(styles)(
+    connect(mapStateToProps, mapDispatchToProps)(({ classes, user, selectedPath, navToHome, navTo }: WithStyles & SidePanelCollapsedProps) => {
+
+        const handleClick = (cat: TCollapsedCategory) => {
+            if (cat.name === SidePanelCollapsedCategory.PROJECTS) navToHome(user.uuid)
+            else navTo(cat.navTarget)
+        }
+
+        const { button, unselected, selected } = classes
+        return (
+            <List data-cy='side-panel-collapsed'>
+                {sidePanelCollapsedCategories.map((cat) => (
+                    <ListItem
+                        key={cat.name}
+                        data-cy={`collapsed-${cat.name.toLowerCase().replace(/\s+/g, '-')}`}
+                        onClick={() => handleClick(cat)}
+                        >
+                        <Tooltip
+                            className={selectedPath === cat.name ? selected : unselected}
+                            title={cat.name}
+                            disableFocusListener
+                            >
+                            <IconButton className={button}>{cat.icon}</IconButton>
+                        </Tooltip>
+                    </ListItem>
+                ))}
+            </List>
+        );
+    })
+)
index 4953022d661bd635ff34fa72e667c30b47a6d077..18aed873aa9fc018b36585ea09ea2846122fd28a 100644 (file)
@@ -13,6 +13,7 @@ import { Grid } from '@material-ui/core';
 import { SidePanelButton } from 'views-components/side-panel-button/side-panel-button';
 import { RootState } from 'store/store';
 import SidePanelToggle from 'views-components/side-panel-toggle/side-panel-toggle';
+import { SidePanelCollapsed } from './side-panel-collapsed';
 
 const DRAWER_WIDTH = 240;
 
@@ -47,7 +48,12 @@ export const SidePanel = withStyles(styles)(
     connect(mapStateToProps, mapDispatchToProps)(
         ({ classes, ...props }: WithStyles<CssRules> & SidePanelTreeProps & { currentRoute: string }) =>
             <Grid item xs>
-                {props.isCollapsed ? <SidePanelToggle /> :
+                {props.isCollapsed ? 
+                <>
+                    <SidePanelToggle />
+                    <SidePanelCollapsed />
+                </>
+                :
                 <>
                     <Grid className={classes.topButtonContainer}>
                         <SidePanelButton key={props.currentRoute} />
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 7731df6bc84693ee6528e9395afdb2e97cdafdd4..f3f827d1469fc27fe8c90d8f543123ba4755698d 100644 (file)
@@ -10,6 +10,7 @@ import { RootState } from 'store/store';
 import { ArvadosTheme } from 'common/custom-theme';
 import { ShareMeIcon } from 'components/icon/icon';
 import { ResourcesState, getResource } from 'store/resources/resources';
+import { ResourceKind } from 'models/resource';
 import { navigateTo } from "store/navigation/navigation-action";
 import { loadDetailsPanel } from "store/details-panel/details-panel-action";
 import { SHARED_WITH_ME_PANEL_ID } from 'store/shared-with-me-panel/shared-with-me-panel-actions';
@@ -17,8 +18,36 @@ import {
     openContextMenu,
     resourceUuidToContextMenuKind
 } from 'store/context-menu/context-menu-actions';
+import {
+    ResourceName,
+    ProcessStatus as ResourceStatus,
+    ResourceType,
+    ResourceOwnerWithNameLink,
+    ResourcePortableDataHash,
+    ResourceFileSize,
+    ResourceFileCount,
+    ResourceUUID,
+    ResourceContainerUuid,
+    ContainerRunTime,
+    ResourceOutputUuid,
+    ResourceLogUuid,
+    ResourceParentProcess,
+    ResourceModifiedByUserUuid,
+    ResourceVersion,
+    ResourceCreatedAtDate,
+    ResourceLastModifiedDate,
+    ResourceTrashDate,
+    ResourceDeleteDate,
+} from 'views-components/data-explorer/renderers';
+import { DataTableFilterItem } from 'components/data-table-filters/data-table-filters';
 import { GroupContentsResource } from 'services/groups-service/groups-service';
 import { toggleOne } from 'store/multiselect/multiselect-actions';
+import { DataColumns } from 'components/data-table/data-table';
+import { ContainerRequestState } from 'models/container-request';
+import { ProjectResource } from 'models/project';
+import { createTree } from 'models/tree';
+import { SortDirection } from 'components/data-table/data-column';
+import { getInitialResourceTypeFilters, getInitialProcessStatusFilters } from 'store/resource-type-filters/resource-type-filters';
 
 type CssRules = "toolbar" | "button" | "root";
 
@@ -35,6 +64,175 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     },
 });
 
+export enum SharedWithMePanelColumnNames {
+    NAME = 'Name',
+    STATUS = 'Status',
+    TYPE = 'Type',
+    OWNER = 'Owner',
+    PORTABLE_DATA_HASH = 'Portable Data Hash',
+    FILE_SIZE = 'File Size',
+    FILE_COUNT = 'File Count',
+    UUID = 'UUID',
+    CONTAINER_UUID = 'Container UUID',
+    RUNTIME = 'Runtime',
+    OUTPUT_UUID = 'Output UUID',
+    LOG_UUID = 'Log UUID',
+    PARENT_PROCESS = 'Parent Process UUID',
+    MODIFIED_BY_USER_UUID = 'Modified by User UUID',
+    VERSION = 'Version',
+    CREATED_AT = 'Date Created',
+    LAST_MODIFIED = 'Last Modified',
+    TRASH_AT = 'Trash at',
+    DELETE_AT = 'Delete at',
+}
+
+export interface ProjectPanelFilter extends DataTableFilterItem {
+    type: ResourceKind | ContainerRequestState;
+}
+
+export const sharedWithMePanelColumns: DataColumns<string, ProjectResource> = [
+    {
+        name: SharedWithMePanelColumnNames.NAME,
+        selected: true,
+        configurable: true,
+        sort: { direction: SortDirection.NONE, field: 'name' },
+        filters: createTree(),
+        render: (uuid) => <ResourceName uuid={uuid} />,
+    },
+    {
+        name: SharedWithMePanelColumnNames.STATUS,
+        selected: true,
+        configurable: true,
+        mutuallyExclusiveFilters: true,
+        filters: getInitialProcessStatusFilters(),
+        render: (uuid) => <ResourceStatus uuid={uuid} />,
+    },
+    {
+        name: SharedWithMePanelColumnNames.TYPE,
+        selected: true,
+        configurable: true,
+        filters: getInitialResourceTypeFilters(),
+        render: (uuid) => <ResourceType uuid={uuid} />,
+    },
+    {
+        name: SharedWithMePanelColumnNames.OWNER,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: (uuid) => <ResourceOwnerWithNameLink uuid={uuid} />,
+    },
+    {
+        name: SharedWithMePanelColumnNames.PORTABLE_DATA_HASH,
+        selected: false,
+        configurable: true,
+        filters: createTree(),
+        render: (uuid) => <ResourcePortableDataHash uuid={uuid} />,
+    },
+    {
+        name: SharedWithMePanelColumnNames.FILE_SIZE,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: (uuid) => <ResourceFileSize uuid={uuid} />,
+    },
+    {
+        name: SharedWithMePanelColumnNames.FILE_COUNT,
+        selected: false,
+        configurable: true,
+        filters: createTree(),
+        render: (uuid) => <ResourceFileCount uuid={uuid} />,
+    },
+    {
+        name: SharedWithMePanelColumnNames.UUID,
+        selected: false,
+        configurable: true,
+        filters: createTree(),
+        render: (uuid) => <ResourceUUID uuid={uuid} />,
+    },
+    {
+        name: SharedWithMePanelColumnNames.CONTAINER_UUID,
+        selected: false,
+        configurable: true,
+        filters: createTree(),
+        render: (uuid) => <ResourceContainerUuid uuid={uuid} />,
+    },
+    {
+        name: SharedWithMePanelColumnNames.RUNTIME,
+        selected: false,
+        configurable: true,
+        filters: createTree(),
+        render: (uuid) => <ContainerRunTime uuid={uuid} />,
+    },
+    {
+        name: SharedWithMePanelColumnNames.OUTPUT_UUID,
+        selected: false,
+        configurable: true,
+        filters: createTree(),
+        render: (uuid) => <ResourceOutputUuid uuid={uuid} />,
+    },
+    {
+        name: SharedWithMePanelColumnNames.LOG_UUID,
+        selected: false,
+        configurable: true,
+        filters: createTree(),
+        render: (uuid) => <ResourceLogUuid uuid={uuid} />,
+    },
+    {
+        name: SharedWithMePanelColumnNames.PARENT_PROCESS,
+        selected: false,
+        configurable: true,
+        filters: createTree(),
+        render: (uuid) => <ResourceParentProcess uuid={uuid} />,
+    },
+    {
+        name: SharedWithMePanelColumnNames.MODIFIED_BY_USER_UUID,
+        selected: false,
+        configurable: true,
+        filters: createTree(),
+        render: (uuid) => <ResourceModifiedByUserUuid uuid={uuid} />,
+    },
+    {
+        name: SharedWithMePanelColumnNames.VERSION,
+        selected: false,
+        configurable: true,
+        filters: createTree(),
+        render: (uuid) => <ResourceVersion uuid={uuid} />,
+    },
+    {
+        name: SharedWithMePanelColumnNames.CREATED_AT,
+        selected: false,
+        configurable: true,
+        sort: { direction: SortDirection.NONE, field: 'createdAt' },
+        filters: createTree(),
+        render: (uuid) => <ResourceCreatedAtDate uuid={uuid} />,
+    },
+    {
+        name: SharedWithMePanelColumnNames.LAST_MODIFIED,
+        selected: true,
+        configurable: true,
+        sort: { direction: SortDirection.DESC, field: 'modifiedAt' },
+        filters: createTree(),
+        render: (uuid) => <ResourceLastModifiedDate uuid={uuid} />,
+    },
+    {
+        name: SharedWithMePanelColumnNames.TRASH_AT,
+        selected: false,
+        configurable: true,
+        sort: { direction: SortDirection.NONE, field: 'trashAt' },
+        filters: createTree(),
+        render: (uuid) => <ResourceTrashDate uuid={uuid} />,
+    },
+    {
+        name: SharedWithMePanelColumnNames.DELETE_AT,
+        selected: false,
+        configurable: true,
+        sort: { direction: SortDirection.NONE, field: 'deleteAt' },
+        filters: createTree(),
+        render: (uuid) => <ResourceDeleteDate uuid={uuid} />,
+    },
+];
+
+
 interface SharedWithMePanelDataProps {
     resources: ResourcesState;
     userUuid: string;
index 9cf1db7753e6a90c8666d5ac4fcee361dff7ce23..dd5229bb3568f65af9743dbc155da1f8201f9e96 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/>}
+        progressBar={<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 a02d346f2857f09c6a31cae7d815a6de19be7c66..bc2396f7cf8c2930444d6ecb3bb80324a9d5326d 100644 (file)
@@ -119,7 +119,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     },
     splitter: {
         "& > .layout-splitter": {
-            width: "2px",
+            width: "3px",
         },
         "& > .layout-splitter-disabled": {
             pointerEvents: "none",
@@ -292,7 +292,7 @@ const applyCollapsedState = isCollapsed => {
     const totalWidth: number = document.getElementsByClassName("splitter-layout")[0]?.clientWidth;
     const rightPanelExpandedWidth = (totalWidth - COLLAPSE_ICON_SIZE) / (totalWidth / 100);
     if (rightPanel) {
-        rightPanel.setAttribute("style", `width: ${isCollapsed ? rightPanelExpandedWidth : getSplitterInitialSize()}%`);
+        rightPanel.setAttribute("style", `width: ${isCollapsed ? `calc(${rightPanelExpandedWidth}% - 1rem)` : `${getSplitterInitialSize()}%`}`);
     }
     const splitter = document.getElementsByClassName("layout-splitter")[0];
     isCollapsed ? splitter?.classList.add("layout-splitter-disabled") : splitter?.classList.remove("layout-splitter-disabled");
index f9dfa6a9546b954ed10657180b72387ff2e8c508..2e0c4f2a1e2dd0b18d9e71faf93c262cc90f40cd 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
@@ -1646,6 +1646,28 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@coreui/coreui@npm:^4.3.2":
+  version: 4.3.2
+  resolution: "@coreui/coreui@npm:4.3.2"
+  dependencies:
+    postcss-combine-duplicated-selectors: ^10.0.3
+  peerDependencies:
+    "@popperjs/core": ^2.11.6
+  checksum: 88fc70f4f681bb796e1d81ca8472a3d36bfcf92866fc7c6810ead850bc371c99bca123a94abb0fafdf2935972d130005cd62b485406631cfd9abd8f38e14be15
+  languageName: node
+  linkType: hard
+
+"@coreui/react@npm:^4.11.0":
+  version: 4.11.0
+  resolution: "@coreui/react@npm:4.11.0"
+  peerDependencies:
+    "@coreui/coreui": 4.3.0
+    react: ">=17"
+    react-dom: ">=17"
+  checksum: 75c9394125e41e24fb5855b82cba93c9abeea080f9ee5bcc063ff2e581318b85c5bbef6f2c5300f5fd7a3450743488daa29b4baee6feabec38a009a452876a88
+  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 +3822,8 @@ __metadata:
   version: 0.0.0-use.local
   resolution: "arvados-workbench-2@workspace:."
   dependencies:
+    "@coreui/coreui": ^4.3.2
+    "@coreui/react": ^4.11.0
     "@date-io/date-fns": 1
     "@fortawesome/fontawesome-svg-core": 1.2.28
     "@fortawesome/free-solid-svg-icons": 5.13.0
@@ -3841,6 +3865,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
@@ -3865,17 +3890,17 @@ __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
     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
@@ -4605,6 +4630,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"
@@ -5103,17 +5137,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30000981, caniuse-lite@npm:^1.0.30001035, caniuse-lite@npm:^1.0.30001109":
-  version: 1.0.30001486
-  resolution: "caniuse-lite@npm:1.0.30001486"
-  checksum: 5e8c2ba2679e4ad17dea6d2761a6449b814441bfeac81af6cc9d58af187df6af4b79b27befcbfc4a557e720b21c0399a7d1911c8705922e38938dcc0f40b5d4b
-  languageName: node
-  linkType: hard
-
-"caniuse-lite@npm:^1.0.30001541":
-  version: 1.0.30001543
-  resolution: "caniuse-lite@npm:1.0.30001543"
-  checksum: 1a65c8b0b93913b6241c7d66e1e1f3ea0f194f7e140eefe500512641c2eb4df285991ec9869a1ba2856ea6f6d21e9f3d7bcd91971b5fb1721e3fa0390feec6f1
+"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30000981, caniuse-lite@npm:^1.0.30001035, caniuse-lite@npm:^1.0.30001109, caniuse-lite@npm:^1.0.30001541":
+  version: 1.0.30001561
+  resolution: "caniuse-lite@npm:1.0.30001561"
+  checksum: 949829fe037e23346595614e01d362130245920503a12677f2506ce68e1240360113d6383febed41e8aa38cd0f5fd9c69c21b0af65a71c0246d560db489f1373
   languageName: node
   linkType: hard
 
@@ -12587,14 +12614,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"moment@npm:2.29.1":
-  version: 2.29.1
-  resolution: "moment@npm:2.29.1"
-  checksum: 1e14d5f422a2687996be11dd2d50c8de3bd577c4a4ca79ba5d02c397242a933e5b941655de6c8cb90ac18f01cc4127e55b4a12ae3c527a6c0a274e455979345e
-  languageName: node
-  linkType: hard
-
-"moment@npm:^2.27.0":
+"moment@npm:^2.27.0, moment@npm:^2.29.4":
   version: 2.29.4
   resolution: "moment@npm:2.29.4"
   checksum: 0ec3f9c2bcba38dc2451b1daed5daded747f17610b92427bebe1d08d48d8b7bdd8d9197500b072d14e326dd0ccf3e326b9e3d07c5895d3d49e39b6803b76e80e
@@ -14120,6 +14140,17 @@ __metadata:
   languageName: node
   linkType: hard
 
+"postcss-combine-duplicated-selectors@npm:^10.0.3":
+  version: 10.0.3
+  resolution: "postcss-combine-duplicated-selectors@npm:10.0.3"
+  dependencies:
+    postcss-selector-parser: ^6.0.4
+  peerDependencies:
+    postcss: ^8.1.0
+  checksum: 45c3dff41d0cddb510752ed92fe8c7fc66e5cf88f4988314655419d3ecdf1dc66f484a25ee73f4f292da5da851a0fdba0ec4d59bdedeee935d05b26d31d997ed
+  languageName: node
+  linkType: hard
+
 "postcss-convert-values@npm:^4.0.1":
   version: 4.0.1
   resolution: "postcss-convert-values@npm:4.0.1"
@@ -14764,6 +14795,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"postcss-selector-parser@npm:^6.0.4":
+  version: 6.0.13
+  resolution: "postcss-selector-parser@npm:6.0.13"
+  dependencies:
+    cssesc: ^3.0.0
+    util-deprecate: ^1.0.2
+  checksum: f89163338a1ce3b8ece8e9055cd5a3165e79a15e1c408e18de5ad8f87796b61ec2d48a2902d179ae0c4b5de10fccd3a325a4e660596549b040bc5ad1b465f096
+  languageName: node
+  linkType: hard
+
 "postcss-svgo@npm:^4.0.3":
   version: 4.0.3
   resolution: "postcss-svgo@npm:4.0.3"
@@ -15317,17 +15358,17 @@ __metadata:
   languageName: node
   linkType: hard
 
-"react-dom@npm:16.8.6":
-  version: 16.8.6
-  resolution: "react-dom@npm:16.8.6"
+"react-dom@npm:16.14.0":
+  version: 16.14.0
+  resolution: "react-dom@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
 
@@ -15651,15 +15692,14 @@ __metadata:
   languageName: node
   linkType: hard
 
-"react@npm:16.8.6":
-  version: 16.8.6
-  resolution: "react@npm:16.8.6"
+"react@npm:16.14.0":
+  version: 16.14.0
+  resolution: "react@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
 
@@ -16664,16 +16704,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"scheduler@npm:^0.13.6":
-  version: 0.13.6
-  resolution: "scheduler@npm:0.13.6"
-  dependencies:
-    loose-envify: ^1.1.0
-    object-assign: ^4.1.1
-  checksum: c82c705f6d0d6df87b26bf2cca33f427e91889438c0435ade3ee7f41860eda4dd7f3171ca2d93e8fe9431f3bd831ca0e267a401a0296e4b14de05e389f82d320
-  languageName: node
-  linkType: hard
-
 "scheduler@npm:^0.19.1":
   version: 0.19.1
   resolution: "scheduler@npm:0.19.1"