UNPKG

@mongodb-js/compass-query-bar

Version:

Renders a component for executing MongoDB queries through a GUI.

381 lines (329 loc) 10.2 kB
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 };