aac1364207420247b0e620b3ee3b2679cda9d08e
[arvados-workbench2.git] / src / views-components / login-form / login-form.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import React from 'react';
6 import { useState, useEffect, useRef } from 'react';
7 import { withStyles, WithStyles, StyleRulesCallback } from '@material-ui/core/styles';
8 import CircularProgress from '@material-ui/core/CircularProgress';
9 import { Button, Card, CardContent, TextField, CardActions } from '@material-ui/core';
10 import { green } from '@material-ui/core/colors';
11 import { AxiosPromise } from 'axios';
12 import { DispatchProp } from 'react-redux';
13 import { saveApiToken } from 'store/auth/auth-action';
14 import { navigateToRootProject } from 'store/navigation/navigation-action';
15
16 type CssRules = 'root' | 'loginBtn' | 'card' | 'wrapper' | 'progress';
17
18 const styles: StyleRulesCallback<CssRules> = theme => ({
19     root: {
20         display: 'flex',
21         flexWrap: 'wrap',
22         width: '100%',
23         margin: `${theme.spacing.unit} auto`
24     },
25     loginBtn: {
26         marginTop: theme.spacing.unit,
27         flexGrow: 1
28     },
29     card: {
30         marginTop: theme.spacing.unit,
31         width: '100%'
32     },
33     wrapper: {
34         margin: theme.spacing.unit,
35         position: 'relative',
36     },
37     progress: {
38         color: green[500],
39         position: 'absolute',
40         top: '50%',
41         left: '50%',
42         marginTop: -12,
43         marginLeft: -12,
44     },
45 });
46
47 type LoginFormProps = DispatchProp<any> & WithStyles<CssRules> & {
48     handleSubmit: (username: string, password: string) => AxiosPromise;
49     loginLabel?: string,
50 };
51
52 export const LoginForm = withStyles(styles)(
53     ({ handleSubmit, loginLabel, dispatch, classes }: LoginFormProps) => {
54         const userInput = useRef<HTMLInputElement>(null);
55         const [username, setUsername] = useState('');
56         const [password, setPassword] = useState('');
57         const [isButtonDisabled, setIsButtonDisabled] = useState(true);
58         const [isSubmitting, setSubmitting] = useState(false);
59         const [helperText, setHelperText] = useState('');
60         const [error, setError] = useState(false);
61
62         useEffect(() => {
63             setError(false);
64             setHelperText('');
65             if (username.trim() && password.trim()) {
66                 setIsButtonDisabled(false);
67             } else {
68                 setIsButtonDisabled(true);
69             }
70         }, [username, password]);
71
72         // This only runs once after render.
73         useEffect(() => {
74             setFocus();
75         }, []);
76
77         const setFocus = () => {
78             userInput.current!.focus();
79         };
80
81         const handleLogin = () => {
82             setError(false);
83             setHelperText('');
84             setSubmitting(true);
85             handleSubmit(username, password)
86             .then((response) => {
87                 setSubmitting(false);
88                 if (response.data.uuid && response.data.api_token) {
89                     const apiToken = `v2/${response.data.uuid}/${response.data.api_token}`;
90                     dispatch<any>(saveApiToken(apiToken)).finally(
91                         () => dispatch(navigateToRootProject));
92                 } else {
93                     setError(true);
94                     setHelperText(response.data.message || 'Please try again');
95                     setFocus();
96                 }
97             })
98             .catch((err) => {
99                 setError(true);
100                 setSubmitting(false);
101                 setHelperText(`${(err.response && err.response.data && err.response.data.errors[0]) || 'Error logging in: '+err}`);
102                 setFocus();
103             });
104         };
105
106         const handleKeyPress = (e: any) => {
107             if (e.keyCode === 13 || e.which === 13) {
108                 if (!isButtonDisabled) {
109                     handleLogin();
110                 }
111             }
112         };
113
114         return (
115             <React.Fragment>
116             <form className={classes.root} noValidate autoComplete="off">
117                 <Card className={classes.card}>
118                     <div className={classes.wrapper}>
119                     <CardContent>
120                         <TextField
121                             inputRef={userInput}
122                             disabled={isSubmitting}
123                             error={error} fullWidth id="username" type="email"
124                             label="Username" margin="normal"
125                             onChange={(e) => setUsername(e.target.value)}
126                             onKeyPress={(e) => handleKeyPress(e)}
127                         />
128                         <TextField
129                             disabled={isSubmitting}
130                             error={error} fullWidth id="password" type="password"
131                             label="Password" margin="normal"
132                             helperText={helperText}
133                             onChange={(e) => setPassword(e.target.value)}
134                             onKeyPress={(e) => handleKeyPress(e)}
135                         />
136                     </CardContent>
137                     <CardActions>
138                         <Button variant="contained" size="large" color="primary"
139                             className={classes.loginBtn} onClick={() => handleLogin()}
140                             disabled={isSubmitting || isButtonDisabled}>
141                             {loginLabel || 'Log in'}
142                         </Button>
143                     </CardActions>
144                     { isSubmitting && <CircularProgress color='secondary' className={classes.progress} />}
145                     </div>
146                 </Card>
147             </form>
148             </React.Fragment>
149         );
150     });