[ARVADOS-WORKBENCH2] created: 2.3.0-175-g7435f8f8

Git user git at public.arvados.org
Wed Mar 2 22:29:08 UTC 2022


        at  7435f8f863ff94834d7188772547cfb0cd4ba1d4 (commit)


commit 7435f8f863ff94834d7188772547cfb0cd4ba1d4
Author: Stephen Smith <stephen at curii.com>
Date:   Wed Mar 2 17:22:22 2022 -0500

    18559: Disable user profile form fields when not admin or self
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/components/select-field/select-field.tsx b/src/components/select-field/select-field.tsx
index e4dcad6c..6fa7ddea 100644
--- a/src/components/select-field/select-field.tsx
+++ b/src/components/select-field/select-field.tsx
@@ -35,14 +35,18 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     }
 });
 
+interface NativeSelectFieldProps {
+    disabled?: boolean;
+}
+
 export const NativeSelectField = withStyles(styles)
-    ((props: WrappedFieldProps & WithStyles<CssRules> & { items: any[] }) =>
+    ((props: WrappedFieldProps & NativeSelectFieldProps & WithStyles<CssRules> & { items: any[] }) =>
         <FormControl className={props.classes.formControl}>
             <Select className={props.classes.selectWrapper}
                 native
                 value={props.input.value}
                 onChange={props.input.onChange}
-                disabled={props.meta.submitting}
+                disabled={props.meta.submitting || props.disabled}
                 name={props.input.name}
                 inputProps={{
                     id: `id-${props.input.name}`,
@@ -81,4 +85,4 @@ export const SelectField = withStyles(selectFieldStyles)(
             </Select>
             <FormHelperText>{props.meta.error}</FormHelperText>
         </FormControl>
-);
\ No newline at end of file
+);
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 a725333d..76cad8a5 100644
--- a/src/views/user-profile-panel/user-profile-panel-root.tsx
+++ b/src/views/user-profile-panel/user-profile-panel-root.tsx
@@ -71,6 +71,8 @@ export interface UserProfilePanelRootActionProps {
 }
 
 export interface UserProfilePanelRootDataProps {
+    isAdmin: boolean;
+    isSelf: boolean;
     isPristine: boolean;
     isValid: boolean;
     initialValues?: User;
@@ -150,95 +152,97 @@ export const UserProfilePanelRoot = withStyles(styles)(
                 <Tabs value={this.state.value} onChange={this.handleChange} variant={"fullWidth"}>
                     <Tab label="PROFILE" />
                     <Tab label="GROUPS" />
-                    <Tab label="ADMIN" />
+                    <Tab label="ADMIN" disabled={!this.props.isAdmin} />
                 </Tabs>
                 {this.state.value === 0 &&
-                    // <Card className={this.props.classes.root}>
-                        <CardContent>
-                            <form onSubmit={this.props.handleSubmit}>
-                                <Grid container spacing={24}>
-                                    <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
-                                        <Field
-                                            label="First name"
-                                            name="firstName"
-                                            component={TextField as any}
-                                            disabled
-                                        />
-                                    </Grid>
-                                    <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
-                                        <Field
-                                            label="Last name"
-                                            name="lastName"
-                                            component={TextField as any}
-                                            disabled
-                                        />
-                                    </Grid>
-                                    <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
-                                        <Field
-                                            label="E-mail"
-                                            name="email"
-                                            component={TextField as any}
-                                            disabled
-                                        />
-                                    </Grid>
-                                    <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
-                                        <Field
-                                            label="Username"
-                                            name="username"
-                                            component={TextField 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}
-                                            validate={MY_ACCOUNT_VALIDATION}
-                                            required
-                                        />
-                                    </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}
-                                            validate={MY_ACCOUNT_VALIDATION}
-                                            required
-                                        />
-                                    </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}
-                                        />
-                                    </Grid>
-                                    <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
-                                        <Field
-                                            label="Website"
-                                            name="prefs.profile.website_url"
-                                            component={TextField as any}
-                                        />
-                                    </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>
+                    <CardContent>
+                        <form onSubmit={this.props.handleSubmit}>
+                            <Grid container spacing={24}>
+                                <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+                                    <Field
+                                        label="First name"
+                                        name="firstName"
+                                        component={TextField as any}
+                                        disabled
+                                    />
+                                </Grid>
+                                <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+                                    <Field
+                                        label="Last name"
+                                        name="lastName"
+                                        component={TextField as any}
+                                        disabled
+                                    />
+                                </Grid>
+                                <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+                                    <Field
+                                        label="E-mail"
+                                        name="email"
+                                        component={TextField as any}
+                                        disabled
+                                    />
+                                </Grid>
+                                <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+                                    <Field
+                                        label="Username"
+                                        name="username"
+                                        component={TextField 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}
+                                        validate={MY_ACCOUNT_VALIDATION}
+                                        required
+                                        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}
+                                        validate={MY_ACCOUNT_VALIDATION}
+                                        required
+                                        disabled={!this.props.isAdmin && !this.props.isSelf}
+                                    />
+                                </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>
-                            </form >
-                        </CardContent>
-                    // </Card>
+                            </Grid>
+                        </form >
+                    </CardContent>
                 }
                 {this.state.value === 1 &&
                     <div className={this.props.classes.content}>
diff --git a/src/views/user-profile-panel/user-profile-panel.tsx b/src/views/user-profile-panel/user-profile-panel.tsx
index caac3e8c..2bafd9fa 100644
--- a/src/views/user-profile-panel/user-profile-panel.tsx
+++ b/src/views/user-profile-panel/user-profile-panel.tsx
@@ -23,7 +23,8 @@ const mapStateToProps = (state: RootState): UserProfilePanelRootDataProps => {
   // 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,

commit 925a083d8e82281a6d1de1f1021a88da147e5bac
Author: Stephen Smith <stephen at curii.com>
Date:   Wed Mar 2 17:21:31 2022 -0500

    18559: Fix user profile form initialization & cleanup
    
    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 103456f3..82e90a2b 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 { reset } 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';
@@ -25,9 +25,10 @@ export const loadUserProfilePanel = (userUuid?: string) =>
     // 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]));
-      await dispatch(propertiesActions.SET_PROPERTY({ key: USER_PROFILE_PANEL_ID, value: uuid }));
       dispatch(UserProfileGroupsActions.REQUEST_ITEMS());
     }
   }
diff --git a/src/views/user-panel/user-panel.tsx b/src/views/user-panel/user-panel.tsx
index be8ef681..9f4d4ce4 100644
--- a/src/views/user-panel/user-panel.tsx
+++ b/src/views/user-panel/user-panel.tsx
@@ -157,7 +157,7 @@ export const UserPanel = compose(
             render() {
                 const { value } = this.state;
                 return <Paper className={this.props.classes.root}>
-                    <Tabs value={value} onChange={this.handleChange} fullWidth>
+                    <Tabs value={value} onChange={this.handleChange} variant={"fullWidth"}>
                         <Tab label="USERS" />
                         <Tab label="ACTIVITY" disabled />
                     </Tabs>
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 c0c80e3c..a725333d 100644
--- a/src/views/user-profile-panel/user-profile-panel-root.tsx
+++ b/src/views/user-profile-panel/user-profile-panel-root.tsx
@@ -89,11 +89,6 @@ const RoleTypes = [
 
 type UserProfilePanelRootProps = InjectedFormProps<{}> & UserProfilePanelRootActionProps & UserProfilePanelRootDataProps & WithStyles<CssRules>;
 
-// type LocalClusterProp = { localCluster: string };
-// const renderField: React.ComponentType<WrappedFieldProps & LocalClusterProp> = ({ input, localCluster }) => (
-//     <span>{localCluster === input.value.substring(0, 5) ? "" : "federated"} user {input.value}</span>
-// );
-
 export enum UserProfileGroupsColumnNames {
     NAME = "Name",
     PERMISSION = "Permission",
@@ -152,10 +147,7 @@ export const UserProfilePanelRoot = withStyles(styles)(
 
         render() {
             return <Paper className={this.props.classes.root}>
-                {/* <Typography variant="title" className={this.props.classes.title}>
-                    Logged in as <Field name="uuid" component={renderField} localCluster={this.props.localCluster} />
-                </Typography> */}
-                <Tabs value={this.state.value} onChange={this.handleChange} fullWidth>
+                <Tabs value={this.state.value} onChange={this.handleChange} variant={"fullWidth"}>
                     <Tab label="PROFILE" />
                     <Tab label="GROUPS" />
                     <Tab label="ADMIN" />
@@ -254,7 +246,6 @@ export const UserProfilePanelRoot = withStyles(styles)(
                                 id={USER_PROFILE_PANEL_ID}
                                 onRowClick={noop}
                                 onRowDoubleClick={noop}
-                                // onContextMenu={this.handleContextMenu}
                                 contextMenuColumn={false}
                                 hideColumnSelector
                                 hideSearchInput
@@ -352,17 +343,5 @@ export const UserProfilePanelRoot = withStyles(styles)(
             this.setState({ value });
         }
 
-        handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
-            // 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
-            //     });
-            // }
-        }
     }
 );

commit 9da78c51275666c685545d29cd92ffa0d32f7b2f
Author: Stephen Smith <stephen at curii.com>
Date:   Wed Mar 2 15:44:47 2022 -0500

    18559: Link from users panel to user profile page.
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/routes/routes.ts b/src/routes/routes.ts
index b0eb7918..205ae08f 100644
--- a/src/routes/routes.ts
+++ b/src/routes/routes.ts
@@ -100,6 +100,8 @@ export const getProcessLogUrl = (uuid: string) => `/process-logs/${uuid}`;
 
 export const getGroupUrl = (uuid: string) => `/group/${uuid}`;
 
+export const getUserProfileUrl = (uuid: string) => `/user/${uuid}`;
+
 export interface ResourceRouteParams {
     id: string;
 }
diff --git a/src/store/navigation/navigation-action.ts b/src/store/navigation/navigation-action.ts
index 47d8e4fb..776409c0 100644
--- a/src/store/navigation/navigation-action.ts
+++ b/src/store/navigation/navigation-action.ts
@@ -6,7 +6,7 @@ import { Dispatch, compose, AnyAction } from 'redux';
 import { push } from "react-router-redux";
 import { ResourceKind, extractUuidKind } from 'models/resource';
 import { SidePanelTreeCategory } from '../side-panel-tree/side-panel-tree-actions';
-import { Routes, getProcessLogUrl, getGroupUrl, getNavUrl } from 'routes/routes';
+import { Routes, getProcessLogUrl, getGroupUrl, getNavUrl, getUserProfileUrl } from 'routes/routes';
 import { RootState } from 'store/store';
 import { ServiceRepository } from 'services/services';
 import { pluginConfig } from 'plugins';
@@ -144,6 +144,8 @@ export const navigateToKeepServices = push(Routes.KEEP_SERVICES);
 
 export const navigateToUsers = push(Routes.USERS);
 
+export const navigateToUserProfile = compose(push, getUserProfileUrl);
+
 export const navigateToApiClientAuthorizations = push(Routes.API_CLIENT_AUTHORIZATIONS);
 
 export const navigateToGroups = push(Routes.GROUPS);
diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx
index a2acaca4..7cdd9b83 100644
--- a/src/views-components/data-explorer/renderers.tsx
+++ b/src/views-components/data-explorer/renderers.tsx
@@ -195,7 +195,10 @@ const renderIsActive = (props: { uuid: string, kind: ResourceKind, isActive: boo
             color="primary"
             checked={props.isActive}
             disabled={!!props.disabled}
-            onClick={() => props.toggleIsActive(props.uuid)} />;
+            onClick={(e) => {
+                e.stopPropagation();
+                props.toggleIsActive(props.uuid)
+            }} />;
     } else {
         return <Typography />;
     }
@@ -230,7 +233,10 @@ const renderIsHidden = (props: {
                 color="primary"
                 checked={props.visible}
                 disabled={!props.canManage}
-                onClick={() => props.setMemberIsHidden(props.memberLinkUuid, props.permissionLinkUuid, !props.visible)} />;
+                onClick={(e) => {
+                    e.stopPropagation();
+                    props.setMemberIsHidden(props.memberLinkUuid, props.permissionLinkUuid, !props.visible);
+                }} />;
     } else {
         return <Typography />;
     }
@@ -263,7 +269,10 @@ const renderIsAdmin = (props: { uuid: string, isAdmin: boolean, toggleIsAdmin: (
     <Checkbox
         color="primary"
         checked={props.isAdmin}
-        onClick={() => props.toggleIsAdmin(props.uuid)} />;
+        onClick={(e) => {
+            e.stopPropagation();
+            props.toggleIsAdmin(props.uuid);
+        }} />;
 
 export const ResourceIsAdmin = connect(
     (state: RootState, props: { uuid: string }) => {
diff --git a/src/views/user-panel/user-panel.tsx b/src/views/user-panel/user-panel.tsx
index 5fb979a2..be8ef681 100644
--- a/src/views/user-panel/user-panel.tsx
+++ b/src/views/user-panel/user-panel.tsx
@@ -20,7 +20,7 @@ import {
     ResourceIsAdmin,
     ResourceUsername
 } from "views-components/data-explorer/renderers";
-import { navigateTo } from "store/navigation/navigation-action";
+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';
@@ -124,7 +124,7 @@ interface UserPanelDataProps {
 
 interface UserPanelActionProps {
     openUserCreateDialog: () => void;
-    handleRowDoubleClick: (uuid: string) => void;
+    handleRowClick: (uuid: string) => void;
     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: any) => void;
 }
 
@@ -136,7 +136,7 @@ const mapStateToProps = (state: RootState) => {
 
 const mapDispatchToProps = (dispatch: Dispatch) => ({
     openUserCreateDialog: () => dispatch<any>(openUserCreateDialog()),
-    handleRowDoubleClick: (uuid: string) => dispatch<any>(navigateTo(uuid)),
+    handleRowClick: (uuid: string) => dispatch<any>(navigateToUserProfile(uuid)),
     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: any) => dispatch<any>(openContextMenu(event, item))
 });
 
@@ -165,7 +165,7 @@ export const UserPanel = compose(
                         <div className={this.props.classes.content}>
                             <DataExplorer
                                 id={USERS_PANEL_ID}
-                                onRowClick={noop}
+                                onRowClick={this.props.handleRowClick}
                                 onRowDoubleClick={noop}
                                 onContextMenu={this.handleContextMenu}
                                 contextMenuColumn={true}
@@ -194,6 +194,7 @@ export const UserPanel = compose(
             }
 
             handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
+                event.stopPropagation();
                 const resource = getResource<UserResource>(resourceUuid)(this.props.resources);
                 if (resource) {
                     this.props.onContextMenu(event, {

commit addb01b6d7636a8963ddb1eff4799ebc96f44739
Author: Stephen Smith <stephen at curii.com>
Date:   Wed Mar 2 10:09:23 2022 -0500

    18559: Add groups and admin tab to user profile, use for other users profile
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/routes/route-change-handlers.ts b/src/routes/route-change-handlers.ts
index 044a38bf..b9158083 100644
--- a/src/routes/route-change-handlers.ts
+++ b/src/routes/route-change-handlers.ts
@@ -42,7 +42,8 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
     const apiClientAuthorizationsMatch = Routes.matchApiClientAuthorizationsRoute(pathname);
     const myAccountMatch = Routes.matchMyAccountRoute(pathname);
     const linkAccountMatch = Routes.matchLinkAccountRoute(pathname);
-    const userMatch = Routes.matchUsersRoute(pathname);
+    const usersMatch = Routes.matchUsersRoute(pathname);
+    const userProfileMatch = Routes.matchUserProfileRoute(pathname);
     const groupsMatch = Routes.matchGroupsRoute(pathname);
     const groupDetailsMatch = Routes.matchGroupDetailsRoute(pathname);
     const linksMatch = Routes.matchLinksRoute(pathname);
@@ -100,11 +101,13 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
     } else if (apiClientAuthorizationsMatch) {
         store.dispatch(WorkbenchActions.loadApiClientAuthorizations);
     } else if (myAccountMatch) {
-        store.dispatch(WorkbenchActions.loadMyAccount);
+        store.dispatch(WorkbenchActions.loadUserProfile());
     } else if (linkAccountMatch) {
         store.dispatch(WorkbenchActions.loadLinkAccount);
-    } else if (userMatch) {
+    } else if (usersMatch) {
         store.dispatch(WorkbenchActions.loadUsers);
+    } else if (userProfileMatch) {
+        store.dispatch(WorkbenchActions.loadUserProfile(userProfileMatch.params.id));
     } else if (groupsMatch) {
         store.dispatch(WorkbenchActions.loadGroupsPanel);
     } else if (groupDetailsMatch) {
diff --git a/src/routes/routes.ts b/src/routes/routes.ts
index 41c71f7c..b0eb7918 100644
--- a/src/routes/routes.ts
+++ b/src/routes/routes.ts
@@ -40,6 +40,7 @@ export const Routes = {
     LINK_ACCOUNT: '/link_account',
     KEEP_SERVICES: `/keep-services`,
     USERS: '/users',
+    USER_PROFILE: `/user/:id(${RESOURCE_UUID_PATTERN})`,
     API_CLIENT_AUTHORIZATIONS: `/api_client_authorizations`,
     GROUPS: '/groups',
     GROUP_DETAILS: `/group/:id(${RESOURCE_UUID_PATTERN})`,
@@ -175,6 +176,9 @@ export const matchFedTokenRoute = (route: string) =>
 export const matchUsersRoute = (route: string) =>
     matchPath(route, { path: Routes.USERS });
 
+export const matchUserProfileRoute = (route: string) =>
+    matchPath<ResourceRouteParams>(route, { path: Routes.USER_PROFILE });
+
 export const matchApiClientAuthorizationsRoute = (route: string) =>
     matchPath(route, { path: Routes.API_CLIENT_AUTHORIZATIONS });
 
diff --git a/src/store/breadcrumbs/breadcrumbs-actions.ts b/src/store/breadcrumbs/breadcrumbs-actions.ts
index 72e908aa..69179272 100644
--- a/src/store/breadcrumbs/breadcrumbs-actions.ts
+++ b/src/store/breadcrumbs/breadcrumbs-actions.ts
@@ -17,6 +17,7 @@ import { updateResources } from '../resources/resources-actions';
 import { ResourceKind } from 'models/resource';
 import { GroupResource } from 'models/group';
 import { extractUuidKind } from 'models/resource';
+import { UserResource } from 'models/user';
 
 export const BREADCRUMBS = 'breadcrumbs';
 
@@ -112,21 +113,47 @@ export const setProcessBreadcrumbs = (processUuid: string) =>
         }
     };
 
-export const GROUPS_PANEL_LABEL = 'Groups';
-
 export const setGroupsBreadcrumbs = () =>
-    setBreadcrumbs([{ label: GROUPS_PANEL_LABEL }]);
+    setBreadcrumbs([{ label: SidePanelTreeCategory.GROUPS }]);
 
 export const setGroupDetailsBreadcrumbs = (groupUuid: string) =>
-    (dispatch: Dispatch, getState: () => RootState) => {
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
 
         const group = getResource<GroupResource>(groupUuid)(getState().resources);
 
         const breadcrumbs: ResourceBreadcrumb[] = [
-            { label: GROUPS_PANEL_LABEL, uuid: GROUPS_PANEL_LABEL },
-            { label: group ? group.name : groupUuid, uuid: groupUuid },
+            { label: SidePanelTreeCategory.GROUPS, uuid: SidePanelTreeCategory.GROUPS },
+            { label: group ? group.name : (await services.groupsService.get(groupUuid)).name, uuid: groupUuid },
         ];
 
         dispatch(setBreadcrumbs(breadcrumbs));
 
     };
+
+export const USERS_PANEL_LABEL = 'Users';
+
+export const setUsersBreadcrumbs = () =>
+    setBreadcrumbs([{ label: USERS_PANEL_LABEL, uuid: USERS_PANEL_LABEL }]);
+
+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));
+
+    };
+
+export const MY_ACCOUNT_PANEL_LABEL = 'My Account';
+
+export const setMyAccountBreadcrumbs = () =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(setBreadcrumbs([
+            { label: MY_ACCOUNT_PANEL_LABEL, uuid: MY_ACCOUNT_PANEL_LABEL },
+        ]));
+    };
diff --git a/src/store/group-details-panel/group-details-panel-actions.ts b/src/store/group-details-panel/group-details-panel-actions.ts
index 71ca67c5..f72ac49c 100644
--- a/src/store/group-details-panel/group-details-panel-actions.ts
+++ b/src/store/group-details-panel/group-details-panel-actions.ts
@@ -16,7 +16,7 @@ import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
 import { LinkResource } from 'models/link';
 import { deleteResources, updateResources } from 'store/resources/resources-actions';
 import { openSharingDialog } from 'store/sharing-dialog/sharing-dialog-actions';
-// import { UserProfileGroupsActions } from 'store/user-profile/user-profile-actions';
+import { UserProfileGroupsActions } from 'store/user-profile/user-profile-actions';
 
 export const GROUP_DETAILS_MEMBERS_PANEL_ID = 'groupDetailsMembersPanel';
 export const GROUP_DETAILS_PERMISSIONS_PANEL_ID = 'groupDetailsPermissionsPanel';
@@ -93,7 +93,7 @@ export const removeGroupMember = (uuid: string) =>
         });
         dispatch<any>(deleteResources([uuid]));
         dispatch(GroupMembersPanelActions.REQUEST_ITEMS());
-        // dispatch(UserProfileGroupsActions.REQUEST_ITEMS());
+        dispatch(UserProfileGroupsActions.REQUEST_ITEMS());
 
         dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removed.', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
     };
diff --git a/src/store/navigation/navigation-action.ts b/src/store/navigation/navigation-action.ts
index 19cc36ae..47d8e4fb 100644
--- a/src/store/navigation/navigation-action.ts
+++ b/src/store/navigation/navigation-action.ts
@@ -11,6 +11,7 @@ import { RootState } from 'store/store';
 import { ServiceRepository } from 'services/services';
 import { pluginConfig } from 'plugins';
 import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
+import { USERS_PANEL_LABEL, MY_ACCOUNT_PANEL_LABEL } from 'store/breadcrumbs/breadcrumbs-actions';
 
 const navigationNotAvailable = (id: string) =>
     snackbarActions.OPEN_SNACKBAR({
@@ -69,6 +70,12 @@ export const navigateTo = (uuid: string) =>
             case SidePanelTreeCategory.ALL_PROCESSES:
                 dispatch(navigateToAllProcesses);
                 return;
+            case USERS_PANEL_LABEL:
+                dispatch(navigateToUsers);
+                return;
+            case MY_ACCOUNT_PANEL_LABEL:
+                dispatch(navigateToMyAccount);
+                return;
         }
 
         dispatch(navigationNotAvailable(uuid));
diff --git a/src/store/store.ts b/src/store/store.ts
index 688c8a05..94f110a0 100644
--- a/src/store/store.ts
+++ b/src/store/store.ts
@@ -49,6 +49,8 @@ import { repositoriesReducer } from 'store/repositories/repositories-reducer';
 import { keepServicesReducer } from 'store/keep-services/keep-services-reducer';
 import { UserMiddlewareService } from 'store/users/user-panel-middleware-service';
 import { USERS_PANEL_ID } from 'store/users/users-actions';
+import { UserProfileGroupsMiddlewareService } from 'store/user-profile/user-profile-groups-middleware-service';
+import { USER_PROFILE_PANEL_ID } from 'store/user-profile/user-profile-actions'
 import { GroupsPanelMiddlewareService } from 'store/groups-panel/groups-panel-middleware-service';
 import { GROUPS_PANEL_ID } from 'store/groups-panel/groups-panel-actions';
 import { GroupDetailsPanelMembersMiddlewareService } from 'store/group-details-panel/group-details-panel-members-middleware-service';
@@ -114,6 +116,9 @@ export function configureStore(history: History, services: ServiceRepository, co
     const userPanelMiddleware = dataExplorerMiddleware(
         new UserMiddlewareService(services, USERS_PANEL_ID)
     );
+    const userProfileGroupsMiddleware = dataExplorerMiddleware(
+        new UserProfileGroupsMiddlewareService(services, USER_PROFILE_PANEL_ID)
+    );
     const groupsPanelMiddleware = dataExplorerMiddleware(
         new GroupsPanelMiddlewareService(services, GROUPS_PANEL_ID)
     );
@@ -160,6 +165,7 @@ export function configureStore(history: History, services: ServiceRepository, co
         sharedWithMePanelMiddleware,
         workflowPanelMiddleware,
         userPanelMiddleware,
+        userProfileGroupsMiddleware,
         groupsPanelMiddleware,
         groupDetailsPanelMembersMiddleware,
         groupDetailsPanelPermissionsMiddleware,
diff --git a/src/store/user-profile/user-profile-actions.ts b/src/store/user-profile/user-profile-actions.ts
new file mode 100644
index 00000000..103456f3
--- /dev/null
+++ b/src/store/user-profile/user-profile-actions.ts
@@ -0,0 +1,79 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+import { RootState } from "store/store";
+import { Dispatch } from 'redux';
+import { 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';
+import { getProperty } from 'store/properties/properties';
+import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
+import { updateResources } from "store/resources/resources-actions";
+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 UserProfileGroupsActions = bindDataExplorerActions(USER_PROFILE_PANEL_ID);
+
+export const getCurrentUserProfilePanelUuid = getProperty<string>(USER_PROFILE_PANEL_ID);
+
+export const loadUserProfilePanel = (userUuid?: string) =>
+  async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    // Get user uuid from route or use current user uuid
+    const uuid = userUuid || getState().auth.user?.uuid;
+    if (uuid) {
+      const user = await services.userService.get(uuid);
+      dispatch(updateResources([user]));
+      await dispatch(propertiesActions.SET_PROPERTY({ key: USER_PROFILE_PANEL_ID, value: uuid }));
+      dispatch(UserProfileGroupsActions.REQUEST_ITEMS());
+    }
+  }
+
+export const saveEditedUser = (resource: any) =>
+  async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+      try {
+          const user = await services.userService.update(resource.uuid, resource);
+          dispatch(updateResources([user]));
+          dispatch(reset(USER_PROFILE_FORM));
+          dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Profile has been updated.", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
+      } catch (e) {
+          dispatch(snackbarActions.OPEN_SNACKBAR({
+              message: "Could not update profile",
+              kind: SnackbarKind.ERROR,
+          }));
+      }
+  };
+
+export const openDeactivateDialog = (uuid: string) =>
+  (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+    dispatch(dialogActions.OPEN_DIALOG({
+      id: DEACTIVATE_DIALOG,
+      data: {
+          title: 'Deactivate user',
+          text: 'Are you sure you want to deactivate this user?',
+          confirmButtonLabel: 'Deactvate',
+          uuid
+      }
+  }));
+}
+
+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,
+          }));
+        }
+    };
diff --git a/src/store/user-profile/user-profile-groups-middleware-service.ts b/src/store/user-profile/user-profile-groups-middleware-service.ts
new file mode 100644
index 00000000..47c63901
--- /dev/null
+++ b/src/store/user-profile/user-profile-groups-middleware-service.ts
@@ -0,0 +1,61 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ServiceRepository } from 'services/services';
+import { MiddlewareAPI, Dispatch } from 'redux';
+import { DataExplorerMiddlewareService, listResultsToDataExplorerItemsMeta } from 'store/data-explorer/data-explorer-middleware-service';
+import { RootState } from 'store/store';
+import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
+import { getCurrentUserProfilePanelUuid, UserProfileGroupsActions } from 'store/user-profile/user-profile-actions';
+import { updateResources } from 'store/resources/resources-actions';
+import { FilterBuilder } from 'services/api/filter-builder';
+import { LinkClass } from 'models/link';
+import { ResourceKind } from 'models/resource';
+
+export class UserProfileGroupsMiddlewareService extends DataExplorerMiddlewareService {
+    constructor(private services: ServiceRepository, id: string) {
+        super(id);
+    }
+
+    async requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
+        const state = api.getState();
+        const userUuid = getCurrentUserProfilePanelUuid(state.properties);
+        try {
+            api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
+
+            // Get user
+            const user = await this.services.userService.get(userUuid || '');
+            api.dispatch(updateResources([user]));
+
+            // Get user's group memberships
+            const groupMembershipLinks = await this.services.permissionService.list({
+                filters: new FilterBuilder()
+                    .addEqual('tail_uuid', userUuid)
+                    .addEqual('link_class', LinkClass.PERMISSION)
+                    .addEqual('head_kind', ResourceKind.GROUP)
+                    .getFilters()
+            });
+            api.dispatch(updateResources(groupMembershipLinks.items));
+
+            // Get user's groups details
+            const groups = await this.services.groupsService.list({
+                filters: new FilterBuilder()
+                    .addIn('uuid', groupMembershipLinks.items
+                        .map(item => item.headUuid))
+                    .getFilters(),
+                count: "none"
+            });
+            api.dispatch(updateResources(groups.items));
+
+            api.dispatch(UserProfileGroupsActions.SET_ITEMS({
+                ...listResultsToDataExplorerItemsMeta(groupMembershipLinks),
+                items: groupMembershipLinks.items.map(item => item.uuid),
+            }));
+        } catch {
+            // api.dispatch(couldNotFetchUsers());
+        } finally {
+            api.dispatch(progressIndicatorActions.STOP_WORKING(this.getId()));
+        }
+    }
+}
diff --git a/src/store/users/users-actions.ts b/src/store/users/users-actions.ts
index cd4d5c73..fded1140 100644
--- a/src/store/users/users-actions.ts
+++ b/src/store/users/users-actions.ts
@@ -8,13 +8,16 @@ 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 } from "redux-form";
+import { startSubmit, reset, initialize, 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 } from "store/virtual-machines/virtual-machines-actions";
+import { PermissionLevel } from "models/permission";
+import { updateResources } from "store/resources/resources-actions";
 
 export const USERS_PANEL_ID = 'usersPanel';
 export const USER_ATTRIBUTES_DIALOG = 'userAttributesDialog';
@@ -28,11 +31,7 @@ export interface UserCreateFormDialogData {
     groupVirtualMachine: string;
 }
 
-export interface SetupShellAccountFormDialogData {
-    email: string;
-    virtualMachineName: string;
-    groupVirtualMachine: string;
-}
+export const userBindedActions = bindDataExplorerActions(USERS_PANEL_ID);
 
 export const openUserAttributes = (uuid: string) =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
@@ -53,8 +52,8 @@ export const openSetupShellAccount = (uuid: string) =>
         const { resources } = getState();
         const user = getResource<UserResource>(uuid)(resources);
         const virtualMachines = await services.virtualMachineService.list();
-        dispatch(dialogActions.CLOSE_DIALOG({ id: USER_MANAGEMENT_DIALOG }));
-        dispatch(dialogActions.OPEN_DIALOG({ id: SETUP_SHELL_ACCOUNT_DIALOG, data: { user, ...virtualMachines } }));
+        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) =>
@@ -84,7 +83,6 @@ export const openUserProjects = (uuid: string) =>
         dispatch<any>(navigateTo(uuid));
     };
 
-
 export const createUser = (user: UserCreateFormDialogData) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(startSubmit(USER_CREATE_FORM_NAME));
@@ -101,20 +99,32 @@ export const createUser = (user: UserCreateFormDialogData) =>
         }
     };
 
-
-export const setupUserVM = (setupData: SetupShellAccountFormDialogData) =>
+export const setupUserVM = (setupData: AddLoginFormData) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        dispatch(startSubmit(USER_CREATE_FORM_NAME));
+        dispatch(startSubmit(SETUP_SHELL_ACCOUNT_DIALOG));
         try {
-            // TODO: make correct API call
-            // const setupResult = await services.userService.setup({ ...setupData });
+            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 }));
-            dispatch<any>(loadUsersPanel());
-            dispatch(userBindedActions.REQUEST_ITEMS());
         } catch (e) {
-            return;
+            dispatch(stopSubmit(SETUP_SHELL_ACCOUNT_DIALOG));
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000, kind: SnackbarKind.ERROR }));
         }
     };
 
@@ -154,13 +164,6 @@ export const toggleIsAdmin = (uuid: string) =>
         return newActivity;
     };
 
-export const userBindedActions = bindDataExplorerActions(USERS_PANEL_ID);
-
-export const loadUsersData = () =>
-    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        await services.userService.list({ count: "none" });
-    };
-
 export const loadUsersPanel = () =>
     (dispatch: Dispatch) => {
         dispatch(userBindedActions.REQUEST_ITEMS());
diff --git a/src/store/virtual-machines/virtual-machines-actions.ts b/src/store/virtual-machines/virtual-machines-actions.ts
index e2cf6fd4..7034b4a5 100644
--- a/src/store/virtual-machines/virtual-machines-actions.ts
+++ b/src/store/virtual-machines/virtual-machines-actions.ts
@@ -157,7 +157,7 @@ export const addUpdateVirtualMachineLogin = ({uuid, vmUuid, user, groups}: AddLo
                 dispatch(updateResources([permission]));
             } else {
                 const permission = await services.permissionService.create({
-                headUuid: vmUuid,
+                    headUuid: vmUuid,
                     tailUuid: userResource.uuid,
                     name: PermissionLevel.CAN_LOGIN,
                     properties: {
diff --git a/src/store/workbench/workbench-actions.ts b/src/store/workbench/workbench-actions.ts
index 98508f75..5e83ed7b 100644
--- a/src/store/workbench/workbench-actions.ts
+++ b/src/store/workbench/workbench-actions.ts
@@ -31,7 +31,10 @@ import {
     setProcessBreadcrumbs,
     setSharedWithMeBreadcrumbs,
     setSidePanelBreadcrumbs,
-    setTrashBreadcrumbs
+    setTrashBreadcrumbs,
+    setUsersBreadcrumbs,
+    setMyAccountBreadcrumbs,
+    setUserProfileBreadcrumbs,
 } from 'store/breadcrumbs/breadcrumbs-actions';
 import { navigateTo, navigateToRootProject } from 'store/navigation/navigation-action';
 import { MoveToFormDialogData } from 'store/move-to-dialog/move-to-dialog';
@@ -58,7 +61,6 @@ import {
 import { CopyFormDialogData } from 'store/copy-dialog/copy-dialog';
 import { loadWorkflowPanel, workflowPanelActions } from 'store/workflow-panel/workflow-panel-actions';
 import { loadSshKeysPanel } from 'store/auth/auth-action-ssh';
-import { loadMyAccountPanel } from 'store/my-account/my-account-panel-actions';
 import { loadLinkAccountPanel, linkAccountPanelActions } from 'store/link-account-panel/link-account-panel-actions';
 import { loadSiteManagerPanel } from 'store/auth/auth-action-session';
 import { workflowPanelColumns } from 'views/workflow-panel/workflow-panel-view';
@@ -80,6 +82,7 @@ import { loadVirtualMachinesPanel } from 'store/virtual-machines/virtual-machine
 import { loadRepositoriesPanel } from 'store/repositories/repositories-actions';
 import { loadKeepServicesPanel } from 'store/keep-services/keep-services-actions';
 import { loadUsersPanel, userBindedActions } from 'store/users/users-actions';
+import * as userProfilePanelActions from 'store/user-profile/user-profile-actions';
 import { linkPanelActions, loadLinkPanel } from 'store/link-panel/link-panel-actions';
 import { linkPanelColumns } from 'views/link-panel/link-panel-root';
 import { userPanelColumns } from 'views/user-panel/user-panel';
@@ -101,6 +104,7 @@ import { allProcessesPanelColumns } from 'views/all-processes-panel/all-processe
 import { collectionPanelFilesAction } from '../collection-panel/collection-panel-files/collection-panel-files-actions';
 import { createTree } from 'models/tree';
 import { AdminMenuIcon } from 'components/icon/icon';
+import { userProfileGroupsColumns } from 'views/user-profile-panel/user-profile-panel-root';
 
 export const WORKBENCH_LOADING_SCREEN = 'workbenchLoadingScreen';
 
@@ -139,6 +143,7 @@ export const loadWorkbench = () =>
             dispatch(groupPanelActions.GroupsPanelActions.SET_COLUMNS({ columns: groupsPanelColumns }));
             dispatch(groupDetailsPanelActions.GroupMembersPanelActions.SET_COLUMNS({ columns: groupDetailsMembersPanelColumns }));
             dispatch(groupDetailsPanelActions.GroupPermissionsPanelActions.SET_COLUMNS({ columns: groupDetailsPermissionsPanelColumns }));
+            dispatch(userProfilePanelActions.UserProfileGroupsActions.SET_COLUMNS({ columns: userProfileGroupsColumns }));
             dispatch(linkPanelActions.SET_COLUMNS({ columns: linkPanelColumns }));
             dispatch(apiClientAuthorizationsActions.SET_COLUMNS({ columns: apiClientAuthorizationPanelColumns }));
             dispatch(collectionsContentAddressActions.SET_COLUMNS({ columns: collectionContentAddressPanelColumns }));
@@ -514,10 +519,18 @@ export const loadSiteManager = handleFirstTimeLoad(
         await dispatch(loadSiteManagerPanel());
     });
 
-export const loadMyAccount = handleFirstTimeLoad(
-    (dispatch: Dispatch<any>) => {
-        dispatch(loadMyAccountPanel());
-    });
+export const loadUserProfile = (userUuid?: string) =>
+    handleFirstTimeLoad(
+        (dispatch: Dispatch<any>) => {
+            if (userUuid) {
+                dispatch(setUserProfileBreadcrumbs(userUuid));
+                dispatch(userProfilePanelActions.loadUserProfilePanel(userUuid));
+            } else {
+                dispatch(setMyAccountBreadcrumbs());
+                dispatch(userProfilePanelActions.loadUserProfilePanel());
+            }
+        }
+    );
 
 export const loadLinkAccount = handleFirstTimeLoad(
     (dispatch: Dispatch<any>) => {
@@ -532,7 +545,7 @@ export const loadKeepServices = handleFirstTimeLoad(
 export const loadUsers = handleFirstTimeLoad(
     async (dispatch: Dispatch<any>) => {
         await dispatch(loadUsersPanel());
-        dispatch(setBreadcrumbs([{ label: 'Users' }]));
+        dispatch(setUsersBreadcrumbs());
     });
 
 export const loadApiClientAuthorizations = handleFirstTimeLoad(
diff --git a/src/views-components/dialog-forms/setup-shell-account-dialog.tsx b/src/views-components/dialog-forms/setup-shell-account-dialog.tsx
index 3bf700ba..666ea38e 100644
--- a/src/views-components/dialog-forms/setup-shell-account-dialog.tsx
+++ b/src/views-components/dialog-forms/setup-shell-account-dialog.tsx
@@ -8,15 +8,17 @@ 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 { USER_LENGTH_VALIDATION, CHOOSE_VM_VALIDATION } from 'validators/validators';
+import { CHOOSE_VM_VALIDATION } from 'validators/validators';
 import { InputLabel } from '@material-ui/core';
 import { NativeSelectField } from 'components/select-field/select-field';
-import { SetupShellAccountFormDialogData, SETUP_SHELL_ACCOUNT_DIALOG, setupUserVM } from 'store/users/users-actions';
+import { SETUP_SHELL_ACCOUNT_DIALOG, setupUserVM } from 'store/users/users-actions';
 import { UserResource } from 'models/user';
+import { VIRTUAL_MACHINE_ADD_LOGIN_USER_FIELD, VIRTUAL_MACHINE_ADD_LOGIN_VM_FIELD, VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD, AddLoginFormData } from 'store/virtual-machines/virtual-machines-actions';
+import { GroupArrayInput } from 'views-components/virtual-machines-dialog/group-array-input';
 
 export const SetupShellAccountDialog = compose(
     withDialog(SETUP_SHELL_ACCOUNT_DIALOG),
-    reduxForm<SetupShellAccountFormDialogData>({
+    reduxForm<AddLoginFormData>({
         form: SETUP_SHELL_ACCOUNT_DIALOG,
         onSubmit: (data, dispatch) => {
             dispatch(setupUserVM(data));
@@ -32,12 +34,6 @@ export const SetupShellAccountDialog = compose(
         />
 );
 
-interface UserProps {
-    data: {
-        user: UserResource;
-    };
-}
-
 interface VirtualMachinesProps {
     data: {
         items: VirtualMachinesResource[];
@@ -48,39 +44,39 @@ interface DataProps {
     items: VirtualMachinesResource[];
 }
 
-const UserEmailField = ({ data }: UserProps) =>
+const UserNameField = () =>
     <span>
+        <InputLabel>VM Login</InputLabel>
         <Field
-            name='email'
+            name={`${VIRTUAL_MACHINE_ADD_LOGIN_USER_FIELD}.username`}
             component={TextField as any}
-            disabled
-            label={data.user.email} /></span>;
+            disabled /></span>;
 
 const UserVirtualMachineField = ({ data }: VirtualMachinesProps) =>
     <div style={{ marginBottom: '21px' }}>
         <InputLabel>Virtual Machine</InputLabel>
         <Field
-            name='virtualMachine'
+            name={VIRTUAL_MACHINE_ADD_LOGIN_VM_FIELD}
             component={NativeSelectField as any}
             validate={CHOOSE_VM_VALIDATION}
             items={getVirtualMachinesList(data.items)} />
     </div>;
 
 const UserGroupsVirtualMachineField = () =>
-    <Field
-        name='groups'
-        component={TextField as any}
-        validate={USER_LENGTH_VALIDATION}
-        label="Groups for virtual machine (comma separated list)" />;
+    <GroupArrayInput
+        name={VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD}
+        input={{id:"Add groups to VM login (eg: docker, sudo)", disabled:false}}
+        required={false}
+    />
 
 const getVirtualMachinesList = (virtualMachines: VirtualMachinesResource[]) =>
-    [{ key: "", value: "" }].concat(virtualMachines.map(it => ({ key: it.hostname, value: it.hostname })));
+    [{ key: "", value: "" }].concat(virtualMachines.map(it => ({ key: it.uuid, value: it.hostname })));
 
-type SetupShellAccountDialogComponentProps = WithDialogProps<{}> & InjectedFormProps<SetupShellAccountFormDialogData>;
+type SetupShellAccountDialogComponentProps = WithDialogProps<{}> & InjectedFormProps<AddLoginFormData>;
 
 const SetupShellAccountFormFields = (props: SetupShellAccountDialogComponentProps) =>
     <>
-        <UserEmailField data={props.data as DataProps} />
+        <UserNameField />
         <UserVirtualMachineField data={props.data as DataProps} />
         <UserGroupsVirtualMachineField />
     </>;
diff --git a/src/views-components/user-dialog/deactivate-dialog.tsx b/src/views-components/user-dialog/deactivate-dialog.tsx
new file mode 100644
index 00000000..6babf367
--- /dev/null
+++ b/src/views-components/user-dialog/deactivate-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 { unsetup, 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));
+    }
+});
+
+export const DeactivateDialog = compose(
+    withDialog(DEACTIVATE_DIALOG),
+    connect(null, mapDispatchToProps)
+)(ConfirmationDialog);
diff --git a/src/views-components/user-dialog/manage-dialog.tsx b/src/views-components/user-dialog/manage-dialog.tsx
index b812f5cb..a62e1a21 100644
--- a/src/views-components/user-dialog/manage-dialog.tsx
+++ b/src/views-components/user-dialog/manage-dialog.tsx
@@ -10,7 +10,8 @@ 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, openSetupShellAccount, loginAs } from "store/users/users-actions";
+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';
diff --git a/src/views/my-account-panel/my-account-panel-root.tsx b/src/views/my-account-panel/my-account-panel-root.tsx
deleted file mode 100644
index 283b9acc..00000000
--- a/src/views/my-account-panel/my-account-panel-root.tsx
+++ /dev/null
@@ -1,164 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import React from 'react';
-import { Field, InjectedFormProps, WrappedFieldProps } from "redux-form";
-import { TextField } from "components/text-field/text-field";
-import { NativeSelectField } from "components/select-field/select-field";
-import {
-    StyleRulesCallback,
-    WithStyles,
-    withStyles,
-    Card,
-    CardContent,
-    Button,
-    Typography,
-    Grid,
-    InputLabel
-} from '@material-ui/core';
-import { ArvadosTheme } from 'common/custom-theme';
-import { User } from "models/user";
-import { MY_ACCOUNT_VALIDATION } from "validators/validators";
-
-type CssRules = 'root' | 'gridItem' | 'label' | 'title' | 'actions';
-
-const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
-    root: {
-        width: '100%',
-        overflow: 'auto'
-    },
-    gridItem: {
-        height: 45,
-        marginBottom: 20
-    },
-    label: {
-        fontSize: '0.675rem'
-    },
-    title: {
-        marginBottom: theme.spacing.unit * 3,
-        color: theme.palette.grey["600"]
-    },
-    actions: {
-        display: 'flex',
-        justifyContent: 'flex-end'
-    }
-});
-
-export interface MyAccountPanelRootActionProps { }
-
-export interface MyAccountPanelRootDataProps {
-    isPristine: boolean;
-    isValid: boolean;
-    initialValues?: User;
-    localCluster: string;
-}
-
-const RoleTypes = [
-    { key: 'Bio-informatician', value: 'Bio-informatician' },
-    { key: 'Data Scientist', value: 'Data Scientist' },
-    { key: 'Analyst', value: 'Analyst' },
-    { key: 'Researcher', value: 'Researcher' },
-    { key: 'Software Developer', value: 'Software Developer' },
-    { key: 'System Administrator', value: 'System Administrator' },
-    { key: 'Other', value: 'Other' }
-];
-
-type MyAccountPanelRootProps = InjectedFormProps<MyAccountPanelRootActionProps> & MyAccountPanelRootDataProps & WithStyles<CssRules>;
-
-type LocalClusterProp = { localCluster: string };
-const renderField: React.ComponentType<WrappedFieldProps & LocalClusterProp> = ({ input, localCluster }) => (
-    <span>{localCluster === input.value.substring(0, 5) ? "" : "federated"} user {input.value}</span>
-);
-
-export const MyAccountPanelRoot = withStyles(styles)(
-    ({ classes, isValid, handleSubmit, reset, isPristine, invalid, submitting, localCluster }: MyAccountPanelRootProps) => {
-        return <Card className={classes.root}>
-            <CardContent>
-                <Typography variant="title" className={classes.title}>
-                    Logged in as <Field name="uuid" component={renderField} localCluster={localCluster} />
-                </Typography>
-                <form onSubmit={handleSubmit}>
-                    <Grid container spacing={24}>
-                        <Grid item className={classes.gridItem} sm={6} xs={12}>
-                            <Field
-                                label="First name"
-                                name="firstName"
-                                component={TextField as any}
-                                disabled
-                            />
-                        </Grid>
-                        <Grid item className={classes.gridItem} sm={6} xs={12}>
-                            <Field
-                                label="Last name"
-                                name="lastName"
-                                component={TextField as any}
-                                disabled
-                            />
-                        </Grid>
-                        <Grid item className={classes.gridItem} sm={6} xs={12}>
-                            <Field
-                                label="E-mail"
-                                name="email"
-                                component={TextField as any}
-                                disabled
-                            />
-                        </Grid>
-                        <Grid item className={classes.gridItem} sm={6} xs={12}>
-                            <Field
-                                label="Username"
-                                name="username"
-                                component={TextField as any}
-                                disabled
-                            />
-                        </Grid>
-                        <Grid item className={classes.gridItem} sm={6} xs={12}>
-                            <Field
-                                label="Organization"
-                                name="prefs.profile.organization"
-                                component={TextField as any}
-                                validate={MY_ACCOUNT_VALIDATION}
-                                required
-                            />
-                        </Grid>
-                        <Grid item className={classes.gridItem} sm={6} xs={12}>
-                            <Field
-                                label="E-mail at Organization"
-                                name="prefs.profile.organization_email"
-                                component={TextField as any}
-                                validate={MY_ACCOUNT_VALIDATION}
-                                required
-                            />
-                        </Grid>
-                        <Grid item className={classes.gridItem} sm={6} xs={12}>
-                            <InputLabel className={classes.label} htmlFor="prefs.profile.role">Role</InputLabel>
-                            <Field
-                                id="prefs.profile.role"
-                                name="prefs.profile.role"
-                                component={NativeSelectField as any}
-                                items={RoleTypes}
-                            />
-                        </Grid>
-                        <Grid item className={classes.gridItem} sm={6} xs={12}>
-                            <Field
-                                label="Website"
-                                name="prefs.profile.website_url"
-                                component={TextField as any}
-                            />
-                        </Grid>
-                        <Grid container direction="row" justify="flex-end" >
-                            <Button color="primary" onClick={reset} disabled={isPristine}>Discard changes</Button>
-                            <Button
-                                color="primary"
-                                variant="contained"
-                                type="submit"
-                                disabled={isPristine || invalid || submitting}>
-                                Save changes
-                            </Button>
-                        </Grid>
-                    </Grid>
-                </form >
-            </CardContent >
-        </Card >;
-    }
-);
diff --git a/src/views/my-account-panel/my-account-panel.tsx b/src/views/my-account-panel/my-account-panel.tsx
deleted file mode 100644
index 2421a28a..00000000
--- a/src/views/my-account-panel/my-account-panel.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { RootState } from 'store/store';
-import { compose } from 'redux';
-import { reduxForm, isPristine, isValid } from 'redux-form';
-import { connect } from 'react-redux';
-import { saveEditedUser } from 'store/my-account/my-account-panel-actions';
-import { MyAccountPanelRoot, MyAccountPanelRootDataProps } from 'views/my-account-panel/my-account-panel-root';
-import { MY_ACCOUNT_FORM } from "store/my-account/my-account-panel-actions";
-
-const mapStateToProps = (state: RootState): MyAccountPanelRootDataProps => ({
-    isPristine: isPristine(MY_ACCOUNT_FORM)(state),
-    isValid: isValid(MY_ACCOUNT_FORM)(state),
-    initialValues: state.auth.user,
-    localCluster: state.auth.localCluster
-});
-
-export const MyAccountPanel = compose(
-    connect(mapStateToProps),
-    reduxForm({
-        form: MY_ACCOUNT_FORM,
-        onSubmit: (data, dispatch) => {
-            dispatch(saveEditedUser(data));
-        }
-    }))(MyAccountPanelRoot);
diff --git a/src/views/user-profile-panel/user-profile-panel-root.tsx b/src/views/user-profile-panel/user-profile-panel-root.tsx
new file mode 100644
index 00000000..c0c80e3c
--- /dev/null
+++ b/src/views/user-profile-panel/user-profile-panel-root.tsx
@@ -0,0 +1,368 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import { Field, InjectedFormProps } from "redux-form";
+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";
+import {
+    StyleRulesCallback,
+    WithStyles,
+    withStyles,
+    Card,
+    CardContent,
+    Button,
+    Typography,
+    Grid,
+    InputLabel,
+    Tabs, Tab,
+    Paper
+} 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 { MY_ACCOUNT_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 { DataColumns } from 'components/data-table/data-table';
+import { ResourceLinkHeadUuid, ResourceLinkHeadPermissionLevel, ResourceLinkHead, ResourceLinkDelete, ResourceLinkTailIsVisible } from 'views-components/data-explorer/renderers';
+import { createTree } from 'models/tree';
+
+type CssRules = 'root' | 'adminRoot' | 'gridItem' | 'label' | 'title' | 'description' | 'actions' | 'content';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    root: {
+        width: '100%',
+        overflow: 'auto'
+    },
+    adminRoot: {
+        // ...theme.mixins.gutters()
+    },
+    gridItem: {
+        height: 45,
+        marginBottom: 20
+    },
+    label: {
+        fontSize: '0.675rem'
+    },
+    title: {
+        fontSize: '1.1rem',
+    },
+    description: {
+        color: theme.palette.grey["600"]
+    },
+    actions: {
+        display: 'flex',
+        justifyContent: 'flex-end'
+    },
+    content: {
+        // reserve space for the tab bar
+        height: `calc(100% - ${theme.spacing.unit * 7}px)`,
+    }
+});
+
+export interface UserProfilePanelRootActionProps {
+    openSetupShellAccount: (uuid: string) => void;
+    loginAs: (uuid: string) => void;
+    openDeactivateDialog: (uuid: string) => void;
+}
+
+export interface UserProfilePanelRootDataProps {
+    isPristine: boolean;
+    isValid: boolean;
+    initialValues?: User;
+    localCluster: string;
+}
+
+const RoleTypes = [
+    { key: 'Bio-informatician', value: 'Bio-informatician' },
+    { key: 'Data Scientist', value: 'Data Scientist' },
+    { key: 'Analyst', value: 'Analyst' },
+    { key: 'Researcher', value: 'Researcher' },
+    { key: 'Software Developer', value: 'Software Developer' },
+    { key: 'System Administrator', value: 'System Administrator' },
+    { key: 'Other', value: 'Other' }
+];
+
+type UserProfilePanelRootProps = InjectedFormProps<{}> & UserProfilePanelRootActionProps & UserProfilePanelRootDataProps & WithStyles<CssRules>;
+
+// type LocalClusterProp = { localCluster: string };
+// const renderField: React.ComponentType<WrappedFieldProps & LocalClusterProp> = ({ input, localCluster }) => (
+//     <span>{localCluster === input.value.substring(0, 5) ? "" : "federated"} user {input.value}</span>
+// );
+
+export enum UserProfileGroupsColumnNames {
+    NAME = "Name",
+    PERMISSION = "Permission",
+    VISIBLE = "Visible to other members",
+    UUID = "UUID",
+    REMOVE = "Remove",
+}
+
+export const userProfileGroupsColumns: DataColumns<string> = [
+    {
+        name: UserProfileGroupsColumnNames.NAME,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <ResourceLinkHead uuid={uuid} />
+    },
+    {
+        name: UserProfileGroupsColumnNames.PERMISSION,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <ResourceLinkHeadPermissionLevel uuid={uuid} />
+    },
+    {
+        name: UserProfileGroupsColumnNames.VISIBLE,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <ResourceLinkTailIsVisible uuid={uuid} />
+    },
+    {
+        name: UserProfileGroupsColumnNames.UUID,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <ResourceLinkHeadUuid uuid={uuid} />
+    },
+    {
+        name: UserProfileGroupsColumnNames.REMOVE,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <ResourceLinkDelete uuid={uuid} />
+    },
+];
+
+export const UserProfilePanelRoot = withStyles(styles)(
+    class extends React.Component<UserProfilePanelRootProps> {
+        state = {
+            value: 0,
+        };
+
+        componentDidMount() {
+            this.setState({ value: 0 });
+        }
+
+        render() {
+            return <Paper className={this.props.classes.root}>
+                {/* <Typography variant="title" className={this.props.classes.title}>
+                    Logged in as <Field name="uuid" component={renderField} localCluster={this.props.localCluster} />
+                </Typography> */}
+                <Tabs value={this.state.value} onChange={this.handleChange} fullWidth>
+                    <Tab label="PROFILE" />
+                    <Tab label="GROUPS" />
+                    <Tab label="ADMIN" />
+                </Tabs>
+                {this.state.value === 0 &&
+                    // <Card className={this.props.classes.root}>
+                        <CardContent>
+                            <form onSubmit={this.props.handleSubmit}>
+                                <Grid container spacing={24}>
+                                    <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+                                        <Field
+                                            label="First name"
+                                            name="firstName"
+                                            component={TextField as any}
+                                            disabled
+                                        />
+                                    </Grid>
+                                    <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+                                        <Field
+                                            label="Last name"
+                                            name="lastName"
+                                            component={TextField as any}
+                                            disabled
+                                        />
+                                    </Grid>
+                                    <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+                                        <Field
+                                            label="E-mail"
+                                            name="email"
+                                            component={TextField as any}
+                                            disabled
+                                        />
+                                    </Grid>
+                                    <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+                                        <Field
+                                            label="Username"
+                                            name="username"
+                                            component={TextField 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}
+                                            validate={MY_ACCOUNT_VALIDATION}
+                                            required
+                                        />
+                                    </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}
+                                            validate={MY_ACCOUNT_VALIDATION}
+                                            required
+                                        />
+                                    </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}
+                                        />
+                                    </Grid>
+                                    <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+                                        <Field
+                                            label="Website"
+                                            name="prefs.profile.website_url"
+                                            component={TextField as any}
+                                        />
+                                    </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>
+                            </form >
+                        </CardContent>
+                    // </Card>
+                }
+                {this.state.value === 1 &&
+                    <div className={this.props.classes.content}>
+                        <DataExplorer
+                                id={USER_PROFILE_PANEL_ID}
+                                onRowClick={noop}
+                                onRowDoubleClick={noop}
+                                // onContextMenu={this.handleContextMenu}
+                                contextMenuColumn={false}
+                                hideColumnSelector
+                                hideSearchInput
+                                paperProps={{
+                                    elevation: 0,
+                                }}
+                                dataTableDefaultView={
+                                    <DataTableDefaultView
+                                        icon={GroupsIcon}
+                                        messages={['Group list is empty.']} />
+                                } />
+                    </div>}
+                {this.state.value === 2 &&
+                    <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.openSetupShellAccount(this.props.initialValues.uuid)}}
+                                            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.initialValues.uuid)}}
+                                            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.initialValues.uuid)}}
+                                            disabled={false}>
+                                            Log In
+                                        </Button>
+                                    </Grid>
+                                </Grid>
+                            </CardContent>
+                        </Card>
+                    </Paper>}
+            </Paper >;
+        }
+
+        handleChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
+            this.setState({ value });
+        }
+
+        handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
+            // 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
+            //     });
+            // }
+        }
+    }
+);
diff --git a/src/views/user-profile-panel/user-profile-panel.tsx b/src/views/user-profile-panel/user-profile-panel.tsx
new file mode 100644
index 00000000..caac3e8c
--- /dev/null
+++ b/src/views/user-profile-panel/user-profile-panel.tsx
@@ -0,0 +1,46 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { RootState } from 'store/store';
+import { compose, Dispatch } from 'redux';
+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 { matchUserProfileRoute } from 'routes/routes';
+import { UserResource } from 'models/user';
+import { getResource } from 'store/resources/resources';
+import { openSetupShellAccount, loginAs } from 'store/users/users-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 {
+
+    isPristine: isPristine(USER_PROFILE_FORM)(state),
+    isValid: isValid(USER_PROFILE_FORM)(state),
+    initialValues: user,
+    localCluster: state.auth.localCluster
+}};
+
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+    openSetupShellAccount: (uuid: string) => dispatch<any>(openSetupShellAccount(uuid)),
+    loginAs: (uuid: string) => dispatch<any>(loginAs(uuid)),
+    openDeactivateDialog: (uuid: string) => dispatch<any>(openDeactivateDialog(uuid)),
+});
+
+export const UserProfilePanel = compose(
+    connect(mapStateToProps, mapDispatchToProps),
+    reduxForm({
+        form: USER_PROFILE_FORM,
+        onSubmit: (data, dispatch) => {
+            dispatch(saveEditedUser(data));
+        }
+    }))(UserProfilePanelRoot);
diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx
index 49922202..1202529c 100644
--- a/src/views/workbench/workbench.tsx
+++ b/src/views/workbench/workbench.tsx
@@ -47,7 +47,7 @@ import { SearchResultsPanel } from 'views/search-results-panel/search-results-pa
 import { SshKeyPanel } from 'views/ssh-key-panel/ssh-key-panel';
 import { SshKeyAdminPanel } from 'views/ssh-key-panel/ssh-key-admin-panel';
 import { SiteManagerPanel } from "views/site-manager-panel/site-manager-panel";
-import { MyAccountPanel } from 'views/my-account-panel/my-account-panel';
+import { UserProfilePanel } from 'views/user-profile-panel/user-profile-panel';
 import { SharingDialog } from 'views-components/sharing-dialog/sharing-dialog';
 import { NotFoundDialog } from 'views-components/not-found-dialog/not-found-dialog';
 import { AdvancedTabDialog } from 'views-components/advanced-tab-dialog/advanced-tab-dialog';
@@ -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 { 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 { GroupsPanel } from 'views/groups-panel/groups-panel';
 import { RemoveGroupDialog } from 'views-components/groups-dialog/remove-dialog';
@@ -172,7 +173,8 @@ let routes = <>
     <Route path={Routes.KEEP_SERVICES} component={KeepServicePanel} />
     <Route path={Routes.USERS} component={UserPanel} />
     <Route path={Routes.API_CLIENT_AUTHORIZATIONS} component={ApiClientAuthorizationPanel} />
-    <Route path={Routes.MY_ACCOUNT} component={MyAccountPanel} />
+    <Route path={Routes.MY_ACCOUNT} component={UserProfilePanel} />
+    <Route path={Routes.USER_PROFILE} component={UserProfilePanel} />
     <Route path={Routes.GROUPS} component={GroupsPanel} />
     <Route path={Routes.GROUP_DETAILS} component={GroupDetailsPanel} />
     <Route path={Routes.LINKS} component={LinkPanel} />
@@ -268,6 +270,7 @@ export const WorkbenchPanel =
             <UpdateProjectDialog />
             <UserAttributesDialog />
             <UserManageDialog />
+            <DeactivateDialog />
             <VirtualMachineAttributesDialog />
             <FedLogin />
             <WebDavS3InfoDialog />

commit 4e862392eae3d1a1846b3f33c6e29f4e68c31aca
Author: Stephen Smith <stephen at curii.com>
Date:   Wed Mar 2 09:28:36 2022 -0500

    18559: Add user service setup and add return type for unsetup.
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/services/user-service/user-service.ts b/src/services/user-service/user-service.ts
index e6560c84..97cb3c71 100644
--- a/src/services/user-service/user-service.ts
+++ b/src/services/user-service/user-service.ts
@@ -6,6 +6,7 @@ import { AxiosInstance } from "axios";
 import { CommonResourceService } from "services/common-service/common-resource-service";
 import { UserResource } from "models/user";
 import { ApiActions } from "services/api/api-actions";
+import { ListResults } from "services/common-service/common-service";
 
 export class UserService extends CommonResourceService<UserResource> {
     constructor(serverApi: AxiosInstance, actions: ApiActions, readOnlyFields: string[] = []) {
@@ -24,8 +25,16 @@ export class UserService extends CommonResourceService<UserResource> {
         );
     }
 
+    setup(uuid: string) {
+        return CommonResourceService.defaultResponse<ListResults<any>>(
+            this.serverApi
+                .post(this.resourceType + `/setup`, {}, { params: { uuid } }),
+            this.actions
+        );
+    }
+
     unsetup(uuid: string) {
-        return CommonResourceService.defaultResponse(
+        return CommonResourceService.defaultResponse<UserResource>(
             this.serverApi
                 .post(this.resourceType + `/${uuid}/unsetup`),
             this.actions

commit 3583b37935585f9b19605d98c47ccef73c23cb15
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Feb 28 21:16:28 2022 -0500

    18559: Add read only fields to user service
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/services/user-service/user-service.ts b/src/services/user-service/user-service.ts
index dbbd5be8..e6560c84 100644
--- a/src/services/user-service/user-service.ts
+++ b/src/services/user-service/user-service.ts
@@ -8,8 +8,12 @@ import { UserResource } from "models/user";
 import { ApiActions } from "services/api/api-actions";
 
 export class UserService extends CommonResourceService<UserResource> {
-    constructor(serverApi: AxiosInstance, actions: ApiActions) {
-        super(serverApi, "users", actions);
+    constructor(serverApi: AxiosInstance, actions: ApiActions, readOnlyFields: string[] = []) {
+        super(serverApi, "users", actions, readOnlyFields.concat([
+            'fullName',
+            'isInvited',
+            'writableBy',
+        ]));
     }
 
     activate(uuid: string) {

commit 33963600639e6e7f3cd4afea9d4210ee815c180b
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Feb 28 18:23:07 2022 -0500

    18559: Reduce unnecessary reloading in groups panel
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/store/group-details-panel/group-details-panel-actions.ts b/src/store/group-details-panel/group-details-panel-actions.ts
index e00ff773..71ca67c5 100644
--- a/src/store/group-details-panel/group-details-panel-actions.ts
+++ b/src/store/group-details-panel/group-details-panel-actions.ts
@@ -14,8 +14,9 @@ import { ServiceRepository } from 'services/services';
 import { PermissionResource, PermissionLevel } from 'models/permission';
 import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
 import { LinkResource } from 'models/link';
-import { deleteResources } from 'store/resources/resources-actions';
+import { deleteResources, updateResources } from 'store/resources/resources-actions';
 import { openSharingDialog } from 'store/sharing-dialog/sharing-dialog-actions';
+// import { UserProfileGroupsActions } from 'store/user-profile/user-profile-actions';
 
 export const GROUP_DETAILS_MEMBERS_PANEL_ID = 'groupDetailsMembersPanel';
 export const GROUP_DETAILS_PERMISSIONS_PANEL_ID = 'groupDetailsPermissionsPanel';
@@ -48,9 +49,8 @@ export const openAddGroupMembersDialog = () =>
 export const editPermissionLevel = (uuid: string, level: PermissionLevel) =>
     async (dispatch: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => {
         try {
-            await permissionService.update(uuid, {name: level});
-            dispatch(GroupMembersPanelActions.REQUEST_ITEMS());
-            dispatch(GroupPermissionsPanelActions.REQUEST_ITEMS());
+            const permission = await permissionService.update(uuid, {name: level});
+            dispatch(updateResources([permission]));
             dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Permission level changed.', hideDuration: 2000 }));
         } catch (e) {
             dispatch(snackbarActions.OPEN_SNACKBAR({
@@ -83,25 +83,19 @@ export const openRemoveGroupMemberDialog = (uuid: string) =>
 export const removeGroupMember = (uuid: string) =>
 
     async (dispatch: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => {
+        dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...', kind: SnackbarKind.INFO }));
+        await deleteGroupMember({
+            link: {
+                uuid,
+            },
+            permissionService,
+            dispatch,
+        });
+        dispatch<any>(deleteResources([uuid]));
+        dispatch(GroupMembersPanelActions.REQUEST_ITEMS());
+        // dispatch(UserProfileGroupsActions.REQUEST_ITEMS());
 
-        const groupUuid = getCurrentGroupDetailsPanelUuid(getState().properties);
-
-        if (groupUuid) {
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...', kind: SnackbarKind.INFO }));
-
-            await deleteGroupMember({
-                link: {
-                    uuid,
-                },
-                permissionService,
-                dispatch,
-            });
-
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removed.', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
-            dispatch(GroupMembersPanelActions.REQUEST_ITEMS());
-
-        }
-
+        dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removed.', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
     };
 
 export const setMemberIsHidden = (memberLinkUuid: string, permissionLinkUuid: string, visible: boolean) =>
@@ -113,7 +107,6 @@ export const setMemberIsHidden = (memberLinkUuid: string, permissionLinkUuid: st
             try {
                 await permissionService.delete(permissionLinkUuid);
                 dispatch<any>(deleteResources([permissionLinkUuid]));
-                dispatch(GroupPermissionsPanelActions.REQUEST_ITEMS());
                 dispatch(snackbarActions.OPEN_SNACKBAR({
                     message: 'Removed read permission.',
                     hideDuration: 2000,
@@ -128,17 +121,17 @@ export const setMemberIsHidden = (memberLinkUuid: string, permissionLinkUuid: st
         } else if (visible && memberLink) {
             // Create read permission
             try {
-                await permissionService.create({
+                const permission = await permissionService.create({
                     headUuid: memberLink.tailUuid,
                     tailUuid: memberLink.headUuid,
                     name: PermissionLevel.CAN_READ,
                 });
+                dispatch(updateResources([permission]));
                 dispatch(snackbarActions.OPEN_SNACKBAR({
                     message: 'Created read permission.',
                     hideDuration: 2000,
                     kind: SnackbarKind.SUCCESS,
                 }));
-                dispatch(GroupPermissionsPanelActions.REQUEST_ITEMS());
             } catch(e) {
                 dispatch(snackbarActions.OPEN_SNACKBAR({
                     message: 'Failed to create permission',
diff --git a/src/store/group-details-panel/group-details-panel-members-middleware-service.ts b/src/store/group-details-panel/group-details-panel-members-middleware-service.ts
index e6f18f7f..3a58927a 100644
--- a/src/store/group-details-panel/group-details-panel-members-middleware-service.ts
+++ b/src/store/group-details-panel/group-details-panel-members-middleware-service.ts
@@ -24,7 +24,8 @@ export class GroupDetailsPanelMembersMiddlewareService extends DataExplorerMiddl
         const dataExplorer = getDataExplorer(api.getState().dataExplorer, this.getId());
         const groupUuid = getCurrentGroupDetailsPanelUuid(api.getState().properties);
         if (!dataExplorer || !groupUuid) {
-            api.dispatch(groupsDetailsPanelDataExplorerIsNotSet());
+            // Noop if data explorer refresh is triggered from another panel
+            return;
         } else {
             try {
                 const groupResource = await this.services.groupsService.get(groupUuid);
@@ -69,12 +70,6 @@ export class GroupDetailsPanelMembersMiddlewareService extends DataExplorerMiddl
     }
 }
 
-const groupsDetailsPanelDataExplorerIsNotSet = () =>
-    snackbarActions.OPEN_SNACKBAR({
-        message: 'Group members panel is not ready.',
-        kind: SnackbarKind.ERROR
-    });
-
 const couldNotFetchGroupDetailsContents = () =>
     snackbarActions.OPEN_SNACKBAR({
         message: 'Could not fetch group members.',

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


hooks/post-receive
-- 




More information about the arvados-commits mailing list