Merge branch '17426-plug-ins' refs #17426
authorPeter Amstutz <peter.amstutz@curii.com>
Mon, 5 Apr 2021 14:32:46 +0000 (10:32 -0400)
committerPeter Amstutz <peter.amstutz@curii.com>
Mon, 5 Apr 2021 14:32:46 +0000 (10:32 -0400)
Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz@curii.com>

1  2 
src/index.tsx
src/store/context-menu/context-menu-actions.ts
src/store/side-panel-tree/side-panel-tree-actions.ts
src/views-components/side-panel-button/side-panel-button.tsx

diff --combined src/index.tsx
index 522d8dc1ee55676b777f09eaa3902ebecf53124d,6f4d9dc2993ecac72120264b8ccc7e215d935fe0..43cfb5fb03e31513e7689561760a4246ec3a35d6
@@@ -21,7 -21,7 +21,7 @@@ import { CustomTheme } from '~/common/c
  import { fetchConfig } from '~/common/config';
  import { addMenuActionSet, ContextMenuKind } from '~/views-components/context-menu/context-menu';
  import { rootProjectActionSet } from "~/views-components/context-menu/action-sets/root-project-action-set";
 -import { projectActionSet, readOnlyProjectActionSet } from "~/views-components/context-menu/action-sets/project-action-set";
 +import { filterGroupActionSet, projectActionSet, readOnlyProjectActionSet } from "~/views-components/context-menu/action-sets/project-action-set";
  import { resourceActionSet } from '~/views-components/context-menu/action-sets/resource-action-set';
  import { favoriteActionSet } from "~/views-components/context-menu/action-sets/favorite-action-set";
  import { collectionFilesActionSet, readOnlyCollectionFilesActionSet } from '~/views-components/context-menu/action-sets/collection-files-action-set';
@@@ -37,7 -37,7 +37,7 @@@ import { initWebSocket } from '~/websoc
  import { Config } from '~/common/config';
  import { addRouteChangeHandlers } from './routes/route-change-handlers';
  import { setTokenDialogApiHost } from '~/store/token-dialog/token-dialog-actions';
 -import { processResourceActionSet } from '~/views-components/context-menu/action-sets/process-resource-action-set';
 +import { processResourceActionSet, readOnlyProcessResourceActionSet } from '~/views-components/context-menu/action-sets/process-resource-action-set';
  import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions';
  import { trashedCollectionActionSet } from '~/views-components/context-menu/action-sets/trashed-collection-action-set';
  import { setBuildInfo } from '~/store/app-info/app-info-actions';
@@@ -58,7 -58,7 +58,7 @@@ import { groupMemberActionSet } from '~
  import { linkActionSet } from '~/views-components/context-menu/action-sets/link-action-set';
  import { loadFileViewersConfig } from '~/store/file-viewers/file-viewers-actions';
  import { processResourceAdminActionSet } from '~/views-components/context-menu/action-sets/process-resource-admin-action-set';
 -import { projectAdminActionSet } from '~/views-components/context-menu/action-sets/project-admin-action-set';
 +import { filterGroupAdminActionSet, projectAdminActionSet } from '~/views-components/context-menu/action-sets/project-admin-action-set';
  import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
  import { openNotFoundDialog } from './store/not-found-panel/not-found-panel-action';
  import { storeRedirects } from './common/redirect-to';
@@@ -68,7 -68,6 +68,7 @@@ console.log(`Starting arvados [${getBui
  addMenuActionSet(ContextMenuKind.ROOT_PROJECT, rootProjectActionSet);
  addMenuActionSet(ContextMenuKind.PROJECT, projectActionSet);
  addMenuActionSet(ContextMenuKind.READONLY_PROJECT, readOnlyProjectActionSet);
 +addMenuActionSet(ContextMenuKind.FILTER_GROUP, filterGroupActionSet);
  addMenuActionSet(ContextMenuKind.RESOURCE, resourceActionSet);
  addMenuActionSet(ContextMenuKind.FAVORITE, favoriteActionSet);
  addMenuActionSet(ContextMenuKind.COLLECTION_FILES, collectionFilesActionSet);
@@@ -82,7 -81,6 +82,7 @@@ addMenuActionSet(ContextMenuKind.OLD_VE
  addMenuActionSet(ContextMenuKind.TRASHED_COLLECTION, trashedCollectionActionSet);
  addMenuActionSet(ContextMenuKind.PROCESS, processActionSet);
  addMenuActionSet(ContextMenuKind.PROCESS_RESOURCE, processResourceActionSet);
 +addMenuActionSet(ContextMenuKind.READONLY_PROCESS_RESOURCE, readOnlyProcessResourceActionSet);
  addMenuActionSet(ContextMenuKind.TRASH, trashActionSet);
  addMenuActionSet(ContextMenuKind.REPOSITORY, repositoryActionSet);
  addMenuActionSet(ContextMenuKind.SSH_KEY, sshKeyActionSet);
@@@ -97,7 -95,6 +97,7 @@@ addMenuActionSet(ContextMenuKind.GROUP_
  addMenuActionSet(ContextMenuKind.COLLECTION_ADMIN, collectionAdminActionSet);
  addMenuActionSet(ContextMenuKind.PROCESS_ADMIN, processResourceAdminActionSet);
  addMenuActionSet(ContextMenuKind.PROJECT_ADMIN, projectAdminActionSet);
 +addMenuActionSet(ContextMenuKind.FILTER_GROUP_ADMIN, filterGroupAdminActionSet);
  
  storeRedirects();
  
@@@ -122,7 -119,8 +122,8 @@@ fetchConfig(
                                  ? error.errors[0]
                                  : error.message}`,
                              kind: SnackbarKind.ERROR,
-                             hideDuration: 8000})
+                             hideDuration: 8000
+                         })
                          );
                      }
                  }
index 83335f83c5aa938d2716f05705695ed7eabce358,2982d0520bf9af364ec5f41da90aab743c1343e2..1997b2a64894b3aba3baa7c3666ea394dd93614c
@@@ -18,7 -18,7 +18,7 @@@ import { VirtualMachinesResource } fro
  import { KeepServiceResource } from '~/models/keep-services';
  import { ProcessResource } from '~/models/process';
  import { CollectionResource } from '~/models/collection';
 -import { GroupResource } from '~/models/group';
 +import { GroupClass, GroupResource } from '~/models/group';
  import { GroupContentsResource } from '~/services/groups-service/groups-service';
  
  export const contextMenuActions = unionize({
@@@ -34,7 -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 +167,7 @@@ export const openProjectContextMenu = (
                  kind: res.kind,
                  menuKind,
                  ownerUuid: res.ownerUuid,
-                 isTrashed: ('isTrashed' in res) ? res.isTrashed: false,
+                 isTrashed: ('isTrashed' in res) ? res.isTrashed : false,
              }));
          }
      };
@@@ -201,24 -201,19 +201,24 @@@ export const openProcessContextMenu = (
          }
      };
  
 -export const resourceUuidToContextMenuKind = (uuid: string) =>
 +export const resourceUuidToContextMenuKind = (uuid: string, readonly = false) =>
      (dispatch: Dispatch, getState: () => RootState) => {
          const { isAdmin: isAdminUser, uuid: userUuid } = getState().auth.user!;
          const kind = extractUuidKind(uuid);
          const resource = getResourceWithEditableStatus<GroupResource & EditableResource>(uuid, userUuid)(getState().resources);
 -        const isEditable = isAdminUser || (resource || {} as EditableResource).isEditable;
 +
 +        const isEditable = (isAdminUser || (resource || {} as EditableResource).isEditable) && !readonly;
          switch (kind) {
              case ResourceKind.PROJECT:
 -                return !isAdminUser
 -                    ? isEditable
 -                        ? ContextMenuKind.PROJECT
 -                        : ContextMenuKind.READONLY_PROJECT
 -                    : ContextMenuKind.PROJECT_ADMIN;
 +                return (isAdminUser && !readonly)
 +                    ? (resource && resource.groupClass !== GroupClass.FILTER)
 +                        ? ContextMenuKind.PROJECT_ADMIN
 +                        : ContextMenuKind.FILTER_GROUP_ADMIN
 +                    : isEditable
 +                        ? (resource && resource.groupClass !== GroupClass.FILTER)
 +                            ? ContextMenuKind.PROJECT
 +                            : ContextMenuKind.FILTER_GROUP
 +                        : ContextMenuKind.READONLY_PROJECT;
              case ResourceKind.COLLECTION:
                  const c = getResource<CollectionResource>(uuid)(getState().resources);
                  if (c === undefined) { return; }
                      ? ContextMenuKind.OLD_VERSION_COLLECTION
                      : (isTrashed && isEditable)
                          ? ContextMenuKind.TRASHED_COLLECTION
 -                        : isAdminUser
 +                        : (isAdminUser && !readonly)
                              ? ContextMenuKind.COLLECTION_ADMIN
                              : isEditable
                                  ? ContextMenuKind.COLLECTION
                                  : ContextMenuKind.READONLY_COLLECTION;
              case ResourceKind.PROCESS:
 -                return !isAdminUser
 -                    ? ContextMenuKind.PROCESS_RESOURCE
 -                    : ContextMenuKind.PROCESS_ADMIN;
 +                return (isAdminUser && !readonly)
 +                    ? ContextMenuKind.PROCESS_ADMIN
 +                    : readonly
 +                        ? ContextMenuKind.READONLY_PROCESS_RESOURCE
 +                        : ContextMenuKind.PROCESS_RESOURCE;
              case ResourceKind.USER:
                  return ContextMenuKind.ROOT_PROJECT;
              case ResourceKind.LINK:
index 05d619270fd2a6a5f1b732172ab4cb00ef5f2f44,dd0f5e681db87bb6c9fd436550005c3a44118435..6152b99f1edde80cae53d1d3dc8d2d0bf8b8170c
@@@ -16,6 -16,8 +16,8 @@@ import { OrderBuilder } from '~/service
  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,34 -46,48 +46,48 @@@ export const getSidePanelTreeBranch = (
      return [];
  };
  
- const SIDE_PANEL_CATEGORIES = [
+ let SIDE_PANEL_CATEGORIES: string[] = [
+     SidePanelTreeCategory.PROJECTS,
+     SidePanelTreeCategory.SHARED_WITH_ME,
      SidePanelTreeCategory.PUBLIC_FAVORITES,
      SidePanelTreeCategory.FAVORITES,
      SidePanelTreeCategory.WORKFLOWS,
      SidePanelTreeCategory.ALL_PROCESSES,
-     SidePanelTreeCategory.TRASH,
+     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());
          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: []
+                 }));
+             }
          });
      };
  
@@@ -112,7 -128,7 +128,7 @@@ const loadSharedRoot = async (dispatch
      const params = {
          filters: `[${new FilterBuilder()
              .addIsA('uuid', ResourceKind.PROJECT)
 -            .addEqual('group_class', GroupClass.PROJECT)
 +            .addIn('group_class', [GroupClass.PROJECT, GroupClass.FILTER])
              .addDistinct('uuid', getState().auth.config.uuidPrefix + '-j7d0g-publicfavorites')
              .getFilters()}]`,
          order: new OrderBuilder<ProjectResource>()
index bf03bf6cb0e9f2b7bcf7be2872debd42fba5363e,151cfb68ab7f68638a12d1d67993bfde89168984..fb5ea11f5f0710afd51fd766c40b82691487ffc4
@@@ -15,9 -15,12 +15,12 @@@ import { navigateToRunProcess } from '~
  import { runProcessPanelActions } from '~/store/run-process-panel/run-process-panel-actions';
  import { getUserUuid } from '~/common/getuser';
  import { matchProjectRoute } from '~/routes/routes';
 -import { GroupResource } from '~/models/group';
 +import { GroupClass, 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 +40,7 @@@ const styles: StyleRulesCallback<CssRul
  });
  
  interface SidePanelDataProps {
-     location: any;
+     location: Location;
      currentItemId: string;
      resources: ResourcesState;
      currentUserUUID: string | undefined;
@@@ -87,11 -90,35 +90,36 @@@ export const SidePanelButton = withStyl
                      const currentProject = getResource<GroupResource>(currentItemId)(resources);
                      if (currentProject &&
                          currentProject.writableBy.indexOf(currentUserUUID || '') >= 0 &&
 -                        !isProjectTrashed(currentProject, resources)) {
 +                        !isProjectTrashed(currentProject, resources) &&
 +                        currentProject.groupClass !== GroupClass.FILTER) {
                          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, classes.menuItem);
+                 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">
                                  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>