diff --git a/frontend/components/hosts/HostDetails/HostDetails.jsx b/frontend/components/hosts/HostDetails/HostDetails.jsx index 4552e5bec..bb070134c 100644 --- a/frontend/components/hosts/HostDetails/HostDetails.jsx +++ b/frontend/components/hosts/HostDetails/HostDetails.jsx @@ -1,5 +1,4 @@ import React, { PropTypes } from 'react'; -import { noop } from 'lodash'; import Button from 'components/buttons/Button'; import hostInterface from 'interfaces/host'; @@ -14,7 +13,7 @@ export const STATUSES = { offline: 'OFFLINE', }; -const HostDetails = ({ host, onQueryClick = noop }) => { +const HostDetails = ({ host, onDestroyHost }) => { const { hostname, ip, @@ -28,9 +27,9 @@ const HostDetails = ({ host, onQueryClick = noop }) => { return (
- - @@ -80,7 +79,7 @@ const HostDetails = ({ host, onQueryClick = noop }) => { HostDetails.propTypes = { host: hostInterface.isRequired, - onQueryClick: PropTypes.func, + onDestroyHost: PropTypes.func.isRequired, }; export default HostDetails; diff --git a/frontend/components/hosts/HostDetails/HostDetails.tests.jsx b/frontend/components/hosts/HostDetails/HostDetails.tests.jsx new file mode 100644 index 000000000..25a0bc13d --- /dev/null +++ b/frontend/components/hosts/HostDetails/HostDetails.tests.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import expect, { createSpy, restoreSpies } from 'expect'; +import { mount } from 'enzyme'; + +import { hostStub } from 'test/stubs'; +import HostDetails from 'components/hosts/HostDetails'; + +describe('HostDetails - component', () => { + afterEach(restoreSpies); + + it('calls the onDestroyHost prop when the trash icon button is clicked', () => { + const spy = createSpy(); + const component = mount(); + const btn = component.find('Button'); + + btn.simulate('click'); + + expect(spy).toHaveBeenCalled(); + }); +}); + diff --git a/frontend/components/hosts/HostDetails/_styles.scss b/frontend/components/hosts/HostDetails/_styles.scss index a5927cfca..c3386bbf3 100644 --- a/frontend/components/hosts/HostDetails/_styles.scss +++ b/frontend/components/hosts/HostDetails/_styles.scss @@ -44,7 +44,7 @@ margin: 0; } - &__add-query { + &__delete-host { float: right; span { @@ -55,7 +55,7 @@ } } - &__add-query-icon { + &__delete-host-icon { color: $link; font-size: 20px; } diff --git a/frontend/components/hosts/HostsTable/HostsTable.jsx b/frontend/components/hosts/HostsTable/HostsTable.jsx index 8c0dda4fe..728389346 100644 --- a/frontend/components/hosts/HostsTable/HostsTable.jsx +++ b/frontend/components/hosts/HostsTable/HostsTable.jsx @@ -1,6 +1,7 @@ import React, { Component, PropTypes } from 'react'; import classnames from 'classnames'; +import Button from 'components/buttons/Button'; import Icon from 'components/icons/Icon'; import PlatformIcon from 'components/icons/PlatformIcon'; import hostInterface from 'interfaces/host'; @@ -11,9 +12,11 @@ const baseClass = 'hosts-table'; class HostsTable extends Component { static propTypes = { hosts: PropTypes.arrayOf(hostInterface), + onDestroyHost: PropTypes.func, }; renderHost = (host) => { + const { onDestroyHost } = this.props; const statusClassName = classnames(`${baseClass}__status`, `${baseClass}__status--${host.status}`); return ( @@ -24,7 +27,7 @@ class HostsTable extends Component { {host.osquery_version} {host.ip} {host.mac} - + ); } @@ -44,7 +47,7 @@ class HostsTable extends Component { Osquery IPv4 Physical Address - + diff --git a/frontend/components/hosts/HostsTable/HostsTable.tests.jsx b/frontend/components/hosts/HostsTable/HostsTable.tests.jsx index e69de29bb..affd36bd5 100644 --- a/frontend/components/hosts/HostsTable/HostsTable.tests.jsx +++ b/frontend/components/hosts/HostsTable/HostsTable.tests.jsx @@ -0,0 +1,20 @@ +import React from 'react'; +import expect, { createSpy, restoreSpies } from 'expect'; +import { mount } from 'enzyme'; + +import { hostStub } from 'test/stubs'; +import HostsTable from 'components/hosts/HostsTable'; + +describe('HostsTable - component', () => { + afterEach(restoreSpies); + + it('calls the onDestroyHost prop when the trash icon button is clicked', () => { + const spy = createSpy(); + const component = mount(); + const btn = component.find('Button'); + + btn.simulate('click'); + + expect(spy).toHaveBeenCalled(); + }); +}); diff --git a/frontend/kolide/index.js b/frontend/kolide/index.js index d4904e52e..ba5972232 100644 --- a/frontend/kolide/index.js +++ b/frontend/kolide/index.js @@ -35,6 +35,21 @@ class Kolide extends Base { }, } + hosts = { + loadAll: () => { + const { HOSTS } = endpoints; + + return this.authenticatedGet(this.endpoint(HOSTS)) + .then(response => response.hosts); + }, + destroy: (host) => { + const { HOSTS } = endpoints; + const endpoint = this.endpoint(`${HOSTS}/${host.id}`); + + return this.authenticatedDelete(endpoint); + }, + } + statusLabels = { getCounts: () => { const { STATUS_LABEL_COUNTS } = endpoints; @@ -181,13 +196,6 @@ class Kolide extends Base { }); } - getHosts = () => { - const { HOSTS } = endpoints; - - return this.authenticatedGet(this.endpoint(HOSTS)) - .then(response => response.hosts); - } - getLabelHosts = (labelID) => { const { LABEL_HOSTS } = endpoints; console.log(LABEL_HOSTS(labelID)); diff --git a/frontend/kolide/index.tests.js b/frontend/kolide/index.tests.js index 11e4ccc68..a11d222cf 100644 --- a/frontend/kolide/index.tests.js +++ b/frontend/kolide/index.tests.js @@ -14,6 +14,7 @@ const { validCreatePackRequest, validCreateQueryRequest, validCreateScheduledQueryRequest, + validDestroyHostRequest, validDestroyLabelRequest, validDestroyQueryRequest, validDestroyPackRequest, @@ -150,6 +151,38 @@ describe('Kolide - API client', () => { }); }); + describe('hosts', () => { + describe('#loadAll', () => { + it('calls the correct endpoint with the correct params', (done) => { + const request = validGetHostsRequest(bearerToken); + + Kolide.setBearerToken(bearerToken); + Kolide.hosts.loadAll() + .then(() => { + expect(request.isDone()).toEqual(true); + done(); + }) + .catch(done); + }); + }); + + describe('#destroy', () => { + it('calls the correct endpoint with the correct params', (done) => { + const request = validDestroyHostRequest(bearerToken, hostStub); + + Kolide.setBearerToken(bearerToken); + Kolide.hosts.destroy(hostStub) + .then(() => { + expect(request.isDone()).toEqual(true); + done(); + }) + .catch(() => { + throw new Error('Expected the request to be stubbed'); + }); + }); + }); + }); + describe('packs', () => { it('#createPack', (done) => { const { description, name } = packStub; @@ -343,20 +376,6 @@ describe('Kolide - API client', () => { }); }); - describe('#getHosts', () => { - it('calls the appropriate endpoint with the correct parameters', (done) => { - const request = validGetHostsRequest(bearerToken); - - Kolide.setBearerToken(bearerToken); - Kolide.getHosts() - .then(() => { - expect(request.isDone()).toEqual(true); - done(); - }) - .catch(done); - }); - }); - describe('#getInvites', () => { it('calls the appropriate endpoint with the correct parameters', (done) => { const request = validGetInvitesRequest(bearerToken); diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.jsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.jsx index 6167e8dd5..da3095809 100644 --- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.jsx +++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.jsx @@ -22,11 +22,11 @@ import osqueryTableInterface from 'interfaces/osquery_table'; import paths from 'router/paths'; import QueryForm from 'components/forms/queries/QueryForm'; import QuerySidePanel from 'components/side_panels/QuerySidePanel'; +import { renderFlash } from 'redux/nodes/notifications/actions'; import Rocker from 'components/buttons/Rocker'; import Button from 'components/buttons/Button'; import Modal from 'components/modals/Modal'; import { selectOsqueryTable } from 'redux/nodes/components/QueryPages/actions'; -import { renderFlash } from 'redux/nodes/notifications/actions'; import statusLabelsInterface from 'interfaces/status_labels'; import iconClassForLabel from 'utilities/icon_class_for_label'; import platformIconClass from 'utilities/platform_icon_class'; @@ -58,7 +58,8 @@ export class ManageHostsPage extends Component { this.state = { labelQueryText: '', - showDeleteModal: false, + selectedHost: null, + showDeleteLabelModal: false, }; } @@ -72,14 +73,6 @@ export class ManageHostsPage extends Component { return false; } - onCancelAddLabel = () => { - const { dispatch } = this.props; - - dispatch(push('/hosts/manage')); - - return false; - } - onAddLabelClick = (evt) => { evt.preventDefault(); @@ -90,15 +83,29 @@ export class ManageHostsPage extends Component { return false; } - onHostDetailActionClick = (type) => { - return (host) => { - return (evt) => { - evt.preventDefault(); + onCancelAddLabel = () => { + const { dispatch } = this.props; - console.log(type, host); - return false; - }; - }; + dispatch(push('/hosts/manage')); + + return false; + } + + onDestroyHost = (evt) => { + evt.preventDefault(); + + const { dispatch } = this.props; + const { selectedHost } = this.state; + + dispatch(hostActions.destroy(selectedHost)) + .then(() => { + this.toggleHostModal(null)(); + + dispatch(getStatusLabelCounts); + dispatch(renderFlash('success', `Host "${selectedHost.hostname}" was successfully deleted`)); + }); + + return false; } onLabelClick = (selectedLabel) => { @@ -144,23 +151,36 @@ export class ManageHostsPage extends Component { } onDeleteLabel = () => { - const { toggleModal } = this; + const { toggleLabelModal } = this; const { dispatch, selectedLabel } = this.props; const { MANAGE_HOSTS } = paths; return dispatch(labelActions.destroy(selectedLabel)) .then(() => { - toggleModal(); + toggleLabelModal(); dispatch(push(MANAGE_HOSTS)); dispatch(renderFlash('success', 'Label successfully deleted')); return false; }); } - toggleModal = () => { - const { showDeleteModal } = this.state; + toggleHostModal = (selectedHost) => { + return () => { + const { showDeleteHostModal } = this.state; - this.setState({ showDeleteModal: !showDeleteModal }); + this.setState({ + selectedHost, + showDeleteHostModal: !showDeleteHostModal, + }); + + return false; + }; + } + + toggleLabelModal = () => { + const { showDeleteLabelModal } = this.state; + + this.setState({ showDeleteLabelModal: !showDeleteLabelModal }); return false; } @@ -177,23 +197,46 @@ export class ManageHostsPage extends Component { return orderedHosts; } - renderModal = () => { - const { showDeleteModal } = this.state; - const { toggleModal, onDeleteLabel } = this; + renderHostModal = () => { + const { showDeleteHostModal } = this.state; + const { toggleHostModal, onDestroyHost } = this; - if (!showDeleteModal) { + if (!showDeleteHostModal) { + return false; + } + + return ( + +

Are you sure you wish to delete this host?

+
+ + +
+
+ ); + } + + renderLabelModal = () => { + const { showDeleteLabelModal } = this.state; + const { toggleLabelModal, onDeleteLabel } = this; + + if (!showDeleteLabelModal) { return false; } return (

Are you sure you wish to delete this label?

- +
@@ -201,7 +244,7 @@ export class ManageHostsPage extends Component { } renderDeleteButton = () => { - const { toggleModal } = this; + const { toggleLabelModal } = this; const { selectedLabel: { type } } = this.props; if (type !== 'custom') { @@ -210,7 +253,7 @@ export class ManageHostsPage extends Component { return (
- +
); } @@ -328,7 +371,7 @@ export class ManageHostsPage extends Component { renderHosts = () => { const { display, isAddLabel, selectedLabel } = this.props; - const { onHostDetailActionClick, filterHosts, sortHosts, renderNoHosts } = this; + const { toggleHostModal, filterHosts, sortHosts, renderNoHosts } = this; if (isAddLabel) { return false; @@ -351,14 +394,13 @@ export class ManageHostsPage extends Component { ); }); } - return ; + return ; } @@ -426,7 +468,7 @@ export class ManageHostsPage extends Component { } render () { - const { renderForm, renderHeader, renderHosts, renderSidePanel, renderModal } = this; + const { renderForm, renderHeader, renderHosts, renderSidePanel, renderHostModal, renderLabelModal } = this; const { display, isAddLabel } = this.props; return ( @@ -442,7 +484,8 @@ export class ManageHostsPage extends Component { } {renderSidePanel()} - {renderModal()} + {renderHostModal()} + {renderLabelModal()}
); } diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tests.jsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tests.jsx index f8b159d15..856e4eee8 100644 --- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tests.jsx +++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tests.jsx @@ -3,6 +3,7 @@ import expect, { spyOn, restoreSpies } from 'expect'; import { mount } from 'enzyme'; import { noop } from 'lodash'; +import hostActions from 'redux/nodes/entities/hosts/actions'; import labelActions from 'redux/nodes/entities/labels/actions'; import ConnectedManageHostsPage, { ManageHostsPage } from 'pages/hosts/ManageHostsPage/ManageHostsPage'; import { connectedComponent, createAceSpy, reduxMockStore, stubbedOsqueryTable } from 'test/helpers'; @@ -221,4 +222,28 @@ describe('ManageHostsPage - component', () => { expect(labelActions.destroy).toHaveBeenCalledWith(customLabel); }); }); + + describe('Delete a host', () => { + it('Deleted host after confirmation modal', () => { + const ownProps = { location: {}, params: { active_label: 'all-hosts' } }; + const component = connectedComponent(ConnectedManageHostsPage, { props: ownProps, mockStore }); + const page = mount(component); + const deleteBtn = page.find('HostDetails').first().find('Button'); + + spyOn(hostActions, 'destroy').andCallThrough(); + + expect(page.find('Modal').length).toEqual(0); + + deleteBtn.simulate('click'); + + const confirmModal = page.find('Modal'); + + expect(confirmModal.length).toEqual(1); + + const confirmBtn = confirmModal.find('.button--alert'); + confirmBtn.simulate('click'); + + expect(hostActions.destroy).toHaveBeenCalledWith(hostStub); + }); + }); }); diff --git a/frontend/pages/hosts/ManageHostsPage/_styles.scss b/frontend/pages/hosts/ManageHostsPage/_styles.scss index 5df8e4a46..cbcefd1eb 100644 --- a/frontend/pages/hosts/ManageHostsPage/_styles.scss +++ b/frontend/pages/hosts/ManageHostsPage/_styles.scss @@ -72,6 +72,12 @@ display: inline-block; } + &__modal { + .button--alert { + margin-left: 10px; + } + } + &__list { &--grid { @include display(flex); diff --git a/frontend/redux/nodes/entities/hosts/config.js b/frontend/redux/nodes/entities/hosts/config.js index d2dc12893..2714fac7a 100644 --- a/frontend/redux/nodes/entities/hosts/config.js +++ b/frontend/redux/nodes/entities/hosts/config.js @@ -5,8 +5,9 @@ import schemas from '../base/schemas'; const { HOSTS: schema } = schemas; export default reduxConfig({ + destroyFunc: Kolide.hosts.destroy, entityName: 'hosts', - loadAllFunc: Kolide.getHosts, + loadAllFunc: Kolide.hosts.loadAll, parseEntityFunc: (host) => { return { ...host, target_type: 'hosts' }; }, diff --git a/frontend/test/mocks.js b/frontend/test/mocks.js index 3279a0b11..70f5d7f65 100644 --- a/frontend/test/mocks.js +++ b/frontend/test/mocks.js @@ -74,6 +74,16 @@ export const validCreateScheduledQueryRequest = (bearerToken, formData) => { .reply(201, { scheduled_query: scheduledQueryStub }); }; +export const validDestroyHostRequest = (bearerToken, host) => { + return nock('http://localhost:8080', { + reqHeaders: { + Authorization: `Bearer ${bearerToken}`, + }, + }) + .delete(`/api/v1/kolide/hosts/${host.id}`) + .reply(200, {}); +}; + export const validDestroyLabelRequest = (bearerToken, label) => { return nock('http://localhost:8080', { reqHeaders: { @@ -439,6 +449,7 @@ export default { validCreatePackRequest, validCreateQueryRequest, validCreateScheduledQueryRequest, + validDestroyHostRequest, validDestroyLabelRequest, validDestroyQueryRequest, validDestroyPackRequest,