diff --git a/client/.eslintignore b/client/.eslintignore index 24669a7008..013f6ab6e5 100644 --- a/client/.eslintignore +++ b/client/.eslintignore @@ -1,3 +1,4 @@ build/*.js +dist config/*.js client/dist diff --git a/client/.eslintrc.js b/client/.eslintrc.js index 4115400825..152bf9ca3d 100644 --- a/client/.eslintrc.js +++ b/client/.eslintrc.js @@ -1,11 +1,10 @@ module.exports = { root: true, - extends: ["airbnb", "plugin:compat/recommended"], + extends: ["react-app", "plugin:compat/recommended", "prettier"], plugins: ["jest", "compat", "no-only-tests"], settings: { "import/resolver": "webpack" }, - parser: "babel-eslint", env: { browser: true, node: true @@ -13,54 +12,6 @@ module.exports = { rules: { // allow debugger during development "no-debugger": process.env.NODE_ENV === "production" ? 2 : 0, - "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", - "no-restricted-globals": "off", - "no-multi-assign": "off", - "no-lonely-if": "off", - "consistent-return": "off", - "no-control-regex": "off", - "no-multiple-empty-lines": "warn", - "no-only-tests/no-only-tests": "error", - "operator-linebreak": "off", - "react/destructuring-assignment": "off", - "react/jsx-filename-extension": "off", - "react/jsx-one-expression-per-line": "off", - "react/jsx-uses-react": "error", - "react/jsx-uses-vars": "error", - "react/jsx-wrap-multilines": "warn", - "react/no-access-state-in-setstate": "warn", - "react/prefer-stateless-function": "warn", - "react/forbid-prop-types": "warn", - "react/prop-types": "warn", "jsx-a11y/anchor-is-valid": "off", - "jsx-a11y/click-events-have-key-events": "off", - "jsx-a11y/label-has-associated-control": [ - "warn", - { - controlComponents: true - } - ], - "jsx-a11y/label-has-for": "off", - "jsx-a11y/no-static-element-interactions": "off", - "max-len": [ - "error", - 120, - 2, - { - ignoreUrls: true, - ignoreComments: false, - ignoreRegExpLiterals: true, - ignoreStrings: true, - ignoreTemplateLiterals: true - } - ], - "no-else-return": ["error", { allowElseIf: true }], - "object-curly-newline": ["error", { consistent: true }] } }; diff --git a/client/app/components/EditParameterSettingsDialog.jsx b/client/app/components/EditParameterSettingsDialog.jsx index fbae2eb71d..19a67cf35e 100644 --- a/client/app/components/EditParameterSettingsDialog.jsx +++ b/client/app/components/EditParameterSettingsDialog.jsx @@ -86,13 +86,13 @@ function EditParameterSettingsDialog(props) { // fetch query by id useEffect(() => { - const { queryId } = props.parameter; + const queryId = props.parameter.queryId; if (queryId) { Query.get({ id: queryId }, (query) => { setInitialQuery(query); }); } - }, []); + }, [props.parameter.queryId]); function isFulfilled() { // name diff --git a/client/app/components/QuerySelector.jsx b/client/app/components/QuerySelector.jsx index d56edeb890..4e082846f7 100644 --- a/client/app/components/QuerySelector.jsx +++ b/client/app/components/QuerySelector.jsx @@ -2,22 +2,15 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import cx from 'classnames'; import { react2angular } from 'react2angular'; -import { debounce, find } from 'lodash'; +import { find } from 'lodash'; import Input from 'antd/lib/input'; import Select from 'antd/lib/select'; import { Query } from '@/services/query'; import notification from '@/services/notification'; import { QueryTagsControl } from '@/components/tags-control/TagsControl'; +import useSearchResults from '@/lib/hooks/useSearchResults'; -const SEARCH_DEBOUNCE_DURATION = 200; const { Option } = Select; - -class StaleSearchError extends Error { - constructor() { - super('stale search'); - } -} - function search(term) { // get recent if (!term) { @@ -34,17 +27,16 @@ function search(term) { } export function QuerySelector(props) { - const [searchTerm, setSearchTerm] = useState(); - const [searching, setSearching] = useState(); - const [searchResults, setSearchResults] = useState([]); + const [searchTerm, setSearchTerm] = useState(''); const [selectedQuery, setSelectedQuery] = useState(); + const [doSearch, searchResults, searching] = useSearchResults(search, { initialResults: [] }); - let isStaleSearch = false; - const debouncedSearch = debounce(_search, SEARCH_DEBOUNCE_DURATION); const placeholder = 'Search a query by name'; const clearIcon = selectQuery(null)} />; const spinIcon = ; + useEffect(() => { doSearch(searchTerm); }, [doSearch, searchTerm]); + // set selected from prop useEffect(() => { if (props.selectedQuery) { @@ -52,43 +44,6 @@ export function QuerySelector(props) { } }, [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) { diff --git a/client/app/components/TimeAgo.jsx b/client/app/components/TimeAgo.jsx index fd4e08b2ca..c2e5b1cb48 100644 --- a/client/app/components/TimeAgo.jsx +++ b/client/app/components/TimeAgo.jsx @@ -26,7 +26,7 @@ export function TimeAgo({ date, placeholder, autoUpdate }) { const timer = setInterval(forceUpdate, 30 * 1000); return () => clearInterval(timer); } - }, [autoUpdate]); + }, [autoUpdate, forceUpdate]); return ( diff --git a/client/app/components/Timer.jsx b/client/app/components/Timer.jsx index ebaa976b74..bdca500715 100644 --- a/client/app/components/Timer.jsx +++ b/client/app/components/Timer.jsx @@ -12,7 +12,7 @@ export function Timer({ from }) { useEffect(() => { const timer = setInterval(forceUpdate, 1000); return () => clearInterval(timer); - }, []); + }, [forceUpdate]); const diff = moment.now() - startTime; const format = diff > 1000 * 60 * 60 ? 'HH:mm:ss' : 'mm:ss'; // no HH under an hour diff --git a/client/app/components/app-header/components/FavoritesDropdown.jsx b/client/app/components/app-header/components/FavoritesDropdown.jsx index d21642792c..5e98a5f723 100644 --- a/client/app/components/app-header/components/FavoritesDropdown.jsx +++ b/client/app/components/app-header/components/FavoritesDropdown.jsx @@ -27,7 +27,9 @@ export default function FavoritesDropdown({ fetch, urlTemplate }) { }, [fetch]); // fetch items on init - useEffect(() => fetchItems(false), []); + useEffect(() => { + fetchItems(false); + }, [fetchItems]); // fetch items on click const onVisibleChange = visible => visible && fetchItems(); diff --git a/client/app/components/dynamic-form/DynamicForm.jsx b/client/app/components/dynamic-form/DynamicForm.jsx index fe96d0adbd..6bcc851e29 100644 --- a/client/app/components/dynamic-form/DynamicForm.jsx +++ b/client/app/components/dynamic-form/DynamicForm.jsx @@ -58,22 +58,22 @@ class DynamicForm extends React.Component { const hasFilledExtraField = some(props.fields, (field) => { const { extra, initialValue } = field; - return extra && (!isEmpty(initialValue) || isNumber(initialValue) || isBoolean(initialValue) && initialValue); + return extra && (!isEmpty(initialValue) || isNumber(initialValue) || (isBoolean(initialValue) && initialValue)); }); + + const inProgressActions = {}; + props.actions.forEach(action => inProgressActions[action.name] = false); + this.state = { isSubmitting: false, - inProgressActions: [], showExtraFields: hasFilledExtraField, + inProgressActions }; this.actionCallbacks = this.props.actions.reduce((acc, cur) => ({ ...acc, [cur.name]: cur.callback, }), null); - - props.actions.forEach((action) => { - this.state.inProgressActions[action.name] = false; - }); } setActionInProgress = (actionName, inProgress) => { diff --git a/client/app/components/permissions-editor/PermissionsEditorDialog.jsx b/client/app/components/permissions-editor/PermissionsEditorDialog.jsx index caa2112f86..6e723ded99 100644 --- a/client/app/components/permissions-editor/PermissionsEditorDialog.jsx +++ b/client/app/components/permissions-editor/PermissionsEditorDialog.jsx @@ -34,7 +34,7 @@ function useGrantees(url) { const addPermission = useCallback((userId, accessType = 'modify') => $http.post( url, { access_type: accessType, user_id: userId }, - ).catch(() => notification.error('Could not grant permission to the user'), [url])); + ).catch(() => notification.error('Could not grant permission to the user')), [url]); const removePermission = useCallback((userId, accessType = 'modify') => $http.delete( url, { data: { access_type: accessType, user_id: userId } }, @@ -77,7 +77,7 @@ function UserSelect({ onSelect, shouldShowUser }) { useEffect(() => { setLoadingUsers(true); debouncedSearchUsers(searchTerm); - }, [searchTerm]); + }, [debouncedSearchUsers, searchTerm]); return (