diff --git a/frontend/.test.setup.js b/frontend/.test.setup.js index ac9dd6a2c5..d458518f92 100644 --- a/frontend/.test.setup.js +++ b/frontend/.test.setup.js @@ -33,4 +33,5 @@ function mockStorage() { }; } +require('radium').TestMode.enable(); global.localStorage = window.localStorage = mockStorage(); diff --git a/frontend/components/Queries/NewQuery/NewQuery.jsx b/frontend/components/Queries/NewQuery/NewQuery.jsx new file mode 100644 index 0000000000..0f88208f72 --- /dev/null +++ b/frontend/components/Queries/NewQuery/NewQuery.jsx @@ -0,0 +1,125 @@ +import React, { Component, PropTypes } from 'react'; +import AceEditor from 'react-ace'; +import 'brace/ext/linking'; +import radium from 'radium'; +import './mode'; +import './theme'; +import componentStyles from './styles'; +import SaveQueryForm from '../../forms/queries/SaveQueryForm'; +import SaveQuerySection from './SaveQuerySection'; +import ThemeDropdown from './ThemeDropdown'; + +class NewQuery extends Component { + static propTypes = { + onOsqueryTableSelect: PropTypes.func, + onTextEditorInputChange: PropTypes.func, + textEditorText: PropTypes.string, + }; + + constructor (props) { + super(props); + + this.state = { + saveQuery: false, + theme: 'kolide', + }; + } + + onLoad = (editor) => { + editor.setOptions({ + enableLinking: true, + }); + + editor.on('linkClick', (data) => { + const { type, value } = data.token; + const { onOsqueryTableSelect } = this.props; + + if (type === 'osquery-token') { + return onOsqueryTableSelect(value); + } + + return false; + }); + } + + onSaveQueryFormSubmit = (formData) => { + console.log('SaveQueryForm submitted', formData); + + return false; + } + + onThemeSelect = (evt) => { + evt.preventDefault(); + + this.setState({ + theme: evt.target.value, + }); + + return false; + } + + onToggleSaveQuery = () => { + const { saveQuery } = this.state; + + this.setState({ + saveQuery: !saveQuery, + }); + + return false; + } + + render () { + const { + containerStyles, + selectTargetsHeaderStyles, + targetsInputStyle, + titleStyles, + } = componentStyles; + const { onTextEditorInputChange, textEditorText } = this.props; + const { saveQuery, theme } = this.state; + const { + onBeforeLoad, + onLoad, + onSaveQueryFormSubmit, + onThemeSelect, + onToggleSaveQuery, + } = this; + + return ( +
+

+ New Query Page +

+ +
+ +
+
+

Select Targets

+ +
+ + +
+ ); + } +} + +export default radium(NewQuery); diff --git a/frontend/components/Queries/NewQuery/NewQuery.tests.jsx b/frontend/components/Queries/NewQuery/NewQuery.tests.jsx new file mode 100644 index 0000000000..1f4cf9f15e --- /dev/null +++ b/frontend/components/Queries/NewQuery/NewQuery.tests.jsx @@ -0,0 +1,80 @@ +import React from 'react'; +import expect, { spyOn, restoreSpies } from 'expect'; +import { mount } from 'enzyme'; +import { noop } from 'lodash'; +import NewQuery from './index'; + +const createAceSpy = () => { + return spyOn(global.window.ace, 'edit').andReturn({ + $options: {}, + getValue: () => { return 'Hello world'; }, + getSession: () => { + return { + getMarkers: noop, + setAnnotations: noop, + setMode: noop, + setUseWrapMode: noop, + }; + }, + handleOptions: noop, + handleMarkers: noop, + on: noop, + renderer: { + setShowGutter: noop, + }, + session: { + on: noop, + }, + setFontSize: noop, + setMode: noop, + setOption: noop, + setOptions: noop, + setShowPrintMargin: noop, + setTheme: noop, + setValue: noop, + }); +}; + +describe('NewQuery - component', () => { + beforeEach(createAceSpy); + afterEach(restoreSpies); + + it('renders the SaveQuerySection', () => { + const component = mount( + + ); + + expect(component.find('SaveQuerySection').length).toEqual(1); + }); + + it('renders the ThemeDropdown', () => { + const component = mount( + + ); + + expect(component.find('ThemeDropdown').length).toEqual(1); + }); + + it('renders the SaveQueryForm', () => { + const component = mount( + + ); + + component.find('Slider').simulate('click'); + + expect(component.find('SaveQueryForm').length).toEqual(1); + }); +}); + diff --git a/frontend/components/Queries/NewQuery/SaveQuerySection/index.jsx b/frontend/components/Queries/NewQuery/SaveQuerySection/index.jsx new file mode 100644 index 0000000000..f101732c6c --- /dev/null +++ b/frontend/components/Queries/NewQuery/SaveQuerySection/index.jsx @@ -0,0 +1,35 @@ +import React, { PropTypes } from 'react'; +import radium from 'radium'; +import componentStyles from './styles'; +import Slider from '../../../buttons/Slider'; + +const SaveQuerySection = ({ onToggleSaveQuery, saveQuery }) => { + const { + saveQuerySection, + saveTextWrapper, + saveWrapper, + sliderTextDontSave, + sliderTextSave, + } = componentStyles; + + return ( +
+
+

Save Query & Results For Later?

+ For certain types of queries, like one that targets many hosts or one you plan to reuse frequently, we suggest saving the query & results. This allows you to set some advanced options, view the results later, and share with other users +
+
+ Dont save + + Save +
+
+ ); +}; + +SaveQuerySection.propTypes = { + onToggleSaveQuery: PropTypes.func, + saveQuery: PropTypes.bool, +}; + +export default radium(SaveQuerySection); diff --git a/frontend/components/Queries/NewQuery/SaveQuerySection/styles.js b/frontend/components/Queries/NewQuery/SaveQuerySection/styles.js new file mode 100644 index 0000000000..b7674e0b62 --- /dev/null +++ b/frontend/components/Queries/NewQuery/SaveQuerySection/styles.js @@ -0,0 +1,38 @@ +import Styles from '../../../../styles'; + +const { color, font } = Styles; + +export default { + saveQuerySection: { + alignItems: 'flex-end', + display: 'flex', + justifyContent: 'space-between', + }, + saveTextWrapper: { + display: 'inline-block', + width: '440px', + '@media (max-width: 911px)': { + width: '400px', + }, + }, + saveWrapper: { + alignItems: 'center', + display: 'flex', + }, + sliderTextDontSave: (saveQuery) => { + return { + color: saveQuery ? color.textDark : color.textUltradark, + textTransform: 'uppercase', + fontSize: font.small, + marginRight: '5px', + }; + }, + sliderTextSave: (saveQuery) => { + return { + color: saveQuery ? color.brand : color.textMedium, + textTransform: 'uppercase', + fontSize: font.small, + marginLeft: '5px', + }; + }, +}; diff --git a/frontend/components/Queries/NewQuery/ThemeDropdown/index.jsx b/frontend/components/Queries/NewQuery/ThemeDropdown/index.jsx new file mode 100644 index 0000000000..c7d5a46343 --- /dev/null +++ b/frontend/components/Queries/NewQuery/ThemeDropdown/index.jsx @@ -0,0 +1,55 @@ +import React, { PropTypes } from 'react'; +import radium from 'radium'; +import 'brace/mode/sql'; +import 'brace/theme/dreamweaver'; +import 'brace/theme/cobalt'; +import 'brace/theme/eclipse'; +import 'brace/theme/github'; +import 'brace/theme/idle_fingers'; +import 'brace/theme/iplastic'; +import 'brace/theme/katzenmilch'; +import 'brace/theme/kr_theme'; +import 'brace/theme/kuroir'; +import 'brace/theme/merbivore'; +import 'brace/theme/merbivore_soft'; +import 'brace/theme/mono_industrial'; +import 'brace/theme/monokai'; +import 'brace/theme/solarized_light'; +import 'brace/theme/sqlserver'; +import 'brace/theme/tomorrow'; +import componentStyles from './styles'; + +const ThemeDropdown = ({ onSelectChange, theme }) => { + const { themeDropdownStyles } = componentStyles; + return ( +
+ Editor Theme: + +
+ ); +}; + +ThemeDropdown.propTypes = { + onSelectChange: PropTypes.func, + theme: PropTypes.string, +}; + +export default radium(ThemeDropdown); diff --git a/frontend/components/Queries/NewQuery/ThemeDropdown/styles.js b/frontend/components/Queries/NewQuery/ThemeDropdown/styles.js new file mode 100644 index 0000000000..8ffad59aae --- /dev/null +++ b/frontend/components/Queries/NewQuery/ThemeDropdown/styles.js @@ -0,0 +1,10 @@ +import Styles from '../../../../styles'; + +const { padding } = Styles; + +export default { + themeDropdownStyles: { + display: 'inline-block', + marginLeft: padding.half, + }, +}; diff --git a/frontend/components/Queries/NewQuery/index.js b/frontend/components/Queries/NewQuery/index.js new file mode 100644 index 0000000000..aea1e640be --- /dev/null +++ b/frontend/components/Queries/NewQuery/index.js @@ -0,0 +1 @@ +export default from './NewQuery'; diff --git a/frontend/components/Queries/NewQuery/mode.js b/frontend/components/Queries/NewQuery/mode.js new file mode 100644 index 0000000000..082a90ee0d --- /dev/null +++ b/frontend/components/Queries/NewQuery/mode.js @@ -0,0 +1,103 @@ +/* eslint-disable */ +ace.define("ace/mode/kolide_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/sql_highlight_rules"], function(acequire, exports, module) { + "use strict"; + + var oop = acequire("../lib/oop"); + var SqlHighlightRules = acequire("./sql_highlight_rules").SqlHighlightRules; + + var KolideHighlightRules = function() { + var keywords = ( + "select|insert|update|delete|from|where|and|or|group|by|order|limit|offset|having|as|case|" + + "when|else|end|type|left|right|join|on|outer|desc|asc|union|create|table|primary|key|if|" + + "foreign|not|references|default|null|inner|cross|natural|database|drop|grant" + ); + + var builtinConstants = ( + "true|false" + ); + + var builtinFunctions = ( + "avg|count|first|last|max|min|sum|ucase|lcase|mid|len|round|rank|now|format|" + + "coalesce|ifnull|isnull|nvl" + ); + + var dataTypes = ( + "int|numeric|decimal|date|varchar|char|bigint|float|double|bit|binary|text|set|timestamp|" + + "money|real|number|integer" + ); + + var osqueryTables = ("users|groups"); + + var keywordMapper = this.createKeywordMapper({ + "osquery-token": osqueryTables, + "support.function": builtinFunctions, + "keyword": keywords, + "constant.language": builtinConstants, + "storage.type": dataTypes, + }, "identifier", true); + + this.$rules = { + "start" : [{ + token : "comment", + regex : "--.*$" + }, { + token : "comment", + start : "/\\*", + end : "\\*/" + }, { + token : "string", // " string + regex : '".*?"' + }, { + token : "string", // ' string + regex : "'.*?'" + }, { + token : "constant.numeric", // float + regex : "[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b" + }, { + token : keywordMapper, + regex : "[a-zA-Z_$][a-zA-Z0-9_$]*\\b" + }, { + token : "keyword.operator", + regex : "\\+|\\-|\\/|\\/\\/|%|<@>|@>|<@|&|\\^|~|<|>|<=|=>|==|!=|<>|=" + }, { + token : "paren.lparen", + regex : "[\\(]" + }, { + token : "paren.rparen", + regex : "[\\)]" + }, { + token : "text", + regex : "\\s+" + }] + }; + + this.normalizeRules(); + }; + + oop.inherits(KolideHighlightRules, SqlHighlightRules); + + exports.KolideHighlightRules = KolideHighlightRules; +}); + +ace.define("ace/mode/kolide",["require","exports","module","ace/lib/oop","ace/mode/sql","ace/mode/kolide_highlight_rules","ace/range"], function(acequire, exports, module) { + "use strict"; + + var oop = acequire("../lib/oop"); + var TextMode = acequire("./sql").Mode; + var KolideHighlightRules = acequire("./kolide_highlight_rules").KolideHighlightRules; + var Range = acequire("../range").Range; + + var Mode = function() { + this.HighlightRules = KolideHighlightRules; + }; + oop.inherits(Mode, TextMode); + + (function() { + + this.lineCommentStart = "--"; + + this.$id = "ace/mode/kolide"; + }).call(Mode.prototype); + + exports.Mode = Mode; +}); diff --git a/frontend/components/Queries/NewQuery/styles.js b/frontend/components/Queries/NewQuery/styles.js new file mode 100644 index 0000000000..79bcaeeb55 --- /dev/null +++ b/frontend/components/Queries/NewQuery/styles.js @@ -0,0 +1,23 @@ +import Styles from '../../../styles'; + +const { color, font, padding } = Styles; + +export default { + containerStyles: { + backgroundColor: color.white, + padding: padding.base, + }, + selectTargetsHeaderStyles: { + fontSize: font.base, + color: color.textMedium, + }, + targetsInputStyle: { + width: '100%', + }, + titleStyles: { + color: color.textMedium, + display: 'inline-block', + fontSize: font.large, + }, +}; + diff --git a/frontend/components/Queries/NewQuery/theme.js b/frontend/components/Queries/NewQuery/theme.js new file mode 100644 index 0000000000..0cbef99d1b --- /dev/null +++ b/frontend/components/Queries/NewQuery/theme.js @@ -0,0 +1,113 @@ +/* eslint-disable */ +ace.define("ace/theme/kolide",["require","exports","module","ace/lib/dom"], function(acequire, exports, module) { + + exports.isDark = false; + exports.cssClass = "ace-kolide"; + exports.cssText = ".ace-kolide .ace_gutter {\ + background: #9CA3AC;\ + color: #FFF\ +}\ +.ace-kolide .ace_print-margin {\ + width: 1px;\ + background: #f6f6f6\ +}\ +.ace-kolide .ace_osquery-token{\ + background-color: #AE6DDF;\ + color: #FFFFFF\ +}\ +.ace-kolide {\ + background-color: #EAEDFB;\ + color: #4D4D4C\ +}\ +.ace-kolide .ace_cursor {\ + color: #AEAFAD\ +}\ +.ace-kolide .ace_marker-layer .ace_selection {\ + background: #D6D6D6\ +}\ + .ace-kolide.ace_multiselect .ace_selection.ace_start {\ + box-shadow: 0 0 3px 0px #FFFFFF;\ + }\ + .ace-kolide .ace_marker-layer .ace_step {\ + background: rgb(255, 255, 0)\ + }\ + .ace-kolide .ace_marker-layer .ace_bracket {\ + margin: -1px 0 0 -1px;\ + border: 1px solid #D1D1D1\ + }\ + .ace-kolide .ace_marker-layer .ace_active-line {\ + background: #EFEFEF\ + }\ + .ace-kolide .ace_gutter-active-line {\ + background-color : #9CA3AC\ + }\ + .ace-kolide .ace_marker-layer .ace_selected-word {\ + border: 1px solid #D6D6D6\ + }\ + .ace-kolide .ace_invisible {\ + color: #D1D1D1\ + }\ + .ace-kolide .ace_keyword,\ + .ace-kolide .ace_meta,\ + .ace-kolide .ace_storage,\ + .ace-kolide .ace_storage.ace_type,\ + .ace-kolide .ace_support.ace_type {\ + color: #8959A8\ + }\ + .ace-kolide .ace_keyword.ace_operator {\ + color: #3E999F\ + }\ + .ace-kolide .ace_constant.ace_character,\ + .ace-kolide .ace_constant.ace_language,\ + .ace-kolide .ace_constant.ace_numeric,\ + .ace-kolide .ace_keyword.ace_other.ace_unit,\ + .ace-kolide .ace_support.ace_constant,\ + .ace-kolide .ace_variable.ace_parameter {\ + color: #F5871F\ + }\ + .ace-kolide .ace_constant.ace_other {\ + color: #666969\ + }\ + .ace-kolide .ace_invalid {\ + color: #FFFFFF;\ + background-color: #C82829\ + }\ + .ace-kolide .ace_invalid.ace_deprecated {\ + color: #FFFFFF;\ + background-color: #AE6DDF\ + }\ + .ace-kolide .ace_fold {\ + background-color: #4271AE;\ + border-color: #4D4D4C\ + }\ + .ace-kolide .ace_entity.ace_name.ace_function,\ + .ace-kolide .ace_support.ace_function,\ + .ace-kolide .ace_variable {\ + color: #4271AE\ + }\ + .ace-kolide .ace_support.ace_class,\ + .ace-kolide .ace_support.ace_type {\ + color: #C99E00\ + }\ + .ace-kolide .ace_heading,\ + .ace-kolide .ace_markup.ace_heading,\ + .ace-kolide .ace_string {\ + color: #718C00\ + }\ + .ace-kolide .ace_entity.ace_name.ace_tag,\ + .ace-kolide .ace_entity.ace_other.ace_attribute-name,\ + .ace-kolide .ace_meta.ace_tag,\ + .ace-kolide .ace_string.ace_regexp,\ + .ace-kolide .ace_variable {\ + color: #C82829\ + }\ + .ace-kolide .ace_comment {\ + color: #8E908C\ + }\ + .ace-kolide .ace_indent-guide {\ + background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bdu3f/BwAlfgctduB85QAAAABJRU5ErkJggg==) right repeat-y\ + }"; + +var dom = acequire("../lib/dom"); +dom.importCssString(exports.cssText, exports.cssClass); +}); diff --git a/frontend/components/Queries/QueryPageWrapper/QueryPageWrapper.jsx b/frontend/components/Queries/QueryPageWrapper/QueryPageWrapper.jsx new file mode 100644 index 0000000000..2eeb946430 --- /dev/null +++ b/frontend/components/Queries/QueryPageWrapper/QueryPageWrapper.jsx @@ -0,0 +1,19 @@ +import React, { Component, PropTypes } from 'react'; + +class QueryPageWrapper extends Component { + static propTypes = { + children: PropTypes.node, + }; + + render () { + const { children } = this.props; + + return ( +
+ {children} +
+ ); + } +} + +export default QueryPageWrapper; diff --git a/frontend/components/Queries/QueryPageWrapper/index.js b/frontend/components/Queries/QueryPageWrapper/index.js new file mode 100644 index 0000000000..9f12c0f6c8 --- /dev/null +++ b/frontend/components/Queries/QueryPageWrapper/index.js @@ -0,0 +1 @@ +export default from './QueryPageWrapper'; diff --git a/frontend/components/SidePanel/SidePanel.jsx b/frontend/components/SidePanel/SidePanel.jsx index cb70b99604..258863431b 100644 --- a/frontend/components/SidePanel/SidePanel.jsx +++ b/frontend/components/SidePanel/SidePanel.jsx @@ -1,6 +1,9 @@ import React, { Component, PropTypes } from 'react'; -import radium from 'radium'; +import { connect } from 'react-redux'; import { isEqual, last } from 'lodash'; +import { push } from 'react-router-redux'; +import radium, { StyleRoot } from 'radium'; +import { activeTabFromPathname, activeSubTabFromPathname } from './helpers'; import componentStyles from './styles'; import kolideLogo from '../../../assets/images/kolide-logo.svg'; import navItems from './navItems'; @@ -8,19 +11,43 @@ import './styles.scss'; class SidePanel extends Component { static propTypes = { + dispatch: PropTypes.func, + pathname: PropTypes.string, user: PropTypes.object, }; constructor (props) { super(props); + const { pathname, user: { admin } } = this.props; + + this.userNavItems = navItems(admin); + + const activeTab = activeTabFromPathname(this.userNavItems, pathname); + const activeSubItem = activeSubTabFromPathname(activeTab, pathname); this.state = { - activeTab: 'Hosts', - activeSubItem: 'Add Hosts', + activeTab, + activeSubItem, showSubItems: false, }; } + componentWillReceiveProps (nextProps) { + if (isEqual(nextProps, this.props)) return false; + + const { pathname } = nextProps; + + const activeTab = activeTabFromPathname(this.userNavItems, pathname); + const activeSubItem = activeSubTabFromPathname(activeTab, pathname); + + this.setState({ + activeTab, + activeSubItem, + }); + + return false; + } + setActiveSubItem = (activeSubItem) => { return (evt) => { evt.preventDefault(); @@ -34,10 +61,17 @@ class SidePanel extends Component { return (evt) => { evt.preventDefault(); + const { pathname, dispatch } = this.props; + const { defaultPathname } = activeTab; + const activeSubItem = activeSubTabFromPathname(activeTab, pathname); + this.setState({ activeTab, + activeSubItem, }); + if (defaultPathname) return dispatch(push(defaultPathname)); + return false; }; } @@ -90,7 +124,7 @@ class SidePanel extends Component { renderNavItem = (navItem, lastChild) => { const { activeTab } = this.state; const { icon, name, subItems } = navItem; - const active = activeTab === name; + const active = activeTab.name === name; const { iconStyles, navItemBeforeStyles, @@ -105,7 +139,7 @@ class SidePanel extends Component { {active &&
}
  • @@ -121,10 +155,9 @@ class SidePanel extends Component { } renderNavItems = () => { - const { renderNavItem } = this; + const { renderNavItem, userNavItems } = this; const { navItemListStyles } = componentStyles; const { user: { admin } } = this.props; - const userNavItems = navItems(admin); return (
      @@ -139,7 +172,7 @@ class SidePanel extends Component { renderSubItem = (subItem) => { const { activeSubItem } = this.state; const { name, path } = subItem; - const active = activeSubItem === name; + const active = activeSubItem === subItem; const { setActiveSubItem } = this; const { subItemBeforeStyles, @@ -155,7 +188,7 @@ class SidePanel extends Component { {active &&
      }
    • {name} @@ -189,7 +222,7 @@ class SidePanel extends Component { return (
      - +
      ); } @@ -199,12 +232,15 @@ class SidePanel extends Component { const { renderHeader, renderNavItems } = this; return ( - + + + ); } } -export default radium(SidePanel); +const ConnectedComponent = connect()(SidePanel); +export default radium(ConnectedComponent); diff --git a/frontend/components/SidePanel/helpers.js b/frontend/components/SidePanel/helpers.js new file mode 100644 index 0000000000..db13671c79 --- /dev/null +++ b/frontend/components/SidePanel/helpers.js @@ -0,0 +1,19 @@ +import { find } from 'lodash'; + +export const activeTabFromPathname = (navItems, pathname) => { + return find(navItems, (item) => { + return item.path.test(pathname); + }); +}; + +export const activeSubTabFromPathname = (activeTab, pathname) => { + if (!activeTab) return undefined; + + const { subItems } = activeTab; + + if (!subItems.length) return undefined; + + return find(subItems, (subItem) => { + return subItem.path.test(pathname); + }); +}; diff --git a/frontend/components/SidePanel/navItems.js b/frontend/components/SidePanel/navItems.js index c9c02ccf2b..3c7a6f71b9 100644 --- a/frontend/components/SidePanel/navItems.js +++ b/frontend/components/SidePanel/navItems.js @@ -1,48 +1,53 @@ export default (admin) => { const adminNavItems = [ { + defaultPathname: '/admin/users', icon: 'kolidecon-admin', name: 'Admin', - path: '/admin', - subItems: [], + path: /^\/admin/, + subItems: [ + { name: 'User Management', path: /\/users/ }, + ], }, ]; const userNavItems = [ { + defaultPathname: '/', icon: 'kolidecon-hosts', name: 'Hosts', - path: '/admin/hosts', + path: /^\/$/, subItems: [ - { name: 'Add Hosts', path: '/' }, - { name: 'Manage Hosts', path: '/' }, + { name: 'Add Hosts', path: /\/add/ }, + { name: 'Manage Hosts', path: /\/manage/ }, ], }, { + defaultPathname: '/queries/new', icon: 'kolidecon-query', name: 'Query', - path: '/admin/queries', + path: /^\/queries/, subItems: [ - { name: 'New Query', path: '/' }, - { name: 'Queries & Results', path: '/' }, + { name: 'New Query', path: /\/new/ }, + { name: 'Queries & Results', path: /\/results/ }, ], }, { icon: 'kolidecon-packs', name: 'Packs', - path: '/admin/packs', + path: /^\/packs/, subItems: [], }, { icon: 'kolidecon-alerts', name: 'Alerts', - path: '/admin/alerts', + path: /^\/alerts/, subItems: [], }, { icon: 'kolidecon-help', name: 'Help', - path: '/admin/help', + path: /^\/help/, subItems: [], }, ]; diff --git a/frontend/components/SidePanel/styles.js b/frontend/components/SidePanel/styles.js index 78e2b120dc..883896668a 100644 --- a/frontend/components/SidePanel/styles.js +++ b/frontend/components/SidePanel/styles.js @@ -124,7 +124,6 @@ const componentStyles = { borderTopColor: color.accentLight, borderTopStyle: 'solid', borderTopWidth: '1px', - marginRight: '16px', marginTop: '5px', '@media (max-width: 760px)': { marginRight: 0, @@ -253,6 +252,7 @@ const componentStyles = { return { listStyle: 'none', paddingLeft: '16px', + minHeight: '87px', '@media (max-width: 760px)': { borderRight: '1px solid rgba(0,0,0,0.16)', display: expanded ? 'inline-block' : 'none', @@ -263,13 +263,13 @@ const componentStyles = { }; }, collapseSubItemsWrapper: { + boxSizing: 'border-box', + cursor: 'pointer', + height: '100%', position: 'absolute', - right: '4px', - top: '0', bottom: '0', lineHeight: '95px', color: '#fff', - '@media (min-width: 761px)': { display: 'none', }, diff --git a/frontend/components/buttons/GradientButton/styles.js b/frontend/components/buttons/GradientButton/styles.js index 08786120c8..4f957adbd4 100644 --- a/frontend/components/buttons/GradientButton/styles.js +++ b/frontend/components/buttons/GradientButton/styles.js @@ -4,7 +4,10 @@ const { border, color, font, padding } = styles; export default { backgroundImage: 'linear-gradient(134deg, #7166D9 0%, #C86DD7 100%)', - border: 'none', + borderBottom: 'none', + borderLeft: 'none', + borderRight: 'none', + borderTop: 'none', cursor: 'pointer', borderBottomLeftRadius: border.radius.base, borderBottomRightRadius: border.radius.base, @@ -16,7 +19,10 @@ export default { fontSize: font.large, fontWeight: '300', letterSpacing: '4px', - padding: padding.medium, + paddingBottom: padding.medium, + paddingLeft: padding.medium, + paddingRight: padding.medium, + paddingTop: padding.medium, position: 'relative', textTransform: 'uppercase', width: '100%', diff --git a/frontend/components/buttons/Slider/Slider.jsx b/frontend/components/buttons/Slider/Slider.jsx new file mode 100644 index 0000000000..fc08435cd4 --- /dev/null +++ b/frontend/components/buttons/Slider/Slider.jsx @@ -0,0 +1,20 @@ +import React, { PropTypes } from 'react'; +import radium from 'radium'; +import componentStyles from './styles'; + +const Slider = ({ onClick, engaged }) => { + const { containerStyles, buttonStyles } = componentStyles; + + return ( +
      +
      +
      + ); +}; + +Slider.propTypes = { + engaged: PropTypes.bool, + onClick: PropTypes.func, +}; + +export default radium(Slider); diff --git a/frontend/components/buttons/Slider/index.js b/frontend/components/buttons/Slider/index.js new file mode 100644 index 0000000000..12b099f9ba --- /dev/null +++ b/frontend/components/buttons/Slider/index.js @@ -0,0 +1 @@ +export default from './Slider'; diff --git a/frontend/components/buttons/Slider/styles.js b/frontend/components/buttons/Slider/styles.js new file mode 100644 index 0000000000..f7a850f8d0 --- /dev/null +++ b/frontend/components/buttons/Slider/styles.js @@ -0,0 +1,35 @@ +import Styles from '../../../styles'; + +const { color } = Styles; + +export default { + containerStyles: (saveQuery) => { + return { + backgroundColor: saveQuery ? color.brand : color.textMedium, + borderTopLeftRadius: '12px', + borderTopRightRadius: '12px', + borderBottomRightRadius: '12px', + borderBottomLeftRadius: '12px', + border: '1px solid #eaeaea', + cursor: 'pointer', + display: 'inline-block', + height: '20px', + position: 'relative', + transition: 'backgroundColor 0.4s ease-in-out', + width: '40px', + }; + }, + buttonStyles: (saveQuery) => { + return { + marginTop: '3px', + position: 'absolute', + top: 0, + transition: 'left 0.3s ease-in-out', + left: saveQuery ? '21px' : '5px', + height: '14px', + width: '14px', + borderRadius: '50%', + backgroundColor: color.white, + }; + }, +}; diff --git a/frontend/components/forms/ForgotPasswordForm/ForgotPasswordForm.tests.jsx b/frontend/components/forms/ForgotPasswordForm/ForgotPasswordForm.tests.jsx index 4496989268..936b204f2b 100644 --- a/frontend/components/forms/ForgotPasswordForm/ForgotPasswordForm.tests.jsx +++ b/frontend/components/forms/ForgotPasswordForm/ForgotPasswordForm.tests.jsx @@ -13,7 +13,7 @@ describe('ForgotPasswordForm - component', () => { it('renders an InputFieldWithIcon components', () => { const form = mount(); - expect(form.find('InputFieldWithIcon').length).toEqual(1); + expect(form.find('InputField').length).toEqual(1); }); it('updates component state when the email field is changed', () => { diff --git a/frontend/components/forms/LoginForm/LoginForm.tests.jsx b/frontend/components/forms/LoginForm/LoginForm.tests.jsx index 74c6b3c7c8..9f99f6c225 100644 --- a/frontend/components/forms/LoginForm/LoginForm.tests.jsx +++ b/frontend/components/forms/LoginForm/LoginForm.tests.jsx @@ -8,10 +8,10 @@ import { fillInFormInput } from '../../../test/helpers'; describe('LoginForm - component', () => { afterEach(restoreSpies); - it('renders 2 InputFieldWithIcon components', () => { + it('renders 2 InputField components', () => { const form = mount(); - expect(form.find('InputFieldWithIcon').length).toEqual(2); + expect(form.find('InputField').length).toEqual(2); }); it('updates component state when the username field is changed', () => { diff --git a/frontend/components/forms/LoginForm/styles.js b/frontend/components/forms/LoginForm/styles.js index c32d525d8f..980c25c469 100644 --- a/frontend/components/forms/LoginForm/styles.js +++ b/frontend/components/forms/LoginForm/styles.js @@ -34,7 +34,10 @@ export default { submitButtonStyles: { borderTopLeftRadius: 0, borderTopRightRadius: 0, - padding: padding.base, + paddingBottom: padding.base, + paddingLeft: padding.base, + paddingRight: padding.base, + paddingTop: padding.base, width: FORM_WIDTH, }, }; diff --git a/frontend/components/forms/fields/InputField/InputField.jsx b/frontend/components/forms/fields/InputField/InputField.jsx new file mode 100644 index 0000000000..468c9e8be5 --- /dev/null +++ b/frontend/components/forms/fields/InputField/InputField.jsx @@ -0,0 +1,110 @@ +import React, { Component, PropTypes } from 'react'; +import radium from 'radium'; +import componentStyles from './styles'; + +class InputField extends Component { + static propTypes = { + autofocus: PropTypes.bool, + error: PropTypes.string, + inputWrapperStyles: PropTypes.object, + inputOptions: PropTypes.object, + label: PropTypes.string, + labelStyles: PropTypes.object, + name: PropTypes.string, + onChange: PropTypes.func, + placeholder: PropTypes.string, + style: PropTypes.object, + type: PropTypes.string, + }; + + static defaultProps = { + autofocus: false, + inputWrapperStyles: {}, + inputOptions: {}, + label: null, + labelStyles: {}, + style: {}, + type: 'text', + }; + + constructor (props) { + super(props); + this.state = { value: null }; + } + + componentDidMount () { + const { autofocus } = this.props; + const { input } = this; + + if (autofocus) input.focus(); + + return false; + } + + onInputChange = (evt) => { + evt.preventDefault(); + + const { value } = evt.target; + const { onChange } = this.props; + + this.setState({ value }); + return onChange(evt); + } + + renderLabel = () => { + const { componentLabelStyles } = componentStyles; + const { error, label, labelStyles, name } = this.props; + + if (!label) return false; + + return ( + + ); + } + + render () { + const { error, inputOptions, inputWrapperStyles, name, placeholder, style, type } = this.props; + const { inputErrorStyles, inputStyles } = componentStyles; + const { value } = this.state; + const { onInputChange, renderLabel } = this; + + if (type === 'textarea') { + return ( +
      + {renderLabel()} +