14841: Add inactive user page.
authorPeter Amstutz <pamstutz@veritasgenetics.com>
Mon, 25 Feb 2019 20:17:42 +0000 (15:17 -0500)
committerPeter Amstutz <pamstutz@veritasgenetics.com>
Mon, 25 Feb 2019 20:17:59 +0000 (15:17 -0500)
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/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/main-app-bar/main-app-bar.tsx
src/views/inactive-panel/inactive-panel.tsx [new file with mode: 0644]
src/views/inactive-panel/inactive-panel.tsx~ [new file with mode: 0644]
src/views/login-panel/login-panel.tsx
src/views/main-panel/main-panel-root.tsx

index 6477dc5339e77cb11a8a2555f6d0e35d97b3e163..2497864507787cef09d6d3914780b79136090d77 100644 (file)
@@ -23,6 +23,7 @@ export interface User {
     username: string;
     prefs: UserPrefs;
     isAdmin: boolean;
+    isActive: boolean;
 }
 
 export const getUserFullname = (user?: User) => {
index 3fd67fc01547af89c91785e2c9ac4f6c160453df..8ce8034ee60983642f194302b022687c65972593 100644 (file)
@@ -17,6 +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_IS_ACTIVE = 'isActive';
 export const USER_USERNAME = 'username';
 export const USER_PREFS = 'prefs';
 
@@ -27,6 +28,7 @@ export interface UserDetailsResponse {
     uuid: string;
     owner_uuid: string;
     is_admin: boolean;
+    is_active: boolean;
     username: string;
     prefs: UserPrefs;
 }
@@ -62,6 +64,11 @@ export class AuthService {
         return localStorage.getItem(USER_IS_ADMIN) === 'true';
     }
 
+    public getIsActive(): boolean {
+        console.log(`uia ${localStorage.getItem(USER_IS_ACTIVE)}`)
+        return localStorage.getItem(USER_IS_ACTIVE) === 'true';
+    }
+
     public getUser(): User | undefined {
         const email = localStorage.getItem(USER_EMAIL_KEY);
         const firstName = localStorage.getItem(USER_FIRST_NAME_KEY);
@@ -69,11 +76,14 @@ export class AuthService {
         const uuid = this.getUuid();
         const ownerUuid = this.getOwnerUuid();
         const isAdmin = this.getIsAdmin();
+        const isActive = this.getIsActive();
         const username = localStorage.getItem(USER_USERNAME);
         const prefs = JSON.parse(localStorage.getItem(USER_PREFS) || '{"profile": {}}');
 
+        console.log(`leg! ${isActive}`)
+
         return email && firstName && lastName && uuid && ownerUuid && username && prefs
-            ? { email, firstName, lastName, uuid, ownerUuid, isAdmin, username, prefs }
+            ? { email, firstName, lastName, uuid, ownerUuid, isAdmin, isActive, username, prefs }
             : undefined;
     }
 
@@ -84,6 +94,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_IS_ACTIVE, JSON.stringify(user.isActive));
         localStorage.setItem(USER_USERNAME, user.username);
         localStorage.setItem(USER_PREFS, JSON.stringify(user.prefs));
     }
@@ -95,6 +106,7 @@ export class AuthService {
         localStorage.removeItem(USER_UUID_KEY);
         localStorage.removeItem(USER_OWNER_UUID_KEY);
         localStorage.removeItem(USER_IS_ADMIN);
+        localStorage.removeItem(USER_IS_ACTIVE);
         localStorage.removeItem(USER_USERNAME);
         localStorage.removeItem(USER_PREFS);
     }
@@ -124,6 +136,7 @@ export class AuthService {
                     uuid: resp.data.uuid,
                     ownerUuid: resp.data.owner_uuid,
                     isAdmin: resp.data.is_admin,
+                    isActive: resp.data.is_active,
                     username: resp.data.username,
                     prefs
                 };
index 986230ed7f2ebd140ac8c3e6400622b0aa2a9e3f..5bb192b8816e6c4fc7bff110424ff7ad83617d02 100644 (file)
@@ -92,6 +92,7 @@ const clusterLogin = async (clusterId: string, baseUrl: string, activeSession: S
             ownerUuid: user.owner_uuid,
             email: user.email,
             isAdmin: user.is_admin,
+            isActive: user.is_active,
             username: user.username,
             prefs: user.prefs
         },
index db3211c47440fe8290569e19488658660396a2bf..11f29e45d304cc4bf866b25b2b900e1c702cc942 100644 (file)
@@ -11,7 +11,10 @@ import {
     USER_LAST_NAME_KEY,
     USER_OWNER_UUID_KEY,
     USER_UUID_KEY,
-    USER_IS_ADMIN, USER_USERNAME, USER_PREFS
+    USER_IS_ADMIN,
+    USER_IS_ACTIVE,
+    USER_USERNAME,
+    USER_PREFS
 } from "~/services/auth-service/auth-service";
 
 import 'jest-localstorage-mock';
@@ -45,7 +48,8 @@ describe('auth-actions', () => {
         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"));
+        localStorage.setItem(USER_IS_ADMIN, JSON.stringify(false));
+        localStorage.setItem(USER_IS_ACTIVE, JSON.stringify(true));
 
         const config: any = {
             rootUrl: "https://zzzzz.arvadosapi.com",
@@ -93,7 +97,8 @@ describe('auth-actions', () => {
                 ownerUuid: "ownerUuid",
                 username: "username",
                 prefs: {},
-                isAdmin: false
+                isAdmin: false,
+                isActive: true
             }
         });
     });
index 166f6d308b81b6d27fe58fc8d9f4729f7f4e9202..38cf1581d3796dc5b89a4d7cfc897adc425271ce 100644 (file)
@@ -32,7 +32,8 @@ describe('auth-reducer', () => {
             ownerUuid: "ownerUuid",
             username: "username",
             prefs: {},
-            isAdmin: false
+            isAdmin: false,
+            isActive: true
         };
         const state = reducer(initialState, authActions.INIT({ user, token: "token" }));
         expect(state).toEqual({
@@ -72,7 +73,8 @@ describe('auth-reducer', () => {
             ownerUuid: "ownerUuid",
             username: "username",
             prefs: {},
-            isAdmin: false
+            isAdmin: false,
+            isActive: true
         };
 
         const state = reducer(initialState, authActions.USER_DETAILS_SUCCESS(user));
@@ -91,7 +93,8 @@ describe('auth-reducer', () => {
                 ownerUuid: "ownerUuid",
                 username: "username",
                 prefs: {},
-                isAdmin: false
+                isAdmin: false,
+                isActive: true
             }
         });
     });
index 16bb8c1347ede28d3dd4b7309f2d7cea493f11d8..3481814def7a8bde7690b6ff69df5d967a6e34ad 100644 (file)
@@ -68,12 +68,14 @@ export const AccountMenu = withStyles(styles)(
                     <MenuItem disabled>
                         {getUserFullname(user)} {user.uuid.substr(0, 5) !== localCluster && `(${user.uuid.substr(0, 5)})`}
                     </MenuItem>
-                    <MenuItem onClick={() => dispatch(openUserVirtualMachines())}>Virtual Machines</MenuItem>
-                    {!user.isAdmin && <MenuItem onClick={() => dispatch(openRepositoriesPanel())}>Repositories</MenuItem>}
-                    <MenuItem onClick={() => dispatch(openCurrentTokenDialog)}>Current token</MenuItem>
-                    <MenuItem onClick={() => dispatch(navigateToSshKeysUser)}>Ssh Keys</MenuItem>
-                    <MenuItem onClick={() => dispatch(navigateToSiteManager)}>Site Manager</MenuItem>
-                    <MenuItem onClick={() => dispatch(navigateToMyAccount)}>My account</MenuItem>
+                    {user.isActive ? <>
+                        <MenuItem onClick={() => dispatch(openUserVirtualMachines())}>Virtual Machines</MenuItem>
+                        {!user.isAdmin && <MenuItem onClick={() => dispatch(openRepositoriesPanel())}>Repositories</MenuItem>}
+                        <MenuItem onClick={() => dispatch(openCurrentTokenDialog)}>Current token</MenuItem>
+                        <MenuItem onClick={() => dispatch(navigateToSshKeysUser)}>Ssh Keys</MenuItem>
+                        <MenuItem onClick={() => dispatch(navigateToSiteManager)}>Site Manager</MenuItem>
+                        <MenuItem onClick={() => dispatch(navigateToMyAccount)}>My account</MenuItem>)
+                     </> : null}
                     <MenuItem>
                         <a href={`${workbenchURL.replace(/\/$/, "")}/${wb1URL(currentRoute)}?api_token=${apiToken}`}
                             className={classes.link}>
index d2a05837dfdaf634788080599f12daa065b822db..475b29e1254f1ffcb0cdc6d720e30074bc5c4515 100644 (file)
@@ -54,7 +54,7 @@ export const MainAppBar = withStyles(styles)(
                         xs={6}
                         container
                         alignItems="center">
-                        {props.user && <SearchBar />}
+                        {props.user && props.user.isActive && <SearchBar />}
                     </Grid>
                     <Grid
                         item
diff --git a/src/views/inactive-panel/inactive-panel.tsx b/src/views/inactive-panel/inactive-panel.tsx
new file mode 100644 (file)
index 0000000..abfa1f8
--- /dev/null
@@ -0,0 +1,78 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { connect, DispatchProp } from 'react-redux';
+import { Grid, Typography, Button, Select } from '@material-ui/core';
+import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
+import { login, authActions } from '~/store/auth/auth-action';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { RootState } from '~/store/store';
+import * as classNames from 'classnames';
+
+type CssRules = 'root' | 'container' | 'title' | 'content' | 'content__bolder' | 'button';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    root: {
+        position: 'relative',
+        backgroundColor: theme.palette.grey["200"],
+        '&::after': {
+            content: `''`,
+            position: 'absolute',
+            top: 0,
+            left: 0,
+            bottom: 0,
+            right: 0,
+            background: 'url("arvados-logo-big.png") no-repeat center center',
+            opacity: 0.2,
+        }
+    },
+    container: {
+        width: '560px',
+        zIndex: 10
+    },
+    title: {
+        marginBottom: theme.spacing.unit * 6,
+        color: theme.palette.grey["800"]
+    },
+    content: {
+        marginBottom: theme.spacing.unit * 3,
+        lineHeight: '1.2rem',
+        color: theme.palette.grey["800"]
+    },
+    'content__bolder': {
+        fontWeight: 'bolder'
+    },
+    button: {
+        boxShadow: 'none'
+    }
+});
+
+type LoginPanelProps = DispatchProp<any> & WithStyles<CssRules> & {
+    remoteHosts: { [key: string]: string },
+    homeCluster: string,
+    uuidPrefix: string
+};
+
+export const InactivePanel = withStyles(styles)(
+    connect((state: RootState) => ({
+        remoteHosts: state.auth.remoteHosts,
+        homeCluster: state.auth.homeCluster,
+        uuidPrefix: state.auth.localCluster
+    }))(({ classes, dispatch, remoteHosts, homeCluster, uuidPrefix }: LoginPanelProps) =>
+        <Grid container justify="center" alignItems="center"
+            className={classes.root}
+            style={{ marginTop: 56, overflowY: "auto", height: "100%" }}>
+            <Grid item className={classes.container}>
+                <Typography variant='h6' align="center" className={classes.title}>
+                    Hi! You're logged in, but...
+               </Typography>
+                <Typography>
+                    Your account is inactive.
+
+                   An administrator must activate your account before you can get any further.
+               </Typography>
+            </Grid>
+        </Grid >
+    ));
diff --git a/src/views/inactive-panel/inactive-panel.tsx~ b/src/views/inactive-panel/inactive-panel.tsx~
new file mode 100644 (file)
index 0000000..eac4034
--- /dev/null
@@ -0,0 +1,104 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { connect, DispatchProp } from 'react-redux';
+import { Grid, Typography, Button, Select, FormControl } from '@material-ui/core';
+import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
+import { login, authActions } from '~/store/auth/auth-action';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { RootState } from '~/store/store';
+import * as classNames from 'classnames';
+
+type CssRules = 'root' | 'container' | 'title' | 'content' | 'content__bolder' | 'button';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    root: {
+        position: 'relative',
+        backgroundColor: theme.palette.grey["200"],
+        '&::after': {
+            content: `''`,
+            position: 'absolute',
+            top: 0,
+            left: 0,
+            bottom: 0,
+            right: 0,
+            background: 'url("arvados-logo-big.png") no-repeat center center',
+            opacity: 0.2,
+        }
+    },
+    container: {
+        width: '560px',
+        zIndex: 10
+    },
+    title: {
+        marginBottom: theme.spacing.unit * 6,
+        color: theme.palette.grey["800"]
+    },
+    content: {
+        marginBottom: theme.spacing.unit * 3,
+        lineHeight: '1.2rem',
+        color: theme.palette.grey["800"]
+    },
+    'content__bolder': {
+        fontWeight: 'bolder'
+    },
+    button: {
+        boxShadow: 'none'
+    }
+});
+
+type LoginPanelProps = DispatchProp<any> & WithStyles<CssRules> & {
+    remoteHosts: { [key: string]: string },
+    homeCluster: string,
+    uuidPrefix: string
+};
+
+export const LoginPanel = withStyles(styles)(
+    connect((state: RootState) => ({
+        remoteHosts: state.auth.remoteHosts,
+        homeCluster: state.auth.homeCluster,
+        uuidPrefix: state.auth.localCluster
+    }))(({ classes, dispatch, remoteHosts, homeCluster, uuidPrefix }: LoginPanelProps) =>
+        <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
+               </Typography>
+                <Typography className={classes.content}>
+                    The "Log in" button below will show you a Google sign-in page.
+                    After you assure Google that you want to log in here with your Google account, you will be redirected back here to Arvados Workbench.
+               </Typography>
+                <Typography className={classes.content}>
+                    If you have never used Arvados Workbench before, logging in for the first time will automatically create a new account.
+               </Typography>
+                <Typography variant='body1' className={classNames(classes.content, classes.content__bolder)}>
+                    IMPORTANT: Please keep in mind to store exploratory data only but not any information used for clinical decision making.
+               </Typography>
+                <Typography className={classes.content}>
+                    Arvados Workbench uses your name and email address only for identification, and does not retrieve any other personal information from Google.
+               </Typography>
+
+                {Object.keys(remoteHosts).length > 1 &&
+                    <Typography component="div" align="right">
+                        <label>Please select the cluster that hosts your user account:</label>
+                        <Select native value={homeCluster} style={{ margin: "1em" }}
+                            onChange={(event) => dispatch(authActions.SET_HOME_CLUSTER(event.target.value))}>
+                            {Object.keys(remoteHosts).map((k) => <option key={k} value={k}>{k}</option>)}
+                        </Select>
+                    </Typography>}
+
+                <Typography component="div" align="right">
+                    <Button variant="contained" color="primary" style={{ margin: "1em" }} className={classes.button}
+                        onClick={() => dispatch(login(uuidPrefix, remoteHosts[homeCluster]))}>
+                        Log in to {uuidPrefix}
+                        {uuidPrefix !== homeCluster &&
+                            <span>&nbsp;with user from {homeCluster}</span>}
+                    </Button>
+                </Typography>
+            </Grid>
+        </Grid >
+    ));
index eac4034b43822c2395f54ebb371ae6e41766993a..b9f3194ab0b2aa12fc375a7ac5f966dc6630db1a 100644 (file)
@@ -63,7 +63,7 @@ export const LoginPanel = withStyles(styles)(
     }))(({ classes, dispatch, remoteHosts, homeCluster, uuidPrefix }: LoginPanelProps) =>
         <Grid container justify="center" alignItems="center"
             className={classes.root}
-            style={{ marginTop: 56, overflowY: "auto" }}>
+            style={{ marginTop: 56, overflowY: "auto", height: "100%" }}>
             <Grid item className={classes.container}>
                 <Typography variant='h6' align="center" className={classes.title}>
                     Welcome to the Arvados Workbench
index b96e3cc8db9814433abbd3cee0802f5666d7ea65..4c64b0b850bc7e2e39e5ef446af61ac5a2a66fa1 100644 (file)
@@ -8,6 +8,7 @@ import { User } from "~/models/user";
 import { ArvadosTheme } from '~/common/custom-theme';
 import { WorkbenchPanel } from '~/views/workbench/workbench';
 import { LoginPanel } from '~/views/login-panel/login-panel';
+import { InactivePanel } from '~/views/inactive-panel/inactive-panel';
 import { WorkbenchLoadingScreen } from '~/views/workbench/workbench-loading-screen';
 import { MainAppBar } from '~/views-components/main-app-bar/main-app-bar';
 
@@ -43,7 +44,7 @@ export const MainPanelRoot = withStyles(styles)(
                     {working ? <LinearProgress color="secondary" /> : null}
                 </MainAppBar>
                 <Grid container direction="column" className={classes.root}>
-                    {user ? <WorkbenchPanel /> : <LoginPanel />}
+                    {user ? (user.isActive ? <WorkbenchPanel /> : <InactivePanel />) : <LoginPanel />}
                 </Grid>
             </>
 );