1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 import React from 'react';
6 import { useState, useEffect, useRef } from 'react';
7 import { CustomStyleRulesCallback } from 'common/custom-theme';
8 import { WithStyles } from '@mui/styles';
9 import withStyles from '@mui/styles/withStyles';
10 import CircularProgress from '@mui/material/CircularProgress';
11 import { Button, Card, CardContent, TextField, CardActions } from '@mui/material';
12 import { green } from '@mui/material/colors';
13 import { AxiosPromise } from 'axios';
14 import { DispatchProp } from 'react-redux';
15 import { saveApiToken } from 'store/auth/auth-action';
16 import { navigateToRootProject } from 'store/navigation/navigation-action';
17 import { replace } from 'react-router-redux';
18 import { PasswordLoginResponse } from 'views/login-panel/login-panel';
20 type CssRules = 'root' | 'loginBtn' | 'card' | 'wrapper' | 'progress';
22 const styles: CustomStyleRulesCallback<CssRules> = theme => ({
27 margin: `${theme.spacing(1)} auto`
30 marginTop: theme.spacing(1),
34 marginTop: theme.spacing(1),
38 margin: theme.spacing(1),
51 type LoginFormProps = DispatchProp<any> & WithStyles<CssRules> & {
52 handleSubmit: (username: string, password: string) => AxiosPromise<PasswordLoginResponse>;
56 export const LoginForm = withStyles(styles)(
57 ({ handleSubmit, loginLabel, dispatch, classes }: LoginFormProps) => {
58 const userInput = useRef<HTMLInputElement>(null);
59 const [username, setUsername] = useState('');
60 const [password, setPassword] = useState('');
61 const [isButtonDisabled, setIsButtonDisabled] = useState(true);
62 const [isSubmitting, setSubmitting] = useState(false);
63 const [helperText, setHelperText] = useState('');
64 const [error, setError] = useState(false);
69 if (username.trim() && password.trim()) {
70 setIsButtonDisabled(false);
72 setIsButtonDisabled(true);
74 }, [username, password]);
76 // This only runs once after render.
81 const setFocus = () => {
82 userInput.current!.focus();
85 const handleLogin = () => {
89 handleSubmit(username, password)
92 if (response.data.uuid && response.data.api_token) {
93 const apiToken = `v2/${response.data.uuid}/${response.data.api_token}`;
94 const rd = new URL(window.location.href);
95 const rdUrl = rd.pathname + rd.search;
96 dispatch<any>(saveApiToken(apiToken)).finally(
98 if ((new URL(window.location.href).pathname) !== '/my-account') {
99 rdUrl === '/' ? dispatch(navigateToRootProject) : dispatch(replace(rdUrl))
105 setHelperText(response.data.message || 'Please try again');
111 setSubmitting(false);
112 setHelperText(`${(err.response && err.response.data && err.response.data.errors[0]) || 'Error logging in: ' + err}`);
117 const handleKeyPress = (e: any) => {
118 if (e.keyCode === 13 || e.which === 13) {
119 if (!isButtonDisabled) {
127 <form className={classes.root} noValidate autoComplete="off">
128 <Card className={classes.card}>
129 <div className={classes.wrapper}>
133 disabled={isSubmitting}
134 error={error} fullWidth id="username" type="email"
135 label="Username" margin="normal"
136 onChange={(e) => setUsername(e.target.value)}
137 onKeyPress={(e) => handleKeyPress(e)}
140 disabled={isSubmitting}
141 error={error} fullWidth id="password" type="password"
142 label="Password" margin="normal"
143 helperText={helperText}
144 onChange={(e) => setPassword(e.target.value)}
145 onKeyPress={(e) => handleKeyPress(e)}
149 <Button variant="contained" size="large" color="primary"
150 className={classes.loginBtn} onClick={() => handleLogin()}
151 disabled={isSubmitting || isButtonDisabled}>
152 {loginLabel || 'Log in'}
155 {isSubmitting && <CircularProgress color='secondary' className={classes.progress} />}