From 8567cc458c0cc5399d65f020782d521a4ceeb636 Mon Sep 17 00:00:00 2001 From: Mike Stone Date: Wed, 21 Dec 2016 12:07:13 -0500 Subject: [PATCH] Run query (#549) * Adds campaigns to redux state * Update campaign with web socket data * Destroy the current campaign when creating a new one * close the socket when leaving the page or creating a new campaign * Allow stopping a running query * Update campaign with query results * Adds QueryResultsTable * Display flash message if campaign can't be created * Allow filtering query results * Adds filter icon * Prevent query text updates when the query is running --- .../forms/fields/InputField/InputField.jsx | 17 +- .../SelectTargetsDropdown.jsx | 2 +- .../fields/SelectTargetsDropdown/helpers.js | 17 -- .../SelectTargetsDropdown/helpers.tests.js | 23 --- .../forms/queries/QueryForm/QueryForm.jsx | 38 ++++- .../queries/QueryForm/QueryForm.tests.jsx | 30 ++++ .../queries/QueryComposer/QueryComposer.jsx | 9 +- .../QueryResultsTable/QueryResultsTable.jsx | 153 ++++++++++++++++++ .../QueryResultsTable.tests.jsx | 81 ++++++++++ .../queries/QueryResultsTable/_styles.scss | 49 ++++++ .../queries/QueryResultsTable/index.js | 1 + frontend/interfaces/campaign.js | 7 + frontend/kolide/base.js | 3 +- frontend/kolide/endpoints.js | 1 + frontend/kolide/helpers.js | 18 ++- frontend/kolide/helpers.tests.js | 18 +++ frontend/kolide/index.js | 26 ++- frontend/kolide/index.tests.js | 17 ++ .../UserManagementPage/UserManagementPage.jsx | 2 +- .../pages/queries/QueryPage/QueryPage.jsx | 105 +++++++++++- .../redux/nodes/entities/base/reduxConfig.js | 6 +- frontend/redux/nodes/entities/base/schemas.js | 2 + .../redux/nodes/entities/campaigns/actions.js | 3 + .../redux/nodes/entities/campaigns/config.js | 15 ++ .../redux/nodes/entities/campaigns/helpers.js | 41 +++++ .../nodes/entities/campaigns/helpers.tests.js | 104 ++++++++++++ .../redux/nodes/entities/campaigns/reducer.js | 3 + frontend/redux/nodes/entities/reducer.js | 2 + frontend/test/mocks.js | 11 ++ .../filter_array_by_hash.js | 22 +++ .../filter_array_by_hash.tests.js | 26 +++ .../utilities/filter_array_by_hash/index.js | 1 + 32 files changed, 790 insertions(+), 63 deletions(-) delete mode 100644 frontend/components/forms/fields/SelectTargetsDropdown/helpers.js delete mode 100644 frontend/components/forms/fields/SelectTargetsDropdown/helpers.tests.js create mode 100644 frontend/components/queries/QueryResultsTable/QueryResultsTable.jsx create mode 100644 frontend/components/queries/QueryResultsTable/QueryResultsTable.tests.jsx create mode 100644 frontend/components/queries/QueryResultsTable/_styles.scss create mode 100644 frontend/components/queries/QueryResultsTable/index.js create mode 100644 frontend/interfaces/campaign.js create mode 100644 frontend/redux/nodes/entities/campaigns/actions.js create mode 100644 frontend/redux/nodes/entities/campaigns/config.js create mode 100644 frontend/redux/nodes/entities/campaigns/helpers.js create mode 100644 frontend/redux/nodes/entities/campaigns/helpers.tests.js create mode 100644 frontend/redux/nodes/entities/campaigns/reducer.js create mode 100644 frontend/utilities/filter_array_by_hash/filter_array_by_hash.js create mode 100644 frontend/utilities/filter_array_by_hash/filter_array_by_hash.tests.js create mode 100644 frontend/utilities/filter_array_by_hash/index.js diff --git a/frontend/components/forms/fields/InputField/InputField.jsx b/frontend/components/forms/fields/InputField/InputField.jsx index c47ac5ffd9..9de03c18d4 100644 --- a/frontend/components/forms/fields/InputField/InputField.jsx +++ b/frontend/components/forms/fields/InputField/InputField.jsx @@ -1,6 +1,6 @@ import React, { Component, PropTypes } from 'react'; import classnames from 'classnames'; -import { pick } from 'lodash'; +import { noop, pick } from 'lodash'; import FormField from 'components/forms/FormField'; @@ -18,6 +18,7 @@ class InputField extends Component { labelClassName: PropTypes.string, name: PropTypes.string, onChange: PropTypes.func, + onFocus: PropTypes.func, placeholder: PropTypes.string, type: PropTypes.string, value: PropTypes.string.isRequired, @@ -29,6 +30,7 @@ class InputField extends Component { inputOptions: {}, label: null, labelClassName: '', + onFocus: noop, type: 'text', value: '', }; @@ -54,7 +56,17 @@ class InputField extends Component { } render () { - const { error, inputClassName, inputOptions, inputWrapperClass, name, placeholder, type, value } = this.props; + const { + error, + inputClassName, + inputOptions, + inputWrapperClass, + name, + onFocus, + placeholder, + type, + value, + } = this.props; const { onInputChange } = this; const shouldShowPasswordClass = type === 'password'; const inputClasses = classnames(baseClass, inputClassName, { @@ -87,6 +99,7 @@ class InputField extends Component { { this.input = r; }} diff --git a/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsDropdown.jsx b/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsDropdown.jsx index 16c255b289..3edeff6a3b 100644 --- a/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsDropdown.jsx +++ b/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsDropdown.jsx @@ -4,7 +4,7 @@ import classnames from 'classnames'; import Kolide from 'kolide'; import targetInterface from 'interfaces/target'; -import { formatSelectedTargetsForApi } from './helpers'; +import { formatSelectedTargetsForApi } from 'kolide/helpers'; import Input from './SelectTargetsInput'; import Menu from './SelectTargetsMenu'; diff --git a/frontend/components/forms/fields/SelectTargetsDropdown/helpers.js b/frontend/components/forms/fields/SelectTargetsDropdown/helpers.js deleted file mode 100644 index 20fb7d3155..0000000000 --- a/frontend/components/forms/fields/SelectTargetsDropdown/helpers.js +++ /dev/null @@ -1,17 +0,0 @@ -import { flatMap } from 'lodash'; - -const filterTarget = (targetType) => { - return (target) => { - return target.target_type === targetType ? [target.id] : []; - }; -}; - -export const formatSelectedTargetsForApi = (selectedTargets) => { - const targets = selectedTargets || []; - const hosts = flatMap(targets, filterTarget('hosts')); - const labels = flatMap(targets, filterTarget('labels')); - - return { hosts, labels }; -}; - -export default { formatSelectedTargetsForApi }; diff --git a/frontend/components/forms/fields/SelectTargetsDropdown/helpers.tests.js b/frontend/components/forms/fields/SelectTargetsDropdown/helpers.tests.js deleted file mode 100644 index ce43d35037..0000000000 --- a/frontend/components/forms/fields/SelectTargetsDropdown/helpers.tests.js +++ /dev/null @@ -1,23 +0,0 @@ -import expect from 'expect'; - -import helpers from './helpers'; - -const label1 = { id: 1, target_type: 'labels' }; -const label2 = { id: 2, target_type: 'labels' }; -const host1 = { id: 6, target_type: 'hosts' }; -const host2 = { id: 5, target_type: 'hosts' }; - -describe('SelectTargetsDropdown - helpers', () => { - describe('#formatSelectedTargetsForApi', () => { - const { formatSelectedTargetsForApi } = helpers; - - it('splits targets into labels and hosts', () => { - const targets = [host1, host2, label1, label2]; - - expect(formatSelectedTargetsForApi(targets)).toEqual({ - hosts: [6, 5], - labels: [1, 2], - }); - }); - }); -}); diff --git a/frontend/components/forms/queries/QueryForm/QueryForm.jsx b/frontend/components/forms/queries/QueryForm/QueryForm.jsx index 9989083362..4075c56a10 100644 --- a/frontend/components/forms/queries/QueryForm/QueryForm.jsx +++ b/frontend/components/forms/queries/QueryForm/QueryForm.jsx @@ -15,8 +15,10 @@ class QueryForm extends Component { onCancel: PropTypes.func, onRunQuery: PropTypes.func, onSave: PropTypes.func, + onStopQuery: PropTypes.func, onUpdate: PropTypes.func, query: queryInterface, + queryIsRunning: PropTypes.bool, queryText: PropTypes.string.isRequired, queryType: PropTypes.string, }; @@ -163,8 +165,35 @@ class QueryForm extends Component { renderButtons = () => { const { canSaveAsNew, canSaveChanges } = helpers; const { formData } = this.state; - const { onRunQuery, query, queryType } = this.props; + const { + onRunQuery, + onStopQuery, + query, + queryIsRunning, + queryType, + } = this.props; const { onCancel, onSave, onUpdate } = this; + let runQueryButton; + + if (queryIsRunning) { + runQueryButton = ( +