mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 17:05:18 +00:00
New query page updates (#1229)
* Stop rendering results when query hasn’t been run * Adds QueryPageSelectTargets component * Re-arranges target select input on Query Page * Adds label to KolideAce component * Re-arrange inputs on the Query Form component
This commit is contained in:
parent
146ee18c62
commit
803bc41366
@ -14,6 +14,7 @@ class KolideAce extends Component {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
error: PropTypes.string,
|
error: PropTypes.string,
|
||||||
fontSize: PropTypes.number,
|
fontSize: PropTypes.number,
|
||||||
|
label: PropTypes.string,
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
onLoad: PropTypes.func,
|
onLoad: PropTypes.func,
|
||||||
@ -31,6 +32,18 @@ class KolideAce extends Component {
|
|||||||
wrapEnabled: false,
|
wrapEnabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
renderLabel = () => {
|
||||||
|
const { error, label } = this.props;
|
||||||
|
|
||||||
|
const labelClassName = classnames(`${baseClass}__label`, {
|
||||||
|
[`${baseClass}__label--error`]: error,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p className={labelClassName}>{error || label}</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const {
|
const {
|
||||||
error,
|
error,
|
||||||
@ -44,6 +57,7 @@ class KolideAce extends Component {
|
|||||||
wrapEnabled,
|
wrapEnabled,
|
||||||
wrapperClassName,
|
wrapperClassName,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
const { renderLabel } = this;
|
||||||
|
|
||||||
const wrapperClass = classnames(wrapperClassName, {
|
const wrapperClass = classnames(wrapperClassName, {
|
||||||
[`${baseClass}__wrapper--error`]: error,
|
[`${baseClass}__wrapper--error`]: error,
|
||||||
@ -51,7 +65,7 @@ class KolideAce extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={wrapperClass}>
|
<div className={wrapperClass}>
|
||||||
<div className={`${baseClass}__error-field`}>{error}</div>
|
{renderLabel()}
|
||||||
<AceEditor
|
<AceEditor
|
||||||
enableBasicAutocompletion
|
enableBasicAutocompletion
|
||||||
enableLiveAutocompletion
|
enableLiveAutocompletion
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
.kolide-ace {
|
.kolide-ace {
|
||||||
&__error-field {
|
&__label {
|
||||||
color: $alert;
|
|
||||||
display: block;
|
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-stretch: normal;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: $bold;
|
font-weight: $bold;
|
||||||
|
font-style: normal;
|
||||||
|
font-stretch: normal;
|
||||||
letter-spacing: -0.5px;
|
letter-spacing: -0.5px;
|
||||||
|
color: $text-dark;
|
||||||
|
display: block;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
min-height: 25px;
|
min-height: 25px;
|
||||||
|
|
||||||
|
&--error {
|
||||||
|
color: $alert;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__wrapper {
|
&__wrapper {
|
||||||
|
@ -75,6 +75,10 @@ $base-class: 'button';
|
|||||||
@include button-variant($warning);
|
@include button-variant($warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--link {
|
||||||
|
@include button-variant($link);
|
||||||
|
}
|
||||||
|
|
||||||
&--inverse {
|
&--inverse {
|
||||||
@include button-variant($white, $inverse-hover, $brand);
|
@include button-variant($white, $inverse-hover, $brand);
|
||||||
color: $brand;
|
color: $brand;
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import { size } from 'lodash';
|
import { size } from 'lodash';
|
||||||
|
|
||||||
import Button from 'components/buttons/Button';
|
|
||||||
import DropdownButton from 'components/buttons/DropdownButton';
|
import DropdownButton from 'components/buttons/DropdownButton';
|
||||||
import Form from 'components/forms/Form';
|
import Form from 'components/forms/Form';
|
||||||
import formFieldInterface from 'interfaces/form_field';
|
import formFieldInterface from 'interfaces/form_field';
|
||||||
@ -9,10 +8,7 @@ import helpers from 'components/forms/queries/QueryForm/helpers';
|
|||||||
import InputField from 'components/forms/fields/InputField';
|
import InputField from 'components/forms/fields/InputField';
|
||||||
import KolideAce from 'components/KolideAce';
|
import KolideAce from 'components/KolideAce';
|
||||||
import queryInterface from 'interfaces/query';
|
import queryInterface from 'interfaces/query';
|
||||||
import SelectTargetsDropdown from 'components/forms/fields/SelectTargetsDropdown';
|
|
||||||
import targetInterface from 'interfaces/target';
|
|
||||||
import validateQuery from 'components/forms/validators/validate_query';
|
import validateQuery from 'components/forms/validators/validate_query';
|
||||||
import Timer from 'components/loaders/Timer';
|
|
||||||
|
|
||||||
const baseClass = 'query-form';
|
const baseClass = 'query-form';
|
||||||
|
|
||||||
@ -46,16 +42,9 @@ class QueryForm extends Component {
|
|||||||
}).isRequired,
|
}).isRequired,
|
||||||
handleSubmit: PropTypes.func,
|
handleSubmit: PropTypes.func,
|
||||||
formData: queryInterface,
|
formData: queryInterface,
|
||||||
onFetchTargets: PropTypes.func,
|
|
||||||
onOsqueryTableSelect: PropTypes.func,
|
onOsqueryTableSelect: PropTypes.func,
|
||||||
onRunQuery: PropTypes.func,
|
|
||||||
onStopQuery: PropTypes.func,
|
|
||||||
onTargetSelect: PropTypes.func,
|
|
||||||
onUpdate: PropTypes.func,
|
onUpdate: PropTypes.func,
|
||||||
queryIsRunning: PropTypes.bool,
|
queryIsRunning: PropTypes.bool,
|
||||||
selectedTargets: PropTypes.arrayOf(targetInterface),
|
|
||||||
targetsCount: PropTypes.number,
|
|
||||||
targetsError: PropTypes.string,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@ -85,16 +74,6 @@ class QueryForm extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onRunQuery = (queryText) => {
|
|
||||||
return (evt) => {
|
|
||||||
evt.preventDefault();
|
|
||||||
|
|
||||||
const { onRunQuery: handleRunQuery } = this.props;
|
|
||||||
|
|
||||||
return handleRunQuery(queryText);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onUpdate = (evt) => {
|
onUpdate = (evt) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
|
|
||||||
@ -126,113 +105,58 @@ class QueryForm extends Component {
|
|||||||
|
|
||||||
renderButtons = () => {
|
renderButtons = () => {
|
||||||
const { canSaveAsNew, canSaveChanges } = helpers;
|
const { canSaveAsNew, canSaveChanges } = helpers;
|
||||||
const {
|
const { fields, formData, handleSubmit } = this.props;
|
||||||
fields,
|
const { onUpdate } = this;
|
||||||
formData,
|
|
||||||
handleSubmit,
|
|
||||||
onStopQuery,
|
|
||||||
queryIsRunning,
|
|
||||||
} = this.props;
|
|
||||||
const { onRunQuery, onUpdate } = this;
|
|
||||||
|
|
||||||
const dropdownBtnOptions = [{
|
const dropdownBtnOptions = [
|
||||||
disabled: !canSaveChanges(fields, formData),
|
{
|
||||||
label: 'Save Changes',
|
disabled: !canSaveChanges(fields, formData),
|
||||||
onClick: onUpdate,
|
label: 'Save Changes',
|
||||||
}, {
|
onClick: onUpdate,
|
||||||
disabled: !canSaveAsNew(fields, formData),
|
},
|
||||||
label: 'Save As New...',
|
{
|
||||||
onClick: handleSubmit,
|
disabled: !canSaveAsNew(fields, formData),
|
||||||
}];
|
label: 'Save As New...',
|
||||||
|
onClick: handleSubmit,
|
||||||
let runQueryButton;
|
},
|
||||||
|
];
|
||||||
if (queryIsRunning) {
|
|
||||||
runQueryButton = (
|
|
||||||
<Button
|
|
||||||
className={`${baseClass}__stop-query-btn`}
|
|
||||||
onClick={onStopQuery}
|
|
||||||
variant="alert"
|
|
||||||
>
|
|
||||||
Stop Query
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
runQueryButton = (
|
|
||||||
<Button
|
|
||||||
className={`${baseClass}__run-query-btn`}
|
|
||||||
onClick={onRunQuery(fields.query.value)}
|
|
||||||
variant="brand"
|
|
||||||
>
|
|
||||||
Run Query
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${baseClass}__button-wrap`}>
|
<div className={`${baseClass}__button-wrap`}>
|
||||||
{queryIsRunning && <Timer running={queryIsRunning} />}
|
|
||||||
|
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
className={`${baseClass}__save`}
|
className={`${baseClass}__save`}
|
||||||
options={dropdownBtnOptions}
|
options={dropdownBtnOptions}
|
||||||
variant="success"
|
variant="brand"
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</DropdownButton>
|
</DropdownButton>
|
||||||
|
|
||||||
{runQueryButton}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderTargetsInput = () => {
|
|
||||||
const {
|
|
||||||
onFetchTargets,
|
|
||||||
onTargetSelect,
|
|
||||||
selectedTargets,
|
|
||||||
targetsCount,
|
|
||||||
targetsError,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<SelectTargetsDropdown
|
|
||||||
error={targetsError}
|
|
||||||
onFetchTargets={onFetchTargets}
|
|
||||||
onSelect={onTargetSelect}
|
|
||||||
selectedTargets={selectedTargets}
|
|
||||||
targetsCount={targetsCount}
|
|
||||||
label="Select Targets"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { errors } = this.state;
|
|
||||||
const { baseError, fields, handleSubmit, queryIsRunning } = this.props;
|
const { baseError, fields, handleSubmit, queryIsRunning } = this.props;
|
||||||
const { onLoad, renderButtons, renderTargetsInput } = this;
|
const { errors } = this.state;
|
||||||
|
const { onLoad, renderButtons } = this;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className={`${baseClass}__wrapper`} onSubmit={handleSubmit}>
|
<form className={`${baseClass}__wrapper`} onSubmit={handleSubmit}>
|
||||||
<h1>New Query</h1>
|
<h1>New Query</h1>
|
||||||
<KolideAce
|
|
||||||
{...fields.query}
|
|
||||||
error={fields.query.error || errors.query}
|
|
||||||
onLoad={onLoad}
|
|
||||||
readOnly={queryIsRunning}
|
|
||||||
wrapperClassName={`${baseClass}__text-editor-wrapper`}
|
|
||||||
/>
|
|
||||||
{baseError && <div className="form__base-error">{baseError}</div>}
|
{baseError && <div className="form__base-error">{baseError}</div>}
|
||||||
{renderTargetsInput()}
|
|
||||||
<InputField
|
<InputField
|
||||||
{...fields.name}
|
{...fields.name}
|
||||||
error={fields.name.error || errors.name}
|
error={fields.name.error || errors.name}
|
||||||
inputClassName={`${baseClass}__query-title`}
|
inputClassName={`${baseClass}__query-title`}
|
||||||
label="Query Title"
|
label="Query Title"
|
||||||
/>
|
/>
|
||||||
|
<KolideAce
|
||||||
|
{...fields.query}
|
||||||
|
error={fields.query.error || errors.query}
|
||||||
|
label="SQL"
|
||||||
|
onLoad={onLoad}
|
||||||
|
readOnly={queryIsRunning}
|
||||||
|
wrapperClassName={`${baseClass}__text-editor-wrapper`}
|
||||||
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
{...fields.description}
|
{...fields.description}
|
||||||
inputClassName={`${baseClass}__query-description`}
|
inputClassName={`${baseClass}__query-description`}
|
||||||
|
@ -37,36 +37,6 @@ describe('QueryForm - component', () => {
|
|||||||
expect(inputFields.find({ name: 'description' }).length).toEqual(1);
|
expect(inputFields.find({ name: 'description' }).length).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a "stop query" button when a query is running', () => {
|
|
||||||
const form = mount(<QueryForm onTargetSelect={noop} query={query} queryIsRunning queryText={queryText} />);
|
|
||||||
const runQueryBtn = form.find('.query-form__run-query-btn');
|
|
||||||
const stopQueryBtn = form.find('.query-form__stop-query-btn');
|
|
||||||
|
|
||||||
expect(runQueryBtn.length).toEqual(0);
|
|
||||||
expect(stopQueryBtn.length).toEqual(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders a "run query" button when a query is not running', () => {
|
|
||||||
const form = mount(<QueryForm formData={{ ...query, query: queryText }} onTargetSelect={noop} queryIsRunning={false} />);
|
|
||||||
const runQueryBtn = form.find('.query-form__run-query-btn');
|
|
||||||
const stopQueryBtn = form.find('.query-form__stop-query-btn');
|
|
||||||
|
|
||||||
expect(runQueryBtn.length).toEqual(1);
|
|
||||||
expect(stopQueryBtn.length).toEqual(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls the onStopQuery prop when the stop query button is clicked', () => {
|
|
||||||
const onStopQuerySpy = createSpy();
|
|
||||||
const form = mount(
|
|
||||||
<QueryForm onStopQuery={onStopQuerySpy} onTargetSelect={noop} formData={query} queryIsRunning queryText={queryText} />
|
|
||||||
);
|
|
||||||
const stopQueryBtn = form.find('.query-form__stop-query-btn');
|
|
||||||
|
|
||||||
stopQueryBtn.simulate('click');
|
|
||||||
|
|
||||||
expect(onStopQuerySpy).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('validates the query name before saving changes', () => {
|
it('validates the query name before saving changes', () => {
|
||||||
const updateSpy = createSpy();
|
const updateSpy = createSpy();
|
||||||
const form = mount(<QueryForm formData={{ ...query, query: queryText }} onTargetSelect={noop} onUpdate={updateSpy} />);
|
const form = mount(<QueryForm formData={{ ...query, query: queryText }} onTargetSelect={noop} onUpdate={updateSpy} />);
|
||||||
@ -175,14 +145,4 @@ describe('QueryForm - component', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls the onRunQuery prop with the query text when "Run Query" is clicked and the form is valid', () => {
|
|
||||||
const onRunQuerySpy = createSpy();
|
|
||||||
const form = mount(<QueryForm formData={{ ...query, query: queryText }} onRunQuery={onRunQuerySpy} onTargetSelect={noop} />);
|
|
||||||
const runQueryBtn = form.find('.query-form__run-query-btn');
|
|
||||||
|
|
||||||
runQueryBtn.simulate('click');
|
|
||||||
|
|
||||||
expect(onRunQuerySpy).toHaveBeenCalledWith(query.query);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
.query-form {
|
.query-form {
|
||||||
&__wrapper {
|
&__wrapper {
|
||||||
padding: $base;
|
padding: $base;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 19px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__query-title,
|
&__query-title,
|
||||||
|
@ -0,0 +1,124 @@
|
|||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { get } from 'lodash';
|
||||||
|
|
||||||
|
import Button from 'components/buttons/Button';
|
||||||
|
import campaignInterface from 'interfaces/campaign';
|
||||||
|
import ProgressBar from 'components/loaders/ProgressBar';
|
||||||
|
import SelectTargetsDropdown from 'components/forms/fields/SelectTargetsDropdown';
|
||||||
|
import targetInterface from 'interfaces/target';
|
||||||
|
import Timer from 'components/loaders/Timer';
|
||||||
|
|
||||||
|
const baseClass = 'query-page-select-targets';
|
||||||
|
|
||||||
|
class QueryPageSelectTargets extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
campaign: campaignInterface,
|
||||||
|
error: PropTypes.string,
|
||||||
|
onFetchTargets: PropTypes.func.isRequired,
|
||||||
|
onRunQuery: PropTypes.func.isRequired,
|
||||||
|
onStopQuery: PropTypes.func.isRequired,
|
||||||
|
onTargetSelect: PropTypes.func.isRequired,
|
||||||
|
query: PropTypes.string,
|
||||||
|
queryIsRunning: PropTypes.bool,
|
||||||
|
selectedTargets: PropTypes.arrayOf(targetInterface),
|
||||||
|
targetsCount: PropTypes.number,
|
||||||
|
};
|
||||||
|
|
||||||
|
onRunQuery = () => {
|
||||||
|
const { onRunQuery, query } = this.props;
|
||||||
|
|
||||||
|
return onRunQuery(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderProgressDetails = () => {
|
||||||
|
const {
|
||||||
|
campaign,
|
||||||
|
onStopQuery,
|
||||||
|
queryIsRunning,
|
||||||
|
} = this.props;
|
||||||
|
const { onRunQuery } = this;
|
||||||
|
const { hosts_count: hostsCount } = campaign;
|
||||||
|
const totalHostsCount = get(campaign, ['totals', 'count'], 0);
|
||||||
|
const totalRowsCount = get(campaign, ['query_results', 'length'], 0);
|
||||||
|
|
||||||
|
const runQueryBtn = (
|
||||||
|
<div className={`${baseClass}__query-btn-wrapper`}>
|
||||||
|
{queryIsRunning && <Timer running={queryIsRunning} />}
|
||||||
|
<Button
|
||||||
|
className={`${baseClass}__run-query-btn`}
|
||||||
|
onClick={onRunQuery}
|
||||||
|
variant="success"
|
||||||
|
>
|
||||||
|
Run
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
const stopQueryBtn = (
|
||||||
|
<div className={`${baseClass}__query-btn-wrapper`}>
|
||||||
|
{queryIsRunning && <Timer running={queryIsRunning} />}
|
||||||
|
<Button
|
||||||
|
className={`${baseClass}__stop-query-btn`}
|
||||||
|
onClick={onStopQuery}
|
||||||
|
variant="alert"
|
||||||
|
>
|
||||||
|
Stop
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hostsCount.total) {
|
||||||
|
return (
|
||||||
|
<div className={`${baseClass}__progress-wrapper`}>
|
||||||
|
<div className={`${baseClass}__progress-details`} />
|
||||||
|
{queryIsRunning ? stopQueryBtn : runQueryBtn}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${baseClass}__progress-wrapper`}>
|
||||||
|
<div className={`${baseClass}__progress-details`}>
|
||||||
|
<span>
|
||||||
|
<b>{hostsCount.total}</b> of
|
||||||
|
<b>{totalHostsCount} Hosts</b> Returning
|
||||||
|
<b>{totalRowsCount} Records </b>
|
||||||
|
({hostsCount.failed} failed)
|
||||||
|
</span>
|
||||||
|
<ProgressBar
|
||||||
|
error={hostsCount.failed}
|
||||||
|
max={totalHostsCount}
|
||||||
|
success={hostsCount.successful}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{queryIsRunning ? stopQueryBtn : runQueryBtn}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const {
|
||||||
|
error,
|
||||||
|
onFetchTargets,
|
||||||
|
onTargetSelect,
|
||||||
|
selectedTargets,
|
||||||
|
targetsCount,
|
||||||
|
} = this.props;
|
||||||
|
const { renderProgressDetails } = this;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${baseClass}__wrapper body-wrap`}>
|
||||||
|
{renderProgressDetails()}
|
||||||
|
<SelectTargetsDropdown
|
||||||
|
error={error}
|
||||||
|
onFetchTargets={onFetchTargets}
|
||||||
|
onSelect={onTargetSelect}
|
||||||
|
selectedTargets={selectedTargets}
|
||||||
|
targetsCount={targetsCount}
|
||||||
|
label="Select Targets"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QueryPageSelectTargets;
|
@ -0,0 +1,173 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import expect, { createSpy, restoreSpies } from 'expect';
|
||||||
|
import { mount } from 'enzyme';
|
||||||
|
import { noop } from 'lodash';
|
||||||
|
|
||||||
|
import { campaignStub } from 'test/stubs';
|
||||||
|
import QueryPageSelectTargets from 'components/queries/QueryPageSelectTargets';
|
||||||
|
|
||||||
|
describe('QueryPageSelectTargets - component', () => {
|
||||||
|
const DEFAULT_CAMPAIGN = {
|
||||||
|
hosts_count: {
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
campaign: DEFAULT_CAMPAIGN,
|
||||||
|
onFetchTargets: noop,
|
||||||
|
onRunQuery: noop,
|
||||||
|
onStopQuery: noop,
|
||||||
|
onTargetSelect: noop,
|
||||||
|
query: 'select * from users',
|
||||||
|
queryIsRunning: false,
|
||||||
|
selectedTargets: [],
|
||||||
|
targetsCount: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(restoreSpies);
|
||||||
|
|
||||||
|
describe('rendering', () => {
|
||||||
|
const DefaultComponent = mount(<QueryPageSelectTargets {...defaultProps} />);
|
||||||
|
|
||||||
|
it('renders', () => {
|
||||||
|
expect(DefaultComponent.length).toEqual(1, 'QueryPageSelectTargets did not render');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders a SelectTargetsDropdown component', () => {
|
||||||
|
const SelectTargetsDropdown = DefaultComponent.find('SelectTargetsDropdown');
|
||||||
|
|
||||||
|
expect(SelectTargetsDropdown.length).toEqual(1, 'SelectTargetsDropdown did not render');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders a Run Query Button', () => {
|
||||||
|
const RunQueryButton = DefaultComponent.find('.query-page-select-targets__run-query-btn');
|
||||||
|
|
||||||
|
expect(RunQueryButton.length).toEqual(1, 'RunQueryButton did not render');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render a Stop Query Button', () => {
|
||||||
|
const StopQueryButton = DefaultComponent.find('.query-page-select-targets__stop-query-btn');
|
||||||
|
|
||||||
|
expect(StopQueryButton.length).toEqual(0, 'StopQueryButton is not expected to render');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render a Timer component', () => {
|
||||||
|
const Timer = DefaultComponent.find('Timer');
|
||||||
|
|
||||||
|
expect(Timer.length).toEqual(0, 'Timer is not expected to render');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render a ProgressBar component', () => {
|
||||||
|
const ProgressBar = DefaultComponent.find('ProgressBar');
|
||||||
|
|
||||||
|
expect(ProgressBar.length).toEqual(0, 'ProgressBar is not expected to render');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the campaign has results', () => {
|
||||||
|
describe('and the query is running', () => {
|
||||||
|
const props = {
|
||||||
|
...defaultProps,
|
||||||
|
campaign: campaignStub,
|
||||||
|
queryIsRunning: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Component = mount(<QueryPageSelectTargets {...props} />);
|
||||||
|
|
||||||
|
it('renders a Timer component', () => {
|
||||||
|
const Timer = Component.find('Timer');
|
||||||
|
|
||||||
|
expect(Timer.length).toEqual(1, 'Timer is expected to render');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders a Stop Query Button', () => {
|
||||||
|
const StopQueryButton = Component.find('.query-page-select-targets__stop-query-btn');
|
||||||
|
|
||||||
|
expect(StopQueryButton.length).toEqual(1, 'StopQueryButton is expected to render');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render a Run Query Button', () => {
|
||||||
|
const RunQueryButton = Component.find('.query-page-select-targets__run-query-btn');
|
||||||
|
|
||||||
|
expect(RunQueryButton.length).toEqual(0, 'RunQueryButton is not expected render');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders a ProgressBar component', () => {
|
||||||
|
const ProgressBar = Component.find('ProgressBar');
|
||||||
|
|
||||||
|
expect(ProgressBar.length).toEqual(1, 'ProgressBar is expected to render');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and the query is not running', () => {
|
||||||
|
const props = {
|
||||||
|
...defaultProps,
|
||||||
|
campaign: campaignStub,
|
||||||
|
queryIsRunning: false,
|
||||||
|
};
|
||||||
|
const Component = mount(<QueryPageSelectTargets {...props} />);
|
||||||
|
|
||||||
|
it('does not render a Timer component', () => {
|
||||||
|
const Timer = Component.find('Timer');
|
||||||
|
|
||||||
|
expect(Timer.length).toEqual(0, 'Timer is not expected to render');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render a Stop Query Button', () => {
|
||||||
|
const StopQueryButton = Component.find('.query-page-select-targets__stop-query-btn');
|
||||||
|
|
||||||
|
expect(StopQueryButton.length).toEqual(0, 'StopQueryButton is not expected to render');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders a Run Query Button', () => {
|
||||||
|
const RunQueryButton = Component.find('.query-page-select-targets__run-query-btn');
|
||||||
|
|
||||||
|
expect(RunQueryButton.length).toEqual(1, 'RunQueryButton did not render');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders a ProgressBar component', () => {
|
||||||
|
const ProgressBar = Component.find('ProgressBar');
|
||||||
|
|
||||||
|
expect(ProgressBar.length).toEqual(1, 'ProgressBar is expected to render');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('running a query', () => {
|
||||||
|
it('calls the onRunQuery prop with the query text', () => {
|
||||||
|
const spy = createSpy();
|
||||||
|
const query = 'select * from groups';
|
||||||
|
const props = {
|
||||||
|
...defaultProps,
|
||||||
|
campaign: campaignStub,
|
||||||
|
onRunQuery: spy,
|
||||||
|
query,
|
||||||
|
};
|
||||||
|
const Component = mount(<QueryPageSelectTargets {...props} />);
|
||||||
|
const RunQueryButton = Component.find('.query-page-select-targets__run-query-btn');
|
||||||
|
|
||||||
|
RunQueryButton.simulate('click');
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalledWith(query);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('stopping a query', () => {
|
||||||
|
it('calls the onStopQuery prop', () => {
|
||||||
|
const spy = createSpy();
|
||||||
|
const props = {
|
||||||
|
...defaultProps,
|
||||||
|
campaign: campaignStub,
|
||||||
|
onStopQuery: spy,
|
||||||
|
queryIsRunning: true,
|
||||||
|
};
|
||||||
|
const Component = mount(<QueryPageSelectTargets {...props} />);
|
||||||
|
const StopQueryButton = Component.find('.query-page-select-targets__stop-query-btn');
|
||||||
|
|
||||||
|
StopQueryButton.simulate('click');
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,18 @@
|
|||||||
|
.query-page-select-targets {
|
||||||
|
&__wrapper {
|
||||||
|
padding: $base;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__progress-details {
|
||||||
|
display: inline-block;
|
||||||
|
width: 378px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__query-btn-wrapper {
|
||||||
|
float: right;
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export default from './QueryPageSelectTargets';
|
@ -1,13 +1,12 @@
|
|||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { get, keys, omit } from 'lodash';
|
import { keys, omit } from 'lodash';
|
||||||
|
|
||||||
import Button from 'components/buttons/Button';
|
import Button from 'components/buttons/Button';
|
||||||
import campaignInterface from 'interfaces/campaign';
|
import campaignInterface from 'interfaces/campaign';
|
||||||
import filterArrayByHash from 'utilities/filter_array_by_hash';
|
import filterArrayByHash from 'utilities/filter_array_by_hash';
|
||||||
import Icon from 'components/icons/Icon';
|
import Icon from 'components/icons/Icon';
|
||||||
import InputField from 'components/forms/fields/InputField';
|
import InputField from 'components/forms/fields/InputField';
|
||||||
import ProgressBar from 'components/loaders/ProgressBar';
|
|
||||||
import QueryResultsRow from 'components/queries/QueryResultsTable/QueryResultsRow';
|
import QueryResultsRow from 'components/queries/QueryResultsTable/QueryResultsRow';
|
||||||
|
|
||||||
const baseClass = 'query-results-table';
|
const baseClass = 'query-results-table';
|
||||||
@ -45,29 +44,6 @@ class QueryResultsTable extends Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
renderProgressDetails = () => {
|
|
||||||
const { campaign } = this.props;
|
|
||||||
const { hosts_count: hostsCount } = campaign;
|
|
||||||
const totalHostsCount = get(campaign, 'totals.count', 0);
|
|
||||||
const totalRowsCount = get(campaign, 'query_results.length', 0);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`${baseClass}__progress-details`}>
|
|
||||||
<span>
|
|
||||||
<b>{hostsCount.total}</b> of
|
|
||||||
<b>{totalHostsCount} Hosts</b> Returning
|
|
||||||
<b>{totalRowsCount} Records </b>
|
|
||||||
({hostsCount.failed} failed)
|
|
||||||
</span>
|
|
||||||
<ProgressBar
|
|
||||||
error={hostsCount.failed}
|
|
||||||
max={totalHostsCount}
|
|
||||||
success={hostsCount.successful}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderTableHeaderRowData = (column, index) => {
|
renderTableHeaderRowData = (column, index) => {
|
||||||
const filterable = column === 'hostname' ? 'host_hostname' : column;
|
const filterable = column === 'hostname' ? 'host_hostname' : column;
|
||||||
const { activeColumn, resultsFilter } = this.state;
|
const { activeColumn, resultsFilter } = this.state;
|
||||||
@ -127,7 +103,6 @@ class QueryResultsTable extends Component {
|
|||||||
render () {
|
render () {
|
||||||
const { campaign, onExportQueryResults } = this.props;
|
const { campaign, onExportQueryResults } = this.props;
|
||||||
const {
|
const {
|
||||||
renderProgressDetails,
|
|
||||||
renderTableHeaderRow,
|
renderTableHeaderRow,
|
||||||
renderTableRows,
|
renderTableRows,
|
||||||
} = this;
|
} = this;
|
||||||
@ -139,8 +114,8 @@ class QueryResultsTable extends Component {
|
|||||||
|
|
||||||
if (!hostsCount.successful) {
|
if (!hostsCount.successful) {
|
||||||
return (
|
return (
|
||||||
<div className={baseClass}>
|
<div className={`${baseClass} ${baseClass}__no-results`}>
|
||||||
{renderProgressDetails()}
|
<em>No results found</em>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -150,11 +125,10 @@ class QueryResultsTable extends Component {
|
|||||||
<Button
|
<Button
|
||||||
className={`${baseClass}__export-btn`}
|
className={`${baseClass}__export-btn`}
|
||||||
onClick={onExportQueryResults}
|
onClick={onExportQueryResults}
|
||||||
variant="brand"
|
variant="link"
|
||||||
>
|
>
|
||||||
Export
|
Export
|
||||||
</Button>
|
</Button>
|
||||||
{renderProgressDetails()}
|
|
||||||
<div className={`${baseClass}__table-wrapper`}>
|
<div className={`${baseClass}__table-wrapper`}>
|
||||||
<table className={`${baseClass}__table`}>
|
<table className={`${baseClass}__table`}>
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -78,12 +78,6 @@ describe('QueryResultsTable - component', () => {
|
|||||||
expect(componentWithoutQueryResults.html()).toNotExist();
|
expect(componentWithoutQueryResults.html()).toNotExist();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a ProgressBar component', () => {
|
|
||||||
expect(
|
|
||||||
componentWithQueryResults.find('ProgressBar').length
|
|
||||||
).toEqual(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets the column headers to the keys of the query results', () => {
|
it('sets the column headers to the keys of the query results', () => {
|
||||||
const queryResultKeys = keys(queryResult.rows[0]);
|
const queryResultKeys = keys(queryResult.rows[0]);
|
||||||
const tableHeaderText = componentWithQueryResults.find('thead').text();
|
const tableHeaderText = componentWithQueryResults.find('thead').text();
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
.query-results-table {
|
.query-results-table {
|
||||||
background-color: $white;
|
background-color: $white;
|
||||||
padding: $pad-base;
|
padding: $pad-base;
|
||||||
max-width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
&__export-btn {
|
&__export-btn {
|
||||||
@ -14,6 +14,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__no-results {
|
||||||
|
@include display(flex);
|
||||||
|
@include align-items(center);
|
||||||
|
@include justify-content(center);
|
||||||
|
}
|
||||||
|
|
||||||
&__progress-details {
|
&__progress-details {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 378px;
|
width: 378px;
|
||||||
@ -24,8 +30,9 @@
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
box-shadow: inset 0 0 8px 0 rgba(0, 0, 0, 0.12);
|
box-shadow: inset 0 0 8px 0 rgba(0, 0, 0, 0.12);
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
margin-top: 20px;
|
margin-top: 58px;
|
||||||
max-height: 550px;
|
max-height: 550px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__table {
|
&__table {
|
||||||
|
@ -18,6 +18,7 @@ import QueryForm from 'components/forms/queries/QueryForm';
|
|||||||
import osqueryTableInterface from 'interfaces/osquery_table';
|
import osqueryTableInterface from 'interfaces/osquery_table';
|
||||||
import queryActions from 'redux/nodes/entities/queries/actions';
|
import queryActions from 'redux/nodes/entities/queries/actions';
|
||||||
import queryInterface from 'interfaces/query';
|
import queryInterface from 'interfaces/query';
|
||||||
|
import QueryPageSelectTargets from 'components/queries/QueryPageSelectTargets';
|
||||||
import QueryResultsTable from 'components/queries/QueryResultsTable';
|
import QueryResultsTable from 'components/queries/QueryResultsTable';
|
||||||
import QuerySidePanel from 'components/side_panels/QuerySidePanel';
|
import QuerySidePanel from 'components/side_panels/QuerySidePanel';
|
||||||
import { renderFlash } from 'redux/nodes/notifications/actions';
|
import { renderFlash } from 'redux/nodes/notifications/actions';
|
||||||
@ -27,6 +28,11 @@ import validateQuery from 'components/forms/validators/validate_query';
|
|||||||
import Spinner from 'components/loaders/Spinner';
|
import Spinner from 'components/loaders/Spinner';
|
||||||
|
|
||||||
const baseClass = 'query-page';
|
const baseClass = 'query-page';
|
||||||
|
const DEFAULT_CAMPAIGN = {
|
||||||
|
hosts_count: {
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export class QueryPage extends Component {
|
export class QueryPage extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -52,10 +58,9 @@ export class QueryPage extends Component {
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
campaign: {
|
campaign: DEFAULT_CAMPAIGN,
|
||||||
hosts_count: { total: 0 },
|
|
||||||
},
|
|
||||||
queryIsRunning: false,
|
queryIsRunning: false,
|
||||||
|
queryText: props.query.query,
|
||||||
targetsCount: 0,
|
targetsCount: 0,
|
||||||
targetsError: null,
|
targetsError: null,
|
||||||
};
|
};
|
||||||
@ -95,6 +100,10 @@ export class QueryPage extends Component {
|
|||||||
this.csvQueryName = value;
|
this.csvQueryName = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fieldName === 'query') {
|
||||||
|
this.setState({ queryText: value });
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,7 +280,7 @@ export class QueryPage extends Component {
|
|||||||
|
|
||||||
if (this.campaign || campaign) {
|
if (this.campaign || campaign) {
|
||||||
this.campaign = null;
|
this.campaign = null;
|
||||||
this.setState({ campaign: {} });
|
this.setState({ campaign: DEFAULT_CAMPAIGN });
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@ -307,6 +316,10 @@ export class QueryPage extends Component {
|
|||||||
});
|
});
|
||||||
let resultBody = '';
|
let resultBody = '';
|
||||||
|
|
||||||
|
if (!loading && isEqual(campaign, DEFAULT_CAMPAIGN)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
resultBody = <Spinner />;
|
resultBody = <Spinner />;
|
||||||
} else {
|
} else {
|
||||||
@ -320,26 +333,45 @@ export class QueryPage extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderTargetsInput = () => {
|
||||||
|
const { onFetchTargets, onRunQuery, onStopQuery, onTargetSelect } = this;
|
||||||
|
const { campaign, queryIsRunning, queryText, targetsCount, targetsError } = this.state;
|
||||||
|
const { selectedTargets } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryPageSelectTargets
|
||||||
|
campaign={campaign}
|
||||||
|
error={targetsError}
|
||||||
|
onFetchTargets={onFetchTargets}
|
||||||
|
onRunQuery={onRunQuery}
|
||||||
|
onStopQuery={onStopQuery}
|
||||||
|
onTargetSelect={onTargetSelect}
|
||||||
|
query={queryText}
|
||||||
|
queryIsRunning={queryIsRunning}
|
||||||
|
selectedTargets={selectedTargets}
|
||||||
|
targetsCount={targetsCount}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const {
|
const {
|
||||||
onChangeQueryFormField,
|
onChangeQueryFormField,
|
||||||
onFetchTargets,
|
|
||||||
onOsqueryTableSelect,
|
onOsqueryTableSelect,
|
||||||
onRunQuery,
|
onRunQuery,
|
||||||
onSaveQueryFormSubmit,
|
onSaveQueryFormSubmit,
|
||||||
onStopQuery,
|
onStopQuery,
|
||||||
onTargetSelect,
|
|
||||||
onTextEditorInputChange,
|
onTextEditorInputChange,
|
||||||
onUpdateQuery,
|
onUpdateQuery,
|
||||||
renderResultsTable,
|
renderResultsTable,
|
||||||
|
renderTargetsInput,
|
||||||
} = this;
|
} = this;
|
||||||
const { queryIsRunning, targetsCount, targetsError } = this.state;
|
const { queryIsRunning } = this.state;
|
||||||
const {
|
const {
|
||||||
errors,
|
errors,
|
||||||
loadingQueries,
|
loadingQueries,
|
||||||
query,
|
query,
|
||||||
selectedOsqueryTable,
|
selectedOsqueryTable,
|
||||||
selectedTargets,
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (loadingQueries) {
|
if (loadingQueries) {
|
||||||
@ -354,20 +386,16 @@ export class QueryPage extends Component {
|
|||||||
formData={query}
|
formData={query}
|
||||||
handleSubmit={onSaveQueryFormSubmit}
|
handleSubmit={onSaveQueryFormSubmit}
|
||||||
onChangeFunc={onChangeQueryFormField}
|
onChangeFunc={onChangeQueryFormField}
|
||||||
onFetchTargets={onFetchTargets}
|
|
||||||
onOsqueryTableSelect={onOsqueryTableSelect}
|
onOsqueryTableSelect={onOsqueryTableSelect}
|
||||||
onRunQuery={onRunQuery}
|
onRunQuery={onRunQuery}
|
||||||
onStopQuery={onStopQuery}
|
onStopQuery={onStopQuery}
|
||||||
onTargetSelect={onTargetSelect}
|
|
||||||
onUpdate={onUpdateQuery}
|
onUpdate={onUpdateQuery}
|
||||||
queryIsRunning={queryIsRunning}
|
queryIsRunning={queryIsRunning}
|
||||||
selectedTargets={selectedTargets}
|
|
||||||
serverErrors={errors}
|
serverErrors={errors}
|
||||||
targetsCount={targetsCount}
|
|
||||||
targetsError={targetsError}
|
|
||||||
selectedOsqueryTable={selectedOsqueryTable}
|
selectedOsqueryTable={selectedOsqueryTable}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{renderTargetsInput()}
|
||||||
{renderResultsTable()}
|
{renderResultsTable()}
|
||||||
</div>
|
</div>
|
||||||
<QuerySidePanel
|
<QuerySidePanel
|
||||||
|
@ -11,7 +11,7 @@ import helpers from 'test/helpers';
|
|||||||
import hostActions from 'redux/nodes/entities/hosts/actions';
|
import hostActions from 'redux/nodes/entities/hosts/actions';
|
||||||
import queryActions from 'redux/nodes/entities/queries/actions';
|
import queryActions from 'redux/nodes/entities/queries/actions';
|
||||||
import ConnectedQueryPage, { QueryPage } from 'pages/queries/QueryPage/QueryPage';
|
import ConnectedQueryPage, { QueryPage } from 'pages/queries/QueryPage/QueryPage';
|
||||||
import { hostStub } from 'test/stubs';
|
import { hostStub, queryStub } from 'test/stubs';
|
||||||
|
|
||||||
const { connectedComponent, createAceSpy, fillInFormInput, reduxMockStore } = helpers;
|
const { connectedComponent, createAceSpy, fillInFormInput, reduxMockStore } = helpers;
|
||||||
const { defaultSelectedOsqueryTable } = queryPageActions;
|
const { defaultSelectedOsqueryTable } = queryPageActions;
|
||||||
@ -113,14 +113,14 @@ describe('QueryPage - component', () => {
|
|||||||
|
|
||||||
it('sets targetError in state when the query is run and there are no selected targets', () => {
|
it('sets targetError in state when the query is run and there are no selected targets', () => {
|
||||||
const page = mount(connectedComponent(ConnectedQueryPage, { mockStore, props: locationProp }));
|
const page = mount(connectedComponent(ConnectedQueryPage, { mockStore, props: locationProp }));
|
||||||
const form = page.find('QueryForm');
|
const QueryPageSelectTargets = page.find('QueryPageSelectTargets');
|
||||||
const runQueryBtn = form.find('.query-form__run-query-btn');
|
const runQueryBtn = page.find('.query-page-select-targets__run-query-btn');
|
||||||
|
|
||||||
expect(form.prop('targetsError')).toNotExist();
|
expect(QueryPageSelectTargets.prop('error')).toNotExist();
|
||||||
|
|
||||||
runQueryBtn.simulate('click');
|
runQueryBtn.simulate('click');
|
||||||
|
|
||||||
expect(form.prop('targetsError')).toEqual('You must select at least one target to run a query');
|
expect(QueryPageSelectTargets.prop('error')).toEqual('You must select at least one target to run a query');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls the onUpdateQuery prop when the query is updated', () => {
|
it('calls the onUpdateQuery prop when the query is updated', () => {
|
||||||
@ -165,11 +165,12 @@ describe('QueryPage - component', () => {
|
|||||||
describe('#componentWillReceiveProps', () => {
|
describe('#componentWillReceiveProps', () => {
|
||||||
it('resets selected targets and removed the campaign when the hostname changes', () => {
|
it('resets selected targets and removed the campaign when the hostname changes', () => {
|
||||||
const queryResult = { org_name: 'Kolide', org_url: 'https://kolide.co' };
|
const queryResult = { org_name: 'Kolide', org_url: 'https://kolide.co' };
|
||||||
const campaign = { id: 1, query_results: [queryResult] };
|
const campaign = { id: 1, query_results: [queryResult], hosts_count: { total: 1 } };
|
||||||
const props = {
|
const props = {
|
||||||
dispatch: noop,
|
dispatch: noop,
|
||||||
loadingQueries: false,
|
loadingQueries: false,
|
||||||
location: { pathname: '/queries/11' },
|
location: { pathname: '/queries/11' },
|
||||||
|
query: { query: 'select * from users' },
|
||||||
selectedOsqueryTable: defaultSelectedOsqueryTable,
|
selectedOsqueryTable: defaultSelectedOsqueryTable,
|
||||||
selectedTargets: [hostStub],
|
selectedTargets: [hostStub],
|
||||||
};
|
};
|
||||||
@ -203,7 +204,7 @@ describe('QueryPage - component', () => {
|
|||||||
};
|
};
|
||||||
const queryResultsCSV = convertToCSV([queryResult]);
|
const queryResultsCSV = convertToCSV([queryResult]);
|
||||||
const fileSaveSpy = spyOn(FileSave, 'saveAs');
|
const fileSaveSpy = spyOn(FileSave, 'saveAs');
|
||||||
const Page = mount(<QueryPage dispatch={noop} selectedOsqueryTable={defaultSelectedOsqueryTable} />);
|
const Page = mount(<QueryPage dispatch={noop} query={queryStub} selectedOsqueryTable={defaultSelectedOsqueryTable} />);
|
||||||
const filename = 'query_results.csv';
|
const filename = 'query_results.csv';
|
||||||
const fileStub = new global.window.File([queryResultsCSV], filename, { type: 'text/csv' });
|
const fileStub = new global.window.File([queryResultsCSV], filename, { type: 'text/csv' });
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
&__results {
|
&__results {
|
||||||
@include display(flex);
|
@include display(flex);
|
||||||
@include flex-grow(1);
|
@include flex-grow(1);
|
||||||
|
min-height: 400px;
|
||||||
|
|
||||||
&--loading {
|
&--loading {
|
||||||
@include align-items(center);
|
@include align-items(center);
|
||||||
|
@ -204,8 +204,36 @@ export const userStub = {
|
|||||||
username: 'gnardog',
|
username: 'gnardog',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const queryResultStub = {
|
||||||
|
description: 'root',
|
||||||
|
directory: '/root',
|
||||||
|
gid: '0',
|
||||||
|
gid_signed: '0',
|
||||||
|
groupname: 'root',
|
||||||
|
host_hostname: hostStub.hostname,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const campaignStub = {
|
||||||
|
hosts: [hostStub, { ...hostStub, id: 100 }],
|
||||||
|
hosts_count: {
|
||||||
|
failed: 0,
|
||||||
|
successful: 2,
|
||||||
|
total: 2,
|
||||||
|
},
|
||||||
|
id: 1,
|
||||||
|
query_id: queryStub.id,
|
||||||
|
query_results: [queryResultStub],
|
||||||
|
totals: {
|
||||||
|
count: 2,
|
||||||
|
missing_in_action: 0,
|
||||||
|
offline: 0,
|
||||||
|
online: 2,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
adminUserStub,
|
adminUserStub,
|
||||||
|
campaignStub,
|
||||||
configStub,
|
configStub,
|
||||||
flatConfigStub,
|
flatConfigStub,
|
||||||
hostStub,
|
hostStub,
|
||||||
|
Loading…
Reference in New Issue
Block a user