Get targets from API (#459)

* API client getTargets

* change label to display_text

* filters options

* send selected targets to server

* get targets when selected targets are added or removed

* show 0 unique hosts when no targets have been selected
This commit is contained in:
Mike Stone 2016-11-09 13:08:00 -05:00 committed by GitHub
parent ac14215e21
commit 995d86e902
14 changed files with 138 additions and 108 deletions

View File

@ -33,13 +33,13 @@ class TargetInfoModal extends Component {
renderHeader = () => {
const { target } = this.props;
const { label } = target;
const { display_text: displayText } = target;
const className = headerClassName(target);
return (
<span className={`${baseClass}__header`}>
<i className={className} />
<span>{label}</span>
<span>{displayText}</span>
</span>
);
}

View File

@ -8,10 +8,10 @@ import TargetInfoModal from './TargetInfoModal';
describe('TargetInfoModal - component', () => {
const hostTarget = {
detail_updated_at: '2016-10-25T16:24:27.679472917-04:00',
display_text: 'Jason Meller\'s Windows Note',
hostname: 'Jason Meller\'s Windows Note',
id: 2,
ip: '192.168.1.11',
label: 'Jason Meller\'s Windows Note',
mac: '0C-BA-8D-45-FD-B9',
memory: 4145483776,
os_version: 'Windows Vista 0.0.1',
@ -26,8 +26,8 @@ describe('TargetInfoModal - component', () => {
const labelTarget = {
count: 38,
description: 'This group consists of machines utilized for developing within the WIN 10 environment',
display_text: 'Windows 10 Development',
hosts: [hostTarget],
label: 'Windows 10 Development',
name: 'windows10',
query: "SELECT * FROM last WHERE username = 'root' AND last.time > ((SELECT unix_time FROM time) - 3600);",
target_type: 'labels',

View File

@ -1,7 +1,7 @@
export const headerClassName = (target) => {
const { label, target_type: targetType } = target;
const { display_text: displayText, target_type: targetType } = target;
if (label.toLowerCase() === 'all hosts') {
if (displayText.toLowerCase() === 'all hosts') {
return 'kolidecon-all-hosts';
}

View File

@ -33,6 +33,10 @@ class QueryComposer extends Component {
textEditorText: PropTypes.string,
};
static defaultProps = {
selectedTargetsCount: 0,
};
constructor (props) {
super(props);

View File

@ -1,4 +1,5 @@
import React, { Component, PropTypes } from 'react';
import { difference } from 'lodash';
import Select from 'react-select';
import 'react-select/dist/react-select.css';
@ -14,6 +15,12 @@ class SelectTargetsInput extends Component {
targets: PropTypes.arrayOf(targetInterface),
};
filterOptions = (options) => {
const { selectedTargets } = this.props;
return difference(options, selectedTargets);
}
render () {
const {
isLoading,
@ -28,6 +35,8 @@ class SelectTargetsInput extends Component {
<Select
className="target-select"
isLoading={isLoading}
filterOptions={this.filterOptions}
labelKey="display_text"
menuRenderer={menuRenderer}
multi
name="targets"
@ -37,7 +46,7 @@ class SelectTargetsInput extends Component {
placeholder="Label Name, Host Name, IP Address, etc."
resetValue={[]}
value={selectedTargets}
valueKey="label"
valueKey="display_text"
/>
);
}

View File

@ -36,9 +36,9 @@ class TargetOption extends Component {
}
targetIconClass = () => {
const { label, target_type: targetType } = this.props.target;
const { display_text: displayText, target_type: targetType } = this.props.target;
if (label.toLowerCase() === 'all hosts') {
if (displayText.toLowerCase() === 'all hosts') {
return 'kolidecon-all-hosts';
}
@ -79,7 +79,7 @@ class TargetOption extends Component {
render () {
const { onMoreInfoClick, target } = this.props;
const { label, target_type: targetType } = target;
const { display_text: displayText, target_type: targetType } = target;
const {
handleSelect,
hostPlatformIconClass,
@ -96,7 +96,7 @@ class TargetOption extends Component {
<div className={wrapperClassName}>
<i className={`${targetIconClass()} ${classBlock}__target-icon`} />
{targetType === 'hosts' && <i className={`${classBlock}__icon ${hostPlatformIconClass()}`} />}
<span className={`${classBlock}__label-label`}>{label}</span>
<span className={`${classBlock}__label-label`}>{displayText}</span>
<span className={`${classBlock}__delimeter`}>&bull;</span>
{renderTargetDetail()}
<Button className={`${classBlock}__btn`} text="ADD" onClick={handleSelect} variant="brand" />

View File

@ -8,10 +8,10 @@ import TargetOption from './TargetOption';
describe('TargetOption - component', () => {
const hostTarget = {
detail_updated_at: '2016-10-25T16:24:27.679472917-04:00',
display_text: 'Jason Meller\'s Windows Note',
hostname: 'Jason Meller\'s Windows Note',
id: 2,
ip: '192.168.1.11',
label: 'Jason Meller\'s Windows Note',
mac: '0C-BA-8D-45-FD-B9',
memory: 4145483776,
os_version: 'Windows Vista 0.0.1',
@ -26,8 +26,8 @@ describe('TargetOption - component', () => {
const labelTarget = {
count: 38,
description: 'This group consists of machines utilized for developing within the WIN 10 environment',
display_text: 'Windows 10 Development',
hosts: [hostTarget],
label: 'Windows 10 Development',
name: 'windows10',
query: "SELECT * FROM last WHERE username = 'root' AND last.time > ((SELECT unix_time FROM time) - 3600);",
target_type: 'labels',

View File

@ -89,64 +89,10 @@ class Kolide extends Base {
.then((response) => { return response.query; });
}
getTargets = () => {
const stubbedResponse = {
targets: {
hosts: [
{
detail_updated_at: '2016-10-25T16:24:27.679472917-04:00',
hostname: 'jmeller-mbp.local',
id: 1,
ip: '192.168.1.10',
label: 'jmeller-mbp.local',
mac: '10:11:12:13:14:15',
memory: 4145483776,
os_version: 'Mac OS X 10.11.6',
osquery_version: '2.0.0',
platform: 'darwin',
status: 'online',
updated_at: '0001-01-01T00:00:00Z',
uptime: 3600000000000,
uuid: '1234-5678-9101',
},
{
detail_updated_at: '2016-10-25T16:24:27.679472917-04:00',
hostname: 'Jason Meller\'s Windows Note',
id: 2,
ip: '192.168.1.11',
label: 'Jason Meller\'s Windows Note',
mac: '0C-BA-8D-45-FD-B9',
memory: 4145483776,
os_version: 'Windows Vista 0.0.1',
osquery_version: '2.0.0',
platform: 'windows',
status: 'offline',
updated_at: '0001-01-01T00:00:00Z',
uptime: 3600000000000,
uuid: '1234-5678-9101',
},
],
labels: [
{
count: 1234,
id: 4,
label: 'All Hosts',
name: 'all',
},
{
count: 38,
description: 'This group consists of machines utilized for developing within the WIN 10 environment',
id: 5,
label: 'Windows 10 Development',
name: 'windows10',
query: "SELECT * FROM last WHERE username = 'root' AND last.time > ((SELECT unix_time FROM time) - 3600);",
},
],
},
selected_targets_count: 1234,
};
getTargets = (query, selected = { hosts: [], labels: [] }) => {
const { TARGETS } = endpoints;
return Promise.resolve(stubbedResponse)
return this.authenticatedPost(this.endpoint(TARGETS), JSON.stringify({ query, selected }))
.then((response) => { return appendTargetTypeToTargets(response); });
}

View File

@ -13,6 +13,7 @@ const {
validGetHostsRequest,
validGetInvitesRequest,
validGetQueryRequest,
validGetTargetsRequest,
validGetUsersRequest,
validInviteUserRequest,
validLoginRequest,
@ -115,36 +116,20 @@ describe('Kolide - API client', () => {
});
describe('#getTargets', () => {
it('correctly parses the response', () => {
Kolide.getTargets()
.then((response) => {
expect(response).toEqual({
targets: [
{
id: 3,
label: 'OS X El Capitan 10.11',
name: 'osx-10.11',
platform: 'darwin',
target_type: 'hosts',
},
{
id: 4,
label: 'Jason Meller\'s Macbook Pro',
name: 'jmeller.local',
platform: 'darwin',
target_type: 'hosts',
},
{
id: 4,
label: 'All Macs',
name: 'macs',
count: 1234,
target_type: 'labels',
},
],
selected_targets_count: 1234,
});
});
it('correctly parses the response', (done) => {
const bearerToken = 'valid-bearer-token';
const hosts = [];
const labels = [];
const query = 'mac';
const request = validGetTargetsRequest(bearerToken, query);
Kolide.setBearerToken(bearerToken);
Kolide.getTargets(query, { hosts, labels })
.then(() => {
expect(request.isDone()).toEqual(true);
done();
})
.catch(done);
});
});

View File

@ -1,5 +1,6 @@
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { flatMap, isEqual } from 'lodash';
import { push } from 'react-router-redux';
import debounce from 'utilities/debounce';
@ -12,7 +13,7 @@ import queryInterface from 'interfaces/query';
import QuerySidePanel from 'components/side_panels/QuerySidePanel';
import { removeRightSidePanel, showRightSidePanel } from 'redux/nodes/app/actions';
import { renderFlash } from 'redux/nodes/notifications/actions';
import { selectOsqueryTable, setQueryText, setSelectedTargets } from 'redux/nodes/components/QueryPages/actions';
import { selectOsqueryTable, setQueryText, setSelectedTargets, setSelectedTargetsQuery } from 'redux/nodes/components/QueryPages/actions';
import targetInterface from 'interfaces/target';
import { validateQuery } from 'pages/queries/QueryPage/helpers';
@ -23,6 +24,7 @@ class QueryPage extends Component {
queryText: PropTypes.string,
selectedOsqueryTable: osqueryTableInterface,
selectedTargets: PropTypes.arrayOf(targetInterface),
selectedTargetsQuery: PropTypes.string,
};
componentWillMount () {
@ -46,7 +48,7 @@ class QueryPage extends Component {
}
componentWillReceiveProps (nextProps) {
const { dispatch, query: newQuery } = nextProps;
const { dispatch, query: newQuery, selectedTargets, selectedTargetsQuery } = nextProps;
const { query: oldQuery } = this.props;
if ((!oldQuery && newQuery) || (oldQuery && oldQuery.query !== newQuery.query)) {
@ -55,6 +57,10 @@ class QueryPage extends Component {
dispatch(setQueryText(queryText));
}
if (!isEqual(selectedTargets, this.props.selectedTargets)) {
this.fetchTargets(selectedTargetsQuery, selectedTargets);
}
return false;
}
@ -177,10 +183,21 @@ class QueryPage extends Component {
return false;
};
fetchTargets = (search) => {
this.setState({ isLoadingTargets: true });
fetchTargets = (query, selectedTargets = this.props.selectedTargets) => {
const { dispatch } = this.props;
return Kolide.getTargets({ search })
this.setState({ isLoadingTargets: true });
dispatch(setSelectedTargetsQuery(query));
const hosts = flatMap(selectedTargets, (target) => {
return target.target_type === 'hosts' ? target.id : null;
});
const labels = flatMap(selectedTargets, (target) => {
return target.target_type === 'labels' ? target.id : null;
});
const selected = { hosts, labels };
return Kolide.getTargets(query, selected)
.then((response) => {
const {
selected_targets_count: selectedTargetsCount,
@ -193,7 +210,7 @@ class QueryPage extends Component {
targets,
});
return search;
return query;
})
.catch((error) => {
this.setState({ isLoadingTargets: false });
@ -261,9 +278,9 @@ class QueryPage extends Component {
const mapStateToProps = (state, { params }) => {
const { id: queryID } = params;
const query = entityGetter(state).get('queries').findBy({ id: queryID });
const { queryText, selectedOsqueryTable, selectedTargets } = state.components.QueryPages;
const { queryText, selectedOsqueryTable, selectedTargets, selectedTargetsQuery } = state.components.QueryPages;
return { query, queryText, selectedOsqueryTable, selectedTargets };
return { query, queryText, selectedOsqueryTable, selectedTargets, selectedTargetsQuery };
};
export default connect(mapStateToProps)(QueryPage);

View File

@ -5,6 +5,7 @@ import { osqueryTables } from '../../../../utilities/osquery_tables';
export const SELECT_OSQUERY_TABLE = 'SELECT_OSQUERY_TABLE';
export const SET_QUERY_TEXT = 'SET_QUERY_TEXT';
export const SET_SELECTED_TARGETS = 'SET_SELECTED_TARGETS';
export const SET_SELECTED_TARGETS_QUERY = 'SET_SELECTED_TARGETS_QUERY';
export const defaultSelectedOsqueryTable = find(osqueryTables, { name: 'users' });
export const selectOsqueryTable = (tableName) => {
const lowerTableName = tableName.toLowerCase();
@ -27,3 +28,9 @@ export const setSelectedTargets = (selectedTargets) => {
payload: { selectedTargets },
};
};
export const setSelectedTargetsQuery = (selectedTargetsQuery) => {
return {
type: SET_SELECTED_TARGETS_QUERY,
payload: { selectedTargetsQuery },
};
};

View File

@ -3,12 +3,14 @@ import {
SELECT_OSQUERY_TABLE,
SET_QUERY_TEXT,
SET_SELECTED_TARGETS,
SET_SELECTED_TARGETS_QUERY,
} from './actions';
export const initialState = {
queryText: 'SELECT * FROM users u JOIN groups g WHERE u.gid = g.gid',
selectedOsqueryTable: defaultSelectedOsqueryTable,
selectedTargets: [],
selectedTargetsQuery: '',
};
const reducer = (state = initialState, { type, payload }) => {
@ -28,6 +30,11 @@ const reducer = (state = initialState, { type, payload }) => {
...state,
selectedTargets: payload.selectedTargets,
};
case SET_SELECTED_TARGETS_QUERY:
return {
...state,
selectedTargetsQuery: payload.selectedTargetsQuery,
};
default:
return state;
}

View File

@ -5,6 +5,7 @@ import {
selectOsqueryTable,
setQueryText,
setSelectedTargets,
setSelectedTargetsQuery,
} from './actions';
describe('QueryPages - reducer', () => {
@ -19,6 +20,7 @@ describe('QueryPages - reducer', () => {
queryText: initialState.queryText,
selectedOsqueryTable: selectOsqueryTableAction.payload.selectedOsqueryTable,
selectedTargets: [],
selectedTargetsQuery: '',
});
});
});
@ -31,6 +33,7 @@ describe('QueryPages - reducer', () => {
queryText,
selectedOsqueryTable: initialState.selectedOsqueryTable,
selectedTargets: [],
selectedTargetsQuery: '',
});
});
});
@ -45,5 +48,15 @@ describe('QueryPages - reducer', () => {
});
});
});
context('setSelectedTargetsQuery action', () => {
it('sets the selectedTarges attribute', () => {
const setSelectedTargetsQueryAction = setSelectedTargetsQuery('192');
expect(reducer(initialState, setSelectedTargetsQueryAction)).toEqual({
...initialState,
selectedTargetsQuery: '192',
});
});
});
});

View File

@ -86,6 +86,47 @@ export const validGetHostsRequest = (bearerToken) => {
.reply(200, { hosts: [] });
};
export const validGetTargetsRequest = (bearerToken, query) => {
return nock('http://localhost:8080', {
reqHeaders: {
Authorization: `Bearer ${bearerToken}`,
},
})
.post('/api/v1/kolide/targets', {
query,
selected: {
hosts: [],
labels: [],
},
})
.reply(200, {
selected_targets_count: 1234,
targets: [
{
id: 3,
label: 'OS X El Capitan 10.11',
name: 'osx-10.11',
platform: 'darwin',
target_type: 'hosts',
},
{
id: 4,
label: 'Jason Meller\'s Macbook Pro',
name: 'jmeller.local',
platform: 'darwin',
target_type: 'hosts',
},
{
id: 4,
label: 'All Macs',
name: 'macs',
count: 1234,
target_type: 'labels',
},
],
});
};
export const validGetUsersRequest = (bearerToken) => {
return nock('http://localhost:8080', {
reqHeaders: {
@ -189,6 +230,7 @@ export default {
validGetHostsRequest,
validGetInvitesRequest,
validGetQueryRequest,
validGetTargetsRequest,
validGetUsersRequest,
validInviteUserRequest,
validLoginRequest,