diff --git a/assets/fonts/kolidecons/kolidecons.woff b/assets/fonts/kolidecons/kolidecons.woff old mode 100644 new mode 100755 index a6206b5194..bf78b1f156 Binary files a/assets/fonts/kolidecons/kolidecons.woff and b/assets/fonts/kolidecons/kolidecons.woff differ diff --git a/frontend/components/buttons/Button/Button.tsx b/frontend/components/buttons/Button/Button.tsx index b4ad0822fd..39fabc0186 100644 --- a/frontend/components/buttons/Button/Button.tsx +++ b/frontend/components/buttons/Button/Button.tsx @@ -9,7 +9,7 @@ interface IButtonProps { onClick: (evt: React.MouseEvent) => boolean; size: string; tabIndex: number; - text: string; + text: any; type: string; variant: string; } diff --git a/frontend/components/forms/ConfigurePackQueryForm/ConfigurePackQueryForm.jsx b/frontend/components/forms/ConfigurePackQueryForm/ConfigurePackQueryForm.jsx new file mode 100644 index 0000000000..55a34f4a81 --- /dev/null +++ b/frontend/components/forms/ConfigurePackQueryForm/ConfigurePackQueryForm.jsx @@ -0,0 +1,115 @@ +import React, { Component, PropTypes } from 'react'; + +import Icon from 'components/Icon'; +import Button from 'components/buttons/Button'; +import Dropdown from 'components/forms/fields/Dropdown'; +import Form from 'components/forms/Form'; +import formFieldInterface from 'interfaces/form_field'; +import InputField from 'components/forms/fields/InputField'; +import validate from 'components/forms/ConfigurePackQueryForm/validate'; + +const baseClass = 'configure-pack-query-form'; +const fieldNames = ['query_id', 'interval', 'logging_type', 'platform', 'version']; +const platformOptions = [ + { label: 'All', value: 'all' }, + { label: 'Windows', value: 'windows' }, + { label: 'Linux', value: 'linux' }, + { label: 'macOS', value: 'darwin' }, +]; +const loggingTypeOptions = [ + { label: 'Differential', value: 'differential' }, + { label: 'Differential (Ignore Removals)', value: 'differential_ignore_removals' }, + { label: 'Snapshot', value: 'snapshot' }, +]; +const minOsqueryVersionOptions = [ + { label: 'All', value: 'all' }, + { label: '1.8.1 +', value: '1.8.1' }, + { label: '1.8.2 +', value: '1.8.2' }, + { label: '2.0.0 +', value: '2.0.0' }, + { label: '2.1.1 +', value: '2.1.1' }, + { label: '2.1.2 +', value: '2.1.2' }, + { label: '2.2.0 +', value: '2.2.0' }, + { label: '2.2.1 +', value: '2.2.1' }, +]; + +class ConfigurePackQueryForm extends Component { + static propTypes = { + fields: PropTypes.shape({ + interval: formFieldInterface.isRequired, + logging_type: formFieldInterface.isRequired, + platform: formFieldInterface.isRequired, + version: formFieldInterface.isRequired, + }).isRequired, + handleSubmit: PropTypes.func, + onCancel: PropTypes.func, + }; + + onCancel = (evt) => { + evt.preventDefault(); + + const { onCancel: handleCancel } = this.props; + + return handleCancel(); + } + + render () { + const { fields, handleSubmit } = this.props; + const { onCancel } = this; + + return ( +
+

configuration

+
+ + + , ' ver.']} + wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--osquer-vers`} + /> + +
+
+
+
+ ); + } +} + +export default Form(ConfigurePackQueryForm, { + fields: fieldNames, + validate, +}); diff --git a/frontend/components/forms/ConfigurePackQueryForm/ConfigurePackQueryForm.tests.jsx b/frontend/components/forms/ConfigurePackQueryForm/ConfigurePackQueryForm.tests.jsx new file mode 100644 index 0000000000..dd5d5d1ed6 --- /dev/null +++ b/frontend/components/forms/ConfigurePackQueryForm/ConfigurePackQueryForm.tests.jsx @@ -0,0 +1,53 @@ +import React from 'react'; +import expect, { createSpy, restoreSpies } from 'expect'; +import { mount } from 'enzyme'; +import { noop } from 'lodash'; + +import ConfigurePackQueryForm from 'components/forms/ConfigurePackQueryForm'; +import { itBehavesLikeAFormDropdownElement, itBehavesLikeAFormInputElement } from 'test/helpers'; + +describe('ConfigurePackQueryForm - component', () => { + afterEach(restoreSpies); + + describe('form fields', () => { + const form = mount( + + ); + + it('updates form state', () => { + itBehavesLikeAFormInputElement(form, 'interval'); + itBehavesLikeAFormDropdownElement(form, 'logging_type'); + itBehavesLikeAFormDropdownElement(form, 'platform'); + itBehavesLikeAFormDropdownElement(form, 'version'); + }); + }); + + describe('submitting the form', () => { + const spy = createSpy(); + const form = mount( + + ); + + it('submits the form with the form data', () => { + itBehavesLikeAFormInputElement(form, 'interval', 'InputField', 123); + itBehavesLikeAFormDropdownElement(form, 'logging_type'); + itBehavesLikeAFormDropdownElement(form, 'platform'); + itBehavesLikeAFormDropdownElement(form, 'version'); + + form.find('form').simulate('submit'); + + expect(spy).toHaveBeenCalledWith({ + interval: 123, + logging_type: 'differential', + platform: 'all', + query_id: 1, + version: 'all', + }); + }); + }); +}); diff --git a/frontend/components/forms/ConfigurePackQueryForm/_styles.scss b/frontend/components/forms/ConfigurePackQueryForm/_styles.scss new file mode 100644 index 0000000000..1ac9a11402 --- /dev/null +++ b/frontend/components/forms/ConfigurePackQueryForm/_styles.scss @@ -0,0 +1,64 @@ +.configure-pack-query-form { + .form-field { + .input-field, + .dropdown__select { + width: 175px; + float: right; + } + + @at-root .configure-pack-query-form__form-field--interval { + .input-field { + padding-right: 70px; + } + } + } + + &__title { + font-size: 16px; + font-weight: $bold; + letter-spacing: -0.5px; + color: $text-dark; + border-bottom: 1px solid #eaeefb; + padding: 0 0 9px; + margin: 0 0 9px; + } + + &__fields { + .form-field { + position: relative; + + &__label { + font-size: 16px; + font-weight: $bold; + line-height: 40px; + letter-spacing: -0.5px; + color: $text-medium; + display: inline-block; + width: 70px; + text-transform: lowercase; + padding-left: 12px; + } + + &__hint { + position: absolute; + bottom: 13px; + right: 10px; + text-transform: lowercase; + } + } + + .kolidecon { + margin-right: 6px; + } + } + + &__btn-wrapper { + padding-top: 20px; + + .button { + display: block; + width: 100%; + margin-bottom: 20px; + } + } +} diff --git a/frontend/components/forms/ConfigurePackQueryForm/index.js b/frontend/components/forms/ConfigurePackQueryForm/index.js new file mode 100644 index 0000000000..4e19b77445 --- /dev/null +++ b/frontend/components/forms/ConfigurePackQueryForm/index.js @@ -0,0 +1 @@ +export default from './ConfigurePackQueryForm'; diff --git a/frontend/components/forms/ConfigurePackQueryForm/validate.js b/frontend/components/forms/ConfigurePackQueryForm/validate.js new file mode 100644 index 0000000000..9be283d5de --- /dev/null +++ b/frontend/components/forms/ConfigurePackQueryForm/validate.js @@ -0,0 +1,33 @@ +import { size } from 'lodash'; + +import validateNumericality from 'components/forms/validators/validate_numericality'; + +const validate = (formData) => { + const errors = {}; + + if (!formData.query_id) { + errors.query_id = 'A query must be selected'; + } + + if (!formData.interval) { + errors.interval = 'Interval must be present'; + } + + if (formData.interval && !validateNumericality(formData.interval)) { + errors.interval = 'Interval must be a number'; + } + + if (!formData.platform) { + errors.platform = 'A platform must be selected'; + } + + if (!formData.logging_type) { + errors.logging_type = 'A Logging Type must be selected'; + } + + const valid = !size(errors); + + return { valid, errors }; +}; + +export default validate; diff --git a/frontend/components/forms/Form.jsx b/frontend/components/forms/Form.jsx index 39376d55b3..e4f43fb6e3 100644 --- a/frontend/components/forms/Form.jsx +++ b/frontend/components/forms/Form.jsx @@ -1,5 +1,5 @@ import React, { Component, PropTypes } from 'react'; -import { noop } from 'lodash'; +import { isEqual, noop } from 'lodash'; const defaultValidate = () => { return { valid: true, errors: {} }; }; @@ -25,6 +25,24 @@ export default (WrappedComponent, { fields, validate = defaultValidate }) => { this.state = { errors, formData }; } + componentWillReceiveProps (nextProps) { + const { formData: formDataProp } = nextProps; + const { formData: oldFormDataProp } = this.props; + + if (!isEqual(formDataProp, oldFormDataProp)) { + const { formData } = this.state; + + this.setState({ + formData: { + ...formData, + ...formDataProp, + }, + }); + } + + return false; + } + onFieldChange = (fieldName) => { return (value) => { const { errors, formData } = this.state; diff --git a/frontend/components/forms/FormField/FormField.jsx b/frontend/components/forms/FormField/FormField.jsx index 9570d54658..1bbd8db1ac 100644 --- a/frontend/components/forms/FormField/FormField.jsx +++ b/frontend/components/forms/FormField/FormField.jsx @@ -9,7 +9,7 @@ class FormField extends Component { className: PropTypes.string, error: PropTypes.string, hint: PropTypes.oneOfType([PropTypes.array, PropTypes.string]), - label: PropTypes.string, + label: PropTypes.oneOfType([PropTypes.array, PropTypes.string]), name: PropTypes.string, type: PropTypes.string, }; @@ -46,9 +46,9 @@ class FormField extends Component { const { renderLabel, renderHint } = this; const { children, className, type } = this.props; - const formFieldClass = classnames(baseClass, className, { + const formFieldClass = classnames(baseClass, { [`${baseClass}--${type}`]: type, - }); + }, className); return (
diff --git a/frontend/components/forms/fields/Checkbox/Checkbox.jsx b/frontend/components/forms/fields/Checkbox/Checkbox.jsx index 602d8847ee..9e7b6882b6 100644 --- a/frontend/components/forms/fields/Checkbox/Checkbox.jsx +++ b/frontend/components/forms/fields/Checkbox/Checkbox.jsx @@ -10,15 +10,14 @@ class Checkbox extends Component { static propTypes = { children: PropTypes.node, className: PropTypes.string, + disabled: PropTypes.bool, name: PropTypes.string, onChange: PropTypes.func, - error: PropTypes.string, - hint: PropTypes.oneOfType([PropTypes.array, PropTypes.string]), - label: PropTypes.string, value: PropTypes.bool, }; static defaultProps = { + disabled: false, onChange: noop, }; @@ -30,16 +29,28 @@ class Checkbox extends Component { render () { const { handleChange } = this; - const { children, className, name, value } = this.props; + const { children, className, disabled, name, value } = this.props; const checkBoxClass = classnames(baseClass, className); const formFieldProps = pick(this.props, ['hint', 'label', 'error', 'name']); + const checkBoxTickClass = classnames(`${checkBoxClass}__tick`, { + [`${checkBoxClass}__tick--disabled`]: disabled, + }); + return ( diff --git a/frontend/components/forms/fields/Checkbox/Checkbox.tests.jsx b/frontend/components/forms/fields/Checkbox/Checkbox.tests.jsx index d577e3e530..b898460716 100644 --- a/frontend/components/forms/fields/Checkbox/Checkbox.tests.jsx +++ b/frontend/components/forms/fields/Checkbox/Checkbox.tests.jsx @@ -1,11 +1,40 @@ import React from 'react'; -import expect from 'expect'; +import expect, { createSpy, restoreSpies } from 'expect'; import { mount } from 'enzyme'; import Checkbox from './Checkbox'; describe('Checkbox - component', () => { + afterEach(restoreSpies); + it('renders', () => { expect(mount()).toExist(); }); + + it('calls the "onChange" handler when changed', () => { + const onCheckedComponentChangeSpy = createSpy(); + const onUncheckedComponentChangeSpy = createSpy(); + + const checkedComponent = mount( + + ).find('input'); + + const uncheckedComponent = mount( + + ).find('input'); + + checkedComponent.simulate('change'); + uncheckedComponent.simulate('change'); + + expect(onCheckedComponentChangeSpy).toHaveBeenCalledWith(false); + expect(onUncheckedComponentChangeSpy).toHaveBeenCalledWith(true); + }); }); diff --git a/frontend/components/forms/fields/Checkbox/_styles.scss b/frontend/components/forms/fields/Checkbox/_styles.scss index 05baccd097..99a3ea0140 100644 --- a/frontend/components/forms/fields/Checkbox/_styles.scss +++ b/frontend/components/forms/fields/Checkbox/_styles.scss @@ -1,6 +1,8 @@ .kolide-checkbox { @include clearfix; + @include size(20px); position: relative; + display: block; &__input { visibility: hidden; @@ -32,6 +34,7 @@ &__tick { @include size(20px); position: absolute; + left: 0; display: inline-block; &::after { @@ -45,6 +48,12 @@ background-color: $white; visibility: visible; } + + &--disabled { + &::after { + background-color: $border-medium; + } + } } &__label { diff --git a/frontend/components/forms/fields/Dropdown/Dropdown.jsx b/frontend/components/forms/fields/Dropdown/Dropdown.jsx index 51add0a4ea..d3c1d29db1 100644 --- a/frontend/components/forms/fields/Dropdown/Dropdown.jsx +++ b/frontend/components/forms/fields/Dropdown/Dropdown.jsx @@ -1,23 +1,26 @@ import React, { Component, PropTypes } from 'react'; -import Select from 'react-select'; +import classnames from 'classnames'; import { noop, pick } from 'lodash'; +import Select from 'react-select'; import dropdownOptionInterface from 'interfaces/dropdownOption'; import FormField from 'components/forms/FormField'; -const baseClass = 'input-dropdown'; +const baseClass = 'dropdown'; class Dropdown extends Component { static propTypes = { - options: PropTypes.arrayOf(dropdownOptionInterface).isRequired, - onChange: PropTypes.func, className: PropTypes.string, - error: PropTypes.string, - hint: PropTypes.oneOfType([PropTypes.array, PropTypes.string]), - label: PropTypes.string, - placeholder: PropTypes.string, - value: PropTypes.string, clearable: PropTypes.bool, + error: PropTypes.string, + label: PropTypes.oneOfType([PropTypes.array, PropTypes.string]), + labelClassName: PropTypes.string, + name: PropTypes.string, + onChange: PropTypes.func, + options: PropTypes.arrayOf(dropdownOptionInterface).isRequired, + placeholder: PropTypes.oneOfType([PropTypes.array, PropTypes.string]), + value: PropTypes.string, + wrapperClassName: PropTypes.string, }; static defaultProps = { @@ -32,22 +35,44 @@ class Dropdown extends Component { return onChange(value); }; + renderLabel = () => { + const { error, label, labelClassName, name } = this.props; + const labelWrapperClasses = classnames( + `${baseClass}__label`, + labelClassName, + { [`${baseClass}__label--error`]: error } + ); + + if (!label) { + return false; + } + + return ( + + ); + } + render () { const { handleChange } = this; - const { options, className, placeholder, value, clearable } = this.props; + const { className, clearable, name, options, placeholder, value, wrapperClassName } = this.props; const formFieldProps = pick(this.props, ['hint', 'label', 'error', 'name']); return ( - + +
+ {renderLabel()} + +
); } } diff --git a/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsInput/SelectTargetsInput.jsx b/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsInput/SelectTargetsInput.jsx index 89609283df..ee91b0bf40 100644 --- a/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsInput/SelectTargetsInput.jsx +++ b/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsInput/SelectTargetsInput.jsx @@ -8,6 +8,7 @@ import targetInterface from 'interfaces/target'; class SelectTargetsInput extends Component { static propTypes = { className: PropTypes.string, + disabled: PropTypes.bool, isLoading: PropTypes.bool, menuRenderer: PropTypes.func, onClose: PropTypes.func, @@ -26,6 +27,7 @@ class SelectTargetsInput extends Component { render () { const { className, + disabled, isLoading, menuRenderer, onClose, @@ -38,6 +40,7 @@ class SelectTargetsInput extends Component { return (