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