Merge branch '18559-user-profile' into main. Closes #18559
authorStephen Smith <stephen@curii.com>
Mon, 4 Apr 2022 21:31:00 +0000 (17:31 -0400)
committerStephen Smith <stephen@curii.com>
Mon, 4 Apr 2022 21:31:00 +0000 (17:31 -0400)
Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen@curii.com>

1  2 
src/components/icon/icon.tsx
src/routes/route-change-handlers.ts
src/routes/routes.ts
src/store/context-menu/context-menu-actions.ts
src/store/navigation/navigation-action.ts
src/store/workbench/workbench-actions.ts
src/views/workbench/workbench.tsx

index 557e22e77c959e08107ca10b11e174e13a967c30,4d17dd2863f6491a02540f0076b88d965e25cee2..19b4beea1eb66274f45c2fc8ba1f1879a3194333
@@@ -65,14 -65,15 +65,18 @@@ import VpnKey from '@material-ui/icons/
  import LinkOutlined from '@material-ui/icons/LinkOutlined';
  import RemoveRedEye from '@material-ui/icons/RemoveRedEye';
  import Computer from '@material-ui/icons/Computer';
 +import WrapText from '@material-ui/icons/WrapText';
 +import TextIncrease from '@material-ui/icons/ZoomIn';
 +import TextDecrease from '@material-ui/icons/ZoomOut';
+ import CropFreeSharp from '@material-ui/icons/CropFreeSharp';
+ import ExitToApp from '@material-ui/icons/ExitToApp';
+ import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline';
+ import RemoveCircleOutline from '@material-ui/icons/RemoveCircleOutline';
+ import NotInterested from '@material-ui/icons/NotInterested';
  
  // Import FontAwesome icons
  import { library } from '@fortawesome/fontawesome-svg-core';
  import { faPencilAlt, faSlash, faUsers, faEllipsisH } from '@fortawesome/free-solid-svg-icons';
- import { CropFreeSharp } from '@material-ui/icons';
  library.add(
      faPencilAlt,
      faSlash,
@@@ -176,6 -177,8 +180,11 @@@ export const CanReadIcon: IconType = (p
  export const CanWriteIcon: IconType = (props) => <Edit {...props} />;
  export const CanManageIcon: IconType = (props) => <Computer {...props} />;
  export const AddUserIcon: IconType = (props) => <PersonAdd {...props} />;
 +export const WordWrapIcon: IconType = (props) => <WrapText {...props} />;
 +export const TextIncreaseIcon: IconType = (props) => <TextIncrease {...props} />;
 +export const TextDecreaseIcon: IconType = (props) => <TextDecrease {...props} />;
+ export const DeactivateUserIcon: IconType = (props) => <NotInterested {...props} />;
+ export const LoginAsIcon: IconType = (props) => <ExitToApp {...props} />;
+ export const ActiveIcon: IconType = (props) => <CheckCircleOutline {...props} />;
+ export const SetupIcon: IconType = (props) => <RemoveCircleOutline {...props} />;
+ export const InactiveIcon: IconType = (props) => <NotInterested {...props} />;
index 5e07e6e8209e716e249a27eea3ee67456ddb1857,b91580835e4e747c499cb8c89aa2f3bda7985f70..5b3ce6687ff0e4cb8b3e17a97d286898dda90946
@@@ -27,6 -27,7 +27,6 @@@ const handleLocationChange = (store: Ro
      const publicFavoritesMatch = Routes.matchPublicFavoritesRoute(pathname);
      const trashMatch = Routes.matchTrashRoute(pathname);
      const processMatch = Routes.matchProcessRoute(pathname);
 -    const processLogMatch = Routes.matchProcessLogRoute(pathname);
      const repositoryMatch = Routes.matchRepositoriesRoute(pathname);
      const searchResultsMatch = Routes.matchSearchResultsRoute(pathname);
      const sharedWithMeMatch = Routes.matchSharedWithMeRoute(pathname);
@@@ -41,7 -42,8 +41,8 @@@
      const apiClientAuthorizationsMatch = Routes.matchApiClientAuthorizationsRoute(pathname);
      const myAccountMatch = Routes.matchMyAccountRoute(pathname);
      const linkAccountMatch = Routes.matchLinkAccountRoute(pathname);
-     const userMatch = Routes.matchUsersRoute(pathname);
+     const usersMatch = Routes.matchUsersRoute(pathname);
+     const userProfileMatch = Routes.matchUserProfileRoute(pathname);
      const groupsMatch = Routes.matchGroupsRoute(pathname);
      const groupDetailsMatch = Routes.matchGroupDetailsRoute(pathname);
      const linksMatch = Routes.matchLinksRoute(pathname);
@@@ -70,6 -72,8 +71,6 @@@
          store.dispatch(WorkbenchActions.loadTrash());
      } else if (processMatch) {
          store.dispatch(WorkbenchActions.loadProcess(processMatch.params.id));
 -    } else if (processLogMatch) {
 -        store.dispatch(WorkbenchActions.loadProcessLog(processLogMatch.params.id));
      } else if (rootMatch) {
          store.dispatch(navigateToRootProject);
      } else if (sharedWithMeMatch) {
      } else if (apiClientAuthorizationsMatch) {
          store.dispatch(WorkbenchActions.loadApiClientAuthorizations);
      } else if (myAccountMatch) {
-         store.dispatch(WorkbenchActions.loadMyAccount);
+         store.dispatch(WorkbenchActions.loadUserProfile());
      } else if (linkAccountMatch) {
          store.dispatch(WorkbenchActions.loadLinkAccount);
-     } else if (userMatch) {
+     } else if (usersMatch) {
          store.dispatch(WorkbenchActions.loadUsers);
+     } else if (userProfileMatch) {
+         store.dispatch(WorkbenchActions.loadUserProfile(userProfileMatch.params.id));
      } else if (groupsMatch) {
          store.dispatch(WorkbenchActions.loadGroupsPanel);
      } else if (groupDetailsMatch) {
diff --combined src/routes/routes.ts
index d7257b5124b38fc0f71ef392d52102ec7b52825e,205ae08fdd3c21978dddb1cc8316d3512d9e0ad8..50689ec37c46ac1fba6d5ff985badeb233944031
@@@ -25,6 -25,7 +25,6 @@@ export const Routes = 
      PROCESSES: `/processes/:id(${RESOURCE_UUID_PATTERN})`,
      FAVORITES: '/favorites',
      TRASH: '/trash',
 -    PROCESS_LOGS: `/process-logs/:id(${RESOURCE_UUID_PATTERN})`,
      REPOSITORIES: '/repositories',
      SHARED_WITH_ME: '/shared-with-me',
      RUN_PROCESS: '/run-process',
@@@ -39,6 -40,7 +39,7 @@@
      LINK_ACCOUNT: '/link_account',
      KEEP_SERVICES: `/keep-services`,
      USERS: '/users',
+     USER_PROFILE: `/user/:id(${RESOURCE_UUID_PATTERN})`,
      API_CLIENT_AUTHORIZATIONS: `/api_client_authorizations`,
      GROUPS: '/groups',
      GROUP_DETAILS: `/group/:id(${RESOURCE_UUID_PATTERN})`,
@@@ -94,8 -96,12 +95,10 @@@ export const getNavUrl = (uuid: string
  
  export const getProcessUrl = (uuid: string) => `/processes/${uuid}`;
  
 -export const getProcessLogUrl = (uuid: string) => `/process-logs/${uuid}`;
 -
  export const getGroupUrl = (uuid: string) => `/group/${uuid}`;
  
+ export const getUserProfileUrl = (uuid: string) => `/user/${uuid}`;
  export interface ResourceRouteParams {
      id: string;
  }
@@@ -121,6 -127,9 +124,6 @@@ export const matchCollectionRoute = (ro
  export const matchProcessRoute = (route: string) =>
      matchPath<ResourceRouteParams>(route, { path: Routes.PROCESSES });
  
 -export const matchProcessLogRoute = (route: string) =>
 -    matchPath<ResourceRouteParams>(route, { path: Routes.PROCESS_LOGS });
 -
  export const matchSharedWithMeRoute = (route: string) =>
      matchPath(route, { path: Routes.SHARED_WITH_ME });
  
@@@ -169,6 -178,9 +172,9 @@@ export const matchFedTokenRoute = (rout
  export const matchUsersRoute = (route: string) =>
      matchPath(route, { path: Routes.USERS });
  
+ export const matchUserProfileRoute = (route: string) =>
+     matchPath<ResourceRouteParams>(route, { path: Routes.USER_PROFILE });
  export const matchApiClientAuthorizationsRoute = (route: string) =>
      matchPath(route, { path: Routes.API_CLIENT_AUTHORIZATIONS });
  
index 336817ea4d0adc18c69a0c9b90347be33a7c33f5,fb5da9fcc0c7d4db80f8d2c1a3f4dcf3645f2947..1116949a6f31f769dfcf0fbb3ed56e147e7166c2
@@@ -190,7 -190,7 +190,7 @@@ export const openProcessContextMenu = (
                  description: res.description,
                  outputUuid: res.outputUuid || '',
                  workflowUuid: res.properties.workflowUuid || '',
 -                menuKind: ContextMenuKind.PROCESS
 +                menuKind: ContextMenuKind.PROCESS_RESOURCE
              }));
          }
      };
@@@ -208,6 -208,17 +208,17 @@@ export const openPermissionEditContextM
          }
      };
  
+ export const openUserContextMenu = (event: React.MouseEvent<HTMLElement>, user: UserResource) =>
+     (dispatch: Dispatch, getState: () => RootState) => {
+         dispatch<any>(openContextMenu(event, {
+             name: '',
+             uuid: user.uuid,
+             ownerUuid: user.ownerUuid,
+             kind: user.kind,
+             menuKind: ContextMenuKind.USER
+         }));
+     };
  export const resourceUuidToContextMenuKind = (uuid: string, readonly = false) =>
      (dispatch: Dispatch, getState: () => RootState) => {
          const { isAdmin: isAdminUser, uuid: userUuid } = getState().auth.user!;
index 49f565911e0b04230502be62c7032b2918a16d11,776409c01a1969604aa2d55898c84fd84c94a613..1cdb6784bf42390c88ef60fa920a76c773ab71c8
@@@ -6,11 -6,12 +6,12 @@@ import { Dispatch, compose, AnyAction 
  import { push } from "react-router-redux";
  import { ResourceKind, extractUuidKind } from 'models/resource';
  import { SidePanelTreeCategory } from '../side-panel-tree/side-panel-tree-actions';
- import { Routes, getGroupUrl, getNavUrl } from 'routes/routes';
 -import { Routes, getProcessLogUrl, getGroupUrl, getNavUrl, getUserProfileUrl } from 'routes/routes';
++import { Routes, getGroupUrl, getNavUrl, getUserProfileUrl } from 'routes/routes';
  import { RootState } from 'store/store';
  import { ServiceRepository } from 'services/services';
  import { pluginConfig } from 'plugins';
  import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
+ import { USERS_PANEL_LABEL, MY_ACCOUNT_PANEL_LABEL } from 'store/breadcrumbs/breadcrumbs-actions';
  
  const navigationNotAvailable = (id: string) =>
      snackbarActions.OPEN_SNACKBAR({
@@@ -69,6 -70,12 +70,12 @@@ export const navigateTo = (uuid: string
              case SidePanelTreeCategory.ALL_PROCESSES:
                  dispatch(navigateToAllProcesses);
                  return;
+             case USERS_PANEL_LABEL:
+                 dispatch(navigateToUsers);
+                 return;
+             case MY_ACCOUNT_PANEL_LABEL:
+                 dispatch(navigateToMyAccount);
+                 return;
          }
  
          dispatch(navigationNotAvailable(uuid));
@@@ -99,6 -106,8 +106,6 @@@ export const pushOrGoto = (url: string)
  };
  
  
 -export const navigateToProcessLogs = compose(push, getProcessLogUrl);
 -
  export const navigateToRootProject = (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
      navigateTo(SidePanelTreeCategory.PROJECTS)(dispatch, getState);
  };
@@@ -135,6 -144,8 +142,8 @@@ export const navigateToKeepServices = p
  
  export const navigateToUsers = push(Routes.USERS);
  
+ export const navigateToUserProfile = compose(push, getUserProfileUrl);
  export const navigateToApiClientAuthorizations = push(Routes.API_CLIENT_AUTHORIZATIONS);
  
  export const navigateToGroups = push(Routes.GROUPS);
index 7958463a5a33e520081badac1f425eeebfcc397c,5e83ed7b6e330751a673b15b3b43f06fe59fece2..ba405cb8f2938f9ce190b32eaed81ec8276aa76a
@@@ -31,7 -31,10 +31,10 @@@ import 
      setProcessBreadcrumbs,
      setSharedWithMeBreadcrumbs,
      setSidePanelBreadcrumbs,
-     setTrashBreadcrumbs
+     setTrashBreadcrumbs,
+     setUsersBreadcrumbs,
+     setMyAccountBreadcrumbs,
+     setUserProfileBreadcrumbs,
  } from 'store/breadcrumbs/breadcrumbs-actions';
  import { navigateTo, navigateToRootProject } from 'store/navigation/navigation-action';
  import { MoveToFormDialogData } from 'store/move-to-dialog/move-to-dialog';
@@@ -49,6 -52,7 +52,6 @@@ import * as processUpdateActions from '
  import * as processCopyActions from 'store/processes/process-copy-actions';
  import { trashPanelColumns } from "views/trash-panel/trash-panel";
  import { loadTrashPanel, trashPanelActions } from "store/trash-panel/trash-panel-action";
 -import { initProcessLogsPanel } from 'store/process-logs-panel/process-logs-panel-actions';
  import { loadProcessPanel } from 'store/process-panel/process-panel-actions';
  import {
      loadSharedWithMePanel,
@@@ -57,7 -61,6 +60,6 @@@
  import { CopyFormDialogData } from 'store/copy-dialog/copy-dialog';
  import { loadWorkflowPanel, workflowPanelActions } from 'store/workflow-panel/workflow-panel-actions';
  import { loadSshKeysPanel } from 'store/auth/auth-action-ssh';
- import { loadMyAccountPanel } from 'store/my-account/my-account-panel-actions';
  import { loadLinkAccountPanel, linkAccountPanelActions } from 'store/link-account-panel/link-account-panel-actions';
  import { loadSiteManagerPanel } from 'store/auth/auth-action-session';
  import { workflowPanelColumns } from 'views/workflow-panel/workflow-panel-view';
@@@ -79,6 -82,7 +81,7 @@@ import { loadVirtualMachinesPanel } fro
  import { loadRepositoriesPanel } from 'store/repositories/repositories-actions';
  import { loadKeepServicesPanel } from 'store/keep-services/keep-services-actions';
  import { loadUsersPanel, userBindedActions } from 'store/users/users-actions';
+ import * as userProfilePanelActions from 'store/user-profile/user-profile-actions';
  import { linkPanelActions, loadLinkPanel } from 'store/link-panel/link-panel-actions';
  import { linkPanelColumns } from 'views/link-panel/link-panel-root';
  import { userPanelColumns } from 'views/user-panel/user-panel';
@@@ -100,6 -104,7 +103,7 @@@ import { allProcessesPanelColumns } fro
  import { collectionPanelFilesAction } from '../collection-panel/collection-panel-files/collection-panel-files-actions';
  import { createTree } from 'models/tree';
  import { AdminMenuIcon } from 'components/icon/icon';
+ import { userProfileGroupsColumns } from 'views/user-profile-panel/user-profile-panel-root';
  
  export const WORKBENCH_LOADING_SCREEN = 'workbenchLoadingScreen';
  
@@@ -138,6 -143,7 +142,7 @@@ export const loadWorkbench = () =
              dispatch(groupPanelActions.GroupsPanelActions.SET_COLUMNS({ columns: groupsPanelColumns }));
              dispatch(groupDetailsPanelActions.GroupMembersPanelActions.SET_COLUMNS({ columns: groupDetailsMembersPanelColumns }));
              dispatch(groupDetailsPanelActions.GroupPermissionsPanelActions.SET_COLUMNS({ columns: groupDetailsPermissionsPanelColumns }));
+             dispatch(userProfilePanelActions.UserProfileGroupsActions.SET_COLUMNS({ columns: userProfileGroupsColumns }));
              dispatch(linkPanelActions.SET_COLUMNS({ columns: linkPanelColumns }));
              dispatch(apiClientAuthorizationsActions.SET_COLUMNS({ columns: apiClientAuthorizationPanelColumns }));
              dispatch(collectionsContentAddressActions.SET_COLUMNS({ columns: collectionContentAddressPanelColumns }));
@@@ -416,6 -422,15 +421,6 @@@ export const copyProcess = (data: CopyF
          }
      };
  
 -export const loadProcessLog = (uuid: string) =>
 -    handleFirstTimeLoad(
 -        async (dispatch: Dispatch) => {
 -            const process = await dispatch<any>(processesActions.loadProcess(uuid));
 -            dispatch<any>(setProcessBreadcrumbs(uuid));
 -            dispatch<any>(initProcessLogsPanel(uuid));
 -            await dispatch<any>(activateSidePanelTreeItem(process.containerRequest.ownerUuid));
 -        });
 -
  export const resourceIsNotLoaded = (uuid: string) =>
      snackbarActions.OPEN_SNACKBAR({
          message: `Resource identified by ${uuid} is not loaded.`,
@@@ -504,10 -519,18 +509,18 @@@ export const loadSiteManager = handleFi
          await dispatch(loadSiteManagerPanel());
      });
  
- export const loadMyAccount = handleFirstTimeLoad(
-     (dispatch: Dispatch<any>) => {
-         dispatch(loadMyAccountPanel());
-     });
+ export const loadUserProfile = (userUuid?: string) =>
+     handleFirstTimeLoad(
+         (dispatch: Dispatch<any>) => {
+             if (userUuid) {
+                 dispatch(setUserProfileBreadcrumbs(userUuid));
+                 dispatch(userProfilePanelActions.loadUserProfilePanel(userUuid));
+             } else {
+                 dispatch(setMyAccountBreadcrumbs());
+                 dispatch(userProfilePanelActions.loadUserProfilePanel());
+             }
+         }
+     );
  
  export const loadLinkAccount = handleFirstTimeLoad(
      (dispatch: Dispatch<any>) => {
@@@ -522,7 -545,7 +535,7 @@@ export const loadKeepServices = handleF
  export const loadUsers = handleFirstTimeLoad(
      async (dispatch: Dispatch<any>) => {
          await dispatch(loadUsersPanel());
-         dispatch(setBreadcrumbs([{ label: 'Users' }]));
+         dispatch(setUsersBreadcrumbs());
      });
  
  export const loadApiClientAuthorizations = handleFirstTimeLoad(
index fe97bd3b449ad4be36f5f099dc6a8f18fd333407,0d1a89503f1b52e2de3315f8e2882e2ddc5461fe..28fae4cd6b1b5a4b7a668d798c55b3271ad4c9f5
@@@ -20,6 -20,7 +20,6 @@@ import { MultipleFilesRemoveDialog } fr
  import { Routes } from 'routes/routes';
  import { SidePanel } from 'views-components/side-panel/side-panel';
  import { ProcessPanel } from 'views/process-panel/process-panel';
 -import { ProcessLogPanel } from 'views/process-log-panel/process-log-panel';
  import { ChangeWorkflowDialog } from 'views-components/run-process-dialog/change-workflow-dialog';
  import { CreateProjectDialog } from 'views-components/dialog-forms/create-project-dialog';
  import { CreateCollectionDialog } from 'views-components/dialog-forms/create-collection-dialog';
@@@ -46,7 -47,7 +46,7 @@@ import { SearchResultsPanel } from 'vie
  import { SshKeyPanel } from 'views/ssh-key-panel/ssh-key-panel';
  import { SshKeyAdminPanel } from 'views/ssh-key-panel/ssh-key-admin-panel';
  import { SiteManagerPanel } from "views/site-manager-panel/site-manager-panel";
- import { MyAccountPanel } from 'views/my-account-panel/my-account-panel';
+ import { UserProfilePanel } from 'views/user-profile-panel/user-profile-panel';
  import { SharingDialog } from 'views-components/sharing-dialog/sharing-dialog';
  import { NotFoundDialog } from 'views-components/not-found-dialog/not-found-dialog';
  import { AdvancedTabDialog } from 'views-components/advanced-tab-dialog/advanced-tab-dialog';
@@@ -79,8 -80,9 +79,9 @@@ import { UserPanel } from 'views/user-p
  import { UserAttributesDialog } from 'views-components/user-dialog/attributes-dialog';
  import { CreateUserDialog } from 'views-components/dialog-forms/create-user-dialog';
  import { HelpApiClientAuthorizationDialog } from 'views-components/api-client-authorizations-dialog/help-dialog';
- import { UserManageDialog } from 'views-components/user-dialog/manage-dialog';
- import { SetupShellAccountDialog } from 'views-components/dialog-forms/setup-shell-account-dialog';
+ import { DeactivateDialog } from 'views-components/user-dialog/deactivate-dialog';
+ import { ActivateDialog } from 'views-components/user-dialog/activate-dialog';
+ import { SetupDialog } from 'views-components/user-dialog/setup-dialog';
  import { GroupsPanel } from 'views/groups-panel/groups-panel';
  import { RemoveGroupDialog } from 'views-components/groups-dialog/remove-dialog';
  import { GroupAttributesDialog } from 'views-components/groups-dialog/attributes-dialog';
@@@ -157,6 -159,7 +158,6 @@@ let routes = <
      <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.KEEP_SERVICES} component={KeepServicePanel} />
      <Route path={Routes.USERS} component={UserPanel} />
      <Route path={Routes.API_CLIENT_AUTHORIZATIONS} component={ApiClientAuthorizationPanel} />
-     <Route path={Routes.MY_ACCOUNT} component={MyAccountPanel} />
+     <Route path={Routes.MY_ACCOUNT} component={UserProfilePanel} />
+     <Route path={Routes.USER_PROFILE} component={UserProfilePanel} />
      <Route path={Routes.GROUPS} component={GroupsPanel} />
      <Route path={Routes.GROUP_DETAILS} component={GroupDetailsPanel} />
      <Route path={Routes.LINKS} component={LinkPanel} />
@@@ -257,7 -261,6 +259,6 @@@ export const WorkbenchPanel 
              <RepositoryAttributesDialog />
              <RepositoriesSampleGitDialog />
              <RichTextEditorDialog />
-             <SetupShellAccountDialog />
              <SharingDialog />
              <NotFoundDialog />
              <Snackbar />
              <UpdateProcessDialog />
              <UpdateProjectDialog />
              <UserAttributesDialog />
-             <UserManageDialog />
+             <DeactivateDialog />
+             <ActivateDialog />
+             <SetupDialog />
              <VirtualMachineAttributesDialog />
              <FedLogin />
              <WebDavS3InfoDialog />