From 160a17bf424def71152e54e41d10164627c26d57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Grozdanovi=C4=87?= Date: Thu, 5 Mar 2020 10:56:23 +0100 Subject: [PATCH] Optimise autocomplete search (#179) * modify course search component to dynamically fetch with timeout * modify user search component to dynamically fetch with timeout * remove initial redux fetch of courses and users index * remove course and user index from redux * add TODO comments * hook up to proper apis and remove TODO comments * Update frontend/src/components/inputs/AutoCompleteCourseSelect.js Co-Authored-By: Omar Al-Ithawi * Update frontend/src/components/inputs/AutoCompleteUserSelect.js Co-Authored-By: Omar Al-Ithawi Co-authored-by: Omar Al-Ithawi --- frontend/src/App.js | 6 +- .../inputs/AutoCompleteCourseSelect.js | 68 ++++++++++--------- .../inputs/AutoCompleteUserSelect.js | 62 +++++++++-------- frontend/src/redux/actions/ActionTypes.js | 2 - frontend/src/redux/actions/Actions.js | 37 ---------- .../redux/reducers/coursesIndexReducers.js | 22 ------ frontend/src/redux/reducers/index.js | 4 -- .../src/redux/reducers/usersIndexReducers.js | 21 ------ 8 files changed, 68 insertions(+), 154 deletions(-) delete mode 100644 frontend/src/redux/reducers/coursesIndexReducers.js delete mode 100644 frontend/src/redux/reducers/usersIndexReducers.js diff --git a/frontend/src/App.js b/frontend/src/App.js index 16dbf3df..2acc8b70 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import { Route, Switch } from 'react-router-dom'; import { connect } from 'react-redux'; -import { fetchCoursesIndex, fetchUserIndex, fetchGeneralData, fetchAllCsvReportsData } from 'base/redux/actions/Actions'; +import { fetchGeneralData, fetchAllCsvReportsData } from 'base/redux/actions/Actions'; import ReactCSSTransitionReplace from 'react-css-transition-replace'; import LoadingSpinner from 'base/containers/loading-spinner/LoadingSpinner'; import DashboardContent from 'base/views/DashboardContent'; @@ -18,8 +18,6 @@ import styles from 'base/sass/base/_grid.scss'; class App extends Component { componentDidMount() { - this.props.fetchCoursesIndex(); - this.props.fetchUserIndex(); this.props.fetchGeneralData(); (process.env.ENABLE_CSV_REPORTS === "enabled") && this.props.fetchAllCsvReportsData(); } @@ -61,8 +59,6 @@ const mapStateToProps = (state, ownProps) => ({ }) const mapDispatchToProps = dispatch => ({ - fetchCoursesIndex: () => dispatch(fetchCoursesIndex()), - fetchUserIndex: () => dispatch(fetchUserIndex()), fetchGeneralData: () => dispatch(fetchGeneralData()), fetchAllCsvReportsData: () => dispatch(fetchAllCsvReportsData()), }) diff --git a/frontend/src/components/inputs/AutoCompleteCourseSelect.js b/frontend/src/components/inputs/AutoCompleteCourseSelect.js index 9a887f48..cdf2766a 100644 --- a/frontend/src/components/inputs/AutoCompleteCourseSelect.js +++ b/frontend/src/components/inputs/AutoCompleteCourseSelect.js @@ -1,5 +1,4 @@ import React, { Component } from 'react'; -import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import Autosuggest from 'react-autosuggest'; import { Link } from 'react-router-dom'; @@ -7,17 +6,11 @@ import styles from './_autocomplete-course-select.scss'; import classNames from 'classnames/bind'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import apiConfig from 'base/apiConfig'; let cx = classNames.bind(styles); -var coursesList = [ -]; - -const getSuggestions = value => { - const inputValue = value.trim().toLowerCase(); - const inputLength = inputValue.length; - return inputLength === coursesList ? [] : coursesList.filter(course => ((course.courseName.toLowerCase().slice(0, inputLength) === inputValue) || (course.courseNumber.toLowerCase().slice(0, inputLength) === inputValue))).toArray(); -}; +const WAIT_INTERVAL = 1000; const getSuggestionValue = suggestion => suggestion.courseName; @@ -32,6 +25,7 @@ class AutoCompleteCourseSelect extends Component { }; this.onChange = this.onChange.bind(this); + this.doSearch = this.doSearch.bind(this); this.modalTrigger = this.modalTrigger.bind(this); this.storeInputReference = this.storeInputReference.bind(this); } @@ -46,11 +40,25 @@ class AutoCompleteCourseSelect extends Component { } onChange = (event, { newValue }) => { + clearTimeout(this.timer); + this.setState({ value: newValue }); + + this.timer = setTimeout(this.doSearch, WAIT_INTERVAL); }; + doSearch = () => { + const requestUrl = apiConfig.coursesGeneral + '?search=' + encodeURIComponent(this.state.value); + fetch((requestUrl), { credentials: "same-origin" }) + .then(response => response.json()) + .then(json => this.setState({ + suggestions: json['results'], + }) + ) + } + onSuggestionsClearRequested = () => { } @@ -63,10 +71,8 @@ class AutoCompleteCourseSelect extends Component { }); } - onSuggestionsFetchRequested = ({ value }) => { - this.setState({ - suggestions: getSuggestions(value) - }); + onSuggestionsFetchRequested = () => { + }; storeInputReference = autosuggest => { @@ -75,6 +81,16 @@ class AutoCompleteCourseSelect extends Component { } }; + componentWillMount() { + this.timer = null; + fetch((apiConfig.coursesGeneral), { credentials: "same-origin" }) + .then(response => response.json()) + .then(json => this.setState({ + suggestions: json['results'], + }) + ) + } + render() { const { value, suggestions } = this.state; @@ -84,21 +100,13 @@ class AutoCompleteCourseSelect extends Component { onChange: this.onChange }; - coursesList = this.props.coursesIndex.map((item, index) => { - return { - courseId: item['course_id'], - courseName: item['course_name'], - courseNumber: item['course_code'] - } - }) - const renderSuggestion = suggestion => ( - +
- {suggestion.courseNumber} - {suggestion.courseId} + {suggestion['course_code']} + {suggestion['course_id']}
- {suggestion.courseName} + {suggestion['course_name']} ); @@ -131,7 +139,7 @@ class AutoCompleteCourseSelect extends Component { AutoCompleteCourseSelect.defaultProps = { negativeStyleButton: false, buttonText: 'Select a course', - inputPlaceholder: 'Select or start typing', + inputPlaceholder: 'Start typing to search...', coursesList: [ { courseId: 'A101', @@ -179,10 +187,4 @@ AutoCompleteCourseSelect.propTypes = { coursesList: PropTypes.array }; -const mapStateToProps = (state, ownProps) => ({ - coursesIndex: state.coursesIndex.coursesIndex, -}) - -export default connect( - mapStateToProps -)(AutoCompleteCourseSelect) +export default AutoCompleteCourseSelect diff --git a/frontend/src/components/inputs/AutoCompleteUserSelect.js b/frontend/src/components/inputs/AutoCompleteUserSelect.js index 6ebab9b3..898455cd 100644 --- a/frontend/src/components/inputs/AutoCompleteUserSelect.js +++ b/frontend/src/components/inputs/AutoCompleteUserSelect.js @@ -1,5 +1,4 @@ import React, { Component } from 'react'; -import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import Autosuggest from 'react-autosuggest'; import { Link } from 'react-router-dom'; @@ -7,17 +6,11 @@ import styles from './_autocomplete-user-select.scss'; import classNames from 'classnames/bind'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import apiConfig from 'base/apiConfig'; let cx = classNames.bind(styles); -var usersList = [ -]; - -const getSuggestions = value => { - const inputValue = value.trim().toLowerCase(); - const inputLength = inputValue.length; - return inputLength === usersList ? [] : usersList.filter(user => ((user.userName.toLowerCase().slice(0, inputLength) === inputValue) || (user.userUsername.toLowerCase().slice(0, inputLength) === inputValue))).toArray(); -}; +const WAIT_INTERVAL = 1000; const getSuggestionValue = suggestion => suggestion.userName; @@ -32,6 +25,7 @@ class AutoCompleteUserSelect extends Component { }; this.onChange = this.onChange.bind(this); + this.doSearch = this.doSearch.bind(this); this.modalTrigger = this.modalTrigger.bind(this); this.storeInputReference = this.storeInputReference.bind(this); } @@ -46,11 +40,25 @@ class AutoCompleteUserSelect extends Component { } onChange = (event, { newValue }) => { + clearTimeout(this.timer); + this.setState({ value: newValue }); + + this.timer = setTimeout(this.doSearch, WAIT_INTERVAL); }; + doSearch = () => { + const requestUrl = apiConfig.learnersGeneral + '?search=' + encodeURIComponent(this.state.value); + fetch((requestUrl), { credentials: "same-origin" }) + .then(response => response.json()) + .then(json => this.setState({ + suggestions: json['results'], + }) + ) + } + onSuggestionsClearRequested = () => { } @@ -63,10 +71,8 @@ class AutoCompleteUserSelect extends Component { }); } - onSuggestionsFetchRequested = ({ value }) => { - this.setState({ - suggestions: getSuggestions(value) - }); + onSuggestionsFetchRequested = () => { + }; storeInputReference = autosuggest => { @@ -75,6 +81,16 @@ class AutoCompleteUserSelect extends Component { } }; + componentWillMount() { + this.timer = null; + fetch((apiConfig.figuresUsersIndexApi), { credentials: "same-origin" }) + .then(response => response.json()) + .then(json => this.setState({ + suggestions: json['results'], + }) + ) + } + render() { const { value, suggestions } = this.state; @@ -84,16 +100,8 @@ class AutoCompleteUserSelect extends Component { onChange: this.onChange }; - usersList = this.props.usersIndex.map((item, index) => { - return { - userId: item['id'], - userName: item['fullname'] ? item['fullname'] : item['username'], - userUsername: item['username'] - } - }) - const renderSuggestion = suggestion => ( - {suggestion.userUsername}{suggestion.userName} + {suggestion['username']}{suggestion['fullname']} ); @@ -125,7 +133,7 @@ class AutoCompleteUserSelect extends Component { AutoCompleteUserSelect.defaultProps = { negativeStyleButton: false, buttonText: 'Select a user', - inputPlaceholder: 'Select or start typing', + inputPlaceholder: 'Start typing to search...', } AutoCompleteUserSelect.propTypes = { @@ -134,10 +142,4 @@ AutoCompleteUserSelect.propTypes = { inputPlaceholder: PropTypes.string }; -const mapStateToProps = (state, ownProps) => ({ - usersIndex: state.usersIndex.usersIndex, -}) - -export default connect( - mapStateToProps -)(AutoCompleteUserSelect) +export default AutoCompleteUserSelect diff --git a/frontend/src/redux/actions/ActionTypes.js b/frontend/src/redux/actions/ActionTypes.js index 0f3a31ed..040822d8 100644 --- a/frontend/src/redux/actions/ActionTypes.js +++ b/frontend/src/redux/actions/ActionTypes.js @@ -9,8 +9,6 @@ export const LOAD_REPORT = 'LOAD_REPORT' export const REQUEST_REPORTS_LIST = 'REQUEST_REPORTS_LIST' export const LOAD_REPORTS_LIST = 'LOAD_REPORTS_LIST' -export const LOAD_COURSES_INDEX = 'LOAD_COURSES_INDEX' -export const LOAD_USER_INDEX = 'LOAD_USER_INDEX' export const LOAD_GENERAL_DATA = 'LOAD_GENERAL_DATA' export const LOAD_CSV_REPORTS_DATA = 'LOAD_CSV_REPORTS_DATA' export const LOAD_CSV_USER_REPORTS_DATA = 'LOAD_CSV_USER_REPORTS_DATA' diff --git a/frontend/src/redux/actions/Actions.js b/frontend/src/redux/actions/Actions.js index 634623b8..ce2c9847 100644 --- a/frontend/src/redux/actions/Actions.js +++ b/frontend/src/redux/actions/Actions.js @@ -3,43 +3,6 @@ import * as types from './ActionTypes'; import apiConfig from 'base/apiConfig'; import { trackPromise } from 'react-promise-tracker'; -// course index data related Redux actions - -export const loadCoursesIndex = ( coursesData ) => ({ - type: types.LOAD_COURSES_INDEX, - fetchedData: coursesData, - receivedAt: Date.now() -}) - -export function fetchCoursesIndex () { - return dispatch => { - return trackPromise( - fetch(apiConfig.coursesGeneral, { credentials: "same-origin" }) - .then(response => response.json()) - .then(json => dispatch(loadCoursesIndex(json['results']))) - ) - } -} - - -// user index data related Redux actions - -export const loadUserIndex = ( coursesData ) => ({ - type: types.LOAD_USER_INDEX, - fetchedData: coursesData, - receivedAt: Date.now() -}) - -export function fetchUserIndex () { - return dispatch => { - return trackPromise( - fetch(apiConfig.learnersGeneral, { credentials: "same-origin" }) - .then(response => response.json()) - .then(json => dispatch(loadUserIndex(json.results))) - ) - } -} - // report related Redux actions diff --git a/frontend/src/redux/reducers/coursesIndexReducers.js b/frontend/src/redux/reducers/coursesIndexReducers.js deleted file mode 100644 index 18558467..00000000 --- a/frontend/src/redux/reducers/coursesIndexReducers.js +++ /dev/null @@ -1,22 +0,0 @@ -import Immutable from 'immutable'; -import { LOAD_COURSES_INDEX } from '../actions/ActionTypes'; - -const initialState = { - receivedAt: '', - coursesIndex: Immutable.List(), -} - -const coursesIndex = (state = initialState, action) => { - switch (action.type) { - case LOAD_COURSES_INDEX: - return Object.assign({}, state, { - isFetching: false, - receivedAt: action.receivedAt, - coursesIndex: Immutable.List(action.fetchedData) - }) - default: - return state - } -} - -export default coursesIndex diff --git a/frontend/src/redux/reducers/index.js b/frontend/src/redux/reducers/index.js index a5a57c18..b8792654 100644 --- a/frontend/src/redux/reducers/index.js +++ b/frontend/src/redux/reducers/index.js @@ -1,7 +1,5 @@ import { combineReducers } from 'redux'; import { routerReducer } from 'react-router-redux'; -import coursesIndex from './coursesIndexReducers'; -import usersIndex from './usersIndexReducers'; import userData from './userDataReducers'; import report from './reportReducers'; import reportsList from './reportsListReducers'; @@ -9,8 +7,6 @@ import generalData from './generalDataReducers'; import csvReportsIndex from './csvReportsIndexReducers'; export default combineReducers({ - coursesIndex, - usersIndex, userData, reportsList, report, diff --git a/frontend/src/redux/reducers/usersIndexReducers.js b/frontend/src/redux/reducers/usersIndexReducers.js deleted file mode 100644 index 019b7d49..00000000 --- a/frontend/src/redux/reducers/usersIndexReducers.js +++ /dev/null @@ -1,21 +0,0 @@ -import Immutable from 'immutable'; -import { LOAD_USER_INDEX } from '../actions/ActionTypes'; - -const initialState = { - usersIndex: Immutable.List(), -} - -const usersIndex = (state = initialState, action) => { - switch (action.type) { - case LOAD_USER_INDEX: - return Object.assign({}, state, { - isFetching: false, - receivedAt: action.receivedAt, - usersIndex: Immutable.List(action.fetchedData) - }) - default: - return state - } -} - -export default usersIndex