mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
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:
parent
ac14215e21
commit
995d86e902
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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';
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,10 @@ class QueryComposer extends Component {
|
||||
textEditorText: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
selectedTargetsCount: 0,
|
||||
};
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
|
@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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`}>•</span>
|
||||
{renderTargetDetail()}
|
||||
<Button className={`${classBlock}__btn`} text="ADD" onClick={handleSelect} variant="brand" />
|
||||
|
@ -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',
|
||||
|
@ -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); });
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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 },
|
||||
};
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user