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
+
+
+
+
+
+
+
+ );
+ }
+}
+
+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:
+
+ Kolide
+ Dreamweaver
+ Cobalt
+ Eclipse
+ Github
+ Idle Fingers
+ Iplastic
+ Katzenmilch
+ KR Theme
+ Kuroir
+ Merbivore
+ Merbivore Soft
+ Mono Industrial
+ Monokai
+ Solarized Light
+ SQL Server
+ Tomorrow
+
+
+ );
+};
+
+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 (
-
- {renderHeader()}
- {renderNavItems()}
-
+
+
+ {renderHeader()}
+ {renderNavItems()}
+
+
);
}
}
-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 (
+
+ {error || label}
+
+ );
+ }
+
+ 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()}
+
+ );
+ }
+
+ return (
+
+ {renderLabel()}
+ { this.input = r; }}
+ style={[inputStyles(type, value), inputErrorStyles(error), style]}
+ type={type}
+ {...inputOptions}
+ />
+
+ );
+ }
+}
+
+export default radium(InputField);
+
diff --git a/frontend/components/forms/fields/InputField/index.js b/frontend/components/forms/fields/InputField/index.js
new file mode 100644
index 0000000000..9a0a1bac26
--- /dev/null
+++ b/frontend/components/forms/fields/InputField/index.js
@@ -0,0 +1 @@
+export default from './InputField';
diff --git a/frontend/components/forms/fields/InputField/styles.js b/frontend/components/forms/fields/InputField/styles.js
new file mode 100644
index 0000000000..3c439f304b
--- /dev/null
+++ b/frontend/components/forms/fields/InputField/styles.js
@@ -0,0 +1,55 @@
+import styles from '../../../../styles';
+
+const { color, font } = styles;
+
+export default {
+ componentLabelStyles: (error) => {
+ if (!error) return {};
+
+ return {
+ color: color.alert,
+ };
+ },
+ inputErrorStyles: (error) => {
+ if (!error) return {};
+
+ return {
+ borderColor: color.alert,
+ };
+ },
+ inputStyles: (type, value) => {
+ const baseStyles = {
+ borderColor: color.brand,
+ borderRadius: '2px',
+ borderStyle: 'solid',
+ borderWidth: '1px',
+ fontSize: font.base,
+ color: color.accentText,
+ paddingRight: '30px',
+ opacity: '1',
+ textIndent: '1px',
+ position: 'relative',
+ boxSizing: 'border-box',
+ ':focus': {
+ outline: 'none',
+ },
+ };
+
+ if (type === 'password' && value) {
+ return {
+ ...baseStyles,
+ letterSpacing: '7px',
+ color: color.textUltradark,
+ };
+ }
+
+ if (value) {
+ return {
+ ...baseStyles,
+ color: color.textUltradark,
+ };
+ }
+
+ return baseStyles;
+ },
+};
diff --git a/frontend/components/forms/fields/InputFieldWithIcon/InputFieldWithIcon.jsx b/frontend/components/forms/fields/InputFieldWithIcon/InputFieldWithIcon.jsx
index 71eb2152fc..c7b9e74f67 100644
--- a/frontend/components/forms/fields/InputFieldWithIcon/InputFieldWithIcon.jsx
+++ b/frontend/components/forms/fields/InputFieldWithIcon/InputFieldWithIcon.jsx
@@ -1,9 +1,9 @@
-import React, { Component, PropTypes } from 'react';
+import React, { PropTypes } from 'react';
import radium from 'radium';
import componentStyles from './styles';
+import InputField from '../InputField';
-
-class InputFieldWithIcon extends Component {
+class InputFieldWithIcon extends InputField {
static propTypes = {
autofocus: PropTypes.bool,
error: PropTypes.string,
@@ -15,36 +15,6 @@ class InputFieldWithIcon extends Component {
type: PropTypes.string,
};
- static defaultProps = {
- autofocus: false,
- 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);
- }
-
renderHeading = () => {
const { error, placeholder } = this.props;
const { value } = this.state;
diff --git a/frontend/components/forms/queries/SaveQueryForm/SaveQueryForm.jsx b/frontend/components/forms/queries/SaveQueryForm/SaveQueryForm.jsx
new file mode 100644
index 0000000000..34028ed124
--- /dev/null
+++ b/frontend/components/forms/queries/SaveQueryForm/SaveQueryForm.jsx
@@ -0,0 +1,366 @@
+import React, { Component, PropTypes } from 'react';
+import radium from 'radium';
+import componentStyles from './styles';
+import GradientButton from '../../../buttons/GradientButton';
+import InputField from '../../fields/InputField';
+import validatePresence from '../../validators/validate_presence';
+
+const RUN_TYPES = {
+ RUN: 'RUN',
+ RUN_AND_SAVE: 'RUN_AND_SAVE',
+ SAVE: 'SAVE',
+};
+
+class SaveQueryForm extends Component {
+ static propTypes = {
+ onSubmit: PropTypes.func,
+ saveQuery: PropTypes.bool,
+ };
+
+ constructor (props) {
+ super(props);
+
+ this.state = {
+ errors: {
+ name: null,
+ description: null,
+ duration: null,
+ platforms: null,
+ hosts: null,
+ hostsPercentage: null,
+ scanInterval: null,
+ },
+ formData: {
+ name: null,
+ description: null,
+ duration: 'short',
+ platforms: 'all',
+ hosts: 'all',
+ hostsPercentage: null,
+ scanInterval: 0,
+ },
+ showMoreOptions: false,
+ };
+ }
+
+ componentWillMount () {
+ global.window.addEventListener('keydown', this.handleKeydown);
+ }
+
+ componentWillUnmount () {
+ global.window.removeEventListener('keydown', this.handleKeydown);
+ }
+
+ onFieldChange = (fieldName) => {
+ return ({ target }) => {
+ const { errors, formData } = this.state;
+
+ this.setState({
+ errors: {
+ ...errors,
+ [fieldName]: null,
+ },
+ formData: {
+ ...formData,
+ [fieldName]: target.value,
+ },
+ });
+ };
+ }
+
+ onFormSubmit = (runType) => {
+ return (evt) => {
+ if (evt) {
+ evt.preventDefault();
+ }
+
+ const { formData } = this.state;
+ const { onSubmit } = this.props;
+ const { validate } = this;
+
+ if (validate(runType)) return onSubmit({ formData, runType });
+
+ return false;
+ };
+ }
+
+ handleKeydown = (evt) => {
+ const { metaKey, code } = evt;
+ const { onFormSubmit } = this;
+ const { RUN_AND_SAVE, RUN } = RUN_TYPES;
+ const { saveQuery } = this.props;
+ const runType = saveQuery ? RUN_AND_SAVE : RUN;
+
+ if (metaKey && code === 'Enter') {
+ return onFormSubmit(runType)();
+ }
+
+ return false;
+ };
+
+ validate = (runType) => {
+ const {
+ errors,
+ formData: { name },
+ } = this.state;
+ const { RUN } = RUN_TYPES;
+
+ if (runType === RUN) return true;
+
+ if (!validatePresence(name)) {
+ this.setState({
+ errors: {
+ ...errors,
+ name: 'Query Name field must be completed',
+ },
+ });
+
+ return false;
+ }
+
+ return true;
+ }
+
+ toggleShowMoreOptions = () => {
+ const { showMoreOptions } = this.state;
+
+ this.setState({
+ showMoreOptions: !showMoreOptions,
+ });
+
+ return false;
+ };
+
+ renderMoreOptionsCtaSection = () => {
+ const { moreOptionsIconStyles, moreOptionsCtaSectionStyles, moreOptionsTextStyles } = componentStyles;
+ const { showMoreOptions } = this.state;
+ const { toggleShowMoreOptions } = this;
+
+ if (showMoreOptions) {
+ return (
+
+
+ Fewer Options
+
+
+
+ );
+ }
+
+ return (
+
+
+ More Options
+
+
+
+ );
+ }
+
+ renderMoreOptionsFormFields = () => {
+ const {
+ errors,
+ formData: {
+ duration,
+ platforms,
+ hosts,
+ },
+ showMoreOptions,
+ } = this.state;
+ const {
+ dropdownInputStyles,
+ formSectionStyles,
+ helpTextStyles,
+ labelStyles,
+ queryDescriptionInputStyles,
+ queryHostsPercentageStyles,
+ queryNameInputStyles,
+ } = componentStyles;
+ const { onFieldChange } = this;
+
+ if (!showMoreOptions) return false;
+
+ return (
+
+
+
+
+ If your query is really complex and/or it is not clear why you wrote this query, you should write a description so others can reuse this query for the correct reason.
+
+
+
+
+ Query Duration
+
+ Short
+ Long
+
+
+
+ Individual hosts are not always online. A longer duration will return more complete results. You can view results of any in-progress query at any time.
+
+
+
+
+ Query Platform
+
+ ALL PLATFORMS
+ NO PLATFORMS
+
+
+
+ Specifying a platform allows you to restrict the query from running on a certain platform (even on hosts specifically targeted that do not match).
+
+
+
+
+
+ Specifying a platform allows you to restrict the query from running on a certain platform (even on hosts specifically targeted that do not match).
+
+
+
+
+
+ You can use queries you write in "scans". The interval can be used to control how frequently the query runs when it is running continuously.
+
+
+
+ );
+ };
+
+ renderRunQuery = () => {
+ const {
+ buttonStyles,
+ runQuerySectionStyles,
+ runQueryTipStyles,
+ } = componentStyles;
+ const { onFormSubmit } = this;
+ const { RUN } = RUN_TYPES;
+
+ return (
+
+ );
+ }
+
+ render () {
+ const {
+ buttonInvertStyles,
+ buttonStyles,
+ buttonWrapperStyles,
+ labelStyles,
+ helpTextStyles,
+ queryNameInputStyles,
+ queryNameWrapperStyles,
+ } = componentStyles;
+ const { errors } = this.state;
+ const {
+ onFieldChange,
+ onFormSubmit,
+ renderMoreOptionsFormFields,
+ renderMoreOptionsCtaSection,
+ renderRunQuery,
+ } = this;
+ const { RUN_AND_SAVE, SAVE } = RUN_TYPES;
+ const { saveQuery } = this.props;
+
+ if (!saveQuery) return renderRunQuery();
+
+ return (
+
+ );
+ }
+}
+
+export default radium(SaveQueryForm);
diff --git a/frontend/components/forms/queries/SaveQueryForm/SaveQueryForm.tests.jsx b/frontend/components/forms/queries/SaveQueryForm/SaveQueryForm.tests.jsx
new file mode 100644
index 0000000000..001cd6029f
--- /dev/null
+++ b/frontend/components/forms/queries/SaveQueryForm/SaveQueryForm.tests.jsx
@@ -0,0 +1,91 @@
+import React from 'react';
+import expect, { createSpy, restoreSpies } from 'expect';
+import { mount } from 'enzyme';
+import { noop } from 'lodash';
+import helpers from '../../../../test/helpers';
+import SaveQueryForm from './index';
+
+const { fillInFormInput } = helpers;
+const queryName = 'My New Query';
+
+describe('SaveQueryForm - component', () => {
+ afterEach(restoreSpies);
+
+ it('handles query name input changes', () => {
+ const form = mount(
+
+ );
+ const queryNameInput = form.find({ name: 'name' });
+
+ fillInFormInput(queryNameInput, queryName);
+
+ const { formData } = form.state();
+
+ expect(formData).toEqual({
+ description: null,
+ duration: 'short',
+ hosts: 'all',
+ hostsPercentage: null,
+ name: queryName,
+ platforms: 'all',
+ scanInterval: 0,
+ });
+ });
+
+ it('does not submit the form if it is invalid', () => {
+ const onSubmit = createSpy();
+ const form = mount(
+
+ );
+
+ form.simulate('submit');
+
+ expect(onSubmit).toNotHaveBeenCalled();
+ });
+
+ it('calls onSubmit with the formData and "RUN_AND_SAVE" runType when the saveQuery prop is present', () => {
+ const onSubmit = createSpy();
+ const form = mount(
+
+ );
+ const queryNameInput = form.find({ name: 'name' });
+
+ fillInFormInput(queryNameInput, queryName);
+ form.simulate('submit');
+
+ expect(onSubmit).toHaveBeenCalledWith({
+ runType: 'RUN_AND_SAVE',
+ formData: {
+ description: null,
+ duration: 'short',
+ hosts: 'all',
+ hostsPercentage: null,
+ name: queryName,
+ platforms: 'all',
+ scanInterval: 0,
+ },
+ });
+ });
+
+ it('calls onSubmit with the formData and "RUN" runType without the saveQuery prop', () => {
+ const onSubmit = createSpy();
+ const form = mount(
+
+ );
+
+ form.simulate('submit');
+
+ expect(onSubmit).toHaveBeenCalledWith({
+ runType: 'RUN',
+ formData: {
+ description: null,
+ duration: 'short',
+ hosts: 'all',
+ hostsPercentage: null,
+ name: null,
+ platforms: 'all',
+ scanInterval: 0,
+ },
+ });
+ });
+});
diff --git a/frontend/components/forms/queries/SaveQueryForm/index.js b/frontend/components/forms/queries/SaveQueryForm/index.js
new file mode 100644
index 0000000000..fb9497900d
--- /dev/null
+++ b/frontend/components/forms/queries/SaveQueryForm/index.js
@@ -0,0 +1 @@
+export default from './SaveQueryForm';
diff --git a/frontend/components/forms/queries/SaveQueryForm/styles.js b/frontend/components/forms/queries/SaveQueryForm/styles.js
new file mode 100644
index 0000000000..438bf9f6f8
--- /dev/null
+++ b/frontend/components/forms/queries/SaveQueryForm/styles.js
@@ -0,0 +1,117 @@
+import Styles from '../../../../styles';
+
+const { color, font, padding } = Styles;
+
+const formSection = {
+ borderTopColor: color.accentLight,
+ borderTopStyle: 'solid',
+ borderTopWidth: '1px',
+ display: 'flex',
+ justifyContent: 'space-between',
+ marginTop: padding.base,
+ paddingTop: padding.base,
+};
+
+const formInput = {
+ fontSize: font.small,
+ color: color.textDark,
+ width: '300px',
+};
+
+const buttonStyles = {
+ backgroundImage: 'none',
+ backgroundColor: color.brandDark,
+ boxShadow: '0 3px 0 #C38DEC',
+ fontSize: font.medium,
+ letterSpacing: '1px',
+ paddingTop: padding.xSmall,
+ paddingBottom: padding.xSmall,
+ width: '200px',
+};
+
+export default {
+ buttonStyles: {
+ ...buttonStyles,
+ },
+ buttonInvertStyles: {
+ ...buttonStyles,
+ backgroundColor: color.white,
+ borderColor: color.brandLight,
+ borderStyle: 'solid',
+ borderWidth: '1px',
+ boxShadow: '0 3px 0 #9651CA',
+ color: color.brandDark,
+ marginRight: padding.half,
+ },
+ buttonWrapperStyles: {
+ ...formSection,
+ justifyContent: 'flex-end',
+ },
+ containerStyles: {},
+ formSectionStyles: formSection,
+ labelStyles: {
+ display: 'block',
+ marginBottom: padding.half,
+ },
+ moreOptionsIconStyles: {
+ fontSize: font.xSmall,
+ marginLeft: padding.half,
+ },
+ moreOptionsCtaSectionStyles: {
+ ...formSection,
+ borderTop: 'none',
+ justifyContent: 'flex-end',
+ paddingTop: 0,
+ },
+ moreOptionsTextStyles: {
+ color: color.brand,
+ cursor: 'pointer',
+ fontSize: font.medium,
+ },
+ queryDescriptionInputStyles: {
+ ...formInput,
+ },
+ queryHostsPercentageStyles: {
+ ...formInput,
+ display: 'inline-block',
+ marginLeft: '5px',
+ paddingRight: 0,
+ maxWidth: '40px',
+ },
+ dropdownInputStyles: {
+ ...formInput,
+ backgroundColor: color.white,
+ borderColor: color.brand,
+ borderStyle: 'solid',
+ borderWidth: '1px',
+ height: '30px',
+ textTransform: 'uppercase',
+ ':focus': {
+ outline: 'none',
+ },
+ },
+ helpTextStyles: {
+ backgroundColor: color.accentLight,
+ padding: padding.base,
+ width: '340px',
+ },
+ queryNameInputStyles: {
+ ...formInput,
+ height: '30px',
+ lineHeight: '30px',
+ paddingLeft: '3px',
+ },
+ queryNameWrapperStyles: {
+ ...formSection,
+ },
+ runQuerySectionStyles: {
+ ...formSection,
+ display: 'block',
+ textAlign: 'right',
+ },
+ runQueryTipStyles: {
+ color: color.textLight,
+ fontSize: font.small,
+ marginRight: padding.half,
+ },
+};
diff --git a/frontend/layouts/CoreLayout/CoreLayout.jsx b/frontend/layouts/CoreLayout/CoreLayout.jsx
index 961e6bbd2b..5b5c91be60 100644
--- a/frontend/layouts/CoreLayout/CoreLayout.jsx
+++ b/frontend/layouts/CoreLayout/CoreLayout.jsx
@@ -1,5 +1,7 @@
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
+import { StyleRoot } from 'radium';
+import componentStyles from './styles';
import SidePanel from '../../components/SidePanel';
export class CoreLayout extends Component {
@@ -11,14 +13,22 @@ export class CoreLayout extends Component {
render () {
const { children, user } = this.props;
+ const { wrapperStyles } = componentStyles;
if (!user) return false;
+ const { pathname } = global.window.location;
+
return (
-
+
+
+
+ {children}
+
+
);
}
}
diff --git a/frontend/layouts/CoreLayout/styles.js b/frontend/layouts/CoreLayout/styles.js
new file mode 100644
index 0000000000..88faa1da26
--- /dev/null
+++ b/frontend/layouts/CoreLayout/styles.js
@@ -0,0 +1,15 @@
+import Styles from '../../styles';
+
+const { padding } = Styles;
+
+export default {
+ wrapperStyles: {
+ marginLeft: '241px',
+ paddingLeft: padding.base,
+ paddingRight: padding.base,
+ paddingTop: padding.base,
+ '@media (max-width: 760px)': {
+ marginLeft: '54px',
+ },
+ },
+};
diff --git a/frontend/pages/Queries/NewQueryPage/NewQueryPage.jsx b/frontend/pages/Queries/NewQueryPage/NewQueryPage.jsx
new file mode 100644
index 0000000000..45b9d1999d
--- /dev/null
+++ b/frontend/pages/Queries/NewQueryPage/NewQueryPage.jsx
@@ -0,0 +1,43 @@
+import React, { Component } from 'react';
+import NewQuery from '../../../components/Queries/NewQuery';
+
+class NewQueryPage extends Component {
+ constructor (props) {
+ super(props);
+
+ this.state = {
+ selectedOsqueryTable: 'users',
+ textEditorText: 'SELECT * FROM users u JOIN groups g WHERE u.gid = g.gid',
+ };
+ }
+
+ onOsqueryTableSelect = (selectedOsqueryTable) => {
+ this.setState({ selectedOsqueryTable });
+
+ return false;
+ }
+
+ onTextEditorInputChange = (textEditorText) => {
+ this.setState({ textEditorText });
+
+ return false;
+ }
+
+ render () {
+ const { selectedOsqueryTable, textEditorText } = this.state;
+ const { onOsqueryTableSelect, onTextEditorInputChange } = this;
+
+ return (
+
+
+
+ );
+ }
+}
+
+export default NewQueryPage;
diff --git a/frontend/pages/Queries/NewQueryPage/index.js b/frontend/pages/Queries/NewQueryPage/index.js
new file mode 100644
index 0000000000..715df50c5c
--- /dev/null
+++ b/frontend/pages/Queries/NewQueryPage/index.js
@@ -0,0 +1 @@
+export default from './NewQueryPage';
diff --git a/frontend/pages/Queries/NewQueryPage/styles.js b/frontend/pages/Queries/NewQueryPage/styles.js
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frontend/router/index.jsx b/frontend/router/index.jsx
index 55c503e1d7..c703359e51 100644
--- a/frontend/router/index.jsx
+++ b/frontend/router/index.jsx
@@ -12,6 +12,8 @@ import ForgotPasswordPage from '../pages/ForgotPasswordPage';
import HomePage from '../pages/HomePage';
import LoginRoutes from '../components/LoginRoutes';
import LogoutPage from '../pages/LogoutPage';
+import NewQueryPage from '../pages/Queries/NewQueryPage';
+import QueryPageWrapper from '../components/Queries/QueryPageWrapper';
import ResetPasswordPage from '../pages/ResetPasswordPage';
import store from '../redux/store';
@@ -34,6 +36,9 @@ const routes = (
+
+
+
diff --git a/frontend/styles/font.js b/frontend/styles/font.js
index 43bd986d08..8cfa948545 100644
--- a/frontend/styles/font.js
+++ b/frontend/styles/font.js
@@ -1,6 +1,7 @@
import { pxToRem } from './helpers';
export default {
+ xSmall: pxToRem(10),
small: pxToRem(14),
medium: pxToRem(16),
base: pxToRem(18),
diff --git a/frontend/styles/global.js b/frontend/styles/global.js
index 499c9340f8..98f2847a00 100644
--- a/frontend/styles/global.js
+++ b/frontend/styles/global.js
@@ -33,5 +33,24 @@ export default (showBackgroundImage) => {
'h1, h2, h3': {
lineHeight: 1.2,
},
+ '.ace_osquery-token': {
+ backgroundColor: color.brand,
+ color: color.white,
+ cursor: 'pointer',
+ },
+ '.ace_cursor': {
+ // height: '30px !important',
+ },
+ '.ace_line': {
+ // height: '30px !important',
+ // lineHeight: '30px',
+ },
+ '.ace_gutter-cell': {
+ // height: '30px',
+ // lineHeight: '30px',
+ },
+ '.ace_gutter-active-line': {
+ // height: '30px !important',
+ },
};
};
diff --git a/package.json b/package.json
index e461da97da..8160ccb278 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
"babel-preset-react": "6.11.1",
"babel-preset-react-hmre": "1.1.1",
"babel-preset-stage-0": "6.5.0",
+ "brace": "^0.8.0",
"css-loader": "^0.23.1",
"cssrecipes-defaults": "^0.5.0",
"enzyme": "^2.4.1",
@@ -42,6 +43,7 @@
"radium": "^0.18.1",
"radium-normalize": "^2.0.4",
"react": "^15.3.2",
+ "react-ace": "^3.6.0",
"react-addons-css-transition-group": "^15.3.2",
"react-dom": "^15.3.2",
"react-redux": "^4.4.5",