Styling User Registration (#529)

This commit is contained in:
Kyle Knight 2016-12-01 12:57:19 -06:00 committed by Jason Meller
parent e1db2d4c27
commit 9e6a8eae56
33 changed files with 864 additions and 188 deletions

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 174 48"><defs><style>.cls-1{fill:#995ecc;}.cls-1,.cls-2,.cls-3,.cls-4,.cls-5{fill-rule:evenodd;}.cls-2{fill:#fff;}.cls-3{fill:#c482f9;}.cls-4{fill:#ae6ddf;}.cls-5{fill:#66696f;}</style></defs><title>Asset 3</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><g id="light-facet-logo"><path class="cls-1" d="M22.72.87.87,22.72A3,3,0,0,0,.1,25.6L3.37,37.8A3,3,0,0,0,5.48,39.9l29.85,8a3,3,0,0,0,2.88-.77l8.93-8.93a3,3,0,0,0,.77-2.88l-8-29.85A3,3,0,0,0,37.8,3.37L25.6.1a3,3,0,0,0-2.88.77"/><polygon class="cls-2" points="25.19 29.34 20.83 29.35 24.39 31.24 24.41 39.05 17.7 39.05 17.67 20.56 13.33 18.25 13.33 15.71 24.37 15.71 24.39 24.07 25.92 24.07 31.21 15.71 39.02 15.71 31.88 27.39 31.86 27.39 39.02 39.05 31.21 39.05 25.19 29.34"/><polygon class="cls-1" points="24.37 15.71 24.37 0.04 31.21 15.71 25.92 24.07 24.38 24.07 24.37 15.71"/><polygon class="cls-3" points="39.02 15.71 39.02 4.04 31.21 15.71 39.02 15.71"/><polygon class="cls-3" points="31.86 27.39 39.02 39.05 48 36.66 31.86 27.39"/><polygon class="cls-4" points="39.02 39.05 31.21 39.05 36.55 47.96 39.02 39.05"/><polygon class="cls-3" points="17.67 20.56 17.7 39.05 4.06 39.05 17.67 20.56"/><polygon class="cls-1" points="17.67 20.56 13.33 18.25 4.06 39.05 17.67 20.56"/><polygon class="cls-4" points="24.37 0.04 24.37 15.71 13.33 15.71 24.37 0.04"/><path class="cls-4" d="M47.9,35.32l-8-29.85A3,3,0,0,0,39,4V15.71L48,36.56a3,3,0,0,0-.06-1.24"/><path class="cls-4" d="M31.21,15.71,39,4a3,3,0,0,0-1.22-.67L25.6.1A3,3,0,0,0,24.37,0Z"/><path class="cls-4" d="M4.06,39l9.27-20.8V15.71L0,24.49A3,3,0,0,0,.1,25.6L3.37,37.8A3,3,0,0,0,4.06,39"/><path class="cls-4" d="M24.41,39H4.07a3,3,0,0,0,1.41.85l29.85,8a3,3,0,0,0,1.23.06Z"/><path class="cls-1" d="M39,39,36.55,48a3,3,0,0,0,1.64-.83l8.93-8.93A3,3,0,0,0,48,36.57Z"/></g><g id="kolide-text"><path class="cls-5" d="M141.58,15.87h-7.9a1.38,1.38,0,0,0-1.58,1.54v20A1.38,1.38,0,0,0,133.67,39h7.9q9.49,0,9.49-4.63V20.49Q151.06,15.86,141.58,15.87Zm4.74,17q0,1.54-4.74,1.54h-3.16a1.38,1.38,0,0,1-1.58-1.54V22.19a1.38,1.38,0,0,1,1.58-1.54h3.16q4.74,0,4.74,1.54Z"/><path class="cls-5" d="M79.89,35.18l-8-7.78,7.9-7.71h0v0a1.34,1.34,0,0,0,0-2.16L78.7,16.43a1.41,1.41,0,0,0-2.23,0l-8.08,7.89a2.86,2.86,0,0,1-2,.8h-.07a1.38,1.38,0,0,1-1.58-1.54V17.41a1.38,1.38,0,0,0-1.58-1.54H61.58A1.38,1.38,0,0,0,60,17.41v20A1.38,1.38,0,0,0,61.58,39h1.58a1.38,1.38,0,0,0,1.58-1.54V31.29a1.38,1.38,0,0,1,1.58-1.54h.1a2.85,2.85,0,0,1,2,.8l6.73,6.55,1.4,1.36a1.43,1.43,0,0,0,2.25,0l1.11-1.09a1.33,1.33,0,0,0,0-2.18Z"/><path class="cls-5" d="M172.42,39h-7.9Q155,39,155,34.37V20.49q0-4.63,9.48-4.62h7.9A1.38,1.38,0,0,1,174,17.41V19a1.38,1.38,0,0,1-1.58,1.54h-7.9q-4.74,0-4.74,1.54v1.54a1.38,1.38,0,0,0,1.58,1.54h3.16a1.38,1.38,0,0,1,1.58,1.54V28.2a1.38,1.38,0,0,1-1.58,1.54h-3.16a1.38,1.38,0,0,0-1.58,1.54v1.54q0,1.54,4.74,1.54h7.9A1.38,1.38,0,0,1,174,35.91v1.54A1.38,1.38,0,0,1,172.42,39Z"/><path class="cls-5" d="M124,39a1.38,1.38,0,0,1-1.58-1.54v-20A1.38,1.38,0,0,1,124,15.87h1.58a1.38,1.38,0,0,1,1.58,1.54v20A1.38,1.38,0,0,1,125.58,39H124Z"/><path class="cls-5" d="M110.28,17.41V32.83q0,1.54,4.74,1.54h3.16a1.38,1.38,0,0,1,1.58,1.54v1.54A1.38,1.38,0,0,1,118.19,39H115q-9.48,0-9.48-4.63v-17a1.38,1.38,0,0,1,1.58-1.54h1.58a1.38,1.38,0,0,1,1.58,1.54Z"/><path class="cls-5" d="M91.79,15.86q-9.48,0-9.48,4.63V34.37q0,4.63,9.48,4.63t9.49-4.63V20.49Q101.28,15.87,91.79,15.86Zm4.74,17q0,1.54-4.74,1.54T87,32.83V22q0-1.39,4.74-1.54,4.74,0,4.74,1.54Z"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -8,6 +8,7 @@ interface IButtonProps {
disabled: boolean;
onClick: (evt: React.MouseEvent<HTMLButtonElement>) => boolean;
size: string;
tabIndex: number;
text: string;
type: string;
variant: string;
@ -37,7 +38,7 @@ class Button extends React.Component<IButtonProps, IButtonState> {
render () {
const { handleClick } = this;
const { className, disabled, size, text, type, variant } = this.props;
const { className, disabled, size, tabIndex, text, type, variant } = this.props;
const fullClassName = classnames(`${baseClass}--${variant}`, className, {
[baseClass]: variant !== 'unstyled',
[`${baseClass}--disabled`]: disabled,
@ -49,6 +50,7 @@ class Button extends React.Component<IButtonProps, IButtonState> {
className={fullClassName}
disabled={disabled}
onClick={handleClick}
tabIndex={tabIndex}
type={type}
>
{text}

View File

@ -6,14 +6,15 @@ import Button from 'components/buttons/Button';
import InputFieldWithIcon from 'components/forms/fields/InputFieldWithIcon';
import helpers from './helpers';
const formFields = ['name', 'username', 'password', 'password_confirmation', 'email'];
const formFields = ['username', 'password', 'password_confirmation', 'email'];
const { validate } = helpers;
class AdminDetails extends Component {
static propTypes = {
className: PropTypes.string,
currentPage: PropTypes.bool,
fields: PropTypes.shape({
email: formFieldInterface.isRequired,
name: formFieldInterface.isRequired,
password: formFieldInterface.isRequired,
password_confirmation: formFieldInterface.isRequired,
username: formFieldInterface.isRequired,
@ -22,40 +23,44 @@ class AdminDetails extends Component {
};
render () {
const { fields, handleSubmit } = this.props;
const { className, currentPage, fields, handleSubmit } = this.props;
const tabIndex = currentPage ? 1 : -1;
return (
<div>
<InputFieldWithIcon
{...fields.name}
placeholder="Full Name"
/>
<InputFieldWithIcon
{...fields.username}
iconName="username"
placeholder="Username"
/>
<InputFieldWithIcon
{...fields.password}
iconName="password"
placeholder="Password"
type="password"
/>
<InputFieldWithIcon
{...fields.password_confirmation}
iconName="password"
placeholder="Confirm Password"
type="password"
/>
<InputFieldWithIcon
{...fields.email}
iconName="email"
placeholder="Email"
/>
<div className={className}>
<div className="registration-fields">
<InputFieldWithIcon
{...fields.username}
iconName="username"
placeholder="Username"
tabIndex={tabIndex}
/>
<InputFieldWithIcon
{...fields.password}
iconName="password"
placeholder="Password"
type="password"
tabIndex={tabIndex}
/>
<InputFieldWithIcon
{...fields.password_confirmation}
iconName="password"
placeholder="Confirm Password"
type="password"
tabIndex={tabIndex}
/>
<InputFieldWithIcon
{...fields.email}
iconName="email"
placeholder="Email"
tabIndex={tabIndex}
/>
</div>
<Button
onClick={handleSubmit}
text="Submit"
variant="gradient"
tabIndex={tabIndex}
/>
</div>
);

View File

@ -9,24 +9,6 @@ import { fillInFormInput } from 'test/helpers';
describe('AdminDetails - form', () => {
afterEach(restoreSpies);
describe('name input', () => {
it('renders an input field', () => {
const form = mount(<AdminDetails handleSubmit={noop} />);
const fullNameField = form.find({ name: 'name' });
expect(fullNameField.length).toEqual(1);
});
it('updates state when the field changes', () => {
const form = mount(<AdminDetails handleSubmit={noop} />);
const fullNameField = form.find({ name: 'name' }).find('input');
fillInFormInput(fullNameField, 'The Gnar Co');
expect(form.state().formData).toInclude({ name: 'The Gnar Co' });
});
});
describe('username input', () => {
it('renders an input field', () => {
const form = mount(<AdminDetails handleSubmit={noop} />);
@ -111,7 +93,6 @@ describe('AdminDetails - form', () => {
expect(onSubmitSpy).toNotHaveBeenCalled();
expect(form.state().errors).toInclude({
email: 'Email must be present',
name: 'Full name must be present',
password: 'Password must be present',
password_confirmation: 'Password confirmation must be present',
username: 'Username must be present',
@ -152,14 +133,12 @@ describe('AdminDetails - form', () => {
const onSubmitSpy = createSpy();
const form = mount(<AdminDetails handleSubmit={onSubmitSpy} />);
const emailField = form.find({ name: 'email' }).find('input');
const fullNameField = form.find({ name: 'name' }).find('input');
const passwordConfirmationField = form.find({ name: 'password_confirmation' }).find('input');
const passwordField = form.find({ name: 'password' }).find('input');
const usernameField = form.find({ name: 'username' }).find('input');
const submitBtn = form.find('Button');
fillInFormInput(emailField, 'hi@gnar.dog');
fillInFormInput(fullNameField, 'Gnar Dog');
fillInFormInput(passwordField, 'p@ssw0rd');
fillInFormInput(passwordConfirmationField, 'p@ssw0rd');
fillInFormInput(usernameField, 'gnardog');

View File

@ -6,7 +6,6 @@ const validate = (formData) => {
const errors = {};
const {
email,
name: fullName,
password,
password_confirmation: passwordConfirmation,
username,
@ -20,10 +19,6 @@ const validate = (formData) => {
errors.email = 'Email must be present';
}
if (!fullName) {
errors.name = 'Full name must be present';
}
if (!username) {
errors.username = 'Username must be present';
}

View File

@ -1,11 +1,16 @@
import React, { Component, PropTypes } from 'react';
import classnames from 'classnames';
import Button from 'components/buttons/Button';
import formDataInterface from 'interfaces/registration_form_data';
import Icon from 'components/Icon';
import Checkbox from 'components/forms/fields/Checkbox';
const baseClass = 'confirm-user-reg';
class ConfirmationPage extends Component {
static propTypes = {
className: PropTypes.string,
formData: formDataInterface,
handleSubmit: PropTypes.func,
};
@ -20,9 +25,9 @@ class ConfirmationPage extends Component {
render () {
const {
className,
formData: {
email,
full_name: fullName,
kolide_server_url: kolideWebAddress,
org_name: orgName,
username,
@ -30,38 +35,47 @@ class ConfirmationPage extends Component {
} = this.props;
const { onSubmit } = this;
const confirmRegClasses = classnames(className, baseClass);
return (
<div>
<Icon name="success-check" />
<table>
<caption>Administrator Configuration</caption>
<tbody>
<tr>
<th>Full Name:</th>
<td>{fullName}</td>
</tr>
<tr>
<th>Username:</th>
<td>{username}</td>
</tr>
<tr>
<th>Email:</th>
<td>{email}</td>
</tr>
<tr>
<th>Organization:</th>
<td>{orgName}</td>
</tr>
<tr>
<th>Kolide URL:</th>
<td>{kolideWebAddress}</td>
</tr>
</tbody>
</table>
<div className={confirmRegClasses}>
<div className={`${baseClass}__wrapper`}>
<Icon name="success-check" className={`${baseClass}__icon`} />
<table className={`${baseClass}__table`}>
<caption>Administrator Configuration</caption>
<tbody>
<tr>
<th>Username:</th>
<td>{username}</td>
</tr>
<tr>
<th>Email:</th>
<td>{email}</td>
</tr>
<tr>
<th>Organization:</th>
<td>{orgName}</td>
</tr>
<tr>
<th>Kolide URL:</th>
<td><span className={`${baseClass}__table-url`} title={kolideWebAddress}>{kolideWebAddress}</span></td>
</tr>
</tbody>
</table>
<div className={`${baseClass}__import`}>
<Checkbox name="import-install">
<p>I am migrating an existing <strong>osquery</strong> installation.</p>
<p>Take me to the <strong>Import Configuration</strong> page.</p>
</Checkbox>
</div>
</div>
<Button
onClick={onSubmit}
text="Submit"
text="Finish"
variant="gradient"
className={`${baseClass}__submit`}
/>
</div>
);

View File

@ -9,7 +9,6 @@ describe('ConfirmationPage - form', () => {
afterEach(restoreSpies);
const formData = {
full_name: 'Jason Meller',
username: 'jmeller',
email: 'jason@kolide.co',
org_name: 'Kolide',
@ -24,7 +23,6 @@ describe('ConfirmationPage - form', () => {
/>
);
expect(form.text()).toInclude(formData.full_name);
expect(form.text()).toInclude(formData.username);
expect(form.text()).toInclude(formData.email);
expect(form.text()).toInclude(formData.org_name);

View File

@ -0,0 +1,92 @@
.confirm-user-reg {
&__wrapper {
padding: 0 35px 25px;
}
&__icon {
color: $success;
font-size: 120px;
display: block;
text-align: center;
margin: 10px 0 35px;
}
&__submit {
bottom: 0;
top: auto;
position: absolute;
border-top-left-radius: 0;
border-top-right-radius: 0;
&:active {
top: auto;
}
}
&__table {
width: 100%;
caption {
font-size: 18px;
font-weight: $bold;
line-height: 0.72;
letter-spacing: 0.6px;
color: $text-dark;
text-transform: uppercase;
padding-bottom: 20px;
}
tr {
vertical-align: bottom;
}
th {
font-size: 14px;
font-weight: $bold;
line-height: 1.71;
letter-spacing: 0.5px;
color: $text-dark;
text-align: left;
}
td {
font-size: 14px;
font-weight: $light;
line-height: 1.71;
letter-spacing: 0.5px;
color: $text-dark;
}
}
&__table-url {
@include ellipsis(90%);
font-family: 'SourceCodePro', $monospace;
vertical-align: bottom;
}
&__import {
@include user-select(none);
font-size: 14px;
font-weight: $light;
line-height: 1.71;
letter-spacing: 0.5px;
color: $text-dark;
margin: 30px 0 0;
label {
display: block;
position: relative;
}
p {
margin: 0;
padding-left: 40px;
}
.kolide-checkbox__input {
position: absolute;
left: 0;
top: 0;
}
}
}

View File

@ -11,6 +11,8 @@ const { validate } = helpers;
class KolideDetails extends Component {
static propTypes = {
className: PropTypes.string,
currentPage: PropTypes.bool,
fields: PropTypes.shape({
kolide_server_url: formFieldInterface.isRequired,
}).isRequired,
@ -18,18 +20,24 @@ class KolideDetails extends Component {
};
render () {
const { fields, handleSubmit } = this.props;
const { className, currentPage, fields, handleSubmit } = this.props;
const tabIndex = currentPage ? 1 : -1;
return (
<div>
<InputFieldWithIcon
{...fields.kolide_server_url}
placeholder="Kolide Web Address"
/>
<div className={className}>
<div className="registration-fields">
<InputFieldWithIcon
{...fields.kolide_server_url}
placeholder="Kolide Web Address"
tabIndex={tabIndex}
hint={['Dont include ', <code key="hint">/v1</code>, ' or any other path']}
/>
</div>
<Button
onClick={handleSubmit}
text="Submit"
variant="gradient"
tabIndex={tabIndex}
/>
</div>
);

View File

@ -11,6 +11,8 @@ const { validate } = helpers;
class OrgDetails extends Component {
static propTypes = {
className: PropTypes.string,
currentPage: PropTypes.bool,
fields: PropTypes.shape({
org_name: formFieldInterface.isRequired,
org_logo_url: formFieldInterface.isRequired,
@ -19,22 +21,29 @@ class OrgDetails extends Component {
};
render () {
const { fields, handleSubmit } = this.props;
const { className, currentPage, fields, handleSubmit } = this.props;
const tabIndex = currentPage ? 1 : -1;
return (
<div>
<InputFieldWithIcon
{...fields.org_name}
placeholder="Organization Name"
/>
<InputFieldWithIcon
{...fields.org_logo_url}
placeholder="Organization Logo URL (must start with https://)"
/>
<div className={className}>
<div className="registration-fields">
<InputFieldWithIcon
{...fields.org_name}
placeholder="Organization Name"
tabIndex={tabIndex}
/>
<InputFieldWithIcon
{...fields.org_logo_url}
placeholder="Organization Logo URL"
tabIndex={tabIndex}
hint="must start with https://"
/>
</div>
<Button
onClick={handleSubmit}
text="Submit"
variant="gradient"
tabIndex={tabIndex}
/>
</div>
);

View File

@ -1,4 +1,5 @@
import React, { Component, PropTypes } from 'react';
import classnames from 'classnames';
import AdminDetails from 'components/forms/RegistrationForm/AdminDetails';
import ConfirmationPage from 'components/forms/RegistrationForm/ConfirmationPage';
@ -12,6 +13,8 @@ const PAGE_HEADER_TEXT = {
4: 'SUCCESS',
};
const baseClass = 'user-registration';
class RegistrationForm extends Component {
static propTypes = {
onNextPage: PropTypes.func,
@ -21,8 +24,14 @@ class RegistrationForm extends Component {
constructor (props) {
super(props);
const { window } = global;
this.state = { errors: {}, formData: {} };
this.state = {
errors: {},
formData: {
kolide_server_url: window.location.origin,
},
};
}
onPageFormSubmit = (pageFormData) => {
@ -39,19 +48,40 @@ class RegistrationForm extends Component {
return onNextPage();
}
onSubmit = () => {
onSubmitConfirmation = () => {
const { formData } = this.state;
const { onSubmit: handleSubmit } = this.props;
return handleSubmit(formData);
}
isCurrentPage = (num) => {
const { page } = this.props;
if (num === page) {
return true;
}
return false;
}
renderHeader = () => {
const { page } = this.props;
const headerText = PAGE_HEADER_TEXT[page];
if (headerText) {
return <h2 className={`${baseClass}__title`}>{headerText}</h2>;
}
return false;
}
renderDescription = () => {
const { page } = this.props;
if (page === 1) {
return (
<div>
<div className={`${baseClass}__description`}>
<p>Additional admins can be designated within the Kolide App</p>
<p>Passwords must include 7 characters, at least 1 number (eg. 0-9) and at least 1 symbol (eg. ^&*#)</p>
</div>
@ -60,7 +90,7 @@ class RegistrationForm extends Component {
if (page === 2) {
return (
<div>
<div className={`${baseClass}__description`}>
<p>Set your Organization&apos;s name (eg. Yahoo! Inc)</p>
<p>Specify the website URL of your organization (eg. Yahoo.com)</p>
</div>
@ -69,7 +99,7 @@ class RegistrationForm extends Component {
if (page === 3) {
return (
<div>
<div className={`${baseClass}__description`}>
<p>Define the base URL which osqueryd clients use to connect and register with Kolide.</p>
<p>
<small>Note: Please ensure the URL you choose is accessible to all endpoints that need to communicate with Kolide. Otherwise, they will not be able to correctly register.</small>
@ -81,51 +111,82 @@ class RegistrationForm extends Component {
return false;
}
renderHeader = () => {
renderContent = () => {
const { page } = this.props;
const headerText = PAGE_HEADER_TEXT[page];
if (headerText) {
return <h2>{headerText}</h2>;
}
return false;
}
renderPageForm = () => {
const { formData } = this.state;
const { onPageFormSubmit, onSubmit } = this;
const { page } = this.props;
if (page === 1) {
return <AdminDetails formData={formData} handleSubmit={onPageFormSubmit} />;
}
if (page === 2) {
return <OrgDetails formData={formData} handleSubmit={onPageFormSubmit} />;
}
if (page === 3) {
return <KolideDetails formData={formData} handleSubmit={onPageFormSubmit} />;
}
const {
onSubmitConfirmation,
renderDescription,
renderHeader,
} = this;
if (page === 4) {
return <ConfirmationPage formData={formData} handleSubmit={onSubmit} />;
return (
<div>
{renderHeader()}
<ConfirmationPage formData={formData} handleSubmit={onSubmitConfirmation} className={`${baseClass}__confirmation`} />
</div>
);
}
return false;
return (
<div>
{renderHeader()}
{renderDescription()}
</div>
);
}
render () {
const { onSubmit } = this.props;
const { renderDescription, renderHeader, renderPageForm } = this;
const { onSubmit, page } = this.props;
const { formData } = this.state;
const { isCurrentPage, onPageFormSubmit, renderContent } = this;
const containerClass = classnames(`${baseClass}__container`, {
[`${baseClass}__container--complete`]: page > 3,
});
const adminDetailsClass = classnames(
`${baseClass}__field-wrapper`,
`${baseClass}__field-wrapper--admin`
);
const orgDetailsClass = classnames(
`${baseClass}__field-wrapper`,
`${baseClass}__field-wrapper--org`
);
const kolideDetailsClass = classnames(
`${baseClass}__field-wrapper`,
`${baseClass}__field-wrapper--kolide`
);
const formSectionClasses = classnames(
`${baseClass}__form`,
{
[`${baseClass}__form--step1-active`]: page === 1,
[`${baseClass}__form--step1-complete`]: page > 1,
[`${baseClass}__form--step2-active`]: page === 2,
[`${baseClass}__form--step2-complete`]: page > 2,
[`${baseClass}__form--step3-active`]: page === 3,
[`${baseClass}__form--step3-complete`]: page > 3,
}
);
return (
<form onSubmit={onSubmit}>
{renderHeader()}
{renderDescription()}
{renderPageForm()}
</form>
<div className={baseClass}>
<div className={containerClass}>
{renderContent()}
<form onSubmit={onSubmit} className={formSectionClasses}>
<AdminDetails formData={formData} handleSubmit={onPageFormSubmit} className={adminDetailsClass} currentPage={isCurrentPage(1)} />
<OrgDetails formData={formData} handleSubmit={onPageFormSubmit} className={orgDetailsClass} currentPage={isCurrentPage(2)} />
<KolideDetails formData={formData} handleSubmit={onPageFormSubmit} className={kolideDetailsClass} currentPage={isCurrentPage(3)} />
</form>
</div>
</div>
);
}
}

View File

@ -0,0 +1,196 @@
.user-registration {
@include display(flex);
@include align-content(center);
@include justify-content(center);
@include flex-grow(1);
&__container {
@include align-self(center);
@include size(500px 520px);
border-radius: 4px;
background-color: $bg-light;
box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.3);
box-sizing: border-box;
padding: 25px 35px;
margin-top: -55px;
&--complete {
padding: 0;
.user-registration__title {
font-size: 24px;
font-weight: $bold;
line-height: 0.54;
letter-spacing: 0.9px;
color: $text-dark;
padding: 25px 35px;
}
}
}
&__form {
@include transform(translateY(-85px));
width: 100%;
height: 470px;
position: absolute;
top: 50%;
left: 0;
overflow: hidden;
box-sizing: border-box;
padding: 25px 0;
@include breakpoint(tablet) {
@include transform(translateY(-100px));
}
&--step1-complete {
.user-registration__field-wrapper--admin {
left: -184px;
}
.user-registration__field-wrapper--org {
left: 50%;
}
.user-registration__field-wrapper--kolide {
left: calc(100% + 184px);
}
}
&--step2-complete {
.user-registration__field-wrapper--admin {
left: calc(-50% - 184px);
}
.user-registration__field-wrapper--org {
left: -184px;
}
.user-registration__field-wrapper--kolide {
left: 50%;
}
}
&--step3-complete {
.user-registration__field-wrapper--admin {
left: calc(-100% - 184px);
}
.user-registration__field-wrapper--org {
left: calc(-50% - 184px);
}
.user-registration__field-wrapper--kolide {
left: -184px;
}
}
&::before,
&::after {
background-image: linear-gradient(to right, $accent-dark 50%, transparent 50%);
background-position: left top;
background-repeat: repeat-x;
background-size: 17px 2px;
position: absolute;
top: 100px;
left: 50%;
width: 50%;
height: 2px;
content: '';
z-index: 1;
}
&::before {
left: auto;
right: 50%;
}
&--step1-active {
&::before {
display: none;
}
}
&--step3-active,
&--step3-complete {
&::after {
display: none;
}
}
}
&__description {
font-size: 14px;
font-weight: $light;
line-height: 1.43;
letter-spacing: 0.5px;
color: $text-dark;
}
&__title {
font-size: 18px;
font-weight: $bold;
line-height: 0.72;
letter-spacing: 0.6px;
color: $text-dark;
margin: 0;
padding: 0;
}
&__field-wrapper {
@include transition(left 300ms ease);
width: 430px;
min-height: 400px;
padding-bottom: 75px;
border-radius: 4px;
background-color: $bg-light;
box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.3);
margin: 0 auto;
position: absolute;
box-sizing: border-box;
margin-left: -215px;
z-index: 2;
&--admin {
left: 50%;
}
&--org {
left: calc(100% + 184px);
}
&--kolide {
top: 45px;
left: calc(150% + 184px);
}
input {
background-color: transparent;
}
.button {
bottom: 0;
top: auto;
position: absolute;
border-top-left-radius: 0;
border-top-right-radius: 0;
&:active {
top: auto;
}
}
.registration-fields {
padding: 0 35px 25px;
box-sizing: border-box;
}
}
&__confirmation {
background-color: $bg-light;
position: relative;
z-index: 2;
width: 500px;
height: 500px;
}
}

View File

@ -0,0 +1,33 @@
import React, { Component, PropTypes } from 'react';
import classnames from 'classnames';
import { noop } from 'lodash';
const baseClass = 'kolide-checkbox';
class InputField extends Component {
static propTypes = {
children: PropTypes.node,
className: PropTypes.string,
name: PropTypes.string,
onChange: PropTypes.func,
};
static defaultProps = {
onChange: noop,
};
render () {
const { children, className, name, onChange } = this.props;
const checkBoxClass = classnames(baseClass, className);
return (
<label htmlFor={name} className={checkBoxClass}>
<input type="checkbox" name={name} id={name} className={`${checkBoxClass}__input`} onChange={onChange} />
<span className={`${checkBoxClass}__tick`} />
{children}
</label>
);
}
}
export default InputField;

View File

@ -0,0 +1,11 @@
import React from 'react';
import expect from 'expect';
import { mount } from 'enzyme';
import Checkbox from './Checkbox';
describe('Checkbox - component', () => {
it('renders', () => {
expect(mount(<Checkbox />)).toExist();
});
});

View File

@ -0,0 +1,45 @@
.kolide-checkbox {
&__input {
visibility: hidden;
margin: 0;
&:checked + .kolide-checkbox__tick {
&::after {
background-color: $brand;
border: solid 2px $brand;
}
&::before {
@include transform(rotate(45deg));
@include position(absolute, 50% null null 50%);
box-sizing: border-box;
display: table;
width: 7px;
height: 13px;
margin: -8px 0 0 -4px;
border: 2px solid $white;
border-top: 0;
border-left: 0;
content: '';
}
}
}
&__tick {
@include size(20px);
position: absolute;
display: inline-block;
&::after {
@include transition(border 75ms ease-in-out, background 75ms ease-in-out);
@include size(20px);
border-radius: 2px;
border: solid 2px $border-medium;
content: '';
box-sizing: border-box;
display: block;
background-color: $white;
visibility: visible;
}
}
}

View File

@ -0,0 +1 @@
export default from './Checkbox';

View File

@ -10,10 +10,12 @@ class InputFieldWithIcon extends InputField {
static propTypes = {
autofocus: PropTypes.bool,
error: PropTypes.string,
hint: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
iconName: PropTypes.string,
name: PropTypes.string,
onChange: PropTypes.func,
placeholder: PropTypes.string,
tabIndex: PropTypes.number,
type: PropTypes.string,
className: PropTypes.string,
};
@ -33,9 +35,19 @@ class InputFieldWithIcon extends InputField {
return <div className={labelClasses}>{placeholder}</div>;
}
renderHint = () => {
const { hint } = this.props;
if (hint) {
return <span className={`${baseClass}__hint`}>{hint}</span>;
}
return false;
}
render () {
const { className, error, iconName, name, placeholder, type, value } = this.props;
const { onInputChange } = this;
const { className, error, iconName, name, placeholder, tabIndex, type, value } = this.props;
const { onInputChange, renderHint } = this;
const inputClasses = classnames(
`${baseClass}__input`,
@ -60,10 +72,12 @@ class InputFieldWithIcon extends InputField {
className={inputClasses}
placeholder={placeholder}
ref={(r) => { this.input = r; }}
tabIndex={tabIndex}
type={type}
value={value}
/>
{iconName && <Icon name={iconName} className={iconClasses} />}
{renderHint()}
</div>
);
}

View File

@ -7,7 +7,7 @@
position: absolute;
right: 6px;
top: 28px;
font-size: 20px;
font-size: 14px;
color: $accent-text;
&--active {
@ -27,13 +27,18 @@
font-size: 20px;
border-bottom-style: solid;
border-bottom-color: $brand-ultralight;
color: $accent-text;
padding-right: 30px;
opacity: 1;
text-indent: 1px;
position: relative;
width: 100%;
box-sizing: border-box;
font-family: 'Oxygen', $helvetica;
color: $text-dark;
@include placeholder {
color: $accent-text;
}
&:focus {
outline: none;
@ -63,4 +68,19 @@
font-size: $small;
text-transform: lowercase;
}
&__hint {
font-size: 14px;
font-weight: $normal;
line-height: 1.57;
letter-spacing: 1px;
color: $accent-text;
code {
color: $brand-light;
background-color: $accent-light;
padding: 2px;
font-family: 'SourceCodePro', $monospace;
}
}
}

View File

@ -7,7 +7,7 @@
font-size: $xsmall;
display: none;
@include breakpoint(tablet) {
@include breakpoint(ltdesktop) {
display: inline-block;
position: absolute;
top: 10px;
@ -20,7 +20,7 @@
}
}
@include breakpoint(tablet) {
@include breakpoint(ltdesktop) {
padding-top: $pad-most;
}
}

View File

@ -181,7 +181,7 @@
}
}
@include breakpoint(tablet) {
@include breakpoint(ltdesktop) {
.Select-menu-outer {
.Select-menu {
min-width: 665px;

View File

@ -1,7 +1,6 @@
import { PropTypes } from 'react';
export default PropTypes.shape({
full_name: PropTypes.string,
username: PropTypes.string,
password: PropTypes.string,
password_confirmation: PropTypes.string,

View File

@ -24,21 +24,26 @@ class Breadcrumbs extends Component {
render () {
const { onClick } = this;
const { page } = this.props;
const page1ClassName = classnames('button--unstyled', 'page-1-btn', {
'is-active': page >= 1,
const baseClass = 'registration-breadcrumbs';
const pageBaseClass = `${baseClass}__page`;
const page1ClassName = classnames(pageBaseClass, `${pageBaseClass}--1`, 'button--unstyled', {
[`${pageBaseClass}--active`]: page === 1,
[`${pageBaseClass}--complete`]: page > 1,
});
const page2ClassName = classnames('button--unstyled', 'page-2-btn', {
'is-active': page >= 2,
const page2ClassName = classnames(pageBaseClass, `${pageBaseClass}--2`, 'button--unstyled', {
[`${pageBaseClass}--active`]: page === 2,
[`${pageBaseClass}--complete`]: page > 2,
});
const page3ClassName = classnames('button--unstyled', 'page-3-btn', {
'is-active': page >= 3,
const page3ClassName = classnames(pageBaseClass, `${pageBaseClass}--3`, 'button--unstyled', {
[`${pageBaseClass}--active`]: page === 3,
[`${pageBaseClass}--complete`]: page > 3,
});
return (
<div>
<button className={page1ClassName} onClick={onClick(1)}>Page 1</button>
<button className={page2ClassName} onClick={onClick(2)}>Page 2</button>
<button className={page3ClassName} onClick={onClick(3)}>Page 3</button>
<div className={baseClass}>
<button className={page1ClassName} onClick={onClick(1)}>Setup User</button>
<button className={page2ClassName} onClick={onClick(2)}>Setup Organization</button>
<button className={page3ClassName} onClick={onClick(3)}>Set Kolide URL</button>
</div>
);
}

View File

@ -14,21 +14,21 @@ describe('Breadcrumbs - component', () => {
it('renders page 1 Button as active when the page prop is 1', () => {
const component = mount(<Breadcrumbs page={1} />);
const page1Btn = component.find('button.page-1-btn');
const page2Btn = component.find('button.page-2-btn');
const page3Btn = component.find('button.page-3-btn');
const page1Btn = component.find('button.registration-breadcrumbs__page--1');
const page2Btn = component.find('button.registration-breadcrumbs__page--2');
const page3Btn = component.find('button.registration-breadcrumbs__page--3');
expect(page1Btn.prop('className')).toInclude('is-active');
expect(page2Btn.prop('className')).toNotInclude('is-active');
expect(page3Btn.prop('className')).toNotInclude('is-active');
expect(page1Btn.prop('className')).toInclude('registration-breadcrumbs__page--active');
expect(page2Btn.prop('className')).toNotInclude('registration-breadcrumbs__page--active');
expect(page3Btn.prop('className')).toNotInclude('registration-breadcrumbs__page--active');
});
it('calls the onClick prop with the page # when clicked', () => {
const onClickSpy = createSpy();
const component = mount(<Breadcrumbs page={1} onClick={onClickSpy} />);
const page1Btn = component.find('button.page-1-btn');
const page2Btn = component.find('button.page-2-btn');
const page3Btn = component.find('button.page-3-btn');
const page1Btn = component.find('button.registration-breadcrumbs__page--1');
const page2Btn = component.find('button.registration-breadcrumbs__page--2');
const page3Btn = component.find('button.registration-breadcrumbs__page--3');
page1Btn.simulate('click');

View File

@ -0,0 +1,146 @@
.registration-breadcrumbs {
@include display(flex);
@include justify-content(space-between);
@include align-content(center);
width: 665px;
height: 125px;
margin: 0 auto;
@include breakpoint(tablet) {
height: 75px;
}
&__page {
text-align: center;
width: 145px;
font-size: 14px;
font-weight: $normal;
line-height: 1.53;
letter-spacing: 0.5px;
color: $text-medium;
position: relative;
&::before {
content: '';
position: absolute;
width: 235px;
height: 2px;
background-image: linear-gradient(to right, $accent-dark 50%, transparent 50%);
background-position: left top;
background-repeat: repeat-x;
background-size: 17px 2px;
bottom: 43px;
left: 84px;
@include breakpoint(tablet) {
bottom: 23px;
}
}
&::after {
@extend %kolidecon;
@include size(24px);
background-color: $white;
display: block;
border-radius: 50%;
content: '';
font-size: 28px;
margin: 14px auto 0;
position: relative;
z-index: 1;
cursor: pointer;
@include breakpoint(tablet) {
margin-top: 4px;
}
}
&:focus {
outline: none;
}
&--active {
font-weight: $bold;
color: $text-dark;
}
&--1 {
&::after {
border: solid 2px $success;
}
&.registration-breadcrumbs__page--active {
&::after {
background-color: $success;
}
}
&.registration-breadcrumbs__page--complete {
&::before {
background-image: linear-gradient(to right, $success 0%, $link 100%);
background-size: auto;
z-index: 2;
}
&::after {
@include size(28px);
content: '\f035';
color: $success;
border: 0;
}
}
}
&--2 {
&::after {
border: solid 2px $link;
}
&.registration-breadcrumbs__page--active {
&::after {
background-color: $link;
}
}
&.registration-breadcrumbs__page--complete {
&::before {
background-image: linear-gradient(to right, $link 0%, $brand-light 100%);
background-size: auto;
z-index: 2;
}
&::after {
@include size(28px);
content: '\f035';
color: $link;
border: 0;
}
}
}
&--3 {
&::before {
display: none;
}
&::after {
border: solid 2px $brand-light;
}
&.registration-breadcrumbs__page--active {
&::after {
background-color: $brand-light;
}
}
&.registration-breadcrumbs__page--complete {
&::after {
@include size(28px);
content: '\f035';
color: $brand-light;
border: 0;
}
}
}
}
}

View File

@ -10,6 +10,8 @@ import { setup } from 'redux/nodes/auth/actions';
import { showBackgroundImage } from 'redux/nodes/app/actions';
import userInterface from 'interfaces/user';
import kolideLogo from '../../../assets/images/kolide-logo-condensed.svg';
export class RegistrationPage extends Component {
static propTypes = {
currentUser: userInterface,
@ -70,6 +72,11 @@ export class RegistrationPage extends Component {
}
onSetPage = (page) => {
const { page: currentPage } = this.state;
if (page >= currentPage) {
return false;
}
this.setState({ page });
return false;
@ -85,7 +92,12 @@ export class RegistrationPage extends Component {
}
return (
<div>
<div className="registration-page">
<img
alt="Kolide"
src={kolideLogo}
className="registration-page__logo"
/>
<Breadcrumbs onClick={onSetPage} page={page} />
<RegistrationForm page={page} onNextPage={onNextPage} onSubmit={onRegistrationFormSubmit} />
</div>

View File

@ -84,6 +84,7 @@ describe('RegistrationPage - component', () => {
describe('#onSetPage', () => {
it('sets state to the page number', () => {
const page = mount(<RegistrationPage />);
page.setState({ page: 3 });
page.node.onSetPage(3);
expect(page.state()).toInclude({ page: 3 });

View File

@ -0,0 +1,19 @@
.registration-page {
@include display(flex);
@include justify-content(center);
@include flex-direction(column);
min-height: calc(100vh - #{$footer-height});
position: relative;
&__logo {
@include position(absolute, 15px null null 15px);
width: 200px;
@include breakpoint(tablet) {
width: 125px;
position: static;
display: block;
margin: 15px 0 0 15px;
}
}
}

View File

@ -26,7 +26,7 @@
border: solid 1px $accent-dark;
box-shadow: inset 0 0 8px 0 rgba($black, 0.12);
box-sizing: border-box;
font-family: Source-codePro, Oxygen;
font-family: SourceCodePro, Oxygen;
font-size: $medium;
height: 60px;
letter-spacing: 1.2px;
@ -100,6 +100,6 @@
code {
color: $brand;
font-family: Source-codePro, Oxygen;
font-family: SourceCodePro, Oxygen;
}
}

View File

@ -1,6 +1,7 @@
html {
position: relative;
min-height: 100%;
min-width: $min-width;
// Because iOS hates us we must fight to the death!
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
// End Apple War
@ -13,6 +14,7 @@ body {
font-family: 'Oxygen', sans-serif;
font-size: $base;
line-height: 1.6;
min-width: $min-width;
}
h1,
@ -37,6 +39,7 @@ h3 {
}
input,
textarea {
textarea,
button {
font-family: 'Oxygen', sans-serif;
}

View File

@ -112,18 +112,6 @@
content: '\f029';
}
.kolidecon-username:before {
content: '\f02a';
}
.kolidecon-password:before {
content: '\f02b';
}
.kolidecon-email:before {
content: '\f02c';
}
.kolidecon-query:before {
content: '\f02d';
}
@ -288,6 +276,18 @@
content: '\f03d';
}
.kolidecon-username:before {
content: '\f02a';
}
.kolidecon-password:before {
content: '\f02b';
}
.kolidecon-email:before {
content: '\f02c';
}
.sr-only {
position: absolute;

View File

@ -1,10 +1,15 @@
$min-width: 768px;
$medium-width: 1280px;
$medium-width: 1024px;
$desktop-width: 1280px;
$max-width: 2560px;
@mixin breakpoint($size: desktop) {
@if ($size == tablet) {
@media (min-width: $min-width) and (max-width: $medium-width - 1) {
@media (max-width: $medium-width) {
@content;
}
} @else if ($size == ltdesktop) {
@media (min-width: $min-width) and (max-width: $desktop-width - 1) {
@content;
}
} @else {

View File

@ -2,6 +2,7 @@
<html data-uuid="{{ .UUID }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="/assets/bundle.css">
<title>Kolide</title>
</head>