diff --git a/assets/fonts/kolidecons/kolidecons.woff b/assets/fonts/kolidecons/kolidecons.woff index 62a532bc51..8ae70f97a8 100644 Binary files a/assets/fonts/kolidecons/kolidecons.woff and b/assets/fonts/kolidecons/kolidecons.woff differ diff --git a/assets/images/kolide-logo-condensed.svg b/assets/images/kolide-logo-condensed.svg new file mode 100644 index 0000000000..c089ee022b --- /dev/null +++ b/assets/images/kolide-logo-condensed.svg @@ -0,0 +1,2 @@ + +Asset 3 diff --git a/frontend/components/buttons/Button/Button.tsx b/frontend/components/buttons/Button/Button.tsx index a5920b54f9..b4ad0822fd 100644 --- a/frontend/components/buttons/Button/Button.tsx +++ b/frontend/components/buttons/Button/Button.tsx @@ -8,6 +8,7 @@ interface IButtonProps { disabled: boolean; onClick: (evt: React.MouseEvent) => boolean; size: string; + tabIndex: number; text: string; type: string; variant: string; @@ -37,7 +38,7 @@ class Button extends React.Component { 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 { className={fullClassName} disabled={disabled} onClick={handleClick} + tabIndex={tabIndex} type={type} > {text} diff --git a/frontend/components/forms/RegistrationForm/AdminDetails/AdminDetails.jsx b/frontend/components/forms/RegistrationForm/AdminDetails/AdminDetails.jsx index f2718d5b56..45bfa6f60f 100644 --- a/frontend/components/forms/RegistrationForm/AdminDetails/AdminDetails.jsx +++ b/frontend/components/forms/RegistrationForm/AdminDetails/AdminDetails.jsx @@ -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 ( -
- - - - - +
+
+ + + + +
); diff --git a/frontend/components/forms/RegistrationForm/AdminDetails/AdminDetails.tests.jsx b/frontend/components/forms/RegistrationForm/AdminDetails/AdminDetails.tests.jsx index 2d84be00d6..9b8ca04b54 100644 --- a/frontend/components/forms/RegistrationForm/AdminDetails/AdminDetails.tests.jsx +++ b/frontend/components/forms/RegistrationForm/AdminDetails/AdminDetails.tests.jsx @@ -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(); - const fullNameField = form.find({ name: 'name' }); - - expect(fullNameField.length).toEqual(1); - }); - - it('updates state when the field changes', () => { - const form = mount(); - 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(); @@ -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(); 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'); diff --git a/frontend/components/forms/RegistrationForm/AdminDetails/helpers.js b/frontend/components/forms/RegistrationForm/AdminDetails/helpers.js index 277b5374dc..8f5f50e87a 100644 --- a/frontend/components/forms/RegistrationForm/AdminDetails/helpers.js +++ b/frontend/components/forms/RegistrationForm/AdminDetails/helpers.js @@ -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'; } diff --git a/frontend/components/forms/RegistrationForm/ConfirmationPage/ConfirmationPage.jsx b/frontend/components/forms/RegistrationForm/ConfirmationPage/ConfirmationPage.jsx index 32a6f2d60a..5e7c5f1b2e 100644 --- a/frontend/components/forms/RegistrationForm/ConfirmationPage/ConfirmationPage.jsx +++ b/frontend/components/forms/RegistrationForm/ConfirmationPage/ConfirmationPage.jsx @@ -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 ( -
- - - - - - - - - - - - - - - - - - - - - - - - - -
Administrator Configuration
Full Name:{fullName}
Username:{username}
Email:{email}
Organization:{orgName}
Kolide URL:{kolideWebAddress}
+
+
+ + + + + + + + + + + + + + + + + + + + + +
Administrator Configuration
Username:{username}
Email:{email}
Organization:{orgName}
Kolide URL:{kolideWebAddress}
+ +
+ +

I am migrating an existing osquery installation.

+

Take me to the Import Configuration page.

+
+
+
+
); diff --git a/frontend/components/forms/RegistrationForm/ConfirmationPage/ConfirmationPage.tests.jsx b/frontend/components/forms/RegistrationForm/ConfirmationPage/ConfirmationPage.tests.jsx index aed6dd3cf3..ed2897416a 100644 --- a/frontend/components/forms/RegistrationForm/ConfirmationPage/ConfirmationPage.tests.jsx +++ b/frontend/components/forms/RegistrationForm/ConfirmationPage/ConfirmationPage.tests.jsx @@ -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); diff --git a/frontend/components/forms/RegistrationForm/ConfirmationPage/_styles.scss b/frontend/components/forms/RegistrationForm/ConfirmationPage/_styles.scss new file mode 100644 index 0000000000..fb12290b21 --- /dev/null +++ b/frontend/components/forms/RegistrationForm/ConfirmationPage/_styles.scss @@ -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; + } + } +} diff --git a/frontend/components/forms/RegistrationForm/KolideDetails/KolideDetails.jsx b/frontend/components/forms/RegistrationForm/KolideDetails/KolideDetails.jsx index 804fff206b..baccf1ef02 100644 --- a/frontend/components/forms/RegistrationForm/KolideDetails/KolideDetails.jsx +++ b/frontend/components/forms/RegistrationForm/KolideDetails/KolideDetails.jsx @@ -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 ( -
- +
+
+ /v1, ' or any other path']} + /> +
); diff --git a/frontend/components/forms/RegistrationForm/OrgDetails/OrgDetails.jsx b/frontend/components/forms/RegistrationForm/OrgDetails/OrgDetails.jsx index 117a470280..abf7755df8 100644 --- a/frontend/components/forms/RegistrationForm/OrgDetails/OrgDetails.jsx +++ b/frontend/components/forms/RegistrationForm/OrgDetails/OrgDetails.jsx @@ -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 ( -
- - +
+
+ + +
); diff --git a/frontend/components/forms/RegistrationForm/RegistrationForm.jsx b/frontend/components/forms/RegistrationForm/RegistrationForm.jsx index b4bfe8d8b0..c5785586ca 100644 --- a/frontend/components/forms/RegistrationForm/RegistrationForm.jsx +++ b/frontend/components/forms/RegistrationForm/RegistrationForm.jsx @@ -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

{headerText}

; + } + + return false; + } + renderDescription = () => { const { page } = this.props; if (page === 1) { return ( -
+

Additional admins can be designated within the Kolide App

Passwords must include 7 characters, at least 1 number (eg. 0-9) and at least 1 symbol (eg. ^&*#)

@@ -60,7 +90,7 @@ class RegistrationForm extends Component { if (page === 2) { return ( -
+

Set your Organization's name (eg. Yahoo! Inc)

Specify the website URL of your organization (eg. Yahoo.com)

@@ -69,7 +99,7 @@ class RegistrationForm extends Component { if (page === 3) { return ( -
+

Define the base URL which osqueryd clients use to connect and register with Kolide.

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. @@ -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

{headerText}

; - } - - return false; - } - - renderPageForm = () => { const { formData } = this.state; - const { onPageFormSubmit, onSubmit } = this; - const { page } = this.props; - - if (page === 1) { - return ; - } - - if (page === 2) { - return ; - } - - if (page === 3) { - return ; - } + const { + onSubmitConfirmation, + renderDescription, + renderHeader, + } = this; if (page === 4) { - return ; + return ( +
+ {renderHeader()} + +
+ ); } - return false; + return ( +
+ {renderHeader()} + {renderDescription()} +
+ ); } 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 ( -
- {renderHeader()} - {renderDescription()} - {renderPageForm()} -
+
+
+ {renderContent()} + +
+ + + + + + +
+
); } } diff --git a/frontend/components/forms/RegistrationForm/_styles.scss b/frontend/components/forms/RegistrationForm/_styles.scss new file mode 100644 index 0000000000..03d00eeac9 --- /dev/null +++ b/frontend/components/forms/RegistrationForm/_styles.scss @@ -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; + } +} diff --git a/frontend/components/forms/fields/Checkbox/Checkbox.jsx b/frontend/components/forms/fields/Checkbox/Checkbox.jsx new file mode 100644 index 0000000000..939587582b --- /dev/null +++ b/frontend/components/forms/fields/Checkbox/Checkbox.jsx @@ -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 ( + + ); + } +} + +export default InputField; diff --git a/frontend/components/forms/fields/Checkbox/Checkbox.tests.jsx b/frontend/components/forms/fields/Checkbox/Checkbox.tests.jsx new file mode 100644 index 0000000000..d577e3e530 --- /dev/null +++ b/frontend/components/forms/fields/Checkbox/Checkbox.tests.jsx @@ -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()).toExist(); + }); +}); diff --git a/frontend/components/forms/fields/Checkbox/_styles.scss b/frontend/components/forms/fields/Checkbox/_styles.scss new file mode 100644 index 0000000000..51262a19c9 --- /dev/null +++ b/frontend/components/forms/fields/Checkbox/_styles.scss @@ -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; + } + } +} diff --git a/frontend/components/forms/fields/Checkbox/index.js b/frontend/components/forms/fields/Checkbox/index.js new file mode 100644 index 0000000000..f3d29de1e6 --- /dev/null +++ b/frontend/components/forms/fields/Checkbox/index.js @@ -0,0 +1 @@ +export default from './Checkbox'; diff --git a/frontend/components/forms/fields/InputFieldWithIcon/InputFieldWithIcon.jsx b/frontend/components/forms/fields/InputFieldWithIcon/InputFieldWithIcon.jsx index ba5e299ed7..9b00a76f3c 100644 --- a/frontend/components/forms/fields/InputFieldWithIcon/InputFieldWithIcon.jsx +++ b/frontend/components/forms/fields/InputFieldWithIcon/InputFieldWithIcon.jsx @@ -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
{placeholder}
; } + renderHint = () => { + const { hint } = this.props; + + if (hint) { + return {hint}; + } + + 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 && } + {renderHint()}
); } diff --git a/frontend/components/forms/fields/InputFieldWithIcon/_styles.scss b/frontend/components/forms/fields/InputFieldWithIcon/_styles.scss index 56941e5d44..168bbcd53d 100644 --- a/frontend/components/forms/fields/InputFieldWithIcon/_styles.scss +++ b/frontend/components/forms/fields/InputFieldWithIcon/_styles.scss @@ -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; + } + } } diff --git a/frontend/components/forms/fields/SelectTargetsDropdown/TargetDetails/_styles.scss b/frontend/components/forms/fields/SelectTargetsDropdown/TargetDetails/_styles.scss index 109a0be625..793f772d16 100644 --- a/frontend/components/forms/fields/SelectTargetsDropdown/TargetDetails/_styles.scss +++ b/frontend/components/forms/fields/SelectTargetsDropdown/TargetDetails/_styles.scss @@ -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; } } diff --git a/frontend/components/forms/fields/SelectTargetsDropdown/_styles.scss b/frontend/components/forms/fields/SelectTargetsDropdown/_styles.scss index 89b8bc5199..b011dd57c0 100644 --- a/frontend/components/forms/fields/SelectTargetsDropdown/_styles.scss +++ b/frontend/components/forms/fields/SelectTargetsDropdown/_styles.scss @@ -181,7 +181,7 @@ } } - @include breakpoint(tablet) { + @include breakpoint(ltdesktop) { .Select-menu-outer { .Select-menu { min-width: 665px; diff --git a/frontend/interfaces/registration_form_data.js b/frontend/interfaces/registration_form_data.js index c61c347b56..f9a8fedc41 100644 --- a/frontend/interfaces/registration_form_data.js +++ b/frontend/interfaces/registration_form_data.js @@ -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, diff --git a/frontend/pages/RegistrationPage/Breadcrumbs/Breadcrumbs.jsx b/frontend/pages/RegistrationPage/Breadcrumbs/Breadcrumbs.jsx index b0488f7d8e..e2b794149b 100644 --- a/frontend/pages/RegistrationPage/Breadcrumbs/Breadcrumbs.jsx +++ b/frontend/pages/RegistrationPage/Breadcrumbs/Breadcrumbs.jsx @@ -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 ( -
- - - +
+ + +
); } diff --git a/frontend/pages/RegistrationPage/Breadcrumbs/Breadcrumbs.tests.jsx b/frontend/pages/RegistrationPage/Breadcrumbs/Breadcrumbs.tests.jsx index 1fb5a6d2d5..566e2b05d0 100644 --- a/frontend/pages/RegistrationPage/Breadcrumbs/Breadcrumbs.tests.jsx +++ b/frontend/pages/RegistrationPage/Breadcrumbs/Breadcrumbs.tests.jsx @@ -14,21 +14,21 @@ describe('Breadcrumbs - component', () => { it('renders page 1 Button as active when the page prop is 1', () => { const component = mount(); - 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(); - 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'); diff --git a/frontend/pages/RegistrationPage/Breadcrumbs/_styles.scss b/frontend/pages/RegistrationPage/Breadcrumbs/_styles.scss new file mode 100644 index 0000000000..112452407a --- /dev/null +++ b/frontend/pages/RegistrationPage/Breadcrumbs/_styles.scss @@ -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; + } + } + } + } +} diff --git a/frontend/pages/RegistrationPage/RegistrationPage.jsx b/frontend/pages/RegistrationPage/RegistrationPage.jsx index 151f4a4751..d744f9bdcd 100644 --- a/frontend/pages/RegistrationPage/RegistrationPage.jsx +++ b/frontend/pages/RegistrationPage/RegistrationPage.jsx @@ -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 ( -
+
+ Kolide
diff --git a/frontend/pages/RegistrationPage/RegistrationPage.tests.jsx b/frontend/pages/RegistrationPage/RegistrationPage.tests.jsx index ea8bc05556..3355eb40f6 100644 --- a/frontend/pages/RegistrationPage/RegistrationPage.tests.jsx +++ b/frontend/pages/RegistrationPage/RegistrationPage.tests.jsx @@ -84,6 +84,7 @@ describe('RegistrationPage - component', () => { describe('#onSetPage', () => { it('sets state to the page number', () => { const page = mount(); + page.setState({ page: 3 }); page.node.onSetPage(3); expect(page.state()).toInclude({ page: 3 }); diff --git a/frontend/pages/RegistrationPage/_styles.scss b/frontend/pages/RegistrationPage/_styles.scss new file mode 100644 index 0000000000..0624707c48 --- /dev/null +++ b/frontend/pages/RegistrationPage/_styles.scss @@ -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; + } + } +} diff --git a/frontend/pages/hosts/NewHostPage/_styles.scss b/frontend/pages/hosts/NewHostPage/_styles.scss index 9328f3df55..3f9ee7942c 100644 --- a/frontend/pages/hosts/NewHostPage/_styles.scss +++ b/frontend/pages/hosts/NewHostPage/_styles.scss @@ -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; } } diff --git a/frontend/styles/global/_global.scss b/frontend/styles/global/_global.scss index 6c0d7672b2..640d0c9563 100644 --- a/frontend/styles/global/_global.scss +++ b/frontend/styles/global/_global.scss @@ -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; } diff --git a/frontend/styles/global/_icons.scss b/frontend/styles/global/_icons.scss index 128a25459b..99cd75c9bd 100644 --- a/frontend/styles/global/_icons.scss +++ b/frontend/styles/global/_icons.scss @@ -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; diff --git a/frontend/styles/var/mixins.scss b/frontend/styles/var/mixins.scss index 9343c705a4..6480d1aa1b 100644 --- a/frontend/styles/var/mixins.scss +++ b/frontend/styles/var/mixins.scss @@ -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 { diff --git a/frontend/templates/react.tmpl b/frontend/templates/react.tmpl index 77716a9ee8..2b01131de0 100644 --- a/frontend/templates/react.tmpl +++ b/frontend/templates/react.tmpl @@ -2,6 +2,7 @@ + Kolide