Improves re-usability of the SelectTargetsDropdown (#518)

* Improves re-usability of the SelectTargetsDropdown
This commit is contained in:
Mike Stone
2016-11-21 10:38:23 -05:00
committed by GitHub
parent 56195b0768
commit 8bb2a39d86
11 changed files with 211 additions and 207 deletions
@@ -0,0 +1,130 @@
import React, { Component, PropTypes } from 'react';
import { isEqual, noop } from 'lodash';
import Kolide from 'kolide';
import targetInterface from 'interfaces/target';
import { formatSelectedTargetsForApi } from './helpers';
import Input from './SelectTargetsInput';
import Menu from './SelectTargetsMenu';
class SelectTargetsDropdown extends Component {
static propTypes = {
onFetchTargets: PropTypes.func,
onSelect: PropTypes.func.isRequired,
selectedTargets: PropTypes.arrayOf(targetInterface),
};
static defaultProps = {
onFetchTargets: noop,
};
constructor (props) {
super(props);
this.state = {
isLoadingTargets: false,
moreInfoTarget: null,
query: '',
targets: [],
};
}
componentDidMount () {
this.fetchTargets();
return false;
}
componentWillReceiveProps (nextProps) {
const { selectedTargets } = nextProps;
const { query } = this.state;
if (!isEqual(selectedTargets, this.props.selectedTargets)) {
this.fetchTargets(query, selectedTargets);
}
}
onInputClose = () => {
this.setState({ moreInfoTarget: null, query: '' });
return false;
}
onTargetSelectMoreInfo = (moreInfoTarget) => {
return (evt) => {
evt.preventDefault();
const currentMoreInfoTarget = this.state.moreInfoTarget || {};
if (isEqual(moreInfoTarget.display_text, currentMoreInfoTarget.display_text)) {
this.setState({ moreInfoTarget: null });
return false;
}
const { target_type: targetType } = moreInfoTarget;
if (targetType.toLowerCase() === 'labels') {
return Kolide.getLabelHosts(moreInfoTarget.id)
.then((hosts) => {
this.setState({
moreInfoTarget: { ...moreInfoTarget, hosts },
});
return false;
});
}
this.setState({ moreInfoTarget });
return false;
};
}
fetchTargets = (query, selectedTargets = this.props.selectedTargets) => {
const { onFetchTargets } = this.props;
this.setState({ isLoadingTargets: true, query });
return Kolide.getTargets(query, formatSelectedTargetsForApi(selectedTargets))
.then((response) => {
const {
targets,
} = response;
onFetchTargets(query, response);
this.setState({ isLoadingTargets: false, targets });
return query;
})
.catch((error) => {
this.setState({ isLoadingTargets: false });
throw error;
});
}
render () {
const { isLoadingTargets, moreInfoTarget, targets } = this.state;
const { fetchTargets, onInputClose, onTargetSelectMoreInfo } = this;
const { onSelect, selectedTargets } = this.props;
const menuRenderer = Menu(onTargetSelectMoreInfo, moreInfoTarget);
return (
<Input
isLoading={isLoadingTargets}
menuRenderer={menuRenderer}
onClose={onInputClose}
onTargetSelect={onSelect}
onTargetSelectInputChange={fetchTargets}
onInputChange={fetchTargets}
selectedTargets={selectedTargets}
targets={targets}
/>
);
}
}
export default SelectTargetsDropdown;
@@ -9,7 +9,7 @@ class SelectTargetsInput extends Component {
static propTypes = {
isLoading: PropTypes.bool,
menuRenderer: PropTypes.func,
onCloseTargetSelect: PropTypes.func,
onClose: PropTypes.func,
onTargetSelect: PropTypes.func,
onTargetSelectInputChange: PropTypes.func,
selectedTargets: PropTypes.arrayOf(targetInterface),
@@ -26,7 +26,7 @@ class SelectTargetsInput extends Component {
const {
isLoading,
menuRenderer,
onCloseTargetSelect,
onClose,
onTargetSelect,
onTargetSelectInputChange,
selectedTargets,
@@ -44,7 +44,7 @@ class SelectTargetsInput extends Component {
name="targets"
options={targets}
onChange={onTargetSelect}
onClose={onCloseTargetSelect}
onClose={onClose}
onInputChange={onTargetSelectInputChange}
placeholder="Label Name, Host Name, IP Address, etc."
resetValue={[]}
@@ -3,10 +3,11 @@ import classNames from 'classnames';
import { filter, includes, isEqual, noop } from 'lodash';
import targetInterface from 'interfaces/target';
import SelectTargets from 'components/forms/fields/SelectTargetsDropdown';
import TargetDetails from '../TargetDetails';
import TargetOption from '../TargetOption';
const SelectTargetsMenuWrapper = (onMoreInfoClick, onRemoveMoreInfoTarget, moreInfoTarget) => {
const SelectTargetsMenuWrapper = (onMoreInfoClick, moreInfoTarget) => {
const SelectTargetsMenu = ({
focusedOption,
instancePrefix,
@@ -49,10 +50,9 @@ const SelectTargetsMenuWrapper = (onMoreInfoClick, onRemoveMoreInfoTarget, moreI
optionIndex={index}
ref={setRef}
>
<SelectTargets.Option
<TargetOption
target={target}
onSelect={onSelect}
onRemoveMoreInfoTarget={onRemoveMoreInfoTarget}
onMoreInfoClick={onMoreInfoClick}
/>
</Option>
@@ -68,7 +68,7 @@ const SelectTargetsMenuWrapper = (onMoreInfoClick, onRemoveMoreInfoTarget, moreI
<div>labels</div>
{renderTargets('labels')}
</div>
<SelectTargets.Details target={moreInfoTarget} />
<TargetDetails target={moreInfoTarget} />
</div>
);
};
@@ -8,19 +8,10 @@ const baseClass = 'target-option';
class TargetOption extends Component {
static propTypes = {
onMoreInfoClick: PropTypes.func,
onRemoveMoreInfoTarget: PropTypes.func,
onSelect: PropTypes.func,
target: targetInterface.isRequired,
};
handleRemoveMoreInfoTarget = (evt) => {
evt.preventDefault();
const { onRemoveMoreInfoTarget } = this.props;
return onRemoveMoreInfoTarget();
}
handleSelect = (evt) => {
const { onSelect, target } = this.props;
@@ -0,0 +1,17 @@
import { flatMap } from 'lodash';
const filterTarget = (targetType) => {
return (target) => {
return target.target_type === targetType ? [target.id] : [];
};
};
export const formatSelectedTargetsForApi = (selectedTargets) => {
const targets = selectedTargets || [];
const hosts = flatMap(targets, filterTarget('hosts'));
const labels = flatMap(targets, filterTarget('labels'));
return { hosts, labels };
};
export default { formatSelectedTargetsForApi };
@@ -0,0 +1,23 @@
import expect from 'expect';
import helpers from './helpers';
const label1 = { id: 1, target_type: 'labels' };
const label2 = { id: 2, target_type: 'labels' };
const host1 = { id: 6, target_type: 'hosts' };
const host2 = { id: 5, target_type: 'hosts' };
describe('SelectTargetsDropdown - helpers', () => {
describe('#formatSelectedTargetsForApi', () => {
const { formatSelectedTargetsForApi } = helpers;
it('splits targets into labels and hosts', () => {
const targets = [host1, host2, label1, label2];
expect(formatSelectedTargetsForApi(targets)).toEqual({
hosts: [6, 5],
labels: [1, 2],
});
});
});
});
@@ -1,11 +1 @@
import Details from './TargetDetails';
import Input from './SelectTargetsInput';
import Option from './TargetOption';
import Menu from './SelectTargetsMenu';
export default {
Details,
Input,
Menu,
Option,
};
export default from './SelectTargetsDropdown';
@@ -5,7 +5,7 @@ import 'brace/ext/linking';
import QueryForm from 'components/forms/queries/QueryForm';
import queryInterface from 'interfaces/query';
import SelectTargets from 'components/forms/fields/SelectTargetsDropdown';
import SelectTargetsDropdown from 'components/forms/fields/SelectTargetsDropdown';
import targetInterface from 'interfaces/target';
import './mode';
import './theme';
@@ -14,24 +14,18 @@ const baseClass = 'query-composer';
class QueryComposer extends Component {
static propTypes = {
isLoadingTargets: PropTypes.bool,
moreInfoTarget: targetInterface,
onCloseTargetSelect: PropTypes.func,
onCancel: PropTypes.func,
onFetchTargets: PropTypes.func,
onFormCancel: PropTypes.func,
onOsqueryTableSelect: PropTypes.func,
onRemoveMoreInfoTarget: PropTypes.func,
onRunQuery: PropTypes.func,
onSave: PropTypes.func,
onTargetSelect: PropTypes.func,
onTargetSelectInputChange: PropTypes.func,
onTargetSelectMoreInfo: PropTypes.func,
onTextEditorInputChange: PropTypes.func,
onUpdate: PropTypes.func,
query: queryInterface,
queryType: PropTypes.string,
selectedTargets: PropTypes.arrayOf(targetInterface),
selectedTargetsCount: PropTypes.number,
targets: PropTypes.arrayOf(targetInterface),
queryText: PropTypes.string,
};
@@ -40,14 +34,6 @@ class QueryComposer extends Component {
selectedTargetsCount: 0,
};
constructor (props) {
super(props);
this.state = {
isSaveQueryForm: false,
};
}
onLoad = (editor) => {
editor.setOptions({
enableLinking: true,
@@ -65,23 +51,9 @@ class QueryComposer extends Component {
});
}
onLoadSaveQueryModal = () => {
this.setState({ isSaveQueryForm: true });
return false;
}
onSaveQueryFormCancel = (evt) => {
evt.preventDefault();
this.setState({ isSaveQueryForm: false });
return false;
}
renderForm = () => {
const {
onCancel,
onFormCancel,
onRunQuery,
onSave,
onUpdate,
@@ -92,7 +64,7 @@ class QueryComposer extends Component {
return (
<QueryForm
onCancel={onCancel}
onCancel={onFormCancel}
onRunQuery={onRunQuery}
onSave={onSave}
onUpdate={onUpdate}
@@ -105,24 +77,17 @@ class QueryComposer extends Component {
renderTargetsInput = () => {
const {
isLoadingTargets,
moreInfoTarget,
onCloseTargetSelect,
onRemoveMoreInfoTarget,
onFetchTargets,
onTargetSelect,
onTargetSelectInputChange,
onTargetSelectMoreInfo,
queryType,
selectedTargets,
selectedTargetsCount,
targets,
} = this.props;
if (queryType === 'label') {
return false;
}
const menuRenderer = SelectTargets.Menu(onTargetSelectMoreInfo, onRemoveMoreInfoTarget, moreInfoTarget);
return (
<div>
@@ -130,14 +95,10 @@ class QueryComposer extends Component {
<span className={`${baseClass}__select-targets`}>Select Targets</span>
<span className={`${baseClass}__targets-count`}> {selectedTargetsCount} unique hosts</span>
</p>
<SelectTargets.Input
isLoading={isLoadingTargets}
menuRenderer={menuRenderer}
onCloseTargetSelect={onCloseTargetSelect}
onTargetSelect={onTargetSelect}
onTargetSelectInputChange={onTargetSelectInputChange}
<SelectTargetsDropdown
onFetchTargets={onFetchTargets}
onSelect={onTargetSelect}
selectedTargets={selectedTargets}
targets={targets}
/>
</div>
);
@@ -114,20 +114,4 @@ describe('QueryComposer - component', () => {
query: query.query,
});
});
it('calls onTargetSelectInputChange when changing the select target input text', () => {
const onTargetSelectInputChangeSpy = createSpy();
const component = mount(
<QueryComposer
onTargetSelectInputChange={onTargetSelectInputChangeSpy}
selectedTargets={[]}
queryText="SELECT * FROM users"
/>
);
const selectTargetsInput = component.find('.Select-input input');
fillInFormInput(selectTargetsInput, 'my target');
expect(onTargetSelectInputChangeSpy).toHaveBeenCalledWith('my target');
});
});
@@ -203,7 +203,7 @@ export class ManageHostsPage extends Component {
return (
<QueryComposer
key="query-composer"
onCancel={onCancelAddLabel}
onFormCancel={onCancelAddLabel}
onSave={onSaveAddLabel}
onTextEditorInputChange={onTextEditorInputChange}
queryType="label"
+22 -114
View File
@@ -1,11 +1,9 @@
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';
import entityGetter from 'redux/utilities/entityGetter';
import Kolide from 'kolide';
import QueryComposer from 'components/queries/QueryComposer';
import osqueryTableInterface from 'interfaces/osquery_table';
import queryActions from 'redux/nodes/entities/queries/actions';
@@ -24,9 +22,16 @@ class QueryPage extends Component {
queryText: PropTypes.string,
selectedOsqueryTable: osqueryTableInterface,
selectedTargets: PropTypes.arrayOf(targetInterface),
selectedTargetsQuery: PropTypes.string,
};
constructor (props) {
super(props);
this.state = {
selectedTargetsCount: 0,
};
}
componentWillMount () {
const { dispatch, query } = this.props;
@@ -34,21 +39,13 @@ class QueryPage extends Component {
dispatch(setQueryText(query.query));
}
this.state = {
isLoadingTargets: false,
moreInfoTarget: null,
selectedTargetsCount: 0,
targets: [],
};
dispatch(showRightSidePanel);
this.fetchTargets();
return false;
}
componentWillReceiveProps (nextProps) {
const { dispatch, query: newQuery, selectedTargets, selectedTargetsQuery } = nextProps;
const { dispatch, query: newQuery } = nextProps;
const { query: oldQuery } = this.props;
if ((!oldQuery && newQuery) || (oldQuery && oldQuery.query !== newQuery.query)) {
@@ -57,10 +54,6 @@ class QueryPage extends Component {
dispatch(setQueryText(queryText));
}
if (!isEqual(selectedTargets, this.props.selectedTargets)) {
this.fetchTargets(selectedTargetsQuery, selectedTargets);
}
return false;
}
@@ -72,8 +65,14 @@ class QueryPage extends Component {
return false;
}
onCloseTargetSelect = () => {
this.onRemoveMoreInfoTarget();
onFetchTargets = (query, targetResponse) => {
const { dispatch } = this.props;
const {
selected_targets_count: selectedTargetsCount,
} = targetResponse;
dispatch(setSelectedTargetsQuery(query));
this.setState({ selectedTargetsCount });
return false;
}
@@ -86,12 +85,6 @@ class QueryPage extends Component {
return false;
}
onRemoveMoreInfoTarget = () => {
this.setState({ moreInfoTarget: null });
return false;
}
onRunQuery = debounce((evt) => {
evt.preventDefault();
@@ -140,41 +133,6 @@ class QueryPage extends Component {
return false;
}
onTargetSelectMoreInfo = (moreInfoTarget) => {
return (evt) => {
evt.preventDefault();
const currentMoreInfoTarget = this.state.moreInfoTarget || {};
if (isEqual(moreInfoTarget.display_text, currentMoreInfoTarget.display_text)) {
this.setState({ moreInfoTarget: null });
return false;
}
const { target_type: targetType } = moreInfoTarget;
if (targetType.toLowerCase() === 'labels') {
return Kolide.getLabelHosts(moreInfoTarget.id)
.then((hosts) => {
this.setState({
moreInfoTarget: {
...moreInfoTarget,
hosts,
},
});
return false;
});
}
this.setState({ moreInfoTarget });
return false;
};
}
onTextEditorInputChange = (queryText) => {
const { dispatch } = this.props;
@@ -194,61 +152,17 @@ class QueryPage extends Component {
return false;
};
fetchTargets = (query, selectedTargets = this.props.selectedTargets) => {
const { dispatch } = this.props;
this.setState({ isLoadingTargets: true });
dispatch(setSelectedTargetsQuery(query));
const hosts = flatMap(selectedTargets, (target) => {
return target.target_type === 'hosts' ? [target.id] : [];
});
const labels = flatMap(selectedTargets, (target) => {
return target.target_type === 'labels' ? [target.id] : [];
});
const selected = { hosts, labels };
return Kolide.getTargets(query, selected)
.then((response) => {
const {
selected_targets_count: selectedTargetsCount,
targets,
} = response;
this.setState({
isLoadingTargets: false,
selectedTargetsCount,
targets,
});
return query;
})
.catch((error) => {
this.setState({ isLoadingTargets: false });
throw error;
});
}
render () {
const {
fetchTargets,
onCloseTargetSelect,
onFetchTargets,
onOsqueryTableSelect,
onRemoveMoreInfoTarget,
onRunQuery,
onSaveQueryFormSubmit,
onTargetSelect,
onTargetSelectMoreInfo,
onTextEditorInputChange,
onUpdateQuery,
} = this;
const {
isLoadingTargets,
moreInfoTarget,
selectedTargetsCount,
targets,
} = this.state;
const { selectedTargetsCount } = this.state;
const {
query,
queryText,
@@ -259,23 +173,17 @@ class QueryPage extends Component {
return (
<div>
<QueryComposer
isLoadingTargets={isLoadingTargets}
moreInfoTarget={moreInfoTarget}
onCloseTargetSelect={onCloseTargetSelect}
onFetchTargets={onFetchTargets}
onOsqueryTableSelect={onOsqueryTableSelect}
onRemoveMoreInfoTarget={onRemoveMoreInfoTarget}
onRunQuery={onRunQuery}
onSave={onSaveQueryFormSubmit}
onTargetSelect={onTargetSelect}
onTargetSelectInputChange={fetchTargets}
onTargetSelectMoreInfo={onTargetSelectMoreInfo}
onTextEditorInputChange={onTextEditorInputChange}
onUpdate={onUpdateQuery}
query={query}
selectedTargets={selectedTargets}
selectedTargetsCount={selectedTargetsCount}
selectedOsqueryTable={selectedOsqueryTable}
targets={targets}
queryText={queryText}
/>
<QuerySidePanel
@@ -291,9 +199,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, selectedTargetsQuery } = state.components.QueryPages;
const { queryText, selectedOsqueryTable, selectedTargets } = state.components.QueryPages;
return { query, queryText, selectedOsqueryTable, selectedTargets, selectedTargetsQuery };
return { query, queryText, selectedOsqueryTable, selectedTargets };
};
export default connect(mapStateToProps)(QueryPage);