[ARVADOS-WORKBENCH2] updated: 2.1.0-256-g160378cd

Git user git at public.arvados.org
Mon Mar 22 21:04:09 UTC 2021


Summary of changes:
 Makefile                                           |  4 +-
 .../details-attribute/details-attribute.tsx        | 49 +++++++++++++---------
 src/plugins.tsx                                    | 10 ++---
 .../token-dialog/token-dialog.test.tsx             |  4 +-
 src/views-components/token-dialog/token-dialog.tsx | 30 ++++++-------
 tools/run-integration-tests.sh                     | 11 ++++-
 6 files changed, 63 insertions(+), 45 deletions(-)

  discards  47d15e0fbf6a2a11a52085bbbeb472c1ae61a4a9 (commit)
  discards  7e21998fe7cf2fc00d712208b261ec8cf97776db (commit)
  discards  66c735017c5ccb08cde2262c723ef7f821ff7c9d (commit)
  discards  3eb0041fd960c3c2728872640e41181171991cfb (commit)
  discards  aa2b5b7846134d1efbff6ed68ce3304316336061 (commit)
  discards  34b3e1b12b9d20a09764f86ec138d2a83c82297b (commit)
  discards  59513f60899ac4b422e2e866ffc5611bb82e4458 (commit)
       via  160378cd58ef72daacfc5577e3c23ddbc35a5f7f (commit)
       via  381ea9efdee553e5b52282f22add58d4859ab4ef (commit)
       via  fa03027f1954dfdd98c3d3d4f74ea3a016cf57c0 (commit)
       via  dd8c2373f5230831c051711d9e52a3b2defd9dd0 (commit)
       via  e4de9a43cee1a8859cb2a42ea01723d632621ce4 (commit)
       via  950ea822015652a479f236fff9ea8271b60f1ee9 (commit)
       via  623f5deee7b203090fb71f028e01ed55e0f8a38c (commit)
       via  30557563fd5fb704554f4d6109493f673ae7f5e5 (commit)
       via  18e2272d85785c4fec83ae2b2b274ccfcfc2fec5 (commit)
       via  5d70775055baf4fab6ff97fff8daa80aa6711d70 (commit)
       via  4c2c72d649f421f63ba95b7bf3978491f78a6c72 (commit)
       via  630d36f4c40ef2544b2ce1a80eca34cd9aa16ea3 (commit)
       via  56156171fe65695dcec41f540735fd559655a11b (commit)
       via  f21bb210446c4230bef9e5e50b4a9e9b1293bd3b (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 (47d15e0fbf6a2a11a52085bbbeb472c1ae61a4a9)
            \
             N -- N -- N (160378cd58ef72daacfc5577e3c23ddbc35a5f7f)

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 160378cd58ef72daacfc5577e3c23ddbc35a5f7f
Author: Peter Amstutz <peter.amstutz at curii.com>
Date:   Mon Mar 22 16:57:23 2021 -0400

    17426: Comment out plugins
    
    Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz at curii.com>

diff --git a/src/plugins.tsx b/src/plugins.tsx
index 1880e62d..03175494 100644
--- a/src/plugins.tsx
+++ b/src/plugins.tsx
@@ -22,11 +22,11 @@ export const pluginConfig: PluginConfig = {
 // Starting here, import and register your Workbench 2 plugins. //
 
 // import { register as blankUIPluginRegister } from '~/plugins/blank/index';
-import { register as sampleTrackerPluginRegister } from '~/plugins/sample-tracker/index';
-import { studyListRoutePath } from '~/plugins/sample-tracker/studyList';
+// import { register as sampleTrackerPluginRegister } from '~/plugins/sample-tracker/index';
+// import { studyListRoutePath } from '~/plugins/sample-tracker/studyList';
 // import { register as examplePluginRegister, routePath as exampleRoutePath } from '~/plugins/example/index';
-import { register as rootRedirectRegister } from '~/plugins/root-redirect/index';
+// import { register as rootRedirectRegister } from '~/plugins/root-redirect/index';
 
 // blankUIPluginRegister(pluginConfig);
-sampleTrackerPluginRegister(pluginConfig);
-rootRedirectRegister(pluginConfig, studyListRoutePath);
+// sampleTrackerPluginRegister(pluginConfig);
+// rootRedirectRegister(pluginConfig, studyListRoutePath);

commit 381ea9efdee553e5b52282f22add58d4859ab4ef
Author: Peter Amstutz <peter.amstutz at curii.com>
Date:   Thu Mar 11 16:03:23 2021 -0500

    17426: Can add plugin middlewares and context menus
    
    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 00cc1e36..2ce0bb12 100644
--- a/src/common/plugintypes.ts
+++ b/src/common/plugintypes.ts
@@ -3,16 +3,18 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { Dispatch } from 'redux';
+import { Dispatch, Middleware } from 'redux';
 import { RootStore, RootState } from '~/store/store';
 import { ResourcesState } from '~/store/resources/resources';
 import { Location } from 'history';
+import { ServiceRepository } from "~/services/services";
 
 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;
 export type EnableNew = (location: Location, currentItemId: string, currentUserUUID: string | undefined, resources: ResourcesState) => boolean;
+export type MiddlewareListReducer = (startingList: Middleware[], services: ServiceRepository) => Middleware[];
 
 export interface PluginConfig {
     // Customize the list of possible center panels by adding or removing Route components.
@@ -42,4 +44,6 @@ export interface PluginConfig {
     enableNewButtonMatchers: EnableNew[];
 
     newButtonMenuList: ElementListReducer[];
+
+    middlewares: MiddlewareListReducer[];
 }
diff --git a/src/models/link.ts b/src/models/link.ts
index 785d531c..1c82fe58 100644
--- a/src/models/link.ts
+++ b/src/models/link.ts
@@ -2,7 +2,6 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { TagProperty } from "~/models/tag";
 import { Resource, ResourceKind } from '~/models/resource';
 
 export interface LinkResource extends Resource {
@@ -12,7 +11,7 @@ export interface LinkResource extends Resource {
     tailKind: string;
     linkClass: string;
     name: string;
-    properties: TagProperty;
+    properties: any;
     kind: ResourceKind.LINK;
 }
 
@@ -21,4 +20,4 @@ export enum LinkClass {
     TAG = 'tag',
     PERMISSION = 'permission',
     PRESET = 'preset',
-}
\ No newline at end of file
+}
diff --git a/src/plugins.tsx b/src/plugins.tsx
index a7d033dd..1880e62d 100644
--- a/src/plugins.tsx
+++ b/src/plugins.tsx
@@ -15,15 +15,18 @@ export const pluginConfig: PluginConfig = {
     appBarRight: undefined,
     accountMenuList: [],
     enableNewButtonMatchers: [],
-    newButtonMenuList: []
+    newButtonMenuList: [],
+    middlewares: []
 };
 
 // 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 sampleTrackerPluginRegister } from '~/plugins/sample-tracker/index';
+import { studyListRoutePath } from '~/plugins/sample-tracker/studyList';
+// 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);
+sampleTrackerPluginRegister(pluginConfig);
+rootRedirectRegister(pluginConfig, studyListRoutePath);
diff --git a/src/store/context-menu/context-menu-actions.ts b/src/store/context-menu/context-menu-actions.ts
index 22553885..2982d052 100644
--- a/src/store/context-menu/context-menu-actions.ts
+++ b/src/store/context-menu/context-menu-actions.ts
@@ -34,7 +34,7 @@ export type ContextMenuResource = {
     ownerUuid: string;
     description?: string;
     kind: ResourceKind,
-    menuKind: ContextMenuKind;
+    menuKind: ContextMenuKind | string;
     isTrashed?: boolean;
     isEditable?: boolean;
     outputUuid?: string;
@@ -167,7 +167,7 @@ export const openProjectContextMenu = (event: React.MouseEvent<HTMLElement>, res
                 kind: res.kind,
                 menuKind,
                 ownerUuid: res.ownerUuid,
-                isTrashed: ('isTrashed' in res) ? res.isTrashed: false,
+                isTrashed: ('isTrashed' in res) ? res.isTrashed : false,
             }));
         }
     };
diff --git a/src/store/store.ts b/src/store/store.ts
index 517368aa..f236d029 100644
--- a/src/store/store.ts
+++ b/src/store/store.ts
@@ -70,6 +70,8 @@ import { SubprocessMiddlewareService } from '~/store/subprocess-panel/subprocess
 import { SUBPROCESS_PANEL_ID } from '~/store/subprocess-panel/subprocess-panel-actions';
 import { ALL_PROCESSES_PANEL_ID } from './all-processes-panel/all-processes-panel-action';
 import { Config } from '~/common/config';
+import { pluginConfig } from '~/plugins';
+import { MiddlewareListReducer } from '~/common/plugintypes';
 
 const composeEnhancers =
     (process.env.NODE_ENV === 'development' &&
@@ -142,7 +144,7 @@ export function configureStore(history: History, services: ServiceRepository, co
         return next(action);
     };
 
-    const middlewares: Middleware[] = [
+    let middlewares: Middleware[] = [
         routerMiddleware(history),
         thunkMiddleware.withExtraArgument(services),
         authMiddleware(services),
@@ -164,6 +166,11 @@ export function configureStore(history: History, services: ServiceRepository, co
         subprocessMiddleware,
     ];
 
+    const reduceMiddlewaresFn: (a: Middleware[],
+        b: MiddlewareListReducer) => Middleware[] = (a, b) => b(a, services);
+
+    middlewares = pluginConfig.middlewares.reduce(reduceMiddlewaresFn, middlewares);
+
     const enhancer = composeEnhancers(applyMiddleware(redirectToMiddleware, ...middlewares));
     return createStore(rootReducer, enhancer);
 }
diff --git a/src/views-components/form-fields/project-form-fields.tsx b/src/views-components/form-fields/project-form-fields.tsx
index dc1e1612..3f576ab1 100644
--- a/src/views-components/form-fields/project-form-fields.tsx
+++ b/src/views-components/form-fields/project-form-fields.tsx
@@ -11,6 +11,7 @@ import { RootState } from "~/store/store";
 
 interface ProjectNameFieldProps {
     validate: Validator[];
+    label?: string;
 }
 
 // Validation behavior depends on the value of ForwardSlashNameSubstitution.
@@ -32,7 +33,7 @@ export const ProjectNameField = connect(
             name='name'
             component={TextField}
             validate={props.validate}
-            label="Project Name"
+            label={props.label || "Project Name"}
             autoFocus={true} /></span>
     );
 
diff --git a/src/views-components/project-properties-dialog/project-properties-dialog.tsx b/src/views-components/project-properties-dialog/project-properties-dialog.tsx
index e1874d95..c2982b3d 100644
--- a/src/views-components/project-properties-dialog/project-properties-dialog.tsx
+++ b/src/views-components/project-properties-dialog/project-properties-dialog.tsx
@@ -40,42 +40,42 @@ const mapDispatchToProps = (dispatch: Dispatch): ProjectPropertiesDialogActionPr
     handleDelete: (key: string, value: string) => () => dispatch<any>(deleteProjectProperty(key, value)),
 });
 
-type ProjectPropertiesDialogProps =  ProjectPropertiesDialogDataProps & ProjectPropertiesDialogActionProps & WithDialogProps<{}> & WithStyles<CssRules>;
+type ProjectPropertiesDialogProps = ProjectPropertiesDialogDataProps & ProjectPropertiesDialogActionProps & WithDialogProps<{}> & WithStyles<CssRules>;
 
 export const ProjectPropertiesDialog = connect(mapStateToProps, mapDispatchToProps)(
     withStyles(styles)(
-    withDialog(PROJECT_PROPERTIES_DIALOG_NAME)(
-        ({ classes, open, closeDialog, handleDelete, project }: ProjectPropertiesDialogProps) =>
-            <Dialog open={open}
-                onClose={closeDialog}
-                fullWidth
-                maxWidth='sm'>
-                <DialogTitle>Properties</DialogTitle>
-                <DialogContent>
-                    <ProjectPropertiesForm />
-                    {project && project.properties &&
-                        Object.keys(project.properties).map(k =>
-                            Array.isArray(project.properties[k])
-                            ? project.properties[k].map((v: string) =>
-                                getPropertyChip(
-                                    k, v,
-                                    handleDelete(k, v),
-                                    classes.tag))
-                            : getPropertyChip(
-                                k, project.properties[k],
-                                handleDelete(k, project.properties[k]),
-                                classes.tag)
-                        )
-                    }
-                </DialogContent>
-                <DialogActions>
-                    <Button
-                        variant='text'
-                        color='primary'
-                        onClick={closeDialog}>
-                        Close
+        withDialog(PROJECT_PROPERTIES_DIALOG_NAME)(
+            ({ classes, open, closeDialog, handleDelete, project }: ProjectPropertiesDialogProps) =>
+                <Dialog open={open}
+                    onClose={closeDialog}
+                    fullWidth
+                    maxWidth='sm'>
+                    <DialogTitle>Properties</DialogTitle>
+                    <DialogContent>
+                        <ProjectPropertiesForm />
+                        {project && project.properties &&
+                            Object.keys(project.properties).map(k =>
+                                Array.isArray(project.properties[k])
+                                    ? project.properties[k].map((v: string) =>
+                                        getPropertyChip(
+                                            k, v,
+                                            handleDelete(k, v),
+                                            classes.tag))
+                                    : getPropertyChip(
+                                        k, project.properties[k],
+                                        handleDelete(k, project.properties[k]),
+                                        classes.tag)
+                            )
+                        }
+                    </DialogContent>
+                    <DialogActions>
+                        <Button
+                            variant='text'
+                            color='primary'
+                            onClick={closeDialog}>
+                            Close
                     </Button>
-                </DialogActions>
-            </Dialog>
-    )
-));
\ No newline at end of file
+                    </DialogActions>
+                </Dialog>
+        )
+    ));

commit fa03027f1954dfdd98c3d3d4f74ea3a016cf57c0
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 dd8c2373f5230831c051711d9e52a3b2defd9dd0
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 e4de9a43cee1a8859cb2a42ea01723d632621ce4
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 950ea822015652a479f236fff9ea8271b60f1ee9
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 623f5deee7b203090fb71f028e01ed55e0f8a38c
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 de88cd35..6cf9c29d 100644
--- a/Makefile
+++ b/Makefile
@@ -25,7 +25,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 30557563fd5fb704554f4d6109493f673ae7f5e5
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