[ARVADOS-WORKBENCH2] updated: 2.3.0-206-ge4a198bd

Git user git at public.arvados.org
Thu Mar 31 05:44:21 UTC 2022


Summary of changes:
 cypress/integration/user-profile.spec.js           | 102 +++++-
 src/components/context-menu/context-menu.tsx       |   1 +
 .../copy-to-clipboard-snackbar.tsx                 |  58 ++++
 src/components/icon/icon.tsx                       |  11 +-
 src/models/group.ts                                |  16 +-
 src/services/user-service/user-service.ts          |   2 +-
 src/store/auth/auth-action.ts                      |   4 +
 src/store/breadcrumbs/breadcrumbs-actions.ts       |  25 +-
 src/store/context-menu/context-menu-actions.ts     |  11 +
 src/store/user-profile/user-profile-actions.ts     | 155 +++++++--
 src/store/users/user-panel-middleware-service.ts   |  74 ++---
 src/store/users/users-actions.ts                   | 102 ++----
 src/validators/validators.tsx                      |   2 +
 .../context-menu/action-sets/user-action-set.ts    |  54 ++-
 src/views-components/context-menu/context-menu.tsx |  14 +-
 src/views-components/data-explorer/renderers.tsx   | 130 +++++---
 .../dialog-forms/setup-shell-account-dialog.tsx    |  56 ----
 .../{deactivate-dialog.tsx => activate-dialog.tsx} |   8 +-
 .../user-dialog/deactivate-dialog.tsx              |   4 +-
 src/views-components/user-dialog/manage-dialog.tsx |  78 -----
 .../{deactivate-dialog.tsx => setup-dialog.tsx}    |   8 +-
 .../group-details-panel/group-details-panel.tsx    |   8 +-
 src/views/user-panel/user-panel.tsx                | 117 +++----
 .../user-profile-panel/user-profile-panel-root.tsx | 369 ++++++++++-----------
 .../user-profile-panel/user-profile-panel.tsx      |  22 +-
 src/views/workbench/workbench.tsx                  |   8 +-
 26 files changed, 783 insertions(+), 656 deletions(-)
 create mode 100644 src/components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar.tsx
 delete mode 100644 src/views-components/dialog-forms/setup-shell-account-dialog.tsx
 copy src/views-components/user-dialog/{deactivate-dialog.tsx => activate-dialog.tsx} (72%)
 delete mode 100644 src/views-components/user-dialog/manage-dialog.tsx
 copy src/views-components/user-dialog/{deactivate-dialog.tsx => setup-dialog.tsx} (72%)

       via  e4a198bd0cc9ed174cb68345623ce9fbd8923951 (commit)
       via  2ab43f230662c3f5d7d7ff75c08bdab0c66b22b0 (commit)
       via  8426035efad90f0745bf1c3e43da3027ef5c2805 (commit)
       via  3b4d9c223bbbd3778063eda39b5bf633b4b47e30 (commit)
       via  1ef4bffbfa4d05ae4356ef22b3964d6152310fb9 (commit)
       via  9828c767c310f24aea087aaa9f33a2134f0b1809 (commit)
       via  352e8d13baae99e7f4e9f1b88253d6988541fecb (commit)
       via  5cefad212822a48c83af1d38cbe14368c0cb1a20 (commit)
       via  d839ac44e90bfca5e07e8cc5ddf56c30deabc008 (commit)
       via  d5f11bb2fbb20c85e8ef857caedd58adf333e522 (commit)
       via  fa947bf995717dafce322213fabb84fbf5fd6d67 (commit)
       via  08f915990ffe4588c076efce4d01e0b3bed2b398 (commit)
       via  736b2d3cc858cfba93bedf5367f5af6c1852fcfe (commit)
       via  d4d0208892ea0f0f4bf79691770b14aca89db92b (commit)
       via  864465d579c4b2caf06fce87b1861674758135f6 (commit)
       via  ed59f8b637bc1131ec95e7215efb8bfa4fde9f04 (commit)
       via  6d49676d1505f09223d85a488e79ca678201a81b (commit)
       via  7e9513099881d63a99a70c69ddd71c5d83567fa4 (commit)
       via  066c2178f99f8fb5c818bc453b49ba8341d440a6 (commit)
       via  09b2d7a937291e1e74d773a4227c4cfc63c30972 (commit)
       via  a5cbf89b125947ddebacfb4475149d77a18d1851 (commit)
      from  46b878b9773789f7a953f58f3de2cc4bf370e153 (commit)

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 e4a198bd0cc9ed174cb68345623ce9fbd8923951
Author: Stephen Smith <stephen at curii.com>
Date:   Thu Mar 31 01:39:05 2022 -0400

    18559: Update user profile cypress tests and fix minor issues
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/cypress/integration/user-profile.spec.js b/cypress/integration/user-profile.spec.js
index 325f38c7..2af2a144 100644
--- a/cypress/integration/user-profile.spec.js
+++ b/cypress/integration/user-profile.spec.js
@@ -35,8 +35,8 @@ describe('User profile tests', function() {
         role,
         website,
     }) {
-        cy.get('[data-cy=profile-form] [data-cy=firstName] [data-cy=value]').contains(firstName);
-        cy.get('[data-cy=profile-form] [data-cy=lastName] [data-cy=value]').contains(lastName);
+        cy.get('[data-cy=profile-form] input[name="firstName"]').invoke('val').should('equal', firstName);
+        cy.get('[data-cy=profile-form] input[name="lastName"]').invoke('val').should('equal', lastName);
         cy.get('[data-cy=profile-form] [data-cy=email] [data-cy=value]').contains(email);
         cy.get('[data-cy=profile-form] [data-cy=username] [data-cy=value]').contains(username);
 
@@ -76,7 +76,12 @@ describe('User profile tests', function() {
             role: '',
             website: '',
         });
-        cy.get('[data-cy=profile-form] button[type="submit"]').click({force: true});
+        cy.get('[data-cy=profile-form] button[type="submit"]').then((btn) => {
+            if (!btn.is(':disabled')) {
+                btn.click();
+            }
+        });
+
 
         cy.goToPath('/user/' + activeUser.user.uuid);
         enterProfileValues({
@@ -85,7 +90,11 @@ describe('User profile tests', function() {
             role: '',
             website: '',
         });
-        cy.get('[data-cy=profile-form] button[type="submit"]').click({force: true});
+        cy.get('[data-cy=profile-form] button[type="submit"]').then((btn) => {
+            if (!btn.is(':disabled')) {
+                btn.click();
+            }
+        });
     });
 
     it('non-admin can edit own profile', function() {
@@ -157,8 +166,15 @@ describe('User profile tests', function() {
         // Submit should be disabled
         cy.get('[data-cy=profile-form] button[type="submit"]').should('be.disabled');
 
-        // Admin tab should be hidden
-        cy.get('div [role="tab"]').should('not.contain', 'ADMIN');
+        // Admin context items should be hidden
+        cy.get('[data-cy=user-profile-panel-options-btn]').click();
+        cy.get('[data-cy=context-menu]').within(() => {
+            cy.get('[role=button]').should('not.contain', 'Activate User')
+            cy.get('[role=button]').should('not.contain', 'Deactivate User')
+            cy.get('[role=button]').should('not.contain', 'Login As User')
+            cy.get('[role=button]').should('not.contain', 'Setup User');
+        });
+        cy.get('div[role=presentation]').click();
     });
 
     it('admin can edit own profile', function() {
@@ -167,8 +183,15 @@ describe('User profile tests', function() {
         cy.get('header button[title="Account Management"]').click();
         cy.get('#account-menu').contains('My account').click();
 
-        // Admin tab should be visible
-        cy.get('div [role="tab"]').should('contain', 'ADMIN');
+        // Admin context items should be visible
+        cy.get('[data-cy=user-profile-panel-options-btn]').click();
+        cy.get('[data-cy=context-menu]').within(() => {
+            cy.get('[role=button]').contains('Activate User')
+            cy.get('[role=button]').contains('Deactivate User')
+            cy.get('[role=button]').contains('Login As User')
+            cy.get('[role=button]').contains('Setup User');
+        });
+        cy.get('div[role=presentation]').click();
 
         // Check initial values
         assertProfileValues({
@@ -291,4 +314,67 @@ describe('User profile tests', function() {
         cy.get('[data-cy=user-profile-groups-data-explorer]').should('not.contain', projectGroupName);
     });
 
+    it('allows performing admin functions', function() {
+        cy.loginAs(adminUser);
+        cy.goToPath('/user/' + activeUser.user.uuid);
+
+        // Check that user is active
+        cy.get('[data-cy=account-status]').contains('Active');
+        cy.get('div [role="tab"]').contains('GROUPS').click();
+        cy.get('[data-cy=user-profile-groups-data-explorer]').should('contain', 'All users');
+        cy.get('div [role="tab"]').contains('PROFILE').click();
+
+        // Deactivate user
+        cy.get('[data-cy=user-profile-panel-options-btn]').click();
+        cy.get('[data-cy=context-menu]').contains('Deactivate User').click();
+        cy.get('[data-cy=confirmation-dialog-ok-btn]').click();
+
+        // Check that user is deactivated
+        cy.get('[data-cy=account-status]').contains('Inactive');
+        cy.get('div [role="tab"]').contains('GROUPS').click();
+        cy.get('[data-cy=user-profile-groups-data-explorer]').should('not.contain', 'All users');
+        cy.get('div [role="tab"]').contains('PROFILE').click();
+
+        // Setup user
+        cy.get('[data-cy=user-profile-panel-options-btn]').click();
+        cy.get('[data-cy=context-menu]').contains('Setup User').click();
+        cy.get('[data-cy=confirmation-dialog-ok-btn]').click();
+
+        // Check that user is setup
+        cy.get('[data-cy=account-status]').contains('Setup');
+        cy.get('div [role="tab"]').contains('GROUPS').click();
+        cy.get('[data-cy=user-profile-groups-data-explorer]').should('contain', 'All users');
+        cy.get('div [role="tab"]').contains('PROFILE').click();
+
+        // Activate user
+        cy.get('[data-cy=user-profile-panel-options-btn]').click();
+        cy.get('[data-cy=context-menu]').contains('Activate User').click();
+        cy.get('[data-cy=confirmation-dialog-ok-btn]').click();
+
+        // Check that user is active
+        cy.get('[data-cy=account-status]').contains('Active');
+        cy.get('div [role="tab"]').contains('GROUPS').click();
+        cy.get('[data-cy=user-profile-groups-data-explorer]').should('contain', 'All users');
+        cy.get('div [role="tab"]').contains('PROFILE').click();
+
+        // Deactivate and activate user skipping setup
+        cy.get('[data-cy=user-profile-panel-options-btn]').click();
+        cy.get('[data-cy=context-menu]').contains('Deactivate User').click();
+        cy.get('[data-cy=confirmation-dialog-ok-btn]').click();
+        //
+        cy.get('[data-cy=account-status]').contains('Inactive');
+        cy.get('div [role="tab"]').contains('GROUPS').click();
+        cy.get('[data-cy=user-profile-groups-data-explorer]').should('not.contain', 'All users');
+        cy.get('div [role="tab"]').contains('PROFILE').click();
+        //
+        cy.get('[data-cy=user-profile-panel-options-btn]').click();
+        cy.get('[data-cy=context-menu]').contains('Activate User').click();
+        cy.get('[data-cy=confirmation-dialog-ok-btn]').click();
+
+        // Check that user is active
+        cy.get('[data-cy=account-status]').contains('Active');
+        cy.get('div [role="tab"]').contains('GROUPS').click();
+        cy.get('[data-cy=user-profile-groups-data-explorer]').should('contain', 'All users');
+    });
+
 });
diff --git a/src/store/user-profile/user-profile-actions.ts b/src/store/user-profile/user-profile-actions.ts
index 4d3d34aa..9935518b 100644
--- a/src/store/user-profile/user-profile-actions.ts
+++ b/src/store/user-profile/user-profile-actions.ts
@@ -114,6 +114,10 @@ export const setup = (uuid: string) =>
     try {
       const resources = await services.userService.setup(uuid);
       dispatch(updateResources(resources.items));
+
+      // Refresh data explorer
+      dispatch(UserProfileGroupsActions.REQUEST_ITEMS());
+
       dispatch(snackbarActions.OPEN_SNACKBAR({ message: "User has been setup", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
     } catch (e) {
       dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000, kind: SnackbarKind.ERROR }));
@@ -127,6 +131,10 @@ export const activate = (uuid: string) =>
     try {
       const user = await services.userService.activate(uuid);
       dispatch(updateResources([user]));
+
+      // Refresh data explorer
+      dispatch(UserProfileGroupsActions.REQUEST_ITEMS());
+
       dispatch(snackbarActions.OPEN_SNACKBAR({ message: "User has been activated", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
     } catch (e) {
       dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000, kind: SnackbarKind.ERROR }));
@@ -152,6 +160,9 @@ export const deactivate = (uuid: string) =>
       // Remove all users membership locally
       dispatch<any>(deleteResources(memberships.map(link => link.uuid)));
 
+      // Refresh data explorer
+      dispatch(UserProfileGroupsActions.REQUEST_ITEMS());
+
       dispatch(snackbarActions.OPEN_SNACKBAR({
         message: "User has been deactivated.",
         hideDuration: 2000,
diff --git a/src/views-components/context-menu/action-sets/user-action-set.ts b/src/views-components/context-menu/action-sets/user-action-set.ts
index b01516e2..6511b9a0 100644
--- a/src/views-components/context-menu/action-sets/user-action-set.ts
+++ b/src/views-components/context-menu/action-sets/user-action-set.ts
@@ -44,6 +44,7 @@ export const userActionSet: ContextMenuActionSet = [[{
     }
 },], [{
     name: "Activate User",
+    adminOnly: true,
     icon: ActiveIcon,
     execute: (dispatch, { uuid }) => {
         dispatch<any>(openActivateDialog(uuid));
diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx
index 8b29b24d..e854da0e 100644
--- a/src/views-components/data-explorer/renderers.tsx
+++ b/src/views-components/data-explorer/renderers.tsx
@@ -220,7 +220,7 @@ enum UserAccountStatus {
 }
 
 const renderAccountStatus = (props: {status: UserAccountStatus}) =>
-    <Grid container alignItems="center" wrap="nowrap" spacing={8}>
+    <Grid container alignItems="center" wrap="nowrap" spacing={8} data-cy="account-status">
         <Grid item>
             {(() => {
                 switch(props.status) {
diff --git a/src/views/user-profile-panel/user-profile-panel-root.tsx b/src/views/user-profile-panel/user-profile-panel-root.tsx
index 445f15e8..1c8b1da7 100644
--- a/src/views/user-profile-panel/user-profile-panel-root.tsx
+++ b/src/views/user-profile-panel/user-profile-panel-root.tsx
@@ -217,7 +217,7 @@ export const UserProfilePanelRoot = withStyles(styles)(
                                         <Grid item>
                                             <Tooltip title="Actions" disableFocusListener>
                                                 <IconButton
-                                                    data-cy='collection-panel-options-btn'
+                                                    data-cy='user-profile-panel-options-btn'
                                                     aria-label="Actions"
                                                     onClick={(event) => this.handleContextMenu(event, this.props.userUuid)}>
                                                     <MoreOptionsIcon />

commit 2ab43f230662c3f5d7d7ff75c08bdab0c66b22b0
Author: Stephen Smith <stephen at curii.com>
Date:   Wed Mar 30 22:29:18 2022 -0400

    18559: Add account status indicator to user profile
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx
index d1fed3a9..8b29b24d 100644
--- a/src/views-components/data-explorer/renderers.tsx
+++ b/src/views-components/data-explorer/renderers.tsx
@@ -225,11 +225,11 @@ const renderAccountStatus = (props: {status: UserAccountStatus}) =>
             {(() => {
                 switch(props.status) {
                     case UserAccountStatus.ACTIVE:
-                        return <ActiveIcon style={{color: '#4caf50'}} />;
+                        return <ActiveIcon style={{color: '#4caf50', verticalAlign: "middle"}} />;
                     case UserAccountStatus.SETUP:
-                        return <SetupIcon style={{color: '#2196f3'}} />;
+                        return <SetupIcon style={{color: '#2196f3', verticalAlign: "middle"}} />;
                     case UserAccountStatus.INACTIVE:
-                        return <InactiveIcon style={{color: '#9e9e9e'}} />;
+                        return <InactiveIcon style={{color: '#9e9e9e', verticalAlign: "middle"}} />;
                     default:
                         return <></>;
                 }
diff --git a/src/views/user-profile-panel/user-profile-panel-root.tsx b/src/views/user-profile-panel/user-profile-panel-root.tsx
index acb1c161..445f15e8 100644
--- a/src/views/user-profile-panel/user-profile-panel-root.tsx
+++ b/src/views/user-profile-panel/user-profile-panel-root.tsx
@@ -30,7 +30,7 @@ import { USER_PROFILE_PANEL_ID } from 'store/user-profile/user-profile-actions';
 import { noop } from 'lodash';
 import { DetailsIcon, GroupsIcon, MoreOptionsIcon } from 'components/icon/icon';
 import { DataColumns } from 'components/data-table/data-table';
-import { ResourceLinkHeadUuid, ResourceLinkHeadPermissionLevel, ResourceLinkHead, ResourceLinkDelete, ResourceLinkTailIsVisible } from 'views-components/data-explorer/renderers';
+import { ResourceLinkHeadUuid, ResourceLinkHeadPermissionLevel, ResourceLinkHead, ResourceLinkDelete, ResourceLinkTailIsVisible, UserResourceAccountStatus } from 'views-components/data-explorer/renderers';
 import { createTree } from 'models/tree';
 import { getResource, ResourcesState } from 'store/resources/resources';
 import { DefaultView } from 'components/default-view/default-view';
@@ -205,21 +205,26 @@ export const UserProfilePanelRoot = withStyles(styles)(
                     {this.state.value === TABS.PROFILE &&
                         <CardContent>
                             <Grid container justify="space-between">
-                                <Grid item xs={11}>
+                                <Grid item>
                                     <Typography className={this.props.classes.title}>
                                         {this.props.userUuid}
                                         <CopyToClipboardSnackbar value={this.props.userUuid} />
                                     </Typography>
                                 </Grid>
-                                <Grid item xs={1} style={{ textAlign: "right" }}>
-                                    <Tooltip title="Actions" disableFocusListener>
-                                        <IconButton
-                                            data-cy='collection-panel-options-btn'
-                                            aria-label="Actions"
-                                            onClick={(event) => this.handleContextMenu(event, this.props.userUuid)}>
-                                            <MoreOptionsIcon />
-                                        </IconButton>
-                                    </Tooltip>
+                                <Grid item>
+                                    <Grid container alignItems="center">
+                                        <Grid item style={{marginRight: '10px'}}><UserResourceAccountStatus uuid={this.props.userUuid} /></Grid>
+                                        <Grid item>
+                                            <Tooltip title="Actions" disableFocusListener>
+                                                <IconButton
+                                                    data-cy='collection-panel-options-btn'
+                                                    aria-label="Actions"
+                                                    onClick={(event) => this.handleContextMenu(event, this.props.userUuid)}>
+                                                    <MoreOptionsIcon />
+                                                </IconButton>
+                                            </Tooltip>
+                                        </Grid>
+                                    </Grid>
                                 </Grid>
                             </Grid>
                             <form onSubmit={this.props.handleSubmit} data-cy="profile-form">

commit 8426035efad90f0745bf1c3e43da3027ef5c2805
Author: Stephen Smith <stephen at curii.com>
Date:   Wed Mar 30 22:17:30 2022 -0400

    18559: Add basic validation to url field
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/validators/validators.tsx b/src/validators/validators.tsx
index b781a3a9..6e72ef68 100644
--- a/src/validators/validators.tsx
+++ b/src/validators/validators.tsx
@@ -32,6 +32,7 @@ export const REPOSITORY_NAME_VALIDATION = [require, maxLength(255)];
 
 export const USER_EMAIL_VALIDATION = [require, maxLength(255)];
 export const PROFILE_EMAIL_VALIDATION = [maxLength(255)];
+export const PROFILE_URL_VALIDATION = [maxLength(255)];
 export const USER_LENGTH_VALIDATION = [maxLength(255)];
 
 export const SSH_KEY_PUBLIC_VALIDATION = [require, isRsaKey, maxLength(1024)];
diff --git a/src/views/user-profile-panel/user-profile-panel-root.tsx b/src/views/user-profile-panel/user-profile-panel-root.tsx
index 531d3bdb..acb1c161 100644
--- a/src/views/user-profile-panel/user-profile-panel-root.tsx
+++ b/src/views/user-profile-panel/user-profile-panel-root.tsx
@@ -25,7 +25,7 @@ import {
 } from '@material-ui/core';
 import { ArvadosTheme } from 'common/custom-theme';
 import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
-import { PROFILE_EMAIL_VALIDATION } from "validators/validators";
+import { PROFILE_EMAIL_VALIDATION, PROFILE_URL_VALIDATION } from "validators/validators";
 import { USER_PROFILE_PANEL_ID } from 'store/user-profile/user-profile-actions';
 import { noop } from 'lodash';
 import { DetailsIcon, GroupsIcon, MoreOptionsIcon } from 'components/icon/icon';
@@ -289,6 +289,7 @@ export const UserProfilePanelRoot = withStyles(styles)(
                                             name="prefs.profile.website_url"
                                             component={TextField as any}
                                             disabled={!this.props.isAdmin && !this.props.isSelf}
+                                            validate={PROFILE_URL_VALIDATION}
                                         />
                                     </Grid>
                                     <Grid item sm={12}>

commit 3b4d9c223bbbd3778063eda39b5bf633b4b47e30
Author: Stephen Smith <stephen at curii.com>
Date:   Wed Mar 30 21:17:47 2022 -0400

    18559: Allow editing user fname/lname
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views/user-profile-panel/user-profile-panel-root.tsx b/src/views/user-profile-panel/user-profile-panel-root.tsx
index 6a3adb92..531d3bdb 100644
--- a/src/views/user-profile-panel/user-profile-panel-root.tsx
+++ b/src/views/user-profile-panel/user-profile-panel-root.tsx
@@ -228,16 +228,16 @@ export const UserProfilePanelRoot = withStyles(styles)(
                                         <Field
                                             label="First name"
                                             name="firstName"
-                                            component={ReadOnlyField as any}
-                                            disabled
+                                            component={TextField as any}
+                                            disabled={!this.props.isAdmin && !this.props.isSelf}
                                         />
                                     </Grid>
                                     <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="lastName">
                                         <Field
                                             label="Last name"
                                             name="lastName"
-                                            component={ReadOnlyField as any}
-                                            disabled
+                                            component={TextField as any}
+                                            disabled={!this.props.isAdmin && !this.props.isSelf}
                                         />
                                     </Grid>
                                     <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="email">
@@ -289,7 +289,6 @@ export const UserProfilePanelRoot = withStyles(styles)(
                                             name="prefs.profile.website_url"
                                             component={TextField as any}
                                             disabled={!this.props.isAdmin && !this.props.isSelf}
-
                                         />
                                     </Grid>
                                     <Grid item sm={12}>

commit 1ef4bffbfa4d05ae4356ef22b3964d6152310fb9
Author: Stephen Smith <stephen at curii.com>
Date:   Wed Mar 30 21:10:04 2022 -0400

    18559: Use full text user search instead of fname or lname only
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/store/users/user-panel-middleware-service.ts b/src/store/users/user-panel-middleware-service.ts
index 83af302c..c0589a60 100644
--- a/src/store/users/user-panel-middleware-service.ts
+++ b/src/store/users/user-panel-middleware-service.ts
@@ -29,15 +29,10 @@ export class UserMiddlewareService extends DataExplorerMiddlewareService {
         const state = api.getState();
         const dataExplorer = getDataExplorer(state.dataExplorer, this.getId());
         try {
-            const responseFirstName = await this.services.userService.list(getParamsFirstName(dataExplorer));
-            if (responseFirstName.itemsAvailable) {
-                api.dispatch(updateResources(responseFirstName.items));
-                api.dispatch(setItems(responseFirstName));
-            } else {
-                const responseLastName = await this.services.userService.list(getParamsLastName(dataExplorer));
-                api.dispatch(updateResources(responseLastName.items));
-                api.dispatch(setItems(responseLastName));
-            }
+            const users = await this.services.userService.list(getParams(dataExplorer));
+            api.dispatch(updateResources(users.items));
+            api.dispatch(setItems(users));
+
             // Get "all users" group memberships
             const allUsersGroupUuid = getBuiltinGroupUuid(state.auth.localCluster, BuiltinGroups.ALL);
             const allUserMemberships = await this.services.permissionService.list({
@@ -53,32 +48,14 @@ export class UserMiddlewareService extends DataExplorerMiddlewareService {
     }
 }
 
-const getParamsFirstName = (dataExplorer: DataExplorer) => ({
-    ...dataExplorerToListParams(dataExplorer),
-    order: getOrder(dataExplorer),
-    filters: getFiltersFirstName(dataExplorer)
-});
-
-const getParamsLastName = (dataExplorer: DataExplorer) => ({
+const getParams = (dataExplorer: DataExplorer) => ({
     ...dataExplorerToListParams(dataExplorer),
     order: getOrder(dataExplorer),
-    filters: getFiltersLastName(dataExplorer)
+    filters: new FilterBuilder()
+        .addFullTextSearch(dataExplorer.searchValue)
+        .getFilters()
 });
 
-const getFiltersFirstName = (dataExplorer: DataExplorer) => {
-    const filters = new FilterBuilder()
-        .addILike("first_name", dataExplorer.searchValue)
-        .getFilters();
-    return filters;
-};
-
-const getFiltersLastName = (dataExplorer: DataExplorer) => {
-    const filters = new FilterBuilder()
-        .addILike("last_name", dataExplorer.searchValue)
-        .getFilters();
-    return filters;
-};
-
 export const getOrder = (dataExplorer: DataExplorer) => {
     const sortColumn = getSortColumn(dataExplorer);
     const order = new OrderBuilder<UserResource>();

commit 9828c767c310f24aea087aaa9f33a2134f0b1809
Author: Stephen Smith <stephen at curii.com>
Date:   Wed Mar 30 20:51:27 2022 -0400

    18559: Add copy to clipboard to rendered UUIDs
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar.tsx b/src/components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar.tsx
new file mode 100644
index 00000000..3b2ff68a
--- /dev/null
+++ b/src/components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar.tsx
@@ -0,0 +1,58 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from "react";
+import { connect, DispatchProp } from "react-redux";
+import { StyleRulesCallback, Tooltip, WithStyles, withStyles } from "@material-ui/core";
+import { ArvadosTheme } from 'common/custom-theme';
+import CopyToClipboard from 'react-copy-to-clipboard';
+import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
+import { CopyIcon } from 'components/icon/icon';
+
+type CssRules = 'copyIcon';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+  copyIcon: {
+    marginLeft: theme.spacing.unit,
+    color: theme.palette.grey["500"],
+    cursor: 'pointer',
+    display: 'inline',
+    '& svg': {
+      fontSize: '1rem',
+      verticalAlign: 'middle',
+    }
+  }
+});
+
+interface CopyToClipboardDataProps {
+  children?: React.ReactNode;
+  value: string;
+}
+
+type CopyToClipboardProps = CopyToClipboardDataProps & WithStyles<CssRules> & DispatchProp;
+
+export const CopyToClipboardSnackbar = connect()(withStyles(styles)(
+  class CopyToClipboardSnackbar extends React.Component<CopyToClipboardProps> {
+    onCopy = () => {
+      this.props.dispatch(snackbarActions.OPEN_SNACKBAR({
+        message: 'Copied',
+        hideDuration: 2000,
+        kind: SnackbarKind.SUCCESS
+    }));
+    };
+
+    render() {
+      const { children, value, classes } = this.props;
+      return (
+        <Tooltip title="Copy to clipboard">
+          <span className={classes.copyIcon}>
+            <CopyToClipboard text={value} onCopy={this.onCopy}>
+              {children || <CopyIcon />}
+            </CopyToClipboard>
+          </span>
+        </Tooltip>
+      );
+    }
+  }
+));
diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx
index 51b7dcc8..d1fed3a9 100644
--- a/src/views-components/data-explorer/renderers.tsx
+++ b/src/views-components/data-explorer/renderers.tsx
@@ -50,6 +50,7 @@ import { PermissionLevel } from 'models/permission';
 import { openPermissionEditContextMenu } from 'store/context-menu/context-menu-actions';
 import { getUserUuid } from 'common/getuser';
 import { VirtualMachinesResource } from 'models/virtual-machines';
+import { CopyToClipboardSnackbar } from 'components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar';
 
 const renderName = (dispatch: Dispatch, item: GroupContentsResource) => {
 
@@ -192,15 +193,15 @@ export const UserResourceFullName = connect(
         return {item: resource || { uuid: '', firstName: '', lastName: '' }, link: props.link};
     })((props: {item: {uuid: string, firstName: string, lastName: string}, link?: boolean} & DispatchProp<any>) => renderFullName(props.dispatch, props.item, props.link));
 
-
 const renderUuid = (item: { uuid: string }) =>
-    <Typography data-cy="uuid" noWrap>{item.uuid}</Typography>;
+    <Typography data-cy="uuid" noWrap>
+        {item.uuid}
+        <CopyToClipboardSnackbar value={item.uuid} />
+    </Typography>;
 
-export const ResourceUuid = connect(
-    (state: RootState, props: { uuid: string }) => {
-        const resource = getResource<UserResource>(props.uuid)(state.resources);
-        return resource || { uuid: '' };
-    })(renderUuid);
+export const ResourceUuid = connect((state: RootState, props: { uuid: string }) => (
+        getResource<UserResource>(props.uuid)(state.resources) || { uuid: '' }
+    ))(renderUuid);
 
 const renderEmail = (item: { email: string }) =>
     <Typography noWrap>{item.email}</Typography>;
diff --git a/src/views/user-profile-panel/user-profile-panel-root.tsx b/src/views/user-profile-panel/user-profile-panel-root.tsx
index f89ca5c5..6a3adb92 100644
--- a/src/views/user-profile-panel/user-profile-panel-root.tsx
+++ b/src/views/user-profile-panel/user-profile-panel-root.tsx
@@ -28,14 +28,13 @@ import { DataTableDefaultView } from 'components/data-table-default-view/data-ta
 import { PROFILE_EMAIL_VALIDATION } from "validators/validators";
 import { USER_PROFILE_PANEL_ID } from 'store/user-profile/user-profile-actions';
 import { noop } from 'lodash';
-import { CopyIcon, DetailsIcon, GroupsIcon, MoreOptionsIcon } from 'components/icon/icon';
+import { DetailsIcon, GroupsIcon, MoreOptionsIcon } from 'components/icon/icon';
 import { DataColumns } from 'components/data-table/data-table';
 import { ResourceLinkHeadUuid, ResourceLinkHeadPermissionLevel, ResourceLinkHead, ResourceLinkDelete, ResourceLinkTailIsVisible } from 'views-components/data-explorer/renderers';
 import { createTree } from 'models/tree';
 import { getResource, ResourcesState } from 'store/resources/resources';
-import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
-import CopyToClipboard from 'react-copy-to-clipboard';
 import { DefaultView } from 'components/default-view/default-view';
+import { CopyToClipboardSnackbar } from 'components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar';
 
 type CssRules = 'root' | 'emptyRoot' | 'gridItem' | 'label' | 'readOnlyValue' | 'title' | 'description' | 'actions' | 'content' | 'copyIcon';
 
@@ -188,14 +187,6 @@ export const UserProfilePanelRoot = withStyles(styles)(
             this.setState({ value: TABS.PROFILE});
         }
 
-        onCopy = (message: string) => {
-            this.props.dispatch(snackbarActions.OPEN_SNACKBAR({
-                message,
-                hideDuration: 2000,
-                kind: SnackbarKind.SUCCESS
-            }));
-        }
-
         render() {
             if (this.props.isInaccessible) {
                 return (
@@ -217,13 +208,7 @@ export const UserProfilePanelRoot = withStyles(styles)(
                                 <Grid item xs={11}>
                                     <Typography className={this.props.classes.title}>
                                         {this.props.userUuid}
-                                        <Tooltip title="Copy to clipboard">
-                                            <span className={this.props.classes.copyIcon}>
-                                                <CopyToClipboard text={this.props.userUuid || ""} onCopy={() => this.onCopy!("Copied")}>
-                                                    <CopyIcon />
-                                                </CopyToClipboard>
-                                            </span>
-                                        </Tooltip>
+                                        <CopyToClipboardSnackbar value={this.props.userUuid} />
                                     </Typography>
                                 </Grid>
                                 <Grid item xs={1} style={{ textAlign: "right" }}>

commit 352e8d13baae99e7f4e9f1b88253d6988541fecb
Author: Stephen Smith <stephen at curii.com>
Date:   Tue Mar 29 23:32:11 2022 -0400

    18559: Re-use account status renderer code for group details panel
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/store/users/users-actions.ts b/src/store/users/users-actions.ts
index e3c965bd..d74e05ee 100644
--- a/src/store/users/users-actions.ts
+++ b/src/store/users/users-actions.ts
@@ -133,21 +133,6 @@ export const openUserPanel = () =>
         }
     };
 
-export const toggleIsActive = (uuid: string) =>
-    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const { resources } = getState();
-        const data = getResource<UserResource>(uuid)(resources);
-        const isActive = data!.isActive;
-        let newActivity;
-        if (isActive) {
-            newActivity = await services.userService.unsetup(uuid);
-        } else {
-            newActivity = await services.userService.update(uuid, { isActive: true });
-        }
-        dispatch<any>(loadUsersPanel());
-        return newActivity;
-    };
-
 export const toggleIsAdmin = (uuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const { resources } = getState();
diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx
index 70508628..51b7dcc8 100644
--- a/src/views-components/data-explorer/renderers.tsx
+++ b/src/views-components/data-explorer/renderers.tsx
@@ -35,7 +35,7 @@ import { ResourceStatus as WorkflowStatus } from 'views/workflow-panel/workflow-
 import { getUuidPrefix, openRunProcess } from 'store/workflow-panel/workflow-panel-actions';
 import { openSharingDialog } from 'store/sharing-dialog/sharing-dialog-actions';
 import { getUserFullname, getUserDisplayName, User, UserResource } from 'models/user';
-import { toggleIsActive, toggleIsAdmin } from 'store/users/users-actions';
+import { toggleIsAdmin } from 'store/users/users-actions';
 import { LinkClass, LinkResource } from 'models/link';
 import { navigateTo, navigateToGroupDetails, navigateToUserProfile } from 'store/navigation/navigation-action';
 import { withResourceData } from 'views-components/data-explorer/with-resources';
@@ -211,33 +211,11 @@ export const ResourceEmail = connect(
         return resource || { email: '' };
     })(renderEmail);
 
-const renderIsActive = (props: { uuid: string, kind: ResourceKind, isActive: boolean, toggleIsActive: (uuid: string) => void, disabled?: boolean }) => {
-    if (props.kind === ResourceKind.USER) {
-        return <Checkbox
-            color="primary"
-            checked={props.isActive}
-            disabled={!!props.disabled}
-            onClick={(e) => {
-                e.stopPropagation();
-                props.toggleIsActive(props.uuid)
-            }} />;
-    } else {
-        return <Typography />;
-    }
-}
-
-export const ResourceIsActive = connect(
-    (state: RootState, props: { uuid: string, disabled?: boolean }) => {
-        const resource = getResource<UserResource>(props.uuid)(state.resources);
-        return resource ? {...resource, disabled: !!props.disabled} : { isActive: false, kind: ResourceKind.NONE };
-    }, { toggleIsActive }
-)(renderIsActive);
-
 enum UserAccountStatus {
     ACTIVE = 'Active',
     INACTIVE = 'Inactive',
     SETUP = 'Setup',
-    UNKNOWN = 'UNKNOWN'
+    UNKNOWN = ''
 }
 
 const renderAccountStatus = (props: {status: UserAccountStatus}) =>
@@ -252,7 +230,7 @@ const renderAccountStatus = (props: {status: UserAccountStatus}) =>
                     case UserAccountStatus.INACTIVE:
                         return <InactiveIcon style={{color: '#9e9e9e'}} />;
                     default:
-                        return <InactiveIcon />;
+                        return <></>;
                 }
             })()}
         </Grid>
@@ -263,33 +241,31 @@ const renderAccountStatus = (props: {status: UserAccountStatus}) =>
         </Grid>
     </Grid>;
 
-export const UserResourceAccountStatus = connect(
-    (state: RootState, props: { uuid: string }) => {
-        const user = getResource<UserResource>(props.uuid)(state.resources);
-        // Get membership links for all users group
-        const allUsersGroupUuid = getBuiltinGroupUuid(state.auth.localCluster, BuiltinGroups.ALL);
-        const permissions = filterResources((resource: LinkResource) =>
-            resource.kind === ResourceKind.LINK &&
-            resource.linkClass === LinkClass.PERMISSION &&
-            resource.headUuid === allUsersGroupUuid &&
-            resource.tailUuid === props.uuid
-        )(state.resources);
-
-        if (user) {
-            return user.isActive ? {status: UserAccountStatus.ACTIVE} : permissions.length > 0 ? {status: UserAccountStatus.SETUP} : {status: UserAccountStatus.INACTIVE};
-        } else {
-            return {status: UserAccountStatus.UNKNOWN};
-        }
-    })(renderAccountStatus);
+const getUserAccountStatus = (state: RootState, props: { uuid: string }) => {
+    const user = getResource<UserResource>(props.uuid)(state.resources);
+    // Get membership links for all users group
+    const allUsersGroupUuid = getBuiltinGroupUuid(state.auth.localCluster, BuiltinGroups.ALL);
+    const permissions = filterResources((resource: LinkResource) =>
+        resource.kind === ResourceKind.LINK &&
+        resource.linkClass === LinkClass.PERMISSION &&
+        resource.headUuid === allUsersGroupUuid &&
+        resource.tailUuid === props.uuid
+    )(state.resources);
+
+    if (user) {
+        return user.isActive ? {status: UserAccountStatus.ACTIVE} : permissions.length > 0 ? {status: UserAccountStatus.SETUP} : {status: UserAccountStatus.INACTIVE};
+    } else {
+        return {status: UserAccountStatus.UNKNOWN};
+    }
+}
 
-export const ResourceLinkTailIsActive = connect(
-    (state: RootState, props: { uuid: string, disabled?: boolean }) => {
+export const ResourceLinkTailAccountStatus = connect(
+    (state: RootState, props: { uuid: string }) => {
         const link = getResource<LinkResource>(props.uuid)(state.resources);
-        const tailResource = getResource<UserResource>(link?.tailUuid || '')(state.resources);
+        return link && link.tailKind === ResourceKind.USER ? getUserAccountStatus(state, {uuid: link.tailUuid}) : {status: UserAccountStatus.UNKNOWN};
+    })(renderAccountStatus);
 
-        return tailResource ? {...tailResource, disabled: !!props.disabled} : { isActive: false, kind: ResourceKind.NONE };
-    }, { toggleIsActive }
-)(renderIsActive);
+export const UserResourceAccountStatus = connect(getUserAccountStatus)(renderAccountStatus);
 
 const renderIsHidden = (props: {
                             memberLinkUuid: string,
diff --git a/src/views/group-details-panel/group-details-panel.tsx b/src/views/group-details-panel/group-details-panel.tsx
index ce3f34c7..9cee3cbc 100644
--- a/src/views/group-details-panel/group-details-panel.tsx
+++ b/src/views/group-details-panel/group-details-panel.tsx
@@ -7,7 +7,7 @@ import { connect } from 'react-redux';
 
 import { DataExplorer } from "views-components/data-explorer/data-explorer";
 import { DataColumns } from 'components/data-table/data-table';
-import { ResourceLinkHeadUuid, ResourceLinkTailUsername, ResourceLinkHeadPermissionLevel, ResourceLinkTailPermissionLevel, ResourceLinkHead, ResourceLinkTail, ResourceLinkDelete, ResourceLinkTailIsActive, ResourceLinkTailIsVisible } from 'views-components/data-explorer/renderers';
+import { ResourceLinkHeadUuid, ResourceLinkTailUsername, ResourceLinkHeadPermissionLevel, ResourceLinkTailPermissionLevel, ResourceLinkHead, ResourceLinkTail, ResourceLinkDelete, ResourceLinkTailAccountStatus, ResourceLinkTailIsVisible } from 'views-components/data-explorer/renderers';
 import { createTree } from 'models/tree';
 import { noop } from 'lodash/fp';
 import { RootState } from 'store/store';
@@ -31,7 +31,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 export enum GroupDetailsPanelMembersColumnNames {
     FULL_NAME = "Name",
     USERNAME = "Username",
-    ACTIVE = "User Active",
+    STATUS = "Account Status",
     VISIBLE = "Visible to other members",
     PERMISSION = "Permission",
     REMOVE = "Remove",
@@ -60,11 +60,11 @@ export const groupDetailsMembersPanelColumns: DataColumns<string> = [
         render: uuid => <ResourceLinkTailUsername uuid={uuid} />
     },
     {
-        name: GroupDetailsPanelMembersColumnNames.ACTIVE,
+        name: GroupDetailsPanelMembersColumnNames.STATUS,
         selected: true,
         configurable: true,
         filters: createTree(),
-        render: uuid => <ResourceLinkTailIsActive uuid={uuid} disabled={true} />
+        render: uuid => <ResourceLinkTailAccountStatus uuid={uuid} />
     },
     {
         name: GroupDetailsPanelMembersColumnNames.VISIBLE,

commit 5cefad212822a48c83af1d38cbe14368c0cb1a20
Author: Stephen Smith <stephen at curii.com>
Date:   Tue Mar 29 22:44:06 2022 -0400

    18559: Add tri-state account status indicator and associated context menu actions
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/components/icon/icon.tsx b/src/components/icon/icon.tsx
index 20ce62c1..4d17dd28 100644
--- a/src/components/icon/icon.tsx
+++ b/src/components/icon/icon.tsx
@@ -66,8 +66,10 @@ import LinkOutlined from '@material-ui/icons/LinkOutlined';
 import RemoveRedEye from '@material-ui/icons/RemoveRedEye';
 import Computer from '@material-ui/icons/Computer';
 import CropFreeSharp from '@material-ui/icons/CropFreeSharp';
-import Cancel from '@material-ui/icons/Cancel';
 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';
@@ -175,5 +177,8 @@ export const CanReadIcon: IconType = (props) => <RemoveRedEye {...props} />;
 export const CanWriteIcon: IconType = (props) => <Edit {...props} />;
 export const CanManageIcon: IconType = (props) => <Computer {...props} />;
 export const AddUserIcon: IconType = (props) => <PersonAdd {...props} />;
-export const DeactivateUserIcon: IconType = (props) => <Cancel {...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} />;
diff --git a/src/models/group.ts b/src/models/group.ts
index 3f3656cc..f6a72c53 100644
--- a/src/models/group.ts
+++ b/src/models/group.ts
@@ -25,14 +25,18 @@ export enum GroupClass {
     ROLE  = 'role',
 }
 
-export const BUILTIN_GROUP_IDS = [
-    'fffffffffffffff',
-    'anonymouspublic',
-    '000000000000000',
-]
+export enum BuiltinGroups {
+    ALL = 'fffffffffffffff',
+    ANON = 'anonymouspublic',
+    SYSTEM = '000000000000000',
+}
+
+export const getBuiltinGroupUuid = (cluster: string, groupName: BuiltinGroups): string => {
+    return cluster ? `${cluster}-${ResourceObjectType.GROUP}-${groupName}` : "";
+};
 
 export const isBuiltinGroup = (uuid: string) => {
     const match = RESOURCE_UUID_REGEX.exec(uuid);
     const parts = match ? match[0].split('-') : [];
-    return parts.length === 3 && parts[1] === ResourceObjectType.GROUP && BUILTIN_GROUP_IDS.includes(parts[2]);
+    return parts.length === 3 && parts[1] === ResourceObjectType.GROUP && Object.values<string>(BuiltinGroups).includes(parts[2]);
 };
diff --git a/src/services/user-service/user-service.ts b/src/services/user-service/user-service.ts
index 97cb3c71..8581b267 100644
--- a/src/services/user-service/user-service.ts
+++ b/src/services/user-service/user-service.ts
@@ -18,7 +18,7 @@ export class UserService extends CommonResourceService<UserResource> {
     }
 
     activate(uuid: string) {
-        return CommonResourceService.defaultResponse(
+        return CommonResourceService.defaultResponse<UserResource>(
             this.serverApi
                 .post(this.resourceType + `/${uuid}/activate`),
             this.actions
diff --git a/src/store/auth/auth-action.ts b/src/store/auth/auth-action.ts
index d58a8103..7fc9df77 100644
--- a/src/store/auth/auth-action.ts
+++ b/src/store/auth/auth-action.ts
@@ -80,6 +80,10 @@ export const getConfig = (dispatch: Dispatch, getState: () => RootState, service
     return state.remoteHostsConfig[state.localCluster];
 };
 
+export const getLocalCluster = (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): string => {
+    return getState().auth.localCluster;
+};
+
 export const saveApiToken = (token: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
     let config: any;
     const tokenParts = token.split('/');
diff --git a/src/store/user-profile/user-profile-actions.ts b/src/store/user-profile/user-profile-actions.ts
index 0fdeef9e..4d3d34aa 100644
--- a/src/store/user-profile/user-profile-actions.ts
+++ b/src/store/user-profile/user-profile-actions.ts
@@ -9,13 +9,18 @@ import { bindDataExplorerActions } from "store/data-explorer/data-explorer-actio
 import { propertiesActions } from 'store/properties/properties-actions';
 import { getProperty } from 'store/properties/properties';
 import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
-import { updateResources } from "store/resources/resources-actions";
+import { deleteResources, updateResources } from "store/resources/resources-actions";
 import { dialogActions } from "store/dialog/dialog-actions";
+import { filterResources } from "store/resources/resources";
+import { ResourceKind } from "models/resource";
+import { LinkClass, LinkResource } from "models/link";
+import { BuiltinGroups, getBuiltinGroupUuid } from "models/group";
 
 export const USER_PROFILE_PANEL_ID = 'userProfilePanel';
 export const USER_PROFILE_FORM = 'userProfileForm';
 export const DEACTIVATE_DIALOG = 'deactivateDialog';
 export const SETUP_DIALOG = 'setupDialog';
+export const ACTIVATE_DIALOG = 'activateDialog';
 export const IS_PROFILE_INACCESSIBLE = 'isProfileInaccessible';
 
 export const UserProfileGroupsActions = bindDataExplorerActions(USER_PROFILE_PANEL_ID);
@@ -65,60 +70,97 @@ export const saveEditedUser = (resource: any) =>
       }
   };
 
-export const openDeactivateDialog = (uuid: string) =>
+export const openSetupDialog = (uuid: string) =>
   (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
     dispatch(dialogActions.OPEN_DIALOG({
-      id: DEACTIVATE_DIALOG,
+      id: SETUP_DIALOG,
       data: {
-          title: 'Deactivate user',
-          text: 'Are you sure you want to deactivate this user?',
-          confirmButtonLabel: 'Deactvate',
-          uuid
+        title: 'Setup user',
+        text: 'Are you sure you want to setup this user?',
+        confirmButtonLabel: 'Confirm',
+        uuid
       }
-  }));
-}
+    }));
+  };
 
-export const openSetupDialog = (uuid: string) =>
+export const openActivateDialog = (uuid: string) =>
   (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
     dispatch(dialogActions.OPEN_DIALOG({
-      id: SETUP_DIALOG,
+      id: ACTIVATE_DIALOG,
+      data: {
+        title: 'Activate user',
+        text: 'Are you sure you want to activate this user?',
+        confirmButtonLabel: 'Confirm',
+        uuid
+      }
+    }));
+  };
+
+export const openDeactivateDialog = (uuid: string) =>
+  (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+    dispatch(dialogActions.OPEN_DIALOG({
+      id: DEACTIVATE_DIALOG,
       data: {
-          title: 'Setup user',
-          text: 'Are you sure you want to setup this user?',
-          confirmButtonLabel: 'Confirm',
-          uuid
+        title: 'Deactivate user',
+        text: 'Are you sure you want to deactivate this user?',
+        confirmButtonLabel: 'Confirm',
+        uuid
       }
-  }));
-}
+    }));
+  };
 
 export const setup = (uuid: string) =>
-    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        try {
-            const resources = await services.userService.setup(uuid);
-            dispatch(updateResources(resources.items));
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: "User has been setup", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
-        } catch (e) {
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000, kind: SnackbarKind.ERROR }));
-        } finally {
-            dispatch(dialogActions.CLOSE_DIALOG({ id: SETUP_DIALOG }));
-        }
+  async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    try {
+      const resources = await services.userService.setup(uuid);
+      dispatch(updateResources(resources.items));
+      dispatch(snackbarActions.OPEN_SNACKBAR({ message: "User has been setup", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
+    } catch (e) {
+      dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000, kind: SnackbarKind.ERROR }));
+    } finally {
+      dispatch(dialogActions.CLOSE_DIALOG({ id: SETUP_DIALOG }));
+    }
+  };
 
-    };
+export const activate = (uuid: string) =>
+  async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    try {
+      const user = await services.userService.activate(uuid);
+      dispatch(updateResources([user]));
+      dispatch(snackbarActions.OPEN_SNACKBAR({ message: "User has been activated", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
+    } catch (e) {
+      dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000, kind: SnackbarKind.ERROR }));
+    }
+  };
 
-export const unsetup = (uuid: string) =>
-    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        try {
-          const user = await services.userService.unsetup(uuid);
-          dispatch(updateResources([user]));
-          dispatch(snackbarActions.OPEN_SNACKBAR({
-              message: "User has been deactivated.",
-              hideDuration: 2000,
-              kind: SnackbarKind.SUCCESS
-          }));
-        } catch (e) {
-          dispatch(snackbarActions.OPEN_SNACKBAR({
-              message: "Could not deactivate user",
-              kind: SnackbarKind.ERROR,
-          }));
-        }
-    };
+export const deactivate = (uuid: string) =>
+  async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    try {
+      const { resources, auth } = getState();
+      // Call unsetup
+      const user = await services.userService.unsetup(uuid);
+      dispatch(updateResources([user]));
+
+      // Find and remove all users membership
+      const allUsersGroupUuid = getBuiltinGroupUuid(auth.localCluster, BuiltinGroups.ALL);
+      const memberships = filterResources((resource: LinkResource) =>
+          resource.kind === ResourceKind.LINK &&
+          resource.linkClass === LinkClass.PERMISSION &&
+          resource.headUuid === allUsersGroupUuid &&
+          resource.tailUuid === uuid
+      )(resources);
+      // Remove all users membership locally
+      dispatch<any>(deleteResources(memberships.map(link => link.uuid)));
+
+      dispatch(snackbarActions.OPEN_SNACKBAR({
+        message: "User has been deactivated.",
+        hideDuration: 2000,
+        kind: SnackbarKind.SUCCESS
+      }));
+    } catch (e) {
+      dispatch(snackbarActions.OPEN_SNACKBAR({
+        message: "Could not deactivate user",
+        kind: SnackbarKind.ERROR,
+      }));
+    }
+  };
diff --git a/src/store/users/user-panel-middleware-service.ts b/src/store/users/user-panel-middleware-service.ts
index 2a742353..83af302c 100644
--- a/src/store/users/user-panel-middleware-service.ts
+++ b/src/store/users/user-panel-middleware-service.ts
@@ -17,6 +17,8 @@ import { userBindedActions } from 'store/users/users-actions';
 import { getSortColumn } from "store/data-explorer/data-explorer-reducer";
 import { UserResource } from 'models/user';
 import { UserPanelColumnNames } from 'views/user-panel/user-panel';
+import { BuiltinGroups, getBuiltinGroupUuid } from 'models/group';
+import { LinkClass } from 'models/link';
 
 export class UserMiddlewareService extends DataExplorerMiddlewareService {
     constructor(private services: ServiceRepository, id: string) {
@@ -36,6 +38,15 @@ export class UserMiddlewareService extends DataExplorerMiddlewareService {
                 api.dispatch(updateResources(responseLastName.items));
                 api.dispatch(setItems(responseLastName));
             }
+            // Get "all users" group memberships
+            const allUsersGroupUuid = getBuiltinGroupUuid(state.auth.localCluster, BuiltinGroups.ALL);
+            const allUserMemberships = await this.services.permissionService.list({
+                filters: new FilterBuilder()
+                    .addEqual('head_uuid', allUsersGroupUuid)
+                    .addEqual('link_class', LinkClass.PERMISSION)
+                    .getFilters()
+            });
+            api.dispatch(updateResources(allUserMemberships.items));
         } catch {
             api.dispatch(couldNotFetchUsers());
         }
diff --git a/src/views-components/context-menu/action-sets/user-action-set.ts b/src/views-components/context-menu/action-sets/user-action-set.ts
index 18426a0d..b01516e2 100644
--- a/src/views-components/context-menu/action-sets/user-action-set.ts
+++ b/src/views-components/context-menu/action-sets/user-action-set.ts
@@ -11,10 +11,11 @@ import {
     UserPanelIcon,
     LoginAsIcon,
     AdminMenuIcon,
+    ActiveIcon,
 } from "components/icon/icon";
 import { openAdvancedTabDialog } from 'store/advanced-tab/advanced-tab';
 import { loginAs, openUserAttributes, openUserProjects } from "store/users/users-actions";
-import { openSetupDialog, openDeactivateDialog } from "store/user-profile/user-profile-actions";
+import { openSetupDialog, openDeactivateDialog, openActivateDialog } from "store/user-profile/user-profile-actions";
 import { navigateToUserProfile } from "store/navigation/navigation-action";
 
 export const userActionSet: ContextMenuActionSet = [[{
@@ -37,12 +38,17 @@ export const userActionSet: ContextMenuActionSet = [[{
     }
 }, {
     name: "Account Settings",
-    adminOnly: true,
     icon: UserPanelIcon,
     execute: (dispatch, { uuid }) => {
         dispatch<any>(navigateToUserProfile(uuid));
     }
-}, {
+},], [{
+    name: "Activate User",
+    icon: ActiveIcon,
+    execute: (dispatch, { uuid }) => {
+        dispatch<any>(openActivateDialog(uuid));
+    }
+},{
     name: "Setup User",
     adminOnly: true,
     icon: AdminMenuIcon,
diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx
index 5068355b..70508628 100644
--- a/src/views-components/data-explorer/renderers.tsx
+++ b/src/views-components/data-explorer/renderers.tsx
@@ -6,7 +6,21 @@ import React from 'react';
 import { Grid, Typography, withStyles, Tooltip, IconButton, Checkbox } from '@material-ui/core';
 import { FavoriteStar, PublicFavoriteStar } from '../favorite-star/favorite-star';
 import { Resource, ResourceKind, TrashableResource } from 'models/resource';
-import { ProjectIcon, FilterGroupIcon, CollectionIcon, ProcessIcon, DefaultIcon, ShareIcon, CollectionOldVersionIcon, WorkflowIcon, RemoveIcon, RenameIcon } from 'components/icon/icon';
+import {
+    ProjectIcon,
+    FilterGroupIcon,
+    CollectionIcon,
+    ProcessIcon,
+    DefaultIcon,
+    ShareIcon,
+    CollectionOldVersionIcon,
+    WorkflowIcon,
+    RemoveIcon,
+    RenameIcon,
+    ActiveIcon,
+    SetupIcon,
+    InactiveIcon,
+} from 'components/icon/icon';
 import { formatDate, formatFileSize, formatTime } from 'common/formatters';
 import { resourceLabel } from 'common/labels';
 import { connect, DispatchProp } from 'react-redux';
@@ -28,7 +42,7 @@ import { withResourceData } from 'views-components/data-explorer/with-resources'
 import { CollectionResource } from 'models/collection';
 import { IllegalNamingWarning } from 'components/warning/warning';
 import { loadResource } from 'store/resources/resources-actions';
-import { GroupClass, GroupResource, isBuiltinGroup } from 'models/group';
+import { BuiltinGroups, getBuiltinGroupUuid, GroupClass, GroupResource, isBuiltinGroup } from 'models/group';
 import { openRemoveGroupMemberDialog } from 'store/group-details-panel/group-details-panel-actions';
 import { setMemberIsHidden } from 'store/group-details-panel/group-details-panel-actions';
 import { formatPermissionLevel } from 'views-components/sharing-dialog/permission-select';
@@ -219,6 +233,55 @@ export const ResourceIsActive = connect(
     }, { toggleIsActive }
 )(renderIsActive);
 
+enum UserAccountStatus {
+    ACTIVE = 'Active',
+    INACTIVE = 'Inactive',
+    SETUP = 'Setup',
+    UNKNOWN = 'UNKNOWN'
+}
+
+const renderAccountStatus = (props: {status: UserAccountStatus}) =>
+    <Grid container alignItems="center" wrap="nowrap" spacing={8}>
+        <Grid item>
+            {(() => {
+                switch(props.status) {
+                    case UserAccountStatus.ACTIVE:
+                        return <ActiveIcon style={{color: '#4caf50'}} />;
+                    case UserAccountStatus.SETUP:
+                        return <SetupIcon style={{color: '#2196f3'}} />;
+                    case UserAccountStatus.INACTIVE:
+                        return <InactiveIcon style={{color: '#9e9e9e'}} />;
+                    default:
+                        return <InactiveIcon />;
+                }
+            })()}
+        </Grid>
+        <Grid item>
+            <Typography noWrap>
+                {props.status}
+            </Typography>
+        </Grid>
+    </Grid>;
+
+export const UserResourceAccountStatus = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const user = getResource<UserResource>(props.uuid)(state.resources);
+        // Get membership links for all users group
+        const allUsersGroupUuid = getBuiltinGroupUuid(state.auth.localCluster, BuiltinGroups.ALL);
+        const permissions = filterResources((resource: LinkResource) =>
+            resource.kind === ResourceKind.LINK &&
+            resource.linkClass === LinkClass.PERMISSION &&
+            resource.headUuid === allUsersGroupUuid &&
+            resource.tailUuid === props.uuid
+        )(state.resources);
+
+        if (user) {
+            return user.isActive ? {status: UserAccountStatus.ACTIVE} : permissions.length > 0 ? {status: UserAccountStatus.SETUP} : {status: UserAccountStatus.INACTIVE};
+        } else {
+            return {status: UserAccountStatus.UNKNOWN};
+        }
+    })(renderAccountStatus);
+
 export const ResourceLinkTailIsActive = connect(
     (state: RootState, props: { uuid: string, disabled?: boolean }) => {
         const link = getResource<LinkResource>(props.uuid)(state.resources);
diff --git a/src/views-components/user-dialog/deactivate-dialog.tsx b/src/views-components/user-dialog/activate-dialog.tsx
similarity index 72%
copy from src/views-components/user-dialog/deactivate-dialog.tsx
copy to src/views-components/user-dialog/activate-dialog.tsx
index 6babf367..79e83303 100644
--- a/src/views-components/user-dialog/deactivate-dialog.tsx
+++ b/src/views-components/user-dialog/activate-dialog.tsx
@@ -6,16 +6,16 @@ import { Dispatch, compose } from 'redux';
 import { connect } from "react-redux";
 import { ConfirmationDialog } from "components/confirmation-dialog/confirmation-dialog";
 import { withDialog, WithDialogProps } from "store/dialog/with-dialog";
-import { unsetup, DEACTIVATE_DIALOG } from 'store/user-profile/user-profile-actions';
+import { activate, ACTIVATE_DIALOG } from 'store/user-profile/user-profile-actions';
 
 const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps<any>) => ({
     onConfirm: () => {
         props.closeDialog();
-        dispatch<any>(unsetup(props.data.uuid));
+        dispatch<any>(activate(props.data.uuid));
     }
 });
 
-export const DeactivateDialog = compose(
-    withDialog(DEACTIVATE_DIALOG),
+export const ActivateDialog = compose(
+    withDialog(ACTIVATE_DIALOG),
     connect(null, mapDispatchToProps)
 )(ConfirmationDialog);
diff --git a/src/views-components/user-dialog/deactivate-dialog.tsx b/src/views-components/user-dialog/deactivate-dialog.tsx
index 6babf367..8aefa929 100644
--- a/src/views-components/user-dialog/deactivate-dialog.tsx
+++ b/src/views-components/user-dialog/deactivate-dialog.tsx
@@ -6,12 +6,12 @@ import { Dispatch, compose } from 'redux';
 import { connect } from "react-redux";
 import { ConfirmationDialog } from "components/confirmation-dialog/confirmation-dialog";
 import { withDialog, WithDialogProps } from "store/dialog/with-dialog";
-import { unsetup, DEACTIVATE_DIALOG } from 'store/user-profile/user-profile-actions';
+import { deactivate, DEACTIVATE_DIALOG } from 'store/user-profile/user-profile-actions';
 
 const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps<any>) => ({
     onConfirm: () => {
         props.closeDialog();
-        dispatch<any>(unsetup(props.data.uuid));
+        dispatch<any>(deactivate(props.data.uuid));
     }
 });
 
diff --git a/src/views/user-panel/user-panel.tsx b/src/views/user-panel/user-panel.tsx
index 169b32ab..589353cd 100644
--- a/src/views/user-panel/user-panel.tsx
+++ b/src/views/user-panel/user-panel.tsx
@@ -15,9 +15,9 @@ import {
     UserResourceFullName,
     ResourceUuid,
     ResourceEmail,
-    ResourceIsActive,
     ResourceIsAdmin,
-    ResourceUsername
+    ResourceUsername,
+    UserResourceAccountStatus,
 } from "views-components/data-explorer/renderers";
 import { navigateToUserProfile } from "store/navigation/navigation-action";
 import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
@@ -46,7 +46,7 @@ export enum UserPanelColumnNames {
     NAME = "Name",
     UUID = "Uuid",
     EMAIL = "Email",
-    ACTIVE = "Active",
+    STATUS = "Account Status",
     ADMIN = "Admin",
     REDIRECT_TO_USER = "Redirect to user",
     USERNAME = "Username"
@@ -78,11 +78,11 @@ export const userPanelColumns: DataColumns<string> = [
         render: uuid => <ResourceEmail uuid={uuid} />
     },
     {
-        name: UserPanelColumnNames.ACTIVE,
+        name: UserPanelColumnNames.STATUS,
         selected: true,
         configurable: true,
         filters: createTree(),
-        render: uuid => <ResourceIsActive uuid={uuid} />
+        render: uuid => <UserResourceAccountStatus uuid={uuid} />
     },
     {
         name: UserPanelColumnNames.ADMIN,
diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx
index 11e038f5..0d1a8950 100644
--- a/src/views/workbench/workbench.tsx
+++ b/src/views/workbench/workbench.tsx
@@ -81,6 +81,7 @@ import { UserAttributesDialog } from 'views-components/user-dialog/attributes-di
 import { CreateUserDialog } from 'views-components/dialog-forms/create-user-dialog';
 import { HelpApiClientAuthorizationDialog } from 'views-components/api-client-authorizations-dialog/help-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';
@@ -268,6 +269,7 @@ export const WorkbenchPanel =
             <UpdateProjectDialog />
             <UserAttributesDialog />
             <DeactivateDialog />
+            <ActivateDialog />
             <SetupDialog />
             <VirtualMachineAttributesDialog />
             <FedLogin />

commit d839ac44e90bfca5e07e8cc5ddf56c30deabc008
Author: Stephen Smith <stephen at curii.com>
Date:   Tue Mar 29 09:17:44 2022 -0400

    18559: Add empty user profile page for 404
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/store/user-profile/user-profile-actions.ts b/src/store/user-profile/user-profile-actions.ts
index c8556862..0fdeef9e 100644
--- a/src/store/user-profile/user-profile-actions.ts
+++ b/src/store/user-profile/user-profile-actions.ts
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 import { RootState } from "store/store";
 import { Dispatch } from 'redux';
-import { initialize } from "redux-form";
+import { initialize, reset } from "redux-form";
 import { ServiceRepository } from "services/services";
 import { bindDataExplorerActions } from "store/data-explorer/data-explorer-action";
 import { propertiesActions } from 'store/properties/properties-actions';
@@ -16,21 +16,37 @@ export const USER_PROFILE_PANEL_ID = 'userProfilePanel';
 export const USER_PROFILE_FORM = 'userProfileForm';
 export const DEACTIVATE_DIALOG = 'deactivateDialog';
 export const SETUP_DIALOG = 'setupDialog';
+export const IS_PROFILE_INACCESSIBLE = 'isProfileInaccessible';
 
 export const UserProfileGroupsActions = bindDataExplorerActions(USER_PROFILE_PANEL_ID);
 
 export const getCurrentUserProfilePanelUuid = getProperty<string>(USER_PROFILE_PANEL_ID);
+export const getUserProfileIsInaccessible = getProperty<boolean>(IS_PROFILE_INACCESSIBLE);
 
 export const loadUserProfilePanel = (userUuid?: string) =>
   async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    // Reset isInacessible to ensure error screen is hidden
+    dispatch(propertiesActions.SET_PROPERTY({ key: IS_PROFILE_INACCESSIBLE, value: false }));
     // Get user uuid from route or use current user uuid
     const uuid = userUuid || getState().auth.user?.uuid;
     if (uuid) {
       await dispatch(propertiesActions.SET_PROPERTY({ key: USER_PROFILE_PANEL_ID, value: uuid }));
-      const user = await services.userService.get(uuid);
-      dispatch(initialize(USER_PROFILE_FORM, user));
-      dispatch(updateResources([user]));
-      dispatch(UserProfileGroupsActions.REQUEST_ITEMS());
+      try {
+        const user = await services.userService.get(uuid, false);
+        dispatch(initialize(USER_PROFILE_FORM, user));
+        dispatch(updateResources([user]));
+        dispatch(UserProfileGroupsActions.REQUEST_ITEMS());
+      } catch (e) {
+        if (e.status === 404) {
+          await dispatch(propertiesActions.SET_PROPERTY({ key: IS_PROFILE_INACCESSIBLE, value: true }));
+          dispatch(reset(USER_PROFILE_FORM));
+        } else {
+          dispatch(snackbarActions.OPEN_SNACKBAR({
+            message: 'Could not load user profile',
+            kind: SnackbarKind.ERROR
+          }));
+        }
+      }
     }
   }
 
diff --git a/src/views/user-profile-panel/user-profile-panel-root.tsx b/src/views/user-profile-panel/user-profile-panel-root.tsx
index 0beeab4f..f89ca5c5 100644
--- a/src/views/user-profile-panel/user-profile-panel-root.tsx
+++ b/src/views/user-profile-panel/user-profile-panel-root.tsx
@@ -28,23 +28,26 @@ import { DataTableDefaultView } from 'components/data-table-default-view/data-ta
 import { PROFILE_EMAIL_VALIDATION } from "validators/validators";
 import { USER_PROFILE_PANEL_ID } from 'store/user-profile/user-profile-actions';
 import { noop } from 'lodash';
-import { CopyIcon, GroupsIcon, MoreOptionsIcon } from 'components/icon/icon';
+import { CopyIcon, DetailsIcon, GroupsIcon, MoreOptionsIcon } from 'components/icon/icon';
 import { DataColumns } from 'components/data-table/data-table';
 import { ResourceLinkHeadUuid, ResourceLinkHeadPermissionLevel, ResourceLinkHead, ResourceLinkDelete, ResourceLinkTailIsVisible } from 'views-components/data-explorer/renderers';
 import { createTree } from 'models/tree';
 import { getResource, ResourcesState } from 'store/resources/resources';
 import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
 import CopyToClipboard from 'react-copy-to-clipboard';
+import { DefaultView } from 'components/default-view/default-view';
 
-type CssRules = 'root' | 'adminRoot' | 'gridItem' | 'label' | 'readOnlyValue' | 'title' | 'description' | 'actions' | 'content' | 'copyIcon';
+type CssRules = 'root' | 'emptyRoot' | 'gridItem' | 'label' | 'readOnlyValue' | 'title' | 'description' | 'actions' | 'content' | 'copyIcon';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
         width: '100%',
         overflow: 'auto'
     },
-    adminRoot: {
-        // ...theme.mixins.gutters()
+    emptyRoot: {
+        width: '100%',
+        overflow: 'auto',
+        padding: theme.spacing.unit * 4,
     },
     gridItem: {
         height: 45,
@@ -91,8 +94,9 @@ export interface UserProfilePanelRootDataProps {
     isSelf: boolean;
     isPristine: boolean;
     isValid: boolean;
+    isInaccessible: boolean;
     userUuid: string;
-    resources: ResourcesState
+    resources: ResourcesState;
     localCluster: string;
 }
 
@@ -193,143 +197,154 @@ export const UserProfilePanelRoot = withStyles(styles)(
         }
 
         render() {
-            return <Paper className={this.props.classes.root}>
-                <Tabs value={this.state.value} onChange={this.handleChange} variant={"fullWidth"}>
-                    <Tab label={TABS.PROFILE} value={TABS.PROFILE} />
-                    <Tab label={TABS.GROUPS} value={TABS.GROUPS} />
-                </Tabs>
-                {this.state.value === TABS.PROFILE &&
-                    <CardContent>
-                        <Grid container justify="space-between">
-                            <Grid item xs={11}>
-                                <Typography className={this.props.classes.title}>
-                                    {this.props.userUuid}
-                                    <Tooltip title="Copy to clipboard">
-                                        <span className={this.props.classes.copyIcon}>
-                                            <CopyToClipboard text={this.props.userUuid || ""} onCopy={() => this.onCopy!("Copied")}>
-                                                <CopyIcon />
-                                            </CopyToClipboard>
-                                        </span>
-                                    </Tooltip>
-                                </Typography>
-                            </Grid>
-                            <Grid item xs={1} style={{ textAlign: "right" }}>
-                                <Tooltip title="Actions" disableFocusListener>
-                                    <IconButton
-                                        data-cy='collection-panel-options-btn'
-                                        aria-label="Actions"
-                                        onClick={(event) => this.handleContextMenu(event, this.props.userUuid)}>
-                                        <MoreOptionsIcon />
-                                    </IconButton>
-                                </Tooltip>
-                            </Grid>
-                        </Grid>
-                        <form onSubmit={this.props.handleSubmit} data-cy="profile-form">
-                            <Grid container spacing={24}>
-                                <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="firstName">
-                                    <Field
-                                        label="First name"
-                                        name="firstName"
-                                        component={ReadOnlyField as any}
-                                        disabled
-                                    />
-                                </Grid>
-                                <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="lastName">
-                                    <Field
-                                        label="Last name"
-                                        name="lastName"
-                                        component={ReadOnlyField as any}
-                                        disabled
-                                    />
-                                </Grid>
-                                <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="email">
-                                    <Field
-                                        label="E-mail"
-                                        name="email"
-                                        component={ReadOnlyField as any}
-                                        disabled
-                                    />
+            if (this.props.isInaccessible) {
+                return (
+                    <Paper className={this.props.classes.emptyRoot}>
+                        <CardContent>
+                            <DefaultView icon={DetailsIcon} messages={['This user does not exist or your account does not have permission to view it']} />
+                        </CardContent>
+                    </Paper>
+                );
+            } else {
+                return <Paper className={this.props.classes.root}>
+                    <Tabs value={this.state.value} onChange={this.handleChange} variant={"fullWidth"}>
+                        <Tab label={TABS.PROFILE} value={TABS.PROFILE} />
+                        <Tab label={TABS.GROUPS} value={TABS.GROUPS} />
+                    </Tabs>
+                    {this.state.value === TABS.PROFILE &&
+                        <CardContent>
+                            <Grid container justify="space-between">
+                                <Grid item xs={11}>
+                                    <Typography className={this.props.classes.title}>
+                                        {this.props.userUuid}
+                                        <Tooltip title="Copy to clipboard">
+                                            <span className={this.props.classes.copyIcon}>
+                                                <CopyToClipboard text={this.props.userUuid || ""} onCopy={() => this.onCopy!("Copied")}>
+                                                    <CopyIcon />
+                                                </CopyToClipboard>
+                                            </span>
+                                        </Tooltip>
+                                    </Typography>
                                 </Grid>
-                                <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="username">
-                                    <Field
-                                        label="Username"
-                                        name="username"
-                                        component={ReadOnlyField as any}
-                                        disabled
-                                    />
-                                </Grid>
-                                <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
-                                    <Field
-                                        label="Organization"
-                                        name="prefs.profile.organization"
-                                        component={TextField as any}
-                                        disabled={!this.props.isAdmin && !this.props.isSelf}
-                                    />
-                                </Grid>
-                                <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
-                                    <Field
-                                        label="E-mail at Organization"
-                                        name="prefs.profile.organization_email"
-                                        component={TextField as any}
-                                        disabled={!this.props.isAdmin && !this.props.isSelf}
-                                        validate={PROFILE_EMAIL_VALIDATION}
-                                    />
-                                </Grid>
-                                <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
-                                    <InputLabel className={this.props.classes.label} htmlFor="prefs.profile.role">Role</InputLabel>
-                                    <Field
-                                        id="prefs.profile.role"
-                                        name="prefs.profile.role"
-                                        component={NativeSelectField as any}
-                                        items={RoleTypes}
-                                        disabled={!this.props.isAdmin && !this.props.isSelf}
-                                    />
-                                </Grid>
-                                <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
-                                    <Field
-                                        label="Website"
-                                        name="prefs.profile.website_url"
-                                        component={TextField as any}
-                                        disabled={!this.props.isAdmin && !this.props.isSelf}
-                                    />
+                                <Grid item xs={1} style={{ textAlign: "right" }}>
+                                    <Tooltip title="Actions" disableFocusListener>
+                                        <IconButton
+                                            data-cy='collection-panel-options-btn'
+                                            aria-label="Actions"
+                                            onClick={(event) => this.handleContextMenu(event, this.props.userUuid)}>
+                                            <MoreOptionsIcon />
+                                        </IconButton>
+                                    </Tooltip>
                                 </Grid>
-                                <Grid item sm={12}>
-                                    <Grid container direction="row" justify="flex-end">
-                                        <Button color="primary" onClick={this.props.reset} disabled={this.props.isPristine}>Discard changes</Button>
-                                        <Button
-                                            color="primary"
-                                            variant="contained"
-                                            type="submit"
-                                            disabled={this.props.isPristine || this.props.invalid || this.props.submitting}>
-                                            Save changes
-                                        </Button>
+                            </Grid>
+                            <form onSubmit={this.props.handleSubmit} data-cy="profile-form">
+                                <Grid container spacing={24}>
+                                    <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="firstName">
+                                        <Field
+                                            label="First name"
+                                            name="firstName"
+                                            component={ReadOnlyField as any}
+                                            disabled
+                                        />
+                                    </Grid>
+                                    <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="lastName">
+                                        <Field
+                                            label="Last name"
+                                            name="lastName"
+                                            component={ReadOnlyField as any}
+                                            disabled
+                                        />
+                                    </Grid>
+                                    <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="email">
+                                        <Field
+                                            label="E-mail"
+                                            name="email"
+                                            component={ReadOnlyField as any}
+                                            disabled
+                                        />
+                                    </Grid>
+                                    <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="username">
+                                        <Field
+                                            label="Username"
+                                            name="username"
+                                            component={ReadOnlyField as any}
+                                            disabled
+                                        />
+                                    </Grid>
+                                    <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+                                        <Field
+                                            label="Organization"
+                                            name="prefs.profile.organization"
+                                            component={TextField as any}
+                                            disabled={!this.props.isAdmin && !this.props.isSelf}
+                                        />
+                                    </Grid>
+                                    <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+                                        <Field
+                                            label="E-mail at Organization"
+                                            name="prefs.profile.organization_email"
+                                            component={TextField as any}
+                                            disabled={!this.props.isAdmin && !this.props.isSelf}
+                                            validate={PROFILE_EMAIL_VALIDATION}
+                                        />
+                                    </Grid>
+                                    <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+                                        <InputLabel className={this.props.classes.label} htmlFor="prefs.profile.role">Role</InputLabel>
+                                        <Field
+                                            id="prefs.profile.role"
+                                            name="prefs.profile.role"
+                                            component={NativeSelectField as any}
+                                            items={RoleTypes}
+                                            disabled={!this.props.isAdmin && !this.props.isSelf}
+                                        />
+                                    </Grid>
+                                    <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+                                        <Field
+                                            label="Website"
+                                            name="prefs.profile.website_url"
+                                            component={TextField as any}
+                                            disabled={!this.props.isAdmin && !this.props.isSelf}
+
+                                        />
+                                    </Grid>
+                                    <Grid item sm={12}>
+                                        <Grid container direction="row" justify="flex-end">
+                                            <Button color="primary" onClick={this.props.reset} disabled={this.props.isPristine}>Discard changes</Button>
+                                            <Button
+                                                color="primary"
+                                                variant="contained"
+                                                type="submit"
+                                                disabled={this.props.isPristine || this.props.invalid || this.props.submitting}>
+                                                Save changes
+                                            </Button>
+                                        </Grid>
                                     </Grid>
                                 </Grid>
-                            </Grid>
-                        </form >
-                    </CardContent>
-                }
-                {this.state.value === TABS.GROUPS &&
-                    <div className={this.props.classes.content}>
-                        <DataExplorer
-                                id={USER_PROFILE_PANEL_ID}
-                                data-cy="user-profile-groups-data-explorer"
-                                onRowClick={noop}
-                                onRowDoubleClick={noop}
-                                onContextMenu={noop}
-                                contextMenuColumn={false}
-                                hideColumnSelector
-                                hideSearchInput
-                                paperProps={{
-                                    elevation: 0,
-                                }}
-                                dataTableDefaultView={
-                                    <DataTableDefaultView
-                                        icon={GroupsIcon}
-                                        messages={['Group list is empty.']} />
-                                } />
-                    </div>}
-            </Paper >;
+                            </form >
+                        </CardContent>
+                    }
+                    {this.state.value === TABS.GROUPS &&
+                        <div className={this.props.classes.content}>
+                            <DataExplorer
+                                    id={USER_PROFILE_PANEL_ID}
+                                    data-cy="user-profile-groups-data-explorer"
+                                    onRowClick={noop}
+                                    onRowDoubleClick={noop}
+                                    onContextMenu={noop}
+                                    contextMenuColumn={false}
+                                    hideColumnSelector
+                                    hideSearchInput
+                                    paperProps={{
+                                        elevation: 0,
+                                    }}
+                                    dataTableDefaultView={
+                                        <DataTableDefaultView
+                                            icon={GroupsIcon}
+                                            messages={['Group list is empty.']} />
+                                    } />
+                        </div>}
+                </Paper >;
+            }
         }
 
         handleChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
diff --git a/src/views/user-profile-panel/user-profile-panel.tsx b/src/views/user-profile-panel/user-profile-panel.tsx
index 207a98fe..a90d44a9 100644
--- a/src/views/user-profile-panel/user-profile-panel.tsx
+++ b/src/views/user-profile-panel/user-profile-panel.tsx
@@ -7,7 +7,7 @@ import { compose, Dispatch } from 'redux';
 import { reduxForm, isPristine, isValid } from 'redux-form';
 import { connect } from 'react-redux';
 import { UserResource } from 'models/user';
-import { saveEditedUser } from 'store/user-profile/user-profile-actions';
+import { getUserProfileIsInaccessible, saveEditedUser } from 'store/user-profile/user-profile-actions';
 import { UserProfilePanelRoot, UserProfilePanelRootDataProps } from 'views/user-profile-panel/user-profile-panel-root';
 import { USER_PROFILE_FORM } from "store/user-profile/user-profile-actions";
 import { matchUserProfileRoute } from 'routes/routes';
@@ -23,6 +23,7 @@ const mapStateToProps = (state: RootState): UserProfilePanelRootDataProps => {
     isSelf: state.auth.user!.uuid === uuid,
     isPristine: isPristine(USER_PROFILE_FORM)(state),
     isValid: isValid(USER_PROFILE_FORM)(state),
+    isInaccessible: getUserProfileIsInaccessible(state.properties) || false,
     localCluster: state.auth.localCluster,
     userUuid: uuid,
     resources: state.resources,

commit d5f11bb2fbb20c85e8ef857caedd58adf333e522
Author: Stephen Smith <stephen at curii.com>
Date:   Tue Mar 29 09:16:30 2022 -0400

    18559: Add better error handling for loginAs action
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/store/users/users-actions.ts b/src/store/users/users-actions.ts
index c34c7f44..e3c965bd 100644
--- a/src/store/users/users-actions.ts
+++ b/src/store/users/users-actions.ts
@@ -40,13 +40,35 @@ export const openUserAttributes = (uuid: string) =>
 
 export const loginAs = (uuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const { resources } = getState();
-        const data = getResource<UserResource>(uuid)(resources);
-        const client = await services.apiClientAuthorizationService.create({ ownerUuid: uuid });
-        if (data) {
-            dispatch<any>(authActions.INIT_USER({ user: data, token: getTokenV2(client) }));
-            window.location.reload();
-            dispatch<any>(navigateToRootProject);
+        const userUuid = getUserUuid(getState());
+        if (userUuid === uuid) {
+            dispatch(snackbarActions.OPEN_SNACKBAR({
+                message: 'You are already logged in as this user',
+                kind: SnackbarKind.WARNING
+            }));
+        } else {
+            try {
+                const { resources } = getState();
+                const data = getResource<UserResource>(uuid)(resources);
+                const client = await services.apiClientAuthorizationService.create({ ownerUuid: uuid }, false);
+                if (data) {
+                    dispatch<any>(authActions.INIT_USER({ user: data, token: getTokenV2(client) }));
+                    window.location.reload();
+                    dispatch<any>(navigateToRootProject);
+                }
+            } catch (e) {
+                if (e.status === 403) {
+                    dispatch(snackbarActions.OPEN_SNACKBAR({
+                        message: 'You do not have permission to login as this user',
+                        kind: SnackbarKind.WARNING
+                    }));
+                } else {
+                    dispatch(snackbarActions.OPEN_SNACKBAR({
+                        message: 'Failed to login as this user',
+                        kind: SnackbarKind.ERROR
+                    }));
+                }
+            }
         }
     };
 

commit fa947bf995717dafce322213fabb84fbf5fd6d67
Author: Stephen Smith <stephen at curii.com>
Date:   Tue Mar 29 08:57:43 2022 -0400

    18559: User breadcrumbs fallback to uuid
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/store/breadcrumbs/breadcrumbs-actions.ts b/src/store/breadcrumbs/breadcrumbs-actions.ts
index 69179272..08e1a132 100644
--- a/src/store/breadcrumbs/breadcrumbs-actions.ts
+++ b/src/store/breadcrumbs/breadcrumbs-actions.ts
@@ -137,16 +137,21 @@ export const setUsersBreadcrumbs = () =>
 
 export const setUserProfileBreadcrumbs = (userUuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-
-        const user = getResource<UserResource>(userUuid)(getState().resources);
-
-        const breadcrumbs: ResourceBreadcrumb[] = [
-            { label: USERS_PANEL_LABEL, uuid: USERS_PANEL_LABEL },
-            { label: user ? user.username : (await services.userService.get(userUuid)).username, uuid: userUuid },
-        ];
-
-        dispatch(setBreadcrumbs(breadcrumbs));
-
+        try {
+            const user = getResource<UserResource>(userUuid)(getState().resources)
+                        || await services.userService.get(userUuid, false);
+            const breadcrumbs: ResourceBreadcrumb[] = [
+                { label: USERS_PANEL_LABEL, uuid: USERS_PANEL_LABEL },
+                { label: user ? user.username : userUuid, uuid: userUuid },
+            ];
+            dispatch(setBreadcrumbs(breadcrumbs));
+        } catch (e) {
+            const breadcrumbs: ResourceBreadcrumb[] = [
+                { label: USERS_PANEL_LABEL, uuid: USERS_PANEL_LABEL },
+                { label: userUuid, uuid: userUuid },
+            ];
+            dispatch(setBreadcrumbs(breadcrumbs));
+        }
     };
 
 export const MY_ACCOUNT_PANEL_LABEL = 'My Account';

commit 08f915990ffe4588c076efce4d01e0b3bed2b398
Author: Stephen Smith <stephen at curii.com>
Date:   Tue Mar 29 08:56:33 2022 -0400

    18559: Add adminOnly context menu item flag
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/components/context-menu/context-menu.tsx b/src/components/context-menu/context-menu.tsx
index 36f0903d..cb53edbc 100644
--- a/src/components/context-menu/context-menu.tsx
+++ b/src/components/context-menu/context-menu.tsx
@@ -10,6 +10,7 @@ export interface ContextMenuItem {
     name?: string | React.ComponentType;
     icon?: IconType;
     component?: React.ComponentType<any>;
+    adminOnly?: boolean;
 }
 
 export type ContextMenuItemGroup = ContextMenuItem[];
diff --git a/src/views-components/context-menu/action-sets/user-action-set.ts b/src/views-components/context-menu/action-sets/user-action-set.ts
index 987eb1c8..18426a0d 100644
--- a/src/views-components/context-menu/action-sets/user-action-set.ts
+++ b/src/views-components/context-menu/action-sets/user-action-set.ts
@@ -37,24 +37,28 @@ export const userActionSet: ContextMenuActionSet = [[{
     }
 }, {
     name: "Account Settings",
+    adminOnly: true,
     icon: UserPanelIcon,
     execute: (dispatch, { uuid }) => {
         dispatch<any>(navigateToUserProfile(uuid));
     }
 }, {
     name: "Setup User",
+    adminOnly: true,
     icon: AdminMenuIcon,
     execute: (dispatch, { uuid }) => {
         dispatch<any>(openSetupDialog(uuid));
     }
 }, {
     name: "Deactivate User",
+    adminOnly: true,
     icon: DeactivateUserIcon,
     execute: (dispatch, { uuid }) => {
         dispatch<any>(openDeactivateDialog(uuid));
     }
 }, {
     name: "Login As User",
+    adminOnly: true,
     icon: LoginAsIcon,
     execute: (dispatch, { uuid }) => {
         dispatch<any>(loginAs(uuid));
diff --git a/src/views-components/context-menu/context-menu.tsx b/src/views-components/context-menu/context-menu.tsx
index f2c43ced..09f6e19c 100644
--- a/src/views-components/context-menu/context-menu.tsx
+++ b/src/views-components/context-menu/context-menu.tsx
@@ -14,9 +14,10 @@ import { sortByProperty } from "common/array-utils";
 type DataProps = Pick<ContextMenuProps, "anchorEl" | "items" | "open"> & { resource?: ContextMenuResource };
 const mapStateToProps = (state: RootState): DataProps => {
     const { open, position, resource } = state.contextMenu;
+    const isAdmin = state.auth.user!.isAdmin;
     return {
         anchorEl: resource ? createAnchorAt(position) : undefined,
-        items: getMenuActionSet(resource),
+        items: getMenuActionSet(resource, isAdmin),
         open,
         resource
     };
@@ -59,8 +60,15 @@ export const addMenuActionSet = (name: string, itemSet: ContextMenuActionSet) =>
 };
 
 const emptyActionSet: ContextMenuActionSet = [];
-const getMenuActionSet = (resource?: ContextMenuResource): ContextMenuActionSet => {
-    return resource ? menuActionSets.get(resource.menuKind) || emptyActionSet : emptyActionSet;
+const getMenuActionSet = (resource?: ContextMenuResource, isAdmin?: boolean): ContextMenuActionSet => {
+    if (resource) {
+        return menuActionSets
+            .get(resource.menuKind)!
+            .map((group) => (group.filter((item) => (item.adminOnly ? isAdmin : true))))
+            || emptyActionSet
+    } else {
+        return emptyActionSet;
+    }
 };
 
 export enum ContextMenuKind {

commit 736b2d3cc858cfba93bedf5367f5af6c1852fcfe
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Mar 28 16:37:27 2022 -0400

    18559: Fix wrong action in user profile context action
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views-components/context-menu/action-sets/user-action-set.ts b/src/views-components/context-menu/action-sets/user-action-set.ts
index 0b2ff379..987eb1c8 100644
--- a/src/views-components/context-menu/action-sets/user-action-set.ts
+++ b/src/views-components/context-menu/action-sets/user-action-set.ts
@@ -15,6 +15,7 @@ import {
 import { openAdvancedTabDialog } from 'store/advanced-tab/advanced-tab';
 import { loginAs, openUserAttributes, openUserProjects } from "store/users/users-actions";
 import { openSetupDialog, openDeactivateDialog } from "store/user-profile/user-profile-actions";
+import { navigateToUserProfile } from "store/navigation/navigation-action";
 
 export const userActionSet: ContextMenuActionSet = [[{
     name: "Attributes",
@@ -38,7 +39,7 @@ export const userActionSet: ContextMenuActionSet = [[{
     name: "Account Settings",
     icon: UserPanelIcon,
     execute: (dispatch, { uuid }) => {
-        dispatch<any>(openAdvancedTabDialog(uuid));
+        dispatch<any>(navigateToUserProfile(uuid));
     }
 }, {
     name: "Setup User",

commit d4d0208892ea0f0f4bf79691770b14aca89db92b
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Mar 28 16:12:23 2022 -0400

    18559: Remove dead code
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/store/users/users-actions.ts b/src/store/users/users-actions.ts
index b5d47d70..c34c7f44 100644
--- a/src/store/users/users-actions.ts
+++ b/src/store/users/users-actions.ts
@@ -22,7 +22,6 @@ import { updateResources } from "store/resources/resources-actions";
 export const USERS_PANEL_ID = 'usersPanel';
 export const USER_ATTRIBUTES_DIALOG = 'userAttributesDialog';
 export const USER_CREATE_FORM_NAME = 'userCreateFormName';
-export const USER_MANAGEMENT_DIALOG = 'userManageDialog';
 
 export interface UserCreateFormDialogData {
     email: string;
@@ -39,13 +38,6 @@ export const openUserAttributes = (uuid: string) =>
         dispatch(dialogActions.OPEN_DIALOG({ id: USER_ATTRIBUTES_DIALOG, data }));
     };
 
-export const openUserManagement = (uuid: string) =>
-    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const { resources } = getState();
-        const data = getResource<UserResource>(uuid)(resources);
-        dispatch(dialogActions.OPEN_DIALOG({ id: USER_MANAGEMENT_DIALOG, data }));
-    };
-
 export const loginAs = (uuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const { resources } = getState();

commit 864465d579c4b2caf06fce87b1861674758135f6
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Mar 28 16:11:51 2022 -0400

    18559: Move user admin functions to context menu
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/components/icon/icon.tsx b/src/components/icon/icon.tsx
index 54b7bee6..20ce62c1 100644
--- a/src/components/icon/icon.tsx
+++ b/src/components/icon/icon.tsx
@@ -65,11 +65,13 @@ import VpnKey from '@material-ui/icons/VpnKey';
 import LinkOutlined from '@material-ui/icons/LinkOutlined';
 import RemoveRedEye from '@material-ui/icons/RemoveRedEye';
 import Computer from '@material-ui/icons/Computer';
+import CropFreeSharp from '@material-ui/icons/CropFreeSharp';
+import Cancel from '@material-ui/icons/Cancel';
+import ExitToApp from '@material-ui/icons/ExitToApp';
 
 // 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,
@@ -173,3 +175,5 @@ export const CanReadIcon: IconType = (props) => <RemoveRedEye {...props} />;
 export const CanWriteIcon: IconType = (props) => <Edit {...props} />;
 export const CanManageIcon: IconType = (props) => <Computer {...props} />;
 export const AddUserIcon: IconType = (props) => <PersonAdd {...props} />;
+export const DeactivateUserIcon: IconType = (props) => <Cancel {...props} />;
+export const LoginAsIcon: IconType = (props) => <ExitToApp {...props} />;
diff --git a/src/store/user-profile/user-profile-actions.ts b/src/store/user-profile/user-profile-actions.ts
index 6042efa9..c8556862 100644
--- a/src/store/user-profile/user-profile-actions.ts
+++ b/src/store/user-profile/user-profile-actions.ts
@@ -75,7 +75,6 @@ export const openSetupDialog = (uuid: string) =>
   }));
 }
 
-
 export const setup = (uuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         try {
diff --git a/src/views-components/context-menu/action-sets/user-action-set.ts b/src/views-components/context-menu/action-sets/user-action-set.ts
index 2edb12d9..0b2ff379 100644
--- a/src/views-components/context-menu/action-sets/user-action-set.ts
+++ b/src/views-components/context-menu/action-sets/user-action-set.ts
@@ -3,9 +3,18 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
-import { AdvancedIcon, ProjectIcon, AttributesIcon } from "components/icon/icon";
+import {
+    AdvancedIcon,
+    ProjectIcon,
+    AttributesIcon,
+    DeactivateUserIcon,
+    UserPanelIcon,
+    LoginAsIcon,
+    AdminMenuIcon,
+} from "components/icon/icon";
 import { openAdvancedTabDialog } from 'store/advanced-tab/advanced-tab';
-import { openUserAttributes, openUserProjects } from "store/users/users-actions";
+import { loginAs, openUserAttributes, openUserProjects } from "store/users/users-actions";
+import { openSetupDialog, openDeactivateDialog } from "store/user-profile/user-profile-actions";
 
 export const userActionSet: ContextMenuActionSet = [[{
     name: "Attributes",
@@ -25,13 +34,30 @@ export const userActionSet: ContextMenuActionSet = [[{
     execute: (dispatch, { uuid }) => {
         dispatch<any>(openAdvancedTabDialog(uuid));
     }
-}, /*
-    // Neither of the buttons on this dialog work correctly (bugs #16114 and #16124) so hide it for now.
-    {
-    name: "Manage",
+}, {
+    name: "Account Settings",
     icon: UserPanelIcon,
     execute: (dispatch, { uuid }) => {
-        dispatch<any>(openUserManagement(uuid));
+        dispatch<any>(openAdvancedTabDialog(uuid));
+    }
+}, {
+    name: "Setup User",
+    icon: AdminMenuIcon,
+    execute: (dispatch, { uuid }) => {
+        dispatch<any>(openSetupDialog(uuid));
+    }
+}, {
+    name: "Deactivate User",
+    icon: DeactivateUserIcon,
+    execute: (dispatch, { uuid }) => {
+        dispatch<any>(openDeactivateDialog(uuid));
+    }
+}, {
+    name: "Login As User",
+    icon: LoginAsIcon,
+    execute: (dispatch, { uuid }) => {
+        dispatch<any>(loginAs(uuid));
     }
-} */
+},
+
 ]];
diff --git a/src/views/user-profile-panel/user-profile-panel-root.tsx b/src/views/user-profile-panel/user-profile-panel-root.tsx
index 4fab7efd..0beeab4f 100644
--- a/src/views/user-profile-panel/user-profile-panel-root.tsx
+++ b/src/views/user-profile-panel/user-profile-panel-root.tsx
@@ -13,7 +13,6 @@ import {
     StyleRulesCallback,
     WithStyles,
     withStyles,
-    Card,
     CardContent,
     Button,
     Typography,
@@ -84,9 +83,6 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 });
 
 export interface UserProfilePanelRootActionProps {
-    openSetupDialog: (uuid: string) => void;
-    loginAs: (uuid: string) => void;
-    openDeactivateDialog: (uuid: string) => void;
     handleContextMenu: (event, resource: UserResource) => void;
 }
 
@@ -124,7 +120,6 @@ export enum UserProfileGroupsColumnNames {
 enum TABS {
     PROFILE = "PROFILE",
     GROUPS = "GROUPS",
-    ADMIN = "ADMIN",
 
 }
 
@@ -202,7 +197,6 @@ export const UserProfilePanelRoot = withStyles(styles)(
                 <Tabs value={this.state.value} onChange={this.handleChange} variant={"fullWidth"}>
                     <Tab label={TABS.PROFILE} value={TABS.PROFILE} />
                     <Tab label={TABS.GROUPS} value={TABS.GROUPS} />
-                    {this.props.isAdmin && <Tab label={TABS.ADMIN} value={TABS.ADMIN} />}
                 </Tabs>
                 {this.state.value === TABS.PROFILE &&
                     <CardContent>
@@ -335,84 +329,6 @@ export const UserProfilePanelRoot = withStyles(styles)(
                                         messages={['Group list is empty.']} />
                                 } />
                     </div>}
-                {this.props.isAdmin && this.state.value === TABS.ADMIN &&
-                    <Paper elevation={0} className={this.props.classes.adminRoot}>
-                        <Card elevation={0}>
-                            <CardContent>
-                                <Grid container
-                                    direction="row"
-                                    justify={'flex-end'}
-                                    alignItems={'center'}>
-                                    <Grid item xs>
-                                        <Typography variant="h6" className={this.props.classes.title}>
-                                            Setup Account
-                                        </Typography>
-                                        <Typography variant="body1" className={this.props.classes.description}>
-                                            This button sets up a user. After setup, they will be able use Arvados. This dialog box also allows you to optionally set up a shell account for this user. The login name is automatically generated from the user's e-mail address.
-                                        </Typography>
-                                    </Grid>
-                                    <Grid item sm={'auto'} xs={12}>
-                                        <Button variant="contained"
-                                            color="primary"
-                                            onClick={() => {this.props.openSetupDialog(this.props.userUuid)}}
-                                            disabled={false}>
-                                            Setup Account
-                                        </Button>
-                                    </Grid>
-                                </Grid>
-                            </CardContent>
-                        </Card>
-                        <Card elevation={0}>
-                            <CardContent>
-                                <Grid container
-                                    direction="row"
-                                    justify={'flex-end'}
-                                    alignItems={'center'}>
-                                    <Grid item xs>
-                                        <Typography variant="h6" className={this.props.classes.title}>
-                                            Deactivate
-                                        </Typography>
-                                        <Typography variant="body1" className={this.props.classes.description}>
-                                            As an admin, you can deactivate and reset this user. This will remove all repository/VM permissions for the user. If you "setup" the user again, the user will have to sign the user agreement again. You may also want to reassign data ownership.
-                                        </Typography>
-                                    </Grid>
-                                    <Grid item sm={'auto'} xs={12}>
-                                        <Button variant="contained"
-                                            color="primary"
-                                            onClick={() => {this.props.openDeactivateDialog(this.props.userUuid)}}
-                                            disabled={false}>
-                                            Deactivate
-                                        </Button>
-                                    </Grid>
-                                </Grid>
-                            </CardContent>
-                        </Card>
-                        <Card elevation={0}>
-                            <CardContent>
-                                <Grid container
-                                    direction="row"
-                                    justify={'flex-end'}
-                                    alignItems={'center'}>
-                                    <Grid item xs>
-                                        <Typography variant="h6" className={this.props.classes.title}>
-                                            Log In
-                                        </Typography>
-                                        <Typography variant="body1" className={this.props.classes.description}>
-                                            As an admin, you can log in as this user. When you’ve finished, you will need to log out and log in again with your own account.
-                                        </Typography>
-                                    </Grid>
-                                    <Grid item sm={'auto'} xs={12}>
-                                        <Button variant="contained"
-                                            color="primary"
-                                            onClick={() => {this.props.loginAs(this.props.userUuid)}}
-                                            disabled={false}>
-                                            Log In
-                                        </Button>
-                                    </Grid>
-                                </Grid>
-                            </CardContent>
-                        </Card>
-                    </Paper>}
             </Paper >;
         }
 
diff --git a/src/views/user-profile-panel/user-profile-panel.tsx b/src/views/user-profile-panel/user-profile-panel.tsx
index 7a55faf3..207a98fe 100644
--- a/src/views/user-profile-panel/user-profile-panel.tsx
+++ b/src/views/user-profile-panel/user-profile-panel.tsx
@@ -9,9 +9,8 @@ import { connect } from 'react-redux';
 import { UserResource } from 'models/user';
 import { saveEditedUser } from 'store/user-profile/user-profile-actions';
 import { UserProfilePanelRoot, UserProfilePanelRootDataProps } from 'views/user-profile-panel/user-profile-panel-root';
-import { openSetupDialog, openDeactivateDialog, USER_PROFILE_FORM } from "store/user-profile/user-profile-actions";
+import { USER_PROFILE_FORM } from "store/user-profile/user-profile-actions";
 import { matchUserProfileRoute } from 'routes/routes';
-import { loginAs } from 'store/users/users-actions';
 import { openUserContextMenu } from 'store/context-menu/context-menu-actions';
 
 const mapStateToProps = (state: RootState): UserProfilePanelRootDataProps => {
@@ -30,9 +29,6 @@ const mapStateToProps = (state: RootState): UserProfilePanelRootDataProps => {
 }};
 
 const mapDispatchToProps = (dispatch: Dispatch) => ({
-    openSetupDialog: (uuid: string) => dispatch<any>(openSetupDialog(uuid)),
-    loginAs: (uuid: string) => dispatch<any>(loginAs(uuid)),
-    openDeactivateDialog: (uuid: string) => dispatch<any>(openDeactivateDialog(uuid)),
     handleContextMenu: (event, resource: UserResource) => dispatch<any>(openUserContextMenu(event, resource)),
 });
 

commit ed59f8b637bc1131ec95e7215efb8bfa4fde9f04
Author: Stephen Smith <stephen at curii.com>
Date:   Thu Mar 24 23:55:24 2022 -0400

    18559: Add uuid with copy and action menu to user profile panel
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/store/context-menu/context-menu-actions.ts b/src/store/context-menu/context-menu-actions.ts
index 38433eb2..fb5da9fc 100644
--- a/src/store/context-menu/context-menu-actions.ts
+++ b/src/store/context-menu/context-menu-actions.ts
@@ -208,6 +208,17 @@ export const openPermissionEditContextMenu = (event: React.MouseEvent<HTMLElemen
         }
     };
 
+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!;
diff --git a/src/views/user-panel/user-panel.tsx b/src/views/user-panel/user-panel.tsx
index 3c835673..169b32ab 100644
--- a/src/views/user-panel/user-panel.tsx
+++ b/src/views/user-panel/user-panel.tsx
@@ -9,7 +9,7 @@ import { connect, DispatchProp } from 'react-redux';
 import { DataColumns } from 'components/data-table/data-table';
 import { RootState } from 'store/store';
 import { SortDirection } from 'components/data-table/data-column';
-import { openContextMenu } from "store/context-menu/context-menu-actions";
+import { openUserContextMenu } from "store/context-menu/context-menu-actions";
 import { getResource, ResourcesState } from "store/resources/resources";
 import {
     UserResourceFullName,
@@ -20,7 +20,6 @@ import {
     ResourceUsername
 } from "views-components/data-explorer/renderers";
 import { navigateToUserProfile } from "store/navigation/navigation-action";
-import { ContextMenuKind } from "views-components/context-menu/context-menu";
 import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
 import { createTree } from 'models/tree';
 import { compose, Dispatch } from 'redux';
@@ -109,7 +108,7 @@ interface UserPanelDataProps {
 interface UserPanelActionProps {
     openUserCreateDialog: () => void;
     handleRowClick: (uuid: string) => void;
-    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: any) => void;
+    handleContextMenu: (event, resource: UserResource) => void;
 }
 
 const mapStateToProps = (state: RootState) => {
@@ -121,7 +120,7 @@ const mapStateToProps = (state: RootState) => {
 const mapDispatchToProps = (dispatch: Dispatch) => ({
     openUserCreateDialog: () => dispatch<any>(openUserCreateDialog()),
     handleRowClick: (uuid: string) => dispatch<any>(navigateToUserProfile(uuid)),
-    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: any) => dispatch<any>(openContextMenu(event, item))
+    handleContextMenu: (event, resource: UserResource) => dispatch<any>(openUserContextMenu(event, resource)),
 });
 
 type UserPanelProps = UserPanelDataProps & UserPanelActionProps & DispatchProp & WithStyles<UserPanelRules>;
@@ -161,13 +160,7 @@ export const UserPanel = compose(
                 event.stopPropagation();
                 const resource = getResource<UserResource>(resourceUuid)(this.props.resources);
                 if (resource) {
-                    this.props.onContextMenu(event, {
-                        name: '',
-                        uuid: resource.uuid,
-                        ownerUuid: resource.ownerUuid,
-                        kind: resource.kind,
-                        menuKind: ContextMenuKind.USER
-                    });
+                    this.props.handleContextMenu(event, resource);
                 }
             }
         }
diff --git a/src/views/user-profile-panel/user-profile-panel-root.tsx b/src/views/user-profile-panel/user-profile-panel-root.tsx
index febe0ab9..4fab7efd 100644
--- a/src/views/user-profile-panel/user-profile-panel-root.tsx
+++ b/src/views/user-profile-panel/user-profile-panel-root.tsx
@@ -4,6 +4,8 @@
 
 import React from 'react';
 import { Field, InjectedFormProps } from "redux-form";
+import { DispatchProp } from 'react-redux';
+import { UserResource } from 'models/user';
 import { TextField } from "components/text-field/text-field";
 import { DataExplorer } from "views-components/data-explorer/data-explorer";
 import { NativeSelectField } from "components/select-field/select-field";
@@ -18,20 +20,24 @@ import {
     Grid,
     InputLabel,
     Tabs, Tab,
-    Paper
+    Paper,
+    Tooltip,
+    IconButton,
 } from '@material-ui/core';
 import { ArvadosTheme } from 'common/custom-theme';
-import { User } from "models/user";
 import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
 import { PROFILE_EMAIL_VALIDATION } from "validators/validators";
 import { USER_PROFILE_PANEL_ID } from 'store/user-profile/user-profile-actions';
 import { noop } from 'lodash';
-import { GroupsIcon } from 'components/icon/icon';
+import { CopyIcon, GroupsIcon, MoreOptionsIcon } from 'components/icon/icon';
 import { DataColumns } from 'components/data-table/data-table';
 import { ResourceLinkHeadUuid, ResourceLinkHeadPermissionLevel, ResourceLinkHead, ResourceLinkDelete, ResourceLinkTailIsVisible } from 'views-components/data-explorer/renderers';
 import { createTree } from 'models/tree';
+import { getResource, ResourcesState } from 'store/resources/resources';
+import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
+import CopyToClipboard from 'react-copy-to-clipboard';
 
-type CssRules = 'root' | 'adminRoot' | 'gridItem' | 'label' | 'readOnlyValue' | 'title' | 'description' | 'actions' | 'content';
+type CssRules = 'root' | 'adminRoot' | 'gridItem' | 'label' | 'readOnlyValue' | 'title' | 'description' | 'actions' | 'content' | 'copyIcon';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
@@ -65,6 +71,15 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     content: {
         // reserve space for the tab bar
         height: `calc(100% - ${theme.spacing.unit * 7}px)`,
+    },
+    copyIcon: {
+        marginLeft: theme.spacing.unit,
+        color: theme.palette.grey["500"],
+        cursor: 'pointer',
+        display: 'inline',
+        '& svg': {
+            fontSize: '1rem'
+        }
     }
 });
 
@@ -72,6 +87,7 @@ export interface UserProfilePanelRootActionProps {
     openSetupDialog: (uuid: string) => void;
     loginAs: (uuid: string) => void;
     openDeactivateDialog: (uuid: string) => void;
+    handleContextMenu: (event, resource: UserResource) => void;
 }
 
 export interface UserProfilePanelRootDataProps {
@@ -79,7 +95,8 @@ export interface UserProfilePanelRootDataProps {
     isSelf: boolean;
     isPristine: boolean;
     isValid: boolean;
-    initialValues?: User;
+    userUuid: string;
+    resources: ResourcesState
     localCluster: string;
 }
 
@@ -94,7 +111,7 @@ const RoleTypes = [
     { key: 'Other', value: 'Other' }
 ];
 
-type UserProfilePanelRootProps = InjectedFormProps<{}> & UserProfilePanelRootActionProps & UserProfilePanelRootDataProps & WithStyles<CssRules>;
+type UserProfilePanelRootProps = InjectedFormProps<{}> & UserProfilePanelRootActionProps & UserProfilePanelRootDataProps & DispatchProp & WithStyles<CssRules>;
 
 export enum UserProfileGroupsColumnNames {
     NAME = "Name",
@@ -172,6 +189,14 @@ export const UserProfilePanelRoot = withStyles(styles)(
             this.setState({ value: TABS.PROFILE});
         }
 
+        onCopy = (message: string) => {
+            this.props.dispatch(snackbarActions.OPEN_SNACKBAR({
+                message,
+                hideDuration: 2000,
+                kind: SnackbarKind.SUCCESS
+            }));
+        }
+
         render() {
             return <Paper className={this.props.classes.root}>
                 <Tabs value={this.state.value} onChange={this.handleChange} variant={"fullWidth"}>
@@ -181,6 +206,30 @@ export const UserProfilePanelRoot = withStyles(styles)(
                 </Tabs>
                 {this.state.value === TABS.PROFILE &&
                     <CardContent>
+                        <Grid container justify="space-between">
+                            <Grid item xs={11}>
+                                <Typography className={this.props.classes.title}>
+                                    {this.props.userUuid}
+                                    <Tooltip title="Copy to clipboard">
+                                        <span className={this.props.classes.copyIcon}>
+                                            <CopyToClipboard text={this.props.userUuid || ""} onCopy={() => this.onCopy!("Copied")}>
+                                                <CopyIcon />
+                                            </CopyToClipboard>
+                                        </span>
+                                    </Tooltip>
+                                </Typography>
+                            </Grid>
+                            <Grid item xs={1} style={{ textAlign: "right" }}>
+                                <Tooltip title="Actions" disableFocusListener>
+                                    <IconButton
+                                        data-cy='collection-panel-options-btn'
+                                        aria-label="Actions"
+                                        onClick={(event) => this.handleContextMenu(event, this.props.userUuid)}>
+                                        <MoreOptionsIcon />
+                                    </IconButton>
+                                </Tooltip>
+                            </Grid>
+                        </Grid>
                         <form onSubmit={this.props.handleSubmit} data-cy="profile-form">
                             <Grid container spacing={24}>
                                 <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="firstName">
@@ -305,7 +354,7 @@ export const UserProfilePanelRoot = withStyles(styles)(
                                     <Grid item sm={'auto'} xs={12}>
                                         <Button variant="contained"
                                             color="primary"
-                                            onClick={() => {this.props.openSetupDialog(this.props.initialValues.uuid)}}
+                                            onClick={() => {this.props.openSetupDialog(this.props.userUuid)}}
                                             disabled={false}>
                                             Setup Account
                                         </Button>
@@ -330,7 +379,7 @@ export const UserProfilePanelRoot = withStyles(styles)(
                                     <Grid item sm={'auto'} xs={12}>
                                         <Button variant="contained"
                                             color="primary"
-                                            onClick={() => {this.props.openDeactivateDialog(this.props.initialValues.uuid)}}
+                                            onClick={() => {this.props.openDeactivateDialog(this.props.userUuid)}}
                                             disabled={false}>
                                             Deactivate
                                         </Button>
@@ -355,7 +404,7 @@ export const UserProfilePanelRoot = withStyles(styles)(
                                     <Grid item sm={'auto'} xs={12}>
                                         <Button variant="contained"
                                             color="primary"
-                                            onClick={() => {this.props.loginAs(this.props.initialValues.uuid)}}
+                                            onClick={() => {this.props.loginAs(this.props.userUuid)}}
                                             disabled={false}>
                                             Log In
                                         </Button>
@@ -371,5 +420,13 @@ export const UserProfilePanelRoot = withStyles(styles)(
             this.setState({ value });
         }
 
+        handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
+            event.stopPropagation();
+            const resource = getResource<UserResource>(resourceUuid)(this.props.resources);
+            if (resource) {
+                this.props.handleContextMenu(event, resource);
+            }
+        }
+
     }
 );
diff --git a/src/views/user-profile-panel/user-profile-panel.tsx b/src/views/user-profile-panel/user-profile-panel.tsx
index e23b8bce..7a55faf3 100644
--- a/src/views/user-profile-panel/user-profile-panel.tsx
+++ b/src/views/user-profile-panel/user-profile-panel.tsx
@@ -6,35 +6,34 @@ import { RootState } from 'store/store';
 import { compose, Dispatch } from 'redux';
 import { reduxForm, isPristine, isValid } from 'redux-form';
 import { connect } from 'react-redux';
+import { UserResource } from 'models/user';
 import { saveEditedUser } from 'store/user-profile/user-profile-actions';
 import { UserProfilePanelRoot, UserProfilePanelRootDataProps } from 'views/user-profile-panel/user-profile-panel-root';
 import { openSetupDialog, openDeactivateDialog, USER_PROFILE_FORM } from "store/user-profile/user-profile-actions";
 import { matchUserProfileRoute } from 'routes/routes';
-import { UserResource } from 'models/user';
-import { getResource } from 'store/resources/resources';
 import { loginAs } from 'store/users/users-actions';
+import { openUserContextMenu } from 'store/context-menu/context-menu-actions';
 
 const mapStateToProps = (state: RootState): UserProfilePanelRootDataProps => {
   const pathname = state.router.location ? state.router.location.pathname : '';
   const match = matchUserProfileRoute(pathname);
   const uuid = match ? match.params.id : state.auth.user?.uuid || '';
-  // get user resource
-  const user = getResource<UserResource>(uuid)(state.resources);
-  // const subprocesses = getSubprocesses(uuid)(resources);
 
   return {
     isAdmin: state.auth.user!.isAdmin,
     isSelf: state.auth.user!.uuid === uuid,
     isPristine: isPristine(USER_PROFILE_FORM)(state),
     isValid: isValid(USER_PROFILE_FORM)(state),
-    initialValues: user,
-    localCluster: state.auth.localCluster
+    localCluster: state.auth.localCluster,
+    userUuid: uuid,
+    resources: state.resources,
 }};
 
 const mapDispatchToProps = (dispatch: Dispatch) => ({
     openSetupDialog: (uuid: string) => dispatch<any>(openSetupDialog(uuid)),
     loginAs: (uuid: string) => dispatch<any>(loginAs(uuid)),
     openDeactivateDialog: (uuid: string) => dispatch<any>(openDeactivateDialog(uuid)),
+    handleContextMenu: (event, resource: UserResource) => dispatch<any>(openUserContextMenu(event, resource)),
 });
 
 export const UserProfilePanel = compose(

commit 6d49676d1505f09223d85a488e79ca678201a81b
Author: Stephen Smith <stephen at curii.com>
Date:   Thu Mar 24 19:33:34 2022 -0400

    18559: Forgot to change users list name column label
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views/user-panel/user-panel.tsx b/src/views/user-panel/user-panel.tsx
index 3bece2f1..3c835673 100644
--- a/src/views/user-panel/user-panel.tsx
+++ b/src/views/user-panel/user-panel.tsx
@@ -44,7 +44,7 @@ const styles = withStyles<UserPanelRules>(theme => ({
 }));
 
 export enum UserPanelColumnNames {
-    NAME = "First Name",
+    NAME = "Name",
     UUID = "Uuid",
     EMAIL = "Email",
     ACTIVE = "Active",

commit 7e9513099881d63a99a70c69ddd71c5d83567fa4
Author: Stephen Smith <stephen at curii.com>
Date:   Thu Mar 24 15:07:27 2022 -0400

    18559: Replace setup vm dialog with setup confirmation dialog
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/store/user-profile/user-profile-actions.ts b/src/store/user-profile/user-profile-actions.ts
index 0c6341f2..6042efa9 100644
--- a/src/store/user-profile/user-profile-actions.ts
+++ b/src/store/user-profile/user-profile-actions.ts
@@ -15,6 +15,7 @@ import { dialogActions } from "store/dialog/dialog-actions";
 export const USER_PROFILE_PANEL_ID = 'userProfilePanel';
 export const USER_PROFILE_FORM = 'userProfileForm';
 export const DEACTIVATE_DIALOG = 'deactivateDialog';
+export const SETUP_DIALOG = 'setupDialog';
 
 export const UserProfileGroupsActions = bindDataExplorerActions(USER_PROFILE_PANEL_ID);
 
@@ -61,6 +62,34 @@ export const openDeactivateDialog = (uuid: string) =>
   }));
 }
 
+export const openSetupDialog = (uuid: string) =>
+  (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+    dispatch(dialogActions.OPEN_DIALOG({
+      id: SETUP_DIALOG,
+      data: {
+          title: 'Setup user',
+          text: 'Are you sure you want to setup this user?',
+          confirmButtonLabel: 'Confirm',
+          uuid
+      }
+  }));
+}
+
+
+export const setup = (uuid: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        try {
+            const resources = await services.userService.setup(uuid);
+            dispatch(updateResources(resources.items));
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: "User has been setup", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
+        } catch (e) {
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000, kind: SnackbarKind.ERROR }));
+        } finally {
+            dispatch(dialogActions.CLOSE_DIALOG({ id: SETUP_DIALOG }));
+        }
+
+    };
+
 export const unsetup = (uuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         try {
diff --git a/src/store/users/users-actions.ts b/src/store/users/users-actions.ts
index 425a2d56..b5d47d70 100644
--- a/src/store/users/users-actions.ts
+++ b/src/store/users/users-actions.ts
@@ -8,14 +8,14 @@ import { RootState } from 'store/store';
 import { getUserUuid } from "common/getuser";
 import { ServiceRepository } from "services/services";
 import { dialogActions } from 'store/dialog/dialog-actions';
-import { startSubmit, reset, initialize, stopSubmit } from "redux-form";
+import { startSubmit, reset, stopSubmit } from "redux-form";
 import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
 import { UserResource } from "models/user";
 import { getResource } from 'store/resources/resources';
 import { navigateTo, navigateToUsers, navigateToRootProject } from "store/navigation/navigation-action";
 import { authActions } from 'store/auth/auth-action';
 import { getTokenV2 } from "models/api-client-authorization";
-import { AddLoginFormData, VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD, VIRTUAL_MACHINE_ADD_LOGIN_USER_FIELD, VIRTUAL_MACHINE_ADD_LOGIN_VM_FIELD } from "store/virtual-machines/virtual-machines-actions";
+import { VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD, VIRTUAL_MACHINE_ADD_LOGIN_VM_FIELD } from "store/virtual-machines/virtual-machines-actions";
 import { PermissionLevel } from "models/permission";
 import { updateResources } from "store/resources/resources-actions";
 
@@ -23,7 +23,6 @@ export const USERS_PANEL_ID = 'usersPanel';
 export const USER_ATTRIBUTES_DIALOG = 'userAttributesDialog';
 export const USER_CREATE_FORM_NAME = 'userCreateFormName';
 export const USER_MANAGEMENT_DIALOG = 'userManageDialog';
-export const SETUP_SHELL_ACCOUNT_DIALOG = 'setupShellAccountDialog';
 
 export interface UserCreateFormDialogData {
     email: string;
@@ -47,15 +46,6 @@ export const openUserManagement = (uuid: string) =>
         dispatch(dialogActions.OPEN_DIALOG({ id: USER_MANAGEMENT_DIALOG, data }));
     };
 
-export const openSetupShellAccount = (uuid: string) =>
-    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const { resources } = getState();
-        const user = getResource<UserResource>(uuid)(resources);
-        const virtualMachines = await services.virtualMachineService.list();
-        dispatch(initialize(SETUP_SHELL_ACCOUNT_DIALOG, {[VIRTUAL_MACHINE_ADD_LOGIN_USER_FIELD]: user, [VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD]: []}));
-        dispatch(dialogActions.OPEN_DIALOG({ id: SETUP_SHELL_ACCOUNT_DIALOG, data: virtualMachines }));
-    };
-
 export const loginAs = (uuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const { resources } = getState();
@@ -118,35 +108,6 @@ export const createUser = (data: UserCreateFormDialogData) =>
         }
     };
 
-export const setupUserVM = (setupData: AddLoginFormData) =>
-    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        dispatch(startSubmit(SETUP_SHELL_ACCOUNT_DIALOG));
-        try {
-            const userResource = await services.userService.get(setupData.user.uuid);
-
-            const resources = await services.userService.setup(setupData.user.uuid);
-            dispatch(updateResources(resources.items));
-
-            const permission = await services.permissionService.create({
-                headUuid: setupData.vmUuid,
-                tailUuid: userResource.uuid,
-                name: PermissionLevel.CAN_LOGIN,
-                properties: {
-                    username: userResource.username,
-                    groups: setupData.groups,
-                }
-            });
-            dispatch(updateResources([permission]));
-
-            dispatch(dialogActions.CLOSE_DIALOG({ id: SETUP_SHELL_ACCOUNT_DIALOG }));
-            dispatch(reset(SETUP_SHELL_ACCOUNT_DIALOG));
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: "User has been added to VM.", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
-        } catch (e) {
-            dispatch(stopSubmit(SETUP_SHELL_ACCOUNT_DIALOG));
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000, kind: SnackbarKind.ERROR }));
-        }
-    };
-
 export const openUserPanel = () =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const user = getState().auth.user;
diff --git a/src/views-components/dialog-forms/setup-shell-account-dialog.tsx b/src/views-components/dialog-forms/setup-shell-account-dialog.tsx
deleted file mode 100644
index 04eae126..00000000
--- a/src/views-components/dialog-forms/setup-shell-account-dialog.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-import React from 'react';
-import { compose } from "redux";
-import { reduxForm, InjectedFormProps, Field } from 'redux-form';
-import { withDialog, WithDialogProps } from "store/dialog/with-dialog";
-import { FormDialog } from 'components/form-dialog/form-dialog';
-import { TextField } from 'components/text-field/text-field';
-import { VirtualMachinesResource } from 'models/virtual-machines';
-import { InputLabel } from '@material-ui/core';
-import { SETUP_SHELL_ACCOUNT_DIALOG, setupUserVM } from 'store/users/users-actions';
-import { UserResource } from 'models/user';
-import { VIRTUAL_MACHINE_ADD_LOGIN_USER_FIELD, AddLoginFormData } from 'store/virtual-machines/virtual-machines-actions';
-import { UserGroupsVirtualMachineField, RequiredUserVirtualMachineField } from 'views-components/form-fields/user-form-fields';
-
-export const SetupShellAccountDialog = compose(
-    withDialog(SETUP_SHELL_ACCOUNT_DIALOG),
-    reduxForm<AddLoginFormData>({
-        form: SETUP_SHELL_ACCOUNT_DIALOG,
-        onSubmit: (data, dispatch) => {
-            dispatch(setupUserVM(data));
-        }
-    })
-)(
-    (props: SetupShellAccountDialogComponentProps) =>
-        <FormDialog
-            dialogTitle='Setup shell account'
-            formFields={SetupShellAccountFormFields}
-            submitLabel='Submit'
-            {...props}
-        />
-);
-
-interface DataProps {
-    user: UserResource;
-    items: VirtualMachinesResource[];
-}
-
-const UserNameField = () =>
-    <span>
-        <InputLabel>VM Login</InputLabel>
-        <Field
-            name={`${VIRTUAL_MACHINE_ADD_LOGIN_USER_FIELD}.username`}
-            component={TextField as any}
-            disabled />
-    </span>;
-
-type SetupShellAccountDialogComponentProps = WithDialogProps<{}> & InjectedFormProps<AddLoginFormData>;
-
-const SetupShellAccountFormFields = (props: SetupShellAccountDialogComponentProps) =>
-    <>
-        <UserNameField />
-        <RequiredUserVirtualMachineField data={props.data as DataProps} />
-        <UserGroupsVirtualMachineField />
-    </>;
diff --git a/src/views-components/user-dialog/manage-dialog.tsx b/src/views-components/user-dialog/manage-dialog.tsx
deleted file mode 100644
index a62e1a21..00000000
--- a/src/views-components/user-dialog/manage-dialog.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import React from "react";
-import { compose, Dispatch } from "redux";
-import { connect } from "react-redux";
-import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography } from "@material-ui/core";
-import { WithDialogProps } from "store/dialog/with-dialog";
-import { withDialog } from 'store/dialog/with-dialog';
-import { WithStyles, withStyles } from '@material-ui/core/styles';
-import { ArvadosTheme } from 'common/custom-theme';
-import { USER_MANAGEMENT_DIALOG } from "store/users/users-actions";
-import { openSetupShellAccount, loginAs } from 'store/users/users-actions';
-import { getUserDisplayName } from "models/user";
-
-type CssRules = 'spacing';
-
-const styles = withStyles<CssRules>((theme: ArvadosTheme) => ({
-    spacing: {
-        paddingBottom: theme.spacing.unit * 2,
-        paddingTop: theme.spacing.unit * 2,
-    }
-}));
-
-interface UserManageDataProps {
-    data: any;
-}
-
-interface UserManageActionProps {
-    openSetupShellAccount: (uuid: string) => void;
-    loginAs: (uuid: string) => void;
-}
-
-const mapDispatchToProps = (dispatch: Dispatch) => ({
-    openSetupShellAccount: (uuid: string) => dispatch<any>(openSetupShellAccount(uuid)),
-    loginAs: (uuid: string) => dispatch<any>(loginAs(uuid))
-});
-
-type UserManageProps = UserManageDataProps & UserManageActionProps & WithStyles<CssRules>;
-
-export const UserManageDialog = compose(
-    connect(null, mapDispatchToProps),
-    withDialog(USER_MANAGEMENT_DIALOG),
-    styles)(
-        (props: WithDialogProps<UserManageProps> & UserManageProps) =>
-            <Dialog open={props.open}
-                onClose={props.closeDialog}
-                fullWidth
-                maxWidth="md">
-                {props.data &&
-                    <span>
-                        <DialogTitle>{`Manage - ${getUserDisplayName(props.data)}`}</DialogTitle>
-                        <DialogContent>
-                            <Typography variant='body1' className={props.classes.spacing}>
-                                As an admin, you can log in as this user. When you’ve finished, you will need to log out and log in again with your own account.
-                    </Typography>
-                            <Button variant="contained" color="primary" onClick={() => props.loginAs(props.data.uuid)}>
-                                {`LOG IN AS ${getUserDisplayName(props.data)}`}
-                            </Button>
-                            <Typography variant='body1' className={props.classes.spacing}>
-                                As an admin, you can setup a shell account for this user. The login name is automatically generated from the user's e-mail address.
-                    </Typography>
-                            <Button variant="contained" color="primary" onClick={() => props.openSetupShellAccount(props.data.uuid)}>
-                                {`SETUP SHELL ACCOUNT FOR ${getUserDisplayName(props.data)}`}
-                            </Button>
-                        </DialogContent></span>}
-
-                <DialogActions>
-                    <Button
-                        variant='text'
-                        color='primary'
-                        onClick={props.closeDialog}>
-                        Close
-                </Button>
-                </DialogActions>
-            </Dialog>
-    );
diff --git a/src/views-components/user-dialog/setup-dialog.tsx b/src/views-components/user-dialog/setup-dialog.tsx
new file mode 100644
index 00000000..3a2fd355
--- /dev/null
+++ b/src/views-components/user-dialog/setup-dialog.tsx
@@ -0,0 +1,21 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch, compose } from 'redux';
+import { connect } from "react-redux";
+import { ConfirmationDialog } from "components/confirmation-dialog/confirmation-dialog";
+import { withDialog, WithDialogProps } from "store/dialog/with-dialog";
+import { setup, SETUP_DIALOG } from 'store/user-profile/user-profile-actions';
+
+const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps<any>) => ({
+    onConfirm: () => {
+        props.closeDialog();
+        dispatch<any>(setup(props.data.uuid));
+    }
+});
+
+export const SetupDialog = compose(
+    withDialog(SETUP_DIALOG),
+    connect(null, mapDispatchToProps)
+)(ConfirmationDialog);
diff --git a/src/views/user-profile-panel/user-profile-panel-root.tsx b/src/views/user-profile-panel/user-profile-panel-root.tsx
index 4572a352..febe0ab9 100644
--- a/src/views/user-profile-panel/user-profile-panel-root.tsx
+++ b/src/views/user-profile-panel/user-profile-panel-root.tsx
@@ -69,7 +69,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 });
 
 export interface UserProfilePanelRootActionProps {
-    openSetupShellAccount: (uuid: string) => void;
+    openSetupDialog: (uuid: string) => void;
     loginAs: (uuid: string) => void;
     openDeactivateDialog: (uuid: string) => void;
 }
@@ -305,7 +305,7 @@ export const UserProfilePanelRoot = withStyles(styles)(
                                     <Grid item sm={'auto'} xs={12}>
                                         <Button variant="contained"
                                             color="primary"
-                                            onClick={() => {this.props.openSetupShellAccount(this.props.initialValues.uuid)}}
+                                            onClick={() => {this.props.openSetupDialog(this.props.initialValues.uuid)}}
                                             disabled={false}>
                                             Setup Account
                                         </Button>
diff --git a/src/views/user-profile-panel/user-profile-panel.tsx b/src/views/user-profile-panel/user-profile-panel.tsx
index 2bafd9fa..e23b8bce 100644
--- a/src/views/user-profile-panel/user-profile-panel.tsx
+++ b/src/views/user-profile-panel/user-profile-panel.tsx
@@ -8,11 +8,11 @@ import { reduxForm, isPristine, isValid } from 'redux-form';
 import { connect } from 'react-redux';
 import { saveEditedUser } from 'store/user-profile/user-profile-actions';
 import { UserProfilePanelRoot, UserProfilePanelRootDataProps } from 'views/user-profile-panel/user-profile-panel-root';
-import { openDeactivateDialog, USER_PROFILE_FORM } from "store/user-profile/user-profile-actions";
+import { openSetupDialog, openDeactivateDialog, USER_PROFILE_FORM } from "store/user-profile/user-profile-actions";
 import { matchUserProfileRoute } from 'routes/routes';
 import { UserResource } from 'models/user';
 import { getResource } from 'store/resources/resources';
-import { openSetupShellAccount, loginAs } from 'store/users/users-actions';
+import { loginAs } from 'store/users/users-actions';
 
 const mapStateToProps = (state: RootState): UserProfilePanelRootDataProps => {
   const pathname = state.router.location ? state.router.location.pathname : '';
@@ -32,7 +32,7 @@ const mapStateToProps = (state: RootState): UserProfilePanelRootDataProps => {
 }};
 
 const mapDispatchToProps = (dispatch: Dispatch) => ({
-    openSetupShellAccount: (uuid: string) => dispatch<any>(openSetupShellAccount(uuid)),
+    openSetupDialog: (uuid: string) => dispatch<any>(openSetupDialog(uuid)),
     loginAs: (uuid: string) => dispatch<any>(loginAs(uuid)),
     openDeactivateDialog: (uuid: string) => dispatch<any>(openDeactivateDialog(uuid)),
 });
diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx
index 1202529c..11e038f5 100644
--- a/src/views/workbench/workbench.tsx
+++ b/src/views/workbench/workbench.tsx
@@ -80,9 +80,8 @@ import { UserPanel } from 'views/user-panel/user-panel';
 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 { DeactivateDialog } from 'views-components/user-dialog/deactivate-dialog';
-import { SetupShellAccountDialog } from 'views-components/dialog-forms/setup-shell-account-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';
@@ -261,7 +260,6 @@ export const WorkbenchPanel =
             <RepositoryAttributesDialog />
             <RepositoriesSampleGitDialog />
             <RichTextEditorDialog />
-            <SetupShellAccountDialog />
             <SharingDialog />
             <NotFoundDialog />
             <Snackbar />
@@ -269,8 +267,8 @@ export const WorkbenchPanel =
             <UpdateProcessDialog />
             <UpdateProjectDialog />
             <UserAttributesDialog />
-            <UserManageDialog />
             <DeactivateDialog />
+            <SetupDialog />
             <VirtualMachineAttributesDialog />
             <FedLogin />
             <WebDavS3InfoDialog />

commit 066c2178f99f8fb5c818bc453b49ba8341d440a6
Author: Stephen Smith <stephen at curii.com>
Date:   Thu Mar 24 15:05:25 2022 -0400

    18559: Add basic field validation for profile email
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/validators/validators.tsx b/src/validators/validators.tsx
index 81fed2cc..b781a3a9 100644
--- a/src/validators/validators.tsx
+++ b/src/validators/validators.tsx
@@ -31,6 +31,7 @@ export const PROCESS_DESCRIPTION_VALIDATION = [maxLength(255)];
 export const REPOSITORY_NAME_VALIDATION = [require, maxLength(255)];
 
 export const USER_EMAIL_VALIDATION = [require, maxLength(255)];
+export const PROFILE_EMAIL_VALIDATION = [maxLength(255)];
 export const USER_LENGTH_VALIDATION = [maxLength(255)];
 
 export const SSH_KEY_PUBLIC_VALIDATION = [require, isRsaKey, maxLength(1024)];
diff --git a/src/views/user-profile-panel/user-profile-panel-root.tsx b/src/views/user-profile-panel/user-profile-panel-root.tsx
index 82c20728..4572a352 100644
--- a/src/views/user-profile-panel/user-profile-panel-root.tsx
+++ b/src/views/user-profile-panel/user-profile-panel-root.tsx
@@ -23,7 +23,7 @@ import {
 import { ArvadosTheme } from 'common/custom-theme';
 import { User } from "models/user";
 import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
-import { MY_ACCOUNT_VALIDATION } from "validators/validators";
+import { PROFILE_EMAIL_VALIDATION } from "validators/validators";
 import { USER_PROFILE_PANEL_ID } from 'store/user-profile/user-profile-actions';
 import { noop } from 'lodash';
 import { GroupsIcon } from 'components/icon/icon';
@@ -229,6 +229,7 @@ export const UserProfilePanelRoot = withStyles(styles)(
                                         name="prefs.profile.organization_email"
                                         component={TextField as any}
                                         disabled={!this.props.isAdmin && !this.props.isSelf}
+                                        validate={PROFILE_EMAIL_VALIDATION}
                                     />
                                 </Grid>
                                 <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>

commit 09b2d7a937291e1e74d773a4227c4cfc63c30972
Author: Stephen Smith <stephen at curii.com>
Date:   Wed Mar 23 20:30:40 2022 -0400

    18559: Combine firstName and lastName users column, add link navigation, fix sorting
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/store/users/user-panel-middleware-service.ts b/src/store/users/user-panel-middleware-service.ts
index 4496cbd2..2a742353 100644
--- a/src/store/users/user-panel-middleware-service.ts
+++ b/src/store/users/user-panel-middleware-service.ts
@@ -75,13 +75,23 @@ export const getOrder = (dataExplorer: DataExplorer) => {
         const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC
             ? OrderDirection.ASC
             : OrderDirection.DESC;
-        const columnName = sortColumn && sortColumn.name === UserPanelColumnNames.LAST_NAME ? "lastName" : "firstName";
-        return order
-            .addOrder(sortDirection, columnName)
-            .getOrder();
-    } else {
-        return order.getOrder();
+        switch (sortColumn.name) {
+            case UserPanelColumnNames.NAME:
+                order.addOrder(sortDirection, "firstName")
+                    .addOrder(sortDirection, "lastName");
+                break;
+            case UserPanelColumnNames.UUID:
+                order.addOrder(sortDirection, "uuid");
+                break;
+            case UserPanelColumnNames.EMAIL:
+                order.addOrder(sortDirection, "email");
+                break;
+            case UserPanelColumnNames.USERNAME:
+                order.addOrder(sortDirection, "username");
+                break;
+        }
     }
+    return order.getOrder();
 };
 
 export const setItems = (listResults: ListResults<UserResource>) =>
@@ -94,4 +104,4 @@ const couldNotFetchUsers = () =>
     snackbarActions.OPEN_SNACKBAR({
         message: 'Could not fetch users.',
         kind: SnackbarKind.ERROR
-    });
\ No newline at end of file
+    });
diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx
index 7cdd9b83..5068355b 100644
--- a/src/views-components/data-explorer/renderers.tsx
+++ b/src/views-components/data-explorer/renderers.tsx
@@ -23,7 +23,7 @@ import { openSharingDialog } from 'store/sharing-dialog/sharing-dialog-actions';
 import { getUserFullname, getUserDisplayName, User, UserResource } from 'models/user';
 import { toggleIsActive, toggleIsAdmin } from 'store/users/users-actions';
 import { LinkClass, LinkResource } from 'models/link';
-import { navigateTo, navigateToGroupDetails } from 'store/navigation/navigation-action';
+import { navigateTo, navigateToGroupDetails, navigateToUserProfile } from 'store/navigation/navigation-action';
 import { withResourceData } from 'views-components/data-explorer/with-resources';
 import { CollectionResource } from 'models/collection';
 import { IllegalNamingWarning } from 'components/warning/warning';
@@ -161,14 +161,22 @@ export const ResourceLastName = connect(
         return resource || { lastName: '' };
     })(renderLastName);
 
-const renderFullName = (item: { firstName: string, lastName: string }) =>
-    <Typography noWrap>{(item.firstName + " " + item.lastName).trim()}</Typography>;
+const renderFullName = (dispatch: Dispatch ,item: { uuid: string, firstName: string, lastName: string }, link?: boolean) => {
+    const displayName = (item.firstName + " " + item.lastName).trim() || item.uuid;
+    return link ? <Typography noWrap
+        color="primary"
+        style={{ 'cursor': 'pointer' }}
+        onClick={() => dispatch<any>(navigateToUserProfile(item.uuid))}>
+            {displayName}
+    </Typography> :
+    <Typography noWrap>{displayName}</Typography>;
+}
 
-export const ResourceFullName = connect(
-    (state: RootState, props: { uuid: string }) => {
+export const UserResourceFullName = connect(
+    (state: RootState, props: { uuid: string, link?: boolean }) => {
         const resource = getResource<UserResource>(props.uuid)(state.resources);
-        return resource || { firstName: '', lastName: '' };
-    })(renderFullName);
+        return {item: resource || { uuid: '', firstName: '', lastName: '' }, link: props.link};
+    })((props: {item: {uuid: string, firstName: string, lastName: string}, link?: boolean} & DispatchProp<any>) => renderFullName(props.dispatch, props.item, props.link));
 
 
 const renderUuid = (item: { uuid: string }) =>
diff --git a/src/views/user-panel/user-panel.tsx b/src/views/user-panel/user-panel.tsx
index cbb59a33..3bece2f1 100644
--- a/src/views/user-panel/user-panel.tsx
+++ b/src/views/user-panel/user-panel.tsx
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import React from 'react';
-import { WithStyles, withStyles, Tabs, Tab, Paper, Button, Grid } from '@material-ui/core';
+import { WithStyles, withStyles, Paper, Button, Grid } from '@material-ui/core';
 import { DataExplorer } from "views-components/data-explorer/data-explorer";
 import { connect, DispatchProp } from 'react-redux';
 import { DataColumns } from 'components/data-table/data-table';
@@ -12,8 +12,7 @@ import { SortDirection } from 'components/data-table/data-column';
 import { openContextMenu } from "store/context-menu/context-menu-actions";
 import { getResource, ResourcesState } from "store/resources/resources";
 import {
-    ResourceFirstName,
-    ResourceLastName,
+    UserResourceFullName,
     ResourceUuid,
     ResourceEmail,
     ResourceIsActive,
@@ -45,8 +44,7 @@ const styles = withStyles<UserPanelRules>(theme => ({
 }));
 
 export enum UserPanelColumnNames {
-    FIRST_NAME = "First Name",
-    LAST_NAME = "Last Name",
+    NAME = "First Name",
     UUID = "Uuid",
     EMAIL = "Email",
     ACTIVE = "Active",
@@ -57,20 +55,12 @@ export enum UserPanelColumnNames {
 
 export const userPanelColumns: DataColumns<string> = [
     {
-        name: UserPanelColumnNames.FIRST_NAME,
+        name: UserPanelColumnNames.NAME,
         selected: true,
         configurable: true,
         sortDirection: SortDirection.NONE,
         filters: createTree(),
-        render: uuid => <ResourceFirstName uuid={uuid} />
-    },
-    {
-        name: UserPanelColumnNames.LAST_NAME,
-        selected: true,
-        configurable: true,
-        sortDirection: SortDirection.NONE,
-        filters: createTree(),
-        render: uuid => <ResourceLastName uuid={uuid} />
+        render: uuid => <UserResourceFullName uuid={uuid} link={true} />
     },
     {
         name: UserPanelColumnNames.UUID,
@@ -92,7 +82,6 @@ export const userPanelColumns: DataColumns<string> = [
         name: UserPanelColumnNames.ACTIVE,
         selected: true,
         configurable: true,
-        sortDirection: SortDirection.NONE,
         filters: createTree(),
         render: uuid => <ResourceIsActive uuid={uuid} />
     },
@@ -100,7 +89,6 @@ export const userPanelColumns: DataColumns<string> = [
         name: UserPanelColumnNames.ADMIN,
         selected: true,
         configurable: false,
-        sortDirection: SortDirection.NONE,
         filters: createTree(),
         render: uuid => <ResourceIsAdmin uuid={uuid} />
     },
@@ -146,7 +134,7 @@ export const UserPanel = compose(
                 return <Paper className={this.props.classes.root}>
                     <DataExplorer
                         id={USERS_PANEL_ID}
-                        onRowClick={this.props.handleRowClick}
+                        onRowClick={noop}
                         onRowDoubleClick={noop}
                         onContextMenu={this.handleContextMenu}
                         contextMenuColumn={true}

commit a5cbf89b125947ddebacfb4475149d77a18d1851
Author: Stephen Smith <stephen at curii.com>
Date:   Wed Mar 23 19:19:46 2022 -0400

    18559: Remove user panel tabs
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views/user-panel/user-panel.tsx b/src/views/user-panel/user-panel.tsx
index 9f4d4ce4..cbb59a33 100644
--- a/src/views/user-panel/user-panel.tsx
+++ b/src/views/user-panel/user-panel.tsx
@@ -30,7 +30,7 @@ import { ShareMeIcon, AddIcon } from 'components/icon/icon';
 import { USERS_PANEL_ID, openUserCreateDialog } from 'store/users/users-actions';
 import { noop } from 'lodash';
 
-type UserPanelRules = "button" | 'root' | 'content';
+type UserPanelRules = "button" | 'root';
 
 const styles = withStyles<UserPanelRules>(theme => ({
     button: {
@@ -42,10 +42,6 @@ const styles = withStyles<UserPanelRules>(theme => ({
     root: {
         width: '100%',
     },
-    content: {
-        // reserve space for the tab bar
-        height: `calc(100% - ${theme.spacing.unit * 7}px)`,
-    }
 }));
 
 export enum UserPanelColumnNames {
@@ -146,53 +142,33 @@ export const UserPanel = compose(
     styles,
     connect(mapStateToProps, mapDispatchToProps))(
         class extends React.Component<UserPanelProps> {
-            state = {
-                value: 0,
-            };
-
-            componentDidMount() {
-                this.setState({ value: 0 });
-            }
-
             render() {
-                const { value } = this.state;
                 return <Paper className={this.props.classes.root}>
-                    <Tabs value={value} onChange={this.handleChange} variant={"fullWidth"}>
-                        <Tab label="USERS" />
-                        <Tab label="ACTIVITY" disabled />
-                    </Tabs>
-                    {value === 0 &&
-                        <div className={this.props.classes.content}>
-                            <DataExplorer
-                                id={USERS_PANEL_ID}
-                                onRowClick={this.props.handleRowClick}
-                                onRowDoubleClick={noop}
-                                onContextMenu={this.handleContextMenu}
-                                contextMenuColumn={true}
-                                hideColumnSelector
-                                actions={
-                                    <Grid container justify='flex-end'>
-                                        <Button variant="contained" color="primary" onClick={this.props.openUserCreateDialog}>
-                                            <AddIcon /> NEW USER
-                                        </Button>
-                                    </Grid>
-                                }
-                                paperProps={{
-                                    elevation: 0,
-                                }}
-                                dataTableDefaultView={
-                                    <DataTableDefaultView
-                                        icon={ShareMeIcon}
-                                        messages={['Your user list is empty.']} />
-                                } />
-                        </div>}
+                    <DataExplorer
+                        id={USERS_PANEL_ID}
+                        onRowClick={this.props.handleRowClick}
+                        onRowDoubleClick={noop}
+                        onContextMenu={this.handleContextMenu}
+                        contextMenuColumn={true}
+                        hideColumnSelector
+                        actions={
+                            <Grid container justify='flex-end'>
+                                <Button variant="contained" color="primary" onClick={this.props.openUserCreateDialog}>
+                                    <AddIcon /> NEW USER
+                                </Button>
+                            </Grid>
+                        }
+                        paperProps={{
+                            elevation: 0,
+                        }}
+                        dataTableDefaultView={
+                            <DataTableDefaultView
+                                icon={ShareMeIcon}
+                                messages={['Your user list is empty.']} />
+                        } />
                 </Paper>;
             }
 
-            handleChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
-                this.setState({ value });
-            }
-
             handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
                 event.stopPropagation();
                 const resource = getResource<UserResource>(resourceUuid)(this.props.resources);

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list