Create packs (#516)
* Creates new PackComposerPage at /packs/new * Creates PackForm component * Adds PackForm to PackComposerPage * Creates QueriesListItem * Creates QueriesList * Creates QueriesListWrapper * Get all queries when the Packs Composer Page loads * Form HOC handles updates to formData prop * Creates form to configure scheduled queries * QueriesListWrapper renders ConfigurePackQueryForm * search queries input filters queries list * Empty state text * create pack when user submits the new pack form * Adds Edit pack page to /packs/:pack_id/edit * API client - get scheduled queries for a pack * API client - create scheduled query * Redux config for scheduled queries * Remove scheduled queries from packs * Add labels to pack on create * Add disabled state to the select targets dropdown * Adds edit route and pushes to new route on edit click * Adds cancel button to edit pack form * Adds Checkbox that selects all scheduled queries in table
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
|
||||
import Icon from 'components/Icon';
|
||||
import Button from 'components/buttons/Button';
|
||||
import Dropdown from 'components/forms/fields/Dropdown';
|
||||
import Form from 'components/forms/Form';
|
||||
import formFieldInterface from 'interfaces/form_field';
|
||||
import InputField from 'components/forms/fields/InputField';
|
||||
import validate from 'components/forms/ConfigurePackQueryForm/validate';
|
||||
|
||||
const baseClass = 'configure-pack-query-form';
|
||||
const fieldNames = ['query_id', 'interval', 'logging_type', 'platform', 'version'];
|
||||
const platformOptions = [
|
||||
{ label: 'All', value: 'all' },
|
||||
{ label: 'Windows', value: 'windows' },
|
||||
{ label: 'Linux', value: 'linux' },
|
||||
{ label: 'macOS', value: 'darwin' },
|
||||
];
|
||||
const loggingTypeOptions = [
|
||||
{ label: 'Differential', value: 'differential' },
|
||||
{ label: 'Differential (Ignore Removals)', value: 'differential_ignore_removals' },
|
||||
{ label: 'Snapshot', value: 'snapshot' },
|
||||
];
|
||||
const minOsqueryVersionOptions = [
|
||||
{ label: 'All', value: 'all' },
|
||||
{ label: '1.8.1 +', value: '1.8.1' },
|
||||
{ label: '1.8.2 +', value: '1.8.2' },
|
||||
{ label: '2.0.0 +', value: '2.0.0' },
|
||||
{ label: '2.1.1 +', value: '2.1.1' },
|
||||
{ label: '2.1.2 +', value: '2.1.2' },
|
||||
{ label: '2.2.0 +', value: '2.2.0' },
|
||||
{ label: '2.2.1 +', value: '2.2.1' },
|
||||
];
|
||||
|
||||
class ConfigurePackQueryForm extends Component {
|
||||
static propTypes = {
|
||||
fields: PropTypes.shape({
|
||||
interval: formFieldInterface.isRequired,
|
||||
logging_type: formFieldInterface.isRequired,
|
||||
platform: formFieldInterface.isRequired,
|
||||
version: formFieldInterface.isRequired,
|
||||
}).isRequired,
|
||||
handleSubmit: PropTypes.func,
|
||||
onCancel: PropTypes.func,
|
||||
};
|
||||
|
||||
onCancel = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
const { onCancel: handleCancel } = this.props;
|
||||
|
||||
return handleCancel();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { fields, handleSubmit } = this.props;
|
||||
const { onCancel } = this;
|
||||
|
||||
return (
|
||||
<form className={baseClass} onSubmit={handleSubmit}>
|
||||
<h2 className={`${baseClass}__title`}>configuration</h2>
|
||||
<div className={`${baseClass}__fields`}>
|
||||
<InputField
|
||||
{...fields.interval}
|
||||
inputWrapperClass={`${baseClass}__form-field ${baseClass}__form-field--interval`}
|
||||
placeholder="- - -"
|
||||
label="Interval"
|
||||
hint="Seconds"
|
||||
type="number"
|
||||
/>
|
||||
<Dropdown
|
||||
{...fields.platform}
|
||||
options={platformOptions}
|
||||
placeholder="- - -"
|
||||
label="Platform"
|
||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--platform`}
|
||||
/>
|
||||
<Dropdown
|
||||
{...fields.version}
|
||||
options={minOsqueryVersionOptions}
|
||||
placeholder="- - -"
|
||||
label={[<Icon name="osquery" key="min-osquery-vers" />, ' ver.']}
|
||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--osquer-vers`}
|
||||
/>
|
||||
<Dropdown
|
||||
{...fields.logging_type}
|
||||
options={loggingTypeOptions}
|
||||
placeholder="- - -"
|
||||
label="Logging"
|
||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--logging`}
|
||||
/>
|
||||
<div className={`${baseClass}__btn-wrapper`}>
|
||||
<Button
|
||||
className={`${baseClass}__cancel-btn`}
|
||||
onClick={onCancel}
|
||||
text="Cancel"
|
||||
variant="inverse"
|
||||
/>
|
||||
<Button
|
||||
className={`${baseClass}__submit-btn`}
|
||||
text="Save"
|
||||
type="submit"
|
||||
variant="brand"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form(ConfigurePackQueryForm, {
|
||||
fields: fieldNames,
|
||||
validate,
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import expect, { createSpy, restoreSpies } from 'expect';
|
||||
import { mount } from 'enzyme';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
import ConfigurePackQueryForm from 'components/forms/ConfigurePackQueryForm';
|
||||
import { itBehavesLikeAFormDropdownElement, itBehavesLikeAFormInputElement } from 'test/helpers';
|
||||
|
||||
describe('ConfigurePackQueryForm - component', () => {
|
||||
afterEach(restoreSpies);
|
||||
|
||||
describe('form fields', () => {
|
||||
const form = mount(
|
||||
<ConfigurePackQueryForm
|
||||
handleSubmit={noop}
|
||||
/>
|
||||
);
|
||||
|
||||
it('updates form state', () => {
|
||||
itBehavesLikeAFormInputElement(form, 'interval');
|
||||
itBehavesLikeAFormDropdownElement(form, 'logging_type');
|
||||
itBehavesLikeAFormDropdownElement(form, 'platform');
|
||||
itBehavesLikeAFormDropdownElement(form, 'version');
|
||||
});
|
||||
});
|
||||
|
||||
describe('submitting the form', () => {
|
||||
const spy = createSpy();
|
||||
const form = mount(
|
||||
<ConfigurePackQueryForm
|
||||
handleSubmit={spy}
|
||||
formData={{ query_id: 1 }}
|
||||
/>
|
||||
);
|
||||
|
||||
it('submits the form with the form data', () => {
|
||||
itBehavesLikeAFormInputElement(form, 'interval', 'InputField', 123);
|
||||
itBehavesLikeAFormDropdownElement(form, 'logging_type');
|
||||
itBehavesLikeAFormDropdownElement(form, 'platform');
|
||||
itBehavesLikeAFormDropdownElement(form, 'version');
|
||||
|
||||
form.find('form').simulate('submit');
|
||||
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
interval: 123,
|
||||
logging_type: 'differential',
|
||||
platform: 'all',
|
||||
query_id: 1,
|
||||
version: 'all',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
.configure-pack-query-form {
|
||||
.form-field {
|
||||
.input-field,
|
||||
.dropdown__select {
|
||||
width: 175px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
@at-root .configure-pack-query-form__form-field--interval {
|
||||
.input-field {
|
||||
padding-right: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 16px;
|
||||
font-weight: $bold;
|
||||
letter-spacing: -0.5px;
|
||||
color: $text-dark;
|
||||
border-bottom: 1px solid #eaeefb;
|
||||
padding: 0 0 9px;
|
||||
margin: 0 0 9px;
|
||||
}
|
||||
|
||||
&__fields {
|
||||
.form-field {
|
||||
position: relative;
|
||||
|
||||
&__label {
|
||||
font-size: 16px;
|
||||
font-weight: $bold;
|
||||
line-height: 40px;
|
||||
letter-spacing: -0.5px;
|
||||
color: $text-medium;
|
||||
display: inline-block;
|
||||
width: 70px;
|
||||
text-transform: lowercase;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
&__hint {
|
||||
position: absolute;
|
||||
bottom: 13px;
|
||||
right: 10px;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
}
|
||||
|
||||
.kolidecon {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
&__btn-wrapper {
|
||||
padding-top: 20px;
|
||||
|
||||
.button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export default from './ConfigurePackQueryForm';
|
||||
@@ -0,0 +1,33 @@
|
||||
import { size } from 'lodash';
|
||||
|
||||
import validateNumericality from 'components/forms/validators/validate_numericality';
|
||||
|
||||
const validate = (formData) => {
|
||||
const errors = {};
|
||||
|
||||
if (!formData.query_id) {
|
||||
errors.query_id = 'A query must be selected';
|
||||
}
|
||||
|
||||
if (!formData.interval) {
|
||||
errors.interval = 'Interval must be present';
|
||||
}
|
||||
|
||||
if (formData.interval && !validateNumericality(formData.interval)) {
|
||||
errors.interval = 'Interval must be a number';
|
||||
}
|
||||
|
||||
if (!formData.platform) {
|
||||
errors.platform = 'A platform must be selected';
|
||||
}
|
||||
|
||||
if (!formData.logging_type) {
|
||||
errors.logging_type = 'A Logging Type must be selected';
|
||||
}
|
||||
|
||||
const valid = !size(errors);
|
||||
|
||||
return { valid, errors };
|
||||
};
|
||||
|
||||
export default validate;
|
||||
Reference in New Issue
Block a user