github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/src/views/login/loginPage.tsx (about) 1 // Copyright 2018 The Cockroach Authors. 2 // 3 // Use of this software is governed by the Business Source License 4 // included in the file licenses/BSL.txt. 5 // 6 // As of the Change Date specified in that file, in accordance with 7 // the Business Source License, use of this software will be governed 8 // by the Apache License, Version 2.0, included in the file 9 // licenses/APL.txt. 10 11 import classNames from "classnames"; 12 import React from "react"; 13 import Helmet from "react-helmet"; 14 import { connect } from "react-redux"; 15 import { RouteComponentProps, withRouter } from "react-router-dom"; 16 17 import { doLogin, LoginAPIState } from "src/redux/login"; 18 import { AdminUIState } from "src/redux/state"; 19 import * as docsURL from "src/util/docs"; 20 import { trustIcon } from "src/util/trust"; 21 import InfoBox from "src/views/shared/components/infoBox"; 22 23 import logo from "assets/crdb.png"; 24 import docsIcon from "!!raw-loader!assets/docs.svg"; 25 import "./loginPage.styl"; 26 27 interface LoginPageProps { 28 loginState: LoginAPIState; 29 handleLogin: (username: string, password: string) => Promise<void>; 30 } 31 32 interface LoginPageState { 33 username: string; 34 password: string; 35 } 36 37 class LoginPage extends React.Component<LoginPageProps & RouteComponentProps, LoginPageState> { 38 constructor(props: LoginPageProps & RouteComponentProps) { 39 super(props); 40 this.state = { 41 username: "", 42 password: "", 43 }; 44 // TODO(vilterp): focus username field on mount 45 } 46 47 handleUpdateUsername = (evt: React.FormEvent<{ value: string }>) => { 48 this.setState({ 49 username: evt.currentTarget.value, 50 }); 51 } 52 53 handleUpdatePassword = (evt: React.FormEvent<{ value: string }>) => { 54 this.setState({ 55 password: evt.currentTarget.value, 56 }); 57 } 58 59 handleSubmit = (evt: React.FormEvent<any>) => { 60 const { location, history, handleLogin} = this.props; 61 const { username, password } = this.state; 62 evt.preventDefault(); 63 64 handleLogin(username, password) 65 .then(() => { 66 const params = new URLSearchParams(location.search); 67 if (params.has("redirectTo")) { 68 history.push(params.get("redirectTo")); 69 } else { 70 history.push("/"); 71 } 72 }); 73 } 74 75 renderError() { 76 const { error } = this.props.loginState; 77 78 if (!error) { 79 return null; 80 } 81 82 let message = "Invalid username or password."; 83 if (error.message !== "Unauthorized") { 84 message = error.message; 85 } 86 return ( 87 <div className="login-page__error">Unable to log in: { message }</div> 88 ); 89 } 90 91 render() { 92 const inputClasses = classNames("input-text", { 93 "input-text--error": !!this.props.loginState.error, 94 }); 95 96 return ( 97 <div className="login-page"> 98 <Helmet title="Login" /> 99 <div className="content"> 100 <section className="section login-page__info"> 101 <img className="logo" alt="CockroachDB" src={logo} /> 102 <InfoBox> 103 <h4 className="login-note-box__heading">Note:</h4> 104 <p> 105 A user with a password is required to log in to the UI 106 on secure clusters. 107 </p> 108 <p className="login-note-box__blurb"> 109 Create a user with this SQL command: 110 </p> 111 <pre className="login-note-box__sql-command"> 112 <span className="sql-keyword">CREATE USER</span> 113 {" "}craig{" "} 114 <span className="sql-keyword">WITH PASSWORD</span> 115 {" "} 116 <span className="sql-string">'cockroach'</span> 117 <span className="sql-keyword">;</span> 118 </pre> 119 <p className="aside"> 120 <a href={docsURL.adminUILoginNoVersion} className="login-docs-link" target="_blank"> 121 <span className="login-docs-link__icon" dangerouslySetInnerHTML={trustIcon(docsIcon)} /> 122 <span className="login-docs-link__text">Read more about configuring login</span> 123 </a> 124 </p> 125 </InfoBox> 126 </section> 127 <section className="section login-page__form"> 128 <div className="form-container"> 129 <h1 className="base-heading heading">Log in to the Web UI</h1> 130 {this.renderError()} 131 <form onSubmit={this.handleSubmit} className="form-internal" method="post"> 132 <input 133 type="text" 134 name="username" 135 className={inputClasses} 136 onChange={this.handleUpdateUsername} 137 value={this.state.username} 138 placeholder="Username" 139 /> 140 <input 141 type="password" 142 name="password" 143 className={inputClasses} 144 onChange={this.handleUpdatePassword} 145 value={this.state.password} 146 placeholder="Password" 147 /> 148 <input 149 type="submit" 150 className="submit-button" 151 disabled={this.props.loginState.inProgress} 152 value={this.props.loginState.inProgress ? "Logging in..." : "Log In"} 153 /> 154 </form> 155 </div> 156 </section> 157 </div> 158 </div> 159 ); 160 } 161 } 162 163 // tslint:disable-next-line:variable-name 164 const LoginPageConnected = withRouter(connect( 165 (state: AdminUIState) => { 166 return { 167 loginState: state.login, 168 location: state.router.location, 169 }; 170 }, 171 (dispatch) => ({ 172 handleLogin: (username: string, password: string) => { 173 return dispatch(doLogin(username, password)); 174 }, 175 }), 176 )(LoginPage)); 177 178 export default LoginPageConnected;