From 549f878c98dcf515968c4f5b94b0fda59f50cc7c Mon Sep 17 00:00:00 2001 From: Ran Byron Date: Thu, 28 Feb 2019 15:07:57 +0200 Subject: [PATCH] Added component (#3494) * Updated npm to support react hooks * Added * Changed selectQuery to also clear, completed 2->3 dots in msg, avoiding setSearching on stale rejection. * Removed unused highlight lib --- client/.eslintrc.js | 1 + client/app/components/QuerySelector.jsx | 160 +++++++++++++++++ .../components/dashboards/AddWidgetDialog.jsx | 162 ++---------------- client/app/lib/highlight.js | 8 - package-lock.json | 131 +++++++++++--- package.json | 4 +- 6 files changed, 283 insertions(+), 183 deletions(-) create mode 100644 client/app/components/QuerySelector.jsx delete mode 100644 client/app/lib/highlight.js 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..26a18593c1 --- /dev/null +++ b/client/app/components/QuerySelector.jsx @@ -0,0 +1,160 @@ +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; + +class StaleSearchError extends Error { + constructor() { + super('stale search'); + } +} + +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 = selectQuery(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]); + + function _search(term) { + setSearching(true); + search(term) + .then(rejectStale) + .then((results) => { + setSearchResults(results); + setSearching(false); + }) + .catch((err) => { + if (!(err instanceof StaleSearchError)) { + setSearching(false); + } + }); + } + + function rejectStale(results) { + return isStaleSearch + ? Promise.reject(new StaleSearchError()) + : Promise.resolve(results); + } + + function selectQuery(queryId) { + let query = null; + if (queryId) { + query = find(searchResults, { id: queryId }); + if (!query) { // shouldn't happen + toastr.error('Something went wrong... Couldn\'t select query'); + } + } + + setSearchTerm(query ? null : ''); // empty string triggers recent fetch + setSelectedQuery(query); + props.onChange(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 ( - - ); - } - - 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()} { diff --git a/client/app/lib/highlight.js b/client/app/lib/highlight.js deleted file mode 100644 index 322a66f722..0000000000 --- a/client/app/lib/highlight.js +++ /dev/null @@ -1,8 +0,0 @@ -function escapeRegexp(queryToEscape) { - return ('' + queryToEscape).replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); -} - -// https://github.com/angular-ui/ui-select/blob/master/src/common.js#L146 -export default function highlight(matchItem, query, template = '$&') { - return query && matchItem ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), template) : matchItem; -} diff --git a/package-lock.json b/package-lock.json index 07d52769e3..07ded53f17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "redash-client", - "version": "6.0.0", + "version": "7.0.0-beta", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -3766,6 +3766,7 @@ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", "dev": true, + "optional": true, "requires": { "delayed-stream": "~1.0.0" } @@ -7023,7 +7024,8 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -7041,11 +7043,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7058,15 +7062,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -7169,7 +7176,8 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -7179,6 +7187,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -7191,17 +7200,20 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true + "bundled": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -7218,6 +7230,7 @@ "mkdirp": { "version": "0.5.1", "bundled": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -7290,7 +7303,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -7300,6 +7314,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -7375,7 +7390,8 @@ }, "safe-buffer": { "version": "5.1.1", - "bundled": true + "bundled": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -7405,6 +7421,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -7422,6 +7439,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -7460,11 +7478,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "optional": true }, "yallist": { "version": "3.0.2", - "bundled": true + "bundled": true, + "optional": true } } }, @@ -11193,6 +11213,7 @@ "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", "dev": true, + "optional": true, "requires": { "hoek": "2.x.x" } @@ -11254,7 +11275,8 @@ "version": "2.16.3", "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", - "dev": true + "dev": true, + "optional": true }, "http-signature": { "version": "1.1.1", @@ -15840,14 +15862,41 @@ } }, "react": { - "version": "16.4.0", - "resolved": "https://registry.npmjs.org/react/-/react-16.4.0.tgz", - "integrity": "sha512-K0UrkLXSAekf5nJu89obKUM7o2vc6MMN9LYoKnCa+c+8MJRAT120xzPLENcWSRc7GYKIg0LlgJRDorrufdglQQ==", + "version": "16.8.3", + "resolved": "https://registry.npmjs.org/react/-/react-16.8.3.tgz", + "integrity": "sha512-3UoSIsEq8yTJuSu0luO1QQWYbgGEILm+eJl2QN/VLDi7hL+EN18M3q3oVZwmVzzBJ3DkM7RMdRwBmZZ+b4IzSA==", "requires": { - "fbjs": "^0.8.16", "loose-envify": "^1.1.0", "object-assign": "^4.1.1", - "prop-types": "^15.6.0" + "prop-types": "^15.6.2", + "scheduler": "^0.13.3" + }, + "dependencies": { + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + }, + "dependencies": { + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + } + } + }, + "react-is": { + "version": "16.8.3", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.3.tgz", + "integrity": "sha512-Y4rC1ZJmsxxkkPuMLwvKvlL1Zfpbcu+Bf4ZigkHup3v9EfdYhAlWAaVyA19olXq2o2mGn0w+dFKvk3pVVlYcIA==" + } } }, "react-ace": { @@ -15884,14 +15933,41 @@ } }, "react-dom": { - "version": "16.4.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.4.0.tgz", - "integrity": "sha512-bbLd+HYpBEnYoNyxDe9XpSG2t9wypMohwQPvKw8Hov3nF7SJiJIgK56b46zHpBUpHb06a1iEuw7G3rbrsnNL6w==", + "version": "16.8.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.3.tgz", + "integrity": "sha512-ttMem9yJL4/lpItZAQ2NTFAbV7frotHk5DZEHXUOws2rMmrsvh1Na7ThGT0dTzUIl6pqTOi5tYREfL8AEna3lA==", "requires": { - "fbjs": "^0.8.16", "loose-envify": "^1.1.0", "object-assign": "^4.1.1", - "prop-types": "^15.6.0" + "prop-types": "^15.6.2", + "scheduler": "^0.13.3" + }, + "dependencies": { + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + }, + "dependencies": { + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + } + } + }, + "react-is": { + "version": "16.8.3", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.3.tgz", + "integrity": "sha512-Y4rC1ZJmsxxkkPuMLwvKvlL1Zfpbcu+Bf4ZigkHup3v9EfdYhAlWAaVyA19olXq2o2mGn0w+dFKvk3pVVlYcIA==" + } } }, "react-is": { @@ -16816,6 +16892,15 @@ "object-assign": "^4.1.1" } }, + "scheduler": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.3.tgz", + "integrity": "sha512-UxN5QRYWtpR1egNWzJcVLk8jlegxAugswQc984lD3kU7NuobsO37/sRfbpTdBjtnD5TBNFA2Q2oLV5+UmPSmEQ==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, "seedrandom": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-2.4.4.tgz", diff --git a/package.json b/package.json index a89d966c94..7ff72bacbf 100644 --- a/package.json +++ b/package.json @@ -76,9 +76,9 @@ "pivottable": "^2.15.0", "plotly.js": "1.41.3", "prop-types": "^15.6.1", - "react": "^16.3.2", + "react": "^16.8.3", "react-ace": "^6.1.0", - "react-dom": "^16.3.2", + "react-dom": "^16.8.3", "react2angular": "^3.2.1", "ui-select": "^0.19.8", "underscore.string": "^3.3.4"