mirror of https://github.com/grafana/grafana
LoginPage: New design (#23892)
* LoginPage: initial poc * wIP * Prgress * Start Forms migration * Fix layout and change password animation * Migrate style to emotion * Fix small things * Remove classes * Fix logo and title * Disable disabled button * Add custom fields and fix layout * Update flyin animation * Change animation timing * Update comment * Same styles for submit button * Update snapshot * Minor tweaks and made slogan random Co-authored-by: Tobias Skarhed <tobias.skarhed@gmail.com>pull/24254/head
parent
3487e518ab
commit
726009870b
@ -1,8 +1,8 @@ |
||||
import { ThemeContext, withTheme, useTheme, mockThemeContext } from './ThemeContext'; |
||||
import { ThemeContext, withTheme, useTheme, useStyles, mockThemeContext } from './ThemeContext'; |
||||
import { getTheme, mockTheme } from './getTheme'; |
||||
import { selectThemeVariant } from './selectThemeVariant'; |
||||
export { stylesFactory } from './stylesFactory'; |
||||
export { ThemeContext, withTheme, mockTheme, getTheme, selectThemeVariant, useTheme, mockThemeContext }; |
||||
export { ThemeContext, withTheme, mockTheme, getTheme, selectThemeVariant, useTheme, mockThemeContext, useStyles }; |
||||
|
||||
import * as styleMixins from './mixins'; |
||||
export { styleMixins }; |
||||
|
@ -1,42 +1,53 @@ |
||||
import React, { FC } from 'react'; |
||||
import { css, cx } from 'emotion'; |
||||
import { useTheme } from '@grafana/ui'; |
||||
|
||||
export interface BrandComponentProps { |
||||
className?: string; |
||||
children?: JSX.Element | JSX.Element[]; |
||||
} |
||||
|
||||
export const LoginLogo: FC<BrandComponentProps> = ({ className }) => { |
||||
const maxSize = css` |
||||
max-width: 150px; |
||||
`;
|
||||
|
||||
return ( |
||||
<> |
||||
<img className={cx(className, maxSize)} src="public/img/grafana_icon.svg" alt="Grafana" /> |
||||
<div className="logo-wordmark" /> |
||||
</> |
||||
); |
||||
const LoginLogo: FC<BrandComponentProps> = ({ className }) => { |
||||
return <img className={className} src="public/img/grafana_icon.svg" alt="Grafana" />; |
||||
}; |
||||
|
||||
export const LoginBackground: FC<BrandComponentProps> = ({ className, children }) => { |
||||
const LoginBackground: FC<BrandComponentProps> = ({ className, children }) => { |
||||
const theme = useTheme(); |
||||
const background = css` |
||||
background: url(public/img/heatmap_bg_test.svg); |
||||
background: url(public/img/login_background_${theme.isDark ? 'dark' : 'light'}.svg); |
||||
background-size: cover; |
||||
`;
|
||||
|
||||
return <div className={cx(background, className)}>{children}</div>; |
||||
}; |
||||
|
||||
export const MenuLogo: FC<BrandComponentProps> = ({ className }) => { |
||||
const MenuLogo: FC<BrandComponentProps> = ({ className }) => { |
||||
return <img className={className} src="public/img/grafana_icon.svg" alt="Grafana" />; |
||||
}; |
||||
|
||||
export const AppTitle = 'Grafana'; |
||||
const LoginBoxBackground = () => { |
||||
const theme = useTheme(); |
||||
return css` |
||||
background: ${theme.isLight ? 'rgba(6, 30, 200, 0.1 )' : 'rgba(18, 28, 41, 0.65)'}; |
||||
background-size: cover; |
||||
`;
|
||||
}; |
||||
|
||||
export class Branding { |
||||
static LoginLogo = LoginLogo; |
||||
static LoginBackground = LoginBackground; |
||||
static MenuLogo = MenuLogo; |
||||
static AppTitle = AppTitle; |
||||
static LoginBoxBackground = LoginBoxBackground; |
||||
static AppTitle = 'Grafana'; |
||||
static LoginTitle = 'Welcome to Grafana'; |
||||
static GetLoginSubTitle = () => { |
||||
const slogans = [ |
||||
"Don't get in the way of the data", |
||||
'Your single pane of glass', |
||||
'Built better together', |
||||
'Democratising data', |
||||
]; |
||||
const count = slogans.length; |
||||
return slogans[Math.floor(Math.random() * count)]; |
||||
}; |
||||
} |
||||
|
@ -1,138 +1,60 @@ |
||||
import React, { ChangeEvent, PureComponent, SyntheticEvent } from 'react'; |
||||
import { Tooltip } from '@grafana/ui'; |
||||
import { AppEvents } from '@grafana/data'; |
||||
|
||||
import appEvents from 'app/core/app_events'; |
||||
import React, { FC, SyntheticEvent } from 'react'; |
||||
import { Tooltip, Form, Field, Input, VerticalGroup, Button, LinkButton } from '@grafana/ui'; |
||||
import { selectors } from '@grafana/e2e-selectors'; |
||||
|
||||
import { submitButton } from './LoginForm'; |
||||
interface Props { |
||||
onSubmit: (pw: string) => void; |
||||
onSkip: Function; |
||||
focus?: boolean; |
||||
onSkip: (event?: SyntheticEvent) => void; |
||||
} |
||||
|
||||
interface State { |
||||
interface PasswordDTO { |
||||
newPassword: string; |
||||
confirmNew: string; |
||||
valid: boolean; |
||||
} |
||||
|
||||
export class ChangePassword extends PureComponent<Props, State> { |
||||
private userInput: HTMLInputElement; |
||||
constructor(props: Props) { |
||||
super(props); |
||||
this.state = { |
||||
newPassword: '', |
||||
confirmNew: '', |
||||
valid: false, |
||||
}; |
||||
} |
||||
|
||||
componentDidUpdate(prevProps: Props) { |
||||
if (!prevProps.focus && this.props.focus) { |
||||
this.focus(); |
||||
} |
||||
} |
||||
|
||||
focus() { |
||||
this.userInput.focus(); |
||||
} |
||||
|
||||
onSubmit = (e: SyntheticEvent) => { |
||||
e.preventDefault(); |
||||
|
||||
const { newPassword, valid } = this.state; |
||||
if (valid) { |
||||
this.props.onSubmit(newPassword); |
||||
} else { |
||||
appEvents.emit(AppEvents.alertWarning, ['New passwords do not match']); |
||||
} |
||||
}; |
||||
|
||||
onNewPasswordChange = (e: ChangeEvent<HTMLInputElement>) => { |
||||
this.setState({ |
||||
newPassword: e.target.value, |
||||
valid: this.validate('newPassword', e.target.value), |
||||
}); |
||||
}; |
||||
|
||||
onConfirmPasswordChange = (e: ChangeEvent<HTMLInputElement>) => { |
||||
this.setState({ |
||||
confirmNew: e.target.value, |
||||
valid: this.validate('confirmNew', e.target.value), |
||||
}); |
||||
}; |
||||
|
||||
onSkip = (e: SyntheticEvent) => { |
||||
this.props.onSkip(); |
||||
export const ChangePassword: FC<Props> = ({ onSubmit, onSkip }) => { |
||||
const submit = (passwords: PasswordDTO) => { |
||||
onSubmit(passwords.newPassword); |
||||
}; |
||||
|
||||
validate(changed: string, pw: string) { |
||||
if (changed === 'newPassword') { |
||||
return this.state.confirmNew === pw; |
||||
} else if (changed === 'confirmNew') { |
||||
return this.state.newPassword === pw; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
render() { |
||||
return ( |
||||
<div className="login-inner-box" id="change-password-view"> |
||||
<div className="text-left login-change-password-info"> |
||||
<h5>Change Password</h5> |
||||
Before you can get started with awesome dashboards we need you to make your account more secure by changing |
||||
your password. |
||||
<br /> |
||||
You can change your password again later. |
||||
</div> |
||||
<form className="login-form-group gf-form-group"> |
||||
<div className="login-form"> |
||||
<input |
||||
<Form onSubmit={submit}> |
||||
{({ errors, register, getValues }) => ( |
||||
<> |
||||
<Field label="New password" invalid={!!errors.newPassword} error={errors?.newPassword?.message}> |
||||
<Input |
||||
autoFocus |
||||
type="password" |
||||
id="newPassword" |
||||
name="newPassword" |
||||
className="gf-form-input login-form-input" |
||||
required |
||||
placeholder="New password" |
||||
onChange={this.onNewPasswordChange} |
||||
ref={input => { |
||||
this.userInput = input; |
||||
}} |
||||
ref={register({ |
||||
required: 'New password required', |
||||
})} |
||||
/> |
||||
</div> |
||||
<div className="login-form"> |
||||
<input |
||||
</Field> |
||||
<Field label="Confirm new password" invalid={!!errors.confirmNew} error={errors?.confirmNew?.message}> |
||||
<Input |
||||
type="password" |
||||
name="confirmNew" |
||||
className="gf-form-input login-form-input" |
||||
required |
||||
ng-model="command.confirmNew" |
||||
placeholder="Confirm new password" |
||||
onChange={this.onConfirmPasswordChange} |
||||
ref={register({ |
||||
required: 'Confirmed password is required', |
||||
validate: v => v === getValues().newPassword || 'Passwords must match!', |
||||
})} |
||||
/> |
||||
</div> |
||||
<div className="login-button-group login-button-group--right text-right"> |
||||
</Field> |
||||
<VerticalGroup> |
||||
<Button type="submit" className={submitButton}> |
||||
Submit |
||||
</Button> |
||||
<Tooltip |
||||
placement="bottom" |
||||
content="If you skip you will be prompted to change password next time you login." |
||||
placement="bottom" |
||||
> |
||||
<a className="btn btn-link" onClick={this.onSkip} aria-label={selectors.pages.Login.skip}> |
||||
<LinkButton variant="link" onClick={onSkip} aria-label={selectors.pages.Login.skip}> |
||||
Skip |
||||
</a> |
||||
</LinkButton> |
||||
</Tooltip> |
||||
|
||||
<button |
||||
type="submit" |
||||
className={`btn btn-large p-x-2 ${this.state.valid ? 'btn-primary' : 'btn-inverse'}`} |
||||
onClick={this.onSubmit} |
||||
disabled={!this.state.valid} |
||||
> |
||||
Save |
||||
</button> |
||||
</div> |
||||
</form> |
||||
</div> |
||||
</VerticalGroup> |
||||
</> |
||||
)} |
||||
</Form> |
||||
); |
||||
} |
||||
} |
||||
}; |
||||
|
@ -1,122 +1,69 @@ |
||||
import React, { ChangeEvent, PureComponent, SyntheticEvent } from 'react'; |
||||
import React, { FC } from 'react'; |
||||
import { selectors } from '@grafana/e2e-selectors'; |
||||
|
||||
import { FormModel } from './LoginCtrl'; |
||||
import { Button, Form, Input, Field } from '@grafana/ui'; |
||||
import { css } from 'emotion'; |
||||
|
||||
interface Props { |
||||
displayForgotPassword: boolean; |
||||
onChange?: (valid: boolean) => void; |
||||
onSubmit: (data: FormModel) => void; |
||||
isLoggingIn: boolean; |
||||
passwordHint: string; |
||||
loginHint: string; |
||||
} |
||||
|
||||
interface State { |
||||
user: string; |
||||
password: string; |
||||
email: string; |
||||
valid: boolean; |
||||
} |
||||
|
||||
export class LoginForm extends PureComponent<Props, State> { |
||||
private userInput: HTMLInputElement; |
||||
constructor(props: Props) { |
||||
super(props); |
||||
this.state = { |
||||
user: '', |
||||
password: '', |
||||
email: '', |
||||
valid: false, |
||||
}; |
||||
} |
||||
|
||||
componentDidMount() { |
||||
this.userInput.focus(); |
||||
} |
||||
onSubmit = (e: SyntheticEvent) => { |
||||
e.preventDefault(); |
||||
|
||||
const { user, password, email } = this.state; |
||||
if (this.state.valid) { |
||||
this.props.onSubmit({ user, password, email }); |
||||
} |
||||
}; |
||||
const forgottenPasswordStyles = css` |
||||
display: inline-block; |
||||
margin-top: 16px; |
||||
float: right; |
||||
`;
|
||||
|
||||
onChangePassword = (e: ChangeEvent<HTMLInputElement>) => { |
||||
this.setState({ |
||||
password: e.target.value, |
||||
valid: this.validate(this.state.user, e.target.value), |
||||
}); |
||||
}; |
||||
const wrapperStyles = css` |
||||
width: 100%; |
||||
padding-bottom: 16px; |
||||
`;
|
||||
|
||||
onChangeUsername = (e: ChangeEvent<HTMLInputElement>) => { |
||||
this.setState({ |
||||
user: e.target.value, |
||||
valid: this.validate(e.target.value, this.state.password), |
||||
}); |
||||
}; |
||||
export const submitButton = css` |
||||
justify-content: center; |
||||
width: 100%; |
||||
`;
|
||||
|
||||
validate(user: string, password: string) { |
||||
return user.length > 0 && password.length > 0; |
||||
} |
||||
|
||||
render() { |
||||
export const LoginForm: FC<Props> = ({ displayForgotPassword, onSubmit, isLoggingIn, passwordHint, loginHint }) => { |
||||
return ( |
||||
<form name="loginForm" className="login-form-group gf-form-group"> |
||||
<div className="login-form"> |
||||
<input |
||||
ref={input => { |
||||
this.userInput = input; |
||||
}} |
||||
type="text" |
||||
<div className={wrapperStyles}> |
||||
<Form onSubmit={onSubmit} validateOn="onChange"> |
||||
{({ register, errors }) => ( |
||||
<> |
||||
<Field label="Email or username" invalid={!!errors.user} error={errors.user?.message}> |
||||
<Input |
||||
autoFocus |
||||
name="user" |
||||
className="gf-form-input login-form-input" |
||||
required |
||||
placeholder={this.props.loginHint} |
||||
ref={register({ required: 'Email or username is required' })} |
||||
placeholder={loginHint} |
||||
aria-label={selectors.pages.Login.username} |
||||
onChange={this.onChangeUsername} |
||||
/> |
||||
</div> |
||||
<div className="login-form"> |
||||
<input |
||||
type="password" |
||||
</Field> |
||||
<Field label="Password" invalid={!!errors.password} error={errors.password?.message}> |
||||
<Input |
||||
name="password" |
||||
className="gf-form-input login-form-input" |
||||
required |
||||
ng-model="formModel.password" |
||||
id="inputPassword" |
||||
placeholder={this.props.passwordHint} |
||||
type="password" |
||||
placeholder={passwordHint} |
||||
ref={register({ required: 'Password is requireed' })} |
||||
aria-label={selectors.pages.Login.password} |
||||
onChange={this.onChangePassword} |
||||
/> |
||||
</div> |
||||
<div className="login-button-group"> |
||||
{!this.props.isLoggingIn ? ( |
||||
<button |
||||
type="submit" |
||||
aria-label={selectors.pages.Login.submit} |
||||
className={`btn btn-large p-x-2 ${this.state.valid ? 'btn-primary' : 'btn-inverse'}`} |
||||
onClick={this.onSubmit} |
||||
disabled={!this.state.valid} |
||||
> |
||||
Log In |
||||
</button> |
||||
) : ( |
||||
<button type="submit" disabled className="btn btn-large p-x-2 btn-inverse btn-loading"> |
||||
Logging In<span>.</span> |
||||
<span>.</span> |
||||
<span>.</span> |
||||
</button> |
||||
</Field> |
||||
<Button aria-label={selectors.pages.Login.submit} className={submitButton} disabled={isLoggingIn}> |
||||
{isLoggingIn ? 'Logging in...' : 'Log in'} |
||||
</Button> |
||||
{displayForgotPassword && ( |
||||
<a className={forgottenPasswordStyles} href="user/password/send-reset-email"> |
||||
Forgot your password? |
||||
</a> |
||||
)} |
||||
|
||||
{this.props.displayForgotPassword ? ( |
||||
<div className="small login-button-forgot-password"> |
||||
<a href="user/password/send-reset-email">Forgot your password?</a> |
||||
</div> |
||||
) : null} |
||||
</> |
||||
)} |
||||
</Form> |
||||
</div> |
||||
</form> |
||||
); |
||||
} |
||||
} |
||||
}; |
||||
|
@ -1,12 +1,13 @@ |
||||
import React, { FC } from 'react'; |
||||
import { LinkButton, HorizontalGroup } from '@grafana/ui'; |
||||
|
||||
export const UserSignup: FC<{}> = () => { |
||||
return ( |
||||
<div className="login-signup-box"> |
||||
<div className="login-signup-title p-r-1">New to Grafana?</div> |
||||
<a href="signup" className="btn btn-medium btn-signup btn-p-x-2"> |
||||
<HorizontalGroup justify="flex-start"> |
||||
<LinkButton href="signup" variant="secondary"> |
||||
Sign Up |
||||
</a> |
||||
</div> |
||||
</LinkButton> |
||||
<span>New to Grafana?</span> |
||||
</HorizontalGroup> |
||||
); |
||||
}; |
||||
|
After Width: | Height: | Size: 483 KiB |
After Width: | Height: | Size: 502 KiB |
Loading…
Reference in new issue