14720: Add UUID to My account page. Layout fixes.
authorPeter Amstutz <pamstutz@veritasgenetics.com>
Tue, 12 Feb 2019 21:24:56 +0000 (16:24 -0500)
committerPeter Amstutz <pamstutz@veritasgenetics.com>
Tue, 12 Feb 2019 21:40:50 +0000 (16:40 -0500)
Makes a number of UI improvements related to logging in as a federated
user.

* My account page includes UUID

* Replace "identityUrl" in user model with "username"

* Rework layout of "My Account" page to reflow properly when view is
  narrow

* Rework layout of login page, add scrolling when view is short

Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <pamstutz@veritasgenetics.com>

src/models/user.ts
src/services/auth-service/auth-service.ts
src/store/advanced-tab/advanced-tab.tsx
src/store/auth/auth-action-session.ts
src/store/auth/auth-action.test.ts
src/store/auth/auth-reducer.test.ts
src/views-components/main-app-bar/account-menu.tsx
src/views-components/user-dialog/attributes-dialog.tsx
src/views/login-panel/login-panel.tsx
src/views/my-account-panel/my-account-panel-root.tsx

index a7b8458bf81fb37eb6480d33ea19c6e484b67180..6477dc5339e77cb11a8a2555f6d0e35d97b3e163 100644 (file)
@@ -20,7 +20,7 @@ export interface User {
     lastName: string;
     uuid: string;
     ownerUuid: string;
-    identityUrl: string;
+    username: string;
     prefs: UserPrefs;
     isAdmin: boolean;
 }
@@ -35,10 +35,9 @@ export interface UserResource extends Resource {
     username: string;
     firstName: string;
     lastName: string;
-    identityUrl: string;
     isAdmin: boolean;
     prefs: UserPrefs;
     defaultOwnerUuid: string;
     isActive: boolean;
     writableBy: string[];
-}
\ No newline at end of file
+}
index fff7d5778f8263d7d2b08a5362d083490c206449..11e3e776f411bbed720fc130cc89308349270158 100644 (file)
@@ -17,7 +17,7 @@ export const USER_LAST_NAME_KEY = 'userLastName';
 export const USER_UUID_KEY = 'userUuid';
 export const USER_OWNER_UUID_KEY = 'userOwnerUuid';
 export const USER_IS_ADMIN = 'isAdmin';
-export const USER_IDENTITY_URL = 'identityUrl';
+export const USER_USERNAME = 'username';
 export const USER_PREFS = 'prefs';
 
 export interface UserDetailsResponse {
@@ -27,7 +27,7 @@ export interface UserDetailsResponse {
     uuid: string;
     owner_uuid: string;
     is_admin: boolean;
-    identity_url: string;
+    username: string;
     prefs: UserPrefs;
 }
 
@@ -69,11 +69,11 @@ export class AuthService {
         const uuid = this.getUuid();
         const ownerUuid = this.getOwnerUuid();
         const isAdmin = this.getIsAdmin();
-        const identityUrl = localStorage.getItem(USER_IDENTITY_URL);
+        const username = localStorage.getItem(USER_USERNAME);
         const prefs = JSON.parse(localStorage.getItem(USER_PREFS) || '{"profile": {}}');
 
-        return email && firstName && lastName && uuid && ownerUuid && identityUrl && prefs
-            ? { email, firstName, lastName, uuid, ownerUuid, isAdmin, identityUrl, prefs }
+        return email && firstName && lastName && uuid && ownerUuid && username && prefs
+            ? { email, firstName, lastName, uuid, ownerUuid, isAdmin, username, prefs }
             : undefined;
     }
 
@@ -84,7 +84,7 @@ export class AuthService {
         localStorage.setItem(USER_UUID_KEY, user.uuid);
         localStorage.setItem(USER_OWNER_UUID_KEY, user.ownerUuid);
         localStorage.setItem(USER_IS_ADMIN, JSON.stringify(user.isAdmin));
-        localStorage.setItem(USER_IDENTITY_URL, user.identityUrl);
+        localStorage.setItem(USER_USERNAME, user.username);
         localStorage.setItem(USER_PREFS, JSON.stringify(user.prefs));
     }
 
@@ -95,7 +95,7 @@ export class AuthService {
         localStorage.removeItem(USER_UUID_KEY);
         localStorage.removeItem(USER_OWNER_UUID_KEY);
         localStorage.removeItem(USER_IS_ADMIN);
-        localStorage.removeItem(USER_IDENTITY_URL);
+        localStorage.removeItem(USER_USERNAME);
         localStorage.removeItem(USER_PREFS);
     }
 
@@ -125,7 +125,7 @@ export class AuthService {
                     uuid: resp.data.uuid,
                     ownerUuid: resp.data.owner_uuid,
                     isAdmin: resp.data.is_admin,
-                    identityUrl: resp.data.identity_url,
+                    username: resp.data.username,
                     prefs
                 };
             })
index 3d10e54a910030cba00846ce0b4e4019f3d435b0..b1f0f983eaf8b78cc6c669ea715ab2a3643ea610 100644 (file)
@@ -554,7 +554,7 @@ const keepServiceApiResponse = (apiResponse: KeepServiceResource) => {
 const userApiResponse = (apiResponse: UserResource) => {
     const {
         uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid,
-        email, firstName, lastName, identityUrl, isActive, isAdmin, prefs, defaultOwnerUuid, username
+        email, firstName, lastName, username, isActive, isAdmin, prefs, defaultOwnerUuid,
     } = apiResponse;
     const response = `
 "uuid": "${uuid}",
@@ -566,7 +566,7 @@ const userApiResponse = (apiResponse: UserResource) => {
 "email": "${email}",
 "first_name": "${firstName}",
 "last_name": "${stringify(lastName)}",
-"identity_url": "${identityUrl}",
+"username": "${username}",
 "is_active": "${isActive},
 "is_admin": "${isAdmin},
 "prefs": "${stringifyObject(prefs)},
@@ -646,4 +646,4 @@ const linkApiResponse = (apiResponse: LinkResource) => {
 "properties": "${JSON.stringify(properties, null, 2)}"`;
 
     return <span style={{ marginLeft: '-15px' }}>{'{'} {response} {'\n'} <span style={{ marginLeft: '-15px' }}>{'}'}</span></span>;
-};
\ No newline at end of file
+};
index b32e205016a1824a8c6ed3554637ffac68b2b726..986230ed7f2ebd140ac8c3e6400622b0aa2a9e3f 100644 (file)
@@ -92,7 +92,7 @@ const clusterLogin = async (clusterId: string, baseUrl: string, activeSession: S
             ownerUuid: user.owner_uuid,
             email: user.email,
             isAdmin: user.is_admin,
-            identityUrl: user.identity_url,
+            username: user.username,
             prefs: user.prefs
         },
         token: saltedToken
index e5775a493d07f38623c22141744d9b949f0fef96..2e36b10758f19f2c9b1fa25247ccc934d30cd824 100644 (file)
@@ -11,7 +11,7 @@ import {
     USER_LAST_NAME_KEY,
     USER_OWNER_UUID_KEY,
     USER_UUID_KEY,
-    USER_IS_ADMIN, USER_IDENTITY_URL, USER_PREFS
+    USER_IS_ADMIN, USER_USERNAME, USER_PREFS
 } from "~/services/auth-service/auth-service";
 
 import 'jest-localstorage-mock';
@@ -25,8 +25,8 @@ describe('auth-actions', () => {
     let reducer: (state: AuthState | undefined, action: AuthAction) => any;
     let store: RootStore;
     const actions: ApiActions = {
-        progressFn: (id: string, working: boolean) => {},
-        errorFn: (id: string, message: string) => {}
+        progressFn: (id: string, working: boolean) => { },
+        errorFn: (id: string, message: string) => { }
     };
 
     beforeEach(() => {
@@ -42,7 +42,7 @@ describe('auth-actions', () => {
         localStorage.setItem(USER_FIRST_NAME_KEY, "John");
         localStorage.setItem(USER_LAST_NAME_KEY, "Doe");
         localStorage.setItem(USER_UUID_KEY, "uuid");
-        localStorage.setItem(USER_IDENTITY_URL, "identityUrl");
+        localStorage.setItem(USER_USERNAME, "username");
         localStorage.setItem(USER_PREFS, JSON.stringify({}));
         localStorage.setItem(USER_OWNER_UUID_KEY, "ownerUuid");
         localStorage.setItem(USER_IS_ADMIN, JSON.stringify("false"));
@@ -83,7 +83,7 @@ describe('auth-actions', () => {
                 lastName: "Doe",
                 uuid: "uuid",
                 ownerUuid: "ownerUuid",
-                identityUrl: "identityUrl",
+                username: "username",
                 prefs: {},
                 isAdmin: false
             }
@@ -111,5 +111,3 @@ describe('auth-actions', () => {
     });
     */
 });
-
-
index 773f9f82dbd737b2e7f0d3fa843b5be4779d6d57..f527edec21cdc69aa00d0de0c79b2b0510edc4dc 100644 (file)
@@ -13,8 +13,8 @@ import { ApiActions } from "~/services/api/api-actions";
 describe('auth-reducer', () => {
     let reducer: (state: AuthState | undefined, action: AuthAction) => any;
     const actions: ApiActions = {
-        progressFn: (id: string, working: boolean) => {},
-        errorFn: (id: string, message: string) => {}
+        progressFn: (id: string, working: boolean) => { },
+        errorFn: (id: string, message: string) => { }
     };
 
     beforeAll(() => {
@@ -30,7 +30,7 @@ describe('auth-reducer', () => {
             lastName: "Doe",
             uuid: "uuid",
             ownerUuid: "ownerUuid",
-            identityUrl: "identityUrl",
+            username: "username",
             prefs: {},
             isAdmin: false
         };
@@ -64,7 +64,7 @@ describe('auth-reducer', () => {
             lastName: "Doe",
             uuid: "uuid",
             ownerUuid: "ownerUuid",
-            identityUrl: "identityUrl",
+            username: "username",
             prefs: {},
             isAdmin: false
         };
@@ -80,7 +80,7 @@ describe('auth-reducer', () => {
                 lastName: "Doe",
                 uuid: "uuid",
                 ownerUuid: "ownerUuid",
-                identityUrl: "identityUrl",
+                username: "username",
                 prefs: {},
                 isAdmin: false
             }
index b50ee336f3a0f8afad3bae973e018e35fd2a6d68..2aec77e2b5389760c95d7413818104eead91773b 100644 (file)
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from "react";
-import { MenuItem } from "@material-ui/core";
+import { MenuItem, Divider } from "@material-ui/core";
 import { User, getUserFullname } from "~/models/user";
 import { DropdownMenu } from "~/components/dropdown-menu/dropdown-menu";
 import { UserPanelIcon } from "~/components/icon/icon";
@@ -37,8 +37,8 @@ export const AccountMenu = connect(mapStateToProps)(
                 id="account-menu"
                 title="Account Management"
                 key={currentRoute}>
-                <MenuItem>
-                    {getUserFullname(user)} ({user.uuid.substr(0, 5)})
+                <MenuItem disabled>
+                    {getUserFullname(user)}
                 </MenuItem>
                 <MenuItem onClick={() => dispatch(openUserVirtualMachines())}>Virtual Machines</MenuItem>
                 {!user.isAdmin && <MenuItem onClick={() => dispatch(openRepositoriesPanel())}>Repositories</MenuItem>}
@@ -46,6 +46,7 @@ export const AccountMenu = connect(mapStateToProps)(
                 <MenuItem onClick={() => dispatch(navigateToSshKeysUser)}>Ssh Keys</MenuItem>
                 <MenuItem onClick={() => dispatch(navigateToSiteManager)}>Site Manager</MenuItem>
                 <MenuItem onClick={() => dispatch(navigateToMyAccount)}>My account</MenuItem>
+                <Divider />
                 <MenuItem onClick={() => dispatch(logout())}>Logout</MenuItem>
             </DropdownMenu>
             : null);
index 11594f5e1d187d39a27072a6e37b42d28cca3a10..67b267aeba16e14b182734b4f27ec20b1f360577 100644 (file)
@@ -61,7 +61,7 @@ export const UserAttributesDialog = compose(
     );
 
 const attributes = (user: UserResource, classes: any) => {
-    const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, firstName, lastName, href, identityUrl, username } = user;
+    const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, firstName, lastName, href, username } = user;
     return (
         <span>
             <Grid container direction="row">
@@ -75,7 +75,7 @@ const attributes = (user: UserResource, classes: any) => {
                     {modifiedByClientUuid && <Grid item>Modified by client uuid</Grid>}
                     {uuid && <Grid item>uuid</Grid>}
                     {href && <Grid item>Href</Grid>}
-                    {identityUrl && <Grid item>Identity url</Grid>}
+                    {username && <Grid item>Username</Grid>}
                     {username && <Grid item>Username</Grid>}
                 </Grid>
                 <Grid item xs={7} className={classes.leftContainer}>
@@ -88,7 +88,7 @@ const attributes = (user: UserResource, classes: any) => {
                     <Grid item>{modifiedByClientUuid}</Grid>
                     <Grid item>{uuid}</Grid>
                     <Grid item>{href}</Grid>
-                    <Grid item>{identityUrl}</Grid>
+                    <Grid item>{username}</Grid>
                     <Grid item>{username}</Grid>
                 </Grid>
             </Grid>
index 12c7d6c79de9ccdc0ccad399250ce6c752ed4e59..eac4034b43822c2395f54ebb371ae6e41766993a 100644 (file)
@@ -61,7 +61,9 @@ export const LoginPanel = withStyles(styles)(
         homeCluster: state.auth.homeCluster,
         uuidPrefix: state.auth.localCluster
     }))(({ classes, dispatch, remoteHosts, homeCluster, uuidPrefix }: LoginPanelProps) =>
-        <Grid container direction="column" item xs alignItems="center" justify="center" className={classes.root}>
+        <Grid container justify="center" alignItems="center"
+            className={classes.root}
+            style={{ marginTop: 56, overflowY: "auto" }}>
             <Grid item className={classes.container}>
                 <Typography variant='h6' align="center" className={classes.title}>
                     Welcome to the Arvados Workbench
@@ -98,5 +100,5 @@ export const LoginPanel = withStyles(styles)(
                     </Button>
                 </Typography>
             </Grid>
-        </Grid>
+        </Grid >
     ));
index 819a16a410801d2ae6bff4638bcf32c71f5bb34d..e728c4ebd552e146efbcf072cb870c26da782045 100644 (file)
@@ -19,7 +19,7 @@ import {
 } from '@material-ui/core';
 import { ArvadosTheme } from '~/common/custom-theme';
 import { User } from "~/models/user";
-import { MY_ACCOUNT_VALIDATION} from "~/validators/validators";
+import { MY_ACCOUNT_VALIDATION } from "~/validators/validators";
 
 type CssRules = 'root' | 'gridItem' | 'label' | 'title' | 'actions';
 
@@ -45,7 +45,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     }
 });
 
-export interface MyAccountPanelRootActionProps {}
+export interface MyAccountPanelRootActionProps { }
 
 export interface MyAccountPanelRootDataProps {
     isPristine: boolean;
@@ -54,13 +54,13 @@ export interface MyAccountPanelRootDataProps {
 }
 
 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'}
+    { 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>;
@@ -69,93 +69,96 @@ export const MyAccountPanelRoot = withStyles(styles)(
     ({ classes, isValid, handleSubmit, reset, isPristine, invalid, submitting }: MyAccountPanelRootProps) => {
         return <Card className={classes.root}>
             <CardContent>
-                <Typography variant='h6' className={classes.title}>User profile</Typography>
                 <form onSubmit={handleSubmit}>
-                    <Grid container direction="row" spacing={24}>
-                        <Grid item xs={6}>
-                            <Grid item className={classes.gridItem}>
-                                <Field
-                                    label="E-mail"
-                                    name="email"
-                                    component={TextField}
-                                    disabled
-                                />
-                            </Grid>
-                            <Grid item className={classes.gridItem}>
-                                <Field
-                                    label="First name"
-                                    name="firstName"
-                                    component={TextField}
-                                    disabled
-                                />
-                            </Grid>
-                            <Grid item className={classes.gridItem}>
-                                <Field
-                                    label="Identity URL"
-                                    name="identityUrl"
-                                    component={TextField}
-                                    disabled
-                                />
-                            </Grid>
-                            <Grid item className={classes.gridItem}>
-                                <Field
-                                    label="Organization"
-                                    name="prefs.profile.organization"
-                                    component={TextField}
-                                    validate={MY_ACCOUNT_VALIDATION}
-                                    required
-                                />
-                            </Grid>
-                            <Grid item className={classes.gridItem}>
-                                <Field
-                                    label="Website"
-                                    name="prefs.profile.website_url"
-                                    component={TextField}
-                                />
-                            </Grid>
-                            <Grid item className={classes.gridItem}>
-                                <InputLabel className={classes.label} htmlFor="prefs.profile.role">Role</InputLabel>
-                                <Field
-                                    id="prefs.profile.role"
-                                    name="prefs.profile.role"
-                                    component={NativeSelectField}
-                                    items={RoleTypes}
-                                />
-                            </Grid>
+                    <Grid container spacing={24}>
+                        <Grid item xs={6} />
+                        <Grid item className={classes.gridItem} sm={6} xs={12}>
+                            <Field
+                                label="UUID"
+                                name="uuid"
+                                component={TextField}
+                                disabled
+                            />
                         </Grid>
-                        <Grid item xs={6}>
-                            <Grid item className={classes.gridItem} />
-                            <Grid item className={classes.gridItem}>
-                                <Field
-                                    label="Last name"
-                                    name="lastName"
-                                    component={TextField}
-                                    disabled
-                                />
-                            </Grid>
-                            <Grid item className={classes.gridItem} />
-                            <Grid item className={classes.gridItem}>
-                                <Field
-                                    label="E-mail at Organization"
-                                    name="prefs.profile.organization_email"
-                                    component={TextField}
-                                    validate={MY_ACCOUNT_VALIDATION}
-                                    required
-                                />
-                            </Grid>
+                        <Grid item className={classes.gridItem} sm={6} xs={12}>
+                            <Field
+                                label="First name"
+                                name="firstName"
+                                component={TextField}
+                                disabled
+                            />
                         </Grid>
-                        <Grid item xs={12} className={classes.actions}>
+                        <Grid item className={classes.gridItem} sm={6} xs={12}>
+                            <Field
+                                label="Last name"
+                                name="lastName"
+                                component={TextField}
+                                disabled
+                            />
+                        </Grid>
+                        <Grid item className={classes.gridItem} sm={6} xs={12}>
+                            <Field
+                                label="E-mail"
+                                name="email"
+                                component={TextField}
+                                disabled
+                            />
+                        </Grid>
+                        <Grid item className={classes.gridItem} sm={6} xs={12}>
+                            <Field
+                                label="Username"
+                                name="username"
+                                component={TextField}
+                                disabled
+                            />
+                        </Grid>
+                        <Grid item className={classes.gridItem} sm={6} xs={12}>
+                            <Field
+                                label="Organization"
+                                name="prefs.profile.organization"
+                                component={TextField}
+                                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}
+                                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}
+                                items={RoleTypes}
+                            />
+                        </Grid>
+                        <Grid item className={classes.gridItem} sm={6} xs={12}>
+                            <Field
+                                label="Website"
+                                name="prefs.profile.website_url"
+                                component={TextField}
+                            />
+                        </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>
+                                Save changes
+                        </Button>
                         </Grid>
                     </Grid>
-                </form>
-            </CardContent>
-        </Card>;}
-);
\ No newline at end of file
+                </form >
+            </CardContent >
+        </Card >;
+    }
+);