[ARVADOS-WORKBENCH2] updated: 2.1.0-248-g7e21998f

Git user git at public.arvados.org
Thu Mar 11 19:11:19 UTC 2021


Summary of changes:
 cypress/integration/favorites.spec.js              | 234 ++++++++++-----------
 cypress/integration/sharing.spec.js                |  81 +++++++
 cypress/support/commands.js                        |  44 +++-
 src/components/code-snippet/code-snippet.tsx       |   2 +-
 .../details-attribute/details-attribute.tsx        |   2 +-
 src/components/tree/virtual-tree.tsx               |  10 +-
 src/index.tsx                                      |   4 +-
 src/models/api-client-authorization.ts             |   5 +-
 src/services/common-service/common-service.ts      |   6 +-
 src/store/auth/auth-action.test.ts                 |  97 ++++++++-
 src/store/auth/auth-action.ts                      |  78 +++++--
 src/store/auth/auth-reducer.ts                     | 103 ++++-----
 src/store/collections/collection-info-actions.ts   |   6 +-
 .../current-token-dialog-actions.tsx               |  26 ---
 src/store/token-dialog/token-dialog-actions.tsx    |  37 ++++
 src/store/users/users-actions.ts                   |   3 +-
 .../auto-logout/auto-logout.test.tsx               |  22 +-
 src/views-components/auto-logout/auto-logout.tsx   |  55 ++++-
 .../current-token-dialog.test.tsx                  |  51 -----
 .../current-token-dialog/current-token-dialog.tsx  | 108 ----------
 src/views-components/main-app-bar/account-menu.tsx |   9 +-
 .../token-dialog/token-dialog.test.tsx             |  80 +++++++
 src/views-components/token-dialog/token-dialog.tsx | 162 ++++++++++++++
 src/views/run-process-panel/inputs/file-input.tsx  |  11 +-
 .../run-process-panel/run-process-inputs-form.tsx  |   2 +-
 src/views/workbench/workbench.tsx                  |   4 +-
 26 files changed, 830 insertions(+), 412 deletions(-)
 create mode 100644 cypress/integration/sharing.spec.js
 delete mode 100644 src/store/current-token-dialog/current-token-dialog-actions.tsx
 create mode 100644 src/store/token-dialog/token-dialog-actions.tsx
 delete mode 100644 src/views-components/current-token-dialog/current-token-dialog.test.tsx
 delete mode 100644 src/views-components/current-token-dialog/current-token-dialog.tsx
 create mode 100644 src/views-components/token-dialog/token-dialog.test.tsx
 create mode 100644 src/views-components/token-dialog/token-dialog.tsx

  discards  0d4f3ea05cd2f3be99e1a3d57ff91fad2bbbfaca (commit)
  discards  f82ac87b7f774992dfba72a5b4f401b8fc037c9f (commit)
  discards  ddaff5cb9937340eed1c8f3b59053146dcefa3b8 (commit)
  discards  9cc4f706feecd621f9d0121942bb0faa881ec926 (commit)
  discards  f18efe026ee07b02e56a2c972c509c2f8b742712 (commit)
  discards  b28028dbe0bea1aeb3b772733a7c61ef202e1526 (commit)
       via  7e21998fe7cf2fc00d712208b261ec8cf97776db (commit)
       via  66c735017c5ccb08cde2262c723ef7f821ff7c9d (commit)
       via  3eb0041fd960c3c2728872640e41181171991cfb (commit)
       via  aa2b5b7846134d1efbff6ed68ce3304316336061 (commit)
       via  34b3e1b12b9d20a09764f86ec138d2a83c82297b (commit)
       via  59513f60899ac4b422e2e866ffc5611bb82e4458 (commit)
       via  a2eefb6888ba68ffdf6efe49f4f05b388443330d (commit)
       via  835fca715fcf8da391049b46ae89f600569896f0 (commit)
       via  e84e9949a55f5e25682bf7c4e4656a123091f3ff (commit)
       via  21cb7a9d671099a17bcdaab1687310d0b9941650 (commit)
       via  e754a019ded68655ba7fc25bfb2d159cdef45c87 (commit)
       via  afdb6a890b2db8e9ab03a584e714e30a6747fd18 (commit)
       via  c9d21a5a2fc84e47cff7862648ccabe98a331e80 (commit)
       via  d2817690f88befb0c306e637d6e87367016c76e3 (commit)
       via  eb633efdb3fa8ecb0c2c6a5e35916585282436ea (commit)
       via  ab1faae94143599e5537b40211d6b4370e0ecef6 (commit)
       via  335fee64075faf3dac519fba45da5ecbe5008f33 (commit)
       via  6b5e94cfb15a88d33aba5e7c192cfbe6aec9ac8d (commit)
       via  e407e100aa0c2fd0a57b1a17a61e0ead775a0a21 (commit)
       via  aef54eeec8f2e7255e2da119a19d14879dfb7b72 (commit)
       via  0ecbcbd790d784e72f2e7c90fc6a417b79acd022 (commit)
       via  a51a9c10678118004ca3c4fb0af38c5c85d050eb (commit)
       via  44a5f6968f1c2fb449dfb22e1742d2770662e6a9 (commit)
       via  becf93a5f9d1f7d96a3ce868c9b70c3d0094cf45 (commit)
       via  d70bcf2acdaaae12a403cdcf047e7514b145d6e4 (commit)
       via  7d797c647a094822bc4f79be2715f47020e479d1 (commit)
       via  211054b9494b034611467321203618d8fbdf05e2 (commit)
       via  1d7fbccb64462c349ea223df3ac02817ba60bfe1 (commit)
       via  5620b9c4236bab9e9c98b6a4e955498823de4136 (commit)
       via  f326404ea8ff55f3b177877eeef1136af34d18ba (commit)
       via  b7eb7d60846333d93e91f98ec3e6fcdd94fca570 (commit)
       via  ba4e011fc9132aa17cc6c4e8e74a6310a308291e (commit)
       via  5f272aec410b2b1dce368c36570c6c786aefbd71 (commit)
       via  abe391c9f3a2a7ad954ecf81198268076b233ef8 (commit)

This update added new revisions after undoing existing revisions.  That is
to say, the old revision is not a strict subset of the new revision.  This
situation occurs when you --force push a change and generate a repository
containing something like this:

 * -- * -- B -- O -- O -- O (0d4f3ea05cd2f3be99e1a3d57ff91fad2bbbfaca)
            \
             N -- N -- N (7e21998fe7cf2fc00d712208b261ec8cf97776db)

When this happens we assume that you've already had alert emails for all
of the O revisions, and so we here report only the revisions in the N
branch from the common base, B.

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.


commit 7e21998fe7cf2fc00d712208b261ec8cf97776db
Author: Peter Amstutz <peter.amstutz at curii.com>
Date:   Sat Feb 27 17:40:37 2021 -0500

    17426: Pass through menu styling.  Make example a card.
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>

diff --git a/src/common/plugintypes.ts b/src/common/plugintypes.ts
index bda92b67..00cc1e36 100644
--- a/src/common/plugintypes.ts
+++ b/src/common/plugintypes.ts
@@ -8,7 +8,7 @@ import { RootStore, RootState } from '~/store/store';
 import { ResourcesState } from '~/store/resources/resources';
 import { Location } from 'history';
 
-export type ElementListReducer = (startingList: React.ReactElement[]) => React.ReactElement[];
+export type ElementListReducer = (startingList: React.ReactElement[], itemClass?: string) => React.ReactElement[];
 export type CategoriesListReducer = (startingList: string[]) => string[];
 export type NavigateMatcher = (dispatch: Dispatch, getState: () => RootState, uuid: string) => boolean;
 export type LocationChangeMatcher = (store: RootStore, pathname: string) => boolean;
diff --git a/src/plugins.tsx b/src/plugins.tsx
index 3a58a8c2..a7d033dd 100644
--- a/src/plugins.tsx
+++ b/src/plugins.tsx
@@ -20,10 +20,10 @@ export const pluginConfig: PluginConfig = {
 
 // Starting here, import and register your Workbench 2 plugins. //
 
-import { register as blankUIPluginRegister } from '~/plugins/blank/index';
+// import { register as blankUIPluginRegister } from '~/plugins/blank/index';
 import { register as examplePluginRegister, routePath as exampleRoutePath } from '~/plugins/example/index';
 import { register as rootRedirectRegister } from '~/plugins/root-redirect/index';
 
-blankUIPluginRegister(pluginConfig);
+// blankUIPluginRegister(pluginConfig);
 examplePluginRegister(pluginConfig);
 rootRedirectRegister(pluginConfig, exampleRoutePath);
diff --git a/src/plugins/example/index.tsx b/src/plugins/example/index.tsx
index 6e2b1dee..36647fac 100644
--- a/src/plugins/example/index.tsx
+++ b/src/plugins/example/index.tsx
@@ -9,7 +9,7 @@ import * as React from 'react';
 import { Dispatch } from 'redux';
 import { RootState } from '~/store/store';
 import { push } from "react-router-redux";
-import { Typography } from "@material-ui/core";
+import { Card, CardContent, Typography } from "@material-ui/core";
 import { Route, matchPath } from "react-router";
 import { RootStore } from '~/store/store';
 import { activateSidePanelTreeItem } from '~/store/side-panel-tree/side-panel-tree-actions';
@@ -26,6 +26,7 @@ const propertyKey = "Example_menu_item_pressed_count";
 
 interface ExampleProps {
     pressedCount: number;
+    className?: string;
 }
 
 const exampleMapStateToProps = (state: RootState) => ({ pressedCount: state.properties[propertyKey] || 0 });
@@ -35,15 +36,19 @@ const incrementPressedCount = (dispatch: Dispatch, pressedCount: number) => {
 };
 
 const ExampleMenuComponent = connect(exampleMapStateToProps)(
-    ({ pressedCount, dispatch }: ExampleProps & DispatchProp<any>) =>
-        <MenuItem onClick={() => incrementPressedCount(dispatch, pressedCount)}>Example menu item</MenuItem >
+    ({ pressedCount, dispatch, className }: ExampleProps & DispatchProp<any>) =>
+        <MenuItem className={className} onClick={() => incrementPressedCount(dispatch, pressedCount)}>Example menu item</MenuItem >
 );
 
 const ExamplePluginMainPanel = connect(exampleMapStateToProps)(
     ({ pressedCount }: ExampleProps) =>
-        <Typography>
-            This is a example main panel plugin.  The example menu item has been pressed {pressedCount} times.
-	</Typography>);
+        <Card>
+            <CardContent>
+                <Typography>
+                    This is a example main panel plugin.  The example menu item has been pressed {pressedCount} times.
+		</Typography>
+            </CardContent>
+        </Card>);
 
 export const register = (pluginConfig: PluginConfig) => {
 
@@ -52,13 +57,13 @@ export const register = (pluginConfig: PluginConfig) => {
         return elms;
     });
 
-    pluginConfig.accountMenuList.push((elms) => {
-        elms.push(<ExampleMenuComponent />);
+    pluginConfig.accountMenuList.push((elms, menuItemClass) => {
+        elms.push(<ExampleMenuComponent className={menuItemClass} />);
         return elms;
     });
 
-    pluginConfig.newButtonMenuList.push((elms) => {
-        elms.push(<ExampleMenuComponent />);
+    pluginConfig.newButtonMenuList.push((elms, menuItemClass) => {
+        elms.push(<ExampleMenuComponent className={menuItemClass} />);
         return elms;
     });
 
diff --git a/src/views-components/side-panel-button/side-panel-button.tsx b/src/views-components/side-panel-button/side-panel-button.tsx
index 4c25bcfe..151cfb68 100644
--- a/src/views-components/side-panel-button/side-panel-button.tsx
+++ b/src/views-components/side-panel-button/side-panel-button.tsx
@@ -113,8 +113,8 @@ export const SidePanelButton = withStyles(styles)(
                     </MenuItem>
                 </>;
 
-                const reduceItemsFn: (a: React.ReactElement[],
-                    b: ElementListReducer) => React.ReactElement[] = (a, b) => b(a);
+                const reduceItemsFn: (a: React.ReactElement[], b: ElementListReducer) => React.ReactElement[] =
+                    (a, b) => b(a, classes.menuItem);
 
                 menuItems = React.createElement(React.Fragment, null,
                     pluginConfig.newButtonMenuList.reduce(reduceItemsFn, React.Children.toArray(menuItems.props.children)));

commit 66c735017c5ccb08cde2262c723ef7f821ff7c9d
Author: Peter Amstutz <peter.amstutz at curii.com>
Date:   Sat Feb 27 17:20:26 2021 -0500

    17426: Fix stuck on loading screen.
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>

diff --git a/src/plugins/example/index.tsx b/src/plugins/example/index.tsx
index b8bfcb0f..6e2b1dee 100644
--- a/src/plugins/example/index.tsx
+++ b/src/plugins/example/index.tsx
@@ -18,6 +18,7 @@ import { DispatchProp, connect } from 'react-redux';
 import { MenuItem } from "@material-ui/core";
 import { propertiesActions } from '~/store/properties/properties-actions';
 import { Location } from 'history';
+import { handleFirstTimeLoad } from '~/store/workbench/workbench-actions';
 
 const categoryName = "Plugin Example";
 export const routePath = "/examplePlugin";
@@ -73,8 +74,11 @@ export const register = (pluginConfig: PluginConfig) => {
 
     pluginConfig.locationChangeHandlers.push((store: RootStore, pathname: string): boolean => {
         if (matchPath(pathname, { path: routePath, exact: true })) {
-            store.dispatch(activateSidePanelTreeItem(categoryName));
-            store.dispatch<any>(setSidePanelBreadcrumbs(categoryName));
+            store.dispatch(handleFirstTimeLoad(
+                (dispatch: Dispatch) => {
+                    dispatch<any>(activateSidePanelTreeItem(categoryName));
+                    dispatch<any>(setSidePanelBreadcrumbs(categoryName));
+                }));
             return true;
         }
         return false;
diff --git a/src/store/workbench/workbench-actions.ts b/src/store/workbench/workbench-actions.ts
index 217c8524..8ea19a14 100644
--- a/src/store/workbench/workbench-actions.ts
+++ b/src/store/workbench/workbench-actions.ts
@@ -110,7 +110,7 @@ export const isWorkbenchLoading = (state: RootState) => {
     return progress ? progress.working : false;
 };
 
-const handleFirstTimeLoad = (action: any) =>
+export const handleFirstTimeLoad = (action: any) =>
     async (dispatch: Dispatch<any>, getState: () => RootState) => {
         try {
             await dispatch(action);

commit 3eb0041fd960c3c2728872640e41181171991cfb
Author: Peter Amstutz <peter.amstutz at curii.com>
Date:   Sat Feb 27 17:05:15 2021 -0500

    17426: Add plugin ability to modify +New and account menu
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>

diff --git a/src/common/plugintypes.ts b/src/common/plugintypes.ts
index dfbe7c45..bda92b67 100644
--- a/src/common/plugintypes.ts
+++ b/src/common/plugintypes.ts
@@ -5,15 +5,18 @@
 import * as React from 'react';
 import { Dispatch } from 'redux';
 import { RootStore, RootState } from '~/store/store';
+import { ResourcesState } from '~/store/resources/resources';
+import { Location } from 'history';
 
-export type RouteListReducer = (startingList: React.ReactElement[]) => React.ReactElement[];
+export type ElementListReducer = (startingList: React.ReactElement[]) => React.ReactElement[];
 export type CategoriesListReducer = (startingList: string[]) => string[];
 export type NavigateMatcher = (dispatch: Dispatch, getState: () => RootState, uuid: string) => boolean;
 export type LocationChangeMatcher = (store: RootStore, pathname: string) => boolean;
+export type EnableNew = (location: Location, currentItemId: string, currentUserUUID: string | undefined, resources: ResourcesState) => boolean;
 
 export interface PluginConfig {
     // Customize the list of possible center panels by adding or removing Route components.
-    centerPanelList: RouteListReducer[];
+    centerPanelList: ElementListReducer[];
 
     // Customize the list of side panel categories
     sidePanelCategories: CategoriesListReducer[];
@@ -32,4 +35,11 @@ export interface PluginConfig {
     appBarMiddle?: React.ReactElement;
 
     appBarRight?: React.ReactElement;
+
+    // Customize the list menu items in the account menu
+    accountMenuList: ElementListReducer[];
+
+    enableNewButtonMatchers: EnableNew[];
+
+    newButtonMenuList: ElementListReducer[];
 }
diff --git a/src/plugins.tsx b/src/plugins.tsx
index 83593f23..3a58a8c2 100644
--- a/src/plugins.tsx
+++ b/src/plugins.tsx
@@ -13,6 +13,9 @@ export const pluginConfig: PluginConfig = {
     appBarLeft: undefined,
     appBarMiddle: undefined,
     appBarRight: undefined,
+    accountMenuList: [],
+    enableNewButtonMatchers: [],
+    newButtonMenuList: []
 };
 
 // Starting here, import and register your Workbench 2 plugins. //
diff --git a/src/plugins/blank/index.tsx b/src/plugins/blank/index.tsx
index 9471372d..0074c02a 100644
--- a/src/plugins/blank/index.tsx
+++ b/src/plugins/blank/index.tsx
@@ -13,6 +13,9 @@ export const register = (pluginConfig: PluginConfig) => {
 
     pluginConfig.sidePanelCategories.push((cats: string[]): string[] => []);
 
+    pluginConfig.accountMenuList.push((elms) => []);
+    pluginConfig.newButtonMenuList.push((elms) => []);
+
     pluginConfig.appBarLeft = <span />;
     pluginConfig.appBarMiddle = <span />;
     pluginConfig.appBarRight = <span />;
diff --git a/src/plugins/example/index.tsx b/src/plugins/example/index.tsx
index 4fa98966..b8bfcb0f 100644
--- a/src/plugins/example/index.tsx
+++ b/src/plugins/example/index.tsx
@@ -14,16 +14,36 @@ import { Route, matchPath } from "react-router";
 import { RootStore } from '~/store/store';
 import { activateSidePanelTreeItem } from '~/store/side-panel-tree/side-panel-tree-actions';
 import { setSidePanelBreadcrumbs } from '~/store/breadcrumbs/breadcrumbs-actions';
+import { DispatchProp, connect } from 'react-redux';
+import { MenuItem } from "@material-ui/core";
+import { propertiesActions } from '~/store/properties/properties-actions';
+import { Location } from 'history';
 
 const categoryName = "Plugin Example";
 export const routePath = "/examplePlugin";
+const propertyKey = "Example_menu_item_pressed_count";
 
-const ExamplePluginMainPanel = (props: {}) => {
-    return <Typography>
-        This is a example main panel plugin.
-    </Typography>;
+interface ExampleProps {
+    pressedCount: number;
+}
+
+const exampleMapStateToProps = (state: RootState) => ({ pressedCount: state.properties[propertyKey] || 0 });
+
+const incrementPressedCount = (dispatch: Dispatch, pressedCount: number) => {
+    dispatch(propertiesActions.SET_PROPERTY({ key: propertyKey, value: pressedCount + 1 }));
 };
 
+const ExampleMenuComponent = connect(exampleMapStateToProps)(
+    ({ pressedCount, dispatch }: ExampleProps & DispatchProp<any>) =>
+        <MenuItem onClick={() => incrementPressedCount(dispatch, pressedCount)}>Example menu item</MenuItem >
+);
+
+const ExamplePluginMainPanel = connect(exampleMapStateToProps)(
+    ({ pressedCount }: ExampleProps) =>
+        <Typography>
+            This is a example main panel plugin.  The example menu item has been pressed {pressedCount} times.
+	</Typography>);
+
 export const register = (pluginConfig: PluginConfig) => {
 
     pluginConfig.centerPanelList.push((elms) => {
@@ -31,6 +51,16 @@ export const register = (pluginConfig: PluginConfig) => {
         return elms;
     });
 
+    pluginConfig.accountMenuList.push((elms) => {
+        elms.push(<ExampleMenuComponent />);
+        return elms;
+    });
+
+    pluginConfig.newButtonMenuList.push((elms) => {
+        elms.push(<ExampleMenuComponent />);
+        return elms;
+    });
+
     pluginConfig.navigateToHandlers.push((dispatch: Dispatch, getState: () => RootState, uuid: string) => {
         if (uuid === categoryName) {
             dispatch(push(routePath));
@@ -49,4 +79,6 @@ export const register = (pluginConfig: PluginConfig) => {
         }
         return false;
     });
+
+    pluginConfig.enableNewButtonMatchers.push((location: Location) => (!!matchPath(location.pathname, { path: routePath, exact: true })));
 };
diff --git a/src/views-components/main-app-bar/account-menu.tsx b/src/views-components/main-app-bar/account-menu.tsx
index ea3a2dd9..7892b8a7 100644
--- a/src/views-components/main-app-bar/account-menu.tsx
+++ b/src/views-components/main-app-bar/account-menu.tsx
@@ -20,6 +20,8 @@ import {
     navigateToLinkAccount
 } from '~/store/navigation/navigation-action';
 import { openUserVirtualMachines } from "~/store/virtual-machines/virtual-machines-actions";
+import { pluginConfig } from '~/plugins';
+import { ElementListReducer } from '~/common/plugintypes';
 
 interface AccountMenuProps {
     user?: User;
@@ -57,38 +59,47 @@ const styles: StyleRulesCallback<CssRules> = () => ({
 });
 
 export const AccountMenuComponent =
-    ({ user, dispatch, currentRoute, workbenchURL, apiToken, localCluster, classes }: AccountMenuProps & DispatchProp<any> & WithStyles<CssRules>) =>
-        user
-        ? <DropdownMenu
-            icon={<UserPanelIcon />}
-            id="account-menu"
-            title="Account Management"
-            key={currentRoute}>
-            <MenuItem disabled>
-                {getUserDisplayName(user)} {user.uuid.substr(0, 5) !== localCluster && `(${user.uuid.substr(0, 5)})`}
-            </MenuItem>
-            {user.isActive ? <>
-                <MenuItem onClick={() => dispatch(openUserVirtualMachines())}>Virtual Machines</MenuItem>
-                {!user.isAdmin && <MenuItem onClick={() => dispatch(openRepositoriesPanel())}>Repositories</MenuItem>}
-                <MenuItem onClick={() => {
-                    dispatch<any>(getNewExtraToken(true));
-                    dispatch(openTokenDialog);
-                }}>Get API token</MenuItem>
-                <MenuItem onClick={() => dispatch(navigateToSshKeysUser)}>Ssh Keys</MenuItem>
-                <MenuItem onClick={() => dispatch(navigateToSiteManager)}>Site Manager</MenuItem>
-                <MenuItem onClick={() => dispatch(navigateToMyAccount)}>My account</MenuItem>
-                <MenuItem onClick={() => dispatch(navigateToLinkAccount)}>Link account</MenuItem>
-            </> : null}
+    ({ user, dispatch, currentRoute, workbenchURL, apiToken, localCluster, classes }: AccountMenuProps & DispatchProp<any> & WithStyles<CssRules>) => {
+        let accountMenuItems = <>
+            <MenuItem onClick={() => dispatch(openUserVirtualMachines())}>Virtual Machines</MenuItem>
+            <MenuItem onClick={() => dispatch(openRepositoriesPanel())}>Repositories</MenuItem>
+            <MenuItem onClick={() => {
+                dispatch<any>(getNewExtraToken(true));
+                dispatch(openTokenDialog);
+            }}>Get API token</MenuItem>
+            <MenuItem onClick={() => dispatch(navigateToSshKeysUser)}>Ssh Keys</MenuItem>
+            <MenuItem onClick={() => dispatch(navigateToSiteManager)}>Site Manager</MenuItem>
+            <MenuItem onClick={() => dispatch(navigateToMyAccount)}>My account</MenuItem>
+            <MenuItem onClick={() => dispatch(navigateToLinkAccount)}>Link account</MenuItem>
             <MenuItem>
                 <a href={`${workbenchURL.replace(/\/$/, "")}/${wb1URL(currentRoute)}?api_token=${apiToken}`}
                     className={classes.link}>
                     Switch to Workbench v1</a></MenuItem>
-            <Divider />
-            <MenuItem data-cy="logout-menuitem"
-                onClick={() => dispatch(authActions.LOGOUT({deleteLinkData: true}))}>
-                Logout
-            </MenuItem>
-        </DropdownMenu>
-        : null;
+        </>;
 
-export const AccountMenu = withStyles(styles)( connect(mapStateToProps)(AccountMenuComponent) );
+        const reduceItemsFn: (a: React.ReactElement[],
+            b: ElementListReducer) => React.ReactElement[] = (a, b) => b(a);
+
+        accountMenuItems = React.createElement(React.Fragment, null,
+            pluginConfig.accountMenuList.reduce(reduceItemsFn, React.Children.toArray(accountMenuItems.props.children)));
+
+        return user
+            ? <DropdownMenu
+                icon={<UserPanelIcon />}
+                id="account-menu"
+                title="Account Management"
+                key={currentRoute}>
+                <MenuItem disabled>
+                    {getUserDisplayName(user)} {user.uuid.substr(0, 5) !== localCluster && `(${user.uuid.substr(0, 5)})`}
+                </MenuItem>
+                {user.isActive && accountMenuItems}
+                <Divider />
+                <MenuItem data-cy="logout-menuitem"
+                    onClick={() => dispatch(authActions.LOGOUT({ deleteLinkData: true }))}>
+                    Logout
+		 </MenuItem>
+            </DropdownMenu>
+            : null;
+    };
+
+export const AccountMenu = withStyles(styles)(connect(mapStateToProps)(AccountMenuComponent));
diff --git a/src/views-components/main-app-bar/main-app-bar.tsx b/src/views-components/main-app-bar/main-app-bar.tsx
index 7bec7b24..44cbe20d 100644
--- a/src/views-components/main-app-bar/main-app-bar.tsx
+++ b/src/views-components/main-app-bar/main-app-bar.tsx
@@ -47,7 +47,7 @@ export const MainAppBar = withStyles(styles)(
                         <Typography variant='h6' color="inherit" noWrap>
                             <Link to={Routes.ROOT} className={props.classes.link}>
                                 <span dangerouslySetInnerHTML={{ __html: props.siteBanner }} /> ({props.uuidPrefix})
-                            </Link>
+                </Link>
                         </Typography>
                         <Typography variant="caption" color="inherit">{props.buildInfo}</Typography>
                     </Grid>}
@@ -65,14 +65,17 @@ export const MainAppBar = withStyles(styles)(
                         alignItems="center"
                         justify="flex-end"
                         wrap="nowrap">
-                        {pluginConfig.appBarRight ||
-                            (props.user ? <>
-                                <NotificationsMenu />
-                                <AccountMenu />
-                                {props.user.isAdmin && <AdminMenu />}
-                                <HelpMenu />
-                            </> :
-                                <HelpMenu />)}
+                        {props.user ? <>
+                            <NotificationsMenu />
+                            <AccountMenu />
+                            {pluginConfig.appBarRight ||
+                                <>
+                                    {props.user.isAdmin && <AdminMenu />}
+                                    <HelpMenu />
+                                </>}
+                        </> :
+                            pluginConfig.appBarRight || <HelpMenu />
+                        }
                     </Grid>
                 </Grid>
             </Toolbar>
diff --git a/src/views-components/side-panel-button/side-panel-button.tsx b/src/views-components/side-panel-button/side-panel-button.tsx
index 3ca2f0d6..4c25bcfe 100644
--- a/src/views-components/side-panel-button/side-panel-button.tsx
+++ b/src/views-components/side-panel-button/side-panel-button.tsx
@@ -18,6 +18,9 @@ import { matchProjectRoute } from '~/routes/routes';
 import { GroupResource } from '~/models/group';
 import { ResourcesState, getResource } from '~/store/resources/resources';
 import { extractUuidKind, ResourceKind } from '~/models/resource';
+import { pluginConfig } from '~/plugins';
+import { ElementListReducer } from '~/common/plugintypes';
+import { Location } from 'history';
 
 type CssRules = 'button' | 'menuItem' | 'icon';
 
@@ -37,7 +40,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 });
 
 interface SidePanelDataProps {
-    location: any;
+    location: Location;
     currentItemId: string;
     resources: ResourcesState;
     currentUserUUID: string | undefined;
@@ -91,6 +94,31 @@ export const SidePanelButton = withStyles(styles)(
                         enabled = true;
                     }
                 }
+
+                for (const enableFn of pluginConfig.enableNewButtonMatchers) {
+                    if (enableFn(location, currentItemId, currentUserUUID, resources)) {
+                        enabled = true;
+                    }
+                }
+
+                let menuItems = <>
+                    <MenuItem data-cy='side-panel-new-collection' className={classes.menuItem} onClick={this.handleNewCollectionClick}>
+                        <CollectionIcon className={classes.icon} /> New collection
+                    </MenuItem>
+                    <MenuItem data-cy='side-panel-run-process' className={classes.menuItem} onClick={this.handleRunProcessClick}>
+                        <ProcessIcon className={classes.icon} /> Run a process
+                    </MenuItem>
+                    <MenuItem data-cy='side-panel-new-project' className={classes.menuItem} onClick={this.handleNewProjectClick}>
+                        <ProjectIcon className={classes.icon} /> New project
+                    </MenuItem>
+                </>;
+
+                const reduceItemsFn: (a: React.ReactElement[],
+                    b: ElementListReducer) => React.ReactElement[] = (a, b) => b(a);
+
+                menuItems = React.createElement(React.Fragment, null,
+                    pluginConfig.newButtonMenuList.reduce(reduceItemsFn, React.Children.toArray(menuItems.props.children)));
+
                 return <Toolbar>
                     <Grid container>
                         <Grid container item xs alignItems="center" justify="flex-start">
@@ -109,15 +137,7 @@ export const SidePanelButton = withStyles(styles)(
                                 onClose={this.handleClose}
                                 onClick={this.handleClose}
                                 transformOrigin={transformOrigin}>
-                                <MenuItem data-cy='side-panel-new-collection' className={classes.menuItem} onClick={this.handleNewCollectionClick}>
-                                    <CollectionIcon className={classes.icon} /> New collection
-                                </MenuItem>
-                                <MenuItem data-cy='side-panel-run-process' className={classes.menuItem} onClick={this.handleRunProcessClick}>
-                                    <ProcessIcon className={classes.icon} /> Run a process
-                                </MenuItem>
-                                <MenuItem data-cy='side-panel-new-project' className={classes.menuItem} onClick={this.handleNewProjectClick}>
-                                    <ProjectIcon className={classes.icon} /> New project
-                                </MenuItem>
+                                {menuItems}
                             </Menu>
                         </Grid>
                     </Grid>
@@ -150,4 +170,4 @@ export const SidePanelButton = withStyles(styles)(
             }
         }
     )
-);
\ No newline at end of file
+);
diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx
index 113cbd67..78ec3c87 100644
--- a/src/views/workbench/workbench.tsx
+++ b/src/views/workbench/workbench.tsx
@@ -102,7 +102,7 @@ import { AutoLogout } from '~/views-components/auto-logout/auto-logout';
 import { RestoreCollectionVersionDialog } from '~/views-components/collections-dialog/restore-version-dialog';
 import { WebDavS3InfoDialog } from '~/views-components/webdav-s3-dialog/webdav-s3-dialog';
 import { pluginConfig } from '~/plugins';
-import { RouteListReducer } from '~/common/plugintypes';
+import { ElementListReducer } from '~/common/plugintypes';
 
 type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content';
 
@@ -183,7 +183,7 @@ let routes = <>
 </>;
 
 const reduceRoutesFn: (a: React.ReactElement[],
-    b: RouteListReducer) => React.ReactElement[] = (a, b) => b(a);
+    b: ElementListReducer) => React.ReactElement[] = (a, b) => b(a);
 
 routes = React.createElement(React.Fragment, null, pluginConfig.centerPanelList.reduce(reduceRoutesFn, React.Children.toArray(routes.props.children)));
 

commit aa2b5b7846134d1efbff6ed68ce3304316336061
Author: Peter Amstutz <peter.amstutz at curii.com>
Date:   Fri Feb 26 23:43:35 2021 -0500

    17426: Add plugin ability to override app bar.
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>

diff --git a/src/common/plugintypes.ts b/src/common/plugintypes.ts
index 489568ea..dfbe7c45 100644
--- a/src/common/plugintypes.ts
+++ b/src/common/plugintypes.ts
@@ -26,4 +26,10 @@ export interface PluginConfig {
 
     // Add handlers for navigation actions
     locationChangeHandlers: LocationChangeMatcher[];
+
+    appBarLeft?: React.ReactElement;
+
+    appBarMiddle?: React.ReactElement;
+
+    appBarRight?: React.ReactElement;
 }
diff --git a/src/plugins.tsx b/src/plugins.tsx
index fb52aade..83593f23 100644
--- a/src/plugins.tsx
+++ b/src/plugins.tsx
@@ -9,12 +9,15 @@ export const pluginConfig: PluginConfig = {
     sidePanelCategories: [],
     dialogs: [],
     navigateToHandlers: [],
-    locationChangeHandlers: []
+    locationChangeHandlers: [],
+    appBarLeft: undefined,
+    appBarMiddle: undefined,
+    appBarRight: undefined,
 };
 
 // Starting here, import and register your Workbench 2 plugins. //
 
-// import { register as blankUIPluginRegister } from '~/plugins/blank/index';
+import { register as blankUIPluginRegister } from '~/plugins/blank/index';
 import { register as examplePluginRegister, routePath as exampleRoutePath } from '~/plugins/example/index';
 import { register as rootRedirectRegister } from '~/plugins/root-redirect/index';
 
diff --git a/src/plugins/blank/index.tsx b/src/plugins/blank/index.tsx
index 416de42d..9471372d 100644
--- a/src/plugins/blank/index.tsx
+++ b/src/plugins/blank/index.tsx
@@ -5,10 +5,15 @@
 // Example plugin.
 
 import { PluginConfig } from '~/common/plugintypes';
+import * as React from 'react';
 
 export const register = (pluginConfig: PluginConfig) => {
 
     pluginConfig.centerPanelList.push((elms) => []);
 
     pluginConfig.sidePanelCategories.push((cats: string[]): string[] => []);
+
+    pluginConfig.appBarLeft = <span />;
+    pluginConfig.appBarMiddle = <span />;
+    pluginConfig.appBarRight = <span />;
 };
diff --git a/src/views-components/main-app-bar/main-app-bar.tsx b/src/views-components/main-app-bar/main-app-bar.tsx
index ce1cab4c..7bec7b24 100644
--- a/src/views-components/main-app-bar/main-app-bar.tsx
+++ b/src/views-components/main-app-bar/main-app-bar.tsx
@@ -14,6 +14,7 @@ import { AccountMenu } from "~/views-components/main-app-bar/account-menu";
 import { HelpMenu } from '~/views-components/main-app-bar/help-menu';
 import { ReactNode } from "react";
 import { AdminMenu } from "~/views-components/main-app-bar/admin-menu";
+import { pluginConfig } from '~/plugins';
 
 type CssRules = 'toolbar' | 'link';
 
@@ -42,20 +43,20 @@ export const MainAppBar = withStyles(styles)(
         return <AppBar position="absolute">
             <Toolbar className={props.classes.toolbar}>
                 <Grid container justify="space-between">
-                    <Grid container item xs={3} direction="column" justify="center">
+                    {pluginConfig.appBarLeft || <Grid container item xs={3} direction="column" justify="center">
                         <Typography variant='h6' color="inherit" noWrap>
                             <Link to={Routes.ROOT} className={props.classes.link}>
                                 <span dangerouslySetInnerHTML={{ __html: props.siteBanner }} /> ({props.uuidPrefix})
                             </Link>
                         </Typography>
                         <Typography variant="caption" color="inherit">{props.buildInfo}</Typography>
-                    </Grid>
+                    </Grid>}
                     <Grid
                         item
                         xs={6}
                         container
                         alignItems="center">
-                        {props.user && props.user.isActive && <SearchBar />}
+                        {pluginConfig.appBarMiddle || (props.user && props.user.isActive && <SearchBar />)}
                     </Grid>
                     <Grid
                         item
@@ -64,14 +65,14 @@ export const MainAppBar = withStyles(styles)(
                         alignItems="center"
                         justify="flex-end"
                         wrap="nowrap">
-                        {props.user
-                            ? <>
+                        {pluginConfig.appBarRight ||
+                            (props.user ? <>
                                 <NotificationsMenu />
                                 <AccountMenu />
                                 {props.user.isAdmin && <AdminMenu />}
                                 <HelpMenu />
-                            </>
-                            : <HelpMenu />}
+                            </> :
+                                <HelpMenu />)}
                     </Grid>
                 </Grid>
             </Toolbar>
diff --git a/src/views-components/main-content-bar/main-content-bar.tsx b/src/views-components/main-content-bar/main-content-bar.tsx
index cad73a3a..60adab66 100644
--- a/src/views-components/main-content-bar/main-content-bar.tsx
+++ b/src/views-components/main-content-bar/main-content-bar.tsx
@@ -30,13 +30,27 @@ interface MainContentBarProps {
 
 const isButtonVisible = ({ router }: RootState) => {
     const pathname = router.location ? router.location.pathname : '';
-    return !Routes.matchWorkflowRoute(pathname) && !Routes.matchUserVirtualMachineRoute(pathname) &&
-        !Routes.matchAdminVirtualMachineRoute(pathname) && !Routes.matchRepositoriesRoute(pathname) &&
-        !Routes.matchSshKeysAdminRoute(pathname) && !Routes.matchSshKeysUserRoute(pathname) &&
-        !Routes.matchSiteManagerRoute(pathname) &&
-        !Routes.matchKeepServicesRoute(pathname) && !Routes.matchComputeNodesRoute(pathname) &&
-        !Routes.matchApiClientAuthorizationsRoute(pathname) && !Routes.matchUsersRoute(pathname) &&
-        !Routes.matchMyAccountRoute(pathname) && !Routes.matchLinksRoute(pathname);
+    return Routes.matchCollectionsContentAddressRoute(pathname) ||
+        Routes.matchPublicFavoritesRoute(pathname) ||
+        Routes.matchGroupDetailsRoute(pathname) ||
+        Routes.matchGroupsRoute(pathname) ||
+        Routes.matchUsersRoute(pathname) ||
+        Routes.matchSearchResultsRoute(pathname) ||
+        Routes.matchSharedWithMeRoute(pathname) ||
+        Routes.matchProcessRoute(pathname) ||
+        Routes.matchCollectionRoute(pathname) ||
+        Routes.matchProjectRoute(pathname) ||
+        Routes.matchAllProcessesRoute(pathname) ||
+        Routes.matchTrashRoute(pathname) ||
+        Routes.matchFavoritesRoute(pathname);
+
+    /* return !Routes.matchWorkflowRoute(pathname) && !Routes.matchUserVirtualMachineRoute(pathname) &&
+     *     !Routes.matchAdminVirtualMachineRoute(pathname) && !Routes.matchRepositoriesRoute(pathname) &&
+     *     !Routes.matchSshKeysAdminRoute(pathname) && !Routes.matchSshKeysUserRoute(pathname) &&
+     *     !Routes.matchSiteManagerRoute(pathname) &&
+     *     !Routes.matchKeepServicesRoute(pathname) && !Routes.matchComputeNodesRoute(pathname) &&
+     *     !Routes.matchApiClientAuthorizationsRoute(pathname) && !Routes.matchUsersRoute(pathname) &&
+     *     !Routes.matchMyAccountRoute(pathname) && !Routes.matchLinksRoute(pathname); */
 };
 
 export const MainContentBar =

commit 34b3e1b12b9d20a09764f86ec138d2a83c82297b
Author: Peter Amstutz <peter.amstutz at curii.com>
Date:   Fri Feb 26 17:29:55 2021 -0500

    17426: Plugins can replace some of main UI
    
    Add example and utility plugins.
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>

diff --git a/Makefile b/Makefile
index 6da3ed10..1e135a7f 100644
--- a/Makefile
+++ b/Makefile
@@ -23,7 +23,7 @@ DESCRIPTION=Arvados Workbench2 - Arvados is a free and open source platform for
 MAINTAINER=Arvados Package Maintainers <packaging at arvados.org>
 
 # DEST_DIR will have the build package copied.
-DEST_DIR=/var/www/arvados-workbench2/workbench2/
+DEST_DIR=/var/www/$(APP_NAME)/workbench2/
 
 # Debian package file
 DEB_FILE=$(APP_NAME)_$(VERSION)-$(ITERATION)_amd64.deb
diff --git a/src/common/plugintypes.ts b/src/common/plugintypes.ts
new file mode 100644
index 00000000..489568ea
--- /dev/null
+++ b/src/common/plugintypes.ts
@@ -0,0 +1,29 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { Dispatch } from 'redux';
+import { RootStore, RootState } from '~/store/store';
+
+export type RouteListReducer = (startingList: React.ReactElement[]) => React.ReactElement[];
+export type CategoriesListReducer = (startingList: string[]) => string[];
+export type NavigateMatcher = (dispatch: Dispatch, getState: () => RootState, uuid: string) => boolean;
+export type LocationChangeMatcher = (store: RootStore, pathname: string) => boolean;
+
+export interface PluginConfig {
+    // Customize the list of possible center panels by adding or removing Route components.
+    centerPanelList: RouteListReducer[];
+
+    // Customize the list of side panel categories
+    sidePanelCategories: CategoriesListReducer[];
+
+    // Add to the list of possible dialogs by adding dialog components.
+    dialogs: React.ReactElement[];
+
+    // Add navigation actions for identifiers
+    navigateToHandlers: NavigateMatcher[];
+
+    // Add handlers for navigation actions
+    locationChangeHandlers: LocationChangeMatcher[];
+}
diff --git a/src/plugins.tsx b/src/plugins.tsx
new file mode 100644
index 00000000..fb52aade
--- /dev/null
+++ b/src/plugins.tsx
@@ -0,0 +1,23 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { PluginConfig } from '~/common/plugintypes';
+
+export const pluginConfig: PluginConfig = {
+    centerPanelList: [],
+    sidePanelCategories: [],
+    dialogs: [],
+    navigateToHandlers: [],
+    locationChangeHandlers: []
+};
+
+// Starting here, import and register your Workbench 2 plugins. //
+
+// import { register as blankUIPluginRegister } from '~/plugins/blank/index';
+import { register as examplePluginRegister, routePath as exampleRoutePath } from '~/plugins/example/index';
+import { register as rootRedirectRegister } from '~/plugins/root-redirect/index';
+
+blankUIPluginRegister(pluginConfig);
+examplePluginRegister(pluginConfig);
+rootRedirectRegister(pluginConfig, exampleRoutePath);
diff --git a/src/plugins/blank/index.tsx b/src/plugins/blank/index.tsx
new file mode 100644
index 00000000..416de42d
--- /dev/null
+++ b/src/plugins/blank/index.tsx
@@ -0,0 +1,14 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+// Example plugin.
+
+import { PluginConfig } from '~/common/plugintypes';
+
+export const register = (pluginConfig: PluginConfig) => {
+
+    pluginConfig.centerPanelList.push((elms) => []);
+
+    pluginConfig.sidePanelCategories.push((cats: string[]): string[] => []);
+};
diff --git a/src/plugins/example/index.tsx b/src/plugins/example/index.tsx
new file mode 100644
index 00000000..4fa98966
--- /dev/null
+++ b/src/plugins/example/index.tsx
@@ -0,0 +1,52 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+// Example plugin.
+
+import { PluginConfig } from '~/common/plugintypes';
+import * as React from 'react';
+import { Dispatch } from 'redux';
+import { RootState } from '~/store/store';
+import { push } from "react-router-redux";
+import { Typography } from "@material-ui/core";
+import { Route, matchPath } from "react-router";
+import { RootStore } from '~/store/store';
+import { activateSidePanelTreeItem } from '~/store/side-panel-tree/side-panel-tree-actions';
+import { setSidePanelBreadcrumbs } from '~/store/breadcrumbs/breadcrumbs-actions';
+
+const categoryName = "Plugin Example";
+export const routePath = "/examplePlugin";
+
+const ExamplePluginMainPanel = (props: {}) => {
+    return <Typography>
+        This is a example main panel plugin.
+    </Typography>;
+};
+
+export const register = (pluginConfig: PluginConfig) => {
+
+    pluginConfig.centerPanelList.push((elms) => {
+        elms.push(<Route path={routePath} component={ExamplePluginMainPanel} />);
+        return elms;
+    });
+
+    pluginConfig.navigateToHandlers.push((dispatch: Dispatch, getState: () => RootState, uuid: string) => {
+        if (uuid === categoryName) {
+            dispatch(push(routePath));
+            return true;
+        }
+        return false;
+    });
+
+    pluginConfig.sidePanelCategories.push((cats: string[]): string[] => { cats.push(categoryName); return cats; });
+
+    pluginConfig.locationChangeHandlers.push((store: RootStore, pathname: string): boolean => {
+        if (matchPath(pathname, { path: routePath, exact: true })) {
+            store.dispatch(activateSidePanelTreeItem(categoryName));
+            store.dispatch<any>(setSidePanelBreadcrumbs(categoryName));
+            return true;
+        }
+        return false;
+    });
+};
diff --git a/src/plugins/root-redirect/index.tsx b/src/plugins/root-redirect/index.tsx
new file mode 100644
index 00000000..13eeb942
--- /dev/null
+++ b/src/plugins/root-redirect/index.tsx
@@ -0,0 +1,20 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { PluginConfig } from '~/common/plugintypes';
+import { Dispatch } from 'redux';
+import { RootState } from '~/store/store';
+import { SidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
+import { push } from "react-router-redux";
+
+export const register = (pluginConfig: PluginConfig, redirect: string) => {
+
+    pluginConfig.navigateToHandlers.push((dispatch: Dispatch, getState: () => RootState, uuid: string) => {
+        if (uuid === SidePanelTreeCategory.PROJECTS) {
+            dispatch(push(redirect));
+            return true;
+        }
+        return false;
+    });
+};
diff --git a/src/routes/route-change-handlers.ts b/src/routes/route-change-handlers.ts
index 400ddc88..8a66e420 100644
--- a/src/routes/route-change-handlers.ts
+++ b/src/routes/route-change-handlers.ts
@@ -10,6 +10,7 @@ import { navigateToRootProject } from '~/store/navigation/navigation-action';
 import { dialogActions } from '~/store/dialog/dialog-actions';
 import { contextMenuActions } from '~/store/context-menu/context-menu-actions';
 import { searchBarActions } from '~/store/search-bar/search-bar-actions';
+import { pluginConfig } from '~/plugins';
 
 export const addRouteChangeHandlers = (history: History, store: RootStore) => {
     const handler = handleLocationChange(store);
@@ -53,6 +54,12 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
     store.dispatch(contextMenuActions.CLOSE_CONTEXT_MENU());
     store.dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
 
+    for (const locChangeFn of pluginConfig.locationChangeHandlers) {
+        if (locChangeFn(store, pathname)) {
+            return;
+        }
+    }
+
     if (projectMatch) {
         store.dispatch(WorkbenchActions.loadProject(projectMatch.params.id));
     } else if (collectionMatch) {
@@ -112,4 +119,4 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
     } else if (allProcessesMatch) {
         store.dispatch(WorkbenchActions.loadAllProcesses());
     }
-};
\ No newline at end of file
+};
diff --git a/src/store/breadcrumbs/breadcrumbs-actions.ts b/src/store/breadcrumbs/breadcrumbs-actions.ts
index 8803cfba..b2857b69 100644
--- a/src/store/breadcrumbs/breadcrumbs-actions.ts
+++ b/src/store/breadcrumbs/breadcrumbs-actions.ts
@@ -65,7 +65,7 @@ export const setSharedWithMeBreadcrumbs = (uuid: string) =>
 export const setTrashBreadcrumbs = (uuid: string) =>
     setCategoryBreadcrumbs(uuid, SidePanelTreeCategory.TRASH);
 
-export const setCategoryBreadcrumbs = (uuid: string, category: SidePanelTreeCategory) =>
+export const setCategoryBreadcrumbs = (uuid: string, category: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const ancestors = await services.ancestorsService.ancestors(uuid, '');
         dispatch(updateResources(ancestors));
diff --git a/src/store/navigation/navigation-action.ts b/src/store/navigation/navigation-action.ts
index d663ae37..7b55f897 100644
--- a/src/store/navigation/navigation-action.ts
+++ b/src/store/navigation/navigation-action.ts
@@ -10,9 +10,25 @@ import { Routes, getProcessLogUrl, getGroupUrl, getNavUrl } from '~/routes/route
 import { RootState } from '~/store/store';
 import { ServiceRepository } from '~/services/services';
 import { GROUPS_PANEL_LABEL } from '~/store/breadcrumbs/breadcrumbs-actions';
+import { pluginConfig } from '~/plugins';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
+
+const navigationNotAvailable = (id: string) =>
+    snackbarActions.OPEN_SNACKBAR({
+        message: `${id} not available`,
+        hideDuration: 3000,
+        kind: SnackbarKind.ERROR
+    });
 
 export const navigateTo = (uuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState) => {
+
+        for (const navToFn of pluginConfig.navigateToHandlers) {
+            if (navToFn(dispatch, getState, uuid)) {
+                return;
+            }
+        }
+
         const kind = extractUuidKind(uuid);
         switch (kind) {
             case ResourceKind.PROJECT:
@@ -27,6 +43,12 @@ export const navigateTo = (uuid: string) =>
         }
 
         switch (uuid) {
+            case SidePanelTreeCategory.PROJECTS:
+                const usr = getState().auth.user;
+                if (usr) {
+                    dispatch<any>(pushOrGoto(getNavUrl(usr.uuid, getState().auth)));
+                }
+                return;
             case SidePanelTreeCategory.FAVORITES:
                 dispatch<any>(navigateToFavorites);
                 return;
@@ -49,8 +71,11 @@ export const navigateTo = (uuid: string) =>
                 dispatch(navigateToAllProcesses);
                 return;
         }
+
+        dispatch(navigationNotAvailable(uuid));
     };
 
+
 export const navigateToNotFound = push(Routes.NO_MATCH);
 
 export const navigateToRoot = push(Routes.ROOT);
@@ -78,10 +103,7 @@ export const pushOrGoto = (url: string): AnyAction => {
 export const navigateToProcessLogs = compose(push, getProcessLogUrl);
 
 export const navigateToRootProject = (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-    const usr = getState().auth.user;
-    if (usr) {
-        dispatch<any>(navigateTo(usr.uuid));
-    }
+    navigateTo(SidePanelTreeCategory.PROJECTS)(dispatch, getState);
 };
 
 export const navigateToSharedWithMe = push(Routes.SHARED_WITH_ME);
diff --git a/src/store/side-panel-tree/side-panel-tree-actions.ts b/src/store/side-panel-tree/side-panel-tree-actions.ts
index d0043da2..dd0f5e68 100644
--- a/src/store/side-panel-tree/side-panel-tree-actions.ts
+++ b/src/store/side-panel-tree/side-panel-tree-actions.ts
@@ -16,6 +16,8 @@ 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';
 
 export enum SidePanelTreeCategory {
     PROJECTS = 'Projects',
@@ -44,19 +46,24 @@ export const getSidePanelTreeBranch = (uuid: string) => (treePicker: TreePicker)
     return [];
 };
 
-const SIDE_PANEL_CATEGORIES: string[] = [
+let SIDE_PANEL_CATEGORIES: string[] = [
     SidePanelTreeCategory.PROJECTS,
     SidePanelTreeCategory.SHARED_WITH_ME,
     SidePanelTreeCategory.PUBLIC_FAVORITES,
     SidePanelTreeCategory.FAVORITES,
     SidePanelTreeCategory.WORKFLOWS,
     SidePanelTreeCategory.ALL_PROCESSES,
-    SidePanelTreeCategory.TRASH,
-    "Blibber blubber"
+    SidePanelTreeCategory.TRASH
 ];
 
+const reduceCatsFn: (a: string[],
+    b: CategoriesListReducer) => string[] = (a, b) => b(a);
+
+SIDE_PANEL_CATEGORIES = pluginConfig.sidePanelCategories.reduce(reduceCatsFn, SIDE_PANEL_CATEGORIES);
+
 export const isSidePanelTreeCategory = (id: string) => SIDE_PANEL_CATEGORIES.some(category => category === id);
 
+
 export const initSidePanelTree = () =>
     (dispatch: Dispatch, getState: () => RootState, { authService }: ServiceRepository) => {
         const rootProjectUuid = getUserUuid(getState());
diff --git a/src/store/side-panel/side-panel-action.ts b/src/store/side-panel/side-panel-action.ts
index 6279aaea..28320f96 100644
--- a/src/store/side-panel/side-panel-action.ts
+++ b/src/store/side-panel/side-panel-action.ts
@@ -3,41 +3,9 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { Dispatch } from 'redux';
-import { isSidePanelTreeCategory, SidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
-import { navigateToFavorites, navigateTo, navigateToTrash, navigateToSharedWithMe, navigateToWorkflows, navigateToPublicFavorites, navigateToAllProcesses } from '~/store/navigation/navigation-action';
-import {snackbarActions, SnackbarKind} from '~/store/snackbar/snackbar-actions';
+import { navigateTo } from '~/store/navigation/navigation-action';
 
 export const navigateFromSidePanel = (id: string) =>
     (dispatch: Dispatch) => {
-        if (isSidePanelTreeCategory(id)) {
-            dispatch<any>(getSidePanelTreeCategoryAction(id));
-        } else {
-            dispatch<any>(navigateTo(id));
-        }
+        dispatch<any>(navigateTo(id));
     };
-
-const getSidePanelTreeCategoryAction = (id: string) => {
-    switch (id) {
-        case SidePanelTreeCategory.FAVORITES:
-            return navigateToFavorites;
-        case SidePanelTreeCategory.PUBLIC_FAVORITES:
-            return navigateToPublicFavorites;
-        case SidePanelTreeCategory.TRASH:
-            return navigateToTrash;
-        case SidePanelTreeCategory.SHARED_WITH_ME:
-            return navigateToSharedWithMe;
-        case SidePanelTreeCategory.WORKFLOWS:
-            return navigateToWorkflows;
-        case SidePanelTreeCategory.ALL_PROCESSES:
-            return navigateToAllProcesses;
-        default:
-            return sidePanelTreeCategoryNotAvailable(id);
-    }
-};
-
-const sidePanelTreeCategoryNotAvailable = (id: string) =>
-    snackbarActions.OPEN_SNACKBAR({
-        message: `${id} not available`,
-        hideDuration: 3000,
-        kind: SnackbarKind.ERROR
-    });
diff --git a/src/store/workbench/workbench-actions.ts b/src/store/workbench/workbench-actions.ts
index 09aad2bd..217c8524 100644
--- a/src/store/workbench/workbench-actions.ts
+++ b/src/store/workbench/workbench-actions.ts
@@ -33,7 +33,7 @@ import {
     setSidePanelBreadcrumbs,
     setTrashBreadcrumbs
 } from '~/store/breadcrumbs/breadcrumbs-actions';
-import { navigateTo } from '~/store/navigation/navigation-action';
+import { navigateTo, navigateToRootProject } from '~/store/navigation/navigation-action';
 import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
 import { ServiceRepository } from '~/services/services';
 import { getResource } from '~/store/resources/resources';
@@ -153,7 +153,7 @@ export const loadWorkbench = () =>
             if (router.location) {
                 const match = matchRootRoute(router.location.pathname);
                 if (match) {
-                    dispatch<any>(navigateTo(user.uuid));
+                    dispatch<any>(navigateToRootProject);
                 }
             }
         } else {
diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx
index b1b071f1..113cbd67 100644
--- a/src/views/workbench/workbench.tsx
+++ b/src/views/workbench/workbench.tsx
@@ -101,7 +101,8 @@ import { NotFoundPanel } from '../not-found-panel/not-found-panel';
 import { AutoLogout } from '~/views-components/auto-logout/auto-logout';
 import { RestoreCollectionVersionDialog } from '~/views-components/collections-dialog/restore-version-dialog';
 import { WebDavS3InfoDialog } from '~/views-components/webdav-s3-dialog/webdav-s3-dialog';
-import { pluginCenterPanelRoutes, RouteReducer, pluginDialogs } from '~/plugins';
+import { pluginConfig } from '~/plugins';
+import { RouteListReducer } from '~/common/plugintypes';
 
 type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content';
 
@@ -182,9 +183,9 @@ let routes = <>
 </>;
 
 const reduceRoutesFn: (a: React.ReactElement[],
-    b: RouteReducer) => React.ReactElement[] = (a, b) => b(a);
+    b: RouteListReducer) => React.ReactElement[] = (a, b) => b(a);
 
-routes = React.createElement(React.Fragment, null, pluginCenterPanelRoutes.reduce(reduceRoutesFn, React.Children.toArray(routes.props.children)));
+routes = React.createElement(React.Fragment, null, pluginConfig.centerPanelList.reduce(reduceRoutesFn, React.Children.toArray(routes.props.children)));
 
 export const WorkbenchPanel =
     withStyles(styles)((props: WorkbenchPanelProps) =>
@@ -274,6 +275,6 @@ export const WorkbenchPanel =
             <VirtualMachineAttributesDialog />
             <FedLogin />
             <WebDavS3InfoDialog />
-            {pluginDialogs}
+            {React.createElement(React.Fragment, null, pluginConfig.dialogs)}
         </Grid>
     );

commit 59513f60899ac4b422e2e866ffc5611bb82e4458
Author: Peter Amstutz <peter.amstutz at curii.com>
Date:   Thu Feb 25 18:04:27 2021 -0500

    17426: WIP adding hooks to add or replace major UI elements
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>

diff --git a/src/index.tsx b/src/index.tsx
index b32066a4..6f4d9dc2 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -119,7 +119,8 @@ fetchConfig()
                                 ? error.errors[0]
                                 : error.message}`,
                             kind: SnackbarKind.ERROR,
-                            hideDuration: 8000})
+                            hideDuration: 8000
+                        })
                         );
                     }
                 }
diff --git a/src/store/side-panel-tree/side-panel-tree-actions.ts b/src/store/side-panel-tree/side-panel-tree-actions.ts
index ff506103..d0043da2 100644
--- a/src/store/side-panel-tree/side-panel-tree-actions.ts
+++ b/src/store/side-panel-tree/side-panel-tree-actions.ts
@@ -44,12 +44,15 @@ export const getSidePanelTreeBranch = (uuid: string) => (treePicker: TreePicker)
     return [];
 };
 
-const SIDE_PANEL_CATEGORIES = [
+const SIDE_PANEL_CATEGORIES: string[] = [
+    SidePanelTreeCategory.PROJECTS,
+    SidePanelTreeCategory.SHARED_WITH_ME,
     SidePanelTreeCategory.PUBLIC_FAVORITES,
     SidePanelTreeCategory.FAVORITES,
     SidePanelTreeCategory.WORKFLOWS,
     SidePanelTreeCategory.ALL_PROCESSES,
     SidePanelTreeCategory.TRASH,
+    "Blibber blubber"
 ];
 
 export const isSidePanelTreeCategory = (id: string) => SIDE_PANEL_CATEGORIES.some(category => category === id);
@@ -58,20 +61,26 @@ export const initSidePanelTree = () =>
     (dispatch: Dispatch, getState: () => RootState, { authService }: ServiceRepository) => {
         const rootProjectUuid = getUserUuid(getState());
         if (!rootProjectUuid) { return; }
-        const nodes = SIDE_PANEL_CATEGORIES.map(id => initTreeNode({ id, value: id }));
-        const projectsNode = initTreeNode({ id: rootProjectUuid, value: SidePanelTreeCategory.PROJECTS });
-        const sharedNode = initTreeNode({ id: SidePanelTreeCategory.SHARED_WITH_ME, value: SidePanelTreeCategory.SHARED_WITH_ME });
+        const nodes = SIDE_PANEL_CATEGORIES.map(id => {
+            if (id === SidePanelTreeCategory.PROJECTS) {
+                return initTreeNode({ id: rootProjectUuid, value: SidePanelTreeCategory.PROJECTS });
+            } else {
+                return initTreeNode({ id, value: id });
+            }
+        });
         dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
             id: '',
             pickerId: SIDE_PANEL_TREE,
-            nodes: [projectsNode, sharedNode, ...nodes]
+            nodes
         }));
         SIDE_PANEL_CATEGORIES.forEach(category => {
-            dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
-                id: category,
-                pickerId: SIDE_PANEL_TREE,
-                nodes: []
-            }));
+            if (category !== SidePanelTreeCategory.PROJECTS && category !== SidePanelTreeCategory.SHARED_WITH_ME) {
+                dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
+                    id: category,
+                    pickerId: SIDE_PANEL_TREE,
+                    nodes: []
+                }));
+            }
         });
     };
 
diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx
index 9c2a7df8..b1b071f1 100644
--- a/src/views/workbench/workbench.tsx
+++ b/src/views/workbench/workbench.tsx
@@ -101,6 +101,7 @@ import { NotFoundPanel } from '../not-found-panel/not-found-panel';
 import { AutoLogout } from '~/views-components/auto-logout/auto-logout';
 import { RestoreCollectionVersionDialog } from '~/views-components/collections-dialog/restore-version-dialog';
 import { WebDavS3InfoDialog } from '~/views-components/webdav-s3-dialog/webdav-s3-dialog';
+import { pluginCenterPanelRoutes, RouteReducer, pluginDialogs } from '~/plugins';
 
 type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content';
 
@@ -149,6 +150,42 @@ const getSplitterInitialSize = () => {
 
 const saveSplitterSize = (size: number) => localStorage.setItem('splitterSize', size.toString());
 
+let routes = <>
+    <Route path={Routes.PROJECTS} component={ProjectPanel} />
+    <Route path={Routes.COLLECTIONS} component={CollectionPanel} />
+    <Route path={Routes.FAVORITES} component={FavoritePanel} />
+    <Route path={Routes.ALL_PROCESSES} component={AllProcessesPanel} />
+    <Route path={Routes.PROCESSES} component={ProcessPanel} />
+    <Route path={Routes.TRASH} component={TrashPanel} />
+    <Route path={Routes.PROCESS_LOGS} component={ProcessLogPanel} />
+    <Route path={Routes.SHARED_WITH_ME} component={SharedWithMePanel} />
+    <Route path={Routes.RUN_PROCESS} component={RunProcessPanel} />
+    <Route path={Routes.WORKFLOWS} component={WorkflowPanel} />
+    <Route path={Routes.SEARCH_RESULTS} component={SearchResultsPanel} />
+    <Route path={Routes.VIRTUAL_MACHINES_USER} component={VirtualMachineUserPanel} />
+    <Route path={Routes.VIRTUAL_MACHINES_ADMIN} component={VirtualMachineAdminPanel} />
+    <Route path={Routes.REPOSITORIES} component={RepositoriesPanel} />
+    <Route path={Routes.SSH_KEYS_USER} component={SshKeyPanel} />
+    <Route path={Routes.SSH_KEYS_ADMIN} component={SshKeyPanel} />
+    <Route path={Routes.SITE_MANAGER} component={SiteManagerPanel} />
+    <Route path={Routes.KEEP_SERVICES} component={KeepServicePanel} />
+    <Route path={Routes.USERS} component={UserPanel} />
+    <Route path={Routes.COMPUTE_NODES} component={ComputeNodePanel} />
+    <Route path={Routes.API_CLIENT_AUTHORIZATIONS} component={ApiClientAuthorizationPanel} />
+    <Route path={Routes.MY_ACCOUNT} component={MyAccountPanel} />
+    <Route path={Routes.GROUPS} component={GroupsPanel} />
+    <Route path={Routes.GROUP_DETAILS} component={GroupDetailsPanel} />
+    <Route path={Routes.LINKS} component={LinkPanel} />
+    <Route path={Routes.PUBLIC_FAVORITES} component={PublicFavoritePanel} />
+    <Route path={Routes.LINK_ACCOUNT} component={LinkAccountPanel} />
+    <Route path={Routes.COLLECTIONS_CONTENT_ADDRESS} component={CollectionsContentAddressPanel} />
+</>;
+
+const reduceRoutesFn: (a: React.ReactElement[],
+    b: RouteReducer) => React.ReactElement[] = (a, b) => b(a);
+
+routes = React.createElement(React.Fragment, null, pluginCenterPanelRoutes.reduce(reduceRoutesFn, React.Children.toArray(routes.props.children)));
+
 export const WorkbenchPanel =
     withStyles(styles)((props: WorkbenchPanelProps) =>
         <Grid container item xs className={props.classes.root}>
@@ -167,34 +204,7 @@ export const WorkbenchPanel =
                         </Grid>
                         <Grid item xs className={props.classes.content}>
                             <Switch>
-                                <Route path={Routes.PROJECTS} component={ProjectPanel} />
-                                <Route path={Routes.COLLECTIONS} component={CollectionPanel} />
-                                <Route path={Routes.FAVORITES} component={FavoritePanel} />
-                                <Route path={Routes.ALL_PROCESSES} component={AllProcessesPanel} />
-                                <Route path={Routes.PROCESSES} component={ProcessPanel} />
-                                <Route path={Routes.TRASH} component={TrashPanel} />
-                                <Route path={Routes.PROCESS_LOGS} component={ProcessLogPanel} />
-                                <Route path={Routes.SHARED_WITH_ME} component={SharedWithMePanel} />
-                                <Route path={Routes.RUN_PROCESS} component={RunProcessPanel} />
-                                <Route path={Routes.WORKFLOWS} component={WorkflowPanel} />
-                                <Route path={Routes.SEARCH_RESULTS} component={SearchResultsPanel} />
-                                <Route path={Routes.VIRTUAL_MACHINES_USER} component={VirtualMachineUserPanel} />
-                                <Route path={Routes.VIRTUAL_MACHINES_ADMIN} component={VirtualMachineAdminPanel} />
-                                <Route path={Routes.REPOSITORIES} component={RepositoriesPanel} />
-                                <Route path={Routes.SSH_KEYS_USER} component={SshKeyPanel} />
-                                <Route path={Routes.SSH_KEYS_ADMIN} component={SshKeyPanel} />
-                                <Route path={Routes.SITE_MANAGER} component={SiteManagerPanel} />
-                                <Route path={Routes.KEEP_SERVICES} component={KeepServicePanel} />
-                                <Route path={Routes.USERS} component={UserPanel} />
-                                <Route path={Routes.COMPUTE_NODES} component={ComputeNodePanel} />
-                                <Route path={Routes.API_CLIENT_AUTHORIZATIONS} component={ApiClientAuthorizationPanel} />
-                                <Route path={Routes.MY_ACCOUNT} component={MyAccountPanel} />
-                                <Route path={Routes.GROUPS} component={GroupsPanel} />
-                                <Route path={Routes.GROUP_DETAILS} component={GroupDetailsPanel} />
-                                <Route path={Routes.LINKS} component={LinkPanel} />
-                                <Route path={Routes.PUBLIC_FAVORITES} component={PublicFavoritePanel} />
-                                <Route path={Routes.LINK_ACCOUNT} component={LinkAccountPanel} />
-                                <Route path={Routes.COLLECTIONS_CONTENT_ADDRESS} component={CollectionsContentAddressPanel} />
+                                {routes}
                                 <Route path={Routes.NO_MATCH} component={NotFoundPanel} />
                             </Switch>
                         </Grid>
@@ -264,5 +274,6 @@ export const WorkbenchPanel =
             <VirtualMachineAttributesDialog />
             <FedLogin />
             <WebDavS3InfoDialog />
+            {pluginDialogs}
         </Grid>
     );

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


hooks/post-receive
-- 




More information about the arvados-commits mailing list