Unverified Commit a5b97f7c authored by Ryan Lieu's avatar Ryan Lieu Committed by GitHub

Added loading spinner for search requests (#182)

* added loading spinner for search requests

* fixed multiple requests issue, SearchResourceAction now sets loading state

* added tests, cleaned up reducer

* fixed typos

* fixed previous search term call

* made tests cleaner
parent 1ef28625
...@@ -8,6 +8,7 @@ import { RouteComponentProps } from 'react-router'; ...@@ -8,6 +8,7 @@ import { RouteComponentProps } from 'react-router';
import SearchBar from './SearchBar'; import SearchBar from './SearchBar';
import SearchList from './SearchList'; import SearchList from './SearchList';
import LoadingSpinner from 'components/common/LoadingSpinner';
import BookmarkList from 'components/common/Bookmark/BookmarkList' import BookmarkList from 'components/common/Bookmark/BookmarkList'
import InfoButton from 'components/common/InfoButton'; import InfoButton from 'components/common/InfoButton';
...@@ -48,6 +49,7 @@ import { ...@@ -48,6 +49,7 @@ import {
export interface StateFromProps { export interface StateFromProps {
searchTerm: string; searchTerm: string;
isLoading: boolean;
popularTables: TableResource[]; popularTables: TableResource[];
tables: TableSearchResults; tables: TableSearchResults;
dashboards: DashboardSearchResults; dashboards: DashboardSearchResults;
...@@ -97,7 +99,10 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState ...@@ -97,7 +99,10 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState
const { searchTerm, pageIndex, selectedTab } = params; const { searchTerm, pageIndex, selectedTab } = params;
const { term, index, currentTab } = this.getSanitizedUrlParams(searchTerm, pageIndex, selectedTab); const { term, index, currentTab } = this.getSanitizedUrlParams(searchTerm, pageIndex, selectedTab);
this.setState({ selectedTab: currentTab }); this.setState({ selectedTab: currentTab });
this.props.searchAll(term, this.createSearchOptions(index, currentTab)); const prevTerm = prevProps.searchTerm;
if (term !== prevTerm) {
this.props.searchAll(term, this.createSearchOptions(index, currentTab));
}
} }
} }
...@@ -252,6 +257,16 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState ...@@ -252,6 +257,16 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState
); );
}; };
renderContent = () => {
if (this.props.isLoading) {
return (<LoadingSpinner/>);
}
if (this.props.searchTerm.length > 0) {
return this.renderSearchResults();
}
return this.renderPopularTables();
};
render() { render() {
const { searchTerm } = this.props; const { searchTerm } = this.props;
const innerContent = ( const innerContent = (
...@@ -259,8 +274,7 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState ...@@ -259,8 +274,7 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState
<div className="row"> <div className="row">
<div className="col-xs-12 col-md-offset-1 col-md-10"> <div className="col-xs-12 col-md-offset-1 col-md-10">
<SearchBar handleValueSubmit={ this.onSearchBarSubmit } searchTerm={ searchTerm }/> <SearchBar handleValueSubmit={ this.onSearchBarSubmit } searchTerm={ searchTerm }/>
{ searchTerm.length > 0 && this.renderSearchResults() } { this.renderContent() }
{ searchTerm.length === 0 && this.renderPopularTables() }
</div> </div>
</div> </div>
</div> </div>
...@@ -279,6 +293,7 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState ...@@ -279,6 +293,7 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState
export const mapStateToProps = (state: GlobalState) => { export const mapStateToProps = (state: GlobalState) => {
return { return {
searchTerm: state.search.search_term, searchTerm: state.search.search_term,
isLoading: state.search.isLoading,
popularTables: state.popularTables, popularTables: state.popularTables,
tables: state.search.tables, tables: state.search.tables,
users: state.search.users, users: state.search.users,
......
...@@ -29,12 +29,14 @@ import SearchBar from '../SearchBar'; ...@@ -29,12 +29,14 @@ import SearchBar from '../SearchBar';
import SearchList from '../SearchList'; import SearchList from '../SearchList';
import globalState from 'fixtures/globalState'; import globalState from 'fixtures/globalState';
import LoadingSpinner from 'components/common/LoadingSpinner';
describe('SearchPage', () => { describe('SearchPage', () => {
const setStateSpy = jest.spyOn(SearchPage.prototype, 'setState'); const setStateSpy = jest.spyOn(SearchPage.prototype, 'setState');
const setup = (propOverrides?: Partial<SearchPageProps>) => { const setup = (propOverrides?: Partial<SearchPageProps>) => {
const props: SearchPageProps = { const props: SearchPageProps = {
searchTerm: globalState.search.search_term, searchTerm: globalState.search.search_term,
isLoading: false,
popularTables: globalState.popularTables, popularTables: globalState.popularTables,
dashboards: globalState.search.dashboards, dashboards: globalState.search.dashboards,
tables: globalState.search.tables, tables: globalState.search.tables,
...@@ -226,16 +228,16 @@ describe('SearchPage', () => { ...@@ -226,16 +228,16 @@ describe('SearchPage', () => {
}); });
describe('componentDidUpdate', () => { describe('componentDidUpdate', () => {
let props;
let wrapper;
let searchAllSpy; let searchAllSpy;
let mockSearchOptions; let mockSearchOptions;
let mockSanitizedUrlParams; let mockSanitizedUrlParams;
let createSearchOptionsSpy; let createSearchOptionsSpy;
let getSanitizedUrlParamsSpy; let getSanitizedUrlParamsSpy;
let props;
let wrapper;
beforeAll(() => { beforeAll(() => {
const setupResult = setup({ const setupResult = setup({
location: { location: {
...@@ -263,6 +265,7 @@ describe('SearchPage', () => { ...@@ -263,6 +265,7 @@ describe('SearchPage', () => {
setStateSpy.mockClear(); setStateSpy.mockClear();
const mockPrevProps = { const mockPrevProps = {
searchTerm: 'previous',
location: { location: {
search: '/search?searchTerm=previous&selectedTab=table&pageIndex=0', search: '/search?searchTerm=previous&selectedTab=table&pageIndex=0',
pathname: 'mockstr', pathname: 'mockstr',
...@@ -277,9 +280,24 @@ describe('SearchPage', () => { ...@@ -277,9 +280,24 @@ describe('SearchPage', () => {
expect(setStateSpy).toHaveBeenCalledWith({ selectedTab: ResourceType.table }); expect(setStateSpy).toHaveBeenCalledWith({ selectedTab: ResourceType.table });
}); });
it('calls searchAll', () => { it('calls searchAll if called with a new search term', () => {
expect(searchAllSpy).toHaveBeenCalledWith(mockSanitizedUrlParams.term, mockSearchOptions); expect(searchAllSpy).toHaveBeenCalledWith(mockSanitizedUrlParams.term, mockSearchOptions);
}); });
it('does not call searchAll if called with the same search term with a new page', () => {
searchAllSpy.mockClear();
const mockPrevProps = {
searchTerm: 'current',
location: {
search: '/search?searchTerm=current&current=table&pageIndex=1',
pathname: 'mockstr',
state: jest.fn(),
hash: 'mockstr',
}
};
wrapper.instance().componentDidUpdate(mockPrevProps);
expect(searchAllSpy).not.toHaveBeenCalled();
});
}); });
describe('getSanitizedUrlParams', () => { describe('getSanitizedUrlParams', () => {
...@@ -606,6 +624,23 @@ describe('SearchPage', () => { ...@@ -606,6 +624,23 @@ describe('SearchPage', () => {
}); });
}); });
describe('renderContent', () => {
it('renders popular tables if searchTerm is empty', () => {
const {props, wrapper} = setup({ searchTerm: '' });
expect(wrapper.instance().renderContent()).toEqual(wrapper.instance().renderPopularTables());
});
it('renders search results when given search term', () => {
const {props, wrapper} = setup({ searchTerm: 'test' });
expect(wrapper.instance().renderContent()).toEqual(wrapper.instance().renderSearchResults());
});
it('renders loading spinner when in loading state', () => {
const {props, wrapper} = setup({ isLoading: true });
expect(wrapper.instance().renderContent()).toEqual(<LoadingSpinner/>);
});
});
describe('renderPopularTables', () => { describe('renderPopularTables', () => {
let content; let content;
let props; let props;
...@@ -727,6 +762,10 @@ describe('mapStateToProps', () => { ...@@ -727,6 +762,10 @@ describe('mapStateToProps', () => {
expect(result.searchTerm).toEqual(globalState.search.search_term); expect(result.searchTerm).toEqual(globalState.search.search_term);
}); });
it('sets isLoading on the props', () => {
expect(result.isLoading).toEqual(globalState.search.isLoading);
});
it('sets popularTables on the props', () => { it('sets popularTables on the props', () => {
expect(result.popularTables).toEqual(globalState.popularTables); expect(result.popularTables).toEqual(globalState.popularTables);
}); });
......
...@@ -12,10 +12,11 @@ import { ...@@ -12,10 +12,11 @@ import {
} from './types'; } from './types';
import { ResourceType } from 'components/common/ResourceListItem/types'; import { ResourceType } from 'components/common/ResourceListItem/types';
export type SearchReducerAction = SearchAllResponse | SearchResourceResponse | SearchAllRequest; export type SearchReducerAction = SearchAllResponse | SearchResourceResponse | SearchAllRequest | SearchResourceRequest;
export interface SearchReducerState { export interface SearchReducerState {
search_term: string; search_term: string;
isLoading: boolean;
dashboards: DashboardSearchResults; dashboards: DashboardSearchResults;
tables: TableSearchResults; tables: TableSearchResults;
users: UserSearchResults; users: UserSearchResults;
...@@ -40,6 +41,7 @@ export function searchResource(resource: ResourceType, term: string, pageIndex: ...@@ -40,6 +41,7 @@ export function searchResource(resource: ResourceType, term: string, pageIndex:
const initialState: SearchReducerState = { const initialState: SearchReducerState = {
search_term: '', search_term: '',
isLoading: false,
dashboards: { dashboards: {
page_index: 0, page_index: 0,
results: [], results: [],
...@@ -64,6 +66,12 @@ export default function reducer(state: SearchReducerState = initialState, action ...@@ -64,6 +66,12 @@ export default function reducer(state: SearchReducerState = initialState, action
return { return {
...state, ...state,
search_term: action.term, search_term: action.term,
isLoading: true,
};
case SearchResource.ACTION:
return {
...state,
isLoading: true,
}; };
// SearchAll will reset all resources with search results or the initial state // SearchAll will reset all resources with search results or the initial state
case SearchAll.SUCCESS: case SearchAll.SUCCESS:
...@@ -71,6 +79,7 @@ export default function reducer(state: SearchReducerState = initialState, action ...@@ -71,6 +79,7 @@ export default function reducer(state: SearchReducerState = initialState, action
return { return {
...initialState, ...initialState,
...newState, ...newState,
isLoading: false,
}; };
// SearchResource will set only a single resource and preserves search state for other resources // SearchResource will set only a single resource and preserves search state for other resources
case SearchResource.SUCCESS: case SearchResource.SUCCESS:
...@@ -78,10 +87,14 @@ export default function reducer(state: SearchReducerState = initialState, action ...@@ -78,10 +87,14 @@ export default function reducer(state: SearchReducerState = initialState, action
return { return {
...state, ...state,
...resourceNewState, ...resourceNewState,
isLoading: false,
}; };
case SearchAll.FAILURE: case SearchAll.FAILURE:
case SearchResource.FAILURE: case SearchResource.FAILURE:
return initialState; return {
...initialState,
isLoading: false,
};
default: default:
return state; return state;
} }
......
...@@ -56,6 +56,7 @@ const globalState: GlobalState = { ...@@ -56,6 +56,7 @@ const globalState: GlobalState = {
], ],
search: { search: {
search_term: 'testName', search_term: 'testName',
isLoading: false,
dashboards: { dashboards: {
page_index: 0, page_index: 0,
results: [], results: [],
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment