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

44 files changed:
cypress/integration/user-profile.spec.js [new file with mode: 0644]
cypress/integration/virtual-machine-admin.spec.js
src/components/context-menu/context-menu.tsx
src/components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar.tsx [new file with mode: 0644]
src/components/icon/icon.tsx
src/components/select-field/select-field.tsx
src/models/group.ts
src/routes/route-change-handlers.ts
src/routes/routes.ts
src/services/user-service/user-service.ts
src/store/auth/auth-action.ts
src/store/breadcrumbs/breadcrumbs-actions.ts
src/store/context-menu/context-menu-actions.ts
src/store/context-menu/context-menu-filters.ts [new file with mode: 0644]
src/store/group-details-panel/group-details-panel-actions.ts
src/store/group-details-panel/group-details-panel-members-middleware-service.ts
src/store/group-details-panel/group-details-panel-permissions-middleware-service.ts
src/store/my-account/my-account-panel-actions.ts [deleted file]
src/store/navigation/navigation-action.ts
src/store/store.ts
src/store/user-profile/user-profile-actions.ts [new file with mode: 0644]
src/store/user-profile/user-profile-groups-middleware-service.ts [new file with mode: 0644]
src/store/users/user-panel-middleware-service.ts
src/store/users/users-actions.ts
src/store/virtual-machines/virtual-machines-actions.ts
src/store/workbench/workbench-actions.ts
src/validators/validators.tsx
src/views-components/context-menu/action-sets/user-action-set.ts
src/views-components/context-menu/context-menu.tsx
src/views-components/data-explorer/renderers.tsx
src/views-components/dialog-create/dialog-user-create.tsx
src/views-components/dialog-forms/setup-shell-account-dialog.tsx [deleted file]
src/views-components/form-fields/user-form-fields.tsx
src/views-components/user-dialog/activate-dialog.tsx [new file with mode: 0644]
src/views-components/user-dialog/deactivate-dialog.tsx [new file with mode: 0644]
src/views-components/user-dialog/manage-dialog.tsx [deleted file]
src/views-components/user-dialog/setup-dialog.tsx [new file with mode: 0644]
src/views/group-details-panel/group-details-panel.tsx
src/views/my-account-panel/my-account-panel-root.tsx [deleted file]
src/views/my-account-panel/my-account-panel.tsx [deleted file]
src/views/user-panel/user-panel.tsx
src/views/user-profile-panel/user-profile-panel-root.tsx [new file with mode: 0644]
src/views/user-profile-panel/user-profile-panel.tsx [new file with mode: 0644]
src/views/workbench/workbench.tsx

diff --git a/cypress/integration/user-profile.spec.js b/cypress/integration/user-profile.spec.js
new file mode 100644 (file)
index 0000000..7d21249
--- /dev/null
@@ -0,0 +1,449 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+describe('User profile tests', function() {
+    let activeUser;
+    let adminUser;
+    const roleGroupName = `Test role group (${Math.floor(999999 * Math.random())})`;
+    const projectGroupName = `Test project group (${Math.floor(999999 * Math.random())})`;
+
+    before(function() {
+        // Only set up common users once. These aren't set up as aliases because
+        // aliases are cleaned up after every test. Also it doesn't make sense
+        // to set the same users on beforeEach() over and over again, so we
+        // separate a little from Cypress' 'Best Practices' here.
+        cy.getUser('admin', 'Admin', 'User', true, true)
+            .as('adminUser').then(function() {
+                adminUser = this.adminUser;
+            }
+        );
+        cy.getUser('user', 'Active', 'User', false, true)
+            .as('activeUser').then(function() {
+                activeUser = this.activeUser;
+            }
+        );
+    });
+
+    function assertProfileValues({
+        firstName,
+        lastName,
+        email,
+        username,
+        org,
+        org_email,
+        role,
+        website,
+    }) {
+        cy.get('[data-cy=profile-form] input[name="firstName"]').invoke('val').should('equal', firstName);
+        cy.get('[data-cy=profile-form] input[name="lastName"]').invoke('val').should('equal', lastName);
+        cy.get('[data-cy=profile-form] [data-cy=email] [data-cy=value]').contains(email);
+        cy.get('[data-cy=profile-form] [data-cy=username] [data-cy=value]').contains(username);
+
+        cy.get('[data-cy=profile-form] input[name="prefs.profile.organization"]').invoke('val').should('equal', org);
+        cy.get('[data-cy=profile-form] input[name="prefs.profile.organization_email"]').invoke('val').should('equal', org_email);
+        cy.get('[data-cy=profile-form] select[name="prefs.profile.role"]').invoke('val').should('equal', role);
+        cy.get('[data-cy=profile-form] input[name="prefs.profile.website_url"]').invoke('val').should('equal', website);
+    }
+
+    function enterProfileValues({
+        org,
+        org_email,
+        role,
+        website,
+    }) {
+        cy.get('[data-cy=profile-form] input[name="prefs.profile.organization"]').clear();
+        if (org) {
+            cy.get('[data-cy=profile-form] input[name="prefs.profile.organization"]').type(org);
+        }
+        cy.get('[data-cy=profile-form] input[name="prefs.profile.organization_email"]').clear();
+        if (org_email) {
+            cy.get('[data-cy=profile-form] input[name="prefs.profile.organization_email"]').type(org_email);
+        }
+        cy.get('[data-cy=profile-form] select[name="prefs.profile.role"]').select(role);
+        cy.get('[data-cy=profile-form] input[name="prefs.profile.website_url"]').clear();
+        if (website) {
+            cy.get('[data-cy=profile-form] input[name="prefs.profile.website_url"]').type(website);
+        }
+    }
+
+    function assertContextMenuItems({
+        account,
+        activate,
+        deactivate,
+        login,
+        setup
+    }) {
+        cy.get('[data-cy=user-profile-panel-options-btn]').click();
+        cy.get('[data-cy=context-menu]').within(() => {
+            cy.get('[role=button]').contains('Advanced');
+
+            cy.get('[role=button]').should(account ? 'contain' : 'not.contain', 'Account Settings');
+            cy.get('[role=button]').should(activate ? 'contain' : 'not.contain', 'Activate User');
+            cy.get('[role=button]').should(deactivate ? 'contain' : 'not.contain', 'Deactivate User');
+            cy.get('[role=button]').should(login ? 'contain' : 'not.contain', 'Login As User');
+            cy.get('[role=button]').should(setup ? 'contain' : 'not.contain', 'Setup User');
+        });
+        cy.get('div[role=presentation]').click();
+    }
+
+    beforeEach(function() {
+        cy.updateResource(adminUser.token, 'users', adminUser.user.uuid, {
+            prefs: {
+                profile: {
+                    organization: '',
+                    organization_email: '',
+                    role: '',
+                    website_url: '',
+                },
+            },
+        });
+        cy.updateResource(adminUser.token, 'users', activeUser.user.uuid, {
+            prefs: {
+                profile: {
+                    organization: '',
+                    organization_email: '',
+                    role: '',
+                    website_url: '',
+                },
+            },
+        });
+    });
+
+    it('non-admin can edit own profile', function() {
+        cy.loginAs(activeUser);
+
+        cy.get('header button[title="Account Management"]').click();
+        cy.get('#account-menu').contains('My account').click();
+
+        // Admin actions should be hidden, no account menu
+        assertContextMenuItems({
+            account: false,
+            activate: false,
+            deactivate: false,
+            login: false,
+            setup: false,
+        });
+
+        // Check initial values
+        assertProfileValues({
+            firstName: 'Active',
+            lastName: 'User',
+            email: 'user@example.local',
+            username: 'user',
+            org: '',
+            org_email: '',
+            role: '',
+            website: '',
+        });
+
+        // Change values
+        enterProfileValues({
+            org: 'Org name',
+            org_email: 'email@example.com',
+            role: 'Data Scientist',
+            website: 'example.com',
+        });
+
+        // Submit
+        cy.get('[data-cy=profile-form] button[type="submit"]').click();
+
+        // Check new values
+        assertProfileValues({
+            firstName: 'Active',
+            lastName: 'User',
+            email: 'user@example.local',
+            username: 'user',
+            org: 'Org name',
+            org_email: 'email@example.com',
+            role: 'Data Scientist',
+            website: 'example.com',
+        });
+    });
+
+    it('non-admin cannot edit other profile', function() {
+        cy.loginAs(activeUser);
+        cy.goToPath('/user/' + adminUser.user.uuid);
+
+        assertProfileValues({
+            firstName: 'Admin',
+            lastName: 'User',
+            email: 'admin@example.local',
+            username: 'admin',
+            org: '',
+            org_email: '',
+            role: '',
+            website: '',
+        });
+
+        // Inputs should be disabled
+        cy.get('[data-cy=profile-form] input[name="prefs.profile.organization"]').should('be.disabled');
+        cy.get('[data-cy=profile-form] input[name="prefs.profile.organization_email"]').should('be.disabled');
+        cy.get('[data-cy=profile-form] select[name="prefs.profile.role"]').should('be.disabled');
+        cy.get('[data-cy=profile-form] input[name="prefs.profile.website_url"]').should('be.disabled');
+
+        // Submit should be disabled
+        cy.get('[data-cy=profile-form] button[type="submit"]').should('be.disabled');
+
+        // Admin actions should be hidden, no account menu
+        assertContextMenuItems({
+            account: false,
+            activate: false,
+            deactivate: false,
+            login: false,
+            setup: false,
+        });
+    });
+
+    it('admin can edit own profile', function() {
+        cy.loginAs(adminUser);
+
+        cy.get('header button[title="Account Management"]').click();
+        cy.get('#account-menu').contains('My account').click();
+
+        // Admin actions should be visible, no account menu
+        assertContextMenuItems({
+            account: false,
+            activate: false,
+            deactivate: true,
+            login: false,
+            setup: false,
+        });
+
+        // Check initial values
+        assertProfileValues({
+            firstName: 'Admin',
+            lastName: 'User',
+            email: 'admin@example.local',
+            username: 'admin',
+            org: '',
+            org_email: '',
+            role: '',
+            website: '',
+        });
+
+        // Change values
+        enterProfileValues({
+            org: 'Admin org name',
+            org_email: 'admin@example.com',
+            role: 'Researcher',
+            website: 'admin.local',
+        });
+        cy.get('[data-cy=profile-form] button[type="submit"]').click();
+
+        // Check new values
+        assertProfileValues({
+            firstName: 'Admin',
+            lastName: 'User',
+            email: 'admin@example.local',
+            username: 'admin',
+            org: 'Admin org name',
+            org_email: 'admin@example.com',
+            role: 'Researcher',
+            website: 'admin.local',
+        });
+    });
+
+    it('admin can edit other profile', function() {
+        cy.loginAs(adminUser);
+        cy.goToPath('/user/' + activeUser.user.uuid);
+
+        // Check new values
+        assertProfileValues({
+            firstName: 'Active',
+            lastName: 'User',
+            email: 'user@example.local',
+            username: 'user',
+            org: '',
+            org_email: '',
+            role: '',
+            website: '',
+        });
+
+        enterProfileValues({
+            org: 'Changed org name',
+            org_email: 'changed@example.com',
+            role: 'Researcher',
+            website: 'changed.local',
+        });
+        cy.get('[data-cy=profile-form] button[type="submit"]').click();
+
+        // Check new values
+        assertProfileValues({
+            firstName: 'Active',
+            lastName: 'User',
+            email: 'user@example.local',
+            username: 'user',
+            org: 'Changed org name',
+            org_email: 'changed@example.com',
+            role: 'Researcher',
+            website: 'changed.local',
+        });
+
+        // Admin actions should be visible, no account menu
+        assertContextMenuItems({
+            account: false,
+            activate: false,
+            deactivate: true,
+            login: true,
+            setup: false,
+        });
+    });
+
+    it('displays role groups on user profile', function() {
+        cy.loginAs(adminUser);
+
+        cy.createGroup(adminUser.token, {
+            name: roleGroupName,
+            group_class: 'role',
+        }).as('roleGroup').then(function() {
+            cy.createLink(adminUser.token, {
+                name: 'can_write',
+                link_class: 'permission',
+                head_uuid: this.roleGroup.uuid,
+                tail_uuid: adminUser.user.uuid
+            });
+            cy.createLink(adminUser.token, {
+                name: 'can_write',
+                link_class: 'permission',
+                head_uuid: this.roleGroup.uuid,
+                tail_uuid: activeUser.user.uuid
+            });
+        });
+
+        cy.createGroup(adminUser.token, {
+            name: projectGroupName,
+            group_class: 'project',
+        }).as('projectGroup').then(function() {
+            cy.createLink(adminUser.token, {
+                name: 'can_write',
+                link_class: 'permission',
+                head_uuid: this.projectGroup.uuid,
+                tail_uuid: adminUser.user.uuid
+            });
+            cy.createLink(adminUser.token, {
+                name: 'can_write',
+                link_class: 'permission',
+                head_uuid: this.projectGroup.uuid,
+                tail_uuid: activeUser.user.uuid
+            });
+        });
+
+        cy.goToPath('/user/' + activeUser.user.uuid);
+        cy.get('div [role="tab"]').contains('GROUPS').click();
+        cy.get('[data-cy=user-profile-groups-data-explorer]').contains(roleGroupName);
+        cy.get('[data-cy=user-profile-groups-data-explorer]').should('not.contain', projectGroupName);
+
+        cy.goToPath('/user/' + adminUser.user.uuid);
+        cy.get('div [role="tab"]').contains('GROUPS').click();
+        cy.get('[data-cy=user-profile-groups-data-explorer]').contains(roleGroupName);
+        cy.get('[data-cy=user-profile-groups-data-explorer]').should('not.contain', projectGroupName);
+    });
+
+    it('allows performing admin functions', function() {
+        cy.loginAs(adminUser);
+        cy.goToPath('/user/' + activeUser.user.uuid);
+
+        // Check that user is active
+        cy.get('[data-cy=account-status]').contains('Active');
+        cy.get('div [role="tab"]').contains('GROUPS').click();
+        cy.get('[data-cy=user-profile-groups-data-explorer]').should('contain', 'All users');
+        cy.get('div [role="tab"]').contains('PROFILE').click();
+        assertContextMenuItems({
+            account: false,
+            activate: false,
+            deactivate: true,
+            login: true,
+            setup: false,
+        });
+
+        // Deactivate user
+        cy.get('[data-cy=user-profile-panel-options-btn]').click();
+        cy.get('[data-cy=context-menu]').contains('Deactivate User').click();
+        cy.get('[data-cy=confirmation-dialog-ok-btn]').click();
+
+        // Check that user is deactivated
+        cy.get('[data-cy=account-status]').contains('Inactive');
+        cy.get('div [role="tab"]').contains('GROUPS').click();
+        cy.get('[data-cy=user-profile-groups-data-explorer]').should('not.contain', 'All users');
+        cy.get('div [role="tab"]').contains('PROFILE').click();
+        assertContextMenuItems({
+            account: false,
+            activate: true,
+            deactivate: false,
+            login: true,
+            setup: true,
+        });
+
+        // Setup user
+        cy.get('[data-cy=user-profile-panel-options-btn]').click();
+        cy.get('[data-cy=context-menu]').contains('Setup User').click();
+        cy.get('[data-cy=confirmation-dialog-ok-btn]').click();
+
+        // Check that user is setup
+        cy.get('[data-cy=account-status]').contains('Setup');
+        cy.get('div [role="tab"]').contains('GROUPS').click();
+        cy.get('[data-cy=user-profile-groups-data-explorer]').should('contain', 'All users');
+        cy.get('div [role="tab"]').contains('PROFILE').click();
+        assertContextMenuItems({
+            account: false,
+            activate: true,
+            deactivate: true,
+            login: true,
+            setup: false,
+        });
+
+        // Activate user
+        cy.get('[data-cy=user-profile-panel-options-btn]').click();
+        cy.get('[data-cy=context-menu]').contains('Activate User').click();
+        cy.get('[data-cy=confirmation-dialog-ok-btn]').click();
+
+        // Check that user is active
+        cy.get('[data-cy=account-status]').contains('Active');
+        cy.get('div [role="tab"]').contains('GROUPS').click();
+        cy.get('[data-cy=user-profile-groups-data-explorer]').should('contain', 'All users');
+        cy.get('div [role="tab"]').contains('PROFILE').click();
+        assertContextMenuItems({
+            account: false,
+            activate: false,
+            deactivate: true,
+            login: true,
+            setup: false,
+        });
+
+        // Deactivate and activate user skipping setup
+        cy.get('[data-cy=user-profile-panel-options-btn]').click();
+        cy.get('[data-cy=context-menu]').contains('Deactivate User').click();
+        cy.get('[data-cy=confirmation-dialog-ok-btn]').click();
+        // Check
+        cy.get('[data-cy=account-status]').contains('Inactive');
+        cy.get('div [role="tab"]').contains('GROUPS').click();
+        cy.get('[data-cy=user-profile-groups-data-explorer]').should('not.contain', 'All users');
+        cy.get('div [role="tab"]').contains('PROFILE').click();
+        assertContextMenuItems({
+            account: false,
+            activate: true,
+            deactivate: false,
+            login: true,
+            setup: true,
+        });
+        // reactivate
+        cy.get('[data-cy=user-profile-panel-options-btn]').click();
+        cy.get('[data-cy=context-menu]').contains('Activate User').click();
+        cy.get('[data-cy=confirmation-dialog-ok-btn]').click();
+
+        // Check that user is active
+        cy.get('[data-cy=account-status]').contains('Active');
+        cy.get('div [role="tab"]').contains('GROUPS').click();
+        cy.get('[data-cy=user-profile-groups-data-explorer]').should('contain', 'All users');
+        cy.get('div [role="tab"]').contains('PROFILE').click();
+        assertContextMenuItems({
+            account: false,
+            activate: false,
+            deactivate: true,
+            login: true,
+            setup: false,
+        });
+    });
+
+});
index 73804b2030da01a5d50f80d4a75da380b49425d7..f01a8911060ffd22e6545f8a3f0ffca137a4e190 100644 (file)
@@ -64,6 +64,7 @@ describe('Virtual machine login manage tests', function() {
         cy.get('[data-cy=form-dialog]').within(() => {
             cy.get('[data-cy=form-submit-btn]').click();
         });
+        cy.get('[data-cy=snackbar]').contains('Permission updated');
         cy.get('[data-cy=vm-admin-table]')
             .contains(vmHost)
             .parents('tr')
@@ -92,6 +93,7 @@ describe('Virtual machine login manage tests', function() {
         cy.get('[data-cy=form-dialog]').within(() => {
             cy.get('[data-cy=form-submit-btn]').click();
         });
+        cy.get('[data-cy=snackbar]').contains('Permission updated');
         cy.get('[data-cy=vm-admin-table]')
             .contains(vmHost)
             .parents('tr')
@@ -160,8 +162,17 @@ describe('Virtual machine login manage tests', function() {
             cy.get('[data-cy=form-submit-btn]').click();
         });
 
+        // Wait for page to finish loading
+        cy.get('[data-cy=snackbar]').contains('Permission updated');
         cy.get('[data-cy=vm-admin-table]')
-            .contains('user'); // Wait for page to finish
+            .contains(vmHost)
+            .parents('tr')
+            .within(() => {
+                cy.get('div[role=button]')
+                    .parent()
+                    .first()
+                    .contains('admin')
+            });
 
         cy.get('[data-cy=vm-admin-table]')
             .contains(vmHost)
@@ -183,6 +194,7 @@ describe('Virtual machine login manage tests', function() {
         cy.get('[data-cy=form-dialog]').within(() => {
             cy.get('[data-cy=form-submit-btn]').click();
         });
+        cy.get('[data-cy=snackbar]').contains('Permission updated');
 
         // Verify new login permissions
         // Check admin's vm page for login
index 36f0903df3784543401518f3f59c0b00eee02334..a44e8b7bd4af0aefee66a22523ee3f9676feb658 100644 (file)
@@ -5,11 +5,15 @@ import React from "react";
 import { Popover, List, ListItem, ListItemIcon, ListItemText, Divider } from "@material-ui/core";
 import { DefaultTransformOrigin } from "../popover/helpers";
 import { IconType } from "../icon/icon";
+import { RootState } from "store/store";
+import { ContextMenuResource } from "store/context-menu/context-menu-actions";
 
 export interface ContextMenuItem {
     name?: string | React.ComponentType;
     icon?: IconType;
     component?: React.ComponentType<any>;
+    adminOnly?: boolean;
+    filters?: ((state: RootState, resource: ContextMenuResource) => boolean)[]
 }
 
 export type ContextMenuItemGroup = ContextMenuItem[];
diff --git a/src/components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar.tsx b/src/components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar.tsx
new file mode 100644 (file)
index 0000000..3b2ff68
--- /dev/null
@@ -0,0 +1,58 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from "react";
+import { connect, DispatchProp } from "react-redux";
+import { StyleRulesCallback, Tooltip, WithStyles, withStyles } from "@material-ui/core";
+import { ArvadosTheme } from 'common/custom-theme';
+import CopyToClipboard from 'react-copy-to-clipboard';
+import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
+import { CopyIcon } from 'components/icon/icon';
+
+type CssRules = 'copyIcon';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+  copyIcon: {
+    marginLeft: theme.spacing.unit,
+    color: theme.palette.grey["500"],
+    cursor: 'pointer',
+    display: 'inline',
+    '& svg': {
+      fontSize: '1rem',
+      verticalAlign: 'middle',
+    }
+  }
+});
+
+interface CopyToClipboardDataProps {
+  children?: React.ReactNode;
+  value: string;
+}
+
+type CopyToClipboardProps = CopyToClipboardDataProps & WithStyles<CssRules> & DispatchProp;
+
+export const CopyToClipboardSnackbar = connect()(withStyles(styles)(
+  class CopyToClipboardSnackbar extends React.Component<CopyToClipboardProps> {
+    onCopy = () => {
+      this.props.dispatch(snackbarActions.OPEN_SNACKBAR({
+        message: 'Copied',
+        hideDuration: 2000,
+        kind: SnackbarKind.SUCCESS
+    }));
+    };
+
+    render() {
+      const { children, value, classes } = this.props;
+      return (
+        <Tooltip title="Copy to clipboard">
+          <span className={classes.copyIcon}>
+            <CopyToClipboard text={value} onCopy={this.onCopy}>
+              {children || <CopyIcon />}
+            </CopyToClipboard>
+          </span>
+        </Tooltip>
+      );
+    }
+  }
+));
index 557e22e77c959e08107ca10b11e174e13a967c30..19b4beea1eb66274f45c2fc8ba1f1879a3194333 100644 (file)
@@ -68,11 +68,15 @@ import Computer from '@material-ui/icons/Computer';
 import WrapText from '@material-ui/icons/WrapText';
 import TextIncrease from '@material-ui/icons/ZoomIn';
 import TextDecrease from '@material-ui/icons/ZoomOut';
+import CropFreeSharp from '@material-ui/icons/CropFreeSharp';
+import ExitToApp from '@material-ui/icons/ExitToApp';
+import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline';
+import RemoveCircleOutline from '@material-ui/icons/RemoveCircleOutline';
+import NotInterested from '@material-ui/icons/NotInterested';
 
 // Import FontAwesome icons
 import { library } from '@fortawesome/fontawesome-svg-core';
 import { faPencilAlt, faSlash, faUsers, faEllipsisH } from '@fortawesome/free-solid-svg-icons';
-import { CropFreeSharp } from '@material-ui/icons';
 library.add(
     faPencilAlt,
     faSlash,
@@ -179,3 +183,8 @@ export const AddUserIcon: IconType = (props) => <PersonAdd {...props} />;
 export const WordWrapIcon: IconType = (props) => <WrapText {...props} />;
 export const TextIncreaseIcon: IconType = (props) => <TextIncrease {...props} />;
 export const TextDecreaseIcon: IconType = (props) => <TextDecrease {...props} />;
+export const DeactivateUserIcon: IconType = (props) => <NotInterested {...props} />;
+export const LoginAsIcon: IconType = (props) => <ExitToApp {...props} />;
+export const ActiveIcon: IconType = (props) => <CheckCircleOutline {...props} />;
+export const SetupIcon: IconType = (props) => <RemoveCircleOutline {...props} />;
+export const InactiveIcon: IconType = (props) => <NotInterested {...props} />;
index e4dcad6c109fdd75cfb2cb14102d161272ceb649..6fa7ddea626396494cc6f3cc5a2e01887a15da7d 100644 (file)
@@ -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
+);
index 3f3656ccd5fba90bcfb6a037c73ce0420c4c343c..f6a72c538f9481392aabac0883077cbf2086a6b4 100644 (file)
@@ -25,14 +25,18 @@ export enum GroupClass {
     ROLE  = 'role',
 }
 
-export const BUILTIN_GROUP_IDS = [
-    'fffffffffffffff',
-    'anonymouspublic',
-    '000000000000000',
-]
+export enum BuiltinGroups {
+    ALL = 'fffffffffffffff',
+    ANON = 'anonymouspublic',
+    SYSTEM = '000000000000000',
+}
+
+export const getBuiltinGroupUuid = (cluster: string, groupName: BuiltinGroups): string => {
+    return cluster ? `${cluster}-${ResourceObjectType.GROUP}-${groupName}` : "";
+};
 
 export const isBuiltinGroup = (uuid: string) => {
     const match = RESOURCE_UUID_REGEX.exec(uuid);
     const parts = match ? match[0].split('-') : [];
-    return parts.length === 3 && parts[1] === ResourceObjectType.GROUP && BUILTIN_GROUP_IDS.includes(parts[2]);
+    return parts.length === 3 && parts[1] === ResourceObjectType.GROUP && Object.values<string>(BuiltinGroups).includes(parts[2]);
 };
index 5e07e6e8209e716e249a27eea3ee67456ddb1857..5b3ce6687ff0e4cb8b3e17a97d286898dda90946 100644 (file)
@@ -41,7 +41,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);
@@ -97,11 +98,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) {
index d7257b5124b38fc0f71ef392d52102ec7b52825e..50689ec37c46ac1fba6d5ff985badeb233944031 100644 (file)
@@ -39,6 +39,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})`,
@@ -96,6 +97,8 @@ export const getProcessUrl = (uuid: string) => `/processes/${uuid}`;
 
 export const getGroupUrl = (uuid: string) => `/group/${uuid}`;
 
+export const getUserProfileUrl = (uuid: string) => `/user/${uuid}`;
+
 export interface ResourceRouteParams {
     id: string;
 }
@@ -169,6 +172,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 });
 
index dbbd5be8c2a7d4297f36c85c63772929d5c649a2..8581b26766715fe57ce1d50657c8e9131f90daa4 100644 (file)
@@ -6,22 +6,35 @@ 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) {
-        super(serverApi, "users", actions);
+    constructor(serverApi: AxiosInstance, actions: ApiActions, readOnlyFields: string[] = []) {
+        super(serverApi, "users", actions, readOnlyFields.concat([
+            'fullName',
+            'isInvited',
+            'writableBy',
+        ]));
     }
 
     activate(uuid: string) {
-        return CommonResourceService.defaultResponse(
+        return CommonResourceService.defaultResponse<UserResource>(
             this.serverApi
                 .post(this.resourceType + `/${uuid}/activate`),
             this.actions
         );
     }
 
+    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
index d58a8103d28c1309d422f5cd4556c92cfbea3840..7fc9df774358a73d3d9448e604f83bf8693242c0 100644 (file)
@@ -80,6 +80,10 @@ export const getConfig = (dispatch: Dispatch, getState: () => RootState, service
     return state.remoteHostsConfig[state.localCluster];
 };
 
+export const getLocalCluster = (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): string => {
+    return getState().auth.localCluster;
+};
+
 export const saveApiToken = (token: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
     let config: any;
     const tokenParts = token.split('/');
index 72e908aa83c91ffe5480532ad08113c36012005e..08e1a132fd12e23f722cb2bc4e88f6c0374caab0 100644 (file)
@@ -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,52 @@ 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) => {
+        try {
+            const user = getResource<UserResource>(userUuid)(getState().resources)
+                        || await services.userService.get(userUuid, false);
+            const breadcrumbs: ResourceBreadcrumb[] = [
+                { label: USERS_PANEL_LABEL, uuid: USERS_PANEL_LABEL },
+                { label: user ? user.username : userUuid, uuid: userUuid },
+            ];
+            dispatch(setBreadcrumbs(breadcrumbs));
+        } catch (e) {
+            const breadcrumbs: ResourceBreadcrumb[] = [
+                { label: USERS_PANEL_LABEL, uuid: USERS_PANEL_LABEL },
+                { label: userUuid, uuid: userUuid },
+            ];
+            dispatch(setBreadcrumbs(breadcrumbs));
+        }
+    };
+
+export const MY_ACCOUNT_PANEL_LABEL = 'My Account';
+
+export const setMyAccountBreadcrumbs = () =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(setBreadcrumbs([
+            { label: MY_ACCOUNT_PANEL_LABEL, uuid: MY_ACCOUNT_PANEL_LABEL },
+        ]));
+    };
index 336817ea4d0adc18c69a0c9b90347be33a7c33f5..1116949a6f31f769dfcf0fbb3ed56e147e7166c2 100644 (file)
@@ -208,6 +208,17 @@ export const openPermissionEditContextMenu = (event: React.MouseEvent<HTMLElemen
         }
     };
 
+export const openUserContextMenu = (event: React.MouseEvent<HTMLElement>, user: UserResource) =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+        dispatch<any>(openContextMenu(event, {
+            name: '',
+            uuid: user.uuid,
+            ownerUuid: user.ownerUuid,
+            kind: user.kind,
+            menuKind: ContextMenuKind.USER
+        }));
+    };
+
 export const resourceUuidToContextMenuKind = (uuid: string, readonly = false) =>
     (dispatch: Dispatch, getState: () => RootState) => {
         const { isAdmin: isAdminUser, uuid: userUuid } = getState().auth.user!;
diff --git a/src/store/context-menu/context-menu-filters.ts b/src/store/context-menu/context-menu-filters.ts
new file mode 100644 (file)
index 0000000..53993fa
--- /dev/null
@@ -0,0 +1,40 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { RootState } from "store/store";
+import { ContextMenuResource } from "store/context-menu/context-menu-actions";
+import { getUserAccountStatus, UserAccountStatus } from "store/users/users-actions";
+import { matchMyAccountRoute, matchUserProfileRoute } from "routes/routes";
+
+export const isAdmin = (state: RootState, resource: ContextMenuResource) => {
+  return state.auth.user!.isAdmin;
+}
+
+export const canActivateUser = (state: RootState, resource: ContextMenuResource) => {
+  const status = getUserAccountStatus(state, resource.uuid);
+  return status === UserAccountStatus.INACTIVE ||
+    status === UserAccountStatus.SETUP;
+};
+
+export const canDeactivateUser = (state: RootState, resource: ContextMenuResource) => {
+  const status = getUserAccountStatus(state, resource.uuid);
+  return status === UserAccountStatus.SETUP ||
+    status === UserAccountStatus.ACTIVE;
+};
+
+export const canSetupUser = (state: RootState, resource: ContextMenuResource) => {
+  const status = getUserAccountStatus(state, resource.uuid);
+  return status === UserAccountStatus.INACTIVE;
+};
+
+export const needsUserProfileLink = (state: RootState, resource: ContextMenuResource) => (
+  state.router.location ?
+    !(matchUserProfileRoute(state.router.location.pathname)
+      || matchMyAccountRoute(state.router.location.pathname)
+    ) : true
+);
+
+export const isOtherUser = (state: RootState, resource: ContextMenuResource) => {
+  return state.auth.user!.uuid !== resource.uuid;
+};
index e00ff77340514714053f20a9bb1ed74b227f79f5..2d34511957fe17b93b2511b96a40659dd3771f1a 100644 (file)
@@ -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) =>
@@ -128,17 +122,18 @@ 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(GroupPermissionsPanelActions.REQUEST_ITEMS());
                 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',
index e6f18f7f98489a451cd0c19afdba912393184cf9..3a58927aac7cd3da6a3c41d13aef2fa3161dc82a 100644 (file)
@@ -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.',
index 9e41409d8053bc3dd3cbbfb8cd14a9a13ba3436a..85beecd726b8ac980a537b5b9ab3db0915e19ae1 100644 (file)
@@ -24,7 +24,7 @@ export class GroupDetailsPanelPermissionsMiddlewareService extends DataExplorerM
         const dataExplorer = getDataExplorer(api.getState().dataExplorer, this.getId());
         const groupUuid = getCurrentGroupDetailsPanelUuid(api.getState().properties);
         if (!dataExplorer || !groupUuid) {
-            api.dispatch(groupsDetailsPanelDataExplorerIsNotSet());
+            // No-op if data explorer is not set since refresh may be triggered from elsewhere
         } else {
             try {
                 const permissionsOut = await this.services.permissionService.list({
@@ -76,12 +76,6 @@ export class GroupDetailsPanelPermissionsMiddlewareService extends DataExplorerM
     }
 }
 
-const groupsDetailsPanelDataExplorerIsNotSet = () =>
-    snackbarActions.OPEN_SNACKBAR({
-        message: 'Group permissions panel is not ready.',
-        kind: SnackbarKind.ERROR
-    });
-
 const couldNotFetchGroupDetailsContents = () =>
     snackbarActions.OPEN_SNACKBAR({
         message: 'Could not fetch group permissions.',
diff --git a/src/store/my-account/my-account-panel-actions.ts b/src/store/my-account/my-account-panel-actions.ts
deleted file mode 100644 (file)
index 9e974aa..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { Dispatch } from "redux";
-import { RootState } from "store/store";
-import { initialize } from "redux-form";
-import { ServiceRepository } from "services/services";
-import { setBreadcrumbs } from "store/breadcrumbs/breadcrumbs-actions";
-import { authActions } from "store/auth/auth-action";
-import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
-
-export const MY_ACCOUNT_FORM = 'myAccountForm';
-
-export const loadMyAccountPanel = () =>
-    (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
-        dispatch(setBreadcrumbs([{ label: 'User profile' }]));
-    };
-
-export const saveEditedUser = (resource: any) =>
-    async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
-        try {
-            await services.userService.update(resource.uuid, resource);
-            dispatch(authActions.USER_DETAILS_SUCCESS(resource));
-            dispatch(initialize(MY_ACCOUNT_FORM, resource));
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Profile has been updated.", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
-        } catch (e) {
-            return;
-        }
-    };
index 49f565911e0b04230502be62c7032b2918a16d11..1cdb6784bf42390c88ef60fa920a76c773ab71c8 100644 (file)
@@ -6,11 +6,12 @@ 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, getGroupUrl, getNavUrl } from 'routes/routes';
+import { Routes, getGroupUrl, getNavUrl, getUserProfileUrl } from 'routes/routes';
 import { RootState } from 'store/store';
 import { ServiceRepository } from 'services/services';
 import { pluginConfig } from 'plugins';
 import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
+import { USERS_PANEL_LABEL, MY_ACCOUNT_PANEL_LABEL } from 'store/breadcrumbs/breadcrumbs-actions';
 
 const navigationNotAvailable = (id: string) =>
     snackbarActions.OPEN_SNACKBAR({
@@ -69,6 +70,12 @@ 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));
@@ -135,6 +142,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);
index 688c8a0564e414dd7eecbffd70ec556debf1ac13..94f110a09563ab17537b44445b964f650fef2ce5 100644 (file)
@@ -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 (file)
index 0000000..9935518
--- /dev/null
@@ -0,0 +1,177 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+import { RootState } from "store/store";
+import { Dispatch } from 'redux';
+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';
+import { getProperty } from 'store/properties/properties';
+import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
+import { deleteResources, updateResources } from "store/resources/resources-actions";
+import { dialogActions } from "store/dialog/dialog-actions";
+import { filterResources } from "store/resources/resources";
+import { ResourceKind } from "models/resource";
+import { LinkClass, LinkResource } from "models/link";
+import { BuiltinGroups, getBuiltinGroupUuid } from "models/group";
+
+export const USER_PROFILE_PANEL_ID = 'userProfilePanel';
+export const USER_PROFILE_FORM = 'userProfileForm';
+export const DEACTIVATE_DIALOG = 'deactivateDialog';
+export const SETUP_DIALOG = 'setupDialog';
+export const ACTIVATE_DIALOG = 'activateDialog';
+export const IS_PROFILE_INACCESSIBLE = 'isProfileInaccessible';
+
+export const UserProfileGroupsActions = bindDataExplorerActions(USER_PROFILE_PANEL_ID);
+
+export const getCurrentUserProfilePanelUuid = getProperty<string>(USER_PROFILE_PANEL_ID);
+export const getUserProfileIsInaccessible = getProperty<boolean>(IS_PROFILE_INACCESSIBLE);
+
+export const loadUserProfilePanel = (userUuid?: string) =>
+  async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    // Reset isInacessible to ensure error screen is hidden
+    dispatch(propertiesActions.SET_PROPERTY({ key: IS_PROFILE_INACCESSIBLE, value: false }));
+    // Get user uuid from route or use current user uuid
+    const uuid = userUuid || getState().auth.user?.uuid;
+    if (uuid) {
+      await dispatch(propertiesActions.SET_PROPERTY({ key: USER_PROFILE_PANEL_ID, value: uuid }));
+      try {
+        const user = await services.userService.get(uuid, false);
+        dispatch(initialize(USER_PROFILE_FORM, user));
+        dispatch(updateResources([user]));
+        dispatch(UserProfileGroupsActions.REQUEST_ITEMS());
+      } catch (e) {
+        if (e.status === 404) {
+          await dispatch(propertiesActions.SET_PROPERTY({ key: IS_PROFILE_INACCESSIBLE, value: true }));
+          dispatch(reset(USER_PROFILE_FORM));
+        } else {
+          dispatch(snackbarActions.OPEN_SNACKBAR({
+            message: 'Could not load user profile',
+            kind: SnackbarKind.ERROR
+          }));
+        }
+      }
+    }
+  }
+
+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(initialize(USER_PROFILE_FORM, user));
+          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 openSetupDialog = (uuid: string) =>
+  (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+    dispatch(dialogActions.OPEN_DIALOG({
+      id: SETUP_DIALOG,
+      data: {
+        title: 'Setup user',
+        text: 'Are you sure you want to setup this user?',
+        confirmButtonLabel: 'Confirm',
+        uuid
+      }
+    }));
+  };
+
+export const openActivateDialog = (uuid: string) =>
+  (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+    dispatch(dialogActions.OPEN_DIALOG({
+      id: ACTIVATE_DIALOG,
+      data: {
+        title: 'Activate user',
+        text: 'Are you sure you want to activate this user?',
+        confirmButtonLabel: 'Confirm',
+        uuid
+      }
+    }));
+  };
+
+export const openDeactivateDialog = (uuid: string) =>
+  (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+    dispatch(dialogActions.OPEN_DIALOG({
+      id: DEACTIVATE_DIALOG,
+      data: {
+        title: 'Deactivate user',
+        text: 'Are you sure you want to deactivate this user?',
+        confirmButtonLabel: 'Confirm',
+        uuid
+      }
+    }));
+  };
+
+export const setup = (uuid: string) =>
+  async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    try {
+      const resources = await services.userService.setup(uuid);
+      dispatch(updateResources(resources.items));
+
+      // Refresh data explorer
+      dispatch(UserProfileGroupsActions.REQUEST_ITEMS());
+
+      dispatch(snackbarActions.OPEN_SNACKBAR({ message: "User has been setup", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
+    } catch (e) {
+      dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000, kind: SnackbarKind.ERROR }));
+    } finally {
+      dispatch(dialogActions.CLOSE_DIALOG({ id: SETUP_DIALOG }));
+    }
+  };
+
+export const activate = (uuid: string) =>
+  async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    try {
+      const user = await services.userService.activate(uuid);
+      dispatch(updateResources([user]));
+
+      // Refresh data explorer
+      dispatch(UserProfileGroupsActions.REQUEST_ITEMS());
+
+      dispatch(snackbarActions.OPEN_SNACKBAR({ message: "User has been activated", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
+    } catch (e) {
+      dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000, kind: SnackbarKind.ERROR }));
+    }
+  };
+
+export const deactivate = (uuid: string) =>
+  async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    try {
+      const { resources, auth } = getState();
+      // Call unsetup
+      const user = await services.userService.unsetup(uuid);
+      dispatch(updateResources([user]));
+
+      // Find and remove all users membership
+      const allUsersGroupUuid = getBuiltinGroupUuid(auth.localCluster, BuiltinGroups.ALL);
+      const memberships = filterResources((resource: LinkResource) =>
+          resource.kind === ResourceKind.LINK &&
+          resource.linkClass === LinkClass.PERMISSION &&
+          resource.headUuid === allUsersGroupUuid &&
+          resource.tailUuid === uuid
+      )(resources);
+      // Remove all users membership locally
+      dispatch<any>(deleteResources(memberships.map(link => link.uuid)));
+
+      // Refresh data explorer
+      dispatch(UserProfileGroupsActions.REQUEST_ITEMS());
+
+      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 (file)
index 0000000..a8a650a
--- /dev/null
@@ -0,0 +1,81 @@
+// 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';
+import { GroupClass } from 'models/group';
+import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
+
+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()
+            });
+            // Update resources, includes "project" groups
+            api.dispatch(updateResources(groupMembershipLinks.items));
+
+            // Get user's groups details and filter to role groups
+            const groups = await this.services.groupsService.list({
+                filters: new FilterBuilder()
+                    .addIn('uuid', groupMembershipLinks.items
+                        .map(item => item.headUuid))
+                    .addEqual('group_class', GroupClass.ROLE)
+                    .getFilters(),
+                count: "none"
+            });
+            api.dispatch(updateResources(groups.items));
+
+            // Get permission links for only role groups
+            const roleGroupMembershipLinks = await this.services.permissionService.list({
+                filters: new FilterBuilder()
+                    .addIn('head_uuid', groups.items.map(item => item.uuid))
+                    .addEqual('tail_uuid', userUuid)
+                    .addEqual('link_class', LinkClass.PERMISSION)
+                    .addEqual('head_kind', ResourceKind.GROUP)
+                    .getFilters()
+            });
+
+            api.dispatch(UserProfileGroupsActions.SET_ITEMS({
+                ...listResultsToDataExplorerItemsMeta(roleGroupMembershipLinks),
+                items: roleGroupMembershipLinks.items.map(item => item.uuid),
+            }));
+        } catch {
+            api.dispatch(couldNotFetchGroups());
+        } finally {
+            api.dispatch(progressIndicatorActions.STOP_WORKING(this.getId()));
+        }
+    }
+}
+
+const couldNotFetchGroups = () =>
+    snackbarActions.OPEN_SNACKBAR({
+        message: 'Could not fetch groups.',
+        kind: SnackbarKind.ERROR
+    });
index 4496cbd2709e5d33894e02c84cd319c5791c5e7f..c0589a6056e0393745800e0559d2af5193a0065e 100644 (file)
@@ -17,6 +17,8 @@ import { userBindedActions } from 'store/users/users-actions';
 import { getSortColumn } from "store/data-explorer/data-explorer-reducer";
 import { UserResource } from 'models/user';
 import { UserPanelColumnNames } from 'views/user-panel/user-panel';
+import { BuiltinGroups, getBuiltinGroupUuid } from 'models/group';
+import { LinkClass } from 'models/link';
 
 export class UserMiddlewareService extends DataExplorerMiddlewareService {
     constructor(private services: ServiceRepository, id: string) {
@@ -27,47 +29,33 @@ export class UserMiddlewareService extends DataExplorerMiddlewareService {
         const state = api.getState();
         const dataExplorer = getDataExplorer(state.dataExplorer, this.getId());
         try {
-            const responseFirstName = await this.services.userService.list(getParamsFirstName(dataExplorer));
-            if (responseFirstName.itemsAvailable) {
-                api.dispatch(updateResources(responseFirstName.items));
-                api.dispatch(setItems(responseFirstName));
-            } else {
-                const responseLastName = await this.services.userService.list(getParamsLastName(dataExplorer));
-                api.dispatch(updateResources(responseLastName.items));
-                api.dispatch(setItems(responseLastName));
-            }
+            const users = await this.services.userService.list(getParams(dataExplorer));
+            api.dispatch(updateResources(users.items));
+            api.dispatch(setItems(users));
+
+            // Get "all users" group memberships
+            const allUsersGroupUuid = getBuiltinGroupUuid(state.auth.localCluster, BuiltinGroups.ALL);
+            const allUserMemberships = await this.services.permissionService.list({
+                filters: new FilterBuilder()
+                    .addEqual('head_uuid', allUsersGroupUuid)
+                    .addEqual('link_class', LinkClass.PERMISSION)
+                    .getFilters()
+            });
+            api.dispatch(updateResources(allUserMemberships.items));
         } catch {
             api.dispatch(couldNotFetchUsers());
         }
     }
 }
 
-const getParamsFirstName = (dataExplorer: DataExplorer) => ({
-    ...dataExplorerToListParams(dataExplorer),
-    order: getOrder(dataExplorer),
-    filters: getFiltersFirstName(dataExplorer)
-});
-
-const getParamsLastName = (dataExplorer: DataExplorer) => ({
+const getParams = (dataExplorer: DataExplorer) => ({
     ...dataExplorerToListParams(dataExplorer),
     order: getOrder(dataExplorer),
-    filters: getFiltersLastName(dataExplorer)
+    filters: new FilterBuilder()
+        .addFullTextSearch(dataExplorer.searchValue)
+        .getFilters()
 });
 
-const getFiltersFirstName = (dataExplorer: DataExplorer) => {
-    const filters = new FilterBuilder()
-        .addILike("first_name", dataExplorer.searchValue)
-        .getFilters();
-    return filters;
-};
-
-const getFiltersLastName = (dataExplorer: DataExplorer) => {
-    const filters = new FilterBuilder()
-        .addILike("last_name", dataExplorer.searchValue)
-        .getFilters();
-    return filters;
-};
-
 export const getOrder = (dataExplorer: DataExplorer) => {
     const sortColumn = getSortColumn(dataExplorer);
     const order = new OrderBuilder<UserResource>();
@@ -75,13 +63,23 @@ export const getOrder = (dataExplorer: DataExplorer) => {
         const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC
             ? OrderDirection.ASC
             : OrderDirection.DESC;
-        const columnName = sortColumn && sortColumn.name === UserPanelColumnNames.LAST_NAME ? "lastName" : "firstName";
-        return order
-            .addOrder(sortDirection, columnName)
-            .getOrder();
-    } else {
-        return order.getOrder();
+        switch (sortColumn.name) {
+            case UserPanelColumnNames.NAME:
+                order.addOrder(sortDirection, "firstName")
+                    .addOrder(sortDirection, "lastName");
+                break;
+            case UserPanelColumnNames.UUID:
+                order.addOrder(sortDirection, "uuid");
+                break;
+            case UserPanelColumnNames.EMAIL:
+                order.addOrder(sortDirection, "email");
+                break;
+            case UserPanelColumnNames.USERNAME:
+                order.addOrder(sortDirection, "username");
+                break;
+        }
     }
+    return order.getOrder();
 };
 
 export const setItems = (listResults: ListResults<UserResource>) =>
@@ -94,4 +92,4 @@ const couldNotFetchUsers = () =>
     snackbarActions.OPEN_SNACKBAR({
         message: 'Could not fetch users.',
         kind: SnackbarKind.ERROR
-    });
\ No newline at end of file
+    });
index cd4d5c734988abc7e549589aad426e2cd6bf67e5..b553b324e5aea7298f26845485ab75f3fe1f0f32 100644 (file)
@@ -8,31 +8,31 @@ 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, stopSubmit } from "redux-form";
 import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
 import { UserResource } from "models/user";
-import { getResource } from 'store/resources/resources';
+import { filterResources, 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 { VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD, VIRTUAL_MACHINE_ADD_LOGIN_VM_FIELD } from "store/virtual-machines/virtual-machines-actions";
+import { PermissionLevel } from "models/permission";
+import { updateResources } from "store/resources/resources-actions";
+import { BuiltinGroups, getBuiltinGroupUuid } from "models/group";
+import { LinkClass, LinkResource } from "models/link";
+import { ResourceKind } from "models/resource";
 
 export const USERS_PANEL_ID = 'usersPanel';
 export const USER_ATTRIBUTES_DIALOG = 'userAttributesDialog';
 export const USER_CREATE_FORM_NAME = 'userCreateFormName';
-export const USER_MANAGEMENT_DIALOG = 'userManageDialog';
-export const SETUP_SHELL_ACCOUNT_DIALOG = 'setupShellAccountDialog';
 
 export interface UserCreateFormDialogData {
     email: string;
-    virtualMachineName: string;
-    groupVirtualMachine: string;
+    [VIRTUAL_MACHINE_ADD_LOGIN_VM_FIELD]: string;
+    [VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD]: 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) => {
@@ -41,31 +41,37 @@ export const openUserAttributes = (uuid: string) =>
         dispatch(dialogActions.OPEN_DIALOG({ id: USER_ATTRIBUTES_DIALOG, data }));
     };
 
-export const openUserManagement = (uuid: string) =>
-    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const { resources } = getState();
-        const data = getResource<UserResource>(uuid)(resources);
-        dispatch(dialogActions.OPEN_DIALOG({ id: USER_MANAGEMENT_DIALOG, data }));
-    };
-
-export const openSetupShellAccount = (uuid: string) =>
-    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const { resources } = getState();
-        const user = getResource<UserResource>(uuid)(resources);
-        const virtualMachines = await services.virtualMachineService.list();
-        dispatch(dialogActions.CLOSE_DIALOG({ id: USER_MANAGEMENT_DIALOG }));
-        dispatch(dialogActions.OPEN_DIALOG({ id: SETUP_SHELL_ACCOUNT_DIALOG, data: { user, ...virtualMachines } }));
-    };
-
 export const loginAs = (uuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const { resources } = getState();
-        const data = getResource<UserResource>(uuid)(resources);
-        const client = await services.apiClientAuthorizationService.create({ ownerUuid: uuid });
-        if (data) {
-            dispatch<any>(authActions.INIT_USER({ user: data, token: getTokenV2(client) }));
-            window.location.reload();
-            dispatch<any>(navigateToRootProject);
+        const userUuid = getUserUuid(getState());
+        if (userUuid === uuid) {
+            dispatch(snackbarActions.OPEN_SNACKBAR({
+                message: 'You are already logged in as this user',
+                kind: SnackbarKind.WARNING
+            }));
+        } else {
+            try {
+                const { resources } = getState();
+                const data = getResource<UserResource>(uuid)(resources);
+                const client = await services.apiClientAuthorizationService.create({ ownerUuid: uuid }, false);
+                if (data) {
+                    dispatch<any>(authActions.INIT_USER({ user: data, token: getTokenV2(client) }));
+                    window.location.reload();
+                    dispatch<any>(navigateToRootProject);
+                }
+            } catch (e) {
+                if (e.status === 403) {
+                    dispatch(snackbarActions.OPEN_SNACKBAR({
+                        message: 'You do not have permission to login as this user',
+                        kind: SnackbarKind.WARNING
+                    }));
+                } else {
+                    dispatch(snackbarActions.OPEN_SNACKBAR({
+                        message: 'Failed to login as this user',
+                        kind: SnackbarKind.ERROR
+                    }));
+                }
+            }
         }
     };
 
@@ -84,12 +90,28 @@ export const openUserProjects = (uuid: string) =>
         dispatch<any>(navigateTo(uuid));
     };
 
-
-export const createUser = (user: UserCreateFormDialogData) =>
+export const createUser = (data: UserCreateFormDialogData) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(startSubmit(USER_CREATE_FORM_NAME));
         try {
-            const newUser = await services.userService.create({ ...user });
+            const newUser = await services.userService.create({
+                email: data.email,
+            });
+            dispatch(updateResources([newUser]));
+
+            if (data[VIRTUAL_MACHINE_ADD_LOGIN_VM_FIELD]) {
+                const permission = await services.permissionService.create({
+                    headUuid: data[VIRTUAL_MACHINE_ADD_LOGIN_VM_FIELD],
+                    tailUuid: newUser.uuid,
+                    name: PermissionLevel.CAN_LOGIN,
+                    properties: {
+                        username: newUser.username,
+                        groups: data.groups,
+                    }
+                });
+                dispatch(updateResources([permission]));
+            }
+
             dispatch(dialogActions.CLOSE_DIALOG({ id: USER_CREATE_FORM_NAME }));
             dispatch(reset(USER_CREATE_FORM_NAME));
             dispatch(snackbarActions.OPEN_SNACKBAR({ message: "User has been successfully created.", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
@@ -98,23 +120,8 @@ export const createUser = (user: UserCreateFormDialogData) =>
             return newUser;
         } catch (e) {
             return;
-        }
-    };
-
-
-export const setupUserVM = (setupData: SetupShellAccountFormDialogData) =>
-    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        dispatch(startSubmit(USER_CREATE_FORM_NAME));
-        try {
-            // TODO: make correct API call
-            // const setupResult = await services.userService.setup({ ...setupData });
-            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;
+        } finally {
+            dispatch(stopSubmit(USER_CREATE_FORM_NAME));
         }
     };
 
@@ -129,21 +136,6 @@ export const openUserPanel = () =>
         }
     };
 
-export const toggleIsActive = (uuid: string) =>
-    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const { resources } = getState();
-        const data = getResource<UserResource>(uuid)(resources);
-        const isActive = data!.isActive;
-        let newActivity;
-        if (isActive) {
-            newActivity = await services.userService.unsetup(uuid);
-        } else {
-            newActivity = await services.userService.update(uuid, { isActive: true });
-        }
-        dispatch<any>(loadUsersPanel());
-        return newActivity;
-    };
-
 export const toggleIsAdmin = (uuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const { resources } = getState();
@@ -154,14 +146,31 @@ 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());
     };
+
+export enum UserAccountStatus {
+        ACTIVE = 'Active',
+        INACTIVE = 'Inactive',
+        SETUP = 'Setup',
+    }
+
+export const getUserAccountStatus = (state: RootState, uuid: string) => {
+    const user = getResource<UserResource>(uuid)(state.resources);
+    // Get membership links for all users group
+    const allUsersGroupUuid = getBuiltinGroupUuid(state.auth.localCluster, BuiltinGroups.ALL);
+    const permissions = filterResources((resource: LinkResource) =>
+        resource.kind === ResourceKind.LINK &&
+        resource.linkClass === LinkClass.PERMISSION &&
+        resource.headUuid === allUsersGroupUuid &&
+        resource.tailUuid === uuid
+    )(state.resources);
+
+    return user && user.isActive
+        ? UserAccountStatus.ACTIVE
+        : permissions.length > 0
+            ? UserAccountStatus.SETUP
+            : UserAccountStatus.INACTIVE;
+}
index e2cf6fd4c0e33da5c3da8a901d153c3937a9c0aa..7034b4a570f1ad1c135f6fc00bfdc6868b5190b0 100644 (file)
@@ -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: {
index 7958463a5a33e520081badac1f425eeebfcc397c..ba405cb8f2938f9ce190b32eaed81ec8276aa76a 100644 (file)
@@ -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';
@@ -57,7 +60,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';
@@ -79,6 +81,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';
@@ -100,6 +103,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';
 
@@ -138,6 +142,7 @@ export const loadWorkbench = () =>
             dispatch(groupPanelActions.GroupsPanelActions.SET_COLUMNS({ columns: groupsPanelColumns }));
             dispatch(groupDetailsPanelActions.GroupMembersPanelActions.SET_COLUMNS({ columns: groupDetailsMembersPanelColumns }));
             dispatch(groupDetailsPanelActions.GroupPermissionsPanelActions.SET_COLUMNS({ columns: groupDetailsPermissionsPanelColumns }));
+            dispatch(userProfilePanelActions.UserProfileGroupsActions.SET_COLUMNS({ columns: userProfileGroupsColumns }));
             dispatch(linkPanelActions.SET_COLUMNS({ columns: linkPanelColumns }));
             dispatch(apiClientAuthorizationsActions.SET_COLUMNS({ columns: apiClientAuthorizationPanelColumns }));
             dispatch(collectionsContentAddressActions.SET_COLUMNS({ columns: collectionContentAddressPanelColumns }));
@@ -504,10 +509,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>) => {
@@ -522,7 +535,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(
index 81fed2ccccdb4220822a342a38eb3af10a38a735..6e72ef689829ef3aad0b6cc1c265d0467491ee21 100644 (file)
@@ -31,6 +31,8 @@ export const PROCESS_DESCRIPTION_VALIDATION = [maxLength(255)];
 export const REPOSITORY_NAME_VALIDATION = [require, maxLength(255)];
 
 export const USER_EMAIL_VALIDATION = [require, maxLength(255)];
+export const PROFILE_EMAIL_VALIDATION = [maxLength(255)];
+export const PROFILE_URL_VALIDATION = [maxLength(255)];
 export const USER_LENGTH_VALIDATION = [maxLength(255)];
 
 export const SSH_KEY_PUBLIC_VALIDATION = [require, isRsaKey, maxLength(1024)];
index 2edb12d9fdb1e22a2d4b8255cc6205a5cceb9147..c298e1ab33ba1ac24564acbbee1f0d8bbb0f2040 100644 (file)
@@ -3,9 +3,21 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
-import { AdvancedIcon, ProjectIcon, AttributesIcon } from "components/icon/icon";
+import {
+    AdvancedIcon,
+    ProjectIcon,
+    AttributesIcon,
+    DeactivateUserIcon,
+    UserPanelIcon,
+    LoginAsIcon,
+    AdminMenuIcon,
+    ActiveIcon,
+} from "components/icon/icon";
 import { openAdvancedTabDialog } from 'store/advanced-tab/advanced-tab';
-import { openUserAttributes, openUserProjects } from "store/users/users-actions";
+import { loginAs, openUserAttributes, openUserProjects } from "store/users/users-actions";
+import { openSetupDialog, openDeactivateDialog, openActivateDialog } from "store/user-profile/user-profile-actions";
+import { navigateToUserProfile } from "store/navigation/navigation-action";
+import { canActivateUser, canDeactivateUser, canSetupUser, isAdmin, needsUserProfileLink, isOtherUser } from "store/context-menu/context-menu-filters";
 
 export const userActionSet: ContextMenuActionSet = [[{
     name: "Attributes",
@@ -25,13 +37,51 @@ export const userActionSet: ContextMenuActionSet = [[{
     execute: (dispatch, { uuid }) => {
         dispatch<any>(openAdvancedTabDialog(uuid));
     }
-}, /*
-    // Neither of the buttons on this dialog work correctly (bugs #16114 and #16124) so hide it for now.
-    {
-    name: "Manage",
+}, {
+    name: "Account Settings",
     icon: UserPanelIcon,
     execute: (dispatch, { uuid }) => {
-        dispatch<any>(openUserManagement(uuid));
-    }
-} */
-]];
+        dispatch<any>(navigateToUserProfile(uuid));
+    },
+    filters: [needsUserProfileLink]
+}],[{
+    name: "Activate User",
+    icon: ActiveIcon,
+    execute: (dispatch, { uuid }) => {
+        dispatch<any>(openActivateDialog(uuid));
+    },
+    filters: [
+        isAdmin,
+        canActivateUser,
+    ],
+}, {
+    name: "Setup User",
+    icon: AdminMenuIcon,
+    execute: (dispatch, { uuid }) => {
+        dispatch<any>(openSetupDialog(uuid));
+    },
+    filters: [
+        isAdmin,
+        canSetupUser,
+    ],
+}, {
+    name: "Deactivate User",
+    icon: DeactivateUserIcon,
+    execute: (dispatch, { uuid }) => {
+        dispatch<any>(openDeactivateDialog(uuid));
+    },
+    filters: [
+        isAdmin,
+        canDeactivateUser,
+    ],
+}, {
+    name: "Login As User",
+    icon: LoginAsIcon,
+    execute: (dispatch, { uuid }) => {
+        dispatch<any>(loginAs(uuid));
+    },
+    filters: [
+        isAdmin,
+        isOtherUser,
+    ],
+}]];
index f2c43ced1f9e0ded2aa6277f72799e45d974863d..6f3a4389211363e9294bbfe3c53fdf530d32195a 100644 (file)
@@ -14,9 +14,19 @@ import { sortByProperty } from "common/array-utils";
 type DataProps = Pick<ContextMenuProps, "anchorEl" | "items" | "open"> & { resource?: ContextMenuResource };
 const mapStateToProps = (state: RootState): DataProps => {
     const { open, position, resource } = state.contextMenu;
+
+    const filteredItems = getMenuActionSet(resource).map((group) => (group.filter((item) => {
+        if (resource && item.filters) {
+            // Execute all filters on this item, every returns true IFF all filters return true
+            return item.filters.every((filter) => filter(state, resource));
+        } else {
+            return true;
+        }
+    })));
+
     return {
         anchorEl: resource ? createAnchorAt(position) : undefined,
-        items: getMenuActionSet(resource),
+        items: filteredItems,
         open,
         resource
     };
@@ -59,9 +69,9 @@ export const addMenuActionSet = (name: string, itemSet: ContextMenuActionSet) =>
 };
 
 const emptyActionSet: ContextMenuActionSet = [];
-const getMenuActionSet = (resource?: ContextMenuResource): ContextMenuActionSet => {
-    return resource ? menuActionSets.get(resource.menuKind) || emptyActionSet : emptyActionSet;
-};
+const getMenuActionSet = (resource?: ContextMenuResource): ContextMenuActionSet => (
+   resource ? menuActionSets.get(resource.menuKind) || emptyActionSet : emptyActionSet
+);
 
 export enum ContextMenuKind {
     API_CLIENT_AUTHORIZATION = "ApiClientAuthorization",
index a2acaca4525e6bceb6d65f869dae09f986412c21..e854da0ee6caeebfdfd41af58dc8a081b58ecf0e 100644 (file)
@@ -6,7 +6,21 @@ import React from 'react';
 import { Grid, Typography, withStyles, Tooltip, IconButton, Checkbox } from '@material-ui/core';
 import { FavoriteStar, PublicFavoriteStar } from '../favorite-star/favorite-star';
 import { Resource, ResourceKind, TrashableResource } from 'models/resource';
-import { ProjectIcon, FilterGroupIcon, CollectionIcon, ProcessIcon, DefaultIcon, ShareIcon, CollectionOldVersionIcon, WorkflowIcon, RemoveIcon, RenameIcon } from 'components/icon/icon';
+import {
+    ProjectIcon,
+    FilterGroupIcon,
+    CollectionIcon,
+    ProcessIcon,
+    DefaultIcon,
+    ShareIcon,
+    CollectionOldVersionIcon,
+    WorkflowIcon,
+    RemoveIcon,
+    RenameIcon,
+    ActiveIcon,
+    SetupIcon,
+    InactiveIcon,
+} from 'components/icon/icon';
 import { formatDate, formatFileSize, formatTime } from 'common/formatters';
 import { resourceLabel } from 'common/labels';
 import { connect, DispatchProp } from 'react-redux';
@@ -21,14 +35,14 @@ import { ResourceStatus as WorkflowStatus } from 'views/workflow-panel/workflow-
 import { getUuidPrefix, openRunProcess } from 'store/workflow-panel/workflow-panel-actions';
 import { openSharingDialog } from 'store/sharing-dialog/sharing-dialog-actions';
 import { getUserFullname, getUserDisplayName, User, UserResource } from 'models/user';
-import { toggleIsActive, toggleIsAdmin } from 'store/users/users-actions';
+import { toggleIsAdmin } from 'store/users/users-actions';
 import { LinkClass, LinkResource } from 'models/link';
-import { navigateTo, navigateToGroupDetails } from 'store/navigation/navigation-action';
+import { navigateTo, navigateToGroupDetails, navigateToUserProfile } from 'store/navigation/navigation-action';
 import { withResourceData } from 'views-components/data-explorer/with-resources';
 import { CollectionResource } from 'models/collection';
 import { IllegalNamingWarning } from 'components/warning/warning';
 import { loadResource } from 'store/resources/resources-actions';
-import { GroupClass, GroupResource, isBuiltinGroup } from 'models/group';
+import { BuiltinGroups, getBuiltinGroupUuid, GroupClass, GroupResource, isBuiltinGroup } from 'models/group';
 import { openRemoveGroupMemberDialog } from 'store/group-details-panel/group-details-panel-actions';
 import { setMemberIsHidden } from 'store/group-details-panel/group-details-panel-actions';
 import { formatPermissionLevel } from 'views-components/sharing-dialog/permission-select';
@@ -36,6 +50,7 @@ import { PermissionLevel } from 'models/permission';
 import { openPermissionEditContextMenu } from 'store/context-menu/context-menu-actions';
 import { getUserUuid } from 'common/getuser';
 import { VirtualMachinesResource } from 'models/virtual-machines';
+import { CopyToClipboardSnackbar } from 'components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar';
 
 const renderName = (dispatch: Dispatch, item: GroupContentsResource) => {
 
@@ -161,24 +176,32 @@ export const ResourceLastName = connect(
         return resource || { lastName: '' };
     })(renderLastName);
 
-const renderFullName = (item: { firstName: string, lastName: string }) =>
-    <Typography noWrap>{(item.firstName + " " + item.lastName).trim()}</Typography>;
+const renderFullName = (dispatch: Dispatch ,item: { uuid: string, firstName: string, lastName: string }, link?: boolean) => {
+    const displayName = (item.firstName + " " + item.lastName).trim() || item.uuid;
+    return link ? <Typography noWrap
+        color="primary"
+        style={{ 'cursor': 'pointer' }}
+        onClick={() => dispatch<any>(navigateToUserProfile(item.uuid))}>
+            {displayName}
+    </Typography> :
+    <Typography noWrap>{displayName}</Typography>;
+}
 
-export const ResourceFullName = connect(
-    (state: RootState, props: { uuid: string }) => {
+export const UserResourceFullName = connect(
+    (state: RootState, props: { uuid: string, link?: boolean }) => {
         const resource = getResource<UserResource>(props.uuid)(state.resources);
-        return resource || { firstName: '', lastName: '' };
-    })(renderFullName);
-
+        return {item: resource || { uuid: '', firstName: '', lastName: '' }, link: props.link};
+    })((props: {item: {uuid: string, firstName: string, lastName: string}, link?: boolean} & DispatchProp<any>) => renderFullName(props.dispatch, props.item, props.link));
 
 const renderUuid = (item: { uuid: string }) =>
-    <Typography data-cy="uuid" noWrap>{item.uuid}</Typography>;
+    <Typography data-cy="uuid" noWrap>
+        {item.uuid}
+        <CopyToClipboardSnackbar value={item.uuid} />
+    </Typography>;
 
-export const ResourceUuid = connect(
-    (state: RootState, props: { uuid: string }) => {
-        const resource = getResource<UserResource>(props.uuid)(state.resources);
-        return resource || { uuid: '' };
-    })(renderUuid);
+export const ResourceUuid = connect((state: RootState, props: { uuid: string }) => (
+        getResource<UserResource>(props.uuid)(state.resources) || { uuid: '' }
+    ))(renderUuid);
 
 const renderEmail = (item: { email: string }) =>
     <Typography noWrap>{item.email}</Typography>;
@@ -189,33 +212,61 @@ export const ResourceEmail = connect(
         return resource || { email: '' };
     })(renderEmail);
 
-const renderIsActive = (props: { uuid: string, kind: ResourceKind, isActive: boolean, toggleIsActive: (uuid: string) => void, disabled?: boolean }) => {
-    if (props.kind === ResourceKind.USER) {
-        return <Checkbox
-            color="primary"
-            checked={props.isActive}
-            disabled={!!props.disabled}
-            onClick={() => props.toggleIsActive(props.uuid)} />;
+enum UserAccountStatus {
+    ACTIVE = 'Active',
+    INACTIVE = 'Inactive',
+    SETUP = 'Setup',
+    UNKNOWN = ''
+}
+
+const renderAccountStatus = (props: {status: UserAccountStatus}) =>
+    <Grid container alignItems="center" wrap="nowrap" spacing={8} data-cy="account-status">
+        <Grid item>
+            {(() => {
+                switch(props.status) {
+                    case UserAccountStatus.ACTIVE:
+                        return <ActiveIcon style={{color: '#4caf50', verticalAlign: "middle"}} />;
+                    case UserAccountStatus.SETUP:
+                        return <SetupIcon style={{color: '#2196f3', verticalAlign: "middle"}} />;
+                    case UserAccountStatus.INACTIVE:
+                        return <InactiveIcon style={{color: '#9e9e9e', verticalAlign: "middle"}} />;
+                    default:
+                        return <></>;
+                }
+            })()}
+        </Grid>
+        <Grid item>
+            <Typography noWrap>
+                {props.status}
+            </Typography>
+        </Grid>
+    </Grid>;
+
+const getUserAccountStatus = (state: RootState, props: { uuid: string }) => {
+    const user = getResource<UserResource>(props.uuid)(state.resources);
+    // Get membership links for all users group
+    const allUsersGroupUuid = getBuiltinGroupUuid(state.auth.localCluster, BuiltinGroups.ALL);
+    const permissions = filterResources((resource: LinkResource) =>
+        resource.kind === ResourceKind.LINK &&
+        resource.linkClass === LinkClass.PERMISSION &&
+        resource.headUuid === allUsersGroupUuid &&
+        resource.tailUuid === props.uuid
+    )(state.resources);
+
+    if (user) {
+        return user.isActive ? {status: UserAccountStatus.ACTIVE} : permissions.length > 0 ? {status: UserAccountStatus.SETUP} : {status: UserAccountStatus.INACTIVE};
     } else {
-        return <Typography />;
+        return {status: UserAccountStatus.UNKNOWN};
     }
 }
 
-export const ResourceIsActive = connect(
-    (state: RootState, props: { uuid: string, disabled?: boolean }) => {
-        const resource = getResource<UserResource>(props.uuid)(state.resources);
-        return resource ? {...resource, disabled: !!props.disabled} : { isActive: false, kind: ResourceKind.NONE };
-    }, { toggleIsActive }
-)(renderIsActive);
-
-export const ResourceLinkTailIsActive = connect(
-    (state: RootState, props: { uuid: string, disabled?: boolean }) => {
+export const ResourceLinkTailAccountStatus = connect(
+    (state: RootState, props: { uuid: string }) => {
         const link = getResource<LinkResource>(props.uuid)(state.resources);
-        const tailResource = getResource<UserResource>(link?.tailUuid || '')(state.resources);
+        return link && link.tailKind === ResourceKind.USER ? getUserAccountStatus(state, {uuid: link.tailUuid}) : {status: UserAccountStatus.UNKNOWN};
+    })(renderAccountStatus);
 
-        return tailResource ? {...tailResource, disabled: !!props.disabled} : { isActive: false, kind: ResourceKind.NONE };
-    }, { toggleIsActive }
-)(renderIsActive);
+export const UserResourceAccountStatus = connect(getUserAccountStatus)(renderAccountStatus);
 
 const renderIsHidden = (props: {
                             memberLinkUuid: string,
@@ -230,7 +281,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 +317,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 }) => {
index d8d25da494c57ff587557039c62d1a828afb7822..6be7b28f0ebea3f5d370d7397cfdbd2e8ec228c6 100644 (file)
@@ -7,8 +7,16 @@ import { InjectedFormProps } from 'redux-form';
 import { WithDialogProps } from 'store/dialog/with-dialog';
 import { FormDialog } from 'components/form-dialog/form-dialog';
 import { UserEmailField, UserVirtualMachineField, UserGroupsVirtualMachineField } from 'views-components/form-fields/user-form-fields';
+import { UserCreateFormDialogData } from 'store/users/users-actions';
+import { UserResource } from 'models/user';
+import { VirtualMachinesResource } from 'models/virtual-machines';
 
-export type DialogUserProps = WithDialogProps<{}> & InjectedFormProps<any>;
+export type DialogUserProps = WithDialogProps<{}> & InjectedFormProps<UserCreateFormDialogData>;
+
+interface DataProps {
+    user: UserResource;
+    items: VirtualMachinesResource[];
+}
 
 export const UserRepositoryCreate = (props: DialogUserProps) =>
     <FormDialog
@@ -20,6 +28,6 @@ export const UserRepositoryCreate = (props: DialogUserProps) =>
 
 const UserAddFields = (props: DialogUserProps) => <span>
     <UserEmailField />
-    <UserVirtualMachineField data={props.data}/>
+    <UserVirtualMachineField data={props.data as DataProps}/>
     <UserGroupsVirtualMachineField />
 </span>;
diff --git a/src/views-components/dialog-forms/setup-shell-account-dialog.tsx b/src/views-components/dialog-forms/setup-shell-account-dialog.tsx
deleted file mode 100644 (file)
index 3bf700b..0000000
+++ /dev/null
@@ -1,86 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-import React from 'react';
-import { compose } from "redux";
-import { reduxForm, InjectedFormProps, Field } from 'redux-form';
-import { withDialog, WithDialogProps } from "store/dialog/with-dialog";
-import { FormDialog } from 'components/form-dialog/form-dialog';
-import { TextField } from 'components/text-field/text-field';
-import { VirtualMachinesResource } from 'models/virtual-machines';
-import { USER_LENGTH_VALIDATION, 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 { UserResource } from 'models/user';
-
-export const SetupShellAccountDialog = compose(
-    withDialog(SETUP_SHELL_ACCOUNT_DIALOG),
-    reduxForm<SetupShellAccountFormDialogData>({
-        form: SETUP_SHELL_ACCOUNT_DIALOG,
-        onSubmit: (data, dispatch) => {
-            dispatch(setupUserVM(data));
-        }
-    })
-)(
-    (props: SetupShellAccountDialogComponentProps) =>
-        <FormDialog
-            dialogTitle='Setup shell account'
-            formFields={SetupShellAccountFormFields}
-            submitLabel='Submit'
-            {...props}
-        />
-);
-
-interface UserProps {
-    data: {
-        user: UserResource;
-    };
-}
-
-interface VirtualMachinesProps {
-    data: {
-        items: VirtualMachinesResource[];
-    };
-}
-interface DataProps {
-    user: UserResource;
-    items: VirtualMachinesResource[];
-}
-
-const UserEmailField = ({ data }: UserProps) =>
-    <span>
-        <Field
-            name='email'
-            component={TextField as any}
-            disabled
-            label={data.user.email} /></span>;
-
-const UserVirtualMachineField = ({ data }: VirtualMachinesProps) =>
-    <div style={{ marginBottom: '21px' }}>
-        <InputLabel>Virtual Machine</InputLabel>
-        <Field
-            name='virtualMachine'
-            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)" />;
-
-const getVirtualMachinesList = (virtualMachines: VirtualMachinesResource[]) =>
-    [{ key: "", value: "" }].concat(virtualMachines.map(it => ({ key: it.hostname, value: it.hostname })));
-
-type SetupShellAccountDialogComponentProps = WithDialogProps<{}> & InjectedFormProps<SetupShellAccountFormDialogData>;
-
-const SetupShellAccountFormFields = (props: SetupShellAccountDialogComponentProps) =>
-    <>
-        <UserEmailField data={props.data as DataProps} />
-        <UserVirtualMachineField data={props.data as DataProps} />
-        <UserGroupsVirtualMachineField />
-    </>;
index 393f29d325a6f28321b8f8effa90ca06d2d66c47..12fc91e2423553b62ceb71bc474c120d6aca04c0 100644 (file)
@@ -5,10 +5,18 @@
 import React from "react";
 import { Field } from "redux-form";
 import { TextField } from "components/text-field/text-field";
-import { USER_EMAIL_VALIDATION, USER_LENGTH_VALIDATION } from "validators/validators";
+import { USER_EMAIL_VALIDATION, CHOOSE_VM_VALIDATION } from "validators/validators";
 import { NativeSelectField } from "components/select-field/select-field";
 import { InputLabel } from "@material-ui/core";
 import { VirtualMachinesResource } from "models/virtual-machines";
+import { VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD, VIRTUAL_MACHINE_ADD_LOGIN_VM_FIELD } from "store/virtual-machines/virtual-machines-actions";
+import { GroupArrayInput } from "views-components/virtual-machines-dialog/group-array-input";
+
+interface VirtualMachinesProps {
+    data: {
+        items: VirtualMachinesResource[];
+    };
+}
 
 export const UserEmailField = () =>
     <Field
@@ -18,24 +26,31 @@ export const UserEmailField = () =>
         autoFocus={true}
         label="Email" />;
 
-export const UserVirtualMachineField = ({ data }: any) =>
+export const RequiredUserVirtualMachineField = ({ data }: VirtualMachinesProps) =>
+    <div style={{ marginBottom: '21px' }}>
+        <InputLabel>Virtual Machine</InputLabel>
+        <Field
+            name={VIRTUAL_MACHINE_ADD_LOGIN_VM_FIELD}
+            component={NativeSelectField as any}
+            validate={CHOOSE_VM_VALIDATION}
+            items={getVirtualMachinesList(data.items)} />
+    </div>;
+
+export 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={USER_LENGTH_VALIDATION}
             items={getVirtualMachinesList(data.items)} />
     </div>;
 
 export 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[]) => {
-    const mappedVirtualMachines = virtualMachines.map(it => ({ key: it.hostname, value: it.hostname }));
-    return mappedVirtualMachines;
-};
+const getVirtualMachinesList = (virtualMachines: VirtualMachinesResource[]) =>
+    [{ key: "", value: "" }].concat(virtualMachines.map(it => ({ key: it.uuid, value: it.hostname })));
diff --git a/src/views-components/user-dialog/activate-dialog.tsx b/src/views-components/user-dialog/activate-dialog.tsx
new file mode 100644 (file)
index 0000000..79e8330
--- /dev/null
@@ -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 { activate, ACTIVATE_DIALOG } from 'store/user-profile/user-profile-actions';
+
+const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps<any>) => ({
+    onConfirm: () => {
+        props.closeDialog();
+        dispatch<any>(activate(props.data.uuid));
+    }
+});
+
+export const ActivateDialog = compose(
+    withDialog(ACTIVATE_DIALOG),
+    connect(null, mapDispatchToProps)
+)(ConfirmationDialog);
diff --git a/src/views-components/user-dialog/deactivate-dialog.tsx b/src/views-components/user-dialog/deactivate-dialog.tsx
new file mode 100644 (file)
index 0000000..8aefa92
--- /dev/null
@@ -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 { deactivate, DEACTIVATE_DIALOG } from 'store/user-profile/user-profile-actions';
+
+const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps<any>) => ({
+    onConfirm: () => {
+        props.closeDialog();
+        dispatch<any>(deactivate(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
deleted file mode 100644 (file)
index b812f5c..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import React from "react";
-import { compose, Dispatch } from "redux";
-import { connect } from "react-redux";
-import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography } from "@material-ui/core";
-import { WithDialogProps } from "store/dialog/with-dialog";
-import { withDialog } from 'store/dialog/with-dialog';
-import { WithStyles, withStyles } from '@material-ui/core/styles';
-import { ArvadosTheme } from 'common/custom-theme';
-import { USER_MANAGEMENT_DIALOG, openSetupShellAccount, loginAs } from "store/users/users-actions";
-import { getUserDisplayName } from "models/user";
-
-type CssRules = 'spacing';
-
-const styles = withStyles<CssRules>((theme: ArvadosTheme) => ({
-    spacing: {
-        paddingBottom: theme.spacing.unit * 2,
-        paddingTop: theme.spacing.unit * 2,
-    }
-}));
-
-interface UserManageDataProps {
-    data: any;
-}
-
-interface UserManageActionProps {
-    openSetupShellAccount: (uuid: string) => void;
-    loginAs: (uuid: string) => void;
-}
-
-const mapDispatchToProps = (dispatch: Dispatch) => ({
-    openSetupShellAccount: (uuid: string) => dispatch<any>(openSetupShellAccount(uuid)),
-    loginAs: (uuid: string) => dispatch<any>(loginAs(uuid))
-});
-
-type UserManageProps = UserManageDataProps & UserManageActionProps & WithStyles<CssRules>;
-
-export const UserManageDialog = compose(
-    connect(null, mapDispatchToProps),
-    withDialog(USER_MANAGEMENT_DIALOG),
-    styles)(
-        (props: WithDialogProps<UserManageProps> & UserManageProps) =>
-            <Dialog open={props.open}
-                onClose={props.closeDialog}
-                fullWidth
-                maxWidth="md">
-                {props.data &&
-                    <span>
-                        <DialogTitle>{`Manage - ${getUserDisplayName(props.data)}`}</DialogTitle>
-                        <DialogContent>
-                            <Typography variant='body1' className={props.classes.spacing}>
-                                As an admin, you can log in as this user. When you’ve finished, you will need to log out and log in again with your own account.
-                    </Typography>
-                            <Button variant="contained" color="primary" onClick={() => props.loginAs(props.data.uuid)}>
-                                {`LOG IN AS ${getUserDisplayName(props.data)}`}
-                            </Button>
-                            <Typography variant='body1' className={props.classes.spacing}>
-                                As an admin, you can setup a shell account for this user. The login name is automatically generated from the user's e-mail address.
-                    </Typography>
-                            <Button variant="contained" color="primary" onClick={() => props.openSetupShellAccount(props.data.uuid)}>
-                                {`SETUP SHELL ACCOUNT FOR ${getUserDisplayName(props.data)}`}
-                            </Button>
-                        </DialogContent></span>}
-
-                <DialogActions>
-                    <Button
-                        variant='text'
-                        color='primary'
-                        onClick={props.closeDialog}>
-                        Close
-                </Button>
-                </DialogActions>
-            </Dialog>
-    );
diff --git a/src/views-components/user-dialog/setup-dialog.tsx b/src/views-components/user-dialog/setup-dialog.tsx
new file mode 100644 (file)
index 0000000..3a2fd35
--- /dev/null
@@ -0,0 +1,21 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch, compose } from 'redux';
+import { connect } from "react-redux";
+import { ConfirmationDialog } from "components/confirmation-dialog/confirmation-dialog";
+import { withDialog, WithDialogProps } from "store/dialog/with-dialog";
+import { setup, SETUP_DIALOG } from 'store/user-profile/user-profile-actions';
+
+const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps<any>) => ({
+    onConfirm: () => {
+        props.closeDialog();
+        dispatch<any>(setup(props.data.uuid));
+    }
+});
+
+export const SetupDialog = compose(
+    withDialog(SETUP_DIALOG),
+    connect(null, mapDispatchToProps)
+)(ConfirmationDialog);
index ce3f34c75348d39c4dbd1ee63df9b2e06a70d73c..9cee3cbc2fd2dc9d2a5f460b8244c17876aa7729 100644 (file)
@@ -7,7 +7,7 @@ import { connect } from 'react-redux';
 
 import { DataExplorer } from "views-components/data-explorer/data-explorer";
 import { DataColumns } from 'components/data-table/data-table';
-import { ResourceLinkHeadUuid, ResourceLinkTailUsername, ResourceLinkHeadPermissionLevel, ResourceLinkTailPermissionLevel, ResourceLinkHead, ResourceLinkTail, ResourceLinkDelete, ResourceLinkTailIsActive, ResourceLinkTailIsVisible } from 'views-components/data-explorer/renderers';
+import { ResourceLinkHeadUuid, ResourceLinkTailUsername, ResourceLinkHeadPermissionLevel, ResourceLinkTailPermissionLevel, ResourceLinkHead, ResourceLinkTail, ResourceLinkDelete, ResourceLinkTailAccountStatus, ResourceLinkTailIsVisible } from 'views-components/data-explorer/renderers';
 import { createTree } from 'models/tree';
 import { noop } from 'lodash/fp';
 import { RootState } from 'store/store';
@@ -31,7 +31,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 export enum GroupDetailsPanelMembersColumnNames {
     FULL_NAME = "Name",
     USERNAME = "Username",
-    ACTIVE = "User Active",
+    STATUS = "Account Status",
     VISIBLE = "Visible to other members",
     PERMISSION = "Permission",
     REMOVE = "Remove",
@@ -60,11 +60,11 @@ export const groupDetailsMembersPanelColumns: DataColumns<string> = [
         render: uuid => <ResourceLinkTailUsername uuid={uuid} />
     },
     {
-        name: GroupDetailsPanelMembersColumnNames.ACTIVE,
+        name: GroupDetailsPanelMembersColumnNames.STATUS,
         selected: true,
         configurable: true,
         filters: createTree(),
-        render: uuid => <ResourceLinkTailIsActive uuid={uuid} disabled={true} />
+        render: uuid => <ResourceLinkTailAccountStatus uuid={uuid} />
     },
     {
         name: GroupDetailsPanelMembersColumnNames.VISIBLE,
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 (file)
index 283b9ac..0000000
+++ /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 (file)
index 2421a28..0000000
+++ /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);
index 5fb979a2194c62af81a0de64b37cdc509096ead0..589353cd880ad91557f80969e6fe1233e818fc2f 100644 (file)
@@ -3,25 +3,23 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import React from 'react';
-import { WithStyles, withStyles, Tabs, Tab, Paper, Button, Grid } from '@material-ui/core';
+import { WithStyles, withStyles, Paper, Button, Grid } from '@material-ui/core';
 import { DataExplorer } from "views-components/data-explorer/data-explorer";
 import { connect, DispatchProp } from 'react-redux';
 import { DataColumns } from 'components/data-table/data-table';
 import { RootState } from 'store/store';
 import { SortDirection } from 'components/data-table/data-column';
-import { openContextMenu } from "store/context-menu/context-menu-actions";
+import { openUserContextMenu } from "store/context-menu/context-menu-actions";
 import { getResource, ResourcesState } from "store/resources/resources";
 import {
-    ResourceFirstName,
-    ResourceLastName,
+    UserResourceFullName,
     ResourceUuid,
     ResourceEmail,
-    ResourceIsActive,
     ResourceIsAdmin,
-    ResourceUsername
+    ResourceUsername,
+    UserResourceAccountStatus,
 } from "views-components/data-explorer/renderers";
-import { navigateTo } from "store/navigation/navigation-action";
-import { ContextMenuKind } from "views-components/context-menu/context-menu";
+import { navigateToUserProfile } from "store/navigation/navigation-action";
 import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
 import { createTree } from 'models/tree';
 import { compose, Dispatch } from 'redux';
@@ -30,7 +28,7 @@ import { ShareMeIcon, AddIcon } from 'components/icon/icon';
 import { USERS_PANEL_ID, openUserCreateDialog } from 'store/users/users-actions';
 import { noop } from 'lodash';
 
-type UserPanelRules = "button" | 'root' | 'content';
+type UserPanelRules = "button" | 'root';
 
 const styles = withStyles<UserPanelRules>(theme => ({
     button: {
@@ -42,18 +40,13 @@ const styles = withStyles<UserPanelRules>(theme => ({
     root: {
         width: '100%',
     },
-    content: {
-        // reserve space for the tab bar
-        height: `calc(100% - ${theme.spacing.unit * 7}px)`,
-    }
 }));
 
 export enum UserPanelColumnNames {
-    FIRST_NAME = "First Name",
-    LAST_NAME = "Last Name",
+    NAME = "Name",
     UUID = "Uuid",
     EMAIL = "Email",
-    ACTIVE = "Active",
+    STATUS = "Account Status",
     ADMIN = "Admin",
     REDIRECT_TO_USER = "Redirect to user",
     USERNAME = "Username"
@@ -61,20 +54,12 @@ export enum UserPanelColumnNames {
 
 export const userPanelColumns: DataColumns<string> = [
     {
-        name: UserPanelColumnNames.FIRST_NAME,
-        selected: true,
-        configurable: true,
-        sortDirection: SortDirection.NONE,
-        filters: createTree(),
-        render: uuid => <ResourceFirstName uuid={uuid} />
-    },
-    {
-        name: UserPanelColumnNames.LAST_NAME,
+        name: UserPanelColumnNames.NAME,
         selected: true,
         configurable: true,
         sortDirection: SortDirection.NONE,
         filters: createTree(),
-        render: uuid => <ResourceLastName uuid={uuid} />
+        render: uuid => <UserResourceFullName uuid={uuid} link={true} />
     },
     {
         name: UserPanelColumnNames.UUID,
@@ -93,18 +78,16 @@ export const userPanelColumns: DataColumns<string> = [
         render: uuid => <ResourceEmail uuid={uuid} />
     },
     {
-        name: UserPanelColumnNames.ACTIVE,
+        name: UserPanelColumnNames.STATUS,
         selected: true,
         configurable: true,
-        sortDirection: SortDirection.NONE,
         filters: createTree(),
-        render: uuid => <ResourceIsActive uuid={uuid} />
+        render: uuid => <UserResourceAccountStatus uuid={uuid} />
     },
     {
         name: UserPanelColumnNames.ADMIN,
         selected: true,
         configurable: false,
-        sortDirection: SortDirection.NONE,
         filters: createTree(),
         render: uuid => <ResourceIsAdmin uuid={uuid} />
     },
@@ -124,8 +107,8 @@ interface UserPanelDataProps {
 
 interface UserPanelActionProps {
     openUserCreateDialog: () => void;
-    handleRowDoubleClick: (uuid: string) => void;
-    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: any) => void;
+    handleRowClick: (uuid: string) => void;
+    handleContextMenu: (event, resource: UserResource) => void;
 }
 
 const mapStateToProps = (state: RootState) => {
@@ -136,8 +119,8 @@ const mapStateToProps = (state: RootState) => {
 
 const mapDispatchToProps = (dispatch: Dispatch) => ({
     openUserCreateDialog: () => dispatch<any>(openUserCreateDialog()),
-    handleRowDoubleClick: (uuid: string) => dispatch<any>(navigateTo(uuid)),
-    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: any) => dispatch<any>(openContextMenu(event, item))
+    handleRowClick: (uuid: string) => dispatch<any>(navigateToUserProfile(uuid)),
+    handleContextMenu: (event, resource: UserResource) => dispatch<any>(openUserContextMenu(event, resource)),
 });
 
 type UserPanelProps = UserPanelDataProps & UserPanelActionProps & DispatchProp & WithStyles<UserPanelRules>;
@@ -146,63 +129,38 @@ export const UserPanel = compose(
     styles,
     connect(mapStateToProps, mapDispatchToProps))(
         class extends React.Component<UserPanelProps> {
-            state = {
-                value: 0,
-            };
-
-            componentDidMount() {
-                this.setState({ value: 0 });
-            }
-
             render() {
-                const { value } = this.state;
                 return <Paper className={this.props.classes.root}>
-                    <Tabs value={value} onChange={this.handleChange} fullWidth>
-                        <Tab label="USERS" />
-                        <Tab label="ACTIVITY" disabled />
-                    </Tabs>
-                    {value === 0 &&
-                        <div className={this.props.classes.content}>
-                            <DataExplorer
-                                id={USERS_PANEL_ID}
-                                onRowClick={noop}
-                                onRowDoubleClick={noop}
-                                onContextMenu={this.handleContextMenu}
-                                contextMenuColumn={true}
-                                hideColumnSelector
-                                actions={
-                                    <Grid container justify='flex-end'>
-                                        <Button variant="contained" color="primary" onClick={this.props.openUserCreateDialog}>
-                                            <AddIcon /> NEW USER
-                                        </Button>
-                                    </Grid>
-                                }
-                                paperProps={{
-                                    elevation: 0,
-                                }}
-                                dataTableDefaultView={
-                                    <DataTableDefaultView
-                                        icon={ShareMeIcon}
-                                        messages={['Your user list is empty.']} />
-                                } />
-                        </div>}
+                    <DataExplorer
+                        id={USERS_PANEL_ID}
+                        onRowClick={noop}
+                        onRowDoubleClick={noop}
+                        onContextMenu={this.handleContextMenu}
+                        contextMenuColumn={true}
+                        hideColumnSelector
+                        actions={
+                            <Grid container justify='flex-end'>
+                                <Button variant="contained" color="primary" onClick={this.props.openUserCreateDialog}>
+                                    <AddIcon /> NEW USER
+                                </Button>
+                            </Grid>
+                        }
+                        paperProps={{
+                            elevation: 0,
+                        }}
+                        dataTableDefaultView={
+                            <DataTableDefaultView
+                                icon={ShareMeIcon}
+                                messages={['Your user list is empty.']} />
+                        } />
                 </Paper>;
             }
 
-            handleChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
-                this.setState({ value });
-            }
-
             handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
+                event.stopPropagation();
                 const resource = getResource<UserResource>(resourceUuid)(this.props.resources);
                 if (resource) {
-                    this.props.onContextMenu(event, {
-                        name: '',
-                        uuid: resource.uuid,
-                        ownerUuid: resource.ownerUuid,
-                        kind: resource.kind,
-                        menuKind: ContextMenuKind.USER
-                    });
+                    this.props.handleContextMenu(event, resource);
                 }
             }
         }
diff --git a/src/views/user-profile-panel/user-profile-panel-root.tsx b/src/views/user-profile-panel/user-profile-panel-root.tsx
new file mode 100644 (file)
index 0000000..1c8b1da
--- /dev/null
@@ -0,0 +1,353 @@
+// 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 { DispatchProp } from 'react-redux';
+import { UserResource } from 'models/user';
+import { TextField } from "components/text-field/text-field";
+import { DataExplorer } from "views-components/data-explorer/data-explorer";
+import { NativeSelectField } from "components/select-field/select-field";
+import {
+    StyleRulesCallback,
+    WithStyles,
+    withStyles,
+    CardContent,
+    Button,
+    Typography,
+    Grid,
+    InputLabel,
+    Tabs, Tab,
+    Paper,
+    Tooltip,
+    IconButton,
+} from '@material-ui/core';
+import { ArvadosTheme } from 'common/custom-theme';
+import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
+import { PROFILE_EMAIL_VALIDATION, PROFILE_URL_VALIDATION } from "validators/validators";
+import { USER_PROFILE_PANEL_ID } from 'store/user-profile/user-profile-actions';
+import { noop } from 'lodash';
+import { DetailsIcon, GroupsIcon, MoreOptionsIcon } from 'components/icon/icon';
+import { DataColumns } from 'components/data-table/data-table';
+import { ResourceLinkHeadUuid, ResourceLinkHeadPermissionLevel, ResourceLinkHead, ResourceLinkDelete, ResourceLinkTailIsVisible, UserResourceAccountStatus } from 'views-components/data-explorer/renderers';
+import { createTree } from 'models/tree';
+import { getResource, ResourcesState } from 'store/resources/resources';
+import { DefaultView } from 'components/default-view/default-view';
+import { CopyToClipboardSnackbar } from 'components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar';
+
+type CssRules = 'root' | 'emptyRoot' | 'gridItem' | 'label' | 'readOnlyValue' | 'title' | 'description' | 'actions' | 'content' | 'copyIcon';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    root: {
+        width: '100%',
+        overflow: 'auto'
+    },
+    emptyRoot: {
+        width: '100%',
+        overflow: 'auto',
+        padding: theme.spacing.unit * 4,
+    },
+    gridItem: {
+        height: 45,
+        marginBottom: 20
+    },
+    label: {
+        fontSize: '0.675rem',
+        color: theme.palette.grey['600']
+    },
+    readOnlyValue: {
+        fontSize: '0.875rem',
+    },
+    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)`,
+    },
+    copyIcon: {
+        marginLeft: theme.spacing.unit,
+        color: theme.palette.grey["500"],
+        cursor: 'pointer',
+        display: 'inline',
+        '& svg': {
+            fontSize: '1rem'
+        }
+    }
+});
+
+export interface UserProfilePanelRootActionProps {
+    handleContextMenu: (event, resource: UserResource) => void;
+}
+
+export interface UserProfilePanelRootDataProps {
+    isAdmin: boolean;
+    isSelf: boolean;
+    isPristine: boolean;
+    isValid: boolean;
+    isInaccessible: boolean;
+    userUuid: string;
+    resources: ResourcesState;
+    localCluster: string;
+}
+
+const RoleTypes = [
+    { key: '', value: '' },
+    { 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 & DispatchProp & WithStyles<CssRules>;
+
+export enum UserProfileGroupsColumnNames {
+    NAME = "Name",
+    PERMISSION = "Permission",
+    VISIBLE = "Visible to other members",
+    UUID = "UUID",
+    REMOVE = "Remove",
+}
+
+enum TABS {
+    PROFILE = "PROFILE",
+    GROUPS = "GROUPS",
+
+}
+
+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} />
+    },
+];
+
+const ReadOnlyField = withStyles(styles)(
+    (props: ({ label: string, input: {value: string} }) & WithStyles<CssRules> ) => (
+        <Grid item xs={12} data-cy="field">
+            <Typography className={props.classes.label}>
+                {props.label}
+            </Typography>
+            <Typography className={props.classes.readOnlyValue} data-cy="value">
+                {props.input.value}
+            </Typography>
+        </Grid>
+    )
+);
+
+export const UserProfilePanelRoot = withStyles(styles)(
+    class extends React.Component<UserProfilePanelRootProps> {
+        state = {
+            value: TABS.PROFILE,
+        };
+
+        componentDidMount() {
+            this.setState({ value: TABS.PROFILE});
+        }
+
+        render() {
+            if (this.props.isInaccessible) {
+                return (
+                    <Paper className={this.props.classes.emptyRoot}>
+                        <CardContent>
+                            <DefaultView icon={DetailsIcon} messages={['This user does not exist or your account does not have permission to view it']} />
+                        </CardContent>
+                    </Paper>
+                );
+            } else {
+                return <Paper className={this.props.classes.root}>
+                    <Tabs value={this.state.value} onChange={this.handleChange} variant={"fullWidth"}>
+                        <Tab label={TABS.PROFILE} value={TABS.PROFILE} />
+                        <Tab label={TABS.GROUPS} value={TABS.GROUPS} />
+                    </Tabs>
+                    {this.state.value === TABS.PROFILE &&
+                        <CardContent>
+                            <Grid container justify="space-between">
+                                <Grid item>
+                                    <Typography className={this.props.classes.title}>
+                                        {this.props.userUuid}
+                                        <CopyToClipboardSnackbar value={this.props.userUuid} />
+                                    </Typography>
+                                </Grid>
+                                <Grid item>
+                                    <Grid container alignItems="center">
+                                        <Grid item style={{marginRight: '10px'}}><UserResourceAccountStatus uuid={this.props.userUuid} /></Grid>
+                                        <Grid item>
+                                            <Tooltip title="Actions" disableFocusListener>
+                                                <IconButton
+                                                    data-cy='user-profile-panel-options-btn'
+                                                    aria-label="Actions"
+                                                    onClick={(event) => this.handleContextMenu(event, this.props.userUuid)}>
+                                                    <MoreOptionsIcon />
+                                                </IconButton>
+                                            </Tooltip>
+                                        </Grid>
+                                    </Grid>
+                                </Grid>
+                            </Grid>
+                            <form onSubmit={this.props.handleSubmit} data-cy="profile-form">
+                                <Grid container spacing={24}>
+                                    <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="firstName">
+                                        <Field
+                                            label="First name"
+                                            name="firstName"
+                                            component={TextField as any}
+                                            disabled={!this.props.isAdmin && !this.props.isSelf}
+                                        />
+                                    </Grid>
+                                    <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="lastName">
+                                        <Field
+                                            label="Last name"
+                                            name="lastName"
+                                            component={TextField as any}
+                                            disabled={!this.props.isAdmin && !this.props.isSelf}
+                                        />
+                                    </Grid>
+                                    <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="email">
+                                        <Field
+                                            label="E-mail"
+                                            name="email"
+                                            component={ReadOnlyField as any}
+                                            disabled
+                                        />
+                                    </Grid>
+                                    <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="username">
+                                        <Field
+                                            label="Username"
+                                            name="username"
+                                            component={ReadOnlyField as any}
+                                            disabled
+                                        />
+                                    </Grid>
+                                    <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+                                        <Field
+                                            label="Organization"
+                                            name="prefs.profile.organization"
+                                            component={TextField as any}
+                                            disabled={!this.props.isAdmin && !this.props.isSelf}
+                                        />
+                                    </Grid>
+                                    <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+                                        <Field
+                                            label="E-mail at Organization"
+                                            name="prefs.profile.organization_email"
+                                            component={TextField as any}
+                                            disabled={!this.props.isAdmin && !this.props.isSelf}
+                                            validate={PROFILE_EMAIL_VALIDATION}
+                                        />
+                                    </Grid>
+                                    <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+                                        <InputLabel className={this.props.classes.label} htmlFor="prefs.profile.role">Role</InputLabel>
+                                        <Field
+                                            id="prefs.profile.role"
+                                            name="prefs.profile.role"
+                                            component={NativeSelectField as any}
+                                            items={RoleTypes}
+                                            disabled={!this.props.isAdmin && !this.props.isSelf}
+                                        />
+                                    </Grid>
+                                    <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+                                        <Field
+                                            label="Website"
+                                            name="prefs.profile.website_url"
+                                            component={TextField as any}
+                                            disabled={!this.props.isAdmin && !this.props.isSelf}
+                                            validate={PROFILE_URL_VALIDATION}
+                                        />
+                                    </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>
+                    }
+                    {this.state.value === TABS.GROUPS &&
+                        <div className={this.props.classes.content}>
+                            <DataExplorer
+                                    id={USER_PROFILE_PANEL_ID}
+                                    data-cy="user-profile-groups-data-explorer"
+                                    onRowClick={noop}
+                                    onRowDoubleClick={noop}
+                                    onContextMenu={noop}
+                                    contextMenuColumn={false}
+                                    hideColumnSelector
+                                    hideSearchInput
+                                    paperProps={{
+                                        elevation: 0,
+                                    }}
+                                    dataTableDefaultView={
+                                        <DataTableDefaultView
+                                            icon={GroupsIcon}
+                                            messages={['Group list is empty.']} />
+                                    } />
+                        </div>}
+                </Paper >;
+            }
+        }
+
+        handleChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
+            this.setState({ value });
+        }
+
+        handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
+            event.stopPropagation();
+            const resource = getResource<UserResource>(resourceUuid)(this.props.resources);
+            if (resource) {
+                this.props.handleContextMenu(event, resource);
+            }
+        }
+
+    }
+);
diff --git a/src/views/user-profile-panel/user-profile-panel.tsx b/src/views/user-profile-panel/user-profile-panel.tsx
new file mode 100644 (file)
index 0000000..a90d44a
--- /dev/null
@@ -0,0 +1,43 @@
+// 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 { UserResource } from 'models/user';
+import { getUserProfileIsInaccessible, saveEditedUser } from 'store/user-profile/user-profile-actions';
+import { UserProfilePanelRoot, UserProfilePanelRootDataProps } from 'views/user-profile-panel/user-profile-panel-root';
+import { USER_PROFILE_FORM } from "store/user-profile/user-profile-actions";
+import { matchUserProfileRoute } from 'routes/routes';
+import { openUserContextMenu } from 'store/context-menu/context-menu-actions';
+
+const mapStateToProps = (state: RootState): UserProfilePanelRootDataProps => {
+  const pathname = state.router.location ? state.router.location.pathname : '';
+  const match = matchUserProfileRoute(pathname);
+  const uuid = match ? match.params.id : state.auth.user?.uuid || '';
+
+  return {
+    isAdmin: state.auth.user!.isAdmin,
+    isSelf: state.auth.user!.uuid === uuid,
+    isPristine: isPristine(USER_PROFILE_FORM)(state),
+    isValid: isValid(USER_PROFILE_FORM)(state),
+    isInaccessible: getUserProfileIsInaccessible(state.properties) || false,
+    localCluster: state.auth.localCluster,
+    userUuid: uuid,
+    resources: state.resources,
+}};
+
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+    handleContextMenu: (event, resource: UserResource) => dispatch<any>(openUserContextMenu(event, resource)),
+});
+
+export const UserProfilePanel = compose(
+    connect(mapStateToProps, mapDispatchToProps),
+    reduxForm({
+        form: USER_PROFILE_FORM,
+        onSubmit: (data, dispatch) => {
+            dispatch(saveEditedUser(data));
+        }
+    }))(UserProfilePanelRoot);
index fe97bd3b449ad4be36f5f099dc6a8f18fd333407..28fae4cd6b1b5a4b7a668d798c55b3271ad4c9f5 100644 (file)
@@ -46,7 +46,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';
@@ -79,8 +79,9 @@ import { UserPanel } from 'views/user-panel/user-panel';
 import { UserAttributesDialog } from 'views-components/user-dialog/attributes-dialog';
 import { CreateUserDialog } from 'views-components/dialog-forms/create-user-dialog';
 import { HelpApiClientAuthorizationDialog } from 'views-components/api-client-authorizations-dialog/help-dialog';
-import { UserManageDialog } from 'views-components/user-dialog/manage-dialog';
-import { SetupShellAccountDialog } from 'views-components/dialog-forms/setup-shell-account-dialog';
+import { DeactivateDialog } from 'views-components/user-dialog/deactivate-dialog';
+import { ActivateDialog } from 'views-components/user-dialog/activate-dialog';
+import { SetupDialog } from 'views-components/user-dialog/setup-dialog';
 import { GroupsPanel } from 'views/groups-panel/groups-panel';
 import { RemoveGroupDialog } from 'views-components/groups-dialog/remove-dialog';
 import { GroupAttributesDialog } from 'views-components/groups-dialog/attributes-dialog';
@@ -170,7 +171,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} />
@@ -257,7 +259,6 @@ export const WorkbenchPanel =
             <RepositoryAttributesDialog />
             <RepositoriesSampleGitDialog />
             <RichTextEditorDialog />
-            <SetupShellAccountDialog />
             <SharingDialog />
             <NotFoundDialog />
             <Snackbar />
@@ -265,7 +266,9 @@ export const WorkbenchPanel =
             <UpdateProcessDialog />
             <UpdateProjectDialog />
             <UserAttributesDialog />
-            <UserManageDialog />
+            <DeactivateDialog />
+            <ActivateDialog />
+            <SetupDialog />
             <VirtualMachineAttributesDialog />
             <FedLogin />
             <WebDavS3InfoDialog />