17426: Add dialog box with form to example plugin.
authorPeter Amstutz <peter.amstutz@curii.com>
Mon, 29 Mar 2021 20:42:08 +0000 (16:42 -0400)
committerPeter Amstutz <peter.amstutz@curii.com>
Mon, 29 Mar 2021 21:55:30 +0000 (17:55 -0400)
Add comments explaining the example plugin

Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz@curii.com>

README.md
src/common/plugintypes.ts
src/plugins.tsx
src/plugins/README.md [new file with mode: 0644]
src/plugins/example/exampleComponents.tsx [new file with mode: 0644]
src/plugins/example/index.tsx

index 55e96af3c14e2d764dac391a2c632137567e293d..f6a7e48587522f8dcf90592582dd47ca571b14e6 100644 (file)
--- a/README.md
+++ b/README.md
@@ -2,59 +2,60 @@
 [comment]: # ()
 [comment]: # (SPDX-License-Identifier: CC-BY-SA-3.0)
 
-## Arvados Workbench 2
+# Arvados Workbench 2
 
-### Setup
-<pre>
-brew install yarn
+## Setup
+```
+npm install yarn
 yarn install
-</pre>
+```
+
 Install [redux-devtools-extension](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd)
 
-### Start project
-<code>yarn start</code>
+## Start project for development
+```
+yarn start
+```
 
-### Run unit tests
-<pre>
+## Run unit tests
+```
 make unit-tests
-</pre>
+```
 
-### Run end-to-end tests
+## Run end-to-end tests
 
-<pre>
+```
 make integration-tests
-</pre>
+```
 
-### Run end-to-end tests in a Docker container
+## Run end-to-end tests in a Docker container
 
-<pre>
+```
 make integration-tests-in-docker
-</pre>
+```
 
-### Run tests interactively in container
+## Run tests interactively in container
 
-<pre>
+```
 $ xhost +local:root
 $ ARVADOS_DIR=/path/to/arvados
 $ docker run -ti -v$PWD:$PWD -v$ARVADOS_DIR:/usr/src/arvados -w$PWD --env="DISPLAY" --volume="/tmp/.X11-unix:/tmp/.X11-unix:rw" workbench2-build /bin/bash
 (inside container)
 # yarn run cypress install
 # tools/run-integration-tests.sh -i -a /usr/src/arvados
-</pre>
+```
 
-### Production build
-<pre>
-yarn install
+## Production build
+```
 yarn build
-</pre>
+```
 
-### Package build
-<pre>
-make workbench2-build-image
-docker run -v$PWD:$PWD -w $PWD arvados/fpm make packages
-</pre>
+## Package build
+```
+make packages
+```
 
-### Build time configuration
+## Build time configuration
 You can customize project global variables using env variables. Default values are placed in the `.env` file.
 
 Example:
@@ -62,8 +63,9 @@ Example:
 REACT_APP_ARVADOS_CONFIG_URL=config.json yarn build
 ```
 
-### Run time configuration
-The app will fetch runtime configuration when starting. By default it will try to fetch `/config.json`. You can customize this url using build time configuration.
+## Run time configuration
+The app will fetch runtime configuration when starting. By default it will try to fetch `/config.json`.  In development mode, this can be found in the `public` directory.
+You can customize this url using build time configuration.
 
 Currently this configuration schema is supported:
 ```
@@ -74,19 +76,19 @@ Currently this configuration schema is supported:
 }
 ```
 
-#### API_HOST
+### API_HOST
 
 The Arvados base URL.
 
 The `REACT_APP_ARVADOS_API_HOST` environment variable can be used to set the default URL if the run time configuration is unreachable.
 
-#### VOCABULARY_URL
+### VOCABULARY_URL
 Local path, or any URL that allows cross-origin requests. See
 [Vocabulary JSON file example](public/vocabulary-example.json).
 
 To use the URL defined in the Arvados cluster configuration, remove the entire `VOCABULARY_URL` entry from the runtime configuration. Found in `/config.json` by default.
 
-### FILE_VIEWERS_CONFIG_URL
+## FILE_VIEWERS_CONFIG_URL
 Local path, or any URL that allows cross-origin requests. See:
 
 [File viewers config file example](public/file-viewers-example.json)
@@ -95,7 +97,14 @@ Local path, or any URL that allows cross-origin requests. See:
 
 To use the URL defined in the Arvados cluster configuration, remove the entire `FILE_VIEWERS_CONFIG_URL` entry from the runtime configuration. Found in `/config.json` by default.
 
-### Licensing
+## Plugin support
+
+Workbench supports plugins to add new functionality to the user
+interface.  For information about installing plugins, the provided
+example plugins, see [src/plugins/README.md](src/plugins/README.md).
+
+
+## Licensing
 
 Arvados is Free Software. See COPYING for information about Arvados Free
 Software licenses.
index 2ce0bb1256615a5e172625d498648ac5e5f8c7e7..a87b0a867ce8693884887353b6428cb50822a40b 100644 (file)
@@ -16,34 +16,163 @@ export type LocationChangeMatcher = (store: RootStore, pathname: string) => bool
 export type EnableNew = (location: Location, currentItemId: string, currentUserUUID: string | undefined, resources: ResourcesState) => boolean;
 export type MiddlewareListReducer = (startingList: Middleware[], services: ServiceRepository) => Middleware[];
 
+/* Workbench Plugin API
+
+   Code to your plugin should go into a subdirectory of '~/plugins'.
+
+   Your plugin should implement a "register" function, which will be
+   called with an object with the PluginConfig interface described
+   below.  The register function may make in-place modifications to
+   the pluginConfig object, but to preserve composability, it is
+   strongly advised this should be limited to push()ing new values
+   onto the various lists of hooks.
+
+   To enable a plugin, edit 'plugins.tsx', import the register
+   function exported by the plugin, and add a call to the register
+   function following the examples in the comments.  Then, build a new
+   Workbench package that includes the plugin.
+
+   Be aware that because plugins heavily leverage workbench, and in
+   fact must be compiled together, they are considered "derived works"
+   and so _must_ be license-compatible with AGPL-3.0.
+
+ */
+
 export interface PluginConfig {
-    // Customize the list of possible center panels by adding or removing Route components.
+
+    /* During initialization, each
+     * function in the callback list will be called with the list of
+     * react - router "Route" components that will be used select what should
+     * be displayed in the central panel based on the navigation bar.
+     *
+     * The callback function may add, edit, or remove items from this list,
+     * and return a new list of components, which will be passed to the next
+     * function in `centerPanelList`.
+     *
+     * The hooks are applied in `views/workbench/workbench.tsx`.
+     *  */
     centerPanelList: ElementListReducer[];
 
-    // Customize the list of side panel categories
+    /* During initialization, each
+     * function in the callback list will be called with the list of strings
+     * that are the top-level categories in the left hand navigation tree.
+     *
+     * The callback function may add, edit, or remove items from this list,
+     * and return a new list of strings, which will be passed to the next
+     * function in `sidePanelCategories`.
+     *
+     * The hooks are applied in `store/side-panel-tree/side-panel-tree-actions.ts`.
+     *  */
     sidePanelCategories: CategoriesListReducer[];
 
-    // Add to the list of possible dialogs by adding dialog components.
+    /* This is a list of additional dialog box components.
+     * Dialogs are components that are wrapped using the "withDialog()" method.
+     *
+     * These are added to the list in `views/workbench/workbench.tsx`.
+     *  */
     dialogs: React.ReactElement[];
 
-    // Add navigation actions for identifiers
+    /* This is a list of additional navigation matchers.
+     * These are callbacks that are called by the navigateTo(uuid) method to
+     * set the path in the navigation bar to display the desired resource.
+     * Each handler should return "true" if the uuid was handled and "false or "undefined" if not.
+     *
+     * These are used in `store/navigation/navigation-action.tsx`.
+     *  */
     navigateToHandlers: NavigateMatcher[];
 
-    // Add handlers for navigation actions
+    /* This is a list of additional location change matchers.
+     * These are callbacks called when the URL in the navigation bar changes
+     * (this could be in response to "navigateTo()" or due to the user
+     * entering/changing the URL directly).
+     *
+     * The Route components in centerPanelList should
+     * automatically change in response to navigation.  The
+     * purpose of these handlers is trigger additional loading,
+     * such as fetching the object contents that will be
+     * displayed.
+     *
+     * Each handler should return "true" if the path was handled and "false or "undefined" if not.
+     *
+     * These are used in `routes/route-change-handlers.ts`.
+     */
     locationChangeHandlers: LocationChangeMatcher[];
 
+    /* Replace the left side of the app bar.  Normally, this displays
+     * the site banner.
+     *
+     * Note: unlike most of the other hooks, this is not composable.
+     * This completely replaces that section of the app bar.  Multiple
+     * plugins setting this value will conflict.
+     *
+     * Used in 'views-components/main-app-bar/main-app-bar.tsx'
+     */
     appBarLeft?: React.ReactElement;
 
+    /* Replace the middle part of the app bar.  Normally, this displays
+     * the search bar.
+     *
+     * Note: unlike most of the other hooks, this is not composable.
+     * This completely replaces that section of the app bar.  Multiple
+     * plugins setting this value will conflict.
+     *
+     * Used in 'views-components/main-app-bar/main-app-bar.tsx'
+     */
     appBarMiddle?: React.ReactElement;
 
+    /* Replace the right part of the app bar.  Normally, this displays
+     * the admin menu and help menu.
+     * (Note: the user menu can be customized separately using accountMenuList)
+     *
+     * Note: unlike most of the other hooks, this is not composable.
+     * This completely replaces that section of the app bar.  Multiple
+     * plugins setting this value will conflict.
+     *
+     * Used in 'views-components/main-app-bar/main-app-bar.tsx'
+     */
     appBarRight?: React.ReactElement;
 
-    // Customize the list menu items in the account menu
+    /* During initialization, each
+     * function in the callback list will be called with the menu items that
+     * will appear in the "user account" menu.
+     *
+     * The callback function may add, edit, or remove items from this list,
+     * and return a new list of menu items, which will be passed to the next
+     * function in `accountMenuList`.
+     *
+     * The hooks are applied in 'views-components/main-app-bar/account-menu.tsx'.
+     *  */
     accountMenuList: ElementListReducer[];
 
+    /* Each function in this list is called to determine if the the "NEW" button
+     * should be enabled or disabled.  If any function returns "true", the button
+     * (and corresponding drop-down menu) will be enabled.
+     *
+     * The hooks are applied in 'views-components/side-panel-button/side-panel-button.tsx'.
+     *  */
     enableNewButtonMatchers: EnableNew[];
 
+    /* During initialization, each
+     * function in the callback list will be called with the menu items that
+     * will appear in the "NEW" dropdown menu.
+     *
+     * The callback function may add, edit, or remove items from this list,
+     * and return a new list of menu items, which will be passed to the next
+     * function in `newButtonMenuList`.
+     *
+     * The hooks are applied in 'views-components/side-panel-button/side-panel-button.tsx'.
+     *  */
     newButtonMenuList: ElementListReducer[];
 
+    /* Add Middlewares to the Redux store.
+     *
+     * Middlewares intercept redux actions before they get to the reducer, and
+     * may produce side effects.  For example, the REQUEST_ITEMS action is intercepted by a middleware to
+     * trigger a load of data table contents.
+     *
+     * https://redux.js.org/tutorials/fundamentals/part-4-store#middleware
+     *
+     * Used in 'store/store.ts'
+     *  */
     middlewares: MiddlewareListReducer[];
 }
index 03175494106a079df8e49c1d22d38e6f80eadb98..9962caef680f992e0d6595cf82be1ec05c71fd6d 100644 (file)
@@ -22,11 +22,9 @@ 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 examplePluginRegister, routePath as exampleRoutePath } from '~/plugins/example/index';
+import { register as examplePluginRegister } from '~/plugins/example/index';
 // import { register as rootRedirectRegister } from '~/plugins/root-redirect/index';
 
 // blankUIPluginRegister(pluginConfig);
-// sampleTrackerPluginRegister(pluginConfig);
-// rootRedirectRegister(pluginConfig, studyListRoutePath);
+examplePluginRegister(pluginConfig);
+// rootRedirectRegister(pluginConfig, exampleRoutePath);
diff --git a/src/plugins/README.md b/src/plugins/README.md
new file mode 100644 (file)
index 0000000..f95317c
--- /dev/null
@@ -0,0 +1,63 @@
+[comment]: # (Copyright © The Arvados Authors. All rights reserved.)
+[comment]: # ()
+[comment]: # (SPDX-License-Identifier: CC-BY-SA-3.0)
+
+# Plugin support
+
+Workbench supports plugins to add new functionality to the user
+interface.  It is also possible to remove the majority of standard UI
+elements and replace them with your own, enabling you to use workbench
+as a basis for developing essentially new applications for Arvados.
+
+## Installing plugins
+
+1. Check out the source of your plugin into a directory under `arvados-workbench2/src/plugins`
+
+2. Register the plugin by editing `arvados-workbench2/src/plugins/plugins.tsx`.
+It will look something like this:
+
+```
+import { register as examplePluginRegister } from '~/plugins/example/index';
+examplePluginRegister(pluginConfig);
+```
+
+3. Rebuild Workbench 2
+
+For testing/development: `yarn start`
+
+For production: `APP_NAME=arvados-workbench2-with-custom-plugins make packages`
+
+Set `APP_NAME=` to whatever you like, but it is important to name it
+differently from the standard `arvados-workbench2` to avoid confusion.
+
+## Existing plugins
+
+* "example"
+
+This is an example plugin showing how to add a new navigation tree
+item, displaying a new center panel, as well as adding account menu
+and "New" menu items, and showing how to use SET_PROPERTY and
+getProperty() for state.
+
+* "blank"
+
+This deletes all of the existing user interface.  If you want the
+application to only display your plugin's UI elements and none of the
+standard elements, you would load and register this first.
+
+* "root-redirect"
+
+This helper takes a path when registered.  It tweaks the navigation
+behavior so that the default starting location when the application
+loads will be the path you provide, instead of "Projects".
+
+* "sample-tracker"
+
+This can be found at
+
+
+
+## Developing plugins
+
+For information about the plugin API, see
+[../common/plugintypes.ts](src/common/plugintypes.ts).
diff --git a/src/plugins/example/exampleComponents.tsx b/src/plugins/example/exampleComponents.tsx
new file mode 100644 (file)
index 0000000..de2be4e
--- /dev/null
@@ -0,0 +1,131 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { WithDialogProps } from '~/store/dialog/with-dialog';
+import { ServiceRepository } from "~/services/services";
+import { Dispatch } from "redux";
+import { RootState } from '~/store/store';
+import { initialize } from 'redux-form';
+import { dialogActions } from "~/store/dialog/dialog-actions";
+import { reduxForm, InjectedFormProps, Field, reset, startSubmit } from 'redux-form';
+import { TextField } from "~/components/text-field/text-field";
+import { FormDialog } from '~/components/form-dialog/form-dialog';
+import { withDialog } from "~/store/dialog/with-dialog";
+import { compose } from "redux";
+import { propertiesActions } from "~/store/properties/properties-actions";
+import { DispatchProp, connect } from 'react-redux';
+import { MenuItem } from "@material-ui/core";
+import { Card, CardContent, Typography } from "@material-ui/core";
+
+// This is the name of the dialog box.  It in store actions that
+// open/close the dialog box.
+export const EXAMPLE_DIALOG_FORM_NAME = "exampleFormName";
+
+// This is the name of the property that will be used to store the
+// "pressed" count
+export const propertyKey = "Example_menu_item_pressed_count";
+
+// The model backing the form.
+export interface ExampleFormDialogData {
+    pressedCount: number | string;  // Supposed to start as a number but TextField seems to turn this into a string, unfortunately.
+}
+
+// The actual component with the editing fields.  Enables editing
+// the 'pressedCount' field.
+const ExampleEditFields = () => <span>
+    <Field
+        name='pressedCount'
+        component={TextField}
+        type="number"
+    />
+</span>;
+
+// Callback for when the form is submitted.
+const submitEditedPressedCount = (data: ExampleFormDialogData) =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(startSubmit(EXAMPLE_DIALOG_FORM_NAME));
+        dispatch(propertiesActions.SET_PROPERTY({
+            key: propertyKey, value: parseInt(data.pressedCount as string, 10)
+        }));
+        dispatch(dialogActions.CLOSE_DIALOG({ id: EXAMPLE_DIALOG_FORM_NAME }));
+        dispatch(reset(EXAMPLE_DIALOG_FORM_NAME));
+    };
+
+// Props for the dialog component
+type DialogExampleProps = WithDialogProps<{ updating: boolean }> & InjectedFormProps<ExampleFormDialogData>;
+
+// This is the component that renders the dialog.
+const DialogExample = (props: DialogExampleProps) =>
+    <FormDialog
+        dialogTitle="Edit pressed count"
+        formFields={ExampleEditFields}
+        submitLabel="Update pressed count"
+        {...props}
+    />;
+
+// This ties it all together, withDialog() determines if the dialog is
+// visible based on state, and reduxForm manages the values of the
+// dialog's fields.
+export const ExampleDialog = compose(
+    withDialog(EXAMPLE_DIALOG_FORM_NAME),
+    reduxForm<ExampleFormDialogData>({
+        form: EXAMPLE_DIALOG_FORM_NAME,
+        onSubmit: (data, dispatch) => {
+            dispatch(submitEditedPressedCount(data));
+        }
+    })
+)(DialogExample);
+
+
+// Callback, dispatches an action to set the value of property
+// "Example_menu_item_pressed_count"
+const incrementPressedCount = (dispatch: Dispatch, pressedCount: number) => {
+    dispatch(propertiesActions.SET_PROPERTY({ key: propertyKey, value: pressedCount + 1 }));
+};
+
+// Callback, dispatches actions required to initialize and open the
+// dialog box.
+export const openExampleDialog = (pressedCount: number) =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(initialize(EXAMPLE_DIALOG_FORM_NAME, { pressedCount }));
+        dispatch(dialogActions.OPEN_DIALOG({
+            id: EXAMPLE_DIALOG_FORM_NAME, data: {}
+        }));
+    };
+
+// Props definition used for menu items.
+interface ExampleProps {
+    pressedCount: number;
+    className?: string;
+}
+
+// Called to get the props from the redux state for several of the
+// following components.
+// Gets the value of the property "Example_menu_item_pressed_count"
+// from the state and puts it in 'pressedCount'
+const exampleMapStateToProps = (state: RootState) => ({ pressedCount: state.properties[propertyKey] || 0 });
+
+// Define component for the menu item that incremens the count each time it is pressed.
+export const ExampleMenuComponent = connect(exampleMapStateToProps)(
+    ({ pressedCount, dispatch, className }: ExampleProps & DispatchProp<any>) =>
+        <MenuItem className={className} onClick={() => incrementPressedCount(dispatch, pressedCount)}>Example menu item</MenuItem >
+);
+
+// Define component for the menu item that opens the dialog box that lets you edit the count directly.
+export const ExampleDialogMenuComponent = connect(exampleMapStateToProps)(
+    ({ pressedCount, dispatch, className }: ExampleProps & DispatchProp<any>) =>
+        <MenuItem className={className} onClick={() => dispatch(openExampleDialog(pressedCount))}>Open example dialog</MenuItem >
+);
+
+// The central panel.  Displays the "pressed" count.
+export const ExamplePluginMainPanel = connect(exampleMapStateToProps)(
+    ({ pressedCount }: ExampleProps) =>
+        <Card>
+            <CardContent>
+                <Typography>
+                    This is a example main panel plugin.  The example menu item has been pressed {pressedCount} times.
+               </Typography>
+            </CardContent>
+        </Card>);
index 36647fac3d274a9951370306bfa6779a8d247e52..f4bb27baa26be4a80fde6ba10f58bbd81a66e221 100644 (file)
@@ -2,71 +2,53 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-// Example plugin.
+// Example workbench plugin.  The entry point is the "register" method.
 
 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 { 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';
 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';
 import { handleFirstTimeLoad } from '~/store/workbench/workbench-actions';
+import {
+    ExampleDialog,
+    ExamplePluginMainPanel,
+    ExampleMenuComponent,
+    ExampleDialogMenuComponent
+} from './exampleComponents';
 
 const categoryName = "Plugin Example";
 export const routePath = "/examplePlugin";
-const propertyKey = "Example_menu_item_pressed_count";
-
-interface ExampleProps {
-    pressedCount: number;
-    className?: string;
-}
-
-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, className }: ExampleProps & DispatchProp<any>) =>
-        <MenuItem className={className} onClick={() => incrementPressedCount(dispatch, pressedCount)}>Example menu item</MenuItem >
-);
-
-const ExamplePluginMainPanel = connect(exampleMapStateToProps)(
-    ({ pressedCount }: ExampleProps) =>
-        <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) => {
 
+    // Add this component to the main panel.  When the app navigates
+    // to '/examplePlugin' it will render ExamplePluginMainPanel.
     pluginConfig.centerPanelList.push((elms) => {
         elms.push(<Route path={routePath} component={ExamplePluginMainPanel} />);
         return elms;
     });
 
+    // Add ExampleDialogMenuComponent to the upper-right user account menu
     pluginConfig.accountMenuList.push((elms, menuItemClass) => {
-        elms.push(<ExampleMenuComponent className={menuItemClass} />);
+        elms.push(<ExampleDialogMenuComponent className={menuItemClass} />);
         return elms;
     });
 
+    // Add ExampleMenuComponent to the "New" button dropdown.
     pluginConfig.newButtonMenuList.push((elms, menuItemClass) => {
         elms.push(<ExampleMenuComponent className={menuItemClass} />);
         return elms;
     });
 
+    // Add a hook so that when the 'Plugin Example' entry in the left
+    // hand tree view is clicked, which calls navigateTo('Plugin Example'),
+    // it will be implemented by navigating to '/examplePlugin'
     pluginConfig.navigateToHandlers.push((dispatch: Dispatch, getState: () => RootState, uuid: string) => {
         if (uuid === categoryName) {
             dispatch(push(routePath));
@@ -75,8 +57,12 @@ export const register = (pluginConfig: PluginConfig) => {
         return false;
     });
 
+    // Adds 'Plugin Example' to the left hand tree view.
     pluginConfig.sidePanelCategories.push((cats: string[]): string[] => { cats.push(categoryName); return cats; });
 
+    // When the location changes to '/examplePlugin', make sure
+    // 'Plugin Example' in the left hand tree view is selected, and
+    // make sure the breadcrumbs are updated.
     pluginConfig.locationChangeHandlers.push((store: RootStore, pathname: string): boolean => {
         if (matchPath(pathname, { path: routePath, exact: true })) {
             store.dispatch(handleFirstTimeLoad(
@@ -89,5 +75,11 @@ export const register = (pluginConfig: PluginConfig) => {
         return false;
     });
 
+    // The "New" button can enabled or disabled based on the current
+    // context or selection.  This adds a new callback to that will
+    // enable the "New" button when the location is '/examplePlugin'
     pluginConfig.enableNewButtonMatchers.push((location: Location) => (!!matchPath(location.pathname, { path: routePath, exact: true })));
+
+    // Add the example dialog box to the list of dialog box controls.
+    pluginConfig.dialogs.push(<ExampleDialog />);
 };