@mongodb-js/compass-query-bar
Version:
Renders a component for executing MongoDB queries through a GUI.
381 lines (329 loc) • 10.2 kB
JSX
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import {
includes,
isFunction,
pick,
isEqual,
isString,
isArray,
map,
noop
} from 'lodash';
import QueryOption from 'components/query-option';
import OptionsToggle from 'components/options-toggle';
import QUERY_PROPERTIES from 'constants/query-properties';
import styles from './query-bar.less';
const OPTION_DEFINITION = {
filter: {
type: 'document',
placeholder: "{ field: 'value' }",
link: 'https://docs.mongodb.com/getting-started/shell/query/'
},
project: {
type: 'document',
placeholder: '{ field: 0 }',
link:
'https://docs.mongodb.com/manual/tutorial/project-fields-from-query-results/'
},
sort: {
type: 'document',
placeholder: '{ field: -1 }',
link: 'https://docs.mongodb.com/manual/reference/method/cursor.sort/'
},
skip: {
type: 'numeric',
placeholder: '0',
link: 'https://docs.mongodb.com/manual/reference/method/cursor.skip/'
},
limit: {
type: 'numeric',
placeholder: '0',
link: 'https://docs.mongodb.com/manual/reference/method/cursor.limit/'
},
sample: {
type: 'boolean',
placeholder: null,
link: 'https://docs.mongodb.com/TBD'
}
};
class QueryBar extends Component {
static displayName = 'QueryBar';
static propTypes = {
filter: PropTypes.object,
project: PropTypes.object,
sort: PropTypes.object,
skip: PropTypes.number,
limit: PropTypes.number,
sample: PropTypes.bool,
valid: PropTypes.bool,
filterValid: PropTypes.bool,
projectValid: PropTypes.bool,
sortValid: PropTypes.bool,
skipValid: PropTypes.bool,
limitValid: PropTypes.bool,
featureFlag: PropTypes.bool,
autoPopulated: PropTypes.bool,
filterString: PropTypes.string,
projectString: PropTypes.string,
sortString: PropTypes.string,
skipString: PropTypes.string,
limitString: PropTypes.string,
actions: PropTypes.object,
buttonLabel: PropTypes.string,
queryState: PropTypes.string,
layout: PropTypes.array,
expanded: PropTypes.bool,
lastExecutedQuery: PropTypes.object,
onReset: PropTypes.func,
onApply: PropTypes.func,
schemaFields: PropTypes.object
};
static defaultProps = {
expanded: false,
buttonLabel: 'Apply',
layout: ['filter', 'project', ['sort', 'skip', 'limit']],
schemaFields: {}
};
state = {
hasFocus: false,
schemaFields: {}
}
componentDidMount() {
this._renderShowQueryHistoryButton = noop;
if (global.hadronApp && global.hadronApp.appRegistry) { // Unit tests don't have appRegistry
this._renderShowQueryHistoryButton = global.hadronApp.appRegistry.getComponent('QueryHistory.ShowQueryHistoryButton');
this.QueryHistoryActions = global.hadronApp.appRegistry.getAction('QueryHistory.Actions');
const fieldStore = global.hadronApp.appRegistry.getStore('Field.Store');
this.unsubscribeFieldStore = fieldStore.listen(this.onFieldsChanged);
}
}
componentWillUnmount() {
this.unsubscribeFieldStore();
if (this.QueryHistoryActions) {
this.QueryHistoryActions.collapse();
}
}
onFieldsChanged = (state) => {
this.setState({ schemaFields: state.fields });
};
onChange(label, evt) {
const type = OPTION_DEFINITION[label].type;
const { actions } = this.props;
if (includes(['numeric', 'document'], type)) {
return actions.typeQueryString(label, evt.target.value);
}
if (type === 'boolean') {
// there is only one boolean toggle: sample
return actions.toggleSample(evt.target.checked);
}
}
onApplyButtonClicked = (evt) => {
evt.preventDefault();
evt.stopPropagation();
const { actions, valid, featureFlag, onApply } = this.props;
if (valid || featureFlag) {
actions.apply();
if (isFunction(onApply)) {
onApply();
}
}
};
onResetButtonClicked = () => {
const { actions, onReset } = this.props;
actions.reset();
if (isFunction(onReset)) {
onReset();
}
};
_onFocus = () => {
this.setState({ hasFocus: true });
};
_onBlur = () => {
this.setState({ hasFocus: false });
};
_showToggle() {
return this.props.layout.length > 1;
}
_queryHasChanges() {
const query = pick(this.props, QUERY_PROPERTIES);
return !isEqual(query, this.props.lastExecutedQuery);
}
/**
* renders the options toggle button in the top right corner, if the layout
* is multi-line.
*
* @returns {Component|null} the toggle component or null.
*/
renderToggle() {
const { expanded, actions } = this.props;
return this._showToggle()
? <OptionsToggle expanded={expanded} actions={actions} />
: null;
}
/**
* renders a single query option, either as its own row, or as part of a
* option group.
*
* @param {String} option the option name to render
* @param {Number} id the option number
* @param {Boolean} hasToggle this option contains the expand toggle
*
* @return {Component} the option component
*/
renderOption(option, id, hasToggle) {
const { filterValid, featureFlag, autoPopulated } = this.props;
// for filter only, also validate feature flag directives
const hasError = option === 'filter'
? !(filterValid || featureFlag)
: !this.props[`${option}Valid`];
// checkbox options use the value directly, text inputs use the
// `<option>String` prop.
const value = OPTION_DEFINITION[option].type === 'boolean' ?
this.props[option] : this.props[`${option}String`];
return (
<QueryOption
label={option}
autoPopulated={autoPopulated}
hasToggle={hasToggle}
hasError={hasError}
key={`query-option-${id}`}
value={value}
actions={this.props.actions}
placeholder={OPTION_DEFINITION[option].placeholder}
link={OPTION_DEFINITION[option].link}
inputType={OPTION_DEFINITION[option].type}
onChange={this.onChange.bind(this, option)}
schemaFields={this.state.schemaFields}
/>
);
}
/**
* renders a group of several query options, that are placed horizontally
* in the same row.
*
* @param {Array} group The group array, e.g. ['sort', 'skip', 'limit']
* @param {Number} id The group number
*
* @returns {Component} The group component
*/
renderOptionGroup(group, id) {
const options = map(group, (option, i) => {
return this.renderOption(option, i, false);
});
return (
<div
className={classnames(styles['option-group'])}
key={`option-group-${id}`}>
{options}
</div>
);
}
/**
* renders the rows of the querybar component
*
* @return {Fragment} array of components, one for each row.
*/
renderOptionRows() {
const { layout, expanded } = this.props;
// for multi-line layouts, the first option must be stand-alone
if (this._showToggle() && !isString(layout[0])) {
throw new Error(`First item in multi-line layout must be single option, found ${layout[0]}`);
}
const rows = map(layout, (row, id) => {
// only the first in multi-line options has the toggle
const hasToggle = id === 0 && this._showToggle();
if (isString(row)) {
return this.renderOption(row, id, hasToggle);
} else if (isArray(row)) {
return this.renderOptionGroup(row, id);
}
throw new Error('Layout items must be string or array, found ' + row);
});
if (expanded) {
return rows;
}
return rows.slice(0, 1);
}
/**
* Render Query Bar input form (just the input fields and buttons).
*
* @returns {React.Component} The Query Bar view.
*/
renderForm = () => {
const { valid, featureFlag, queryState, buttonLabel } = this.props;
const { hasFocus } = this.state;
const _inputGroupClassName = classnames(
styles['input-group'],
{ ['has-error']: !valid },
{ ['is-feature-flag']: featureFlag }
);
const applyDisabled = !(valid || featureFlag);
const _queryOptionClassName = classnames(
styles['option-container'],
{ [ styles['has-focus'] ]: hasFocus }
);
const _applyButtonClassName = classnames(
'btn',
'btn-primary',
'btn-sm',
styles['apply-button']
);
const _resetButtonClassName = classnames(
'btn',
'btn-default',
'btn-sm',
styles['reset-button'],
{ [ styles['is-visible'] ]: queryState === 'apply'}
);
return (
<form onSubmit={this.onApplyButtonClicked}>
<div className={_inputGroupClassName}>
<div
onBlur={this._onBlur}
onFocus={this._onFocus}
className={_queryOptionClassName}>
{this.renderOptionRows()}
{this.renderToggle()}
</div>
<div className={classnames(styles['button-group'])}>
<button
data-test-id="query-bar-apply-filter-button"
key="apply-button"
className={_applyButtonClassName}
type="button"
onClick={this.onApplyButtonClicked}
disabled={applyDisabled}>
{buttonLabel}
</button>
<button
data-test-id="query-bar-reset-filter-button"
key="reset-button"
className={_resetButtonClassName}
type="button"
onClick={this.onResetButtonClicked}>
Reset
</button>
{this._renderShowQueryHistoryButton}
</div>
</div>
</form>
);
}
render() {
return (
<div className={classnames(styles.component)}>
<div className={classnames(styles['input-container'])}>
<div className={classnames('row')}>
<div className={classnames('col-md-12')}>
{this.renderForm()}
</div>
</div>
</div>
</div>
);
}
}
export default QueryBar;
export { QueryBar };