Login page form submission (#157)

* API client utility

* moves test helpers to the test directory

* Utility to namespace local storage keys

* LoginSuccessfulPage component

* Check icon

* adds auth to redux state

* successful form submission

* Allow tests to load dummy SVG static images & test fixes
This commit is contained in:
Mike Stone 2016-09-13 15:50:37 -04:00 committed by Mike Arpaia
parent 6cb05a58e6
commit b638ae186d
65 changed files with 654 additions and 267 deletions

View File

@ -28,7 +28,8 @@ module.exports = {
'new-cap': 0,
'import/no-unresolved': 'error',
'linebreak-style': 0,
'import/no-named-as-default': 'off'
'import/no-named-as-default': 'off',
'import/no-named-as-default-member': 'off'
},
settings: {
'import/resolver': {

View File

@ -37,7 +37,7 @@ generate: .prefix
$(shell npm bin)/webpack --progress --colors --bail
generate-dev: .prefix
go-bindata -debug -pkg=server -o=server/bindata.go frontend/templates/ assets/ assets/images/
go-bindata -debug -pkg=server -o=server/bindata.go frontend/templates/ assets/...
$(shell npm bin)/webpack --progress --colors --bail --watch
deps:

View File

@ -0,0 +1 @@
<svg width="96" height="96" viewBox="0 0 96 96" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>User Avatar Default</title><defs><circle id="c" cx="48" cy="48" r="48"/><circle id="a" cx="48" cy="39.1" r="24"/><mask id="e" x="0" y="0" width="48" height="48" fill="#fff"><use xlink:href="#a"/></mask><circle id="b" cx="48" cy="48" r="48"/><mask id="f" x="0" y="0" width="96" height="96" fill="#fff"><use xlink:href="#b"/></mask><mask id="g" x="0" y="0" width="96" height="96" fill="#fff"><use xlink:href="#b"/></mask></defs><g fill="none" fill-rule="evenodd"><mask id="d" fill="#fff"><use xlink:href="#c"/></mask><path d="M48.267 103.1c20.03 0 36.266-11.462 36.266-25.6 0-14.138-16.237-25.6-36.266-25.6C28.237 51.9 12 63.362 12 77.5c0 14.138 16.237 25.6 36.267 25.6z" stroke="#B9C2E4" stroke-width="3" fill="#EAEEFB" mask="url(#d)"/><g mask="url(#d)" stroke-width="6" stroke="#B9C2E4" fill="#EAEEFB"><use mask="url(#e)" xlink:href="#a"/></g><g stroke="#C48DED" mask="url(#f)" stroke-width="2"><use mask="url(#g)" xlink:href="#b"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
<svg width="156" height="40" viewBox="0 0 156 40" xmlns="http://www.w3.org/2000/svg"><title>Kolide Icon Footer</title><g fill="#B4B4B4" fill-rule="evenodd"><path d="M73.396 13.867c0-2.503 2.564-3.754 7.694-3.754s7.696 1.252 7.696 3.754v11.259c0 2.502-2.565 3.753-7.696 3.753-5.13 0-7.694-1.25-7.694-3.753v-11.26zm7.694 11.259c2.566 0 3.848-.417 3.848-1.251v-8.758c0-.834-1.282-1.25-3.848-1.25-2.565.083-3.847.5-3.847 1.25v8.758c0 .834 1.282 1.251 3.847 1.251zM97.81 11.364v12.511c0 .834 1.283 1.251 3.848 1.251h2.566c.854 0 1.282.417 1.282 1.251v1.251c0 .835-.428 1.251-1.282 1.251h-2.566c-5.13 0-7.695-1.251-7.695-3.753V11.364c0-.834.428-1.25 1.283-1.25h1.282c.855 0 1.283.417 1.283 1.25M111.967 28.879c-.855 0-1.282-.417-1.282-1.251V11.364c0-.834.427-1.25 1.282-1.25h1.283c.854 0 1.282.416 1.282 1.25v16.264c0 .834-.428 1.25-1.282 1.25h-1.283v.001zM119.711 11.364c0-.834.428-1.25 1.282-1.25h6.412c5.13 0 7.696 1.25 7.696 3.752v11.26c0 2.502-2.565 3.753-7.696 3.753h-6.412c-.854 0-1.282-.417-1.282-1.25V11.363zm7.694 13.762c2.566 0 3.848-.417 3.848-1.251v-8.633c0-.834-1.282-1.25-3.848-1.25h-2.566c-.854 0-1.282.416-1.282 1.25v8.633c0 .834.428 1.251 1.282 1.251h2.566zM154.386 28.879h-6.413c-5.13 0-7.695-1.251-7.695-3.753v-11.26c0-2.502 2.565-3.752 7.695-3.752h6.413c.855 0 1.282.416 1.282 1.25v1.252c0 .834-.427 1.25-1.282 1.25h-6.413c-2.565 0-3.848.417-3.848 1.252v1.25c0 .835.428 1.252 1.283 1.252h2.565c.855 0 1.283.416 1.283 1.251v1.251c0 .834-.428 1.251-1.283 1.251h-2.565c-.855 0-1.283.417-1.283 1.251v1.251c0 .834 1.283 1.251 3.848 1.251h6.413c.855 0 1.282.418 1.282 1.251v1.251c0 .834-.427 1.251-1.282 1.251M70.316 25.786l-6.468-6.308 6.412-6.254.007-.007c.005-.006.006-.012.011-.017.583-.584.58-1.168-.018-1.753l-.91-.876c-.607-.592-1.211-.592-1.809 0l-6.558 6.398a2.317 2.317 0 0 1-1.616.653h-.056c-.855 0-1.282-.416-1.282-1.251v-5.005c0-.834-.428-1.251-1.283-1.251h-1.283c-.854 0-1.282.417-1.282 1.251V27.63c0 .834.428 1.251 1.282 1.251h1.283c.855 0 1.282-.417 1.282-1.251v-5.004c0-.835.428-1.251 1.283-1.251h.085c.606 0 1.183.232 1.612.648l5.456 5.312 1.132 1.104c.607.591 1.215.591 1.822 0l.897-.888c.608-.593.608-1.181 0-1.765M33.25 4.598a2.472 2.472 0 0 0-1.75-1.754L21.348.125a2.482 2.482 0 0 0-2.394.641l-9.06 9.059-9.128 9.129a2.48 2.48 0 0 0-.641 2.395l2.72 10.152c.23.855.897 1.523 1.753 1.752l24.844 6.658a2.479 2.479 0 0 0 2.395-.643l7.432-7.431a2.48 2.48 0 0 0 .642-2.394L33.25 4.598zm-.73 27.947h-6.498l-5.013-8.08-3.628.003 2.964 1.577.01 6.5h-5.582l-.021-15.388-3.612-1.926-.003-2.11h9.192l.009 6.955h1.28l4.404-6.954h6.497l-5.938 9.718 5.938 9.705z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

0
assets/images/.keep Normal file
View File

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@ -0,0 +1,29 @@
@font-face {
font-family: 'Oxygen';
src: url('/assets/fonts/oxygen/Oxygen-Light.eot');
src: url('/assets/fonts/oxygen/Oxygen-Light.eot?#iefix') format('embedded-opentype'),
url('/assets/fonts/oxygen/Oxygen-Light.woff') format('woff'),
url('/assets/fonts/oxygen/Oxygen-Light.ttf') format('truetype');
font-weight: 300;
font-style: normal;
}
@font-face {
font-family: 'Oxygen';
src: url('/assets/fonts/oxygen/Oxygen-Bold.eot');
src: url('/assets/fonts/oxygen/Oxygen-Bold.eot?#iefix') format('embedded-opentype'),
url('/assets/fonts/oxygen/Oxygen-Bold.woff') format('woff'),
url('/assets/fonts/oxygen/Oxygen-Bold.ttf') format('truetype');
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: 'Oxygen';
src: url('/assets/fonts/oxygen/Oxygen-Regular.eot');
src: url('/assets/fonts/oxygen/Oxygen-Regular.eot?#iefix') format('embedded-opentype'),
url('/assets/fonts/oxygen/Oxygen-Regular.woff') format('woff'),
url('/assets/fonts/oxygen/Oxygen-Regular.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}

View File

@ -5,3 +5,27 @@ const doc = jsdom.jsdom('<!doctype html><html><body></body></html>');
global.document = doc;
global.window = doc.defaultView;
global.navigator = global.window.navigator;
function mockStorage() {
const storage = {};
return {
setItem(key, value = '') {
storage[key] = value;
},
getItem(key) {
return storage[key];
},
removeItem(key) {
delete storage[key];
},
get length() {
return Object.keys(storage).length;
},
key(i) {
return Object.keys(storage)[i] || null;
},
};
}
global.localStorage = window.localStorage = mockStorage();

View File

@ -1,7 +1,6 @@
import React, { Component, PropTypes } from 'react';
import { Style } from 'radium';
import Footer from './Footer';
import componentStyles from './styles';
import globalStyles from '../../styles/global';
export class App extends Component {
@ -11,14 +10,11 @@ export class App extends Component {
render () {
const { children } = this.props;
const { containerStyles, childWrapperStyles } = componentStyles;
return (
<div style={containerStyles}>
<div>
<Style rules={globalStyles} />
<div style={childWrapperStyles}>
{children}
</div>
{children}
<Footer />
</div>
);

View File

@ -1,31 +1,15 @@
import React from 'react';
import radium from 'radium';
import Icon from '../icons/Icon';
import styles from '../../styles';
import componentStyles from './styles';
import footerLogo from './footer-logo.svg';
const { color, padding } = styles;
const { footerStyles } = componentStyles;
const Footer = () => {
const style = {
container: {
alignItems: 'center',
backgroundColor: color.darkGrey,
display: 'flex',
justifyContent: 'center',
height: '74px',
},
textLogo: {
height: '20px',
marginLeft: padding.base,
width: '104px',
},
};
return (
<div style={style.container}>
<Icon name="kolideLogo" />
<Icon name="kolideText" style={style.textLogo} variant="lightGrey" />
</div>
<footer style={footerStyles}>
<img alt="Kolide logo" src={footerLogo} />
</footer>
);
};

View File

@ -4,17 +4,7 @@ import { mount } from 'enzyme';
import Footer from './Footer';
describe('Footer - component', () => {
it('renders the Kolide logo', () => {
const footer = mount(<Footer />);
const kolideLogo = footer.find('KolideLogo');
expect(kolideLogo.length).toEqual(1);
});
it('renders the Kolide text logo', () => {
const footer = mount(<Footer />);
const kolideTextLogo = footer.find('KolideText');
expect(kolideTextLogo.length).toEqual(1);
it('renders', () => {
expect(mount(<Footer />)).toExist();
});
});

View File

@ -0,0 +1 @@
<svg width="156" height="40" viewBox="0 0 156 40" xmlns="http://www.w3.org/2000/svg"><title>Kolide Icon Footer</title><g fill="#B4B4B4" fill-rule="evenodd"><path d="M73.396 13.867c0-2.503 2.564-3.754 7.694-3.754s7.696 1.252 7.696 3.754v11.259c0 2.502-2.565 3.753-7.696 3.753-5.13 0-7.694-1.25-7.694-3.753v-11.26zm7.694 11.259c2.566 0 3.848-.417 3.848-1.251v-8.758c0-.834-1.282-1.25-3.848-1.25-2.565.083-3.847.5-3.847 1.25v8.758c0 .834 1.282 1.251 3.847 1.251zM97.81 11.364v12.511c0 .834 1.283 1.251 3.848 1.251h2.566c.854 0 1.282.417 1.282 1.251v1.251c0 .835-.428 1.251-1.282 1.251h-2.566c-5.13 0-7.695-1.251-7.695-3.753V11.364c0-.834.428-1.25 1.283-1.25h1.282c.855 0 1.283.417 1.283 1.25M111.967 28.879c-.855 0-1.282-.417-1.282-1.251V11.364c0-.834.427-1.25 1.282-1.25h1.283c.854 0 1.282.416 1.282 1.25v16.264c0 .834-.428 1.25-1.282 1.25h-1.283v.001zM119.711 11.364c0-.834.428-1.25 1.282-1.25h6.412c5.13 0 7.696 1.25 7.696 3.752v11.26c0 2.502-2.565 3.753-7.696 3.753h-6.412c-.854 0-1.282-.417-1.282-1.25V11.363zm7.694 13.762c2.566 0 3.848-.417 3.848-1.251v-8.633c0-.834-1.282-1.25-3.848-1.25h-2.566c-.854 0-1.282.416-1.282 1.25v8.633c0 .834.428 1.251 1.282 1.251h2.566zM154.386 28.879h-6.413c-5.13 0-7.695-1.251-7.695-3.753v-11.26c0-2.502 2.565-3.752 7.695-3.752h6.413c.855 0 1.282.416 1.282 1.25v1.252c0 .834-.427 1.25-1.282 1.25h-6.413c-2.565 0-3.848.417-3.848 1.252v1.25c0 .835.428 1.252 1.283 1.252h2.565c.855 0 1.283.416 1.283 1.251v1.251c0 .834-.428 1.251-1.283 1.251h-2.565c-.855 0-1.283.417-1.283 1.251v1.251c0 .834 1.283 1.251 3.848 1.251h6.413c.855 0 1.282.418 1.282 1.251v1.251c0 .834-.427 1.251-1.282 1.251M70.316 25.786l-6.468-6.308 6.412-6.254.007-.007c.005-.006.006-.012.011-.017.583-.584.58-1.168-.018-1.753l-.91-.876c-.607-.592-1.211-.592-1.809 0l-6.558 6.398a2.317 2.317 0 0 1-1.616.653h-.056c-.855 0-1.282-.416-1.282-1.251v-5.005c0-.834-.428-1.251-1.283-1.251h-1.283c-.854 0-1.282.417-1.282 1.251V27.63c0 .834.428 1.251 1.282 1.251h1.283c.855 0 1.282-.417 1.282-1.251v-5.004c0-.835.428-1.251 1.283-1.251h.085c.606 0 1.183.232 1.612.648l5.456 5.312 1.132 1.104c.607.591 1.215.591 1.822 0l.897-.888c.608-.593.608-1.181 0-1.765M33.25 4.598a2.472 2.472 0 0 0-1.75-1.754L21.348.125a2.482 2.482 0 0 0-2.394.641l-9.06 9.059-9.128 9.129a2.48 2.48 0 0 0-.641 2.395l2.72 10.152c.23.855.897 1.523 1.753 1.752l24.844 6.658a2.479 2.479 0 0 0 2.395-.643l7.432-7.431a2.48 2.48 0 0 0 .642-2.394L33.25 4.598zm-.73 27.947h-6.498l-5.013-8.08-3.628.003 2.964 1.577.01 6.5h-5.582l-.021-15.388-3.612-1.926-.003-2.11h9.192l.009 6.955h1.28l4.404-6.954h6.497l-5.938 9.718 5.938 9.705z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -1,9 +1,17 @@
import styles from '../../styles';
const { color } = styles;
export default {
containerStyles: {
minHeight: '100vh',
},
childWrapperStyles: {
minHeight: '100vh',
marginBottom: '-74px',
footerStyles: {
alignItems: 'center',
backgroundColor: color.darkGrey,
height: '55px',
textAlign: 'center',
position: 'absolute',
paddingTop: '15px',
left: '0',
right: '0',
bottom: '0',
},
};

View File

@ -1,7 +1,7 @@
import React, { Component, PropTypes } from 'react';
import radium from 'radium';
import componentStyles from './styles';
import Icon from '../../icons/Icon';
import avatar from './avatar.svg';
import InputFieldWithIcon from '../fields/InputFieldWithIcon';
class LoginForm extends Component {
@ -14,7 +14,7 @@ class LoginForm extends Component {
this.state = {
formData: {
email: null,
username: null,
password: null,
},
};
@ -34,27 +34,38 @@ class LoginForm extends Component {
};
}
onFormSubmit = () => {
const { formData } = this.state;
const { onSubmit } = this.props;
onFormSubmit = (evt) => {
evt.preventDefault();
return onSubmit(formData);
if (this.canSubmit()) {
const { formData } = this.state;
const { onSubmit } = this.props;
return onSubmit(formData);
}
return false;
}
canSubmit = () => {
const { formData: { username, password } } = this.state;
return username && password;
}
render () {
const { containerStyles, submitButtonStyles, userIconStyles } = componentStyles;
const { containerStyles, submitButtonStyles, formStyles } = componentStyles;
const { onInputChange, onFormSubmit } = this;
const { formData } = this.state;
const canSubmit = formData.email && formData.password;
const canSubmit = this.canSubmit();
return (
<div>
<form onSubmit={onFormSubmit} style={formStyles}>
<div style={containerStyles}>
<Icon name="user" variant="circle" style={userIconStyles} />
<img alt="Avatar" src={avatar} />
<InputFieldWithIcon
iconName="user"
name="email"
onChange={onInputChange('email')}
name="username"
onChange={onInputChange('username')}
placeholder="Username or Email"
/>
<InputFieldWithIcon
@ -66,13 +77,12 @@ class LoginForm extends Component {
/>
</div>
<button
disabled={!canSubmit}
onClick={onFormSubmit}
style={submitButtonStyles(canSubmit)}
type="submit"
>
Login
</button>
</div>
</form>
);
}
}

View File

@ -3,7 +3,7 @@ import expect, { createSpy, restoreSpies } from 'expect';
import { mount } from 'enzyme';
import { noop } from 'lodash';
import LoginForm from './LoginForm';
import { fillInFormInput } from '../../../utilities/testHelpers.js';
import { fillInFormInput } from '../../../test/helpers';
describe('LoginForm - component', () => {
afterEach(restoreSpies);
@ -14,16 +14,15 @@ describe('LoginForm - component', () => {
expect(form.find('InputFieldWithIcon').length).toEqual(2);
});
it('updates component state when the email field is changed', () => {
it('updates component state when the username field is changed', () => {
const form = mount(<LoginForm onSubmit={noop} />);
const username = 'hi@thegnar.co';
const emailField = form.find({ name: 'email' });
fillInFormInput(emailField, 'hello');
const usernameField = form.find({ name: 'username' });
fillInFormInput(usernameField, username);
const { formData } = form.state();
expect(formData).toContain({
email: 'hello',
});
expect(formData).toContain({ username });
});
it('updates component state when the password field is changed', () => {
@ -45,23 +44,25 @@ describe('LoginForm - component', () => {
submitBtn.simulate('click');
expect(submitBtn.prop('disabled')).toEqual(true);
expect(submitBtn.prop('style')).toInclude({
cursor: 'not-allowed',
});
expect(submitSpy).toNotHaveBeenCalled();
});
it('submits the form data when the submit button is clicked', () => {
it('submits the form data when form is submitted', () => {
const submitSpy = createSpy();
const form = mount(<LoginForm onSubmit={submitSpy} />);
const emailField = form.find({ name: 'email' });
const usernameField = form.find({ name: 'username' });
const passwordField = form.find({ name: 'password' });
const submitBtn = form.find('button');
fillInFormInput(emailField, 'my@email.com');
fillInFormInput(usernameField, 'my@email.com');
fillInFormInput(passwordField, 'p@ssw0rd');
submitBtn.simulate('click');
submitBtn.simulate('submit');
expect(submitSpy).toHaveBeenCalledWith({
email: 'my@email.com',
username: 'my@email.com',
password: 'p@ssw0rd',
});
});

View File

@ -0,0 +1 @@
<svg width="96" height="96" viewBox="0 0 96 96" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>User Avatar Default</title><defs><circle id="c" cx="48" cy="48" r="48"/><circle id="a" cx="48" cy="39.1" r="24"/><mask id="e" x="0" y="0" width="48" height="48" fill="#fff"><use xlink:href="#a"/></mask><circle id="b" cx="48" cy="48" r="48"/><mask id="f" x="0" y="0" width="96" height="96" fill="#fff"><use xlink:href="#b"/></mask><mask id="g" x="0" y="0" width="96" height="96" fill="#fff"><use xlink:href="#b"/></mask></defs><g fill="none" fill-rule="evenodd"><mask id="d" fill="#fff"><use xlink:href="#c"/></mask><path d="M48.267 103.1c20.03 0 36.266-11.462 36.266-25.6 0-14.138-16.237-25.6-36.266-25.6C28.237 51.9 12 63.362 12 77.5c0 14.138 16.237 25.6 36.267 25.6z" stroke="#B9C2E4" stroke-width="3" fill="#EAEEFB" mask="url(#d)"/><g mask="url(#d)" stroke-width="6" stroke="#B9C2E4" fill="#EAEEFB"><use mask="url(#e)" xlink:href="#a"/></g><g stroke="#C48DED" mask="url(#f)" stroke-width="2"><use mask="url(#g)" xlink:href="#b"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,8 +1,7 @@
import styles from '../../../styles';
const { border, color, font, padding } = styles;
const FORM_WIDTH = '480px';
const FORM_WIDTH = '460px';
export default {
containerStyles: {
@ -10,37 +9,42 @@ export default {
backgroundColor: color.white,
borderTopLeftRadius: border.radius.base,
borderTopRightRadius: border.radius.base,
boxShadow: '0 0 30px 0 rgba(0,0,0,0.30)',
boxSizing: 'border-box',
display: 'flex',
flexDirection: 'column',
padding: padding.base,
width: FORM_WIDTH,
minHeight: '350px',
},
formStyles: {
boxShadow: '0 5px 30px 0 rgba(0,0,0,0.30)',
},
submitButtonStyles: (canSubmit) => {
const bgColor = {
start: canSubmit ? '#7166D9' : '#B2B2B2',
end: canSubmit ? '#C86DD7' : '#C7B7C9',
};
const cursor = canSubmit ? 'pointer' : 'not-allowed';
return {
backgroundImage: `linear-gradient(to bottom right, ${bgColor.start}, ${bgColor.end})`,
backgroundImage: 'linear-gradient(134deg, #7166D9 0%, #C86DD7 100%)',
border: 'none',
cursor,
borderBottomLeftRadius: border.radius.base,
borderBottomRightRadius: border.radius.base,
boxSizing: 'border-box',
color: color.white,
cursor: canSubmit ? 'pointer' : 'not-allowed',
fontSize: font.large,
letterSpacing: '4px',
padding: padding.base,
fontWeight: '300',
textTransform: 'uppercase',
width: FORM_WIDTH,
boxShadow: '0 3px 0 #734893',
position: 'relative',
':active': {
top: '2px',
boxShadow: '0 1px 0 #734893, 0 -2px 0 #D1D9E9',
},
':focus': {
outline: 'none',
},
};
},
userIconStyles: {
},
};

View File

@ -20,7 +20,7 @@ export default {
borderBottomWidth: '1px',
borderBottomStyle: 'solid',
borderBottomColor: color.brightPurple,
color: color.purple,
color: '#A2A1C8',
width: '378px',
':focus': {
outline: 'none',

View File

@ -1,8 +1,7 @@
import React, { Component, PropTypes } from 'react';
import radium from 'radium';
import Check from './svg/Check';
import KolideLoginBackground from './svg/KolideLoginBackground';
import KolideLogo from './svg/KolideLogo';
import KolideText from './svg/KolideText';
import Lock from './svg/Lock';
import User from './svg/User';
@ -15,9 +14,8 @@ class Icon extends Component {
};
static iconNames = {
check: Check,
kolideLoginBackground: KolideLoginBackground,
kolideLogo: KolideLogo,
kolideText: KolideText,
lock: Lock,
user: User,
};

View File

@ -0,0 +1,43 @@
import React, { Component } from 'react';
import base, { basePropTypes } from '../base';
class Check extends Component {
static propTypes = {
...basePropTypes,
};
static variants = {
default: (
<g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
<g transform="translate(-452.000000, -373.000000)">
<g transform="translate(310.000000, 226.000000)">
<g transform="translate(0.000000, 83.500000)">
<g transform="translate(192.000000, 114.000000) rotate(-315.000000) translate(-192.000000, -114.000000) translate(142.000000, 64.000000)">
<circle fill="#4ED061" cx="49.7056816" cy="49.7056816" r="49.2991274" />
<path d="M68.850926,68.2514103 L68.8702874,68.2707717 L74.3744581,62.7666011 L34.0105401,22.4026832 L28.5063695,27.9068538 L63.3467554,62.7472397 L48.7968715,77.2971236 L54.3010421,82.8012943 L68.850926,68.2514103 Z" fill="#FFFFFF" transform="translate(51.440414, 52.601989) rotate(45.000000) translate(-51.440414, -52.601989) " />
</g>
</g>
</g>
</g>
</g>
),
};
render () {
const { alt, style, variant } = this.props;
return (
<svg
width="100px"
height="100px"
viewBox="0 0 100 100"
alt={alt}
style={style}
>
{Check.variants[variant]}
</svg>
);
}
}
export default base(Check);

View File

@ -0,0 +1,2 @@
export default from './Check.svg.jsx';

View File

@ -1,47 +0,0 @@
import React, { Component } from 'react';
import base, { basePropTypes } from '../base';
import color from '../../../../styles/color';
class KolideLogo extends Component {
static propTypes = {
...basePropTypes,
};
static variants = {
default: {
logo: color.lightGrey,
logoFill: color.darkGrey,
},
};
render () {
const { alt, style, variant } = this.props;
const fill = KolideLogo.variants[variant];
return (
<svg
width="40px"
height="41px"
viewBox="0 0 40 41"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
alt={alt}
style={style}
>
<g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
<g transform="translate(-434.000000, -1083.000000)">
<g transform="translate(0.000000, 1065.500000)">
<g transform="translate(434.000000, 18.000000)">
<path d="M33.25,4.5975082 C33.0240516,3.74168852 32.3545434,3.07455738 31.4997073,2.84447541 L21.3483959,0.125295082 C20.4927401,-0.103557377 19.5800352,0.14095082 18.9538057,0.766196721 C18.9538057,0.766196721 12.9144843,6.80532761 9.89482359,9.82489305 L0.765772915,18.9536557 C0.139625374,19.580459 -0.104800856,20.4933279 0.124707341,21.3490656 L2.84478931,31.5009508 C3.0740516,32.3563607 3.74200242,33.0239836 4.59757619,33.252918 L29.4420844,39.910623 C30.2971664,40.1395574 31.210609,39.8951311 31.8367565,39.2682459 L39.2687237,31.8369344 C39.8948713,31.2112787 40.1392975,30.2985738 39.9108549,29.4431639 L33.25,4.5975082 Z" fill={fill.logo} />
<polygon fill={fill.logoFill} points="32.5193279 32.5449508 26.0222787 32.5449508 21.0085902 24.4649508 17.3807213 24.4679836 20.3447377 26.0453607 20.3551475 32.5449508 14.7725246 32.5449508 14.751623 17.1566721 11.1396557 15.2310164 11.1368689 13.1216721 20.3288361 13.1216721 20.3376885 20.0761803 21.6184262 20.0761803 26.0222787 13.1216721 32.5193279 13.1216721 26.5808852 22.8397869" />
</g>
</g>
</g>
</g>
</svg>
);
}
}
export default base(KolideLogo);

View File

@ -1 +0,0 @@
export default from './KolideLogo.svg.jsx';

View File

@ -1,47 +0,0 @@
import React, { Component } from 'react';
import base, { basePropTypes } from '../base';
import color from '../../../../styles/color';
export class KolideText extends Component {
static propTypes = {
...basePropTypes,
};
static variants = {
default: color.logoPurple,
lightGrey: color.lightGrey,
};
render () {
const { alt, style, variant } = this.props;
const fill = KolideText.variants[variant];
return (
<svg
width="286px"
height="52px"
viewBox="0 0 286 52"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
alt={alt}
style={style}
>
<g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
<g transform="translate(-356.000000, -227.000000)" fill={fill}>
<g transform="translate(356.000000, 226.500000)">
<path d="M54.6336418,10.9723591 C54.6336418,4.07452486 61.8146409,0.625607736 76.17951,0.625607736 C90.5472497,0.625607736 97.7296843,4.07452486 97.7296843,10.9723591 L97.7296843,42.0139917 C97.7296843,48.9132044 90.5472497,52.3621215 76.17951,52.3621215 C61.8146409,52.3621215 54.6336418,48.9132044 54.6336418,42.0139917 L54.6336418,10.9723591 L54.6336418,10.9723591 Z M76.17951,42.0139917 C83.3648152,42.0139917 86.9560325,40.8643526 86.9560325,38.5650745 L86.9560325,14.4212762 C86.9560325,12.1219981 83.3648152,10.9723591 76.17951,10.9723591 C68.9970755,11.2025626 65.4058582,12.3508232 65.4058582,14.4212762 L65.4058582,38.5650745 C65.4058582,40.8643526 68.9970755,42.0139917 76.17951,42.0139917 L76.17951,42.0139917 Z" />
<path d="M123.003127,4.07452486 L123.003127,38.5650745 C123.003127,40.8643526 126.594344,42.0139917 133.776779,42.0139917 L140.962084,42.0139917 C143.354793,42.0139917 144.551866,43.1636307 144.551866,45.4629088 L144.551866,48.9118259 C144.551866,51.2138609 143.354793,52.360743 140.962084,52.360743 L133.776779,52.360743 C119.41191,52.360743 112.229475,48.9118259 112.229475,42.0139917 L112.229475,4.07452486 C112.229475,1.77524678 113.427983,0.625607736 115.820692,0.625607736 L119.41191,0.625607736 C121.806055,0.6269862 123.003127,1.77662524 123.003127,4.07452486" />
<path d="M162.645745,52.360743 C160.2516,52.360743 159.054527,51.211104 159.054527,48.9118259 L159.054527,4.07452486 C159.054527,1.77524678 160.2516,0.625607736 162.645745,0.625607736 L166.236962,0.625607736 C168.628236,0.625607736 169.828179,1.77524678 169.828179,4.07452486 L169.828179,48.9104474 C169.828179,51.2097255 168.628236,52.3593646 166.236962,52.3593646 L162.645745,52.3593646 L162.645745,52.360743 Z" />
<path d="M184.330841,4.07452486 C184.330841,1.77386831 185.527913,0.625607736 187.922058,0.625607736 L205.876709,0.625607736 C220.244449,0.625607736 227.426883,4.07452486 227.426883,10.9723591 L227.426883,42.0139917 C227.426883,48.9132044 220.244449,52.3621215 205.876709,52.3621215 L187.922058,52.3621215 C185.527913,52.3621215 184.330841,51.211104 184.330841,48.9132044 L184.330841,4.07452486 L184.330841,4.07452486 Z M205.875274,42.0139917 C213.060579,42.0139917 216.651796,40.8643526 216.651796,38.5650745 L216.651796,14.7658922 C216.651796,12.4666142 213.060579,11.3169751 205.875274,11.3169751 L198.691404,11.3169751 C196.30013,11.3169751 195.100187,12.4666142 195.100187,14.7658922 L195.100187,38.5650745 C195.100187,40.8643526 196.30013,42.0139917 198.691404,42.0139917 L205.875274,42.0139917 L205.875274,42.0139917 Z" />
<path d="M281.431499,52.360743 L263.472542,52.360743 C249.107673,52.360743 241.925239,48.9118259 241.925239,42.0139917 L241.925239,10.9723591 C241.925239,4.07452486 249.107673,0.625607736 263.472542,0.625607736 L281.431499,0.625607736 C283.825644,0.625607736 285.022717,1.77524678 285.022717,4.07452486 L285.022717,7.52344198 C285.022717,9.82272006 283.825644,10.9723591 281.431499,10.9723591 L263.472542,10.9723591 C256.290108,10.9723591 252.698891,12.1206197 252.698891,14.4212762 L252.698891,17.8701933 C252.698891,20.1708499 253.895963,21.3204889 256.290108,21.3204889 L263.472542,21.3204889 C265.866687,21.3204889 267.06663,22.4687495 267.06663,24.769406 L267.06663,28.2183232 C267.06663,30.5176012 265.866687,31.6672403 263.472542,31.6672403 L256.290108,31.6672403 C253.895963,31.6672403 252.698891,32.8168793 252.698891,35.1161574 L252.698891,38.5650745 C252.698891,40.8643526 256.290108,42.0139917 263.472542,42.0139917 L281.431499,42.0139917 C283.825644,42.0139917 285.022717,43.1663876 285.022717,45.4629088 L285.022717,48.9118259 C285.022717,51.211104 283.825644,52.360743 281.431499,52.360743" />
<path d="M46.0086919,43.8335642 L27.897589,26.4428614 L45.85224,9.20103276 L45.8708994,9.18311273 C45.8852528,9.16657116 45.8895588,9.14865113 45.9039122,9.13486648 C47.5358922,7.52482044 47.5287155,5.9147744 45.85224,4.30197143 L43.3030786,1.88690237 C41.6036376,0.253422435 39.9128087,0.253422435 38.2392039,1.88690237 L19.874046,19.5257287 C18.6726676,20.6781247 17.0449936,21.3260028 15.346988,21.3260028 L15.1919714,21.3260028 C12.7978266,21.3260028 11.6007541,20.1777422 11.6007541,17.8770857 L11.6007541,4.07866025 C11.6007541,1.77938217 10.4036817,0.629743129 8.00953687,0.629743129 L4.41688427,0.629743129 C2.02417477,0.629743129 0.827102344,1.77938217 0.827102344,4.07866025 L0.827102344,48.9173398 C0.827102344,51.2166178 2.02417477,52.3662569 4.41688427,52.3662569 L8.00810154,52.3662569 C10.4022464,52.3662569 11.5993188,51.2166178 11.5993188,48.9173398 L11.5993188,35.1216713 C11.5993188,32.8210147 12.7963912,31.6727541 15.1905361,31.6727541 L15.4288023,31.6727541 C17.1253725,31.6727541 18.7429992,32.310983 19.9429423,33.4592436 C23.4250738,36.7951267 30.9347656,43.996223 35.2221221,48.1040459 C37.1239457,49.9291324 38.3927851,51.1463162 38.3927851,51.1463162 C40.092226,52.7784176 41.7945377,52.7784176 43.4939786,51.1463162 L46.0072566,48.6995424 C47.7081328,47.064684 47.7081328,45.4436103 46.0086919,43.8335642" />
</g>
</g>
</g>
</svg>
);
}
}
export default base(KolideText);

View File

@ -1 +0,0 @@
export default from './KolideText.svg.jsx';

View File

@ -21,45 +21,6 @@ export class User extends Component {
</g>
</g>
),
circle: (
<g>
<defs>
<circle id="path-1" cx="48" cy="48" r="48" />
<mask id="mask-2" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="-1" y="-1" width="98" height="98">
<rect x="-1" y="-1" width="98" height="98" fill="white" />
<use xlinkHref="#path-1" fill="black" />
</mask>
<mask id="mask-4" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="-1" y="-1" width="98" height="98">
<rect x="-1" y="-1" width="98" height="98" fill="white" />
<use xlinkHref="#path-1" fill="black" />
</mask>
<circle id="path-5" cx="48" cy="39.6" r="24" />
<mask id="mask-6" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="48" height="48" fill="white">
<use xlinkHref="#path-5" />
</mask>
</defs>
<g id="Page-1" stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
<g id="Kolide-App-Login-Base-State" transform="translate(-453.000000, -374.000000)">
<g id="Page-Content">
<g id="Child-Div" transform="translate(272.000000, 340.000000)">
<g id="Oval-+-Oval-Mask" transform="translate(182.000000, 35.500000)">
<mask id="mask-3" fill="white">
<use xlinkHref="#path-1" />
</mask>
<g id="Mask" stroke="#B9C2E4" mask="url(#mask-2)" strokeWidth="2">
<use mask="url(#mask-4)" xlinkHref="#path-1" />
</g>
<path d="M48.2666667,103.6 C68.2961936,103.6 84.5333333,92.1384896 84.5333333,78 C84.5333333,63.8615104 68.2961936,52.4 48.2666667,52.4 C28.2371397,52.4 12,63.8615104 12,78 C12,92.1384896 28.2371397,103.6 48.2666667,103.6 Z" id="Oval" stroke="#D2DAF4" strokeWidth="3" fill="#EAEEFB" mask="url(#mask-3)" />
<g id="Oval" mask="url(#mask-3)" strokeWidth="6" stroke="#D2DAF4" fill="#EAEEFB">
<use mask="url(#mask-6)" xlinkHref="#path-5" />
</g>
</g>
</g>
</g>
</g>
</g>
</g>
),
colored: (
<g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
<g transform="translate(-672.000000, -519.000000)" fill="#EED6FF" stroke="#C48DED">

View File

@ -0,0 +1,5 @@
import { environments } from './index.js';
export default {
env: environments.development,
};

View File

@ -0,0 +1,5 @@
import { environments } from './index.js';
export default {
env: environments.production,
};

9
frontend/config/index.js Normal file
View File

@ -0,0 +1,9 @@
export const environments = {
development: 'DEV',
production: 'PROD',
};
const env = process.env.NODE_ENV || environments.development;
const configFileLocation = `./config.${env}.js`;
const settings = require(configFileLocation).default;
export default { environments, settings };

View File

@ -0,0 +1,3 @@
export default {
LOGIN: '/v1/kolide/login',
};

62
frontend/kolide/index.js Normal file
View File

@ -0,0 +1,62 @@
import fetch from 'isomorphic-fetch';
import config from '../config';
import endpoints from './endpoints';
import local from '../utilities/local';
class Kolide {
constructor () {
this.baseURL = this.setBaseURL();
}
setBaseURL () {
const {
settings: { env },
environments: { development },
} = config;
if (env === development) {
return 'http://localhost:8080/api';
}
throw new Error(`API base URL is not configured for environment: ${env}`);
}
setBearerToken (bearerToken) {
this.bearerToken = bearerToken;
}
loginUser ({ username, password }) {
const { LOGIN } = endpoints;
const endpoint = this.baseURL + LOGIN;
return fetch(endpoint, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password }),
})
.then(response => {
return response.json()
.then(user => {
if (response.ok) {
const { token } = user;
local.setItem('auth_token', token);
this.setBearerToken(token);
return user;
}
const error = new Error(response.statusText);
error.response = response;
error.message = user.error;
throw error;
});
});
}
}
export default new Kolide();

View File

@ -0,0 +1,28 @@
import expect from 'expect';
import Kolide from './index';
import { validLoginRequest } from '../test/mocks';
describe('Kolide - API client', () => {
describe('defaults', () => {
it('sets the base URL', () => {
expect(Kolide.baseURL).toEqual('http://localhost:8080/api');
});
});
describe('#loginUser', () => {
it('sets the bearer token', (done) => {
const request = validLoginRequest();
Kolide.loginUser({
username: 'admin',
password: 'secret',
})
.then(() => {
expect(request.isDone()).toEqual(true);
expect(Kolide.bearerToken).toEqual('auth_token');
done();
})
.catch(done);
});
});
});

View File

@ -1,19 +1,40 @@
import React, { Component } from 'react';
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { push } from 'react-router-redux';
import componentStyles from './styles';
import Icon from '../../components/icons/Icon';
import { loadBackground, resizeBackground } from '../../utilities/backgroundImage';
import local from '../../utilities/local';
import LoginForm from '../../components/forms/LoginForm';
import { loginUser } from '../../redux/nodes/auth/actions';
export class LoginPage extends Component {
static propTypes = {
dispatch: PropTypes.func,
error: PropTypes.string,
loading: PropTypes.bool,
user: PropTypes.object,
};
componentWillMount () {
const { dispatch } = this.props;
const { window } = global;
if (local.getItem('auth_token')) {
return dispatch(push('/'));
}
loadBackground();
window.onresize = resizeBackground;
return false;
}
onSubmit = (formData) => {
console.log('formData', formData);
const { dispatch } = this.props;
return dispatch(loginUser(formData))
.then(() => {
return dispatch(push('/login_successful'));
});
}
render () {
@ -23,7 +44,7 @@ export class LoginPage extends Component {
return (
<div style={containerStyles}>
<div style={formWrapperStyles}>
<Icon name="kolideText" />
<img alt="Kolide text logo" src="/assets/images/kolide-logo-text.svg" />
<div style={whiteTabStyles} />
<LoginForm onSubmit={onSubmit} />
</div>
@ -32,4 +53,14 @@ export class LoginPage extends Component {
}
}
export default LoginPage;
const mapStateToProps = (state) => {
const { error, loading, user } = state.auth;
return {
error,
loading,
user,
};
};
export default connect(mapStateToProps)(LoginPage);

View File

@ -1,9 +1,10 @@
import React from 'react';
import expect, { spyOn, restoreSpies } from 'expect';
import { mount } from 'enzyme';
import { noop } from 'lodash';
import LoginPage from './LoginPage';
import * as bgImageUtility from '../../utilities/backgroundImage';
import { connectedComponent, reduxMockStore } from '../../test/helpers';
import local from '../../utilities/local';
import LoginPage from './LoginPage';
describe('LoginPage - component', () => {
beforeEach(() => {
@ -13,17 +14,35 @@ describe('LoginPage - component', () => {
afterEach(restoreSpies);
it('renders the LoginForm', () => {
const page = mount(<LoginPage />);
context('when the user is not logged in', () => {
const mockStore = reduxMockStore({ auth: {} });
expect(page.find('LoginForm').length).toEqual(1);
it('renders the LoginForm', () => {
const page = mount(connectedComponent(LoginPage, { mockStore }));
expect(page.find('LoginForm').length).toEqual(1);
});
});
it('render the Kolide Text logo', () => {
const page = mount(<LoginPage />);
context('when the user is logged in', () => {
beforeEach(() => {
local.setItem('auth_token', 'fake-auth-token');
});
expect(page.find('Icon').first().props()).toInclude({
name: 'kolideText',
const user = { id: 1, firstName: 'Bill', lastName: 'Shakespeare' };
it('redirects to the home page', () => {
const mockStore = reduxMockStore({ auth: { user } });
const redirectAction = {
type: '@@router/CALL_HISTORY_METHOD',
payload: {
method: 'push',
args: ['/'],
},
};
mount(connectedComponent(LoginPage, { mockStore }));
expect(mockStore.getActions()).toInclude(redirectAction);
});
});
});

View File

@ -7,7 +7,7 @@ export default {
alignItems: 'center',
display: 'flex',
flexDirection: 'column',
paddingTop: '10%',
paddingTop: '80px',
},
formWrapperStyles: {
alignItems: 'center',
@ -17,8 +17,11 @@ export default {
},
whiteTabStyles: {
backgroundColor: color.white,
height: '20px',
height: '30px',
marginTop: padding.base,
width: '384px',
borderTopLeftRadius: '4px',
borderTopRightRadius: '4px',
boxShadow: '0 5px 30px 0 rgba(0,0,0,0.3)',
width: '400px',
},
};

View File

@ -0,0 +1,32 @@
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import componentStyles from './styles';
import Icon from '../../components/icons/Icon';
import { removeBackground } from '../../utilities/backgroundImage';
export class LoginSuccessfulPage extends Component {
static propTypes = {
dispatch: PropTypes.func,
};
componentWillUnmount () {
removeBackground();
}
render () {
const { containerStyles, loginSuccessStyles, subtextStyles, whiteBoxStyles } = componentStyles;
return (
<div style={containerStyles}>
<img alt="Kolide text logo" src="/assets/images/kolide-logo-text.svg" />
<div style={whiteBoxStyles}>
<Icon name="check" />
<p style={loginSuccessStyles}>Login successful</p>
<p style={subtextStyles}>Hold on to your butts.</p>
</div>
</div>
);
}
}
export default connect()(LoginSuccessfulPage);

View File

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

View File

@ -0,0 +1,32 @@
import styles from '../../styles';
const { color, font, padding } = styles;
export default {
containerStyles: {
paddingTop: '100px',
textAlign: 'center',
},
loginSuccessStyles: {
color: color.green,
textTransform: 'uppercase',
fontSize: font.large,
letterSpacing: '2px',
fontWeight: '300',
},
subtextStyles: {
fontSize: font.medium,
color: color.lightGrey,
},
whiteBoxStyles: {
backgroundColor: color.white,
margin: '0 auto',
boxShadow: '0 5px 30px 0 rgba(0,0,0,0.30)',
borderRadius: '4px',
marginTop: padding.base,
padding: padding.base,
paddingTop: padding.most,
textAlign: 'center',
width: '384px',
},
};

View File

@ -0,0 +1,41 @@
import Kolide from '../../../kolide';
export const LOGIN_REQUEST = 'LOGIN_REQUEST';
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
export const LOGIN_FAILURE = 'LOGIN_FAILURE';
export const loginRequest = { type: LOGIN_REQUEST };
export const loginSuccess = (user) => {
return {
type: LOGIN_SUCCESS,
payload: {
data: user,
},
};
};
export const loginFailure = (error) => {
return {
type: LOGIN_FAILURE,
payload: {
error,
},
};
};
// formData should be { username: <string>, password: <string> }
export const loginUser = (formData) => {
return (dispatch) => {
return new Promise((resolve, reject) => {
dispatch(loginRequest);
Kolide.loginUser(formData)
.then(user => {
dispatch(loginSuccess(user));
return resolve(user);
})
.catch(error => {
dispatch(loginFailure(error.message));
return reject(error);
});
});
};
};

View File

@ -0,0 +1,37 @@
import {
LOGIN_FAILURE,
LOGIN_REQUEST,
LOGIN_SUCCESS,
} from './actions';
export const initialState = {
loading: false,
error: null,
user: null,
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case LOGIN_REQUEST:
return {
...state,
loading: true,
};
case LOGIN_SUCCESS:
return {
...state,
loading: false,
user: action.payload.data,
};
case LOGIN_FAILURE:
return {
...state,
loading: false,
error: action.payload.error,
};
default:
return state;
}
};
export default reducer;

View File

@ -0,0 +1,20 @@
import expect from 'expect';
import reducer, { initialState } from './reducer';
import { loginRequest } from './actions';
describe('Auth - reducer', () => {
it('sets the initial state', () => {
const state = reducer(undefined, { type: 'FOO' });
expect(state).toEqual(initialState);
});
it('changes loading to true for the userLogin action', () => {
const state = reducer(initialState, loginRequest);
expect(state).toEqual({
...initialState,
loading: true,
});
});
});

View File

@ -1,8 +1,10 @@
import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
import app from './nodes/app/reducer';
import auth from './nodes/auth/reducer';
export default combineReducers({
app,
auth,
routing: routerReducer,
});

View File

@ -6,6 +6,7 @@ import { syncHistoryWithStore } from 'react-router-redux';
import App from '../components/App';
import HomePage from '../pages/HomePage';
import LoginPage from '../pages/LoginPage';
import LoginSuccessfulPage from '../pages/LoginSuccessfulPage';
import store from '../redux/store';
const history = syncHistoryWithStore(browserHistory, store);
@ -16,6 +17,7 @@ const routes = (
<Route path="/" component={radium(App)}>
<IndexRoute component={radium(HomePage)} />
<Route path="login" component={radium(LoginPage)} />
<Route path="login_successful" component={radium(LoginSuccessfulPage)} />
</Route>
</Router>
</Provider>

View File

@ -1,12 +1,13 @@
const grey = '#333333';
const grey = '#66696f';
export default {
brightPurple: '#AE6DDF',
darkGrey: '#202532',
green: '#4FD061',
grey,
lightGrey: '#B4B4B4',
logoPurple: '#9651CA',
primary: grey,
purple: '#B8C2E3',
white: '#FDFDFD',
purple: '#c38dec',
white: '#FFF',
};

View File

@ -2,6 +2,7 @@ import { pxToRem } from './helpers';
export default {
small: pxToRem(14),
medium: pxToRem(16),
base: pxToRem(18),
large: pxToRem(24),
};

View File

@ -10,24 +10,31 @@ const defaultPadding = paddingLonghand(none);
export default {
...normalize,
html: {
position: 'relative',
minHeight: '100%',
},
body: {
color: color.primary,
...defaultMargin,
...defaultPadding,
display: 'flex',
flexDirection: 'column',
fontFamily: 'Oxygen, sans-serif',
fontSize: font.base,
lineHeight: 1.6,
minHeight: '100vh',
margin: '0 0 94px',
},
'h1, h2, h3': {
lineHeight: 1.2,
},
'#app': {
minHeight: '100vh',
},
'#bg': {
position: 'absolute',
position: 'fixed',
left: 0,
right: 0,
top: 0,
bottom: 0,
zIndex: '-1',
opacity: '0.4',
},
};

View File

@ -2,6 +2,7 @@
<html data-uuid="{{ .UUID }}">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" type="text/css" href="/assets/stylesheets/fonts.css">
<title>Kolide</title>
</head>
<body>

30
frontend/test/helpers.jsx Normal file
View File

@ -0,0 +1,30 @@
import React from 'react';
import configureStore from 'redux-mock-store';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
export const fillInFormInput = (inputComponent, value) => {
return inputComponent.simulate('change', { target: { value } });
};
export const reduxMockStore = (store = {}) => {
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
return mockStore(store);
};
export const connectedComponent = (ComponentClass, { props = {}, mockStore }) => {
return (
<Provider store={mockStore}>
<ComponentClass {...props} />
</Provider>
);
};
export default {
connectedComponent,
fillInFormInput,
reduxMockStore,
};

20
frontend/test/mocks.js Normal file
View File

@ -0,0 +1,20 @@
import nock from 'nock';
export const validLoginRequest = () => {
return nock('http://localhost:8080')
.post('/api/v1/kolide/login')
.reply(200, {
token: 'auth_token',
id: 1,
username: 'admin',
email: 'admin@kolide.co',
name: '',
admin: true,
enabled: true,
needs_password_reset: false,
});
};
export default {
validLoginRequest,
};

View File

@ -54,7 +54,7 @@ export const loadBackground = () => {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
const appElement = document.querySelector('#bg');
const { innerWidth } = window;
const innerHeight = window.innerHeight - 20;
const innerHeight = window.innerHeight;
const unitSize = (innerWidth + innerHeight) / SHAPE_DENSITY;
svg.setAttribute('width', innerWidth);
svg.setAttribute('height', innerHeight);
@ -142,9 +142,14 @@ export const loadBackground = () => {
refresh();
};
export const resizeBackground = () => {
document.querySelector('#bg svg').remove();
clearTimeout(refreshTimeout);
loadBackground();
export const removeBackground = () => {
if (document.querySelector('#bg svg')) {
document.querySelector('#bg svg').remove();
clearTimeout(refreshTimeout);
}
};
export const resizeBackground = () => {
removeBackground();
loadBackground();
};

View File

@ -0,0 +1,19 @@
import config from '../config';
const { window } = global;
const { settings } = config;
export default {
getItem: (itemName) => {
const { localStorage } = window;
const { env } = settings;
return localStorage.getItem(`KOLIDE-${env}::${itemName}`);
},
setItem: (itemName, value) => {
const { localStorage } = window;
const { env } = settings;
return localStorage.setItem(`KOLIDE-${env}::${itemName}`, value);
},
};

View File

@ -1,7 +0,0 @@
export const fillInFormInput = (inputComponent, value) => {
return inputComponent.simulate('change', { target: { value } });
};
export default {
fillInFormInput,
};

View File

@ -29,8 +29,10 @@
"extract-text-webpack-plugin": "^1.0.1",
"file-loader": "^0.8.5",
"history": "2.0.0",
"isomorphic-fetch": "^2.2.1",
"jsdom": "^9.5.0",
"lodash": "^4.3.0",
"nock": "^8.0.0",
"postcss-functions": "^2.1.0",
"postcss-loader": "^0.8.0",
"precss": "^1.4.0",
@ -43,7 +45,9 @@
"react-router": "^2.7.0",
"react-router-redux": "^4.0.5",
"redux": "^3.6.0",
"redux-mock-store": "^1.2.0",
"redux-thunk": "^2.1.0",
"require-hacker": "^2.1.4",
"style-loader": "^0.13.0",
"stylus-loader": "1.5.1",
"url-loader": "^0.5.7",

12
test/loaderMock.js Normal file
View File

@ -0,0 +1,12 @@
import requireHacker from 'require-hacker';
const fakeComponentString = `
require('react').createClass({
render () {
return null;
}
})
`;
requireHacker.hook('svg', () => `module.exports = ${fakeComponentString}`);

3
test/mocha.opts Normal file
View File

@ -0,0 +1,3 @@
--compilers js:babel-register
--require test/loaderMock.js

View File

@ -29,7 +29,7 @@ var config = {
bundle: path.join(repo, 'frontend/index.jsx')
},
output: {
path: path.join(repo, 'assets'),
path: path.join(repo, 'assets/'),
publicPath: "/assets/",
filename: '[name].js'
},