From d1a18bcb893c7ccdea51ec2fb7e383239beddbaf Mon Sep 17 00:00:00 2001 From: Mike Stone Date: Wed, 11 Jan 2017 12:10:14 -0500 Subject: [PATCH] Add targets to packs (#831) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Allow form field values to be an array * Send the server host and label ids on create * Get and display the targets in a pack * Adds target_type to labels and hosts * Allow updating a pack’s targets as well as name and description * Adds select targets dropdown to edit pack page * Adds targets to dropdown when pack is edited --- .../forms/packs/EditPackForm/EditPackForm.jsx | 24 +++++++- .../EditPackFormWrapper.jsx | 8 ++- frontend/interfaces/form_field.js | 4 +- frontend/interfaces/target.js | 9 +-- frontend/kolide/helpers.js | 6 +- frontend/kolide/helpers.tests.js | 9 +++ frontend/kolide/index.js | 14 +++-- frontend/kolide/index.tests.js | 8 ++- .../pages/packs/EditPackPage/EditPackPage.jsx | 60 +++++++++++++++++-- .../packs/EditPackPage/EditPackPage.tests.jsx | 2 + .../PackComposerPage/PackComposerPage.jsx | 27 +-------- frontend/redux/nodes/entities/hosts/config.js | 3 + .../redux/nodes/entities/labels/config.js | 3 + frontend/test/stubs.js | 57 ++++++++++++++++++ 14 files changed, 184 insertions(+), 50 deletions(-) diff --git a/frontend/components/forms/packs/EditPackForm/EditPackForm.jsx b/frontend/components/forms/packs/EditPackForm/EditPackForm.jsx index 871ad3d3d0..c9ff8e3281 100644 --- a/frontend/components/forms/packs/EditPackForm/EditPackForm.jsx +++ b/frontend/components/forms/packs/EditPackForm/EditPackForm.jsx @@ -4,8 +4,9 @@ import Button from 'components/buttons/Button'; import Form from 'components/forms/Form'; import formFieldInterface from 'interfaces/form_field'; import InputField from 'components/forms/fields/InputField'; +import SelectTargetsDropdown from 'components/forms/fields/SelectTargetsDropdown'; -const fieldNames = ['description', 'name']; +const fieldNames = ['description', 'name', 'targets']; class EditPackForm extends Component { static propTypes = { @@ -13,13 +14,23 @@ class EditPackForm extends Component { fields: PropTypes.shape({ description: formFieldInterface.isRequired, name: formFieldInterface.isRequired, + targets: formFieldInterface.isRequired, }).isRequired, handleSubmit: PropTypes.func.isRequired, onCancel: PropTypes.func.isRequired, + onFetchTargets: PropTypes.func, + targetsCount: PropTypes.number, }; render () { - const { className, fields, handleSubmit, onCancel } = this.props; + const { + className, + fields, + handleSubmit, + onCancel, + onFetchTargets, + targetsCount, + } = this.props; return (
@@ -29,6 +40,15 @@ class EditPackForm extends Component { + diff --git a/frontend/components/packs/EditPackFormWrapper/EditPackFormWrapper.jsx b/frontend/components/packs/EditPackFormWrapper/EditPackFormWrapper.jsx index 8bae9df2a5..f9b3c7ef8b 100644 --- a/frontend/components/packs/EditPackFormWrapper/EditPackFormWrapper.jsx +++ b/frontend/components/packs/EditPackFormWrapper/EditPackFormWrapper.jsx @@ -6,6 +6,7 @@ import EditPackForm from 'components/forms/packs/EditPackForm'; import Icon from 'components/icons/Icon'; import packInterface from 'interfaces/pack'; import SelectTargetsDropdown from 'components/forms/fields/SelectTargetsDropdown'; +import targetInterface from 'interfaces/target'; const baseClass = 'edit-pack-form'; @@ -18,6 +19,7 @@ class EditPackFormWrapper extends Component { onEditPack: PropTypes.func.isRequired, onFetchTargets: PropTypes.func, pack: packInterface.isRequired, + packTargets: PropTypes.arrayOf(targetInterface), targetsCount: PropTypes.number, }; @@ -30,6 +32,7 @@ class EditPackFormWrapper extends Component { onEditPack, onFetchTargets, pack, + packTargets, targetsCount, } = this.props; @@ -37,9 +40,11 @@ class EditPackFormWrapper extends Component { return ( ); } @@ -63,6 +68,7 @@ class EditPackFormWrapper extends Component { name="selected-pack-targets" onFetchTargets={onFetchTargets} onSelect={noop} + selectedTargets={packTargets} targetsCount={targetsCount} disabled className={`${baseClass}__select-targets`} diff --git a/frontend/interfaces/form_field.js b/frontend/interfaces/form_field.js index ec07788e82..2149e94f05 100644 --- a/frontend/interfaces/form_field.js +++ b/frontend/interfaces/form_field.js @@ -4,6 +4,8 @@ export default PropTypes.shape({ error: PropTypes.string, name: PropTypes.string, onChange: PropTypes.func, - value: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]), + value: PropTypes.oneOfType( + [PropTypes.array, PropTypes.bool, PropTypes.number, PropTypes.string] + ), }); diff --git a/frontend/interfaces/target.js b/frontend/interfaces/target.js index f6404fd551..993d3a7926 100644 --- a/frontend/interfaces/target.js +++ b/frontend/interfaces/target.js @@ -1,8 +1,5 @@ import { PropTypes } from 'react'; +import hostInterface from 'interfaces/host'; +import labelInterface from 'interfaces/label'; -export default PropTypes.shape({ - id: PropTypes.number, - label: PropTypes.string, - name: PropTypes.string, - target_type: PropTypes.string, -}); +export default PropTypes.oneOfType([hostInterface, labelInterface]); diff --git a/frontend/kolide/helpers.js b/frontend/kolide/helpers.js index 69cac9f5c5..15e69564e1 100644 --- a/frontend/kolide/helpers.js +++ b/frontend/kolide/helpers.js @@ -47,11 +47,15 @@ export const formatConfigDataForServer = (config) => { }; }; -export const formatSelectedTargetsForApi = (selectedTargets) => { +export const formatSelectedTargetsForApi = (selectedTargets, appendID = false) => { const targets = selectedTargets || []; const hosts = flatMap(targets, filterTarget('hosts')); const labels = flatMap(targets, filterTarget('labels')); + if (appendID) { + return { host_ids: hosts, label_ids: labels }; + } + return { hosts, labels }; }; diff --git a/frontend/kolide/helpers.tests.js b/frontend/kolide/helpers.tests.js index 663ddb2282..5d24d288ac 100644 --- a/frontend/kolide/helpers.tests.js +++ b/frontend/kolide/helpers.tests.js @@ -53,6 +53,15 @@ describe('Kolide API - helpers', () => { labels: [1, 2], }); }); + + it('appends `_id` when appendID is specified', () => { + const targets = [host1, host2, label1, label2]; + + expect(formatSelectedTargetsForApi(targets, true)).toEqual({ + host_ids: [6, 5], + label_ids: [1, 2], + }); + }); }); describe('#setupData', () => { diff --git a/frontend/kolide/index.js b/frontend/kolide/index.js index ed400d0793..9720f879a7 100644 --- a/frontend/kolide/index.js +++ b/frontend/kolide/index.js @@ -32,10 +32,11 @@ class Kolide extends Base { }); } - createPack = ({ name, description }) => { + createPack = ({ name, description, targets }) => { const { PACKS } = endpoints; + const packTargets = helpers.formatSelectedTargetsForApi(targets, true); - return this.authenticatedPost(this.endpoint(PACKS), JSON.stringify({ description, name })) + return this.authenticatedPost(this.endpoint(PACKS), JSON.stringify({ description, name, ...packTargets })) .then((response) => { return response.pack; }); } @@ -121,7 +122,7 @@ class Kolide extends Base { const { HOSTS } = endpoints; return this.authenticatedGet(this.endpoint(HOSTS)) - .then((response) => { return response.hosts; }); + .then(response => response.hosts); } getLabelHosts = (labelID) => { @@ -358,11 +359,12 @@ class Kolide extends Base { return this.authenticatedPatch(this.endpoint(CONFIG), JSON.stringify(configData)); } - updatePack = ({ id: packID }, updateParams) => { + updatePack = (pack, { description, name, targets }) => { const { PACKS } = endpoints; - const updatePackEndpoint = `${this.baseURL}${PACKS}/${packID}`; + const updatePackEndpoint = `${this.baseURL}${PACKS}/${pack.id}`; + const packTargets = helpers.formatSelectedTargetsForApi(targets, true); - return this.authenticatedPatch(updatePackEndpoint, JSON.stringify(updateParams)) + return this.authenticatedPatch(updatePackEndpoint, JSON.stringify({ description, name, ...packTargets })) .then((response) => { return response.pack; }); } diff --git a/frontend/kolide/index.tests.js b/frontend/kolide/index.tests.js index 8906086682..1150b33dff 100644 --- a/frontend/kolide/index.tests.js +++ b/frontend/kolide/index.tests.js @@ -4,7 +4,7 @@ import nock from 'nock'; import Kolide from 'kolide'; import helpers from 'kolide/helpers'; import mocks from 'test/mocks'; -import { queryStub, userStub } from 'test/stubs'; +import { hostStub, queryStub, userStub } from 'test/stubs'; const { invalidForgotPasswordRequest, @@ -80,7 +80,7 @@ describe('Kolide - API client', () => { it('#createPack', (done) => { const { description, name } = pack; - const params = { description, name }; + const params = { description, name, host_ids: [], label_ids: [] }; const request = validCreatePackRequest(bearerToken, params); Kolide.setBearerToken(bearerToken); @@ -105,7 +105,9 @@ describe('Kolide - API client', () => { }); it('#updatePack', (done) => { - const updatePackParams = { name: 'New Pack Name' }; + const targets = [hostStub]; + const packTargets = helpers.formatSelectedTargetsForApi(targets, true); + const updatePackParams = { name: 'New Pack Name', ...packTargets }; const request = validUpdatePackRequest(bearerToken, pack, updatePackParams); Kolide.setBearerToken(bearerToken); diff --git a/frontend/pages/packs/EditPackPage/EditPackPage.jsx b/frontend/pages/packs/EditPackPage/EditPackPage.jsx index 7ad2522751..1a257502c6 100644 --- a/frontend/pages/packs/EditPackPage/EditPackPage.jsx +++ b/frontend/pages/packs/EditPackPage/EditPackPage.jsx @@ -1,9 +1,13 @@ import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; -import { noop, size, find } from 'lodash'; +import { filter, includes, isEqual, noop, size, find } from 'lodash'; import { push } from 'react-router-redux'; import EditPackFormWrapper from 'components/packs/EditPackFormWrapper'; +import hostActions from 'redux/nodes/entities/hosts/actions'; +import hostInterface from 'interfaces/host'; +import labelActions from 'redux/nodes/entities/labels/actions'; +import labelInterface from 'interfaces/label'; import packActions from 'redux/nodes/entities/packs/actions'; import ScheduleQuerySidePanel from 'components/side_panels/ScheduleQuerySidePanel'; import packInterface from 'interfaces/pack'; @@ -24,7 +28,9 @@ export class EditPackPage extends Component { isLoadingPack: PropTypes.bool, isLoadingScheduledQueries: PropTypes.bool, pack: packInterface, + packHosts: PropTypes.arrayOf(hostInterface), packID: PropTypes.string, + packLabels: PropTypes.arrayOf(labelInterface), scheduledQueries: PropTypes.arrayOf(queryInterface), }; @@ -41,7 +47,16 @@ export class EditPackPage extends Component { } componentDidMount () { - const { allQueries, dispatch, isLoadingPack, pack, packID, scheduledQueries } = this.props; + const { + allQueries, + dispatch, + isLoadingPack, + pack, + packHosts, + packID, + packLabels, + scheduledQueries, + } = this.props; const { load } = packActions; const { loadAll } = queryActions; @@ -49,6 +64,16 @@ export class EditPackPage extends Component { dispatch(load(packID)); } + if (pack) { + if (!packHosts || packHosts.length !== pack.host_ids.length) { + dispatch(hostActions.loadAll()); + } + + if (!packLabels || packLabels.length !== pack.label_ids.length) { + dispatch(labelActions.loadAll()); + } + } + if (!size(scheduledQueries)) { dispatch(scheduledQueryActions.loadAll({ id: packID })); } @@ -60,6 +85,20 @@ export class EditPackPage extends Component { return false; } + componentWillReceiveProps ({ dispatch, pack, packHosts, packLabels }) { + if (!isEqual(pack, this.props.pack)) { + if (!packHosts || packHosts.length !== pack.host_ids.length) { + dispatch(hostActions.loadAll()); + } + + if (!packLabels || packLabels.length !== pack.label_ids.length) { + dispatch(labelActions.loadAll()); + } + } + + return false; + } + onCancelEditPack = () => { const { dispatch, isEdit, packID } = this.props; @@ -97,10 +136,10 @@ export class EditPackPage extends Component { } handlePackFormSubmit = (formData) => { - const { dispatch } = this.props; + const { dispatch, pack } = this.props; const { update } = packActions; - return dispatch(update(formData)); + return dispatch(update(pack, formData)); } handleRemoveScheduledQueries = (scheduledQueryIDs) => { @@ -149,7 +188,9 @@ export class EditPackPage extends Component { onToggleEdit, } = this; const { targetsCount, selectedQuery } = this.state; - const { allQueries, isEdit, isLoadingScheduledQueries, pack, scheduledQueries } = this.props; + const { allQueries, isEdit, isLoadingScheduledQueries, pack, packHosts, packLabels, scheduledQueries } = this.props; + + const packTargets = [...packHosts, ...packLabels]; if (!pack || isLoadingScheduledQueries) { return false; @@ -166,6 +207,7 @@ export class EditPackPage extends Component { onEditPack={onToggleEdit} onFetchTargets={onFetchTargets} pack={pack} + packTargets={packTargets} targetsCount={targetsCount} /> { const scheduledQueries = entityGetter.get('scheduled_queries').where({ pack_id: packID }); const isLoadingScheduledQueries = state.entities.scheduled_queries.loading; const isEdit = route.path === 'edit'; + const packHosts = pack ? filter(state.entities.hosts.data, (host) => { + return includes(pack.host_ids, host.id); + }) : []; + const packLabels = pack ? filter(state.entities.labels.data, (label) => { + return includes(pack.label_ids, label.id); + }) : []; return { allQueries, @@ -201,7 +249,9 @@ const mapStateToProps = (state, { params, route }) => { isLoadingPack, isLoadingScheduledQueries, pack, + packHosts, packID, + packLabels, scheduledQueries, }; }; diff --git a/frontend/pages/packs/EditPackPage/EditPackPage.tests.jsx b/frontend/pages/packs/EditPackPage/EditPackPage.tests.jsx index 864e07331d..e6d9b2f789 100644 --- a/frontend/pages/packs/EditPackPage/EditPackPage.tests.jsx +++ b/frontend/pages/packs/EditPackPage/EditPackPage.tests.jsx @@ -8,6 +8,8 @@ import EditPackPage from './EditPackPage'; describe('EditPackPage - component', () => { const store = { entities: { + hosts: { data: {} }, + labels: { data: {} }, packs: { data: { [packStub.id]: packStub, diff --git a/frontend/pages/packs/PackComposerPage/PackComposerPage.jsx b/frontend/pages/packs/PackComposerPage/PackComposerPage.jsx index 494c67631e..a2a85f58e5 100644 --- a/frontend/pages/packs/PackComposerPage/PackComposerPage.jsx +++ b/frontend/pages/packs/PackComposerPage/PackComposerPage.jsx @@ -3,7 +3,6 @@ import { connect } from 'react-redux'; import { noop } from 'lodash'; import { push } from 'react-router-redux'; -import Kolide from 'kolide'; import packActions from 'redux/nodes/entities/packs/actions'; import PackForm from 'components/forms/packs/PackForm'; import PackInfoSidePanel from 'components/side_panels/PackInfoSidePanel'; @@ -47,37 +46,15 @@ export class PackComposerPage extends Component { } handleSubmit = (formData) => { - const { create, load } = packActions; + const { create } = packActions; const { dispatch } = this.props; const { visitPackPage } = this; return dispatch(create(formData)) .then((pack) => { const { id: packID } = pack; - const { targets } = formData; - if (!targets) { - return visitPackPage(packID); - } - - const promises = targets.map((target) => { - const { id: targetID } = target; - - if (target.target_type === 'labels') { - Kolide.addLabelToPack(packID, targetID); - } - - // TODO: Add host to pack when API is available - return Promise.resolve(); - }); - - return Promise.all(promises) - .then(() => { - dispatch(load(packID)) - .then(() => { - return visitPackPage(packID); - }); - }); + return visitPackPage(packID); }); } diff --git a/frontend/redux/nodes/entities/hosts/config.js b/frontend/redux/nodes/entities/hosts/config.js index 6cab00a22b..d2dc128939 100644 --- a/frontend/redux/nodes/entities/hosts/config.js +++ b/frontend/redux/nodes/entities/hosts/config.js @@ -7,5 +7,8 @@ const { HOSTS: schema } = schemas; export default reduxConfig({ entityName: 'hosts', loadAllFunc: Kolide.getHosts, + parseEntityFunc: (host) => { + return { ...host, target_type: 'hosts' }; + }, schema, }); diff --git a/frontend/redux/nodes/entities/labels/config.js b/frontend/redux/nodes/entities/labels/config.js index b8205cd6cf..82ba351289 100644 --- a/frontend/redux/nodes/entities/labels/config.js +++ b/frontend/redux/nodes/entities/labels/config.js @@ -8,5 +8,8 @@ export default reduxConfig({ createFunc: Kolide.createLabel, entityName: 'labels', loadAllFunc: Kolide.getLabels, + parseEntityFunc: (label) => { + return { ...label, target_type: 'labels' }; + }, schema, }); diff --git a/frontend/test/stubs.js b/frontend/test/stubs.js index f849bdc89c..cd182ba1e6 100644 --- a/frontend/test/stubs.js +++ b/frontend/test/stubs.js @@ -30,6 +30,60 @@ export const configStub = { }, }; +export const hostStub = { + created_at: '2017-01-10T19:18:55Z', + updated_at: '2017-01-10T20:13:52Z', + deleted_at: null, + deleted: false, + id: 1, + detail_updated_at: '2017-01-10T20:01:48Z', + seen_time: '2017-01-10T20:13:54Z', + hostname: '52883a0ba916', + uuid: 'FD87130B-09A9-683D-9095-D92CD20728CA', + platform: 'ubuntu', + osquery_version: '2.1.2', + os_version: 'Ubuntu 14.4.', + build: '', + platform_like: 'debian', + code_name: '', + uptime: 45469000000000, + memory: 2094940160, + cpu_type: '6', + cpu_subtype: '78', + cpu_brand: 'Intel(R) Core(TM) i5-6267U CPU @ 2.90GHz', + cpu_physical_cores: 2, + cpu_logical_cores: 2, + hardware_vendor: ' ', + hardware_model: 'BHYVE', + hardware_version: '1.0', + hardware_serial: 'None', + computer_name: '52883a0ba916', + primary_ip_id: 1, + network_interfaces: [ + { + id: 1, + interface: 'eth0', + address: '172.19.0.8', + mask: '255.255.0.0', + broadcast: '172.19.0.8', + point_to_point: '', + mac: '02:42:ac:13:00:08', + type: 1, + mtu: 1500, + metric: 0, + ipackets: 512, + opackets: 317, + ibytes: 97231, + obytes: 43502, + ierrors: 0, + oerrors: 0, + last_change: -1, + }, + ], + status: 'online', + display_text: '52883a0ba916', +}; + export const packStub = { created_at: '0001-01-01T00:00:00Z', updated_at: '0001-01-01T00:00:00Z', @@ -41,6 +95,8 @@ export const packStub = { platform: '', created_by: 1, disabled: false, + host_ids: [], + label_ids: [], }; export const queryStub = { @@ -82,6 +138,7 @@ export const userStub = { export default { adminUserStub, configStub, + hostStub, packStub, queryStub, scheduledQueryStub,