From 32c51d2be7f15900e8b782e4bd117da93b51bb2b Mon Sep 17 00:00:00 2001 From: Kyle Knight Date: Thu, 19 Jan 2017 11:57:59 -0600 Subject: [PATCH] Add delete button to custom Host Labels (#1014) --- frontend/kolide/index.js | 9 +++ frontend/kolide/index.tests.js | 24 +++++-- .../hosts/ManageHostsPage/ManageHostsPage.jsx | 70 ++++++++++++++++++- .../ManageHostsPage/ManageHostsPage.tests.jsx | 29 +++++++- .../pages/hosts/ManageHostsPage/_styles.scss | 5 ++ .../redux/nodes/entities/labels/config.js | 1 + frontend/test/mocks.js | 11 +++ 7 files changed, 142 insertions(+), 7 deletions(-) diff --git a/frontend/kolide/index.js b/frontend/kolide/index.js index a48d3f497..ec28c4cdb 100644 --- a/frontend/kolide/index.js +++ b/frontend/kolide/index.js @@ -43,6 +43,15 @@ class Kolide extends Base { }, } + labels = { + destroy: (label) => { + const { LABELS } = endpoints; + const endpoint = this.endpoint(`${LABELS}/${label.id}`); + + return this.authenticatedDelete(endpoint); + }, + }; + createLabel = ({ description, name, query }) => { const { LABELS } = endpoints; diff --git a/frontend/kolide/index.tests.js b/frontend/kolide/index.tests.js index 0ae05d87f..2ca85df29 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 { configOptionStub, hostStub, packStub, queryStub, userStub } from 'test/stubs'; +import { configOptionStub, hostStub, packStub, queryStub, userStub, labelStub } from 'test/stubs'; const { invalidForgotPasswordRequest, @@ -13,6 +13,7 @@ const { validCreatePackRequest, validCreateQueryRequest, validCreateScheduledQueryRequest, + validDestroyLabelRequest, validDestroyQueryRequest, validDestroyPackRequest, validDestroyScheduledQueryRequest, @@ -72,9 +73,10 @@ describe('Kolide - API client', () => { }); }); - describe('#createLabel', () => { - it('calls the appropriate endpoint with the correct parameters', (done) => { - const bearerToken = 'valid-bearer-token'; + describe('labels', () => { + const bearerToken = 'valid-bearer-token'; + + it('#createLabel', (done) => { const description = 'label description'; const name = 'label name'; const query = 'SELECT * FROM users'; @@ -95,6 +97,20 @@ describe('Kolide - API client', () => { }) .catch(done); }); + + it('#destroyLabel', (done) => { + const request = validDestroyLabelRequest(bearerToken, labelStub); + + Kolide.setBearerToken(bearerToken); + Kolide.labels.destroy(labelStub) + .then(() => { + expect(request.isDone()).toEqual(true); + done(); + }) + .catch(() => { + throw new Error('Request should have been stubbed'); + }); + }); }); describe('configOptions', () => { diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.jsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.jsx index b3a37c477..545006ee1 100644 --- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.jsx +++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.jsx @@ -21,7 +21,10 @@ import paths from 'router/paths'; import QueryForm from 'components/forms/queries/QueryForm'; import QuerySidePanel from 'components/side_panels/QuerySidePanel'; 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'; @@ -53,6 +56,7 @@ export class ManageHostsPage extends Component { this.state = { labelQueryText: '', + showDeleteModal: false, }; } @@ -137,6 +141,27 @@ export class ManageHostsPage extends Component { return false; } + onDeleteLabel = () => { + const { toggleModal } = this; + const { dispatch, selectedLabel } = this.props; + const { MANAGE_HOSTS } = paths; + + return dispatch(labelActions.destroy(selectedLabel)) + .then(() => { + toggleModal(); + dispatch(push(MANAGE_HOSTS)); + dispatch(renderFlash('success', 'Label successfully deleted')); + return false; + }); + } + + toggleModal = () => { + const { showDeleteModal } = this.state; + + this.setState({ showDeleteModal: !showDeleteModal }); + return false; + } + filterHosts = () => { const { hosts, selectedLabel } = this.props; @@ -150,6 +175,44 @@ export class ManageHostsPage extends Component { return orderedHosts; } + renderModal = () => { + const { showDeleteModal } = this.state; + const { toggleModal, onDeleteLabel } = this; + + if (!showDeleteModal) { + return false; + } + + return ( + +

Are you sure you wish to delete this label?

+
+ + +
+
+ ); + } + + renderDeleteButton = () => { + const { toggleModal } = this; + const { selectedLabel: { type } } = this.props; + + if (type !== 'custom') { + return false; + } + + return ( +
+ +
+ ); + } + renderIcon = () => { const { selectedLabel } = this.props; @@ -188,7 +251,7 @@ export class ManageHostsPage extends Component { } renderHeader = () => { - const { renderIcon, renderQuery } = this; + const { renderIcon, renderQuery, renderDeleteButton } = this; const { display, isAddLabel, selectedLabel, statusLabels } = this.props; if (!selectedLabel || isAddLabel) { @@ -209,6 +272,8 @@ export class ManageHostsPage extends Component { return (
+ {renderDeleteButton()} +

{renderIcon()} {displayText} @@ -327,7 +392,7 @@ export class ManageHostsPage extends Component { } render () { - const { renderForm, renderHeader, renderHosts, renderSidePanel } = this; + const { renderForm, renderHeader, renderHosts, renderSidePanel, renderModal } = this; const { display, isAddLabel } = this.props; return ( @@ -343,6 +408,7 @@ export class ManageHostsPage extends Component { } {renderSidePanel()} + {renderModal()}

); } diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tests.jsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tests.jsx index e037afe2c..664223b64 100644 --- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tests.jsx +++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tests.jsx @@ -1,8 +1,9 @@ import React from 'react'; -import expect, { restoreSpies } from 'expect'; +import expect, { spyOn, restoreSpies } from 'expect'; import { mount } from 'enzyme'; import { noop } from 'lodash'; +import labelActions from 'redux/nodes/entities/labels/actions'; import ConnectedManageHostsPage, { ManageHostsPage } from 'pages/hosts/ManageHostsPage/ManageHostsPage'; import { connectedComponent, createAceSpy, reduxMockStore, stubbedOsqueryTable } from 'test/helpers'; import { hostStub } from 'test/stubs'; @@ -11,6 +12,7 @@ const allHostsLabel = { id: 1, display_text: 'All Hosts', slug: 'all-hosts', typ const windowsLabel = { id: 2, display_text: 'Windows', slug: 'windows', type: 'platform', count: 22 }; const offlineHost = { ...hostStub, id: 111, status: 'offline' }; const offlineHostsLabel = { id: 5, display_text: 'OFFLINE', slug: 'offline', status: 'offline', type: 'status', count: 1 }; +const customLabel = { id: 6, display_text: 'Custom Label', slug: 'custom-label', type: 'custom', count: 3 }; const mockStore = reduxMockStore({ components: { ManageHostsPage: { @@ -36,6 +38,7 @@ const mockStore = reduxMockStore({ 3: { id: 3, display_text: 'Ubuntu', slug: 'ubuntu', type: 'platform', count: 22 }, 4: { id: 4, display_text: 'ONLINE', slug: 'online', type: 'status', count: 22 }, 5: offlineHostsLabel, + 6: customLabel, }, }, }, @@ -182,4 +185,28 @@ describe('ManageHostsPage - component', () => { }); }); }); + + describe('Delete a label', () => { + it('Deleted label after confirmation modal', () => { + const ownProps = { location: {}, params: { active_label: 'custom-label' } }; + const component = connectedComponent(ConnectedManageHostsPage, { props: ownProps, mockStore }); + const page = mount(component); + const deleteBtn = page.find('.manage-hosts__delete-label').find('button'); + + spyOn(labelActions, '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(labelActions.destroy).toHaveBeenCalledWith(customLabel); + }); + }); }); diff --git a/frontend/pages/hosts/ManageHostsPage/_styles.scss b/frontend/pages/hosts/ManageHostsPage/_styles.scss index 2299766fe..fd496c2cf 100644 --- a/frontend/pages/hosts/ManageHostsPage/_styles.scss +++ b/frontend/pages/hosts/ManageHostsPage/_styles.scss @@ -27,6 +27,11 @@ } } + &__delete-label { + float: right; + margin-bottom: 15px; + } + &__description { line-height: 1.54; letter-spacing: 0.5px; diff --git a/frontend/redux/nodes/entities/labels/config.js b/frontend/redux/nodes/entities/labels/config.js index 82ba35128..78fe22c65 100644 --- a/frontend/redux/nodes/entities/labels/config.js +++ b/frontend/redux/nodes/entities/labels/config.js @@ -6,6 +6,7 @@ const { LABELS: schema } = schemas; export default reduxConfig({ createFunc: Kolide.createLabel, + destroyFunc: Kolide.labels.destroy, entityName: 'labels', loadAllFunc: Kolide.getLabels, parseEntityFunc: (label) => { diff --git a/frontend/test/mocks.js b/frontend/test/mocks.js index 111566c81..9b5f6c371 100644 --- a/frontend/test/mocks.js +++ b/frontend/test/mocks.js @@ -64,6 +64,16 @@ export const validCreateScheduledQueryRequest = (bearerToken, formData) => { .reply(201, { scheduled_query: scheduledQueryStub }); }; +export const validDestroyLabelRequest = (bearerToken, label) => { + return nock('http://localhost:8080', { + reqHeaders: { + Authorization: `Bearer ${bearerToken}`, + }, + }) + .delete(`/api/v1/kolide/labels/${label.id}`) + .reply(200, {}); +}; + export const validDestroyQueryRequest = (bearerToken, query) => { return nock('http://localhost:8080', { reqHeaders: { @@ -398,6 +408,7 @@ export default { validCreatePackRequest, validCreateQueryRequest, validCreateScheduledQueryRequest, + validDestroyLabelRequest, validDestroyQueryRequest, validDestroyPackRequest, validDestroyScheduledQueryRequest,