Commit 0c4dae32 by Tobias Skarhed Committed by GitHub

Migrations: Signup page (#21514)

* Start Angular migration

* Add SignupCtrl

* Change name signup

* Add backend call

* Put form in separate file

* Add form model

* Start using react-hook-forms

* Add FormModel to state

* Reduxify

* Connect nav with Redux

* Fix routing and navModel

* Fetch state options on mount

* Add default values and add button margin

* Add errror messages

* Fix title

* Remove files and cleanup

* Add Signup tests

* Add boot config assingnAutoOrg and verifyEmailEnabled

* Remove onmount call

* Remove ctrl and move everything to SignupForm

* Make routeParams optional for testing

* Remove name if it is empty

* Set username

* Make function component

* Fix subpath issues and add link button

* Move redux to SignupPage
parent 029a6c64
...@@ -115,6 +115,7 @@ ...@@ -115,6 +115,7 @@
"mocha": "7.0.1", "mocha": "7.0.1",
"module-alias": "2.2.0", "module-alias": "2.2.0",
"monaco-editor": "0.15.6", "monaco-editor": "0.15.6",
"mutationobserver-shim": "0.3.3",
"ngtemplate-loader": "2.0.1", "ngtemplate-loader": "2.0.1",
"node-sass": "4.13.1", "node-sass": "4.13.1",
"npm": "6.13.4", "npm": "6.13.4",
......
...@@ -49,6 +49,8 @@ export class GrafanaBootConfig { ...@@ -49,6 +49,8 @@ export class GrafanaBootConfig {
exploreEnabled = false; exploreEnabled = false;
ldapEnabled = false; ldapEnabled = false;
samlEnabled = false; samlEnabled = false;
autoAssignOrg = true;
verifyEmailEnabled = false;
oauth: any; oauth: any;
disableUserSignUp = false; disableUserSignUp = false;
loginHint: any; loginHint: any;
......
...@@ -179,6 +179,8 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interf ...@@ -179,6 +179,8 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interf
"alertingErrorOrTimeout": setting.AlertingErrorOrTimeout, "alertingErrorOrTimeout": setting.AlertingErrorOrTimeout,
"alertingNoDataOrNullValues": setting.AlertingNoDataOrNullValues, "alertingNoDataOrNullValues": setting.AlertingNoDataOrNullValues,
"alertingMinInterval": setting.AlertingMinInterval, "alertingMinInterval": setting.AlertingMinInterval,
"autoAssignOrg": setting.AutoAssignOrg,
"verfiyEmailEnabled": setting.VerifyEmailEnabled,
"exploreEnabled": setting.ExploreEnabled, "exploreEnabled": setting.ExploreEnabled,
"googleAnalyticsId": setting.GoogleAnalyticsId, "googleAnalyticsId": setting.GoogleAnalyticsId,
"disableLoginForm": setting.DisableLoginForm, "disableLoginForm": setting.DisableLoginForm,
......
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(<SignupForm verifyEmailEnabled={true} autoAssignOrg={false} />);
expect(wrapper.exists('Forms.Input[name="orgName"]'));
expect(wrapper.exists('Forms.Input[name="code"]'));
});
it('should not render input fields', () => {
const wrapper = shallow(<SignupForm verifyEmailEnabled={false} autoAssignOrg={true} />);
expect(wrapper.exists('Forms.Input[name="orgName"]')).toBeFalsy();
expect(wrapper.exists('Forms.Input[name="code"]')).toBeFalsy();
});
});
});
import React, { FC } from 'react';
import { Forms } 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> = 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 (
<Forms.Form defaultValues={defaultValues} onSubmit={onSubmit}>
{({ register, errors }) => {
return (
<>
{verifyEmailEnabled && (
<Forms.Field label="Email verification code (sent to your email)">
<Forms.Input name="code" size="md" ref={register} placeholder="Code" />
</Forms.Field>
)}
{!autoAssignOrg && (
<Forms.Field label="Org. name">
<Forms.Input size="md" name="orgName" placeholder="Org. name" ref={register} />
</Forms.Field>
)}
<Forms.Field label="Your name">
<Forms.Input size="md" name="name" placeholder="(optional)" ref={register} />
</Forms.Field>
<Forms.Field label="Email" invalid={!!errors.email} error={!!errors.email && errors.email.message}>
<Forms.Input
size="md"
name="email"
type="email"
placeholder="Email"
ref={register({
required: 'Email is required',
pattern: {
value: /^\S+@\S+$/,
message: 'Email is invalid',
},
})}
/>
</Forms.Field>
<Forms.Field
label="Password"
invalid={!!errors.password}
error={!!errors.password && errors.password.message}
>
<Forms.Input
size="md"
name="password"
type="password"
placeholder="Password"
ref={register({ required: 'Password is required' })}
/>
</Forms.Field>
<Forms.Button type="submit">Submit</Forms.Button>
<span className={buttonSpacing}>
<Forms.LinkButton href={getConfig().appSubUrl + '/login'} variant="secondary">
Back
</Forms.LinkButton>
</span>
</>
);
}}
</Forms.Form>
);
};
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: 'gicon gicon-branding',
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> = props => {
return (
<Page navModel={navModel}>
<Page.Contents>
<h3 className="p-b-1">You're almost there.</h3>
<div className="p-b-1">
We just need a couple of more bits of
<br /> information to finish creating your account.
</div>
<SignupForm
{...props}
verifyEmailEnabled={getConfig().verifyEmailEnabled}
autoAssignOrg={getConfig().autoAssignOrg}
/>
</Page.Contents>
</Page>
);
};
const mapStateToProps = (state: StoreState) => ({
...state.location.routeParams,
});
export default hot(module)(connect(mapStateToProps)(SignupPage));
<page-header model="navModel"></page-header>
<div class="page-container page-body">
<div class="signup">
<h3 class="p-b-1">You're almost there.</h3>
<div class="p-b-1">
We just need a couple of more bits of<br> information to finish creating your account.
</div>
<form name="signUpForm" class="login-form gf-form-group">
<div class="gf-form" ng-if="verifyEmailEnabled">
<span class="gf-form-label width-9">
Email code<tip>Email verification code (sent to your email)</tip>
</span>
<input type="text" class="gf-form-input max-width-14" ng-model="formModel.code" required></input>
</div>
<div class="gf-form" ng-if="!autoAssignOrg">
<span class="gf-form-label width-9">Org. name</span>
<input type="text" name="orgName" class="gf-form-input max-width-14" ng-model="formModel.orgName" placeholder="Name your organization">
</div>
<div class="gf-form">
<span class="gf-form-label width-9">Your name</span>
<input type="text" name="name" class="gf-form-input max-width-14" ng-model="formModel.name" placeholder="(optional)">
</div>
<div class="gf-form">
<span class="gf-form-label width-9">Email</span>
<input type="text" class="gf-form-input max-width-14" required ng-model="formModel.username" placeholder="Email" autocomplete="off">
</div>
<div class="gf-form">
<span class="gf-form-label width-9">Password</span>
<input type="password" class="gf-form-input max-width-14" required ng-model="formModel.password" id="inputPassword" placeholder="password" autocomplete="off">
</div>
<div class="gf-form-button-row p-t-3">
<button type="submit" class="btn btn-primary" ng-click="ctrl.submit();" ng-disabled="!signUpForm.$valid">
Sign Up
</button>
<a href="login" class="btn btn-inverse">
Back
</a>
</div>
</form>
</div>
</div>
<footer />
...@@ -7,6 +7,8 @@ import FolderDashboardsCtrl from 'app/features/folders/FolderDashboardsCtrl'; ...@@ -7,6 +7,8 @@ import FolderDashboardsCtrl from 'app/features/folders/FolderDashboardsCtrl';
import DashboardImportCtrl from 'app/features/manage-dashboards/DashboardImportCtrl'; import DashboardImportCtrl from 'app/features/manage-dashboards/DashboardImportCtrl';
import LdapPage from 'app/features/admin/ldap/LdapPage'; import LdapPage from 'app/features/admin/ldap/LdapPage';
import UserAdminPage from 'app/features/admin/UserAdminPage'; import UserAdminPage from 'app/features/admin/UserAdminPage';
import SignupPage from 'app/features/profile/SignupPage';
import config from 'app/core/config'; import config from 'app/core/config';
import { ILocationProvider, route } from 'angular'; import { ILocationProvider, route } from 'angular';
// Types // Types
...@@ -351,8 +353,10 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati ...@@ -351,8 +353,10 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati
pageClass: 'sidemenu-hidden', pageClass: 'sidemenu-hidden',
}) })
.when('/signup', { .when('/signup', {
templateUrl: 'public/app/partials/signup_step2.html', template: '<react-container/>',
controller: 'SignUpCtrl', resolve: {
component: () => SignupPage,
},
pageClass: 'sidemenu-hidden', pageClass: 'sidemenu-hidden',
}) })
.when('/user/password/send-reset-email', { .when('/user/password/send-reset-email', {
......
...@@ -2,6 +2,7 @@ import { configure } from 'enzyme'; ...@@ -2,6 +2,7 @@ import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16'; import Adapter from 'enzyme-adapter-react-16';
import 'jquery'; import 'jquery';
import $ from 'jquery'; import $ from 'jquery';
import 'mutationobserver-shim';
const global = window as any; const global = window as any;
global.$ = global.jQuery = $; global.$ = global.jQuery = $;
......
...@@ -16824,6 +16824,11 @@ multimatch@^4.0.0: ...@@ -16824,6 +16824,11 @@ multimatch@^4.0.0:
arrify "^2.0.1" arrify "^2.0.1"
minimatch "^3.0.4" minimatch "^3.0.4"
mutationobserver-shim@0.3.3:
version "0.3.3"
resolved "https://registry.yarnpkg.com/mutationobserver-shim/-/mutationobserver-shim-0.3.3.tgz#65869630bc89d7bf8c9cd9cb82188cd955aacd2b"
integrity sha512-gciOLNN8Vsf7YzcqRjKzlAJ6y7e+B86u7i3KXes0xfxx/nfLmozlW1Vn+Sc9x3tPIePFgc1AeIFhtRgkqTjzDQ==
mute-stream@0.0.5: mute-stream@0.0.5:
version "0.0.5" version "0.0.5"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0"
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment