mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 17:05:18 +00:00
Allow users to delete hosts (#1028)
This commit is contained in:
parent
28130b529b
commit
3c6b59197d
@ -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 (
|
||||
<div className={`${baseClass} ${baseClass}--${status}`}>
|
||||
<span className={`${baseClass}__add-query`}>
|
||||
<Button onClick={onQueryClick(host)} variant="unstyled" title="Query this host">
|
||||
<Icon name="query" className={`${baseClass}__add-query-icon`} key="add-query" />
|
||||
<span className={`${baseClass}__delete-host`}>
|
||||
<Button onClick={onDestroyHost(host)} variant="unstyled" title="Delete this host">
|
||||
<Icon name="trash" className={`${baseClass}__delete-host-icon`} />
|
||||
</Button>
|
||||
</span>
|
||||
|
||||
@ -80,7 +79,7 @@ const HostDetails = ({ host, onQueryClick = noop }) => {
|
||||
|
||||
HostDetails.propTypes = {
|
||||
host: hostInterface.isRequired,
|
||||
onQueryClick: PropTypes.func,
|
||||
onDestroyHost: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default HostDetails;
|
||||
|
21
frontend/components/hosts/HostDetails/HostDetails.tests.jsx
Normal file
21
frontend/components/hosts/HostDetails/HostDetails.tests.jsx
Normal file
@ -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(<HostDetails host={hostStub} onDestroyHost={spy} />);
|
||||
const btn = component.find('Button');
|
||||
|
||||
btn.simulate('click');
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 {
|
||||
<td>{host.osquery_version}</td>
|
||||
<td>{host.ip}</td>
|
||||
<td>{host.mac}</td>
|
||||
<td><a href="#add-query"><Icon name="add-plus" /></a></td>
|
||||
<td><Button onClick={onDestroyHost(host)} variant="unstyled"><Icon name="trash" /></Button></td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
@ -44,7 +47,7 @@ class HostsTable extends Component {
|
||||
<th>Osquery</th>
|
||||
<th>IPv4</th>
|
||||
<th>Physical Address</th>
|
||||
<th><Icon name="query" /></th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -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(<HostsTable hosts={[hostStub]} onDestroyHost={spy} />);
|
||||
const btn = component.find('Button');
|
||||
|
||||
btn.simulate('click');
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
@ -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));
|
||||
|
@ -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);
|
||||
|
@ -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 (
|
||||
<Modal
|
||||
title="Delete Host"
|
||||
onExit={toggleHostModal(null)}
|
||||
className={`${baseClass}__modal`}
|
||||
>
|
||||
<p>Are you sure you wish to delete this host?</p>
|
||||
<div>
|
||||
<Button onClick={toggleHostModal(null)} variant="inverse">Cancel</Button>
|
||||
<Button onClick={onDestroyHost} variant="alert">Delete</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
renderLabelModal = () => {
|
||||
const { showDeleteLabelModal } = this.state;
|
||||
const { toggleLabelModal, onDeleteLabel } = this;
|
||||
|
||||
if (!showDeleteLabelModal) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Delete Label"
|
||||
onExit={toggleModal}
|
||||
onExit={toggleLabelModal}
|
||||
className={`${baseClass}__modal`}
|
||||
>
|
||||
<p>Are you sure you wish to delete this label?</p>
|
||||
<div>
|
||||
<Button onClick={toggleModal} variant="inverse">Cancel</Button>
|
||||
<Button onClick={toggleLabelModal} variant="inverse">Cancel</Button>
|
||||
<Button onClick={onDeleteLabel} variant="alert">Delete</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
@ -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 (
|
||||
<div className={`${baseClass}__delete-label`}>
|
||||
<Button onClick={toggleModal} variant="alert">Delete</Button>
|
||||
<Button onClick={toggleLabelModal} variant="alert">Delete</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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 {
|
||||
<HostDetails
|
||||
host={host}
|
||||
key={`host-${host.id}-details`}
|
||||
onDisableClick={onHostDetailActionClick('disable')}
|
||||
onQueryClick={onHostDetailActionClick('query')}
|
||||
onDestroyHost={toggleHostModal}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return <HostsTable hosts={sortedHosts} />;
|
||||
return <HostsTable hosts={sortedHosts} onDestroyHost={toggleHostModal} />;
|
||||
}
|
||||
|
||||
|
||||
@ -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()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -72,6 +72,12 @@
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&__modal {
|
||||
.button--alert {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__list {
|
||||
&--grid {
|
||||
@include display(flex);
|
||||
|
@ -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' };
|
||||
},
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user