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