diff --git a/client/.eslintrc.js b/client/.eslintrc.js index 525d82292b..73caba79e3 100644 --- a/client/.eslintrc.js +++ b/client/.eslintrc.js @@ -18,6 +18,7 @@ module.exports = { 'no-param-reassign': 0, 'no-mixed-operators': 0, 'no-underscore-dangle': 0, + "no-use-before-define": ["error", "nofunc"], "prefer-destructuring": "off", "prefer-template": "off", "no-restricted-properties": "off", diff --git a/client/app/components/QuerySelector.jsx b/client/app/components/QuerySelector.jsx new file mode 100644 index 0000000000..7ce49ef23d --- /dev/null +++ b/client/app/components/QuerySelector.jsx @@ -0,0 +1,149 @@ +import React, { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; +import { debounce, find } from 'lodash'; +import Input from 'antd/lib/input'; +import { Query } from '@/services/query'; +import { toastr } from '@/services/ng'; +import { QueryTagsControl } from '@/components/tags-control/TagsControl'; + +const SEARCH_DEBOUNCE_DURATION = 200; + +function search(term) { + // get recent + if (!term) { + return Query.recent().$promise + .then((results) => { + const filteredResults = results.filter(item => !item.is_draft); // filter out draft + return Promise.resolve(filteredResults); + }); + } + + // search by query + return Query.query({ q: term }).$promise + .then(({ results }) => Promise.resolve(results)); +} + +export function QuerySelector(props) { + const [searchTerm, setSearchTerm] = useState(); + const [searching, setSearching] = useState(); + const [searchResults, setSearchResults] = useState([]); + const [selectedQuery, setSelectedQuery] = useState(); + + let isStaleSearch = false; + const debouncedSearch = debounce(_search, SEARCH_DEBOUNCE_DURATION); + const placeholder = 'Search a query by name'; + const clearIcon = setSelectedQuery(null)} />; + const spinIcon = ; + + // set selected from prop + useEffect(() => { + if (props.selectedQuery) { + setSelectedQuery(props.selectedQuery); + } + }, [props.selectedQuery]); + + // on search term changed, debounced + useEffect(() => { + // clear results, no search + if (searchTerm === null) { + setSearchResults(null); + return () => {}; + } + + // search + debouncedSearch(searchTerm); + return () => { + debouncedSearch.cancel(); + isStaleSearch = true; + }; + }, [searchTerm]); + + // on query selected/cleared + useEffect(() => { + setSearchTerm(selectedQuery ? null : ''); // empty string forces recent fetch + props.onChange(selectedQuery); + }, [selectedQuery]); + + function _search(term) { + setSearching(true); + search(term) + .then(rejectStale) + .then(setSearchResults) + .finally(() => { + setSearching(false); + }); + } + + function rejectStale(results) { + return isStaleSearch + ? Promise.reject(new Error('stale')) + : Promise.resolve(results); + } + + function selectQuery(queryId) { + const query = find(searchResults, { id: queryId }); + if (!query) { // shouldn't happen + toastr.error('Something went wrong.. Couldn\'t select query'); + } + setSelectedQuery(query); + } + + function renderResults() { + if (!searchResults.length) { + return
No results matching search term.
; + } + + return ( +
+ {searchResults.map(q => ( + selectQuery(q.id)} + > + {q.name} + {' '} + + + ))} +
+ ); + } + + if (props.disabled) { + return ; + } + + return ( + + {selectedQuery ? ( + + ) : ( + setSearchTerm(e.target.value)} + suffix={spinIcon} + /> + )} +
+ {searchResults && renderResults()} +
+
+ ); +} + +QuerySelector.propTypes = { + onChange: PropTypes.func.isRequired, + selectedQuery: PropTypes.object, // eslint-disable-line react/forbid-prop-types + disabled: PropTypes.bool, +}; + +QuerySelector.defaultProps = { + selectedQuery: null, + disabled: false, +}; + +export default QuerySelector; diff --git a/client/app/components/dashboards/AddWidgetDialog.jsx b/client/app/components/dashboards/AddWidgetDialog.jsx index b7cb1f22fc..6821035f07 100644 --- a/client/app/components/dashboards/AddWidgetDialog.jsx +++ b/client/app/components/dashboards/AddWidgetDialog.jsx @@ -1,18 +1,16 @@ -import { debounce, each, values, map, includes, first, identity } from 'lodash'; +import { each, values, map, includes, first } from 'lodash'; import React from 'react'; import PropTypes from 'prop-types'; import Select from 'antd/lib/select'; import Modal from 'antd/lib/modal'; import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper'; -import { BigMessage } from '@/components/BigMessage'; -import highlight from '@/lib/highlight'; import { MappingType, ParameterMappingListInput, editableMappingsToParameterMappings, synchronizeWidgetTitles, } from '@/components/ParameterMappingInput'; -import { QueryTagsControl } from '@/components/tags-control/TagsControl'; +import { QuerySelector } from '@/components/QuerySelector'; import { toastr } from '@/services/ng'; import { Widget } from '@/services/widget'; @@ -26,42 +24,14 @@ class AddWidgetDialog extends React.Component { dialog: DialogPropType.isRequired, }; - constructor(props) { - super(props); - this.state = { - saveInProgress: false, - selectedQuery: null, - searchTerm: '', - highlightSearchTerm: false, - recentQueries: [], - queries: [], - selectedVis: null, - parameterMappings: [], - isLoaded: false, - }; - - const searchQueries = debounce(this.searchQueries.bind(this), 200); - this.onSearchTermChanged = (event) => { - const searchTerm = event.target.value; - this.setState({ searchTerm }); - searchQueries(searchTerm); - }; - } - - componentDidMount() { - Query.recent().$promise.then((items) => { - // Don't show draft (unpublished) queries in recent queries. - const results = items.filter(item => !item.is_draft); - this.setState({ - recentQueries: results, - queries: results, - isLoaded: true, - highlightSearchTerm: false, - }); - }); - } + state = { + saveInProgress: false, + selectedQuery: null, + selectedVis: null, + parameterMappings: [], + }; - selectQuery(queryId) { + selectQuery(selectedQuery) { // Clear previously selected query (if any) this.setState({ selectedQuery: null, @@ -69,8 +39,8 @@ class AddWidgetDialog extends React.Component { parameterMappings: [], }); - if (queryId) { - Query.get({ id: queryId }, (query) => { + if (selectedQuery) { + Query.get({ id: selectedQuery.id }, (query) => { if (query) { const existingParamNames = map( this.props.dashboard.getParametersDefs(), @@ -96,31 +66,6 @@ class AddWidgetDialog extends React.Component { } } - searchQueries(term) { - if (!term || term.length === 0) { - this.setState(prevState => ({ - queries: prevState.recentQueries, - isLoaded: true, - highlightSearchTerm: false, - })); - return; - } - - Query.query({ q: term }, (results) => { - // If user will type too quick - it's possible that there will be - // several requests running simultaneously. So we need to check - // which results are matching current search term and ignore - // outdated results. - if (this.state.searchTerm === term) { - this.setState({ - queries: results.results, - isLoaded: true, - highlightSearchTerm: true, - }); - } - }); - } - selectVisualization(query, visualizationId) { each(query.visualizations, (visualization) => { if (visualization.id === visualizationId) { @@ -173,88 +118,6 @@ class AddWidgetDialog extends React.Component { this.setState({ parameterMappings }); } - renderQueryInput() { - return ( -
- {!this.state.selectedQuery && ( - - )} - {this.state.selectedQuery && ( -
- - this.selectQuery(null)} - className="d-flex align-items-center justify-content-center" - style={{ - position: 'absolute', - right: '1px', - top: '1px', - bottom: '1px', - width: '30px', - background: '#fff', - borderRadius: '3px', - }} - > - - -
- )} -
- ); - } - - renderSearchQueryResults() { - const { isLoaded, queries, highlightSearchTerm, searchTerm } = this.state; - - const highlightSearchResult = highlightSearchTerm ? highlight : identity; - - return ( -
- {!isLoaded && ( -
- -
- )} - - {isLoaded && ( -
- { - (queries.length === 0) && -
No results matching search term.
- } - {(queries.length > 0) && ( -
- {queries.map(query => ( - this.selectQuery(query.id)} - > - - )} -
- )} -
- ); - } - renderVisualizationInput() { let visualizationGroups = {}; if (this.state.selectedQuery) { @@ -304,8 +167,7 @@ class AddWidgetDialog extends React.Component { okText="Add to Dashboard" width={700} > - {this.renderQueryInput()} - {!this.state.selectedQuery && this.renderSearchQueryResults()} + this.selectQuery(query)} /> {this.state.selectedQuery && this.renderVisualizationInput()} {