Add delete button to custom Host Labels (#1014)

This commit is contained in:
Kyle Knight 2017-01-19 11:57:59 -06:00 committed by Jason Meller
parent 2b55cf3acf
commit 32c51d2be7
7 changed files with 142 additions and 7 deletions

View File

@ -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 }) => { createLabel = ({ description, name, query }) => {
const { LABELS } = endpoints; const { LABELS } = endpoints;

View File

@ -4,7 +4,7 @@ import nock from 'nock';
import Kolide from 'kolide'; import Kolide from 'kolide';
import helpers from 'kolide/helpers'; import helpers from 'kolide/helpers';
import mocks from 'test/mocks'; 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 { const {
invalidForgotPasswordRequest, invalidForgotPasswordRequest,
@ -13,6 +13,7 @@ const {
validCreatePackRequest, validCreatePackRequest,
validCreateQueryRequest, validCreateQueryRequest,
validCreateScheduledQueryRequest, validCreateScheduledQueryRequest,
validDestroyLabelRequest,
validDestroyQueryRequest, validDestroyQueryRequest,
validDestroyPackRequest, validDestroyPackRequest,
validDestroyScheduledQueryRequest, validDestroyScheduledQueryRequest,
@ -72,9 +73,10 @@ describe('Kolide - API client', () => {
}); });
}); });
describe('#createLabel', () => { describe('labels', () => {
it('calls the appropriate endpoint with the correct parameters', (done) => { const bearerToken = 'valid-bearer-token';
const bearerToken = 'valid-bearer-token';
it('#createLabel', (done) => {
const description = 'label description'; const description = 'label description';
const name = 'label name'; const name = 'label name';
const query = 'SELECT * FROM users'; const query = 'SELECT * FROM users';
@ -95,6 +97,20 @@ describe('Kolide - API client', () => {
}) })
.catch(done); .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', () => { describe('configOptions', () => {

View File

@ -21,7 +21,10 @@ import paths from 'router/paths';
import QueryForm from 'components/forms/queries/QueryForm'; import QueryForm from 'components/forms/queries/QueryForm';
import QuerySidePanel from 'components/side_panels/QuerySidePanel'; import QuerySidePanel from 'components/side_panels/QuerySidePanel';
import Rocker from 'components/buttons/Rocker'; 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 { selectOsqueryTable } from 'redux/nodes/components/QueryPages/actions';
import { renderFlash } from 'redux/nodes/notifications/actions';
import statusLabelsInterface from 'interfaces/status_labels'; import statusLabelsInterface from 'interfaces/status_labels';
import iconClassForLabel from 'utilities/icon_class_for_label'; import iconClassForLabel from 'utilities/icon_class_for_label';
import platformIconClass from 'utilities/platform_icon_class'; import platformIconClass from 'utilities/platform_icon_class';
@ -53,6 +56,7 @@ export class ManageHostsPage extends Component {
this.state = { this.state = {
labelQueryText: '', labelQueryText: '',
showDeleteModal: false,
}; };
} }
@ -137,6 +141,27 @@ export class ManageHostsPage extends Component {
return false; 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 = () => { filterHosts = () => {
const { hosts, selectedLabel } = this.props; const { hosts, selectedLabel } = this.props;
@ -150,6 +175,44 @@ export class ManageHostsPage extends Component {
return orderedHosts; return orderedHosts;
} }
renderModal = () => {
const { showDeleteModal } = this.state;
const { toggleModal, onDeleteLabel } = this;
if (!showDeleteModal) {
return false;
}
return (
<Modal
title="Delete Label"
onExit={toggleModal}
className={`${baseClass}__modal`}
>
<p>Are you sure you wish to delete this label?</p>
<div>
<Button onClick={toggleModal} variant="inverse">Cancel</Button>
<Button onClick={onDeleteLabel} variant="alert">Delete</Button>
</div>
</Modal>
);
}
renderDeleteButton = () => {
const { toggleModal } = this;
const { selectedLabel: { type } } = this.props;
if (type !== 'custom') {
return false;
}
return (
<div className={`${baseClass}__delete-label`}>
<Button onClick={toggleModal} variant="alert">Delete</Button>
</div>
);
}
renderIcon = () => { renderIcon = () => {
const { selectedLabel } = this.props; const { selectedLabel } = this.props;
@ -188,7 +251,7 @@ export class ManageHostsPage extends Component {
} }
renderHeader = () => { renderHeader = () => {
const { renderIcon, renderQuery } = this; const { renderIcon, renderQuery, renderDeleteButton } = this;
const { display, isAddLabel, selectedLabel, statusLabels } = this.props; const { display, isAddLabel, selectedLabel, statusLabels } = this.props;
if (!selectedLabel || isAddLabel) { if (!selectedLabel || isAddLabel) {
@ -209,6 +272,8 @@ export class ManageHostsPage extends Component {
return ( return (
<div className={`${baseClass}__header`}> <div className={`${baseClass}__header`}>
{renderDeleteButton()}
<h1 className={`${baseClass}__title`}> <h1 className={`${baseClass}__title`}>
{renderIcon()} {renderIcon()}
<span>{displayText}</span> <span>{displayText}</span>
@ -327,7 +392,7 @@ export class ManageHostsPage extends Component {
} }
render () { render () {
const { renderForm, renderHeader, renderHosts, renderSidePanel } = this; const { renderForm, renderHeader, renderHosts, renderSidePanel, renderModal } = this;
const { display, isAddLabel } = this.props; const { display, isAddLabel } = this.props;
return ( return (
@ -343,6 +408,7 @@ export class ManageHostsPage extends Component {
} }
{renderSidePanel()} {renderSidePanel()}
{renderModal()}
</div> </div>
); );
} }

View File

@ -1,8 +1,9 @@
import React from 'react'; import React from 'react';
import expect, { restoreSpies } from 'expect'; import expect, { spyOn, restoreSpies } from 'expect';
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import { noop } from 'lodash'; import { noop } from 'lodash';
import labelActions from 'redux/nodes/entities/labels/actions';
import ConnectedManageHostsPage, { ManageHostsPage } from 'pages/hosts/ManageHostsPage/ManageHostsPage'; import ConnectedManageHostsPage, { ManageHostsPage } from 'pages/hosts/ManageHostsPage/ManageHostsPage';
import { connectedComponent, createAceSpy, reduxMockStore, stubbedOsqueryTable } from 'test/helpers'; import { connectedComponent, createAceSpy, reduxMockStore, stubbedOsqueryTable } from 'test/helpers';
import { hostStub } from 'test/stubs'; 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 windowsLabel = { id: 2, display_text: 'Windows', slug: 'windows', type: 'platform', count: 22 };
const offlineHost = { ...hostStub, id: 111, status: 'offline' }; const offlineHost = { ...hostStub, id: 111, status: 'offline' };
const offlineHostsLabel = { id: 5, display_text: 'OFFLINE', slug: 'offline', status: 'offline', type: 'status', count: 1 }; 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({ const mockStore = reduxMockStore({
components: { components: {
ManageHostsPage: { ManageHostsPage: {
@ -36,6 +38,7 @@ const mockStore = reduxMockStore({
3: { id: 3, display_text: 'Ubuntu', slug: 'ubuntu', type: 'platform', count: 22 }, 3: { id: 3, display_text: 'Ubuntu', slug: 'ubuntu', type: 'platform', count: 22 },
4: { id: 4, display_text: 'ONLINE', slug: 'online', type: 'status', count: 22 }, 4: { id: 4, display_text: 'ONLINE', slug: 'online', type: 'status', count: 22 },
5: offlineHostsLabel, 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);
});
});
}); });

View File

@ -27,6 +27,11 @@
} }
} }
&__delete-label {
float: right;
margin-bottom: 15px;
}
&__description { &__description {
line-height: 1.54; line-height: 1.54;
letter-spacing: 0.5px; letter-spacing: 0.5px;

View File

@ -6,6 +6,7 @@ const { LABELS: schema } = schemas;
export default reduxConfig({ export default reduxConfig({
createFunc: Kolide.createLabel, createFunc: Kolide.createLabel,
destroyFunc: Kolide.labels.destroy,
entityName: 'labels', entityName: 'labels',
loadAllFunc: Kolide.getLabels, loadAllFunc: Kolide.getLabels,
parseEntityFunc: (label) => { parseEntityFunc: (label) => {

View File

@ -64,6 +64,16 @@ export const validCreateScheduledQueryRequest = (bearerToken, formData) => {
.reply(201, { scheduled_query: scheduledQueryStub }); .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) => { export const validDestroyQueryRequest = (bearerToken, query) => {
return nock('http://localhost:8080', { return nock('http://localhost:8080', {
reqHeaders: { reqHeaders: {
@ -398,6 +408,7 @@ export default {
validCreatePackRequest, validCreatePackRequest,
validCreateQueryRequest, validCreateQueryRequest,
validCreateScheduledQueryRequest, validCreateScheduledQueryRequest,
validDestroyLabelRequest,
validDestroyQueryRequest, validDestroyQueryRequest,
validDestroyPackRequest, validDestroyPackRequest,
validDestroyScheduledQueryRequest, validDestroyScheduledQueryRequest,