From f099b2ae22637b0f960b0618ccc4ecdb72b93edb Mon Sep 17 00:00:00 2001 From: Mike Stone Date: Wed, 21 Dec 2016 12:25:54 -0500 Subject: [PATCH] Create packs (#516) * Creates new PackComposerPage at /packs/new * Creates PackForm component * Adds PackForm to PackComposerPage * Creates QueriesListItem * Creates QueriesList * Creates QueriesListWrapper * Get all queries when the Packs Composer Page loads * Form HOC handles updates to formData prop * Creates form to configure scheduled queries * QueriesListWrapper renders ConfigurePackQueryForm * search queries input filters queries list * Empty state text * create pack when user submits the new pack form * Adds Edit pack page to /packs/:pack_id/edit * API client - get scheduled queries for a pack * API client - create scheduled query * Redux config for scheduled queries * Remove scheduled queries from packs * Add labels to pack on create * Add disabled state to the select targets dropdown * Adds edit route and pushes to new route on edit click * Adds cancel button to edit pack form * Adds Checkbox that selects all scheduled queries in table --- assets/fonts/kolidecons/kolidecons.woff | Bin 12308 -> 10164 bytes frontend/components/buttons/Button/Button.tsx | 2 +- .../ConfigurePackQueryForm.jsx | 115 ++++++++++ .../ConfigurePackQueryForm.tests.jsx | 53 +++++ .../forms/ConfigurePackQueryForm/_styles.scss | 64 ++++++ .../forms/ConfigurePackQueryForm/index.js | 1 + .../forms/ConfigurePackQueryForm/validate.js | 33 +++ frontend/components/forms/Form.jsx | 20 +- .../components/forms/FormField/FormField.jsx | 6 +- .../forms/fields/Checkbox/Checkbox.jsx | 23 +- .../forms/fields/Checkbox/Checkbox.tests.jsx | 31 ++- .../forms/fields/Checkbox/_styles.scss | 9 + .../forms/fields/Dropdown/Dropdown.jsx | 53 +++-- .../forms/fields/Dropdown/Dropdown.tests.jsx | 19 +- .../forms/fields/Dropdown/_styles.scss | 55 +++-- .../forms/fields/InputField/InputField.jsx | 5 +- .../forms/fields/InputField/_styles.scss | 1 + .../SelectTargetsDropdown.jsx | 59 +++-- .../SelectTargetsInput/SelectTargetsInput.jsx | 3 + .../fields/SelectTargetsDropdown/_styles.scss | 30 +++ .../components/forms/fields/Slider/Slider.jsx | 4 - .../forms/packs/EditPackForm/EditPackForm.jsx | 50 +++++ .../packs/EditPackForm/EditPackForm.tests.jsx | 54 +++++ .../forms/packs/EditPackForm/index.js | 1 + .../forms/packs/PackForm/PackForm.jsx | 79 +++++++ .../forms/packs/PackForm/PackForm.tests.jsx | 45 ++++ .../forms/packs/PackForm/_styles.scss | 26 +++ .../components/forms/packs/PackForm/index.js | 1 + .../forms/packs/PackForm/validate.js | 15 ++ .../forms/queries/QueryForm/QueryForm.jsx | 2 +- .../validators/validate_numericality/index.js | 3 + .../EditPackFormWrapper.jsx | 68 ++++++ .../EditPackFormWrapper.tests.jsx | 53 +++++ .../packs/EditPackFormWrapper/index.js | 1 + .../packs/PackPageWrapper/PackPageWrapper.jsx | 12 +- .../queries/QueriesList/QueriesList.jsx | 128 +++++++++++ .../queries/QueriesList/QueriesList.tests.jsx | 57 +++++ .../QueriesListItem/QueriesListItem.jsx | 72 ++++++ .../QueriesListItem/QueriesListItem.tests.jsx | 33 +++ .../QueriesList/QueriesListItem/index.js | 1 + .../queries/QueriesList/_styles.scss | 164 ++++++++++++++ .../components/queries/QueriesList/index.js | 1 + .../QueriesListWrapper/QueriesListWrapper.jsx | 150 +++++++++++++ .../QueriesListWrapper.tests.jsx | 90 ++++++++ .../queries/QueriesListWrapper/_styles.scss | 44 ++++ .../queries/QueriesListWrapper/helpers.js | 13 ++ .../QueriesListWrapper/helpers.tests.js | 28 +++ .../queries/QueriesListWrapper/index.js | 1 + .../queries/QueryComposer/QueryComposer.jsx | 6 +- .../QueryComposer/QueryComposer.tests.jsx | 5 + .../queries/QueryComposer/_styles.scss | 22 -- .../PackInfoSidePanel/PackInfoSidePanel.jsx | 96 ++++---- .../PackInfoSidePanel/_styles.scss | 50 ++++- .../ScheduleQuerySidePanel.jsx | 45 ++++ .../ScheduleQuerySidePanel.tests.jsx | 15 ++ .../SearchPackQuery/SearchPackQuery.jsx | 124 +++++++++++ .../SearchPackQuery/SearchPackQuery.tests.jsx | 0 .../SearchPackQuery/_styles.scss | 39 ++++ .../SearchPackQuery/index.js | 1 + .../ScheduleQuerySidePanel/_styles.scss | 3 + .../ScheduleQuerySidePanel/index.js | 1 + .../components/side_panels/ShowSidePanel.jsx | 33 +++ frontend/interfaces/form_field.js | 2 +- frontend/interfaces/scheduled_query.js | 14 ++ frontend/kolide/endpoints.js | 3 + frontend/kolide/index.js | 66 ++++++ frontend/kolide/index.tests.js | 92 ++++++++ .../UserBlock/UserBlock.jsx | 2 +- .../UserManagementPage/UserBlock/_styles.scss | 2 +- .../UserManagementPage.tests.jsx | 2 +- .../pages/packs/AllPacksPage/AllPacksPage.jsx | 10 +- .../pages/packs/EditPackPage/EditPackPage.jsx | 209 ++++++++++++++++++ .../packs/EditPackPage/EditPackPage.tests.jsx | 35 +++ .../pages/packs/EditPackPage/_styles.scss | 5 + frontend/pages/packs/EditPackPage/index.js | 1 + .../PackComposerPage/PackComposerPage.jsx | 99 +++++++++ .../PackComposerPage.tests.jsx | 25 +++ .../pages/packs/PackComposerPage/_styles.scss | 9 + .../pages/packs/PackComposerPage/index.js | 1 + frontend/redux/nodes/entities/base/schemas.js | 2 + frontend/redux/nodes/entities/packs/config.js | 2 + .../redux/nodes/entities/queries/config.js | 1 + frontend/redux/nodes/entities/reducer.js | 2 + .../entities/scheduled_queries/actions.js | 3 + .../entities/scheduled_queries/config.js | 14 ++ .../entities/scheduled_queries/reducer.js | 3 + frontend/router/index.jsx | 17 +- frontend/styles/global/_global.scss | 26 ++- frontend/styles/global/_icons.scss | 24 ++ frontend/test/helpers.jsx | 23 +- frontend/test/mocks.js | 68 ++++++ frontend/test/stubs.js | 49 +++- frontend/utilities/icon_class.js | 4 + 93 files changed, 2849 insertions(+), 184 deletions(-) mode change 100644 => 100755 assets/fonts/kolidecons/kolidecons.woff create mode 100644 frontend/components/forms/ConfigurePackQueryForm/ConfigurePackQueryForm.jsx create mode 100644 frontend/components/forms/ConfigurePackQueryForm/ConfigurePackQueryForm.tests.jsx create mode 100644 frontend/components/forms/ConfigurePackQueryForm/_styles.scss create mode 100644 frontend/components/forms/ConfigurePackQueryForm/index.js create mode 100644 frontend/components/forms/ConfigurePackQueryForm/validate.js create mode 100644 frontend/components/forms/packs/EditPackForm/EditPackForm.jsx create mode 100644 frontend/components/forms/packs/EditPackForm/EditPackForm.tests.jsx create mode 100644 frontend/components/forms/packs/EditPackForm/index.js create mode 100644 frontend/components/forms/packs/PackForm/PackForm.jsx create mode 100644 frontend/components/forms/packs/PackForm/PackForm.tests.jsx create mode 100644 frontend/components/forms/packs/PackForm/_styles.scss create mode 100644 frontend/components/forms/packs/PackForm/index.js create mode 100644 frontend/components/forms/packs/PackForm/validate.js create mode 100644 frontend/components/forms/validators/validate_numericality/index.js create mode 100644 frontend/components/packs/EditPackFormWrapper/EditPackFormWrapper.jsx create mode 100644 frontend/components/packs/EditPackFormWrapper/EditPackFormWrapper.tests.jsx create mode 100644 frontend/components/packs/EditPackFormWrapper/index.js create mode 100644 frontend/components/queries/QueriesList/QueriesList.jsx create mode 100644 frontend/components/queries/QueriesList/QueriesList.tests.jsx create mode 100644 frontend/components/queries/QueriesList/QueriesListItem/QueriesListItem.jsx create mode 100644 frontend/components/queries/QueriesList/QueriesListItem/QueriesListItem.tests.jsx create mode 100644 frontend/components/queries/QueriesList/QueriesListItem/index.js create mode 100644 frontend/components/queries/QueriesList/_styles.scss create mode 100644 frontend/components/queries/QueriesList/index.js create mode 100644 frontend/components/queries/QueriesListWrapper/QueriesListWrapper.jsx create mode 100644 frontend/components/queries/QueriesListWrapper/QueriesListWrapper.tests.jsx create mode 100644 frontend/components/queries/QueriesListWrapper/_styles.scss create mode 100644 frontend/components/queries/QueriesListWrapper/helpers.js create mode 100644 frontend/components/queries/QueriesListWrapper/helpers.tests.js create mode 100644 frontend/components/queries/QueriesListWrapper/index.js create mode 100644 frontend/components/side_panels/ScheduleQuerySidePanel/ScheduleQuerySidePanel.jsx create mode 100644 frontend/components/side_panels/ScheduleQuerySidePanel/ScheduleQuerySidePanel.tests.jsx create mode 100644 frontend/components/side_panels/ScheduleQuerySidePanel/SearchPackQuery/SearchPackQuery.jsx create mode 100644 frontend/components/side_panels/ScheduleQuerySidePanel/SearchPackQuery/SearchPackQuery.tests.jsx create mode 100644 frontend/components/side_panels/ScheduleQuerySidePanel/SearchPackQuery/_styles.scss create mode 100644 frontend/components/side_panels/ScheduleQuerySidePanel/SearchPackQuery/index.js create mode 100644 frontend/components/side_panels/ScheduleQuerySidePanel/_styles.scss create mode 100644 frontend/components/side_panels/ScheduleQuerySidePanel/index.js create mode 100644 frontend/components/side_panels/ShowSidePanel.jsx create mode 100644 frontend/interfaces/scheduled_query.js create mode 100644 frontend/pages/packs/EditPackPage/EditPackPage.jsx create mode 100644 frontend/pages/packs/EditPackPage/EditPackPage.tests.jsx create mode 100644 frontend/pages/packs/EditPackPage/_styles.scss create mode 100644 frontend/pages/packs/EditPackPage/index.js create mode 100644 frontend/pages/packs/PackComposerPage/PackComposerPage.jsx create mode 100644 frontend/pages/packs/PackComposerPage/PackComposerPage.tests.jsx create mode 100644 frontend/pages/packs/PackComposerPage/_styles.scss create mode 100644 frontend/pages/packs/PackComposerPage/index.js create mode 100644 frontend/redux/nodes/entities/scheduled_queries/actions.js create mode 100644 frontend/redux/nodes/entities/scheduled_queries/config.js create mode 100644 frontend/redux/nodes/entities/scheduled_queries/reducer.js diff --git a/assets/fonts/kolidecons/kolidecons.woff b/assets/fonts/kolidecons/kolidecons.woff old mode 100644 new mode 100755 index a6206b51948cf996c9628531c327d5081fa6b61d..bf78b1f1566c188aafc571744db33e137d01296b GIT binary patch literal 10164 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 Xt3l_0OHUi@FE>bl{2|1*i|u~_ju|k4 literal 12308 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 ipo9(7{?`%$T_) => 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 (