diff --git a/frontend/components/forms/ConfigurePackQueryForm/ConfigurePackQueryForm.jsx b/frontend/components/forms/ConfigurePackQueryForm/ConfigurePackQueryForm.jsx index 0261d4c65f..c84c2f53d9 100644 --- a/frontend/components/forms/ConfigurePackQueryForm/ConfigurePackQueryForm.jsx +++ b/frontend/components/forms/ConfigurePackQueryForm/ConfigurePackQueryForm.jsx @@ -40,6 +40,9 @@ class ConfigurePackQueryForm extends Component { platform: formFieldInterface.isRequired, version: formFieldInterface.isRequired, }).isRequired, + formData: PropTypes.shape({ + id: PropTypes.number, + }), handleSubmit: PropTypes.func, onCancel: PropTypes.func, }; @@ -47,14 +50,29 @@ class ConfigurePackQueryForm extends Component { onCancel = (evt) => { evt.preventDefault(); - const { onCancel: handleCancel } = this.props; + const { formData, onCancel: handleCancel } = this.props; - return handleCancel(); + return handleCancel(formData); + } + + renderCancelButton = () => { + const { formData } = this.props; + const { onCancel } = this; + + if (!formData.id) { + return false; + } + + return ( + + ); } render () { const { fields, handleSubmit } = this.props; - const { onCancel } = this; + const { renderCancelButton } = this; return (
@@ -98,9 +116,7 @@ class ConfigurePackQueryForm extends Component { type="number" />
- + {renderCancelButton()} diff --git a/frontend/components/forms/ConfigurePackQueryForm/ConfigurePackQueryForm.tests.jsx b/frontend/components/forms/ConfigurePackQueryForm/ConfigurePackQueryForm.tests.jsx index b0e07a5c17..90b7dea39b 100644 --- a/frontend/components/forms/ConfigurePackQueryForm/ConfigurePackQueryForm.tests.jsx +++ b/frontend/components/forms/ConfigurePackQueryForm/ConfigurePackQueryForm.tests.jsx @@ -5,6 +5,7 @@ import { noop } from 'lodash'; import ConfigurePackQueryForm from 'components/forms/ConfigurePackQueryForm'; import { itBehavesLikeAFormDropdownElement, itBehavesLikeAFormInputElement } from 'test/helpers'; +import { scheduledQueryStub } from 'test/stubs'; describe('ConfigurePackQueryForm - component', () => { afterEach(restoreSpies); @@ -53,4 +54,43 @@ describe('ConfigurePackQueryForm - component', () => { }); }); }); + + describe('cancelling the form', () => { + const CancelButton = form => form.find('.configure-pack-query-form__cancel-btn'); + + it('displays a cancel Button when updating a scheduled query', () => { + const NewScheduledQueryForm = mount( + + ); + const UpdateScheduledQueryForm = mount( + + ); + + expect(CancelButton(NewScheduledQueryForm).length).toEqual(0); + expect(CancelButton(UpdateScheduledQueryForm).length).toEqual(1); + }); + + it('calls the onCancel prop when the cancel Button is clicked', () => { + const spy = createSpy(); + const UpdateScheduledQueryForm = mount( + + ); + + CancelButton(UpdateScheduledQueryForm).simulate('click'); + + expect(spy).toHaveBeenCalledWith(scheduledQueryStub); + }); + }); }); diff --git a/frontend/components/queries/ScheduledQueriesList/ScheduledQueriesList.jsx b/frontend/components/queries/ScheduledQueriesList/ScheduledQueriesList.jsx index 6ae65a3f05..4be6d68ea6 100644 --- a/frontend/components/queries/ScheduledQueriesList/ScheduledQueriesList.jsx +++ b/frontend/components/queries/ScheduledQueriesList/ScheduledQueriesList.jsx @@ -12,10 +12,11 @@ const baseClass = 'scheduled-queries-list'; class ScheduledQueriesList extends Component { static propTypes = { isScheduledQueriesAvailable: PropTypes.bool, - onSelectAllQueries: PropTypes.func.isRequired, + onCheckAllQueries: PropTypes.func.isRequired, + onCheckQuery: PropTypes.func.isRequired, onSelectQuery: PropTypes.func.isRequired, scheduledQueries: PropTypes.arrayOf(queryInterface).isRequired, - selectedScheduledQueryIDs: PropTypes.arrayOf(PropTypes.number).isRequired, + checkedScheduledQueryIDs: PropTypes.arrayOf(PropTypes.number).isRequired, }; constructor (props) { @@ -25,23 +26,23 @@ class ScheduledQueriesList extends Component { } isChecked = (scheduledQuery) => { - const { selectedScheduledQueryIDs } = this.props; + const { checkedScheduledQueryIDs } = this.props; const { allQueriesSelected } = this.state; if (allQueriesSelected) { return true; } - return includes(selectedScheduledQueryIDs, scheduledQuery.id); + return includes(checkedScheduledQueryIDs, scheduledQuery.id); } handleSelectAllQueries = (shouldSelectAllQueries) => { - const { onSelectAllQueries } = this.props; + const { onCheckAllQueries } = this.props; const { allQueriesSelected } = this.state; this.setState({ allQueriesSelected: !allQueriesSelected }); - return onSelectAllQueries(shouldSelectAllQueries); + return onCheckAllQueries(shouldSelectAllQueries); } renderHelpText = () => { @@ -83,12 +84,12 @@ class ScheduledQueriesList extends Component { } render () { - const { onSelectQuery, scheduledQueries, selectedScheduledQueryIDs } = this.props; + const { onCheckQuery, onSelectQuery, scheduledQueries, checkedScheduledQueryIDs } = this.props; const { allQueriesSelected } = this.state; const { renderHelpText, handleSelectAllQueries } = this; const wrapperClassName = classnames(`${baseClass}__table`, { - [`${baseClass}__table--query-selected`]: size(selectedScheduledQueryIDs), + [`${baseClass}__table--query-selected`]: size(checkedScheduledQueryIDs), }); return ( @@ -116,6 +117,7 @@ class ScheduledQueriesList extends Component { diff --git a/frontend/components/queries/ScheduledQueriesList/ScheduledQueriesListItem/ScheduledQueriesListItem.jsx b/frontend/components/queries/ScheduledQueriesList/ScheduledQueriesListItem/ScheduledQueriesListItem.jsx index 7277e59f64..a10d81b3a1 100644 --- a/frontend/components/queries/ScheduledQueriesList/ScheduledQueriesListItem/ScheduledQueriesListItem.jsx +++ b/frontend/components/queries/ScheduledQueriesList/ScheduledQueriesListItem/ScheduledQueriesListItem.jsx @@ -1,6 +1,7 @@ import React, { Component, PropTypes } from 'react'; import Checkbox from 'components/forms/fields/Checkbox'; +import ClickableTableRow from 'components/ClickableTableRow'; import Icon from 'components/icons/Icon'; import PlatformIcon from 'components/icons/PlatformIcon'; import { isEqual } from 'lodash'; @@ -10,6 +11,7 @@ class ScheduledQueriesListItem extends Component { static propTypes = { checked: PropTypes.bool, disabled: PropTypes.bool, + onCheck: PropTypes.func.isRequired, onSelect: PropTypes.func.isRequired, scheduledQuery: scheduledQueryInterface.isRequired, }; @@ -23,9 +25,15 @@ class ScheduledQueriesListItem extends Component { } onCheck = (value) => { + const { onCheck, scheduledQuery } = this.props; + + return onCheck(value, scheduledQuery.id); + } + + onSelect = () => { const { onSelect, scheduledQuery } = this.props; - return onSelect(value, scheduledQuery.id); + return onSelect(scheduledQuery); } loggingTypeString = () => { @@ -55,12 +63,11 @@ class ScheduledQueriesListItem extends Component { render () { const { checked, disabled, scheduledQuery } = this.props; - const { onCheck, renderPlatformIcon } = this; const { id, name, interval, shard, version } = scheduledQuery; - const { loggingTypeString } = this; + const { loggingTypeString, onCheck, onSelect, renderPlatformIcon } = this; return ( - + {version ? `${version}+` : 'Any'} {shard} - + ); } } diff --git a/frontend/components/queries/ScheduledQueriesList/ScheduledQueriesListItem/ScheduledQueriesListItem.tests.jsx b/frontend/components/queries/ScheduledQueriesList/ScheduledQueriesListItem/ScheduledQueriesListItem.tests.jsx index a46ae32cd9..d2268b7794 100644 --- a/frontend/components/queries/ScheduledQueriesList/ScheduledQueriesListItem/ScheduledQueriesListItem.tests.jsx +++ b/frontend/components/queries/ScheduledQueriesList/ScheduledQueriesListItem/ScheduledQueriesListItem.tests.jsx @@ -6,11 +6,18 @@ import { noop } from 'lodash'; import { scheduledQueryStub } from 'test/stubs'; import ScheduledQueriesListItem from './index'; +const defaultProps = { + checked: false, + onCheck: noop, + onSelect: noop, + scheduledQuery: scheduledQueryStub, +}; + describe('ScheduledQueriesListItem - component', () => { afterEach(restoreSpies); it('renders the scheduled query data', () => { - const component = mount(); + const component = mount(); expect(component.text()).toInclude(scheduledQueryStub.name); expect(component.text()).toInclude(scheduledQueryStub.interval); expect(component.text()).toInclude(scheduledQueryStub.shard); @@ -18,17 +25,29 @@ describe('ScheduledQueriesListItem - component', () => { }); it('renders a Checkbox component', () => { - const component = mount(); + const component = mount(); expect(component.find('Checkbox').length).toEqual(1); }); - it('calls the onSelect prop when a checkbox is changed', () => { - const onSelectSpy = createSpy(); - const component = mount(); + it('calls the onCheck prop when a checkbox is changed', () => { + const onCheckSpy = createSpy(); + const props = { ...defaultProps, onCheck: onCheckSpy }; + const component = mount(); const checkbox = component.find('Checkbox').first(); checkbox.find('input').simulate('change'); - expect(onSelectSpy).toHaveBeenCalledWith(true, scheduledQueryStub.id); + expect(onCheckSpy).toHaveBeenCalledWith(true, scheduledQueryStub.id); + }); + + it('calls the onSelect prop when a list item is selected', () => { + const spy = createSpy(); + const props = { ...defaultProps, onSelect: spy }; + const component = mount(); + const tableRow = component.find('ClickableTableRow'); + + tableRow.simulate('click'); + + expect(spy).toHaveBeenCalledWith(scheduledQueryStub); }); }); diff --git a/frontend/components/queries/ScheduledQueriesListWrapper/ScheduledQueriesListWrapper.jsx b/frontend/components/queries/ScheduledQueriesListWrapper/ScheduledQueriesListWrapper.jsx index 852a304247..ccb0a377a8 100644 --- a/frontend/components/queries/ScheduledQueriesListWrapper/ScheduledQueriesListWrapper.jsx +++ b/frontend/components/queries/ScheduledQueriesListWrapper/ScheduledQueriesListWrapper.jsx @@ -14,6 +14,7 @@ class ScheduledQueriesListWrapper extends Component { static propTypes = { onRemoveScheduledQueries: PropTypes.func, onScheduledQueryFormSubmit: PropTypes.func, + onSelectScheduledQuery: PropTypes.func, scheduledQueries: PropTypes.arrayOf(queryInterface), }; @@ -22,8 +23,7 @@ class ScheduledQueriesListWrapper extends Component { this.state = { querySearchText: '', - selectAll: false, - selectedScheduledQueryIDs: [], + checkedScheduledQueryIDs: [], }; } @@ -31,35 +31,35 @@ class ScheduledQueriesListWrapper extends Component { evt.preventDefault(); const { onRemoveScheduledQueries: handleRemoveScheduledQueries } = this.props; - const { selectedScheduledQueryIDs } = this.state; + const { checkedScheduledQueryIDs } = this.state; - this.setState({ selectedScheduledQueryIDs: [] }); + this.setState({ checkedScheduledQueryIDs: [] }); - return handleRemoveScheduledQueries(selectedScheduledQueryIDs); + return handleRemoveScheduledQueries(checkedScheduledQueryIDs); } - onSelectAllQueries = (shouldSelectAll) => { - if (shouldSelectAll) { + onCheckAllQueries = (shouldCheckAll) => { + if (shouldCheckAll) { const allScheduledQueries = this.getQueries(); - const selectedScheduledQueryIDs = allScheduledQueries.map(sq => sq.id); + const checkedScheduledQueryIDs = allScheduledQueries.map(sq => sq.id); - this.setState({ selectedScheduledQueryIDs }); + this.setState({ checkedScheduledQueryIDs }); return false; } - this.setState({ selectedScheduledQueryIDs: [] }); + this.setState({ checkedScheduledQueryIDs: [] }); return false; } - onSelectQuery = (shouldAddQuery, scheduledQueryID) => { - const { selectedScheduledQueryIDs } = this.state; - const newSelectedScheduledQueryIDs = shouldAddQuery ? - selectedScheduledQueryIDs.concat(scheduledQueryID) : - pull(selectedScheduledQueryIDs, scheduledQueryID); + onCheckQuery = (shouldCheckQuery, scheduledQueryID) => { + const { checkedScheduledQueryIDs } = this.state; + const newCheckedScheduledQueryIDs = shouldCheckQuery ? + checkedScheduledQueryIDs.concat(scheduledQueryID) : + pull(checkedScheduledQueryIDs, scheduledQueryID); - this.setState({ selectedScheduledQueryIDs: newSelectedScheduledQueryIDs }); + this.setState({ checkedScheduledQueryIDs: newCheckedScheduledQueryIDs }); return false; } @@ -77,9 +77,9 @@ class ScheduledQueriesListWrapper extends Component { renderButton = () => { const { onRemoveScheduledQueries } = this; - const { selectedScheduledQueryIDs } = this.state; + const { checkedScheduledQueryIDs } = this.state; - const scheduledQueryCount = selectedScheduledQueryIDs.length; + const scheduledQueryCount = checkedScheduledQueryIDs.length; if (scheduledQueryCount) { const queryText = scheduledQueryCount === 1 ? 'Query' : 'Queries'; @@ -107,19 +107,20 @@ class ScheduledQueriesListWrapper extends Component { } renderQueriesList = () => { - const { getQueries, onHidePackForm, onSelectAllQueries, onSelectQuery } = this; - const { onScheduledQueryFormSubmit, scheduledQueries } = this.props; - const { selectedScheduledQueryIDs } = this.state; + const { getQueries, onHidePackForm, onCheckAllQueries, onCheckQuery } = this; + const { onScheduledQueryFormSubmit, onSelectScheduledQuery, scheduledQueries } = this.props; + const { checkedScheduledQueryIDs } = this.state; return (
diff --git a/frontend/components/queries/ScheduledQueriesListWrapper/ScheduledQueriesListWrapper.tests.jsx b/frontend/components/queries/ScheduledQueriesListWrapper/ScheduledQueriesListWrapper.tests.jsx index 920bbf07d1..c59a17a250 100644 --- a/frontend/components/queries/ScheduledQueriesListWrapper/ScheduledQueriesListWrapper.tests.jsx +++ b/frontend/components/queries/ScheduledQueriesListWrapper/ScheduledQueriesListWrapper.tests.jsx @@ -1,27 +1,28 @@ import React from 'react'; import expect, { createSpy, restoreSpies } from 'expect'; import { mount } from 'enzyme'; +import { noop } from 'lodash'; -import { queryStub, scheduledQueryStub } from 'test/stubs'; +import { scheduledQueryStub } from 'test/stubs'; import { fillInFormInput } from 'test/helpers'; import ScheduledQueriesListWrapper from './index'; -const allQueries = [queryStub]; const scheduledQueries = [ scheduledQueryStub, { ...scheduledQueryStub, id: 100, name: 'mac hosts' }, ]; +const defaultProps = { + onRemoveScheduledQueries: noop, + onScheduledQueryFormSubmit: noop, + onSelectScheduledQuery: noop, + scheduledQueries, +}; describe('ScheduledQueriesListWrapper - component', () => { afterEach(restoreSpies); it('renders the "Remove Query" button when queries have been selected', () => { - const component = mount( - - ); + const component = mount(); component.find('Checkbox').last().find('input').simulate('change'); @@ -34,15 +35,14 @@ describe('ScheduledQueriesListWrapper - component', () => { it('calls the onRemoveScheduledQueries prop', () => { const spy = createSpy(); - const component = mount( - - ); + const props = { ...defaultProps, onRemoveScheduledQueries: spy }; + const component = mount(); - component.find('Checkbox').last().find('input').simulate('change'); + component + .find('Checkbox') + .find({ name: `scheduled-query-checkbox-${scheduledQueryStub.id}` }) + .find('input') + .simulate('change'); const removeQueryBtn = component.find('Button').find({ children: ['Remove ', 'Query'] }); @@ -52,12 +52,7 @@ describe('ScheduledQueriesListWrapper - component', () => { }); it('filters queries', () => { - const component = mount( - - ); + const component = mount(); const searchQueriesInput = component.find({ name: 'search-queries' }); const QueriesList = component.find('ScheduledQueriesList'); @@ -71,20 +66,15 @@ describe('ScheduledQueriesListWrapper - component', () => { it('allows selecting all scheduled queries at once', () => { const allScheduledQueryIDs = scheduledQueries.map(sq => sq.id); - const component = mount( - - ); + const component = mount(); const selectAllCheckbox = component.find({ name: 'select-all-scheduled-queries' }); selectAllCheckbox.simulate('change'); - expect(component.state('selectedScheduledQueryIDs')).toEqual(allScheduledQueryIDs); + expect(component.state('checkedScheduledQueryIDs')).toEqual(allScheduledQueryIDs); selectAllCheckbox.simulate('change'); - expect(component.state('selectedScheduledQueryIDs')).toEqual([]); + expect(component.state('checkedScheduledQueryIDs')).toEqual([]); }); }); diff --git a/frontend/components/side_panels/ScheduleQuerySidePanel/ScheduleQuerySidePanel.jsx b/frontend/components/side_panels/ScheduleQuerySidePanel/ScheduleQuerySidePanel.jsx index 4dfaeca45e..421400e4bf 100644 --- a/frontend/components/side_panels/ScheduleQuerySidePanel/ScheduleQuerySidePanel.jsx +++ b/frontend/components/side_panels/ScheduleQuerySidePanel/ScheduleQuerySidePanel.jsx @@ -2,23 +2,37 @@ import React, { PropTypes } from 'react'; import ConfigurePackQueryForm from 'components/forms/ConfigurePackQueryForm'; import queryInterface from 'interfaces/query'; +import scheduledQueryInterface from 'interfaces/scheduled_query'; import SearchPackQuery from './SearchPackQuery'; import SecondarySidePanelContainer from '../SecondarySidePanelContainer'; const baseClass = 'schedule-query-side-panel'; -const ScheduleQuerySidePanel = ({ allQueries, onConfigurePackQuerySubmit, onSelectQuery, selectedQuery }) => { +const ScheduleQuerySidePanel = ({ + allQueries, + onConfigurePackQuerySubmit, + onFormCancel, + onUpdateScheduledQuery, + onSelectQuery, + selectedQuery, + selectedScheduledQuery, +}) => { const renderForm = () => { if (!selectedQuery) { return false; } - const formData = { query_id: selectedQuery.id }; + const formData = selectedScheduledQuery || {}; + + formData.query_id = selectedQuery.id; + + const handleSubmit = selectedScheduledQuery ? onUpdateScheduledQuery : onConfigurePackQuerySubmit; return ( ); }; @@ -38,8 +52,11 @@ const ScheduleQuerySidePanel = ({ allQueries, onConfigurePackQuerySubmit, onSele ScheduleQuerySidePanel.propTypes = { allQueries: PropTypes.arrayOf(queryInterface), onConfigurePackQuerySubmit: PropTypes.func, + onFormCancel: PropTypes.func, onSelectQuery: PropTypes.func, + onUpdateScheduledQuery: PropTypes.func, selectedQuery: queryInterface, + selectedScheduledQuery: scheduledQueryInterface, }; export default ScheduleQuerySidePanel; diff --git a/frontend/kolide/endpoints.js b/frontend/kolide/endpoints.js index cee0180fdd..cd48a8b5dc 100644 --- a/frontend/kolide/endpoints.js +++ b/frontend/kolide/endpoints.js @@ -21,7 +21,8 @@ export default { QUERIES: '/v1/kolide/queries', RESET_PASSWORD: '/v1/kolide/reset_password', RUN_QUERY: '/v1/kolide/queries/run', - SCHEDULED_QUERIES: (pack) => { + SCHEDULED_QUERIES: '/v1/kolide/schedule', + SCHEDULED_QUERY: (pack) => { return `/v1/kolide/packs/${pack.id}/scheduled`; }, SETUP: '/v1/setup', diff --git a/frontend/kolide/helpers.js b/frontend/kolide/helpers.js index 954b779589..aedbe65ae9 100644 --- a/frontend/kolide/helpers.js +++ b/frontend/kolide/helpers.js @@ -1,4 +1,4 @@ -import { flatMap, kebabCase, pick, size } from 'lodash'; +import { flatMap, kebabCase, omit, pick, size } from 'lodash'; import md5 from 'js-md5'; const ORG_INFO_ATTRS = ['org_name', 'org_logo_url']; @@ -69,6 +69,61 @@ const parseLicense = (license) => { return { ...license, allowed_hosts: allowedHosts }; }; +export const formatScheduledQueryForServer = (scheduledQuery) => { + const { + interval, + logging_type: loggingType, + pack_id: packID, + platform, + query_id: queryID, + shard, + } = scheduledQuery; + const result = omit(scheduledQuery, ['logging_type']); + + if (platform === 'all') { + result.platform = ''; + } + + if (interval) { + result.interval = Number(interval); + } + + if (loggingType) { + result.removed = loggingType === 'differential'; + result.snapshot = loggingType === 'snapshot'; + } + + if (packID) { + result.pack_id = Number(packID); + } + + if (queryID) { + result.query_id = Number(queryID); + } + + if (shard) { + result.shard = Number(shard); + } + + return result; +}; + +export const formatScheduledQueryForClient = (scheduledQuery) => { + if (scheduledQuery.platform === '') { + scheduledQuery.platform = 'all'; + } + + if (scheduledQuery.snapshot) { + scheduledQuery.logging_type = 'snapshot'; + } else if (scheduledQuery.removed) { + scheduledQuery.logging_type = 'differential'; + } else { + scheduledQuery.logging_type = 'differential_ignore_removals'; + } + + return scheduledQuery; +}; + const setupData = (formData) => { const orgInfo = pick(formData, ORG_INFO_ATTRS); const adminInfo = pick(formData, ADMIN_ATTRS); @@ -88,6 +143,8 @@ const setupData = (formData) => { export default { addGravatarUrlToResource, formatConfigDataForServer, + formatScheduledQueryForClient, + formatScheduledQueryForServer, formatSelectedTargetsForApi, labelSlug, parseLicense, diff --git a/frontend/kolide/helpers.tests.js b/frontend/kolide/helpers.tests.js index e30142d355..47e93ce9fa 100644 --- a/frontend/kolide/helpers.tests.js +++ b/frontend/kolide/helpers.tests.js @@ -1,7 +1,7 @@ import expect from 'expect'; import { omit } from 'lodash'; -import { configStub, licenseStub } from 'test/stubs'; +import { configStub, licenseStub, scheduledQueryStub } from 'test/stubs'; import helpers from 'kolide/helpers'; const label1 = { id: 1, target_type: 'labels' }; @@ -76,6 +76,69 @@ describe('Kolide API - helpers', () => { }); }); + describe('#formatScheduledQueryForServer', () => { + const { formatScheduledQueryForServer } = helpers; + const scheduledQuery = { + ...scheduledQueryStub, + logging_type: 'snapshot', + pack_id: '3', + platform: 'all', + query_id: '1', + shard: '12', + }; + + it('sets the correct attributes for the server', () => { + expect(formatScheduledQueryForServer(scheduledQuery)).toEqual({ + ...scheduledQueryStub, + pack_id: 3, + platform: '', + query_id: 1, + shard: 12, + snapshot: true, + removed: false, + }); + }); + }); + + describe('#formatScheduledQueryForClient', () => { + const { formatScheduledQueryForClient } = helpers; + const scheduledQuery = { + ...scheduledQueryStub, + platform: '', + snapshot: true, + }; + + it('sets the correct attributes for the server', () => { + expect(formatScheduledQueryForClient(scheduledQuery)).toEqual({ + ...scheduledQueryStub, + logging_type: 'snapshot', + platform: 'all', + }); + }); + + it('sets the logging_type attribute', () => { + expect(formatScheduledQueryForClient({ ...scheduledQueryStub, removed: true, snapshot: false })).toEqual({ + ...scheduledQueryStub, + logging_type: 'differential', + removed: true, + snapshot: false, + }); + + expect(formatScheduledQueryForClient({ ...scheduledQueryStub, snapshot: true })).toEqual({ + ...scheduledQueryStub, + logging_type: 'snapshot', + snapshot: true, + }); + + expect(formatScheduledQueryForClient({ ...scheduledQueryStub, snapshot: false, removed: false })).toEqual({ + ...scheduledQueryStub, + logging_type: 'differential_ignore_removals', + removed: false, + snapshot: false, + }); + }); + }); + describe('#formatSelectedTargetsForApi', () => { const { formatSelectedTargetsForApi } = helpers; diff --git a/frontend/kolide/index.js b/frontend/kolide/index.js index b334fc8cf1..b9b2b69c7a 100644 --- a/frontend/kolide/index.js +++ b/frontend/kolide/index.js @@ -177,6 +177,50 @@ class Kolide extends Base { }, } + scheduledQueries = { + create: (formData) => { + const { SCHEDULED_QUERIES } = endpoints; + const { interval, logging_type: loggingType, pack_id: packID, platform, query_id: queryID, shard, version } = formData; + const removed = loggingType === 'differential'; + const snapshot = loggingType === 'snapshot'; + + const params = { + interval: Number(interval), + pack_id: Number(packID), + platform, + query_id: Number(queryID), + removed, + snapshot, + shard: Number(shard), + version, + }; + + return this.authenticatedPost(this.endpoint(SCHEDULED_QUERIES), JSON.stringify(params)) + .then(response => response.scheduled); + }, + destroy: ({ id }) => { + const { SCHEDULED_QUERIES } = endpoints; + const endpoint = `${this.endpoint(SCHEDULED_QUERIES)}/${id}`; + + return this.authenticatedDelete(endpoint); + }, + loadAll: (pack) => { + const { SCHEDULED_QUERY } = endpoints; + const scheduledQueryPath = SCHEDULED_QUERY(pack); + + return this.authenticatedGet(this.endpoint(scheduledQueryPath)) + .then(response => response.scheduled); + }, + update: (scheduledQuery, updatedAttributes) => { + const { SCHEDULED_QUERIES } = endpoints; + const endpoint = this.endpoint(`${SCHEDULED_QUERIES}/${scheduledQuery.id}`); + const params = helpers.formatScheduledQueryForServer(updatedAttributes); + + return this.authenticatedPatch(endpoint, JSON.stringify(params)) + .then(response => response.scheduled); + }, + } + users = { changePassword: (passwordParams) => { const { CHANGE_PASSWORD } = endpoints; @@ -236,25 +280,6 @@ class Kolide extends Base { .then((response) => { return response.query; }); } - createScheduledQuery = ({ interval, logging_type: loggingType, pack_id: packID, platform, query_id: queryID, shard, version }) => { - const removed = loggingType === 'differential'; - const snapshot = loggingType === 'snapshot'; - - const formData = { - interval: Number(interval), - pack_id: Number(packID), - platform, - query_id: Number(queryID), - removed, - snapshot, - shard: Number(shard), - version, - }; - - return this.authenticatedPost(this.endpoint('/v1/kolide/schedule'), JSON.stringify(formData)) - .then(response => response.scheduled); - } - destroyQuery = ({ id }) => { const { QUERIES } = endpoints; const endpoint = `${this.endpoint(QUERIES)}/${id}`; @@ -269,12 +294,6 @@ class Kolide extends Base { return this.authenticatedDelete(endpoint); } - destroyScheduledQuery = ({ id }) => { - const endpoint = `${this.endpoint('/v1/kolide/schedule')}/${id}`; - - return this.authenticatedDelete(endpoint); - } - createUser = (formData) => { const { USERS } = endpoints; @@ -397,14 +416,6 @@ class Kolide extends Base { .then((response) => { return response.packs; }); } - getScheduledQueries = (pack) => { - const { SCHEDULED_QUERIES } = endpoints; - const scheduledQueryPath = SCHEDULED_QUERIES(pack); - - return this.authenticatedGet(this.endpoint(scheduledQueryPath)) - .then(response => response.scheduled); - } - getUsers = () => { const { USERS } = endpoints; diff --git a/frontend/kolide/index.tests.js b/frontend/kolide/index.tests.js index 42dbe97c72..1985ad7467 100644 --- a/frontend/kolide/index.tests.js +++ b/frontend/kolide/index.tests.js @@ -11,6 +11,7 @@ import { licenseStub, packStub, queryStub, + scheduledQueryStub, userStub, } from 'test/stubs'; @@ -56,6 +57,7 @@ const { validUpdateLabelRequest, validUpdatePackRequest, validUpdateQueryRequest, + validUpdateScheduledQueriesRequest, validUpdateUserRequest, validUser, } = mocks; @@ -455,6 +457,75 @@ describe('Kolide - API client', () => { }); }); + describe('scheduledQueries', () => { + describe('#create', () => { + it('calls the appropriate endpoint with the correct parameters', (done) => { + const formData = { + interval: 60, + logging_type: 'differential', + pack_id: 1, + platform: 'darwin', + query_id: 2, + shard: 12, + }; + const request = validCreateScheduledQueryRequest(bearerToken, formData); + + Kolide.setBearerToken(bearerToken); + Kolide.scheduledQueries.create(formData) + .then(() => { + expect(request.isDone()).toEqual(true); + done(); + }) + .catch(done); + }); + }); + + describe('#destroy', () => { + it('calls the appropriate endpoint with the correct parameters', (done) => { + const scheduledQuery = { id: 1 }; + const request = validDestroyScheduledQueryRequest(bearerToken, scheduledQuery); + + Kolide.setBearerToken(bearerToken); + Kolide.scheduledQueries.destroy(scheduledQuery) + .then(() => { + expect(request.isDone()).toEqual(true); + done(); + }) + .catch(done); + }); + }); + + describe('#loadAll', () => { + it('calls the appropriate endpoint with the correct parameters', (done) => { + const pack = { id: 1 }; + const request = validGetScheduledQueriesRequest(bearerToken, pack); + + Kolide.setBearerToken(bearerToken); + Kolide.scheduledQueries.loadAll(pack) + .then(() => { + expect(request.isDone()).toEqual(true); + done(); + }) + .catch(done); + }); + }); + + describe('#update', () => { + it('calls the appropriate endpoint with the correct parameters', (done) => { + const updatedAttrs = { interval: 200 }; + const request = validUpdateScheduledQueriesRequest(bearerToken, scheduledQueryStub, updatedAttrs); + + Kolide.setBearerToken(bearerToken); + Kolide.scheduledQueries.update(scheduledQueryStub, updatedAttrs) + .then(() => { + expect(request.isDone()).toEqual(true); + done(); + }) + .catch(done); + }); + }); + }); + describe('users', () => { describe('#changePassword', () => { it('calls the appropriate endpoint with the correct parameters', (done) => { @@ -535,58 +606,6 @@ describe('Kolide - API client', () => { }); }); - describe('#createScheduledQuery', () => { - it('calls the appropriate endpoint with the correct parameters', (done) => { - const formData = { - interval: 60, - logging_type: 'differential', - pack_id: 1, - platform: 'darwin', - query_id: 2, - shard: 12, - }; - const request = validCreateScheduledQueryRequest(bearerToken, formData); - - Kolide.setBearerToken(bearerToken); - Kolide.createScheduledQuery(formData) - .then(() => { - expect(request.isDone()).toEqual(true); - done(); - }) - .catch(done); - }); - }); - - describe('#destroyScheduledQuery', () => { - it('calls the appropriate endpoint with the correct parameters', (done) => { - const scheduledQuery = { id: 1 }; - const request = validDestroyScheduledQueryRequest(bearerToken, scheduledQuery); - - Kolide.setBearerToken(bearerToken); - Kolide.destroyScheduledQuery(scheduledQuery) - .then(() => { - expect(request.isDone()).toEqual(true); - done(); - }) - .catch(done); - }); - }); - - describe('#getScheduledQueries', () => { - it('calls the appropriate endpoint with the correct parameters', (done) => { - const pack = { id: 1 }; - const request = validGetScheduledQueriesRequest(bearerToken, pack); - - Kolide.setBearerToken(bearerToken); - Kolide.getScheduledQueries(pack) - .then(() => { - expect(request.isDone()).toEqual(true); - done(); - }) - .catch(done); - }); - }); - describe('#getTargets', () => { it('correctly parses the response', (done) => { nock.cleanAll(); diff --git a/frontend/pages/packs/EditPackPage/EditPackPage.jsx b/frontend/pages/packs/EditPackPage/EditPackPage.jsx index 21ff22e08b..67756a858c 100644 --- a/frontend/pages/packs/EditPackPage/EditPackPage.jsx +++ b/frontend/pages/packs/EditPackPage/EditPackPage.jsx @@ -43,6 +43,8 @@ export class EditPackPage extends Component { super(props); this.state = { + selectedQuery: null, + selectedScheduledQuery: null, targetsCount: 0, }; } @@ -118,14 +120,27 @@ export class EditPackPage extends Component { return false; } - onSelectQuery = (query) => { + onSelectQuery = (queryID) => { const { allQueries } = this.props; - const selectedQuery = find(allQueries, { id: Number(query) }); + const selectedQuery = find(allQueries, { id: Number(queryID) }); this.setState({ selectedQuery }); return false; } + onSelectScheduledQuery = (scheduledQuery) => { + const { selectedScheduledQuery } = this.state; + + if (isEqual(scheduledQuery, selectedScheduledQuery)) { + this.setState({ selectedScheduledQuery: null, selectedQuery: null }); + } else { + this.onSelectQuery(scheduledQuery.query_id); + this.setState({ selectedScheduledQuery: scheduledQuery }); + } + + return false; + } + onToggleEdit = () => { const { dispatch, isEdit, packID } = this.props; @@ -136,6 +151,22 @@ export class EditPackPage extends Component { return dispatch(push(`/packs/${packID}/edit`)); } + onUpdateScheduledQuery = (formData) => { + const { dispatch } = this.props; + const { selectedScheduledQuery } = this.state; + const { update } = scheduledQueryActions; + const updatedAttrs = deepDifference(formData, selectedScheduledQuery); + + dispatch(update(selectedScheduledQuery, updatedAttrs)) + .then(() => { + this.setState({ selectedScheduledQuery: null, selectedQuery: null }); + dispatch(renderFlash('success', 'Scheduled Query updated!')); + }) + .catch(() => { + dispatch(renderFlash('error', 'Unable to update your Scheduled Query.')); + }); + } + handlePackFormSubmit = (formData) => { const { dispatch, pack } = this.props; const { update } = packActions; @@ -164,12 +195,12 @@ export class EditPackPage extends Component { const { dispatch, packID } = this.props; const scheduledQueryData = { ...formData, - snapshot: formData.logging_type === 'snapshot', pack_id: packID, }; dispatch(create(scheduledQueryData)) .then(() => { + this.setState({ selectedScheduledQuery: null, selectedQuery: null }); dispatch(renderFlash('success', 'Query scheduled!')); }) .catch(() => { @@ -188,9 +219,11 @@ export class EditPackPage extends Component { onCancelEditPack, onFetchTargets, onSelectQuery, + onSelectScheduledQuery, onToggleEdit, + onUpdateScheduledQuery, } = this; - const { targetsCount, selectedQuery } = this.state; + const { targetsCount, selectedQuery, selectedScheduledQuery } = this.state; const { allQueries, isEdit, @@ -225,14 +258,18 @@ export class EditPackPage extends Component {
); diff --git a/frontend/pages/packs/EditPackPage/EditPackPage.tests.jsx b/frontend/pages/packs/EditPackPage/EditPackPage.tests.jsx index c288ada4cc..3cca3e5893 100644 --- a/frontend/pages/packs/EditPackPage/EditPackPage.tests.jsx +++ b/frontend/pages/packs/EditPackPage/EditPackPage.tests.jsx @@ -1,9 +1,10 @@ import React from 'react'; import expect, { spyOn, restoreSpies } from 'expect'; import { mount } from 'enzyme'; +import { noop } from 'lodash'; import { connectedComponent, reduxMockStore } from 'test/helpers'; -import { packStub } from 'test/stubs'; +import { packStub, queryStub, scheduledQueryStub } from 'test/stubs'; import ConnectedEditPackPage, { EditPackPage } from 'pages/packs/EditPackPage/EditPackPage'; import hostActions from 'redux/nodes/entities/hosts/actions'; import labelActions from 'redux/nodes/entities/labels/actions'; @@ -127,4 +128,47 @@ describe('EditPackPage - component', () => { expect(packActions.update).toHaveBeenCalledWith(packStub, updatedAttrs); }); }); + + describe('updating a scheduled query', () => { + const scheduledQuery = { ...scheduledQueryStub, query_id: queryStub.id }; + const defaultProps = { + allQueries: [queryStub], + dispatch: noop, + isEdit: true, + isLoadingPack: false, + isLoadingScheduledQueries: false, + pack: packStub, + packHosts: [], + packID: String(packStub.id), + packLabels: [], + scheduledQueries: [scheduledQuery], + }; + + it('de-selects the scheduledQuery when cancel is clicked', () => { + const Form = Page => Page.find('ConfigurePackQueryForm'); + const Page = mount().find('EditPackPage'); + const QueryRow = Page + .find('ScheduledQueriesList') + .find('ClickableTableRow'); + + expect(Page.node.state.selectedScheduledQuery).toNotExist(); + + QueryRow.simulate('click'); + + expect(Page.node.state.selectedScheduledQuery) + .toEqual(scheduledQuery, 'Expected clicking a scheduled query row to set the scheduled query in component state'); + + const PageForm = Form(Page); + + expect(PageForm.length) + .toEqual(1, 'Expected clicking a scheduled query row to render the ConfigurePackQueryForm component'); + + PageForm.find('.configure-pack-query-form__cancel-btn').simulate('click'); + + expect(Page.node.state.selectedScheduledQuery).toNotExist(); + + expect(Form(Page).length) + .toEqual(0, 'Expected clicking Cancel to remove the ConfigurePackQueryForm component'); + }); + }); }); diff --git a/frontend/redux/nodes/entities/scheduled_queries/config.js b/frontend/redux/nodes/entities/scheduled_queries/config.js index e669810bfa..43138a1298 100644 --- a/frontend/redux/nodes/entities/scheduled_queries/config.js +++ b/frontend/redux/nodes/entities/scheduled_queries/config.js @@ -1,3 +1,4 @@ +import helpers from 'kolide/helpers'; import Kolide from 'kolide'; import reduxConfig from 'redux/nodes/entities/base/reduxConfig'; import schemas from 'redux/nodes/entities/base/schemas'; @@ -5,10 +6,12 @@ import schemas from 'redux/nodes/entities/base/schemas'; const { SCHEDULED_QUERIES: schema } = schemas; export default reduxConfig({ - createFunc: Kolide.createScheduledQuery, - destroyFunc: Kolide.destroyScheduledQuery, + createFunc: Kolide.scheduledQueries.create, + destroyFunc: Kolide.scheduledQueries.destroy, entityName: 'scheduled_queries', - loadAllFunc: Kolide.getScheduledQueries, + loadAllFunc: Kolide.scheduledQueries.loadAll, + parseEntityFunc: helpers.formatScheduledQueryForClient, schema, + updateFunc: Kolide.scheduledQueries.update, }); diff --git a/frontend/test/mocks.js b/frontend/test/mocks.js index 886a965556..d5ac7cebaa 100644 --- a/frontend/test/mocks.js +++ b/frontend/test/mocks.js @@ -337,6 +337,21 @@ export const validGetScheduledQueriesRequest = (bearerToken, pack) => { .reply(200, { scheduled: [scheduledQueryStub] }); }; +export const validUpdateScheduledQueriesRequest = (bearerToken, scheduledQuery, updatedAttrs) => { + return nock('http://localhost:8080', { + reqHeaders: { + Authorization: `Bearer ${bearerToken}`, + }, + }) + .patch(`/api/v1/kolide/schedule/${scheduledQuery.id}`, JSON.stringify(updatedAttrs)) + .reply(200, { + scheduled: { + ...scheduledQuery, + ...updatedAttrs, + }, + }); +}; + export const validLoginRequest = (bearerToken = 'abc123') => { return nock('http://localhost:8080') .post('/api/v1/kolide/login') @@ -541,6 +556,7 @@ export default { validUpdateLabelRequest, validUpdatePackRequest, validUpdateQueryRequest, + validUpdateScheduledQueriesRequest, validUpdateUserRequest, validUser, };