[ARVADOS-WORKBENCH2] updated: 2.3.0-185-g46b878b9

Git user git at public.arvados.org
Tue Mar 15 18:02:40 UTC 2022


Summary of changes:
 cypress/integration/user-profile.spec.js           | 294 +++++++++++++++++++++
 cypress/integration/virtual-machine-admin.spec.js  |  14 +-
 ...details-panel-permissions-middleware-service.ts |   8 +-
 src/store/my-account/my-account-panel-actions.ts   |  30 ---
 src/store/user-profile/user-profile-actions.ts     |   4 +-
 .../user-profile-groups-middleware-service.ts      |  28 +-
 src/store/users/users-actions.ts                   |  29 +-
 .../dialog-create/dialog-user-create.tsx           |  12 +-
 .../dialog-forms/setup-shell-account-dialog.tsx    |  36 +--
 .../form-fields/user-form-fields.tsx               |  41 ++-
 .../user-profile-panel/user-profile-panel-root.tsx |  69 +++--
 11 files changed, 447 insertions(+), 118 deletions(-)
 create mode 100644 cypress/integration/user-profile.spec.js
 delete mode 100644 src/store/my-account/my-account-panel-actions.ts

       via  46b878b9773789f7a953f58f3de2cc4bf370e153 (commit)
       via  96a705a50db3d742fd897c12a236b8a44bbcaffb (commit)
       via  ab13bb0f02c562635b95b3bdf908bd9941d149e4 (commit)
       via  36b2ed3407e92cb7788a87eb52badd18eb7b2b18 (commit)
       via  695ee8df0aeadd5eb788ca29da6450813880da76 (commit)
       via  514574540614593941c39592d9f9972f770e0cb7 (commit)
       via  71a95158c62c2dd106e1874c0b811c56b35cf9cc (commit)
       via  888c698aa77e4cfdeb346bed8995ba0ec2c17a08 (commit)
       via  aa14e6d194dde8f2ce473c0ed3b5d0b63af19c5c (commit)
      from  80852d13ef70331d0b5dcb7c0741956967129728 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.


commit 46b878b9773789f7a953f58f3de2cc4bf370e153
Author: Stephen Smith <stephen at curii.com>
Date:   Tue Mar 15 14:02:17 2022 -0400

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

diff --git a/cypress/integration/user-profile.spec.js b/cypress/integration/user-profile.spec.js
new file mode 100644
index 00000000..325f38c7
--- /dev/null
+++ b/cypress/integration/user-profile.spec.js
@@ -0,0 +1,294 @@
+// 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] [data-cy=firstName] [data-cy=value]').contains(firstName);
+        cy.get('[data-cy=profile-form] [data-cy=lastName] [data-cy=value]').contains(lastName);
+        cy.get('[data-cy=profile-form] [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);
+        }
+    }
+
+    beforeEach(function() {
+        cy.loginAs(adminUser);
+        cy.goToPath('/my-account');
+        enterProfileValues({
+            org: '',
+            org_email: '',
+            role: '',
+            website: '',
+        });
+        cy.get('[data-cy=profile-form] button[type="submit"]').click({force: true});
+
+        cy.goToPath('/user/' + activeUser.user.uuid);
+        enterProfileValues({
+            org: '',
+            org_email: '',
+            role: '',
+            website: '',
+        });
+        cy.get('[data-cy=profile-form] button[type="submit"]').click({force: true});
+    });
+
+    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 tab should be hidden
+        cy.get('div [role="tab"]').should('not.contain', 'ADMIN');
+
+        // Check initial values
+        assertProfileValues({
+            firstName: 'Active',
+            lastName: 'User',
+            email: 'user at example.local',
+            username: 'user',
+            org: '',
+            org_email: '',
+            role: '',
+            website: '',
+        });
+
+        // Change values
+        enterProfileValues({
+            org: 'Org name',
+            org_email: 'email at 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 at example.local',
+            username: 'user',
+            org: 'Org name',
+            org_email: 'email at 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 at 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 tab should be hidden
+        cy.get('div [role="tab"]').should('not.contain', 'ADMIN');
+    });
+
+    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 tab should be visible
+        cy.get('div [role="tab"]').should('contain', 'ADMIN');
+
+        // Check initial values
+        assertProfileValues({
+            firstName: 'Admin',
+            lastName: 'User',
+            email: 'admin at example.local',
+            username: 'admin',
+            org: '',
+            org_email: '',
+            role: '',
+            website: '',
+        });
+
+        // Change values
+        enterProfileValues({
+            org: 'Admin org name',
+            org_email: 'admin at 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 at example.local',
+            username: 'admin',
+            org: 'Admin org name',
+            org_email: 'admin at 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 at example.local',
+            username: 'user',
+            org: '',
+            org_email: '',
+            role: '',
+            website: '',
+        });
+
+        enterProfileValues({
+            org: 'Changed org name',
+            org_email: 'changed at 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 at example.local',
+            username: 'user',
+            org: 'Changed org name',
+            org_email: 'changed at example.com',
+            role: 'Researcher',
+            website: 'changed.local',
+        });
+    });
+
+    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);
+    });
+
+});
diff --git a/src/views/user-profile-panel/user-profile-panel-root.tsx b/src/views/user-profile-panel/user-profile-panel-root.tsx
index d42fb6e1..82c20728 100644
--- a/src/views/user-profile-panel/user-profile-panel-root.tsx
+++ b/src/views/user-profile-panel/user-profile-panel-root.tsx
@@ -84,6 +84,7 @@ export interface UserProfilePanelRootDataProps {
 }
 
 const RoleTypes = [
+    { key: '', value: '' },
     { key: 'Bio-informatician', value: 'Bio-informatician' },
     { key: 'Data Scientist', value: 'Data Scientist' },
     { key: 'Analyst', value: 'Analyst' },
@@ -150,11 +151,11 @@ export const userProfileGroupsColumns: DataColumns<string> = [
 
 const ReadOnlyField = withStyles(styles)(
     (props: ({ label: string, input: {value: string} }) & WithStyles<CssRules> ) => (
-        <Grid item xs={12}>
+        <Grid item xs={12} data-cy="field">
             <Typography className={props.classes.label}>
                 {props.label}
             </Typography>
-            <Typography className={props.classes.readOnlyValue}>
+            <Typography className={props.classes.readOnlyValue} data-cy="value">
                 {props.input.value}
             </Typography>
         </Grid>
@@ -180,9 +181,9 @@ export const UserProfilePanelRoot = withStyles(styles)(
                 </Tabs>
                 {this.state.value === TABS.PROFILE &&
                     <CardContent>
-                        <form onSubmit={this.props.handleSubmit}>
+                        <form onSubmit={this.props.handleSubmit} data-cy="profile-form">
                             <Grid container spacing={24}>
-                                <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+                                <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="firstName">
                                     <Field
                                         label="First name"
                                         name="firstName"
@@ -190,7 +191,7 @@ export const UserProfilePanelRoot = withStyles(styles)(
                                         disabled
                                     />
                                 </Grid>
-                                <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+                                <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="lastName">
                                     <Field
                                         label="Last name"
                                         name="lastName"
@@ -198,7 +199,7 @@ export const UserProfilePanelRoot = withStyles(styles)(
                                         disabled
                                     />
                                 </Grid>
-                                <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+                                <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="email">
                                     <Field
                                         label="E-mail"
                                         name="email"
@@ -206,7 +207,7 @@ export const UserProfilePanelRoot = withStyles(styles)(
                                         disabled
                                     />
                                 </Grid>
-                                <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+                                <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="username">
                                     <Field
                                         label="Username"
                                         name="username"
@@ -219,7 +220,6 @@ export const UserProfilePanelRoot = withStyles(styles)(
                                         label="Organization"
                                         name="prefs.profile.organization"
                                         component={TextField as any}
-                                        validate={MY_ACCOUNT_VALIDATION}
                                         disabled={!this.props.isAdmin && !this.props.isSelf}
                                     />
                                 </Grid>
@@ -228,7 +228,6 @@ export const UserProfilePanelRoot = withStyles(styles)(
                                         label="E-mail at Organization"
                                         name="prefs.profile.organization_email"
                                         component={TextField as any}
-                                        validate={MY_ACCOUNT_VALIDATION}
                                         disabled={!this.props.isAdmin && !this.props.isSelf}
                                     />
                                 </Grid>
@@ -270,8 +269,10 @@ export const UserProfilePanelRoot = withStyles(styles)(
                     <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

commit 96a705a50db3d742fd897c12a236b8a44bbcaffb
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Mar 14 17:20:25 2022 -0400

    18559: Update the new user dialog and fix groups functionality
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/store/users/users-actions.ts b/src/store/users/users-actions.ts
index fded1140..425a2d56 100644
--- a/src/store/users/users-actions.ts
+++ b/src/store/users/users-actions.ts
@@ -15,7 +15,7 @@ import { getResource } from 'store/resources/resources';
 import { navigateTo, navigateToUsers, navigateToRootProject } from "store/navigation/navigation-action";
 import { authActions } from 'store/auth/auth-action';
 import { getTokenV2 } from "models/api-client-authorization";
-import { AddLoginFormData, VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD, VIRTUAL_MACHINE_ADD_LOGIN_USER_FIELD } from "store/virtual-machines/virtual-machines-actions";
+import { AddLoginFormData, VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD, VIRTUAL_MACHINE_ADD_LOGIN_USER_FIELD, VIRTUAL_MACHINE_ADD_LOGIN_VM_FIELD } from "store/virtual-machines/virtual-machines-actions";
 import { PermissionLevel } from "models/permission";
 import { updateResources } from "store/resources/resources-actions";
 
@@ -27,8 +27,8 @@ 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 const userBindedActions = bindDataExplorerActions(USERS_PANEL_ID);
@@ -83,11 +83,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 }));
@@ -96,6 +113,8 @@ export const createUser = (user: UserCreateFormDialogData) =>
             return newUser;
         } catch (e) {
             return;
+        } finally {
+            dispatch(stopSubmit(USER_CREATE_FORM_NAME));
         }
     };
 
diff --git a/src/views-components/dialog-create/dialog-user-create.tsx b/src/views-components/dialog-create/dialog-user-create.tsx
index d8d25da4..6be7b28f 100644
--- a/src/views-components/dialog-create/dialog-user-create.tsx
+++ b/src/views-components/dialog-create/dialog-user-create.tsx
@@ -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
index 666ea38e..04eae126 100644
--- a/src/views-components/dialog-forms/setup-shell-account-dialog.tsx
+++ b/src/views-components/dialog-forms/setup-shell-account-dialog.tsx
@@ -8,13 +8,11 @@ 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 { CHOOSE_VM_VALIDATION } from 'validators/validators';
 import { InputLabel } from '@material-ui/core';
-import { NativeSelectField } from 'components/select-field/select-field';
 import { SETUP_SHELL_ACCOUNT_DIALOG, setupUserVM } from 'store/users/users-actions';
 import { UserResource } from 'models/user';
-import { VIRTUAL_MACHINE_ADD_LOGIN_USER_FIELD, VIRTUAL_MACHINE_ADD_LOGIN_VM_FIELD, VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD, AddLoginFormData } from 'store/virtual-machines/virtual-machines-actions';
-import { GroupArrayInput } from 'views-components/virtual-machines-dialog/group-array-input';
+import { VIRTUAL_MACHINE_ADD_LOGIN_USER_FIELD, AddLoginFormData } from 'store/virtual-machines/virtual-machines-actions';
+import { UserGroupsVirtualMachineField, RequiredUserVirtualMachineField } from 'views-components/form-fields/user-form-fields';
 
 export const SetupShellAccountDialog = compose(
     withDialog(SETUP_SHELL_ACCOUNT_DIALOG),
@@ -34,11 +32,6 @@ export const SetupShellAccountDialog = compose(
         />
 );
 
-interface VirtualMachinesProps {
-    data: {
-        items: VirtualMachinesResource[];
-    };
-}
 interface DataProps {
     user: UserResource;
     items: VirtualMachinesResource[];
@@ -50,33 +43,14 @@ const UserNameField = () =>
         <Field
             name={`${VIRTUAL_MACHINE_ADD_LOGIN_USER_FIELD}.username`}
             component={TextField as any}
-            disabled /></span>;
-
-const UserVirtualMachineField = ({ 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>;
-
-const UserGroupsVirtualMachineField = () =>
-    <GroupArrayInput
-        name={VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD}
-        input={{id:"Add groups to VM login (eg: docker, sudo)", disabled:false}}
-        required={false}
-    />
-
-const getVirtualMachinesList = (virtualMachines: VirtualMachinesResource[]) =>
-    [{ key: "", value: "" }].concat(virtualMachines.map(it => ({ key: it.uuid, value: it.hostname })));
+            disabled />
+    </span>;
 
 type SetupShellAccountDialogComponentProps = WithDialogProps<{}> & InjectedFormProps<AddLoginFormData>;
 
 const SetupShellAccountFormFields = (props: SetupShellAccountDialogComponentProps) =>
     <>
         <UserNameField />
-        <UserVirtualMachineField data={props.data as DataProps} />
+        <RequiredUserVirtualMachineField data={props.data as DataProps} />
         <UserGroupsVirtualMachineField />
     </>;
diff --git a/src/views-components/form-fields/user-form-fields.tsx b/src/views-components/form-fields/user-form-fields.tsx
index 393f29d3..12fc91e2 100644
--- a/src/views-components/form-fields/user-form-fields.tsx
+++ b/src/views-components/form-fields/user-form-fields.tsx
@@ -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 })));

commit ab13bb0f02c562635b95b3bdf908bd9941d149e4
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Mar 14 17:16:34 2022 -0400

    18559: Remove warning snackbar that gets triggered from other pages
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/store/group-details-panel/group-details-panel-permissions-middleware-service.ts b/src/store/group-details-panel/group-details-panel-permissions-middleware-service.ts
index 9e41409d..85beecd7 100644
--- a/src/store/group-details-panel/group-details-panel-permissions-middleware-service.ts
+++ b/src/store/group-details-panel/group-details-panel-permissions-middleware-service.ts
@@ -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.',

commit 36b2ed3407e92cb7788a87eb52badd18eb7b2b18
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Mar 14 17:13:34 2022 -0400

    18559: Hide profile admin tab to non-admins
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views/user-profile-panel/user-profile-panel-root.tsx b/src/views/user-profile-panel/user-profile-panel-root.tsx
index 2176b323..d42fb6e1 100644
--- a/src/views/user-profile-panel/user-profile-panel-root.tsx
+++ b/src/views/user-profile-panel/user-profile-panel-root.tsx
@@ -103,6 +103,13 @@ export enum UserProfileGroupsColumnNames {
     REMOVE = "Remove",
 }
 
+enum TABS {
+    PROFILE = "PROFILE",
+    GROUPS = "GROUPS",
+    ADMIN = "ADMIN",
+
+}
+
 export const userProfileGroupsColumns: DataColumns<string> = [
     {
         name: UserProfileGroupsColumnNames.NAME,
@@ -157,21 +164,21 @@ const ReadOnlyField = withStyles(styles)(
 export const UserProfilePanelRoot = withStyles(styles)(
     class extends React.Component<UserProfilePanelRootProps> {
         state = {
-            value: 0,
+            value: TABS.PROFILE,
         };
 
         componentDidMount() {
-            this.setState({ value: 0 });
+            this.setState({ value: TABS.PROFILE});
         }
 
         render() {
             return <Paper className={this.props.classes.root}>
                 <Tabs value={this.state.value} onChange={this.handleChange} variant={"fullWidth"}>
-                    <Tab label="PROFILE" />
-                    <Tab label="GROUPS" />
-                    <Tab label="ADMIN" disabled={!this.props.isAdmin} />
+                    <Tab label={TABS.PROFILE} value={TABS.PROFILE} />
+                    <Tab label={TABS.GROUPS} value={TABS.GROUPS} />
+                    {this.props.isAdmin && <Tab label={TABS.ADMIN} value={TABS.ADMIN} />}
                 </Tabs>
-                {this.state.value === 0 &&
+                {this.state.value === TABS.PROFILE &&
                     <CardContent>
                         <form onSubmit={this.props.handleSubmit}>
                             <Grid container spacing={24}>
@@ -259,7 +266,7 @@ export const UserProfilePanelRoot = withStyles(styles)(
                         </form >
                     </CardContent>
                 }
-                {this.state.value === 1 &&
+                {this.state.value === TABS.GROUPS &&
                     <div className={this.props.classes.content}>
                         <DataExplorer
                                 id={USER_PROFILE_PANEL_ID}
@@ -277,7 +284,7 @@ export const UserProfilePanelRoot = withStyles(styles)(
                                         messages={['Group list is empty.']} />
                                 } />
                     </div>}
-                {this.state.value === 2 &&
+                {this.props.isAdmin && this.state.value === TABS.ADMIN &&
                     <Paper elevation={0} className={this.props.classes.adminRoot}>
                         <Card elevation={0}>
                             <CardContent>

commit 695ee8df0aeadd5eb788ca29da6450813880da76
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Mar 14 17:11:32 2022 -0400

    18559: Filter to only role groups in profile groups list
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/store/user-profile/user-profile-groups-middleware-service.ts b/src/store/user-profile/user-profile-groups-middleware-service.ts
index 47c63901..a8a650a2 100644
--- a/src/store/user-profile/user-profile-groups-middleware-service.ts
+++ b/src/store/user-profile/user-profile-groups-middleware-service.ts
@@ -12,6 +12,8 @@ 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) {
@@ -36,26 +38,44 @@ export class UserProfileGroupsMiddlewareService extends DataExplorerMiddlewareSe
                     .addEqual('head_kind', ResourceKind.GROUP)
                     .getFilters()
             });
+            // Update resources, includes "project" groups
             api.dispatch(updateResources(groupMembershipLinks.items));
 
-            // Get user's groups details
+            // 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(groupMembershipLinks),
-                items: groupMembershipLinks.items.map(item => item.uuid),
+                ...listResultsToDataExplorerItemsMeta(roleGroupMembershipLinks),
+                items: roleGroupMembershipLinks.items.map(item => item.uuid),
             }));
         } catch {
-            // api.dispatch(couldNotFetchUsers());
+            api.dispatch(couldNotFetchGroups());
         } finally {
             api.dispatch(progressIndicatorActions.STOP_WORKING(this.getId()));
         }
     }
 }
+
+const couldNotFetchGroups = () =>
+    snackbarActions.OPEN_SNACKBAR({
+        message: 'Could not fetch groups.',
+        kind: SnackbarKind.ERROR
+    });

commit 514574540614593941c39592d9f9972f770e0cb7
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Mar 14 17:06:11 2022 -0400

    18559: Show read-only profile values without input fields
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views/user-profile-panel/user-profile-panel-root.tsx b/src/views/user-profile-panel/user-profile-panel-root.tsx
index 76cad8a5..2176b323 100644
--- a/src/views/user-profile-panel/user-profile-panel-root.tsx
+++ b/src/views/user-profile-panel/user-profile-panel-root.tsx
@@ -31,7 +31,7 @@ import { DataColumns } from 'components/data-table/data-table';
 import { ResourceLinkHeadUuid, ResourceLinkHeadPermissionLevel, ResourceLinkHead, ResourceLinkDelete, ResourceLinkTailIsVisible } from 'views-components/data-explorer/renderers';
 import { createTree } from 'models/tree';
 
-type CssRules = 'root' | 'adminRoot' | 'gridItem' | 'label' | 'title' | 'description' | 'actions' | 'content';
+type CssRules = 'root' | 'adminRoot' | 'gridItem' | 'label' | 'readOnlyValue' | 'title' | 'description' | 'actions' | 'content';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
@@ -46,7 +46,11 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         marginBottom: 20
     },
     label: {
-        fontSize: '0.675rem'
+        fontSize: '0.675rem',
+        color: theme.palette.grey['600']
+    },
+    readOnlyValue: {
+        fontSize: '0.875rem',
     },
     title: {
         fontSize: '1.1rem',
@@ -137,6 +141,19 @@ export const userProfileGroupsColumns: DataColumns<string> = [
     },
 ];
 
+const ReadOnlyField = withStyles(styles)(
+    (props: ({ label: string, input: {value: string} }) & WithStyles<CssRules> ) => (
+        <Grid item xs={12}>
+            <Typography className={props.classes.label}>
+                {props.label}
+            </Typography>
+            <Typography className={props.classes.readOnlyValue}>
+                {props.input.value}
+            </Typography>
+        </Grid>
+    )
+);
+
 export const UserProfilePanelRoot = withStyles(styles)(
     class extends React.Component<UserProfilePanelRootProps> {
         state = {
@@ -162,7 +179,7 @@ export const UserProfilePanelRoot = withStyles(styles)(
                                     <Field
                                         label="First name"
                                         name="firstName"
-                                        component={TextField as any}
+                                        component={ReadOnlyField as any}
                                         disabled
                                     />
                                 </Grid>
@@ -170,7 +187,7 @@ export const UserProfilePanelRoot = withStyles(styles)(
                                     <Field
                                         label="Last name"
                                         name="lastName"
-                                        component={TextField as any}
+                                        component={ReadOnlyField as any}
                                         disabled
                                     />
                                 </Grid>
@@ -178,7 +195,7 @@ export const UserProfilePanelRoot = withStyles(styles)(
                                     <Field
                                         label="E-mail"
                                         name="email"
-                                        component={TextField as any}
+                                        component={ReadOnlyField as any}
                                         disabled
                                     />
                                 </Grid>
@@ -186,7 +203,7 @@ export const UserProfilePanelRoot = withStyles(styles)(
                                     <Field
                                         label="Username"
                                         name="username"
-                                        component={TextField as any}
+                                        component={ReadOnlyField as any}
                                         disabled
                                     />
                                 </Grid>
@@ -196,7 +213,6 @@ export const UserProfilePanelRoot = withStyles(styles)(
                                         name="prefs.profile.organization"
                                         component={TextField as any}
                                         validate={MY_ACCOUNT_VALIDATION}
-                                        required
                                         disabled={!this.props.isAdmin && !this.props.isSelf}
                                     />
                                 </Grid>
@@ -206,7 +222,6 @@ export const UserProfilePanelRoot = withStyles(styles)(
                                         name="prefs.profile.organization_email"
                                         component={TextField as any}
                                         validate={MY_ACCOUNT_VALIDATION}
-                                        required
                                         disabled={!this.props.isAdmin && !this.props.isSelf}
                                     />
                                 </Grid>

commit 71a95158c62c2dd106e1874c0b811c56b35cf9cc
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Mar 14 17:03:10 2022 -0400

    18559: Fix up to date profile data not showing after save
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/store/user-profile/user-profile-actions.ts b/src/store/user-profile/user-profile-actions.ts
index 82e90a2b..0c6341f2 100644
--- a/src/store/user-profile/user-profile-actions.ts
+++ b/src/store/user-profile/user-profile-actions.ts
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 import { RootState } from "store/store";
 import { Dispatch } from 'redux';
-import { initialize, reset } from "redux-form";
+import { initialize } from "redux-form";
 import { ServiceRepository } from "services/services";
 import { bindDataExplorerActions } from "store/data-explorer/data-explorer-action";
 import { propertiesActions } from 'store/properties/properties-actions';
@@ -38,7 +38,7 @@ export const saveEditedUser = (resource: any) =>
       try {
           const user = await services.userService.update(resource.uuid, resource);
           dispatch(updateResources([user]));
-          dispatch(reset(USER_PROFILE_FORM));
+          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({

commit 888c698aa77e4cfdeb346bed8995ba0ec2c17a08
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Mar 14 17:00:28 2022 -0400

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

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
index 9e974aa9..00000000
--- a/src/store/my-account/my-account-panel-actions.ts
+++ /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;
-        }
-    };

commit aa14e6d194dde8f2ce473c0ed3b5d0b63af19c5c
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Mar 14 16:28:44 2022 -0400

    18559: Improve reliability of vm admin tests
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/cypress/integration/virtual-machine-admin.spec.js b/cypress/integration/virtual-machine-admin.spec.js
index 73804b20..f01a8911 100644
--- a/cypress/integration/virtual-machine-admin.spec.js
+++ b/cypress/integration/virtual-machine-admin.spec.js
@@ -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

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


hooks/post-receive
-- 




More information about the arvados-commits mailing list