mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 17:05:18 +00:00
Allow users to export query results (#1082)
This commit is contained in:
parent
ac95b764eb
commit
8432d0494f
@ -15,13 +15,13 @@ class QueryResultsRow extends Component {
|
||||
|
||||
render () {
|
||||
const { index, queryResult } = this.props;
|
||||
const { hostname } = queryResult;
|
||||
const queryAttrs = omit(queryResult, ['hostname']);
|
||||
const { host_hostname: hostHostname } = queryResult;
|
||||
const queryAttrs = omit(queryResult, ['host_hostname']);
|
||||
const queryAttrValues = values(queryAttrs);
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>{hostname}</td>
|
||||
<td>{hostHostname}</td>
|
||||
{queryAttrValues.map((attribute, i) => {
|
||||
return <td key={`query-results-table-row-${index}-${i}`}>{attribute}</td>;
|
||||
})}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { get, keys, omit } from 'lodash';
|
||||
|
||||
import Button from 'components/buttons/Button';
|
||||
import campaignInterface from 'interfaces/campaign';
|
||||
import filterArrayByHash from 'utilities/filter_array_by_hash';
|
||||
import Icon from 'components/icons/Icon';
|
||||
@ -14,6 +15,7 @@ const baseClass = 'query-results-table';
|
||||
class QueryResultsTable extends Component {
|
||||
static propTypes = {
|
||||
campaign: campaignInterface.isRequired,
|
||||
onExportQueryResults: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor (props) {
|
||||
@ -117,7 +119,7 @@ class QueryResultsTable extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { campaign } = this.props;
|
||||
const { campaign, onExportQueryResults } = this.props;
|
||||
const {
|
||||
renderProgressDetails,
|
||||
renderTableHeaderRow,
|
||||
@ -131,6 +133,13 @@ class QueryResultsTable extends Component {
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<Button
|
||||
className={`${baseClass}__export-btn`}
|
||||
onClick={onExportQueryResults}
|
||||
variant="brand"
|
||||
>
|
||||
Export
|
||||
</Button>
|
||||
{renderProgressDetails()}
|
||||
<div className={`${baseClass}__table-wrapper`}>
|
||||
<table className={`${baseClass}__table`}>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import expect from 'expect';
|
||||
import expect, { createSpy, restoreSpies } from 'expect';
|
||||
import { keys } from 'lodash';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
@ -48,6 +48,8 @@ const campaignWithQueryResults = {
|
||||
};
|
||||
|
||||
describe('QueryResultsTable - component', () => {
|
||||
afterEach(restoreSpies);
|
||||
|
||||
const componentWithoutQueryResults = mount(
|
||||
<QueryResultsTable campaign={campaignWithNoQueryResults} />
|
||||
);
|
||||
@ -78,4 +80,17 @@ describe('QueryResultsTable - component', () => {
|
||||
expect(tableHeaderText).toInclude(key);
|
||||
});
|
||||
});
|
||||
|
||||
it('calls the onExportQueryResults prop when the export button is clicked', () => {
|
||||
const spy = createSpy();
|
||||
const component = mount(<QueryResultsTable campaign={campaignWithQueryResults} onExportQueryResults={spy} />);
|
||||
|
||||
const exportBtn = component.find('Button');
|
||||
|
||||
expect(spy).toNotHaveBeenCalled();
|
||||
|
||||
exportBtn.simulate('click');
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
@ -4,6 +4,10 @@
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
&__export-btn {
|
||||
float: right;
|
||||
}
|
||||
|
||||
&__filter-icon {
|
||||
&--is-active {
|
||||
color: $brand;
|
||||
@ -20,6 +24,7 @@
|
||||
border-radius: 3px;
|
||||
box-shadow: inset 0 0 8px 0 rgba(0, 0, 0, 0.12);
|
||||
overflow: scroll;
|
||||
margin-top: 20px;
|
||||
max-height: 550px;
|
||||
}
|
||||
|
||||
|
@ -63,7 +63,8 @@ class Kolide extends Base {
|
||||
getCounts: () => {
|
||||
const { STATUS_LABEL_COUNTS } = endpoints;
|
||||
|
||||
return this.authenticatedGet(this.endpoint(STATUS_LABEL_COUNTS));
|
||||
return this.authenticatedGet(this.endpoint(STATUS_LABEL_COUNTS))
|
||||
.catch(() => false);
|
||||
},
|
||||
}
|
||||
|
||||
@ -76,6 +77,15 @@ class Kolide extends Base {
|
||||
},
|
||||
}
|
||||
|
||||
queries = {
|
||||
run: ({ query, selected }) => {
|
||||
const { RUN_QUERY } = endpoints;
|
||||
|
||||
return this.authenticatedPost(this.endpoint(RUN_QUERY), JSON.stringify({ query, selected }))
|
||||
.then(response => response.campaign);
|
||||
},
|
||||
}
|
||||
|
||||
users = {
|
||||
changePassword: (passwordParams) => {
|
||||
const { CHANGE_PASSWORD } = endpoints;
|
||||
@ -104,6 +114,22 @@ class Kolide extends Base {
|
||||
},
|
||||
}
|
||||
|
||||
websockets = {
|
||||
queries: {
|
||||
run: (campaignID) => {
|
||||
return new Promise((resolve) => {
|
||||
const socket = new global.WebSocket(`${this.websocketBaseURL}/v1/kolide/results/${campaignID}`);
|
||||
|
||||
socket.onopen = () => {
|
||||
socket.send(JSON.stringify({ type: 'auth', data: { token: local.getItem('auth_token') } }));
|
||||
};
|
||||
|
||||
return resolve(socket);
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
createLabel = ({ description, name, query }) => {
|
||||
const { LABELS } = endpoints;
|
||||
|
||||
@ -405,25 +431,6 @@ class Kolide extends Base {
|
||||
return this.authenticatedDelete(endpoint);
|
||||
}
|
||||
|
||||
runQuery = ({ query, selected }) => {
|
||||
const { RUN_QUERY } = endpoints;
|
||||
|
||||
return this.authenticatedPost(this.endpoint(RUN_QUERY), JSON.stringify({ query, selected }))
|
||||
.then(response => response.campaign);
|
||||
}
|
||||
|
||||
runQueryWebsocket = (campaignID) => {
|
||||
return new Promise((resolve) => {
|
||||
const socket = new global.WebSocket(`${this.websocketBaseURL}/v1/kolide/results/${campaignID}`);
|
||||
|
||||
socket.onopen = () => {
|
||||
socket.send(JSON.stringify({ type: 'auth', data: { token: local.getItem('auth_token') } }));
|
||||
};
|
||||
|
||||
return resolve(socket);
|
||||
});
|
||||
}
|
||||
|
||||
setup = (formData) => {
|
||||
const { SETUP } = endpoints;
|
||||
const setupData = helpers.setupData(formData);
|
||||
|
@ -296,17 +296,19 @@ describe('Kolide - API client', () => {
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('#runQuery', (done) => {
|
||||
const data = { query: 'select * from users', selected: { hosts: [], labels: [] } };
|
||||
const request = validRunQueryRequest(bearerToken, data);
|
||||
describe('#run', () => {
|
||||
it('calls the correct endpoint with the correct params', (done) => {
|
||||
const data = { query: 'select * from users', selected: { hosts: [], labels: [] } };
|
||||
const request = validRunQueryRequest(bearerToken, data);
|
||||
|
||||
Kolide.setBearerToken(bearerToken);
|
||||
Kolide.runQuery(data)
|
||||
.then(() => {
|
||||
expect(request.isDone()).toEqual(true);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
Kolide.setBearerToken(bearerToken);
|
||||
Kolide.queries.run(data)
|
||||
.then(() => {
|
||||
expect(request.isDone()).toEqual(true);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,12 +1,14 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { push } from 'react-router-redux';
|
||||
import { first, filter, includes, isArray, isEqual, values } from 'lodash';
|
||||
import classnames from 'classnames';
|
||||
import { connect } from 'react-redux';
|
||||
import FileSaver from 'file-saver';
|
||||
import { filter, includes, isArray, isEqual, size } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { push } from 'react-router-redux';
|
||||
|
||||
import Kolide from 'kolide';
|
||||
import campaignActions from 'redux/nodes/entities/campaigns/actions';
|
||||
import campaignInterface from 'interfaces/campaign';
|
||||
import campaignHelpers from 'redux/nodes/entities/campaigns/helpers';
|
||||
import convertToCSV from 'utilities/convert_to_csv';
|
||||
import debounce from 'utilities/debounce';
|
||||
import deepDifference from 'utilities/deep_difference';
|
||||
import entityGetter from 'redux/utilities/entityGetter';
|
||||
@ -26,9 +28,8 @@ import Spinner from 'components/loaders/Spinner';
|
||||
|
||||
const baseClass = 'query-page';
|
||||
|
||||
class QueryPage extends Component {
|
||||
export class QueryPage extends Component {
|
||||
static propTypes = {
|
||||
campaign: campaignInterface,
|
||||
dispatch: PropTypes.func,
|
||||
errors: PropTypes.shape({
|
||||
base: PropTypes.string,
|
||||
@ -43,10 +44,13 @@ class QueryPage extends Component {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
campaign: {},
|
||||
queryIsRunning: false,
|
||||
targetsCount: 0,
|
||||
targetsError: null,
|
||||
};
|
||||
|
||||
this.csvQueryName = 'Query Results';
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
@ -68,6 +72,38 @@ class QueryPage extends Component {
|
||||
return false;
|
||||
}
|
||||
|
||||
onChangeQueryFormField = (fieldName, value) => {
|
||||
if (fieldName === 'name') {
|
||||
this.csvQueryName = value;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onExportQueryResults = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
const { campaign } = this.state;
|
||||
const { query_results: queryResults } = campaign;
|
||||
|
||||
if (queryResults) {
|
||||
const csv = convertToCSV(queryResults, (fields) => {
|
||||
const result = filter(fields, f => f !== 'host_hostname');
|
||||
|
||||
result.unshift('host_hostname');
|
||||
|
||||
return result;
|
||||
});
|
||||
const formattedTime = moment(new Date()).format('MM-DD-YY hh-mm-ss');
|
||||
const filename = `${this.csvQueryName} (${formattedTime}).csv`;
|
||||
const file = new global.window.File([csv], filename, { type: 'text/csv' });
|
||||
|
||||
FileSaver.saveAs(file);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onFetchTargets = (query, targetResponse) => {
|
||||
const { dispatch } = this.props;
|
||||
const {
|
||||
@ -104,18 +140,17 @@ class QueryPage extends Component {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { create, update } = campaignActions;
|
||||
const { destroyCampaign, removeSocket } = this;
|
||||
const selected = formatSelectedTargetsForApi(selectedTargets);
|
||||
|
||||
removeSocket();
|
||||
destroyCampaign();
|
||||
|
||||
dispatch(create({ query: queryText, selected }))
|
||||
Kolide.queries.run({ query: queryText, selected })
|
||||
.then((campaignResponse) => {
|
||||
return Kolide.runQueryWebsocket(campaignResponse.id)
|
||||
return Kolide.websockets.queries.run(campaignResponse.id)
|
||||
.then((socket) => {
|
||||
this.campaign = campaignResponse;
|
||||
this.setState({ campaign: campaignResponse });
|
||||
this.socket = socket;
|
||||
this.setState({ queryIsRunning: true });
|
||||
|
||||
@ -129,10 +164,21 @@ class QueryPage extends Component {
|
||||
return false;
|
||||
}
|
||||
|
||||
return dispatch(update(this.campaign, socketData))
|
||||
return campaignHelpers.update(this.state.campaign, socketData)
|
||||
.then((updatedCampaign) => {
|
||||
const { status } = updatedCampaign;
|
||||
|
||||
if (status === 'finished') {
|
||||
this.setState({ queryIsRunning: false });
|
||||
removeSocket();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
this.previousSocketData = socketData;
|
||||
this.campaign = updatedCampaign;
|
||||
this.setState({ campaign: updatedCampaign });
|
||||
|
||||
return false;
|
||||
});
|
||||
};
|
||||
});
|
||||
@ -203,12 +249,11 @@ class QueryPage extends Component {
|
||||
};
|
||||
|
||||
destroyCampaign = () => {
|
||||
const { campaign, dispatch } = this.props;
|
||||
const { destroy } = campaignActions;
|
||||
const { campaign } = this.state;
|
||||
|
||||
if (campaign) {
|
||||
if (this.campaign || campaign) {
|
||||
this.campaign = null;
|
||||
dispatch(destroy(campaign));
|
||||
this.setState({ campaign: {} });
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -225,20 +270,21 @@ class QueryPage extends Component {
|
||||
}
|
||||
|
||||
renderResultsTable = () => {
|
||||
const { campaign } = this.props;
|
||||
const { campaign } = this.state;
|
||||
const { onExportQueryResults } = this;
|
||||
const resultsClasses = classnames(`${baseClass}__results`, 'body-wrap', {
|
||||
[`${baseClass}__results--loading`]: this.socket && !campaign.query_results,
|
||||
});
|
||||
let resultBody = '';
|
||||
|
||||
if (!campaign || (!this.socket && !campaign.query_results)) {
|
||||
if (!size(campaign) || (!this.socket && !campaign.query_results)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((!campaign.query_results || campaign.query_results.length < 1) && this.socket) {
|
||||
resultBody = <Spinner />;
|
||||
} else {
|
||||
resultBody = <QueryResultsTable campaign={campaign} />;
|
||||
resultBody = <QueryResultsTable campaign={campaign} onExportQueryResults={onExportQueryResults} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -250,6 +296,7 @@ class QueryPage extends Component {
|
||||
|
||||
render () {
|
||||
const {
|
||||
onChangeQueryFormField,
|
||||
onFetchTargets,
|
||||
onOsqueryTableSelect,
|
||||
onRunQuery,
|
||||
@ -275,6 +322,7 @@ class QueryPage extends Component {
|
||||
<QueryForm
|
||||
formData={query}
|
||||
handleSubmit={onSaveQueryFormSubmit}
|
||||
onChangeFunc={onChangeQueryFormField}
|
||||
onFetchTargets={onFetchTargets}
|
||||
onOsqueryTableSelect={onOsqueryTableSelect}
|
||||
onRunQuery={onRunQuery}
|
||||
@ -304,10 +352,8 @@ class QueryPage extends Component {
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const stateEntities = entityGetter(state);
|
||||
const { id: queryID } = ownProps.params;
|
||||
const { entities: campaigns } = stateEntities.get('campaigns');
|
||||
const reduxQuery = entityGetter(state).get('queries').findBy({ id: queryID });
|
||||
const { queryText, selectedOsqueryTable } = state.components.QueryPages;
|
||||
const campaign = first(values(campaigns));
|
||||
const { errors } = state.entities.queries;
|
||||
const queryStub = { description: '', name: '', query: queryText };
|
||||
const query = reduxQuery || queryStub;
|
||||
@ -329,7 +375,6 @@ const mapStateToProps = (state, ownProps) => {
|
||||
}
|
||||
|
||||
return {
|
||||
campaign,
|
||||
errors,
|
||||
hostIDs,
|
||||
query,
|
||||
|
@ -1,11 +1,15 @@
|
||||
import React from 'react';
|
||||
import expect, { spyOn, restoreSpies } from 'expect';
|
||||
import FileSave from 'file-saver';
|
||||
import { mount } from 'enzyme';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
import convertToCSV from 'utilities/convert_to_csv';
|
||||
import { defaultSelectedOsqueryTable } from 'redux/nodes/components/QueryPages/actions';
|
||||
import helpers from 'test/helpers';
|
||||
import kolide from 'kolide';
|
||||
import queryActions from 'redux/nodes/entities/queries/actions';
|
||||
import QueryPage from 'pages/queries/QueryPage';
|
||||
import ConnectedQueryPage, { QueryPage } from 'pages/queries/QueryPage/QueryPage';
|
||||
import { validUpdateQueryRequest } from 'test/mocks';
|
||||
import { hostStub } from 'test/stubs';
|
||||
|
||||
@ -37,13 +41,13 @@ describe('QueryPage - component', () => {
|
||||
});
|
||||
|
||||
it('renders the QueryForm component', () => {
|
||||
const page = mount(connectedComponent(QueryPage, { mockStore, props: locationProp }));
|
||||
const page = mount(connectedComponent(ConnectedQueryPage, { mockStore, props: locationProp }));
|
||||
|
||||
expect(page.find('QueryForm').length).toEqual(1);
|
||||
});
|
||||
|
||||
it('renders the QuerySidePanel component', () => {
|
||||
const page = mount(connectedComponent(QueryPage, { mockStore, props: locationProp }));
|
||||
const page = mount(connectedComponent(ConnectedQueryPage, { mockStore, props: locationProp }));
|
||||
|
||||
expect(page.find('QuerySidePanel').length).toEqual(1);
|
||||
});
|
||||
@ -51,15 +55,15 @@ describe('QueryPage - component', () => {
|
||||
it('sets selectedTargets based on host_ids', () => {
|
||||
const singleHostProps = { params: {}, location: { query: { host_ids: String(hostStub.id) } } };
|
||||
const multipleHostsProps = { params: {}, location: { query: { host_ids: [String(hostStub.id), '99'] } } };
|
||||
const singleHostPage = mount(connectedComponent(QueryPage, { mockStore, props: singleHostProps }));
|
||||
const multipleHostsPage = mount(connectedComponent(QueryPage, { mockStore, props: multipleHostsProps }));
|
||||
const singleHostPage = mount(connectedComponent(ConnectedQueryPage, { mockStore, props: singleHostProps }));
|
||||
const multipleHostsPage = mount(connectedComponent(ConnectedQueryPage, { mockStore, props: multipleHostsProps }));
|
||||
|
||||
expect(singleHostPage.find('QueryPage').prop('selectedTargets')).toEqual([hostStub]);
|
||||
expect(multipleHostsPage.find('QueryPage').prop('selectedTargets')).toEqual([hostStub, { ...hostStub, id: 99 }]);
|
||||
});
|
||||
|
||||
it('sets targetError in state when the query is run and there are no selected targets', () => {
|
||||
const page = mount(connectedComponent(QueryPage, { mockStore, props: locationProp }));
|
||||
const page = mount(connectedComponent(ConnectedQueryPage, { mockStore, props: locationProp }));
|
||||
const form = page.find('QueryForm');
|
||||
const runQueryBtn = form.find('.query-form__run-query-btn');
|
||||
|
||||
@ -91,7 +95,7 @@ describe('QueryPage - component', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const page = mount(connectedComponent(QueryPage, {
|
||||
const page = mount(connectedComponent(ConnectedQueryPage, {
|
||||
mockStore: mockStoreWithQuery,
|
||||
props: locationWithQueryProp,
|
||||
}));
|
||||
@ -115,4 +119,25 @@ describe('QueryPage - component', () => {
|
||||
type: 'queries_UPDATE_REQUEST',
|
||||
});
|
||||
});
|
||||
|
||||
describe('export as csv', () => {
|
||||
it('exports the campaign query results in csv format', () => {
|
||||
const queryResult = { org_name: 'Kolide', org_url: 'https://kolide.co' };
|
||||
const campaign = { id: 1, query_results: [queryResult] };
|
||||
const queryResultsCSV = convertToCSV([queryResult]);
|
||||
const fileSaveSpy = spyOn(FileSave, 'saveAs');
|
||||
const Page = mount(<QueryPage dispatch={noop} selectedOsqueryTable={defaultSelectedOsqueryTable} />);
|
||||
const filename = 'query_results.csv';
|
||||
const fileStub = new global.window.File([queryResultsCSV], filename, { type: 'text/csv' });
|
||||
|
||||
Page.setState({ campaign });
|
||||
Page.node.socket = {};
|
||||
|
||||
const QueryResultsTable = Page.find('QueryResultsTable');
|
||||
|
||||
QueryResultsTable.find('Button').simulate('click');
|
||||
|
||||
expect(fileSaveSpy).toHaveBeenCalledWith(fileStub);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,4 +1,4 @@
|
||||
import { destroyFunc, updateFunc } from 'redux/nodes/entities/campaigns/helpers';
|
||||
import { destroyFunc, update } from 'redux/nodes/entities/campaigns/helpers';
|
||||
import Kolide from 'kolide';
|
||||
import reduxConfig from 'redux/nodes/entities/base/reduxConfig';
|
||||
import schemas from 'redux/nodes/entities/base/schemas';
|
||||
@ -6,9 +6,9 @@ import schemas from 'redux/nodes/entities/base/schemas';
|
||||
const { CAMPAIGNS: schema } = schemas;
|
||||
|
||||
export default reduxConfig({
|
||||
createFunc: Kolide.runQuery,
|
||||
createFunc: Kolide.queries.run,
|
||||
destroyFunc,
|
||||
updateFunc,
|
||||
updateFunc: update,
|
||||
entityName: 'campaigns',
|
||||
schema,
|
||||
});
|
||||
|
@ -2,8 +2,8 @@ export const destroyFunc = (campaign) => {
|
||||
return Promise.resolve(campaign);
|
||||
};
|
||||
|
||||
export const updateFunc = (campaign, socketData) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
export const update = (campaign, socketData) => {
|
||||
return new Promise((resolve) => {
|
||||
const { type, data } = socketData;
|
||||
|
||||
if (type === 'totals') {
|
||||
@ -17,9 +17,6 @@ export const updateFunc = (campaign, socketData) => {
|
||||
const queryResults = campaign.query_results || [];
|
||||
const hosts = campaign.hosts || [];
|
||||
const { host, rows } = data;
|
||||
const newQueryResults = rows.map((row) => {
|
||||
return { ...row, hostname: host.hostname };
|
||||
});
|
||||
|
||||
return resolve({
|
||||
...campaign,
|
||||
@ -29,13 +26,19 @@ export const updateFunc = (campaign, socketData) => {
|
||||
],
|
||||
query_results: [
|
||||
...queryResults,
|
||||
...newQueryResults,
|
||||
...rows,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return reject();
|
||||
if (type === 'status') {
|
||||
const { status } = data;
|
||||
|
||||
return resolve({ ...campaign, status });
|
||||
}
|
||||
|
||||
return resolve(campaign);
|
||||
});
|
||||
};
|
||||
|
||||
export default { destroyFunc, updateFunc };
|
||||
export default { destroyFunc, update };
|
||||
|
@ -23,7 +23,7 @@ const campaignWithResults = {
|
||||
online: 2,
|
||||
},
|
||||
};
|
||||
const { destroyFunc, updateFunc } = helpers;
|
||||
const { destroyFunc, update } = helpers;
|
||||
const resultSocketData = {
|
||||
type: 'result',
|
||||
data: {
|
||||
@ -55,14 +55,14 @@ describe('campaign entity - helpers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#updateFunc', () => {
|
||||
describe('#update', () => {
|
||||
it('appends query results to the campaign when the campaign has query results', (done) => {
|
||||
updateFunc(campaignWithResults, resultSocketData)
|
||||
update(campaignWithResults, resultSocketData)
|
||||
.then((response) => {
|
||||
expect(response.query_results).toEqual([
|
||||
...campaignWithResults.query_results,
|
||||
{ hostname: host.hostname, feature: 'product_name', value: 'Intel Core' },
|
||||
{ hostname: host.hostname, feature: 'family', value: '0600' },
|
||||
{ feature: 'product_name', value: 'Intel Core' },
|
||||
{ feature: 'family', value: '0600' },
|
||||
]);
|
||||
expect(response.hosts).toInclude(host);
|
||||
done();
|
||||
@ -71,11 +71,11 @@ describe('campaign entity - helpers', () => {
|
||||
});
|
||||
|
||||
it('adds query results to the campaign when the campaign does not have query results', (done) => {
|
||||
updateFunc(campaign, resultSocketData)
|
||||
update(campaign, resultSocketData)
|
||||
.then((response) => {
|
||||
expect(response.query_results).toEqual([
|
||||
{ hostname: host.hostname, feature: 'product_name', value: 'Intel Core' },
|
||||
{ hostname: host.hostname, feature: 'family', value: '0600' },
|
||||
{ feature: 'product_name', value: 'Intel Core' },
|
||||
{ feature: 'family', value: '0600' },
|
||||
]);
|
||||
expect(response.hosts).toInclude(host);
|
||||
done();
|
||||
@ -84,7 +84,7 @@ describe('campaign entity - helpers', () => {
|
||||
});
|
||||
|
||||
it('updates totals on the campaign when the campaign has totals', (done) => {
|
||||
updateFunc(campaignWithResults, totalsSocketData)
|
||||
update(campaignWithResults, totalsSocketData)
|
||||
.then((response) => {
|
||||
expect(response.totals).toEqual(totalsSocketData.data);
|
||||
done();
|
||||
@ -93,7 +93,7 @@ describe('campaign entity - helpers', () => {
|
||||
});
|
||||
|
||||
it('adds totals to the campaign when the campaign does not have totals', (done) => {
|
||||
updateFunc(campaign, totalsSocketData)
|
||||
update(campaign, totalsSocketData)
|
||||
.then((response) => {
|
||||
expect(response.totals).toEqual(totalsSocketData.data);
|
||||
done();
|
||||
|
20
frontend/utilities/convert_to_csv/convert_to_csv.tests.js
Normal file
20
frontend/utilities/convert_to_csv/convert_to_csv.tests.js
Normal file
@ -0,0 +1,20 @@
|
||||
import expect from 'expect';
|
||||
|
||||
import convertToCSV from 'utilities/convert_to_csv';
|
||||
|
||||
const objArray = [
|
||||
{
|
||||
first_name: 'Mike',
|
||||
last_name: 'Stone',
|
||||
},
|
||||
{
|
||||
first_name: 'Paul',
|
||||
last_name: 'Simon',
|
||||
},
|
||||
];
|
||||
|
||||
describe('convertToCSV - utility', () => {
|
||||
it('converts an array of objects to CSV format', () => {
|
||||
expect(convertToCSV(objArray)).toEqual('"first_name","last_name"\n"Mike","Stone"\n"Paul","Simon"');
|
||||
});
|
||||
});
|
18
frontend/utilities/convert_to_csv/index.js
Normal file
18
frontend/utilities/convert_to_csv/index.js
Normal file
@ -0,0 +1,18 @@
|
||||
import { keys } from 'lodash';
|
||||
|
||||
const defaultFieldSortFunc = fields => fields;
|
||||
|
||||
const convertToCSV = (objArray, fieldSortFunc = defaultFieldSortFunc) => {
|
||||
const fields = fieldSortFunc(keys(objArray[0]));
|
||||
const jsonFields = fields.map(field => JSON.stringify(field));
|
||||
const rows = objArray.map((row) => {
|
||||
return fields.map(field => JSON.stringify(row[field])).join(',');
|
||||
});
|
||||
|
||||
|
||||
rows.unshift(jsonFields.join(','));
|
||||
|
||||
return rows.join('\n');
|
||||
};
|
||||
|
||||
export default convertToCSV;
|
Loading…
Reference in New Issue
Block a user