diff --git a/frontend/kolide/index.js b/frontend/kolide/index.js
index a48d3f4979..ec28c4cdbb 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 0ae05d87f2..2ca85df298 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 b3a37c4776..545006ee11 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 e037afe2c8..664223b642 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 2299766fec..fd496c2cfd 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 82ba351289..78fe22c65f 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 111566c817..9b5f6c371c 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,