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
pull/25772/head
Tobias Skarhed 5 years ago committed by GitHub
parent 1bde4de827
commit f5de4f1fb1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 23
      public/app/core/components/ForgottenPassword/ChangePassword.tsx
  2. 16
      public/app/core/components/ForgottenPassword/ChangePasswordPage.tsx
  3. 66
      public/app/core/components/ForgottenPassword/ForgottenPassword.tsx
  4. 14
      public/app/core/components/ForgottenPassword/SendResetMailPage.tsx
  5. 20
      public/app/core/components/Login/LoginCtrl.tsx
  6. 18
      public/app/core/components/Login/LoginForm.tsx
  7. 123
      public/app/core/components/Login/LoginLayout.tsx
  8. 203
      public/app/core/components/Login/LoginPage.tsx
  9. 22
      public/app/core/components/Login/UserSignup.tsx
  10. 24
      public/app/routes/routes.ts

@ -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,14 +44,17 @@ export const ChangePassword: FC<Props> = ({ onSubmit, onSkip }) => {
<Button type="submit" className={submitButton}>
Submit
</Button>
<Tooltip
content="If you skip you will be prompted to change password next time you login."
placement="bottom"
>
<LinkButton variant="link" onClick={onSkip} aria-label={selectors.pages.Login.skip}>
Skip
</LinkButton>
</Tooltip>
{onSkip && (
<Tooltip
content="If you skip you will be prompted to change password next time you login."
placement="bottom"
>
<LinkButton variant="link" onClick={onSkip} aria-label={selectors.pages.Login.skip}>
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()
.put('/api/user/password', pw)
.post('/api/user/password/reset', resetModel)
.then(() => {
this.toGrafana();
})
.catch((err: any) => console.log(err));
});
};
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>

@ -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,162 +1,77 @@
// 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>
<LoginCtrl>
{({
loginHint,
passwordHint,
ldapEnabled,
authProxyEnabled,
disableLoginForm,
disableUserSignUp,
login,
isLoggingIn,
changePassword,
skipPasswordChange,
isChangingPassword,
}) => (
<div className={loginStyles.loginOuterBox}>
{!isChangingPassword && (
<div className={`${loginStyles.loginInnerBox} ${isChangingPassword ? 'hidden' : ''}`} id="login-view">
{!disableLoginForm && (
<LoginLayout>
<LoginCtrl>
{({
loginHint,
passwordHint,
ldapEnabled,
authProxyEnabled,
disableLoginForm,
disableUserSignUp,
login,
isLoggingIn,
changePassword,
skipPasswordChange,
isChangingPassword,
}) => (
<>
{!isChangingPassword && (
<InnerBox>
{!disableLoginForm && (
<>
<LoginForm
displayForgotPassword={!(ldapEnabled || authProxyEnabled)}
onSubmit={login}
loginHint={loginHint}
passwordHint={passwordHint}
isLoggingIn={isLoggingIn}
/>
)}
<LoginServiceButtons />
{!disableUserSignUp && <UserSignup />}
</div>
)}
{isChangingPassword && (
<div className={cx(loginStyles.loginInnerBox, loginStyles.enterAnimation)}>
<ChangePassword onSubmit={changePassword} onSkip={skipPasswordChange as any} />
</div>
)}
</div>
)}
</LoginCtrl>
</div>
<Footer />
</Branding.LoginBackground>
>
{!(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 />}
</InnerBox>
)}
{isChangingPassword && (
<InnerBox>
<ChangePassword onSubmit={changePassword} onSkip={() => skipPasswordChange()} />
</InnerBox>
)}
</>
)}
</LoginCtrl>
</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…
Cancel
Save