From f6c31c2e101b2182c1f7a7555442a698dd7ce7cc Mon Sep 17 00:00:00 2001 From: Kamal Galrani Date: Mon, 7 Sep 2020 20:54:46 +0530 Subject: [PATCH] Fixes signup workflow and UI (#26263) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fixes signup flow * Apply suggestions from code review Co-authored-by: Hugo Häggmark * Update ForgottenPassword.tsx * fixes build failure * fixes build failure Co-authored-by: Hugo Häggmark --- pkg/api/api.go | 1 + .../ForgottenPassword/ForgottenPassword.tsx | 6 +- .../app/core/components/Login/LoginPage.tsx | 1 - .../app/core/components/Login/UserSignup.tsx | 4 +- public/app/core/components/Signup/Signup.tsx | 126 ++++++++++++++++++ .../app/core/components/Signup/SignupPage.tsx | 15 +++ .../core/components/Signup/VerifyEmail.tsx | 62 +++++++++ .../components/Signup/VerifyEmailPage.tsx | 22 +++ .../app/features/profile/SignupForm.test.tsx | 18 --- public/app/features/profile/SignupForm.tsx | 114 ---------------- public/app/features/profile/SignupPage.tsx | 51 ------- public/app/routes/routes.ts | 23 +++- 12 files changed, 249 insertions(+), 194 deletions(-) create mode 100644 public/app/core/components/Signup/Signup.tsx create mode 100644 public/app/core/components/Signup/SignupPage.tsx create mode 100644 public/app/core/components/Signup/VerifyEmail.tsx create mode 100644 public/app/core/components/Signup/VerifyEmailPage.tsx delete mode 100644 public/app/features/profile/SignupForm.test.tsx delete mode 100644 public/app/features/profile/SignupForm.tsx delete mode 100644 public/app/features/profile/SignupPage.tsx diff --git a/pkg/api/api.go b/pkg/api/api.go index c73331ce84d..bd0a771f731 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -88,6 +88,7 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/alerting/*", reqEditorRole, hs.Index) // sign up + r.Get("/verify", hs.Index) r.Get("/signup", hs.Index) r.Get("/api/user/signup/options", Wrap(GetSignUpOptions)) r.Post("/api/user/signup", quota("user"), bind(dtos.SignUpForm{}), Wrap(SignUp)) diff --git a/public/app/core/components/ForgottenPassword/ForgottenPassword.tsx b/public/app/core/components/ForgottenPassword/ForgottenPassword.tsx index 51dce749063..88e067c6354 100644 --- a/public/app/core/components/ForgottenPassword/ForgottenPassword.tsx +++ b/public/app/core/components/ForgottenPassword/ForgottenPassword.tsx @@ -3,7 +3,6 @@ import { Form, Field, Input, Button, Legend, Container, useStyles, HorizontalGro import { getBackendSrv } from '@grafana/runtime'; import { css } from 'emotion'; import { GrafanaTheme } from '@grafana/data'; - import config from 'app/core/config'; interface EmailDTO { @@ -21,6 +20,7 @@ const paragraphStyles = (theme: GrafanaTheme) => css` export const ForgottenPassword: FC = () => { const [emailSent, setEmailSent] = useState(false); const styles = useStyles(paragraphStyles); + const loginHref = `${config.appSubUrl}/login`; const sendEmail = async (formModel: EmailDTO) => { const res = await getBackendSrv().post('/api/user/password/send-reset-email', formModel); @@ -34,7 +34,7 @@ export const ForgottenPassword: FC = () => {

An email with a reset link has been sent to the email address. You should receive it shortly.

- + Back to login
@@ -55,7 +55,7 @@ export const ForgottenPassword: FC = () => { - + Back to login diff --git a/public/app/core/components/Login/LoginPage.tsx b/public/app/core/components/Login/LoginPage.tsx index 7ae220a4d46..f5d90bb7645 100644 --- a/public/app/core/components/Login/LoginPage.tsx +++ b/public/app/core/components/Login/LoginPage.tsx @@ -11,7 +11,6 @@ import { ChangePassword } from '../ForgottenPassword/ChangePassword'; import { Branding } from 'app/core/components/Branding/Branding'; import { HorizontalGroup, LinkButton } from '@grafana/ui'; import { LoginLayout, InnerBox } from './LoginLayout'; - import config from 'app/core/config'; const forgottenPasswordStyles = css` diff --git a/public/app/core/components/Login/UserSignup.tsx b/public/app/core/components/Login/UserSignup.tsx index 8819f5ccdfb..7487ad1283d 100644 --- a/public/app/core/components/Login/UserSignup.tsx +++ b/public/app/core/components/Login/UserSignup.tsx @@ -1,8 +1,10 @@ import React, { FC } from 'react'; import { LinkButton, VerticalGroup } from '@grafana/ui'; import { css } from 'emotion'; +import { getConfig } from 'app/core/config'; export const UserSignup: FC<{}> = () => { + const href = getConfig().verifyEmailEnabled ? `${getConfig().appSubUrl}/verify` : `${getConfig().appSubUrl}/signup`; return ( = () => { width: 100%; justify-content: center; `} - href="signup" + href={href} variant="secondary" > Sign Up diff --git a/public/app/core/components/Signup/Signup.tsx b/public/app/core/components/Signup/Signup.tsx new file mode 100644 index 00000000000..91fc707e3bf --- /dev/null +++ b/public/app/core/components/Signup/Signup.tsx @@ -0,0 +1,126 @@ +import React, { FC } from 'react'; +import { connect, MapStateToProps } from 'react-redux'; +import { StoreState } from 'app/types'; +import { Form, Field, Input, Button, HorizontalGroup, LinkButton } from '@grafana/ui'; +import { getConfig } from 'app/core/config'; +import { getBackendSrv } from '@grafana/runtime'; +import appEvents from 'app/core/app_events'; +import { AppEvents } from '@grafana/data'; + +interface SignupDTO { + name: string; + email: string; + username: string; + orgName?: string; + password: string; + code: string; + confirm: string; +} + +interface ConnectedProps { + email?: string; + code?: string; +} + +const SignupUnconnected: FC = props => { + const onSubmit = async (formData: SignupDTO) => { + if (formData.name === '') { + delete formData.name; + } + delete formData.confirm; + + const response = await getBackendSrv() + .post('/api/user/signup/step2', { + email: formData.email, + code: formData.code, + username: formData.email, + orgName: formData.orgName, + password: formData.password, + name: formData.name, + }) + .catch(err => { + const msg = err.data?.message || err; + appEvents.emit(AppEvents.alertWarning, [msg]); + }); + + if (response.code === 'redirect-to-select-org') { + window.location.href = getConfig().appSubUrl + '/profile/select-org?signup=1'; + } + window.location.href = getConfig().appSubUrl + '/'; + }; + + const defaultValues = { + email: props.email, + code: props.code, + }; + + return ( +
+ {({ errors, register, getValues }) => ( + <> + + + + + + + {!getConfig().autoAssignOrg && ( + + + + )} + {getConfig().verifyEmailEnabled && ( + + + + )} + + + + + v === getValues().password || 'Passwords must match!', + })} + /> + + + + + + Back to login + + + + )} +
+ ); +}; + +const mapStateToProps: MapStateToProps = (state: StoreState) => ({ + email: state.location.routeParams.email?.toString(), + code: state.location.routeParams.code?.toString(), +}); + +export const Signup = connect(mapStateToProps)(SignupUnconnected); diff --git a/public/app/core/components/Signup/SignupPage.tsx b/public/app/core/components/Signup/SignupPage.tsx new file mode 100644 index 00000000000..1855b1a0d5d --- /dev/null +++ b/public/app/core/components/Signup/SignupPage.tsx @@ -0,0 +1,15 @@ +import React, { FC } from 'react'; +import { LoginLayout, InnerBox } from '../Login/LoginLayout'; +import { Signup } from './Signup'; + +export const SignupPage: FC = () => { + return ( + + + + + + ); +}; + +export default SignupPage; diff --git a/public/app/core/components/Signup/VerifyEmail.tsx b/public/app/core/components/Signup/VerifyEmail.tsx new file mode 100644 index 00000000000..d82b0213c4b --- /dev/null +++ b/public/app/core/components/Signup/VerifyEmail.tsx @@ -0,0 +1,62 @@ +import React, { FC, useState } from 'react'; +import { Form, Field, Input, Button, Legend, Container, HorizontalGroup, LinkButton } from '@grafana/ui'; +import { getConfig } from 'app/core/config'; +import { getBackendSrv } from '@grafana/runtime'; +import appEvents from 'app/core/app_events'; +import { AppEvents } from '@grafana/data'; + +interface EmailDTO { + email: string; +} + +export const VerifyEmail: FC = () => { + const [emailSent, setEmailSent] = useState(false); + + const onSubmit = (formModel: EmailDTO) => { + getBackendSrv() + .post('/api/user/signup', formModel) + .then(() => { + setEmailSent(true); + }) + .catch(err => { + const msg = err.data?.message || err; + appEvents.emit(AppEvents.alertWarning, [msg]); + }); + }; + + if (emailSent) { + return ( +
+

An email with a verification link has been sent to the email address. You should receive it shortly.

+ + + Complete Signup + +
+ ); + } + + return ( +
+ {({ register, errors }) => ( + <> + Verify Email + + + + + + + Back to login + + + + )} +
+ ); +}; diff --git a/public/app/core/components/Signup/VerifyEmailPage.tsx b/public/app/core/components/Signup/VerifyEmailPage.tsx new file mode 100644 index 00000000000..e1c024ea16d --- /dev/null +++ b/public/app/core/components/Signup/VerifyEmailPage.tsx @@ -0,0 +1,22 @@ +import React, { FC } from 'react'; + +import { LoginLayout, InnerBox } from '../Login/LoginLayout'; +import { VerifyEmail } from './VerifyEmail'; +import { getConfig } from 'app/core/config'; + +export const VerifyEmailPage: FC = () => { + if (!getConfig().verifyEmailEnabled) { + window.location.href = getConfig().appSubUrl + '/signup'; + return <>; + } + + return ( + + + + + + ); +}; + +export default VerifyEmailPage; diff --git a/public/app/features/profile/SignupForm.test.tsx b/public/app/features/profile/SignupForm.test.tsx deleted file mode 100644 index 4c3822b7147..00000000000 --- a/public/app/features/profile/SignupForm.test.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import { SignupForm } from './SignupForm'; - -describe('SignupForm', () => { - describe('With different values for verifyEmail and autoAssignOrg', () => { - it('should render input fields', () => { - const wrapper = shallow(); - expect(wrapper.exists('Forms.Input[name="orgName"]')); - expect(wrapper.exists('Forms.Input[name="code"]')); - }); - it('should not render input fields', () => { - const wrapper = shallow(); - expect(wrapper.exists('Forms.Input[name="orgName"]')).toBeFalsy(); - expect(wrapper.exists('Forms.Input[name="code"]')).toBeFalsy(); - }); - }); -}); diff --git a/public/app/features/profile/SignupForm.tsx b/public/app/features/profile/SignupForm.tsx deleted file mode 100644 index 5f83d9622ff..00000000000 --- a/public/app/features/profile/SignupForm.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import React, { FC } from 'react'; -import { Button, LinkButton, Input, Form, Field } from '@grafana/ui'; -import { css } from 'emotion'; - -import { getConfig } from 'app/core/config'; -import { getBackendSrv } from '@grafana/runtime'; - -interface SignupFormModel { - email: string; - username?: string; - password: string; - orgName: string; - code?: string; - name?: string; -} -interface Props { - email?: string; - orgName?: string; - username?: string; - code?: string; - name?: string; - verifyEmailEnabled?: boolean; - autoAssignOrg?: boolean; -} - -const buttonSpacing = css` - margin-left: 15px; -`; - -export const SignupForm: FC = props => { - const verifyEmailEnabled = props.verifyEmailEnabled; - const autoAssignOrg = props.autoAssignOrg; - - const onSubmit = async (formData: SignupFormModel) => { - if (formData.name === '') { - delete formData.name; - } - - const response = await getBackendSrv().post('/api/user/signup/step2', { - email: formData.email, - code: formData.code, - username: formData.email, - orgName: formData.orgName, - password: formData.password, - name: formData.name, - }); - - if (response.code === 'redirect-to-select-org') { - window.location.href = getConfig().appSubUrl + '/profile/select-org?signup=1'; - } - window.location.href = getConfig().appSubUrl + '/'; - }; - - const defaultValues = { - orgName: props.orgName, - email: props.email, - username: props.email, - code: props.code, - name: props.name, - }; - - return ( -
- {({ register, errors }) => { - return ( - <> - {verifyEmailEnabled && ( - - - - )} - {!autoAssignOrg && ( - - - - )} - - - - - - - - - - - - - - Back - - - - ); - }} -
- ); -}; diff --git a/public/app/features/profile/SignupPage.tsx b/public/app/features/profile/SignupPage.tsx deleted file mode 100644 index 50554188f28..00000000000 --- a/public/app/features/profile/SignupPage.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React, { FC } from 'react'; -import { SignupForm } from './SignupForm'; -import Page from 'app/core/components/Page/Page'; -import { getConfig } from 'app/core/config'; -import { connect } from 'react-redux'; -import { hot } from 'react-hot-loader'; -import { StoreState } from 'app/types'; - -const navModel = { - main: { - icon: 'grafana', - text: 'Sign Up', - subTitle: 'Register your Grafana account', - breadcrumbs: [{ title: 'Login', url: 'login' }], - }, - node: { - text: '', - }, -}; - -interface Props { - email?: string; - orgName?: string; - username?: string; - code?: string; - name?: string; -} -export const SignupPage: FC = props => { - return ( - - -

You're almost there.

-
- We just need a couple of more bits of -
information to finish creating your account. -
- -
-
- ); -}; - -const mapStateToProps = (state: StoreState) => ({ - ...state.location.routeParams, -}); - -export default hot(module)(connect(mapStateToProps)(SignupPage)); diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index c874fe3d12a..185cd3882b6 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -4,7 +4,6 @@ import { applyRouteRegistrationHandlers } from './registry'; // Pages import LdapPage from 'app/features/admin/ldap/LdapPage'; import UserAdminPage from 'app/features/admin/UserAdminPage'; -import SignupPage from 'app/features/profile/SignupPage'; import { LoginPage } from 'app/core/components/Login/LoginPage'; import config from 'app/core/config'; @@ -436,13 +435,25 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati SafeDynamicImport(import(/* webpackChunkName: "SignupInvited" */ 'app/features/users/SignupInvited')), }, }) - .when('/signup', { - template: '', - //@ts-ignore - pageClass: 'sidemenu-hidden', + .when('/verify', { + template: '', resolve: { - component: () => SignupPage, + component: () => + SafeDynamicImport( + import(/* webpackChunkName: "VerifyEmailPage" */ 'app/core/components/Signup/VerifyEmailPage') + ), }, + // @ts-ignore + pageClass: 'login-page sidemenu-hidden', + }) + .when('/signup', { + template: '', + resolve: { + component: () => + SafeDynamicImport(import(/* webpackChunkName: "SignupPage" */ 'app/core/components/Signup/SignupPage')), + }, + // @ts-ignore + pageClass: 'login-page sidemenu-hidden', }) .when('/user/password/send-reset-email', { template: '',