[ARVADOS-WORKBENCH2] updated: 1.4.1-330-g1866fb49

Git user git at public.arvados.org
Mon Apr 27 23:22:50 UTC 2020


Summary of changes:
 .gitignore                                         |   2 +
 Dockerfile                                         |  10 -
 Makefile                                           |  15 +-
 README.md                                          |  19 +-
 cypress.json                                       |   4 +
 cypress/fixtures/.gitkeep                          |   0
 cypress/integration/login.spec.js                  |  81 +++
 cypress/plugins/index.js                           |  25 +
 cypress/support/commands.js                        |  92 +++
 cypress/support/index.js                           |  24 +
 docker/Dockerfile                                  |  17 +
 package.json                                       |   8 +-
 src/models/user.test.ts                            | 124 ++++
 src/models/user.ts                                 |  14 +-
 src/models/vocabulary.test.ts                      |   1 -
 src/services/auth-service/auth-service.ts          |   4 +-
 src/store/auth/auth-action-session.ts              |   6 +-
 src/store/auth/auth-action.ts                      |   2 +
 ...llections-content-address-middleware-service.ts |   7 +-
 .../group-details-panel-actions.ts                 |   8 +-
 src/store/groups-panel/groups-panel-actions.ts     |   4 +-
 .../advanced-tab-dialog/metadataTab.tsx            |   4 +-
 .../dialog-forms/add-group-member-dialog.tsx       |   6 +-
 .../dialog-forms/create-group-dialog.tsx           |   6 +-
 src/views-components/main-app-bar/account-menu.tsx |   4 +-
 .../{people-select.tsx => participant-select.tsx}  |  81 ++-
 .../sharing-invitation-form-component.tsx          |   6 +-
 src/views-components/user-dialog/manage-dialog.tsx |  11 +-
 src/views/main-panel/main-panel-root.tsx           |  10 +-
 tools/arvados_config.yml                           |  14 +
 tools/run-integration-tests.sh                     | 131 ++++
 tsconfig.json                                      |   1 +
 yarn.lock                                          | 720 +++++++++++++++++++--
 33 files changed, 1314 insertions(+), 147 deletions(-)
 delete mode 100644 Dockerfile
 create mode 100644 cypress.json
 create mode 100644 cypress/fixtures/.gitkeep
 create mode 100644 cypress/integration/login.spec.js
 create mode 100644 cypress/plugins/index.js
 create mode 100644 cypress/support/commands.js
 create mode 100644 cypress/support/index.js
 create mode 100644 docker/Dockerfile
 create mode 100644 src/models/user.test.ts
 rename src/views-components/sharing-dialog/{people-select.tsx => participant-select.tsx} (60%)
 create mode 100644 tools/arvados_config.yml
 create mode 100755 tools/run-integration-tests.sh

       via  1866fb495c300387c2549cbf21a7f8206224314a (commit)
       via  02d2a62e8cb4790126934ae369a70918c7524318 (commit)
       via  dc92aa04f168f7755192d6569ff6b5a32f8fff6e (commit)
       via  177e8ba35f7b2d4826477db2bc58cf1c50b7d0ac (commit)
       via  27f584f8f0d3270f3e737c401a794a37aa1b45c5 (commit)
       via  f3569ce613f0e19a63149c08be68e0bcbb7eaf92 (commit)
       via  0c1be947ff362ebf31e2af9440abbb4464a8c6f5 (commit)
       via  855193b8c5ab28a2b82999b0f4911d17f874303b (commit)
       via  8831e4dcad5d21cc86257b70123e542de4afe1b1 (commit)
       via  5b4a300804b3e06f8debc0e12cab01f0704d840d (commit)
       via  9bd1a28a2f55eb435ff808cc118fe4f0b7f94c51 (commit)
       via  badbbdc043054816d63b96e239123dd67febfa5e (commit)
       via  580cf2a7f5a26954eeb6ded91a28838b0a150925 (commit)
       via  6fd948636d41c240d45b7969994b55e9f0d9a977 (commit)
       via  76cc719bbb330ab23759c090a28c4c178d953436 (commit)
       via  3a1711b41363ae562303f219df0e8d6ba7f521cd (commit)
       via  5d405a641ea6821b0966f1e059bf7ed5a52f4a81 (commit)
       via  5dc561de99bdba0443568b8adeea59511a02c6ac (commit)
       via  c3601385a43e60ab6557b681f0290082bd56670f (commit)
       via  cbf153e45314bddf4458a5f8fc02462edc0d0595 (commit)
       via  321eb4b2fd6d7ef7cfe8318dc87b14eb64c152ad (commit)
       via  bbeb1b4bc19356e4d826d4915ae091300be97198 (commit)
       via  ac975ab3e027c2b7bca986f218e6e81c45f292cb (commit)
       via  86fa2c035ad8a3d5de0972aa48181d4d784c81fc (commit)
      from  2c1a7eb9248df217c86caf1685a05d5a2aaaac84 (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 1866fb495c300387c2549cbf21a7f8206224314a
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Mon Apr 27 19:06:29 2020 -0300

    16212: Refactors PeopleSelect component used on 'Share' dialog.
    
    * Now named ParticipantSelect as it also retrieves Groups.
    * Search for 'any' field on Users instead of just email.
    * Don't request groups if 'onlyPeople' prop passed.
    * Show users' display name, including email.
    * Fix chip rendering to show the same as what's listed.
    
    TBD: ParticipantSelect retrieves only 5 items per request when autocompleting.
    This may not be what users expect, but listing too many items require UI
    adjustments.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas at di-pentima.com.ar>

diff --git a/src/models/user.test.ts b/src/models/user.test.ts
index baa86da9..4fab59d5 100644
--- a/src/models/user.test.ts
+++ b/src/models/user.test.ts
@@ -8,6 +8,7 @@ describe('User', () => {
     it('gets the user display name', () => {
         type UserCase = {
             caseName: string;
+            withEmail?: boolean;
             user: User;
             expect: string;
         };
@@ -23,6 +24,18 @@ describe('User', () => {
                 },
                 expect: 'Some User'
             },
+            {
+                caseName: 'Full data available (with email)',
+                withEmail: true,
+                user: {
+                    email: 'someuser at example.com', username: 'someuser',
+                    firstName: 'Some', lastName: 'User',
+                    uuid: 'zzzzz-tpzed-someusersuuid',
+                    ownerUuid: 'zzzzz-tpzed-someusersowneruuid',
+                    prefs: {}, isAdmin: false, isActive: true
+                },
+                expect: 'Some User <<someuser at example.com>>'
+            },
             {
                 caseName: 'Missing first name',
                 user: {
@@ -56,6 +69,18 @@ describe('User', () => {
                 },
                 expect: 'someuser at example.com'
             },
+            {
+                caseName: 'Missing first & last names (with email)',
+                withEmail: true,
+                user: {
+                    email: 'someuser at example.com', username: 'someuser',
+                    firstName: '', lastName: '',
+                    uuid: 'zzzzz-tpzed-someusersuuid',
+                    ownerUuid: 'zzzzz-tpzed-someusersowneruuid',
+                    prefs: {}, isAdmin: false, isActive: true
+                },
+                expect: 'someuser at example.com'
+            },
             {
                 caseName: 'Missing first & last names, and email address',
                 user: {
@@ -67,6 +92,18 @@ describe('User', () => {
                 },
                 expect: 'someuser'
             },
+            {
+                caseName: 'Missing first & last names, and email address (with email)',
+                withEmail: true,
+                user: {
+                    email: '', username: 'someuser',
+                    firstName: '', lastName: '',
+                    uuid: 'zzzzz-tpzed-someusersuuid',
+                    ownerUuid: 'zzzzz-tpzed-someusersowneruuid',
+                    prefs: {}, isAdmin: false, isActive: true
+                },
+                expect: 'someuser'
+            },
             {
                 caseName: 'Missing all data (should not happen)',
                 user: {
@@ -80,7 +117,7 @@ describe('User', () => {
             },
         ];
         testCases.forEach(c => {
-            const dispName = getUserDisplayName(c.user);
+            const dispName = getUserDisplayName(c.user, c.withEmail);
             expect(dispName).toEqual(c.expect);
         })
     });
diff --git a/src/models/user.ts b/src/models/user.ts
index 1e9a0260..3f0bcf47 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -32,8 +32,12 @@ export const getUserFullname = (user: User) => {
         : "";
 };
 
-export const getUserDisplayName = (user: User) => {
-    return getUserFullname(user) || user.email || user.username || user.uuid;
+export const getUserDisplayName = (user: User, withEmail = false) => {
+    const displayName = getUserFullname(user) || user.email || user.username || user.uuid;
+    if (withEmail && user.email && displayName !== user.email) {
+        return `${displayName} <<${user.email}>>`;
+    }
+    return displayName;
 };
 
 export interface UserResource extends Resource, User {
diff --git a/src/store/group-details-panel/group-details-panel-actions.ts b/src/store/group-details-panel/group-details-panel-actions.ts
index 41fb690f..b2a72807 100644
--- a/src/store/group-details-panel/group-details-panel-actions.ts
+++ b/src/store/group-details-panel/group-details-panel-actions.ts
@@ -6,7 +6,7 @@ import { bindDataExplorerActions } from '~/store/data-explorer/data-explorer-act
 import { Dispatch } from 'redux';
 import { propertiesActions } from '~/store/properties/properties-actions';
 import { getProperty } from '~/store/properties/properties';
-import { Person } from '~/views-components/sharing-dialog/people-select';
+import { Participant } from '~/views-components/sharing-dialog/participant-select';
 import { dialogActions } from '~/store/dialog/dialog-actions';
 import { reset, startSubmit } from 'redux-form';
 import { addGroupMember, deleteGroupMember } from '~/store/groups-panel/groups-panel-actions';
@@ -36,7 +36,7 @@ export const loadGroupDetailsPanel = (groupUuid: string) =>
 export const getCurrentGroupDetailsPanelUuid = getProperty<string>(GROUP_DETAILS_PANEL_ID);
 
 export interface AddGroupMembersFormData {
-    [ADD_GROUP_MEMBERS_USERS_FIELD_NAME]: Person[];
+    [ADD_GROUP_MEMBERS_USERS_FIELD_NAME]: Participant[];
 }
 
 export const openAddGroupMembersDialog = () =>
diff --git a/src/store/groups-panel/groups-panel-actions.ts b/src/store/groups-panel/groups-panel-actions.ts
index 35ec413c..b5ca3775 100644
--- a/src/store/groups-panel/groups-panel-actions.ts
+++ b/src/store/groups-panel/groups-panel-actions.ts
@@ -6,7 +6,7 @@ import { Dispatch } from 'redux';
 import { reset, startSubmit, stopSubmit, FormErrors } from 'redux-form';
 import { bindDataExplorerActions } from "~/store/data-explorer/data-explorer-action";
 import { dialogActions } from '~/store/dialog/dialog-actions';
-import { Person } from '~/views-components/sharing-dialog/people-select';
+import { Participant } from '~/views-components/sharing-dialog/participant-select';
 import { RootState } from '~/store/store';
 import { ServiceRepository } from '~/services/services';
 import { getResource } from '~/store/resources/resources';
@@ -65,7 +65,7 @@ export const openRemoveGroupDialog = (uuid: string) =>
 
 export interface CreateGroupFormData {
     [CREATE_GROUP_NAME_FIELD_NAME]: string;
-    [CREATE_GROUP_USERS_FIELD_NAME]?: Person[];
+    [CREATE_GROUP_USERS_FIELD_NAME]?: Participant[];
 }
 
 export const createGroup = ({ name, users = [] }: CreateGroupFormData) =>
diff --git a/src/views-components/dialog-forms/add-group-member-dialog.tsx b/src/views-components/dialog-forms/add-group-member-dialog.tsx
index 2bd2109e..53917f54 100644
--- a/src/views-components/dialog-forms/add-group-member-dialog.tsx
+++ b/src/views-components/dialog-forms/add-group-member-dialog.tsx
@@ -7,7 +7,7 @@ import { compose } from "redux";
 import { reduxForm, InjectedFormProps, WrappedFieldArrayProps, FieldArray } from 'redux-form';
 import { withDialog, WithDialogProps } from "~/store/dialog/with-dialog";
 import { FormDialog } from '~/components/form-dialog/form-dialog';
-import { PeopleSelect, Person } from '~/views-components/sharing-dialog/people-select';
+import { ParticipantSelect, Participant } from '~/views-components/sharing-dialog/participant-select';
 import { ADD_GROUP_MEMBERS_DIALOG, ADD_GROUP_MEMBERS_FORM, AddGroupMembersFormData, ADD_GROUP_MEMBERS_USERS_FIELD_NAME, addGroupMembers } from '~/store/group-details-panel/group-details-panel-actions';
 import { minLength } from '~/validators/min-length';
 
@@ -39,8 +39,8 @@ const UsersField = () =>
 
 const UsersFieldValidation = [minLength(1, () => 'Select at least one user')];
 
-const UsersSelect = ({ fields }: WrappedFieldArrayProps<Person>) =>
-    <PeopleSelect
+const UsersSelect = ({ fields }: WrappedFieldArrayProps<Participant>) =>
+    <ParticipantSelect
         onlyPeople
         autofocus
         label='Enter email adresses '
diff --git a/src/views-components/dialog-forms/create-group-dialog.tsx b/src/views-components/dialog-forms/create-group-dialog.tsx
index ff692fbd..7af5317d 100644
--- a/src/views-components/dialog-forms/create-group-dialog.tsx
+++ b/src/views-components/dialog-forms/create-group-dialog.tsx
@@ -11,7 +11,7 @@ import { CREATE_GROUP_DIALOG, CREATE_GROUP_FORM, createGroup, CreateGroupFormDat
 import { TextField } from '~/components/text-field/text-field';
 import { maxLength } from '~/validators/max-length';
 import { require } from '~/validators/require';
-import { PeopleSelect, Person } from '~/views-components/sharing-dialog/people-select';
+import { ParticipantSelect, Participant } from '~/views-components/sharing-dialog/participant-select';
 
 export const CreateGroupDialog = compose(
     withDialog(CREATE_GROUP_DIALOG),
@@ -54,8 +54,8 @@ const UsersField = () =>
         name={CREATE_GROUP_USERS_FIELD_NAME}
         component={UsersSelect} />;
 
-const UsersSelect = ({ fields }: WrappedFieldArrayProps<Person>) =>
-    <PeopleSelect
+const UsersSelect = ({ fields }: WrappedFieldArrayProps<Participant>) =>
+    <ParticipantSelect
         onlyPeople
         label='Enter email adresses '
         items={fields.getAll() || []}
diff --git a/src/views-components/sharing-dialog/people-select.tsx b/src/views-components/sharing-dialog/participant-select.tsx
similarity index 60%
rename from src/views-components/sharing-dialog/people-select.tsx
rename to src/views-components/sharing-dialog/participant-select.tsx
index 90235cd5..5d062da0 100644
--- a/src/views-components/sharing-dialog/people-select.tsx
+++ b/src/views-components/sharing-dialog/participant-select.tsx
@@ -10,38 +10,50 @@ import { FilterBuilder } from '../../services/api/filter-builder';
 import { debounce } from 'debounce';
 import { ListItemText, Typography } from '@material-ui/core';
 import { noop } from 'lodash/fp';
-import { GroupClass } from '~/models/group';
+import { GroupClass, GroupResource } from '~/models/group';
+import { getUserDisplayName, UserResource } from '~/models/user';
+import { ResourceKind } from '~/models/resource';
+import { ListResults } from '~/services/common-service/common-service';
 
-export interface Person {
+export interface Participant {
     name: string;
-    email: string;
     uuid: string;
 }
 
-export interface PeopleSelectProps {
+type ParticipantResource = GroupResource & UserResource;
 
-    items: Person[];
+interface ParticipantSelectProps {
+    items: Participant[];
     label?: string;
     autofocus?: boolean;
     onlyPeople?: boolean;
 
     onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
     onFocus?: (event: React.FocusEvent<HTMLInputElement>) => void;
-    onCreate?: (person: Person) => void;
+    onCreate?: (person: Participant) => void;
     onDelete?: (index: number) => void;
-    onSelect?: (person: Person) => void;
-
+    onSelect?: (person: Participant) => void;
 }
 
-export interface PeopleSelectState {
+interface ParticipantSelectState {
     value: string;
-    suggestions: any[];
+    suggestions: ParticipantResource[];
 }
 
-export const PeopleSelect = connect()(
-    class PeopleSelect extends React.Component<PeopleSelectProps & DispatchProp, PeopleSelectState> {
-
-        state: PeopleSelectState = {
+const getDisplayName = (item: GroupResource & UserResource) => {
+    switch(item.kind) {
+        case ResourceKind.USER:
+            return getUserDisplayName(item, true);
+        case ResourceKind.GROUP:
+            return item.name;
+        default:
+            return item.uuid;
+    }
+};
+
+export const ParticipantSelect = connect()(
+    class ParticipantSelect extends React.Component<ParticipantSelectProps & DispatchProp, ParticipantSelectState> {
+        state: ParticipantSelectState = {
             value: '',
             suggestions: []
         };
@@ -67,21 +79,20 @@ export const PeopleSelect = connect()(
             );
         }
 
-        renderChipValue({ name, uuid }: Person) {
-            return name ? name : uuid;
+        renderChipValue(chipValue: Participant) {
+            const { name, uuid } = chipValue;
+            return name || uuid;
         }
 
-        renderSuggestion({ firstName, lastName, email, name }: any) {
+        renderSuggestion(item: ParticipantResource) {
             return (
                 <ListItemText>
-                    {name ?
-                        <Typography noWrap>{name}</Typography> :
-                        <Typography noWrap>{`${firstName} ${lastName} <<${email}>>`}</Typography>}
+                    <Typography noWrap>{getDisplayName(item)}</Typography>
                 </ListItemText>
             );
         }
 
-        handleDelete = (_: Person, index: number) => {
+        handleDelete = (_: Participant, index: number) => {
             const { onDelete = noop } = this.props;
             onDelete(index);
         }
@@ -91,19 +102,18 @@ export const PeopleSelect = connect()(
             if (onCreate) {
                 this.setState({ value: '', suggestions: [] });
                 onCreate({
-                    email: '',
                     name: '',
                     uuid: this.state.value,
                 });
             }
         }
 
-        handleSelect = ({ email, firstName, lastName, uuid, name }: any) => {
+        handleSelect = (selection: ParticipantResource) => {
+            const { uuid } = selection;
             const { onSelect = noop } = this.props;
             this.setState({ value: '', suggestions: [] });
             onSelect({
-                email,
-                name: `${name ? name : `${firstName} ${lastName}`}`,
+                name: getDisplayName(selection),
                 uuid,
             });
         }
@@ -116,16 +126,23 @@ export const PeopleSelect = connect()(
 
         requestSuggestions = async (_: void, __: void, { userService, groupsService }: ServiceRepository) => {
             const { value } = this.state;
+            const limit = 5; // FIXME: Does this provide a good UX?
+
+            const filterUsers = new FilterBuilder()
+                .addILike('any', value)
+                .getFilters();
+            const userItems: ListResults<any> = await userService.list({ filters: filterUsers, limit });
+
             const filterGroups = new FilterBuilder()
                 .addNotIn('group_class', [GroupClass.PROJECT])
                 .addILike('name', value)
                 .getFilters();
-            const groupItems = await groupsService.list({ filters: filterGroups, limit: 5 });
-            const filterUsers = new FilterBuilder()
-                .addILike('email', value)
-                .getFilters();
-            const userItems: any = await userService.list({ filters: filterUsers, limit: 5 });
-            const items = groupItems.items.concat(userItems.items);
-            this.setState({ suggestions: this.props.onlyPeople ? userItems.items : items });
+
+            const groupItems: ListResults<any> = await groupsService.list({ filters: filterGroups, limit });
+            this.setState({
+                suggestions: this.props.onlyPeople
+                    ? userItems.items
+                    : userItems.items.concat(groupItems.items)
+            });
         }
     });
diff --git a/src/views-components/sharing-dialog/sharing-invitation-form-component.tsx b/src/views-components/sharing-dialog/sharing-invitation-form-component.tsx
index 5aec8feb..59456fb3 100644
--- a/src/views-components/sharing-dialog/sharing-invitation-form-component.tsx
+++ b/src/views-components/sharing-dialog/sharing-invitation-form-component.tsx
@@ -6,7 +6,7 @@ import * as React from 'react';
 import { Field, WrappedFieldProps, FieldArray, WrappedFieldArrayProps } from 'redux-form';
 import { Grid, FormControl, InputLabel } from '@material-ui/core';
 import { PermissionSelect, parsePermissionLevel, formatPermissionLevel } from './permission-select';
-import { PeopleSelect, Person } from './people-select';
+import { ParticipantSelect, Participant } from './participant-select';
 
 export default () =>
     <Grid container spacing={8}>
@@ -24,8 +24,8 @@ const InvitedPeopleField = () =>
         component={InvitedPeopleFieldComponent} />;
 
 
-const InvitedPeopleFieldComponent = ({ fields }: WrappedFieldArrayProps<Person>) =>
-    <PeopleSelect
+const InvitedPeopleFieldComponent = ({ fields }: WrappedFieldArrayProps<Participant>) =>
+    <ParticipantSelect
         items={fields.getAll() || []}
         onSelect={fields.push}
         onDelete={fields.remove} />;

commit 02d2a62e8cb4790126934ae369a70918c7524318
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Fri Apr 24 17:55:34 2020 -0300

    16212: Uses getUserDisplayName() wherever needed to show a user's name.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas at di-pentima.com.ar>

diff --git a/src/models/user.test.ts b/src/models/user.test.ts
index d313ef4e..baa86da9 100644
--- a/src/models/user.test.ts
+++ b/src/models/user.test.ts
@@ -5,10 +5,6 @@
 import { User, getUserDisplayName } from './user';
 
 describe('User', () => {
-
-    beforeEach(() => {
-    });
-
     it('gets the user display name', () => {
         type UserCase = {
             caseName: string;
diff --git a/src/store/collections-content-address-panel/collections-content-address-middleware-service.ts b/src/store/collections-content-address-panel/collections-content-address-middleware-service.ts
index dc0f2c52..72da1d2e 100644
--- a/src/store/collections-content-address-panel/collections-content-address-middleware-service.ts
+++ b/src/store/collections-content-address-panel/collections-content-address-middleware-service.ts
@@ -24,6 +24,7 @@ import { updatePublicFavorites } from '~/store/public-favorites/public-favorites
 import { setBreadcrumbs } from '../breadcrumbs/breadcrumbs-actions';
 import { ResourceKind, extractUuidKind } from '~/models/resource';
 import { ownerNameActions } from '~/store/owner-name/owner-name-actions';
+import { getUserDisplayName } from '~/models/user';
 
 export class CollectionsWithSameContentAddressMiddlewareService extends DataExplorerMiddlewareService {
     constructor(private services: ServiceRepository, id: string) {
@@ -90,7 +91,11 @@ export class CollectionsWithSameContentAddressMiddlewareService extends DataExpl
                         .getFilters()
                 });
                 responseUsers.items.map(it => {
-                    api.dispatch<any>(ownerNameActions.SET_OWNER_NAME({ name: it.uuid === userUuid ? 'User: Me' : `User: ${it.firstName} ${it.lastName}`, uuid: it.uuid }));
+                    api.dispatch<any>(ownerNameActions.SET_OWNER_NAME({
+                        name: it.uuid === userUuid
+                            ? 'User: Me'
+                            : `User: ${getUserDisplayName(it)}`,
+                        uuid: it.uuid }));
                 });
                 responseGroups.items.map(it => {
                     api.dispatch<any>(ownerNameActions.SET_OWNER_NAME({ name: `Project: ${it.name}`, uuid: it.uuid }));
diff --git a/src/views-components/advanced-tab-dialog/metadataTab.tsx b/src/views-components/advanced-tab-dialog/metadataTab.tsx
index bcf277c0..467501a7 100644
--- a/src/views-components/advanced-tab-dialog/metadataTab.tsx
+++ b/src/views-components/advanced-tab-dialog/metadataTab.tsx
@@ -4,7 +4,7 @@
 
 import * as React from "react";
 import { Table, TableHead, TableCell, TableRow, TableBody, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
-import { UserResource } from "~/models/user";
+import { UserResource, getUserDisplayName } from "~/models/user";
 
 type CssRules = 'cell';
 
@@ -47,7 +47,7 @@ export const MetadataTab = withStyles(styles)((props: MetadataProps & WithStyles
                     <TableCell className={props.classes.cell}>{it.uuid}</TableCell>
                     <TableCell className={props.classes.cell}>{it.linkClass}</TableCell>
                     <TableCell className={props.classes.cell}>{it.name}</TableCell>
-                    <TableCell className={props.classes.cell}>{props.user && `User: ${props.user.firstName} ${props.user.lastName}`}</TableCell>
+                    <TableCell className={props.classes.cell}>{props.user && `User: ${getUserDisplayName(props.user)}`}</TableCell>
                     <TableCell className={props.classes.cell}>{it.headUuid === props.uuid ? 'this' : it.headUuid}</TableCell>
                     <TableCell className={props.classes.cell}>{JSON.stringify(it.properties)}</TableCell>
                 </TableRow>
diff --git a/src/views-components/user-dialog/manage-dialog.tsx b/src/views-components/user-dialog/manage-dialog.tsx
index 05e4a3fc..f1f0b6ce 100644
--- a/src/views-components/user-dialog/manage-dialog.tsx
+++ b/src/views-components/user-dialog/manage-dialog.tsx
@@ -3,14 +3,15 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as 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 { compose, Dispatch } from "redux";
 import { USER_MANAGEMENT_DIALOG, openSetupShellAccount, loginAs } from "~/store/users/users-actions";
-import { connect } from "react-redux";
+import { getUserDisplayName } from "~/models/user";
 
 type CssRules = 'spacing';
 
@@ -48,19 +49,19 @@ export const UserManageDialog = compose(
                 maxWidth="md">
                 {props.data &&
                     <span>
-                        <DialogTitle>{`Manage - ${props.data.firstName} ${props.data.lastName}`}</DialogTitle>
+                        <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 ${props.data.firstName} ${props.data.lastName}`}
+                                {`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 ${props.data.firstName} ${props.data.lastName}`}
+                                {`SETUP SHELL ACCOUNT FOR ${getUserDisplayName(props.data)}`}
                             </Button>
                         </DialogContent></span>}
 

commit dc92aa04f168f7755192d6569ff6b5a32f8fff6e
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Fri Apr 24 12:06:29 2020 -0300

    16212: Displays user depending on available user data.
    
    Some times first/last names or email aren't available, so we want to
    show other than an empty string.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas at di-pentima.com.ar>

diff --git a/src/models/user.test.ts b/src/models/user.test.ts
new file mode 100644
index 00000000..d313ef4e
--- /dev/null
+++ b/src/models/user.test.ts
@@ -0,0 +1,91 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { User, getUserDisplayName } from './user';
+
+describe('User', () => {
+
+    beforeEach(() => {
+    });
+
+    it('gets the user display name', () => {
+        type UserCase = {
+            caseName: string;
+            user: User;
+            expect: string;
+        };
+        const testCases: UserCase[] = [
+            {
+                caseName: 'Full data available',
+                user: {
+                    email: 'someuser at example.com', username: 'someuser',
+                    firstName: 'Some', lastName: 'User',
+                    uuid: 'zzzzz-tpzed-someusersuuid',
+                    ownerUuid: 'zzzzz-tpzed-someusersowneruuid',
+                    prefs: {}, isAdmin: false, isActive: true
+                },
+                expect: 'Some User'
+            },
+            {
+                caseName: 'Missing first name',
+                user: {
+                    email: 'someuser at example.com', username: 'someuser',
+                    firstName: '', lastName: 'User',
+                    uuid: 'zzzzz-tpzed-someusersuuid',
+                    ownerUuid: 'zzzzz-tpzed-someusersowneruuid',
+                    prefs: {}, isAdmin: false, isActive: true
+                },
+                expect: 'someuser at example.com'
+            },
+            {
+                caseName: 'Missing last name',
+                user: {
+                    email: 'someuser at example.com', username: 'someuser',
+                    firstName: 'Some', lastName: '',
+                    uuid: 'zzzzz-tpzed-someusersuuid',
+                    ownerUuid: 'zzzzz-tpzed-someusersowneruuid',
+                    prefs: {}, isAdmin: false, isActive: true
+                },
+                expect: 'someuser at example.com'
+            },
+            {
+                caseName: 'Missing first & last names',
+                user: {
+                    email: 'someuser at example.com', username: 'someuser',
+                    firstName: '', lastName: '',
+                    uuid: 'zzzzz-tpzed-someusersuuid',
+                    ownerUuid: 'zzzzz-tpzed-someusersowneruuid',
+                    prefs: {}, isAdmin: false, isActive: true
+                },
+                expect: 'someuser at example.com'
+            },
+            {
+                caseName: 'Missing first & last names, and email address',
+                user: {
+                    email: '', username: 'someuser',
+                    firstName: '', lastName: '',
+                    uuid: 'zzzzz-tpzed-someusersuuid',
+                    ownerUuid: 'zzzzz-tpzed-someusersowneruuid',
+                    prefs: {}, isAdmin: false, isActive: true
+                },
+                expect: 'someuser'
+            },
+            {
+                caseName: 'Missing all data (should not happen)',
+                user: {
+                    email: '', username: '',
+                    firstName: '', lastName: '',
+                    uuid: 'zzzzz-tpzed-someusersuuid',
+                    ownerUuid: 'zzzzz-tpzed-someusersowneruuid',
+                    prefs: {}, isAdmin: false, isActive: true
+                },
+                expect: 'zzzzz-tpzed-someusersuuid'
+            },
+        ];
+        testCases.forEach(c => {
+            const dispName = getUserDisplayName(c.user);
+            expect(dispName).toEqual(c.expect);
+        })
+    });
+});
diff --git a/src/models/user.ts b/src/models/user.ts
index 87a97dfc..1e9a0260 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -26,8 +26,14 @@ export interface User {
     isActive: boolean;
 }
 
-export const getUserFullname = (user?: User) => {
-    return user ? `${user.firstName} ${user.lastName}` : "";
+export const getUserFullname = (user: User) => {
+    return user.firstName && user.lastName
+        ? `${user.firstName} ${user.lastName}`
+        : "";
+};
+
+export const getUserDisplayName = (user: User) => {
+    return getUserFullname(user) || user.email || user.username || user.uuid;
 };
 
 export interface UserResource extends Resource, User {
diff --git a/src/models/vocabulary.test.ts b/src/models/vocabulary.test.ts
index 87a8dfb2..18e2f19f 100644
--- a/src/models/vocabulary.test.ts
+++ b/src/models/vocabulary.test.ts
@@ -3,7 +3,6 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as Vocabulary from './vocabulary';
-import { pipe } from 'lodash/fp';
 
 describe('Vocabulary', () => {
     let vocabulary: Vocabulary.Vocabulary;
diff --git a/src/services/auth-service/auth-service.ts b/src/services/auth-service/auth-service.ts
index 690420e7..61db625c 100644
--- a/src/services/auth-service/auth-service.ts
+++ b/src/services/auth-service/auth-service.ts
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { getUserFullname, User, UserPrefs } from '~/models/user';
+import { User, UserPrefs, getUserDisplayName } from '~/models/user';
 import { AxiosInstance } from "axios";
 import { ApiActions } from "~/services/api/api-actions";
 import * as uuid from "uuid/v4";
@@ -129,7 +129,7 @@ export class AuthService {
             clusterId: cfg.uuidPrefix,
             remoteHost: cfg.rootUrl,
             baseUrl: cfg.baseUrl,
-            name: getUserFullname(user),
+            name: user ? getUserDisplayName(user): '',
             email: user ? user.email : '',
             token: this.getApiToken(),
             loggedIn: true,
diff --git a/src/store/auth/auth-action-session.ts b/src/store/auth/auth-action-session.ts
index 52a1e23a..fc35ff88 100644
--- a/src/store/auth/auth-action-session.ts
+++ b/src/store/auth/auth-action-session.ts
@@ -7,7 +7,7 @@ import { setBreadcrumbs } from "~/store/breadcrumbs/breadcrumbs-actions";
 import { RootState } from "~/store/store";
 import { ServiceRepository, createServices, setAuthorizationHeader } from "~/services/services";
 import Axios from "axios";
-import { getUserFullname, User } from "~/models/user";
+import { User, getUserDisplayName } from "~/models/user";
 import { authActions } from "~/store/auth/auth-action";
 import {
     Config, ClusterConfigJSON, CLUSTER_CONFIG_PATH, DISCOVERY_DOC_PATH,
@@ -131,7 +131,7 @@ export const validateSession = (session: Session, activeSession: Session) =>
             session.token = token;
             session.email = user.email;
             session.uuid = user.uuid;
-            session.name = getUserFullname(user);
+            session.name = getUserDisplayName(user);
             session.loggedIn = true;
             session.apiRevision = apiRevision;
         };
@@ -242,7 +242,7 @@ export const addSession = (remoteHost: string, token?: string, sendToLogin?: boo
                     status: SessionStatus.VALIDATED,
                     active: false,
                     email: user.email,
-                    name: getUserFullname(user),
+                    name: getUserDisplayName(user),
                     uuid: user.uuid,
                     baseUrl: config.baseUrl,
                     clusterId: config.uuidPrefix,
diff --git a/src/store/group-details-panel/group-details-panel-actions.ts b/src/store/group-details-panel/group-details-panel-actions.ts
index 55bfd5ae..41fb690f 100644
--- a/src/store/group-details-panel/group-details-panel-actions.ts
+++ b/src/store/group-details-panel/group-details-panel-actions.ts
@@ -16,7 +16,7 @@ import { RootState } from '~/store/store';
 import { ServiceRepository } from '~/services/services';
 import { PermissionResource } from '~/models/permission';
 import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
-import { UserResource, getUserFullname } from '~/models/user';
+import { UserResource, getUserDisplayName } from '~/models/user';
 
 export const GROUP_DETAILS_PANEL_ID = 'groupDetailsPanel';
 export const ADD_GROUP_MEMBERS_DIALOG = 'addGrupMembers';
@@ -113,7 +113,7 @@ export const removeGroupMember = (uuid: string) =>
             await deleteGroupMember({
                 user: {
                     uuid,
-                    name: user ? getUserFullname(user) : uuid,
+                    name: user ? getUserDisplayName(user) : uuid,
                 },
                 group: {
                     uuid: groupUuid,
diff --git a/src/views-components/main-app-bar/account-menu.tsx b/src/views-components/main-app-bar/account-menu.tsx
index 346a9ef0..37702536 100644
--- a/src/views-components/main-app-bar/account-menu.tsx
+++ b/src/views-components/main-app-bar/account-menu.tsx
@@ -5,7 +5,7 @@
 import * as React from "react";
 import { MenuItem, Divider } from "@material-ui/core";
 import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
-import { User, getUserFullname } from "~/models/user";
+import { User, getUserDisplayName } from "~/models/user";
 import { DropdownMenu } from "~/components/dropdown-menu/dropdown-menu";
 import { UserPanelIcon } from "~/components/icon/icon";
 import { DispatchProp, connect } from 'react-redux';
@@ -66,7 +66,7 @@ export const AccountMenu = withStyles(styles)(
                     title="Account Management"
                     key={currentRoute}>
                     <MenuItem disabled>
-                        {getUserFullname(user)} {user.uuid.substr(0, 5) !== localCluster && `(${user.uuid.substr(0, 5)})`}
+                        {getUserDisplayName(user)} {user.uuid.substr(0, 5) !== localCluster && `(${user.uuid.substr(0, 5)})`}
                     </MenuItem>
                     {user.isActive ? <>
                         <MenuItem onClick={() => dispatch(openUserVirtualMachines())}>Virtual Machines</MenuItem>

commit 177e8ba35f7b2d4826477db2bc58cf1c50b7d0ac
Merge: 2c1a7eb9 27f584f8
Author: Lucas Di Pentima <lucas at di-pentima.com.ar>
Date:   Fri Apr 24 10:28:58 2020 -0300

    16212: Merge branch 'master' into 16212-login-form
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas at di-pentima.com.ar>


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


hooks/post-receive
-- 




More information about the arvados-commits mailing list