17426: Add plugin ability to modify +New and account menu
authorPeter Amstutz <peter.amstutz@curii.com>
Sat, 27 Feb 2021 22:05:15 +0000 (17:05 -0500)
committerPeter Amstutz <peter.amstutz@curii.com>
Mon, 22 Mar 2021 21:02:56 +0000 (17:02 -0400)
Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz@curii.com>

src/common/plugintypes.ts
src/plugins.tsx
src/plugins/blank/index.tsx
src/plugins/example/index.tsx
src/views-components/main-app-bar/account-menu.tsx
src/views-components/main-app-bar/main-app-bar.tsx
src/views-components/side-panel-button/side-panel-button.tsx
src/views/workbench/workbench.tsx

index dfbe7c45f0c572cf4a133d6b1d5bd545dac30c2c..bda92b67afe434b9d152eb0f2811d18a6f914088 100644 (file)
@@ -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[];
 }
index 83593f23ff7fa8088424becfb0da8fbbc46abf11..3a58a8c2c9d4439b6ccfd387c80b41517a3859a2 100644 (file)
@@ -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. //
index 9471372d8da79d34d63a96cf564e69de9a7c5ee6..0074c02a4c37d5fa984ade039cc6f215d8c1596b 100644 (file)
@@ -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 />;
index 4fa98966f0b1edcff016e7822519937580494c94..b8bfcb0fa95cf10384410a4e54ca0164fd515277 100644 (file)
@@ -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 })));
 };
index ea3a2dd932409efce0b9ea3849180705dbcd4026..7892b8a77487f032ce503beb8081ac28378c5dff 100644 (file)
@@ -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));
index 7bec7b24f3dffa0c24cff40189a145ee742f8342..44cbe20dd6df2f96b1c32d71bdbb72cdb7805afb 100644 (file)
@@ -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>
index 3ca2f0d66e95d4cc552c54a70ca27f4644063d96..4c25bcfebd96302f61c4d13fcbbc638d230a71d8 100644 (file)
@@ -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
+);
index 113cbd670ce4af3212ddf8f22521c24116e7081c..78ec3c87cb24bce04e5a7387963409d94db074df 100644 (file)
@@ -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)));