mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
ForgottenPassword: Move view to login screen (#25366)
* Add forgot password to login screen * Add ForgottenPassword component * Add spacing * Generate new emails and handle resetCode * Fix animation and small UX issues * Extract LoginLayout and add route for reset mail * Reset email template * Add ChangePasswordPage * Remove resetCode * Move style into variable * Fix strict null
This commit is contained in:
parent
1bde4de827
commit
f5de4f1fb1
@ -1,10 +1,10 @@
|
||||
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';
|
||||
import { submitButton } from '../Login/LoginForm';
|
||||
interface Props {
|
||||
onSubmit: (pw: string) => void;
|
||||
onSkip: (event?: SyntheticEvent) => void;
|
||||
onSkip?: (event?: SyntheticEvent) => void;
|
||||
}
|
||||
|
||||
interface PasswordDTO {
|
||||
@ -44,6 +44,8 @@ export const ChangePassword: FC<Props> = ({ onSubmit, onSkip }) => {
|
||||
<Button type="submit" className={submitButton}>
|
||||
Submit
|
||||
</Button>
|
||||
|
||||
{onSkip && (
|
||||
<Tooltip
|
||||
content="If you skip you will be prompted to change password next time you login."
|
||||
placement="bottom"
|
||||
@ -52,6 +54,7 @@ export const ChangePassword: FC<Props> = ({ onSubmit, onSkip }) => {
|
||||
Skip
|
||||
</LinkButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
</>
|
||||
)}
|
@ -0,0 +1,16 @@
|
||||
import React, { FC } from 'react';
|
||||
import { LoginLayout, InnerBox } from '../Login/LoginLayout';
|
||||
import { ChangePassword } from './ChangePassword';
|
||||
import LoginCtrl from '../Login/LoginCtrl';
|
||||
|
||||
export const ChangePasswordPage: FC = () => {
|
||||
return (
|
||||
<LoginLayout>
|
||||
<InnerBox>
|
||||
<LoginCtrl>{({ changePassword }) => <ChangePassword onSubmit={changePassword} />}</LoginCtrl>
|
||||
</InnerBox>
|
||||
</LoginLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangePasswordPage;
|
@ -0,0 +1,66 @@
|
||||
import React, { FC, useState } from 'react';
|
||||
import { Form, Field, Input, Button, Legend, Container, useStyles, HorizontalGroup, LinkButton } from '@grafana/ui';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { css } from 'emotion';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
|
||||
interface EmailDTO {
|
||||
userOrEmail: string;
|
||||
}
|
||||
|
||||
const paragraphStyles = (theme: GrafanaTheme) => css`
|
||||
color: ${theme.colors.formDescription};
|
||||
font-size: ${theme.typography.size.sm};
|
||||
font-weight: ${theme.typography.weight.regular};
|
||||
margin-top: ${theme.spacing.sm};
|
||||
display: block;
|
||||
`;
|
||||
|
||||
export const ForgottenPassword: FC = () => {
|
||||
const [emailSent, setEmailSent] = useState(false);
|
||||
const styles = useStyles(paragraphStyles);
|
||||
|
||||
const sendEmail = async (formModel: EmailDTO) => {
|
||||
const res = await getBackendSrv().post('/api/user/password/send-reset-email', formModel);
|
||||
if (res) {
|
||||
setEmailSent(true);
|
||||
}
|
||||
};
|
||||
|
||||
if (emailSent) {
|
||||
return (
|
||||
<div>
|
||||
<p>An email with a reset link has been sent to the email address. You should receive it shortly.</p>
|
||||
<Container margin="md" />
|
||||
<LinkButton variant="primary" href="/login">
|
||||
Back to login
|
||||
</LinkButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Form onSubmit={sendEmail}>
|
||||
{({ register, errors }) => (
|
||||
<>
|
||||
<Legend>Reset password</Legend>
|
||||
<Field
|
||||
label="User"
|
||||
description="Enter your informaton to get a reset link sent to you"
|
||||
invalid={!!errors.userOrEmail}
|
||||
error={errors?.userOrEmail?.message}
|
||||
>
|
||||
<Input placeholder="Email or username" name="userOrEmail" ref={register({ required: true })} />
|
||||
</Field>
|
||||
<HorizontalGroup>
|
||||
<Button>Send reset email</Button>
|
||||
<LinkButton variant="link" href="/login">
|
||||
Back to login
|
||||
</LinkButton>
|
||||
</HorizontalGroup>
|
||||
|
||||
<p className={styles}>Did you forget your username or email? Contact your Grafana administrator.</p>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
@ -0,0 +1,14 @@
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import { LoginLayout, InnerBox } from '../Login/LoginLayout';
|
||||
import { ForgottenPassword } from './ForgottenPassword';
|
||||
|
||||
export const SendResetMailPage: FC = () => (
|
||||
<LoginLayout>
|
||||
<InnerBox>
|
||||
<ForgottenPassword />
|
||||
</InnerBox>
|
||||
</LoginLayout>
|
||||
);
|
||||
|
||||
export default SendResetMailPage;
|
@ -63,12 +63,26 @@ export class LoginCtrl extends PureComponent<Props, State> {
|
||||
confirmNew: password,
|
||||
oldPassword: 'admin',
|
||||
};
|
||||
if (!this.props.routeParams.code) {
|
||||
getBackendSrv()
|
||||
.put('/api/user/password', pw)
|
||||
.then(() => {
|
||||
this.toGrafana();
|
||||
})
|
||||
.catch((err: any) => console.log(err));
|
||||
}
|
||||
|
||||
const resetModel = {
|
||||
code: this.props.routeParams.code,
|
||||
newPassword: password,
|
||||
confirmPassword: password,
|
||||
};
|
||||
|
||||
getBackendSrv()
|
||||
.post('/api/user/password/reset', resetModel)
|
||||
.then(() => {
|
||||
this.toGrafana();
|
||||
});
|
||||
};
|
||||
|
||||
login = (formModel: FormModel) => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { FC } from 'react';
|
||||
import React, { FC, ReactElement } from 'react';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { FormModel } from './LoginCtrl';
|
||||
@ -6,19 +6,13 @@ import { Button, Form, Input, Field } from '@grafana/ui';
|
||||
import { css } from 'emotion';
|
||||
|
||||
interface Props {
|
||||
displayForgotPassword: boolean;
|
||||
children: ReactElement;
|
||||
onSubmit: (data: FormModel) => void;
|
||||
isLoggingIn: boolean;
|
||||
passwordHint: string;
|
||||
loginHint: string;
|
||||
}
|
||||
|
||||
const forgottenPasswordStyles = css`
|
||||
display: inline-block;
|
||||
margin-top: 16px;
|
||||
float: right;
|
||||
`;
|
||||
|
||||
const wrapperStyles = css`
|
||||
width: 100%;
|
||||
padding-bottom: 16px;
|
||||
@ -29,7 +23,7 @@ export const submitButton = css`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const LoginForm: FC<Props> = ({ displayForgotPassword, onSubmit, isLoggingIn, passwordHint, loginHint }) => {
|
||||
export const LoginForm: FC<Props> = ({ children, onSubmit, isLoggingIn, passwordHint, loginHint }) => {
|
||||
return (
|
||||
<div className={wrapperStyles}>
|
||||
<Form onSubmit={onSubmit} validateOn="onChange">
|
||||
@ -56,11 +50,7 @@ export const LoginForm: FC<Props> = ({ displayForgotPassword, onSubmit, isLoggin
|
||||
<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>
|
||||
)}
|
||||
{children}
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
|
123
public/app/core/components/Login/LoginLayout.tsx
Normal file
123
public/app/core/components/Login/LoginLayout.tsx
Normal file
@ -0,0 +1,123 @@
|
||||
import React, { FC } from 'react';
|
||||
import { cx, css, keyframes } from 'emotion';
|
||||
import { useStyles } from '@grafana/ui';
|
||||
import { Branding } from '../Branding/Branding';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { Footer } from '../Footer/Footer';
|
||||
|
||||
interface InnerBoxProps {
|
||||
enterAnimation?: boolean;
|
||||
}
|
||||
export const InnerBox: FC<InnerBoxProps> = ({ children, enterAnimation = true }) => {
|
||||
const loginStyles = useStyles(getLoginStyles);
|
||||
return <div className={cx(loginStyles.loginInnerBox, enterAnimation && loginStyles.enterAnimation)}>{children}</div>;
|
||||
};
|
||||
|
||||
export const LoginLayout: FC = ({ children }) => {
|
||||
const loginStyles = useStyles(getLoginStyles);
|
||||
return (
|
||||
<Branding.LoginBackground className={loginStyles.container}>
|
||||
<div className={cx(loginStyles.loginContent, Branding.LoginBoxBackground())}>
|
||||
<div className={loginStyles.loginLogoWrapper}>
|
||||
<Branding.LoginLogo className={loginStyles.loginLogo} />
|
||||
<div className={loginStyles.titleWrapper}>
|
||||
<h1 className={loginStyles.mainTitle}>{Branding.LoginTitle}</h1>
|
||||
<h3 className={loginStyles.subTitle}>{Branding.GetLoginSubTitle()}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className={loginStyles.loginOuterBox}>{children}</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</Branding.LoginBackground>
|
||||
);
|
||||
};
|
||||
|
||||
const flyInAnimation = keyframes`
|
||||
from{
|
||||
opacity: 0;
|
||||
transform: translate(-60px, 0px);
|
||||
}
|
||||
|
||||
to{
|
||||
opacity: 1;
|
||||
transform: translate(0px, 0px);
|
||||
}`;
|
||||
|
||||
export const getLoginStyles = (theme: GrafanaTheme) => {
|
||||
return {
|
||||
container: css`
|
||||
min-height: 100vh;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
min-width: 100%;
|
||||
margin-left: 0;
|
||||
background-color: $black;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`,
|
||||
submitButton: css`
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
`,
|
||||
loginLogo: css`
|
||||
width: 100%;
|
||||
max-width: 100px;
|
||||
margin-bottom: 15px;
|
||||
`,
|
||||
loginLogoWrapper: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
padding: ${theme.spacing.lg};
|
||||
`,
|
||||
titleWrapper: css`
|
||||
text-align: center;
|
||||
`,
|
||||
mainTitle: css`
|
||||
font-size: '32px';
|
||||
`,
|
||||
subTitle: css`
|
||||
font-size: ${theme.typography.size.md};
|
||||
color: ${theme.colors.textSemiWeak};
|
||||
`,
|
||||
loginContent: css`
|
||||
max-width: 550px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
min-height: 320px;
|
||||
border-radius: 3px;
|
||||
padding: 20px 0;
|
||||
`,
|
||||
loginOuterBox: css`
|
||||
display: flex;
|
||||
overflow-y: hidden;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`,
|
||||
loginInnerBox: css`
|
||||
padding: ${theme.spacing.xl};
|
||||
@media (max-width: 320px) {
|
||||
padding: ${theme.spacing.lg};
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
max-width: 415px;
|
||||
width: 100%;
|
||||
transform: translate(0px, 0px);
|
||||
transition: 0.25s ease;
|
||||
`,
|
||||
enterAnimation: css`
|
||||
animation: ${flyInAnimation} ease-out 0.2s;
|
||||
`,
|
||||
};
|
||||
};
|
@ -1,31 +1,24 @@
|
||||
// Libraries
|
||||
import React, { FC } from 'react';
|
||||
import { cx, keyframes, css } from 'emotion';
|
||||
import { css } from 'emotion';
|
||||
|
||||
// Components
|
||||
import { UserSignup } from './UserSignup';
|
||||
import { LoginServiceButtons } from './LoginServiceButtons';
|
||||
import LoginCtrl from './LoginCtrl';
|
||||
import { LoginForm } from './LoginForm';
|
||||
import { ChangePassword } from './ChangePassword';
|
||||
import { Branding } from 'app/core/components/Branding/Branding';
|
||||
import { useStyles } from '@grafana/ui';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { Footer } from '../Footer/Footer';
|
||||
import { ChangePassword } from '../ForgottenPassword/ChangePassword';
|
||||
import { HorizontalGroup, LinkButton } from '@grafana/ui';
|
||||
import { LoginLayout, InnerBox } from './LoginLayout';
|
||||
|
||||
const forgottenPasswordStyles = css`
|
||||
padding: 0;
|
||||
margin-top: 4px;
|
||||
`;
|
||||
|
||||
export const LoginPage: FC = () => {
|
||||
document.title = Branding.AppTitle;
|
||||
const loginStyles = useStyles(getLoginStyles);
|
||||
return (
|
||||
<Branding.LoginBackground className={loginStyles.container}>
|
||||
<div className={cx(loginStyles.loginContent, Branding.LoginBoxBackground())}>
|
||||
<div className={loginStyles.loginLogoWrapper}>
|
||||
<Branding.LoginLogo className={loginStyles.loginLogo} />
|
||||
<div className={loginStyles.titleWrapper}>
|
||||
<h1 className={loginStyles.mainTitle}>{Branding.LoginTitle}</h1>
|
||||
<h3 className={loginStyles.subTitle}>{Branding.GetLoginSubTitle()}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<LoginLayout>
|
||||
<LoginCtrl>
|
||||
{({
|
||||
loginHint,
|
||||
@ -40,123 +33,45 @@ export const LoginPage: FC = () => {
|
||||
skipPasswordChange,
|
||||
isChangingPassword,
|
||||
}) => (
|
||||
<div className={loginStyles.loginOuterBox}>
|
||||
<>
|
||||
{!isChangingPassword && (
|
||||
<div className={`${loginStyles.loginInnerBox} ${isChangingPassword ? 'hidden' : ''}`} id="login-view">
|
||||
<InnerBox>
|
||||
{!disableLoginForm && (
|
||||
<>
|
||||
<LoginForm
|
||||
displayForgotPassword={!(ldapEnabled || authProxyEnabled)}
|
||||
onSubmit={login}
|
||||
loginHint={loginHint}
|
||||
passwordHint={passwordHint}
|
||||
isLoggingIn={isLoggingIn}
|
||||
/>
|
||||
>
|
||||
{!(ldapEnabled || authProxyEnabled) ? (
|
||||
<HorizontalGroup justify="flex-end">
|
||||
<LinkButton
|
||||
className={forgottenPasswordStyles}
|
||||
variant="link"
|
||||
href="/user/password/send-reset-email"
|
||||
>
|
||||
Forgot your password?
|
||||
</LinkButton>
|
||||
</HorizontalGroup>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</LoginForm>
|
||||
</>
|
||||
)}
|
||||
|
||||
<LoginServiceButtons />
|
||||
{!disableUserSignUp && <UserSignup />}
|
||||
</div>
|
||||
</InnerBox>
|
||||
)}
|
||||
|
||||
{isChangingPassword && (
|
||||
<div className={cx(loginStyles.loginInnerBox, loginStyles.enterAnimation)}>
|
||||
<ChangePassword onSubmit={changePassword} onSkip={skipPasswordChange as any} />
|
||||
</div>
|
||||
<InnerBox>
|
||||
<ChangePassword onSubmit={changePassword} onSkip={() => skipPasswordChange()} />
|
||||
</InnerBox>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</LoginCtrl>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</Branding.LoginBackground>
|
||||
</LoginLayout>
|
||||
);
|
||||
};
|
||||
|
||||
const flyInAnimation = keyframes`
|
||||
from{
|
||||
transform: translate(-400px, 0px);
|
||||
}
|
||||
|
||||
to{
|
||||
transform: translate(0px, 0px);
|
||||
}`;
|
||||
|
||||
export const getLoginStyles = (theme: GrafanaTheme) => {
|
||||
return {
|
||||
container: css`
|
||||
min-height: 100vh;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
min-width: 100%;
|
||||
margin-left: 0;
|
||||
background-color: $black;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`,
|
||||
submitButton: css`
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
`,
|
||||
loginLogo: css`
|
||||
width: 100%;
|
||||
max-width: 100px;
|
||||
margin-bottom: 15px;
|
||||
`,
|
||||
loginLogoWrapper: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
padding: ${theme.spacing.lg};
|
||||
`,
|
||||
titleWrapper: css`
|
||||
text-align: center;
|
||||
`,
|
||||
mainTitle: css`
|
||||
font-size: '32px';
|
||||
`,
|
||||
subTitle: css`
|
||||
font-size: ${theme.typography.size.md};
|
||||
color: ${theme.colors.textSemiWeak};
|
||||
`,
|
||||
loginContent: css`
|
||||
max-width: 550px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
min-height: 320px;
|
||||
border-radius: 3px;
|
||||
padding: 20px 0;
|
||||
`,
|
||||
loginOuterBox: css`
|
||||
display: flex;
|
||||
overflow-y: hidden;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`,
|
||||
loginInnerBox: css`
|
||||
padding: ${theme.spacing.xl};
|
||||
@media (max-width: 320px) {
|
||||
padding: ${theme.spacing.lg};
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
max-width: 415px;
|
||||
width: 100%;
|
||||
transform: translate(0px, 0px);
|
||||
transition: 0.25s ease;
|
||||
`,
|
||||
enterAnimation: css`
|
||||
animation: ${flyInAnimation} ease-out 0.2s;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
@ -1,13 +1,25 @@
|
||||
import React, { FC } from 'react';
|
||||
import { LinkButton, HorizontalGroup } from '@grafana/ui';
|
||||
import { LinkButton, VerticalGroup } from '@grafana/ui';
|
||||
import { css } from 'emotion';
|
||||
|
||||
export const UserSignup: FC<{}> = () => {
|
||||
return (
|
||||
<HorizontalGroup justify="flex-start">
|
||||
<LinkButton href="signup" variant="secondary">
|
||||
<VerticalGroup
|
||||
className={css`
|
||||
margin-top: 8px;
|
||||
`}
|
||||
>
|
||||
<span>New to Grafana?</span>
|
||||
<LinkButton
|
||||
className={css`
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
`}
|
||||
href="signup"
|
||||
variant="secondary"
|
||||
>
|
||||
Sign Up
|
||||
</LinkButton>
|
||||
<span>New to Grafana?</span>
|
||||
</HorizontalGroup>
|
||||
</VerticalGroup>
|
||||
);
|
||||
};
|
||||
|
@ -438,14 +438,28 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati
|
||||
},
|
||||
})
|
||||
.when('/user/password/send-reset-email', {
|
||||
templateUrl: 'public/app/partials/reset_password.html',
|
||||
controller: 'ResetPasswordCtrl',
|
||||
//@ts-ignore
|
||||
template: '<react-container />',
|
||||
resolve: {
|
||||
component: () =>
|
||||
SafeDynamicImport(
|
||||
import(
|
||||
/* webpackChunkName: "SendResetMailPage" */ 'app/core/components/ForgottenPassword/SendResetMailPage'
|
||||
)
|
||||
),
|
||||
},
|
||||
// @ts-ignore
|
||||
pageClass: 'sidemenu-hidden',
|
||||
})
|
||||
.when('/user/password/reset', {
|
||||
templateUrl: 'public/app/partials/reset_password.html',
|
||||
controller: 'ResetPasswordCtrl',
|
||||
template: '<react-container />',
|
||||
resolve: {
|
||||
component: () =>
|
||||
SafeDynamicImport(
|
||||
import(
|
||||
/* webpackChunkName: "ChangePasswordPage" */ 'app/core/components/ForgottenPassword/ChangePasswordPage'
|
||||
)
|
||||
),
|
||||
},
|
||||
//@ts-ignore
|
||||
pageClass: 'sidemenu-hidden',
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user