Unverified Commit 1c286309 authored by Daniel's avatar Daniel Committed by GitHub

Added `selectedTab` to global search state (#214)

Breadcrumbs will return to the correct search tab and page index

- Added selectedTab to global search state, removed selectedTab from SearchPage state.
- Simplified SearchPage logic. Every action first updates the page URL, which is then read by componentDidUpdate and compared to the current state to determine if an action needs to be taken.
- Split SearchResponsePayload into SearchResponsePayload and searchAllResponsepayload
- Added jest mock functions to mock router history functions.
- Clicking breadcrumb does not re-execute a search
- Fixes redirect issue creating multiple window.history entries.
parent c7607a91
...@@ -4,6 +4,7 @@ import { bindActionCreators } from 'redux'; ...@@ -4,6 +4,7 @@ import { bindActionCreators } from 'redux';
import * as DocumentTitle from 'react-document-title'; import * as DocumentTitle from 'react-document-title';
import * as qs from 'simple-query-string'; import * as qs from 'simple-query-string';
import { RouteComponentProps } from 'react-router'; import { RouteComponentProps } from 'react-router';
import { Search } from 'history';
import AppConfig from 'config/config'; import AppConfig from 'config/config';
import LoadingSpinner from 'components/common/LoadingSpinner'; import LoadingSpinner from 'components/common/LoadingSpinner';
...@@ -13,17 +14,18 @@ import TabsComponent from 'components/common/Tabs'; ...@@ -13,17 +14,18 @@ import TabsComponent from 'components/common/Tabs';
import SearchBar from './SearchBar'; import SearchBar from './SearchBar';
import { GlobalState } from 'ducks/rootReducer'; import { GlobalState } from 'ducks/rootReducer';
import { searchAll, searchResource } from 'ducks/search/reducer'; import { searchAll, searchResource, updateSearchTab } from 'ducks/search/reducer';
import { import {
DashboardSearchResults, DashboardSearchResults,
SearchAllRequest, SearchAllRequest,
SearchResourceRequest, SearchResourceRequest,
SearchResults, SearchResults,
TableSearchResults, TableSearchResults,
UpdateSearchTabRequest,
UserSearchResults, UserSearchResults,
} from 'ducks/search/types'; } from 'ducks/search/types';
import { Resource, ResourceType, SearchAllOptions } from 'interfaces'; import { Resource, ResourceType } from 'interfaces';
// TODO: Use css-modules instead of 'import' // TODO: Use css-modules instead of 'import'
import './styles.scss'; import './styles.scss';
...@@ -44,6 +46,7 @@ import { ...@@ -44,6 +46,7 @@ import {
export interface StateFromProps { export interface StateFromProps {
searchTerm: string; searchTerm: string;
selectedTab: ResourceType;
isLoading: boolean; isLoading: boolean;
tables: TableSearchResults; tables: TableSearchResults;
dashboards: DashboardSearchResults; dashboards: DashboardSearchResults;
...@@ -51,53 +54,80 @@ export interface StateFromProps { ...@@ -51,53 +54,80 @@ export interface StateFromProps {
} }
export interface DispatchFromProps { export interface DispatchFromProps {
searchAll: (term: string, options?: SearchAllOptions) => SearchAllRequest; searchAll: (term: string, selectedTab: ResourceType, pageIndex: number) => SearchAllRequest;
searchResource: (resource: ResourceType, term: string, pageIndex: number) => SearchResourceRequest; searchResource: (term: string, resource: ResourceType, pageIndex: number) => SearchResourceRequest;
updateSearchTab: (selectedTab: ResourceType) => UpdateSearchTabRequest;
} }
export type SearchPageProps = StateFromProps & DispatchFromProps & RouteComponentProps<any>; export type SearchPageProps = StateFromProps & DispatchFromProps & RouteComponentProps<any>;
interface SearchPageState { export class SearchPage extends React.Component<SearchPageProps> {
selectedTab: ResourceType;
}
export class SearchPage extends React.Component<SearchPageProps, SearchPageState> {
public static defaultProps: Partial<SearchPageProps> = {}; public static defaultProps: Partial<SearchPageProps> = {};
constructor(props) { constructor(props) {
super(props); super(props);
this.state = {
selectedTab: ResourceType.table,
};
} }
componentDidMount() { componentDidMount() {
const params = qs.parse(this.props.location.search); const urlSearchParams = this.getUrlParams(this.props.location.search);
const { searchTerm, pageIndex, selectedTab } = params; const globalStateParams = this.getGlobalStateParams();
const { term, index, currentTab } = this.getSanitizedUrlParams(searchTerm, pageIndex, selectedTab);
this.setState({ selectedTab: currentTab }); if (this.shouldUpdateFromGlobalState(urlSearchParams, globalStateParams)) {
if (term !== '') { this.updatePageUrl(globalStateParams.term, globalStateParams.tab, globalStateParams.index, true);
this.props.searchAll(term, this.createSearchOptions(index, currentTab));
if (currentTab !== selectedTab || pageIndex !== index) { } else if (this.shouldUpdateFromUrlParams(urlSearchParams, globalStateParams)) {
this.updatePageUrl(term, currentTab, index); this.props.searchAll(urlSearchParams.term, urlSearchParams.tab, urlSearchParams.index);
} this.updatePageUrl(urlSearchParams.term, urlSearchParams.tab, urlSearchParams.index, true);
} }
} }
componentDidUpdate(prevProps) { shouldUpdateFromGlobalState(urlParams, globalStateParams): boolean {
if (this.props.location.search !== prevProps.location.search) { return urlParams.term === '' && globalStateParams.term !== '';
const params = qs.parse(this.props.location.search); }
const { searchTerm, pageIndex, selectedTab } = params;
const { term, index, currentTab } = this.getSanitizedUrlParams(searchTerm, pageIndex, selectedTab); shouldUpdateFromUrlParams(urlParams, globalStateParams): boolean {
this.setState({ selectedTab: currentTab }); return urlParams.term !== '' && urlParams.term !== globalStateParams.term;
const prevTerm = prevProps.searchTerm; }
if (term !== prevTerm) {
this.props.searchAll(term, this.createSearchOptions(index, currentTab)); componentDidUpdate(prevProps: SearchPageProps) {
} const nextUrlParams = this.getUrlParams(this.props.location.search);
const prevUrlParams = this.getUrlParams(prevProps.location.search);
// If urlParams and globalState are synced, no need to update
if (this.isUrlStateSynced(nextUrlParams)) return;
// Capture any updates in URL
if (this.shouldUpdateSearchTerm(nextUrlParams, prevUrlParams)) {
this.props.searchAll(nextUrlParams.term, nextUrlParams.tab, nextUrlParams.index);
} else if (this.shouldUpdateTab(nextUrlParams, prevUrlParams)) {
this.props.updateSearchTab(nextUrlParams.tab)
} else if (this.shouldUpdatePageIndex(nextUrlParams, prevUrlParams)) {
this.props.searchResource(nextUrlParams.term, nextUrlParams.tab, nextUrlParams.index);
} }
} }
isUrlStateSynced(urlParams): boolean {
const globalStateParams = this.getGlobalStateParams();
return urlParams.term === globalStateParams.term &&
urlParams.tab === globalStateParams.tab &&
urlParams.index === globalStateParams.index;
}
shouldUpdateSearchTerm(nextUrlParams, prevUrlParams): boolean {
return nextUrlParams.term !== prevUrlParams.term;
}
shouldUpdateTab(nextUrlParams, prevUrlParams): boolean {
return nextUrlParams.tab !== prevUrlParams.tab;
}
shouldUpdatePageIndex(nextUrlParams, prevUrlParams): boolean {
return nextUrlParams.index !== prevUrlParams.index;
}
getSelectedTabByResourceType = (newTab: ResourceType): ResourceType => { getSelectedTabByResourceType = (newTab: ResourceType): ResourceType => {
switch(newTab) { switch(newTab) {
case ResourceType.table: case ResourceType.table:
...@@ -105,24 +135,29 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState ...@@ -105,24 +135,29 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState
return newTab; return newTab;
case ResourceType.dashboard: case ResourceType.dashboard:
default: default:
return this.state.selectedTab; return this.props.selectedTab;
} }
}; };
createSearchOptions = (pageIndex: number, selectedTab: ResourceType) => { getUrlParams(search: Search) {
const urlParams = qs.parse(search);
const { searchTerm, pageIndex, selectedTab } = urlParams;
const index = parseInt(pageIndex, 10);
return { return {
dashboardIndex: (selectedTab === ResourceType.dashboard) ? pageIndex : 0, term: (searchTerm || '').trim(),
userIndex: (selectedTab === ResourceType.user) ? pageIndex : 0, tab: this.getSelectedTabByResourceType(selectedTab),
tableIndex: (selectedTab === ResourceType.table) ? pageIndex : 0, index: isNaN(index) ? 0 : index,
}; };
}; };
getSanitizedUrlParams = (searchTerm: string, pageIndex: number, selectedTab: ResourceType) => { getGlobalStateParams() {
const currentTab = this.getSelectedTabByResourceType(selectedTab); return {
const index = pageIndex || 0; term: this.props.searchTerm,
const term = searchTerm ? searchTerm : ""; tab: this.props.selectedTab,
return {term, index, currentTab}; index: this.getPageIndexByResourceType(this.props.selectedTab),
}; };
}
getPageIndexByResourceType = (tab: ResourceType): number => { getPageIndexByResourceType = (tab: ResourceType): number => {
switch(tab) { switch(tab) {
...@@ -137,19 +172,22 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState ...@@ -137,19 +172,22 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState
}; };
onPaginationChange = (index: number): void => { onPaginationChange = (index: number): void => {
this.props.searchResource(this.state.selectedTab, this.props.searchTerm, index); this.updatePageUrl(this.props.searchTerm, this.props.selectedTab, index);
this.updatePageUrl(this.props.searchTerm, this.state.selectedTab, index);
}; };
onTabChange = (tab: ResourceType): void => { onTabChange = (tab: ResourceType): void => {
const currentTab = this.getSelectedTabByResourceType(tab); const newTab = this.getSelectedTabByResourceType(tab);
this.setState({ selectedTab: currentTab }); this.updatePageUrl(this.props.searchTerm, newTab, this.getPageIndexByResourceType(newTab));
this.updatePageUrl(this.props.searchTerm, currentTab, this.getPageIndexByResourceType(currentTab));
}; };
updatePageUrl = (searchTerm: string, tab: ResourceType, pageIndex: number): void => { updatePageUrl = (searchTerm: string, tab: ResourceType, pageIndex: number, replace: boolean = false): void => {
const pathName = `/search?searchTerm=${searchTerm}&selectedTab=${tab}&pageIndex=${pageIndex}`; const pathName = `/search?searchTerm=${searchTerm}&selectedTab=${tab}&pageIndex=${pageIndex}`;
this.props.history.push(pathName);
if (replace) {
this.props.history.replace(pathName);
} else {
this.props.history.push(pathName);
}
}; };
renderSearchResults = () => { renderSearchResults = () => {
...@@ -173,7 +211,7 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState ...@@ -173,7 +211,7 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState
<TabsComponent <TabsComponent
tabs={ tabConfig } tabs={ tabConfig }
defaultTab={ ResourceType.table } defaultTab={ ResourceType.table }
activeKey={ this.state.selectedTab } activeKey={ this.props.selectedTab }
onSelect={ this.onTabChange } onSelect={ this.onTabChange }
/> />
</div> </div>
...@@ -283,6 +321,7 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState ...@@ -283,6 +321,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,
selectedTab: state.search.selectedTab,
isLoading: state.search.isLoading, isLoading: state.search.isLoading,
tables: state.search.tables, tables: state.search.tables,
users: state.search.users, users: state.search.users,
...@@ -291,7 +330,7 @@ export const mapStateToProps = (state: GlobalState) => { ...@@ -291,7 +330,7 @@ export const mapStateToProps = (state: GlobalState) => {
}; };
export const mapDispatchToProps = (dispatch: any) => { export const mapDispatchToProps = (dispatch: any) => {
return bindActionCreators({ searchAll, searchResource } , dispatch); return bindActionCreators({ searchAll, searchResource, updateSearchTab }, dispatch);
}; };
export default connect<StateFromProps, DispatchFromProps>(mapStateToProps, mapDispatchToProps)(SearchPage); export default connect<StateFromProps, DispatchFromProps>(mapStateToProps, mapDispatchToProps)(SearchPage);
...@@ -6,10 +6,9 @@ import { shallow } from 'enzyme'; ...@@ -6,10 +6,9 @@ import { shallow } from 'enzyme';
import AppConfig from 'config/config'; import AppConfig from 'config/config';
import { ResourceType } from 'interfaces'; import { ResourceType } from 'interfaces';
import { SearchPage, SearchPageProps, mapDispatchToProps, mapStateToProps } from '../'; import { mapDispatchToProps, mapStateToProps, SearchPage, SearchPageProps } from '../';
import { import {
DOCUMENT_TITLE_SUFFIX, DOCUMENT_TITLE_SUFFIX,
PAGINATION_PAGE_RANGE,
PAGE_INDEX_ERROR_MESSAGE, PAGE_INDEX_ERROR_MESSAGE,
RESULTS_PER_PAGE, RESULTS_PER_PAGE,
SEARCH_ERROR_MESSAGE_INFIX, SEARCH_ERROR_MESSAGE_INFIX,
...@@ -31,6 +30,7 @@ import LoadingSpinner from 'components/common/LoadingSpinner'; ...@@ -31,6 +30,7 @@ import LoadingSpinner from 'components/common/LoadingSpinner';
import ResourceList from 'components/common/ResourceList'; import ResourceList from 'components/common/ResourceList';
import globalState from 'fixtures/globalState'; import globalState from 'fixtures/globalState';
import { searchAll, updateSearchTab } from 'ducks/search/reducer';
import { getMockRouterProps } from 'fixtures/mockRouter'; import { getMockRouterProps } from 'fixtures/mockRouter';
describe('SearchPage', () => { describe('SearchPage', () => {
...@@ -39,12 +39,14 @@ describe('SearchPage', () => { ...@@ -39,12 +39,14 @@ describe('SearchPage', () => {
const routerProps = getMockRouterProps<any>(null, location); const routerProps = getMockRouterProps<any>(null, location);
const props: SearchPageProps = { const props: SearchPageProps = {
searchTerm: globalState.search.search_term, searchTerm: globalState.search.search_term,
selectedTab: ResourceType.table,
isLoading: false, isLoading: false,
dashboards: globalState.search.dashboards, dashboards: globalState.search.dashboards,
tables: globalState.search.tables, tables: globalState.search.tables,
users: globalState.search.users, users: globalState.search.users,
searchAll: jest.fn(), searchAll: jest.fn(),
searchResource: jest.fn(), searchResource: jest.fn(),
updateSearchTab: jest.fn(),
...routerProps, ...routerProps,
...propOverrides, ...propOverrides,
}; };
...@@ -52,23 +54,18 @@ describe('SearchPage', () => { ...@@ -52,23 +54,18 @@ describe('SearchPage', () => {
return { props, wrapper }; return { props, wrapper };
}; };
describe('constructor', () => {
it('sets the default selectedTab', () => {
const { props, wrapper } = setup();
expect(wrapper.state().selectedTab).toEqual(ResourceType.table);
});
});
describe('componentDidMount', () => { describe('componentDidMount', () => {
let props; let props;
let wrapper; let wrapper;
let mockSearchOptions;
let mockSanitizedUrlParams;
let createSearchOptionsSpy; let getGlobalStateParamsSpy;
let getSanitizedUrlParamsSpy; let shouldUpdateFromGlobalStateSpy;
let searchAllSpy; let shouldUpdateFromUrlParamsSpy;
let getUrlParamsSpy;
let updatePageUrlSpy; let updatePageUrlSpy;
let searchAllSpy;
let mockUrlParams;
let mockGlobalStateParams;
beforeAll(() => { beforeAll(() => {
const setupResult = setup(null, { const setupResult = setup(null, {
...@@ -77,141 +74,158 @@ describe('SearchPage', () => { ...@@ -77,141 +74,158 @@ describe('SearchPage', () => {
props = setupResult.props; props = setupResult.props;
wrapper = setupResult.wrapper; wrapper = setupResult.wrapper;
mockSanitizedUrlParams = { 'term': 'testName', ' index': 1, 'currentTab': 'table' }; getUrlParamsSpy = jest.spyOn(wrapper.instance(), 'getUrlParams');
getSanitizedUrlParamsSpy = jest.spyOn(wrapper.instance(), 'getSanitizedUrlParams').mockImplementation(() => { getGlobalStateParamsSpy = jest.spyOn(wrapper.instance(), 'getGlobalStateParams');
return mockSanitizedUrlParams;
});
mockSearchOptions = { 'dashboardIndex': 0, 'tableIndex': 0, 'userIndex': 1 };
createSearchOptionsSpy = jest.spyOn(wrapper.instance(), 'createSearchOptions').mockImplementation(() => {
return mockSearchOptions;
});
searchAllSpy = jest.spyOn(props, 'searchAll');
updatePageUrlSpy = jest.spyOn(wrapper.instance(), 'updatePageUrl');
setStateSpy.mockClear();
wrapper.instance().componentDidMount(); shouldUpdateFromUrlParamsSpy = jest.spyOn(wrapper.instance(), 'shouldUpdateFromUrlParams');
shouldUpdateFromGlobalStateSpy = jest.spyOn(wrapper.instance(), 'shouldUpdateFromGlobalState');
updatePageUrlSpy = jest.spyOn(wrapper.instance(), 'updatePageUrl');
searchAllSpy = jest.spyOn(props, 'searchAll');
}); });
it('calls setState', () => { it('calls getUrlParams and getGlobalStateParams', () => {
expect(setStateSpy).toHaveBeenCalledWith({ selectedTab: mockSanitizedUrlParams.currentTab }); wrapper.instance().componentDidMount();
expect(getUrlParamsSpy).toHaveBeenCalledWith(props.location.search);
expect(getGlobalStateParamsSpy).toHaveBeenCalled();
}); });
describe('when searchTerm in params is valid', () => { describe('when rendering from GlobalState', () => {
beforeAll(() => { beforeAll(() => {
updatePageUrlSpy.mockClear(); mockUrlParams = { term: '', tab: ResourceType.table, index: 0 };
const {props, wrapper} = setup(null, { mockGlobalStateParams = { term: 'test', tab: ResourceType.table, index: 2 };
search: '/search?searchTerm=testName&selectedTab=table&pageIndex=1', getUrlParamsSpy.mockReset().mockImplementationOnce(() => mockUrlParams);
}); getGlobalStateParamsSpy.mockReset().mockImplementationOnce(() => mockGlobalStateParams);
updatePageUrlSpy = jest.spyOn(wrapper.instance(), 'updatePageUrl');
shouldUpdateFromGlobalStateSpy.mockReset().mockImplementationOnce(() => true);
shouldUpdateFromUrlParamsSpy.mockReset().mockImplementationOnce(() => false);
wrapper.instance().componentDidMount(); wrapper.instance().componentDidMount();
}); });
it('calls searchAll', () => {
expect(searchAllSpy).toHaveBeenCalledWith(mockSanitizedUrlParams.term, mockSearchOptions);
});
it('does not call updateURL', () => { it('calls updatePageUrl with correct parameters', () => {
expect(updatePageUrlSpy).not.toHaveBeenCalled(); expect(updatePageUrlSpy).toHaveBeenCalledWith(mockGlobalStateParams.term, mockGlobalStateParams.tab, mockGlobalStateParams.index, true)
}); });
});
describe('when pageIndex in params is undefined', () => {
let mockSanitizedUrlParams;
let getSanitizedUrlParamsSpy;
let updatePageUrlSpy;
beforeAll(() => { it('does not call shouldUpdateFromUrlParams', () => {
const {props, wrapper} = setup(null, { expect(shouldUpdateFromUrlParamsSpy).not.toHaveBeenCalled()
search: '/search?searchTerm=testName',
});
mockSanitizedUrlParams = { 'term': 'testName', ' index': 0, 'currentTab': 'table' };
getSanitizedUrlParamsSpy = jest.spyOn(wrapper.instance(), 'getSanitizedUrlParams').mockImplementation(() => {
return mockSanitizedUrlParams;
});
updatePageUrlSpy = jest.spyOn(wrapper.instance(), 'updatePageUrl');
wrapper.instance().componentDidMount();
});
it('uses 0 as pageIndex', () => {
expect(updatePageUrlSpy).toHaveBeenCalledWith(mockSanitizedUrlParams.term, mockSanitizedUrlParams.currentTab, mockSanitizedUrlParams.index);
}); });
}); });
describe('when searchTerm in params is undefined', () => { describe('when rendering from URL state', () => {
beforeAll(() => { beforeAll(() => {
mockUrlParams = { term: '', tab: ResourceType.table, index: 0 };
mockGlobalStateParams = { term: 'test', tab: ResourceType.table, index: 2 };
getUrlParamsSpy.mockReset().mockImplementationOnce(() => mockUrlParams);
getGlobalStateParamsSpy.mockReset().mockImplementationOnce(() => mockGlobalStateParams);
searchAllSpy.mockClear(); searchAllSpy.mockClear();
updatePageUrlSpy.mockClear(); updatePageUrlSpy.mockClear();
const {props, wrapper} = setup(null, { shouldUpdateFromGlobalStateSpy.mockReset().mockImplementationOnce(() => false);
search: '/search?selectedTab=table&pageIndex=1', shouldUpdateFromUrlParamsSpy.mockReset().mockImplementationOnce(() => true);
});
updatePageUrlSpy = jest.spyOn(wrapper.instance(), 'updatePageUrl');
wrapper.instance().componentDidMount(); wrapper.instance().componentDidMount();
}); });
it('does not call searchAll', () => {
expect(searchAllSpy).not.toHaveBeenCalled(); it('calls shouldUpdateFromGlobalState with correct params', () => {
expect(searchAllSpy).toHaveBeenCalledWith(mockUrlParams.term, mockUrlParams.tab, mockUrlParams.index);
}); });
it('does not call updatePageUrl', () => { it('calls updatePageUrl with correct params', () => {
expect(updatePageUrlSpy).not.toHaveBeenCalled(); expect(updatePageUrlSpy).toHaveBeenCalledWith(mockUrlParams.term, mockUrlParams.tab, mockUrlParams.index, true);
}); });
}); });
});
describe('when searchTerm in params is empty string', () => { describe('shouldUpdateFromGlobalState', () => {
beforeAll(() => { let wrapper;
searchAllSpy.mockClear();
updatePageUrlSpy.mockClear(); beforeAll(() => {
const {props, wrapper} = setup(null, { const setupResult = setup();
search: '/search?searchTerm=&selectedTab=table&pageIndex=1', wrapper = setupResult.wrapper;
}); });
updatePageUrlSpy = jest.spyOn(wrapper.instance(), 'updatePageUrl');
wrapper.instance().componentDidMount(); describe('when `urlParams.term` is empty and `globalState.term` is initialized', () => {
}); it('returns a value of true', () => {
it('does not call searchAll', () => { const mockUrlParams = { term: '', tab: ResourceType.table, index: 0 };
expect(searchAllSpy).not.toHaveBeenCalled(); const mockGlobalStateParams = { term: 'test', tab: ResourceType.table, index: 2 };
expect(wrapper.instance().shouldUpdateFromGlobalState(mockUrlParams, mockGlobalStateParams)).toBe(true);
}); });
});
it('does not call updatePageUrl', () => { describe('when `urlParams.term` is initialized', () => {
expect(updatePageUrlSpy).not.toHaveBeenCalled(); it('returns a value of false', () => {
const mockUrlParams = { term: 'testTerm', tab: ResourceType.table, index: 0 };
const mockGlobalStateParams = { term: '', tab: ResourceType.table, index: 2 };
expect(wrapper.instance().shouldUpdateFromGlobalState(mockUrlParams, mockGlobalStateParams)).toBe(false);
}); });
}); });
afterAll(() => { describe('when `globalState.term` is empty', () => {
createSearchOptionsSpy.mockRestore(); it('returns a value of false', () => {
const mockUrlParams = { term: '', tab: ResourceType.table, index: 0 };
const mockGlobalStateParams = { term: '', tab: ResourceType.table, index: 0 };
expect(wrapper.instance().shouldUpdateFromGlobalState(mockUrlParams, mockGlobalStateParams)).toBe(false);
});
}); });
}); });
describe('componentDidUpdate', () => { describe('shouldUpdateFromUrlParams', () => {
let searchAllSpy; let wrapper;
let mockSearchOptions; beforeAll(() => {
let mockSanitizedUrlParams; const setupResult = setup();
wrapper = setupResult.wrapper;
});
let createSearchOptionsSpy; describe('when urlParams.term is empty', () => {
let getSanitizedUrlParamsSpy; it('returns a value of false', () => {
let props; const mockUrlParams = { term: '', tab: ResourceType.table, index: 0 };
let wrapper; const mockGlobalStateParams = { term: '', tab: ResourceType.table, index: 0 };
beforeAll(() => { expect(wrapper.instance().shouldUpdateFromUrlParams(mockUrlParams, mockGlobalStateParams)).toBe(false);
const setupResult = setup(null, {
search: '/search?searchTerm=current&selectedTab=table&pageIndex=0',
}); });
props = setupResult.props; });
wrapper = setupResult.wrapper;
mockSanitizedUrlParams = { 'term': 'current', ' index': 0, 'currentTab': 'table' }; describe('when urlParams.term is initialized and equals globalState.term', () => {
getSanitizedUrlParamsSpy = jest.spyOn(wrapper.instance(), 'getSanitizedUrlParams').mockImplementation(() => { it('returns a value of false', () => {
return mockSanitizedUrlParams; const mockUrlParams = { term: 'test', tab: ResourceType.table, index: 0 };
const mockGlobalStateParams = { term: 'test', tab: ResourceType.table, index: 0 };
expect(wrapper.instance().shouldUpdateFromUrlParams(mockUrlParams, mockGlobalStateParams)).toBe(false);
}); });
});
mockSearchOptions = { 'dashboardIndex': 0, 'tableIndex': 1, 'userIndex': 0 }; describe('when urlParams are initialized and not equal to global state', () => {
createSearchOptionsSpy = jest.spyOn(wrapper.instance(), 'createSearchOptions').mockImplementation(() => { it('returns a value of true', () => {
return mockSearchOptions; const mockUrlParams = { term: 'test', tab: ResourceType.table, index: 0 };
const mockGlobalStateParams = { term: '', tab: ResourceType.table, index: 0 };
expect(wrapper.instance().shouldUpdateFromUrlParams(mockUrlParams, mockGlobalStateParams)).toBe(true);
}); });
});
});
describe('componentDidUpdate', () => {
let props;
let wrapper;
searchAllSpy = jest.spyOn(props, 'searchAll'); let mockNextUrlParams;
let mockPrevUrlParams;
let mockPrevProps;
setStateSpy.mockClear(); let getUrlParamsSpy;
let isUrlStateSyncedSpy;
let shouldUpdateSearchTermSpy;
let shouldUpdateTabSpy;
let shouldUpdatePageIndexSpy;
let searchAllSpy;
let updateSearchTabSpy;
let searchResourceSpy;
const mockPrevProps = { beforeAll(() => {
const setupResult = setup(null, {
search: '/search?searchTerm=testName&selectedTab=table&pageIndex=1',
});
props = setupResult.props;
wrapper = setupResult.wrapper;
mockPrevProps = {
searchTerm: 'previous', searchTerm: 'previous',
location: { location: {
search: '/search?searchTerm=previous&selectedTab=table&pageIndex=0', search: '/search?searchTerm=previous&selectedTab=table&pageIndex=0',
...@@ -220,39 +234,185 @@ describe('SearchPage', () => { ...@@ -220,39 +234,185 @@ describe('SearchPage', () => {
hash: 'mockstr', hash: 'mockstr',
} }
}; };
getUrlParamsSpy = jest.spyOn(wrapper.instance(), 'getUrlParams');
isUrlStateSyncedSpy = jest.spyOn(wrapper.instance(), 'isUrlStateSynced');
shouldUpdateSearchTermSpy = jest.spyOn(wrapper.instance(), 'shouldUpdateSearchTerm');
shouldUpdateTabSpy = jest.spyOn(wrapper.instance(), 'shouldUpdateTab');
shouldUpdatePageIndexSpy = jest.spyOn(wrapper.instance(), 'shouldUpdatePageIndex');
searchAllSpy = jest.spyOn(props, 'searchAll');
updateSearchTabSpy = jest.spyOn(props, 'updateSearchTab');
searchResourceSpy = jest.spyOn(props, 'searchResource');
});
it('calls getUrlParams for both current and prev props', () => {
wrapper.instance().componentDidUpdate(mockPrevProps); wrapper.instance().componentDidUpdate(mockPrevProps);
expect(getUrlParamsSpy).toHaveBeenCalledTimes(2);
expect(getUrlParamsSpy).toHaveBeenCalledWith(mockPrevProps.location.search);
expect(getUrlParamsSpy).toHaveBeenCalledWith(props.location.search);
});
it('exits the function when isUrlStateSynced returns true', () => {
isUrlStateSyncedSpy.mockImplementationOnce(() => true);
shouldUpdateSearchTermSpy.mockClear();
shouldUpdateTabSpy.mockClear();
wrapper.instance().componentDidUpdate(mockPrevProps);
expect(shouldUpdateSearchTermSpy).not.toHaveBeenCalled();
expect(shouldUpdateTabSpy).not.toHaveBeenCalled();
});
describe('when the search term has changed', () => {
beforeAll(() => {
mockNextUrlParams = { term: 'new term', tab: ResourceType.table, index: 0 };
mockPrevUrlParams = { term: 'old term', tab: ResourceType.table, index: 0 };
getUrlParamsSpy.mockReset()
.mockImplementationOnce(() => mockNextUrlParams)
.mockImplementationOnce(() => mockPrevUrlParams);
isUrlStateSyncedSpy.mockClear().mockImplementationOnce(() => false);
shouldUpdateSearchTermSpy.mockReset().mockImplementationOnce(() => true);
shouldUpdateTabSpy.mockReset().mockImplementationOnce(() => false);
shouldUpdatePageIndexSpy.mockReset().mockImplementationOnce(() => false);
searchAllSpy.mockClear();
updateSearchTabSpy.mockClear();
searchResourceSpy.mockClear();
wrapper.instance().componentDidUpdate(mockPrevProps);
});
it('calls searchAll', () => {
expect(searchAllSpy).toHaveBeenCalledWith(mockNextUrlParams.term, mockNextUrlParams.tab, mockNextUrlParams.index);
});
it('does not call updateSearchTab nor searchResource', () => {
expect(updateSearchTabSpy).not.toHaveBeenCalled();
expect(searchResourceSpy).not.toHaveBeenCalled();
});
}); });
it('calls setState', () => { describe('when the search tab has changed', () => {
expect(setStateSpy).toHaveBeenCalledWith({ selectedTab: ResourceType.table }); beforeAll(() => {
mockNextUrlParams = { term: 'old term', tab: ResourceType.user, index: 0 };
mockPrevUrlParams = { term: 'old term', tab: ResourceType.table, index: 0 };
getUrlParamsSpy.mockReset()
.mockImplementationOnce(() => mockNextUrlParams)
.mockImplementationOnce(() => mockPrevUrlParams);
isUrlStateSyncedSpy.mockClear().mockImplementationOnce(() => false);
shouldUpdateSearchTermSpy.mockReset().mockImplementationOnce(() => false);
shouldUpdateTabSpy.mockReset().mockImplementationOnce(() => true);
shouldUpdatePageIndexSpy.mockReset().mockImplementationOnce(() => false);
searchAllSpy.mockClear();
updateSearchTabSpy.mockClear();
searchResourceSpy.mockClear();
wrapper.instance().componentDidUpdate(mockPrevProps);
});
it('calls updateSearchTab', () => {
expect(updateSearchTabSpy).toHaveBeenCalledWith(mockNextUrlParams.tab)
});
it('does not call searchAll nor searchResource', () => {
expect(searchAllSpy).not.toHaveBeenCalled();
expect(searchResourceSpy).not.toHaveBeenCalled();
});
}); });
it('calls searchAll if called with a new search term', () => { describe('when the page index has changed', () => {
expect(searchAllSpy).toHaveBeenCalledWith(mockSanitizedUrlParams.term, mockSearchOptions); beforeAll(() => {
mockNextUrlParams = { term: 'old term', tab: ResourceType.table, index: 1 };
mockPrevUrlParams = { term: 'old term', tab: ResourceType.table, index: 0 };
getUrlParamsSpy.mockReset()
.mockImplementationOnce(() => mockNextUrlParams)
.mockImplementationOnce(() => mockPrevUrlParams);
isUrlStateSyncedSpy.mockClear().mockImplementationOnce(() => false);
shouldUpdateSearchTermSpy.mockReset().mockImplementationOnce(() => false);
shouldUpdateTabSpy.mockReset().mockImplementationOnce(() => false);
shouldUpdatePageIndexSpy.mockReset().mockImplementationOnce(() => true);
searchAllSpy.mockClear();
updateSearchTabSpy.mockClear();
searchResourceSpy.mockClear();
wrapper.instance().componentDidUpdate(mockPrevProps);
});
it('calls searchResource', () => {
expect(searchResourceSpy).toHaveBeenCalledWith(mockNextUrlParams.term, mockNextUrlParams.tab, mockNextUrlParams.index)
});
it('does not call searchAll nor updateSearchTab', () => {
expect(searchAllSpy).not.toHaveBeenCalled();
expect(updateSearchTabSpy).not.toHaveBeenCalled();
});
}); });
});
it('does not call searchAll if called with the same search term with a new page', () => { describe('shouldUpdateSearchTerm', () => {
searchAllSpy.mockClear(); const wrapper = setup().wrapper;
const mockPrevProps = {
searchTerm: 'current', it('returns true when the search term is different', () => {
location: { const nextUrlParams = { term: 'new term', tab: ResourceType.table, index: 0 };
search: '/search?searchTerm=current&current=table&pageIndex=1', const prevUrlParams = { term: 'old term', tab: ResourceType.table, index: 0 };
pathname: 'mockstr', expect(wrapper.instance().shouldUpdateSearchTerm(nextUrlParams, prevUrlParams)).toBe(true)
state: jest.fn(), });
hash: 'mockstr',
} it('returns false when the search term is the same', () => {
}; const nextUrlParams = { term: 'old term', tab: ResourceType.table, index: 0 };
wrapper.instance().componentDidUpdate(mockPrevProps); const prevUrlParams = { term: 'old term', tab: ResourceType.table, index: 0 };
expect(searchAllSpy).not.toHaveBeenCalled(); expect(wrapper.instance().shouldUpdateSearchTerm(nextUrlParams, prevUrlParams)).toBe(false)
});
});
describe('shouldUpdateTab', () => {
const wrapper = setup().wrapper;
it('returns true when the tab is different', () => {
const nextUrlParams = { term: 'old term', tab: ResourceType.user, index: 0 };
const prevUrlParams = { term: 'old term', tab: ResourceType.table, index: 0 };
expect(wrapper.instance().shouldUpdateTab(nextUrlParams, prevUrlParams)).toBe(true)
});
it('returns false when the tab is the same', () => {
const nextUrlParams = { term: 'old term', tab: ResourceType.table, index: 0 };
const prevUrlParams = { term: 'old term', tab: ResourceType.table, index: 0 };
expect(wrapper.instance().shouldUpdateTab(nextUrlParams, prevUrlParams)).toBe(false)
});
});
describe('shouldUpdatePageIndex', () => {
const wrapper = setup().wrapper;
it('returns true when the pageIndex is different', () => {
const nextUrlParams = { term: 'old term', tab: ResourceType.table, index: 1 };
const prevUrlParams = { term: 'old term', tab: ResourceType.table, index: 0 };
expect(wrapper.instance().shouldUpdatePageIndex(nextUrlParams, prevUrlParams)).toBe(true)
});
it('returns false when the pageIndex is the same', () => {
const nextUrlParams = { term: 'old term', tab: ResourceType.table, index: 0 };
const prevUrlParams = { term: 'old term', tab: ResourceType.table, index: 0 };
expect(wrapper.instance().shouldUpdatePageIndex(nextUrlParams, prevUrlParams)).toBe(false)
}); });
}); });
describe('getSanitizedUrlParams', () => { describe('getUrlParams', () => {
let props; let props;
let wrapper; let wrapper;
let getSelectedTabByResourceTypeSpy;
let mockSelectedTab; let urlString;
let urlParams;
let getSelectedTabByResourceTypeSpy;
beforeAll(() => { beforeAll(() => {
const setupResult = setup(null, { const setupResult = setup(null, {
...@@ -260,38 +420,51 @@ describe('SearchPage', () => { ...@@ -260,38 +420,51 @@ describe('SearchPage', () => {
}); });
props = setupResult.props; props = setupResult.props;
wrapper = setupResult.wrapper; wrapper = setupResult.wrapper;
const mockResourceType = ResourceType.table;
getSelectedTabByResourceTypeSpy = jest.spyOn(wrapper.instance(), 'getSelectedTabByResourceType').mockImplementation(() => {
return mockResourceType;
});
mockSelectedTab = ResourceType.table; getSelectedTabByResourceTypeSpy = jest.spyOn(wrapper.instance(), 'getSelectedTabByResourceType')
.mockImplementation((selectedTab) => selectedTab);
});
it('parses url params correctly', () => {
urlString = '/search?searchTerm=tableSearch&selectedTab=table&pageIndex=3';
urlParams = wrapper.instance().getUrlParams(urlString);
wrapper.instance().getSanitizedUrlParams('current', 0, mockSelectedTab) expect(getSelectedTabByResourceTypeSpy).toHaveBeenLastCalledWith('table');
expect(urlParams.term).toEqual('tableSearch');
expect(urlParams.tab).toEqual('table');
expect(urlParams.index).toEqual(3);
}); });
it('calls getSelectedTabByResourceType with correct value', () => { it('trims empty spaces from searchTerm', () => {
expect(getSelectedTabByResourceTypeSpy).toHaveBeenCalledWith(mockSelectedTab); urlString = '/search?searchTerm= term%20&selectedTab=user&pageIndex=0';
urlParams = wrapper.instance().getUrlParams(urlString);
expect(urlParams.term).toEqual('term');
}); });
it('output of getSanitizedUrlParams is expected', () => { it('defaults NaN pageIndex as 0', () => {
const expected = {'term': 'current', 'index': 0, 'currentTab': ResourceType.table}; urlString = '/search?searchTerm=current&selectedTab=table&pageIndex=NotANumber';
expect(wrapper.instance().getSanitizedUrlParams('current', 0, ResourceType.table)).toEqual(expected); urlParams = wrapper.instance().getUrlParams(urlString);
expect(urlParams.index).toEqual(0);
}); });
it('output of getSanitizedUrlParams is expected for undefined vars', () => { it('defaults invalid tabs to the current tab', () => {
const expected = {'term': '', 'index': 0, 'currentTab': ResourceType.table}; getSelectedTabByResourceTypeSpy.mockRestore();
expect(wrapper.instance().getSanitizedUrlParams(undefined, undefined, ResourceType.table)).toEqual(expected); getSelectedTabByResourceTypeSpy = jest.spyOn(wrapper.instance(), 'getSelectedTabByResourceType');
urlString = '/search?searchTerm=current&selectedTab=invalidTabType&pageIndex=0';
urlParams = wrapper.instance().getUrlParams(urlString);
expect(getSelectedTabByResourceTypeSpy).toHaveBeenLastCalledWith('invalidTabType');
expect(urlParams.tab).toEqual(props.selectedTab);
}); });
}); });
describe('getSelectedTabByResourceType', () => { describe('getSelectedTabByResourceType', () => {
let props;
let wrapper; let wrapper;
beforeAll(() => { beforeAll(() => {
const setupResult = setup(); const setupResult = setup();
props = setupResult.props;
wrapper = setupResult.wrapper; wrapper = setupResult.wrapper;
}); });
...@@ -303,11 +476,6 @@ describe('SearchPage', () => { ...@@ -303,11 +476,6 @@ describe('SearchPage', () => {
expect(wrapper.instance().getSelectedTabByResourceType(ResourceType.user)).toEqual(ResourceType.user); expect(wrapper.instance().getSelectedTabByResourceType(ResourceType.user)).toEqual(ResourceType.user);
}); });
it('returns state.selectedTab if given equal to ResourceType.dashboard', () => {
wrapper.setState({ selectedTab: 'user' })
expect(wrapper.instance().getSelectedTabByResourceType(ResourceType.dashboard)).toEqual('user');
});
it('returns state.selectedTab in default case', () => { it('returns state.selectedTab in default case', () => {
wrapper.setState({ selectedTab: 'table' }) wrapper.setState({ selectedTab: 'table' })
// @ts-ignore: cover default case // @ts-ignore: cover default case
...@@ -315,40 +483,6 @@ describe('SearchPage', () => { ...@@ -315,40 +483,6 @@ describe('SearchPage', () => {
}); });
}); });
describe('createSearchOptions', () => {
let props;
let wrapper;
beforeAll(() => {
const setupResult = setup();
props = setupResult.props;
wrapper = setupResult.wrapper;
});
it('generates correct options if selectedTab === ResourceType.dashboard', () => {
expect(wrapper.instance().createSearchOptions(5, ResourceType.dashboard)).toMatchObject({
dashboardIndex: 5,
userIndex: 0,
tableIndex: 0,
});
});
it('generates correct options if selectedTab === ResourceType.user', () => {
expect(wrapper.instance().createSearchOptions(5, ResourceType.user)).toMatchObject({
dashboardIndex: 0,
userIndex: 5,
tableIndex: 0,
});
});
it('generates correct options if selectedTab === ResourceType.table', () => {
expect(wrapper.instance().createSearchOptions(5, ResourceType.table)).toMatchObject({
dashboardIndex: 0,
userIndex: 0,
tableIndex: 5,
});
});
});
describe('getPageIndexByResourceType', () => { describe('getPageIndexByResourceType', () => {
let props; let props;
let wrapper; let wrapper;
...@@ -398,12 +532,8 @@ describe('SearchPage', () => { ...@@ -398,12 +532,8 @@ describe('SearchPage', () => {
wrapper.instance().onPaginationChange(testIndex); wrapper.instance().onPaginationChange(testIndex);
}); });
it('calls props.searchResource with correct parameters', () => {
expect(searchResourceSpy).toHaveBeenCalledWith(wrapper.state().selectedTab, props.searchTerm, testIndex);
});
it('calls updatePageUrl with correct parameters', () => { it('calls updatePageUrl with correct parameters', () => {
expect(updatePageUrlSpy).toHaveBeenCalledWith(props.searchTerm, wrapper.state().selectedTab, testIndex); expect(updatePageUrlSpy).toHaveBeenCalledWith(props.searchTerm, props.selectedTab, testIndex);
}); });
}); });
...@@ -437,10 +567,6 @@ describe('SearchPage', () => { ...@@ -437,10 +567,6 @@ describe('SearchPage', () => {
expect(getSelectedTabByResourceTypeSpy).toHaveBeenCalledWith(givenTab); expect(getSelectedTabByResourceTypeSpy).toHaveBeenCalledWith(givenTab);
}); });
it('calls setState with correct parameters', () => {
expect(setStateSpy).toHaveBeenCalledWith({ selectedTab: givenTab });
});
it('calls updatePageUrl with correct parameters', () => { it('calls updatePageUrl with correct parameters', () => {
expect(updatePageUrlSpy).toHaveBeenCalledWith(props.searchTerm, givenTab, mockPageIndex); expect(updatePageUrlSpy).toHaveBeenCalledWith(props.searchTerm, givenTab, mockPageIndex);
}); });
...@@ -452,15 +578,39 @@ describe('SearchPage', () => { ...@@ -452,15 +578,39 @@ describe('SearchPage', () => {
}); });
describe('updatePageUrl', () => { describe('updatePageUrl', () => {
let props;
let wrapper;
let historyPushSpy;
let historyReplaceSpy;
const pageIndex = 2;
const searchTerm = 'testing';
const tab = ResourceType.user;
const expectedPath = `/search?searchTerm=${searchTerm}&selectedTab=${tab}&pageIndex=${pageIndex}`;
beforeAll(() => {
const setupResult = setup();
props = setupResult.props;
wrapper = setupResult.wrapper;
historyPushSpy = jest.spyOn(props.history, 'push');
historyReplaceSpy = jest.spyOn(props.history, 'replace');
});
it('pushes correct update to the window state', () => { it('pushes correct update to the window state', () => {
const { props, wrapper } = setup(); historyPushSpy.mockClear();
const pageIndex = 2; historyReplaceSpy.mockClear();
const searchTerm = 'testing';
const tab = ResourceType.user;
const expectedPath = `/search?searchTerm=${searchTerm}&selectedTab=${tab}&pageIndex=${pageIndex}`;
const historyPushSpy = jest.spyOn(props.history, 'push');
wrapper.instance().updatePageUrl(searchTerm, tab, pageIndex); wrapper.instance().updatePageUrl(searchTerm, tab, pageIndex);
expect(historyPushSpy).toHaveBeenCalledWith(expectedPath); expect(historyPushSpy).toHaveBeenCalledWith(expectedPath);
expect(historyReplaceSpy).not.toHaveBeenCalled();
});
it('calls `history.replace` when replace is set to true', () => {
historyPushSpy.mockClear();
historyReplaceSpy.mockClear();
wrapper.instance().updatePageUrl(searchTerm, tab, pageIndex, true);
expect(historyPushSpy).not.toHaveBeenCalled();
expect(historyReplaceSpy).toHaveBeenCalledWith(expectedPath);
}); });
}); });
...@@ -603,7 +753,7 @@ describe('SearchPage', () => { ...@@ -603,7 +753,7 @@ describe('SearchPage', () => {
AppConfig.indexUsers.enabled = false; AppConfig.indexUsers.enabled = false;
const content = shallow(wrapper.instance().renderSearchResults()); const content = shallow(wrapper.instance().renderSearchResults());
const tabProps = content.find(TabsComponent).props(); const tabProps = content.find(TabsComponent).props();
expect(tabProps.activeKey).toEqual(wrapper.state().selectedTab); expect(tabProps.activeKey).toEqual(props.selectedTab);
expect(tabProps.defaultTab).toEqual(ResourceType.table); expect(tabProps.defaultTab).toEqual(ResourceType.table);
expect(tabProps.onSelect).toEqual(wrapper.instance().onTabChange); expect(tabProps.onSelect).toEqual(wrapper.instance().onTabChange);
......
...@@ -23,7 +23,7 @@ export const Breadcrumb: React.SFC<BreadcrumbProps> = (props) => { ...@@ -23,7 +23,7 @@ export const Breadcrumb: React.SFC<BreadcrumbProps> = (props) => {
path = '/'; path = '/';
text = 'Home'; text = 'Home';
if (props.searchTerm) { if (props.searchTerm) {
path = `/search?searchTerm=${props.searchTerm}&selectedTab=table&pageIndex=0`; path = `/search`;
text = 'Search Results'; text = 'Search Results';
} }
} }
......
...@@ -40,7 +40,7 @@ describe('Breadcrumb', () => { ...@@ -40,7 +40,7 @@ describe('Breadcrumb', () => {
it('renders Link with correct path', () => { it('renders Link with correct path', () => {
expect(subject.find(Link).props()).toMatchObject({ expect(subject.find(Link).props()).toMatchObject({
to: '/search?searchTerm=testTerm&selectedTab=table&pageIndex=0', to: '/search',
}); });
}); });
......
...@@ -6,7 +6,7 @@ import { DashboardSearchResults, TableSearchResults, UserSearchResults } from 'd ...@@ -6,7 +6,7 @@ import { DashboardSearchResults, TableSearchResults, UserSearchResults } from 'd
import globalState from 'fixtures/globalState'; import globalState from 'fixtures/globalState';
import { ResourceType, SearchAllOptions } from 'interfaces'; import { ResourceType } from 'interfaces';
import * as API from '../v0'; import * as API from '../v0';
......
import { ResourceType, SearchAllOptions } from 'interfaces'; import { ResourceType } from 'interfaces';
import { import {
SearchResponsePayload, DashboardSearchResults,
SearchAll, SearchAll,
SearchAllRequest, SearchAllRequest,
SearchAllReset, SearchAllReset,
SearchAllResponse, SearchAllResponse,
SearchAllResponsePayload,
SearchResource, SearchResource,
SearchResponsePayload,
SearchResourceRequest, SearchResourceRequest,
SearchResourceResponse, SearchResourceResponse,
DashboardSearchResults,
TableSearchResults, TableSearchResults,
UpdateSearchTab,
UpdateSearchTabRequest,
UserSearchResults, UserSearchResults,
} from './types'; } from './types';
export interface SearchReducerState { export interface SearchReducerState {
search_term: string; search_term: string;
selectedTab: ResourceType;
isLoading: boolean; isLoading: boolean;
dashboards: DashboardSearchResults; dashboards: DashboardSearchResults;
tables: TableSearchResults; tables: TableSearchResults;
...@@ -23,23 +27,24 @@ export interface SearchReducerState { ...@@ -23,23 +27,24 @@ export interface SearchReducerState {
}; };
/* ACTIONS */ /* ACTIONS */
export function searchAll(term: string, options: SearchAllOptions): SearchAllRequest { export function searchAll(term: string, resource: ResourceType, pageIndex: number): SearchAllRequest {
return { return {
payload: { payload: {
options, resource,
pageIndex,
term, term,
}, },
type: SearchAll.REQUEST, type: SearchAll.REQUEST,
}; };
}; };
export function searchAllSuccess(searchResults: SearchResponsePayload): SearchAllResponse { export function searchAllSuccess(searchResults: SearchAllResponsePayload): SearchAllResponse {
return { type: SearchAll.SUCCESS, payload: searchResults }; return { type: SearchAll.SUCCESS, payload: searchResults };
}; };
export function searchAllFailure(): SearchAllResponse { export function searchAllFailure(): SearchAllResponse {
return { type: SearchAll.FAILURE }; return { type: SearchAll.FAILURE };
}; };
export function searchResource(resource: ResourceType, term: string, pageIndex: number): SearchResourceRequest { export function searchResource(term: string, resource: ResourceType, pageIndex: number): SearchResourceRequest {
return { return {
payload: { payload: {
pageIndex, pageIndex,
...@@ -62,10 +67,19 @@ export function searchReset(): SearchAllReset { ...@@ -62,10 +67,19 @@ export function searchReset(): SearchAllReset {
}; };
}; };
export function updateSearchTab(selectedTab: ResourceType): UpdateSearchTabRequest {
return {
payload: { selectedTab },
type: UpdateSearchTab.REQUEST,
};
}
/* REDUCER */ /* REDUCER */
export const initialState: SearchReducerState = { export const initialState: SearchReducerState = {
search_term: '', search_term: '',
isLoading: false, isLoading: false,
selectedTab: ResourceType.table,
dashboards: { dashboards: {
page_index: 0, page_index: 0,
results: [], results: [],
...@@ -121,6 +135,11 @@ export default function reducer(state: SearchReducerState = initialState, action ...@@ -121,6 +135,11 @@ export default function reducer(state: SearchReducerState = initialState, action
...initialState, ...initialState,
search_term: state.search_term, search_term: state.search_term,
}; };
case UpdateSearchTab.REQUEST:
return {
...state,
selectedTab: (<UpdateSearchTabRequest>action).payload.selectedTab
};
default: default:
return state; return state;
}; };
......
...@@ -13,25 +13,30 @@ import { ...@@ -13,25 +13,30 @@ import {
} from './types'; } from './types';
import { import {
searchAllSuccess, searchAllFailure, initialState, searchAllSuccess, searchAllFailure,
searchResourceSuccess, searchResourceFailure, searchResourceSuccess, searchResourceFailure,
} from './reducer'; } from './reducer';
export function* searchAllWorker(action: SearchAllRequest): SagaIterator { export function* searchAllWorker(action: SearchAllRequest): SagaIterator {
const { options, term } = action.payload; const { resource, pageIndex, term } = action.payload;
const tableIndex = resource === ResourceType.table ? pageIndex : 0;
const userIndex = resource === ResourceType.user ? pageIndex : 0;
const dashboardIndex = resource === ResourceType.dashboard ? pageIndex : 0;
try { try {
const [tableResponse, userResponse, dashboardResponse] = yield all([ const [tableResponse, userResponse, dashboardResponse] = yield all([
call(API.searchResource, options.tableIndex, ResourceType.table, term), call(API.searchResource, tableIndex, ResourceType.table, term),
call(API.searchResource, options.userIndex, ResourceType.user, term), call(API.searchResource, userIndex, ResourceType.user, term),
call(API.searchResource, options.dashboardIndex, ResourceType.dashboard, term), call(API.searchResource, dashboardIndex, ResourceType.dashboard, term),
]); ]);
const searchAllResponse = { const searchAllResponse = {
search_term: term, search_term: term,
tables: tableResponse.tables, selectedTab: resource,
users: userResponse.users, tables: tableResponse.tables || initialState.tables,
dashboards: dashboardResponse.dashboards, users: userResponse.users || initialState.users,
dashboards: dashboardResponse.dashboards || initialState.dashboards,
}; };
yield put({ type: SearchAll.SUCCESS, payload: searchAllResponse }); yield put(searchAllSuccess(searchAllResponse));
} catch (e) { } catch (e) {
yield put(searchAllFailure()); yield put(searchAllFailure());
} }
......
...@@ -2,48 +2,92 @@ import { testSaga } from 'redux-saga-test-plan'; ...@@ -2,48 +2,92 @@ import { testSaga } from 'redux-saga-test-plan';
import { ResourceType } from 'interfaces'; import { ResourceType } from 'interfaces';
import globalState from 'fixtures/globalState';
import * as API from '../api/v0'; import * as API from '../api/v0';
import reducer, { import reducer, {
searchAll, searchAllSuccess, searchAllFailure, initialState,
searchResource, searchResourceSuccess, searchResourceFailure, searchAll,
searchAllFailure,
searchAllSuccess,
SearchReducerState,
searchReset, searchReset,
initialState, SearchReducerState, searchResource,
searchResourceFailure,
searchResourceSuccess,
updateSearchTab,
} from '../reducer'; } from '../reducer';
import { import { searchAllWatcher, searchAllWorker, searchResourceWatcher, searchResourceWorker } from '../sagas';
searchAllWatcher, searchAllWorker, import { SearchAll, SearchAllResponsePayload, SearchResource, SearchResponsePayload, UpdateSearchTab, } from '../types';
searchResourceWatcher, searchResourceWorker
} from '../sagas';
import {
SearchAll, SearchAllRequest, SearchAllResponse,
SearchResource, SearchResourceRequest, SearchResourceResponse,
SearchResponsePayload,
} from '../types';
describe('search ducks', () => { describe('search ducks', () => {
let expectedSearchResults: SearchResponsePayload; const expectedSearchResults: SearchResponsePayload = {
beforeAll(() => { search_term: 'testName',
expectedSearchResults = globalState.search; tables: {
}); page_index: 0,
results: [
{
cluster: 'testCluster',
database: 'testDatabase',
description: 'I have a lot of users',
key: 'testDatabase://testCluster.testSchema/testName',
last_updated_epoch: 946684799,
name: 'testName',
schema_name: 'testSchema',
type: ResourceType.table,
},
],
total_results: 1,
},
};
const expectedSearchAllResults: SearchAllResponsePayload = {
search_term: 'testName',
selectedTab: ResourceType.table,
dashboards: {
page_index: 0,
results: [],
total_results: 0,
},
tables: {
page_index: 0,
results: [
{
cluster: 'testCluster',
database: 'testDatabase',
description: 'I have a lot of users',
key: 'testDatabase://testCluster.testSchema/testName',
last_updated_epoch: 946684799,
name: 'testName',
schema_name: 'testSchema',
type: ResourceType.table,
},
],
total_results: 1,
},
users: {
page_index: 0,
results: [],
total_results: 0,
},
};
describe('actions', () => { describe('actions', () => {
it('searchAll - returns the action to search all resources', () => { it('searchAll - returns the action to search all resources', () => {
const term = 'test'; const term = 'test';
const options = {}; const resource = ResourceType.table;
const action = searchAll(term, options); const pageIndex = 0;
const action = searchAll(term, resource, pageIndex);
const { payload } = action; const { payload } = action;
expect(action.type).toBe(SearchAll.REQUEST); expect(action.type).toBe(SearchAll.REQUEST);
expect(payload.options).toBe(options); expect(payload.resource).toBe(resource);
expect(payload.term).toBe(term); expect(payload.term).toBe(term);
expect(payload.pageIndex).toBe(pageIndex);
}); });
it('searchAllSuccess - returns the action to process the success', () => { it('searchAllSuccess - returns the action to process the success', () => {
const action = searchAllSuccess(expectedSearchResults); const action = searchAllSuccess(expectedSearchAllResults);
const { payload } = action; const { payload } = action;
expect(action.type).toBe(SearchAll.SUCCESS); expect(action.type).toBe(SearchAll.SUCCESS);
expect(payload).toBe(expectedSearchResults); expect(payload).toBe(expectedSearchAllResults);
}); });
it('searchAllFailure - returns the action to process the failure', () => { it('searchAllFailure - returns the action to process the failure', () => {
...@@ -52,10 +96,10 @@ describe('search ducks', () => { ...@@ -52,10 +96,10 @@ describe('search ducks', () => {
}); });
it('searchResource - returns the action to search all resources', () => { it('searchResource - returns the action to search all resources', () => {
const pageIndex = 0;
const resource = ResourceType.table;
const term = 'test'; const term = 'test';
const action = searchResource(resource, term, pageIndex); const resource = ResourceType.table;
const pageIndex = 0;
const action = searchResource(term, resource, pageIndex);
const { payload } = action; const { payload } = action;
expect(action.type).toBe(SearchResource.REQUEST); expect(action.type).toBe(SearchResource.REQUEST);
expect(payload.resource).toBe(resource); expect(payload.resource).toBe(resource);
...@@ -79,6 +123,14 @@ describe('search ducks', () => { ...@@ -79,6 +123,14 @@ describe('search ducks', () => {
const action = searchReset(); const action = searchReset();
expect(action.type).toBe(SearchAll.RESET); expect(action.type).toBe(SearchAll.RESET);
}); });
it('updateSearchTab - returns the action to update the search tab', () => {
const selectedTab = ResourceType.user;
const action = updateSearchTab(selectedTab);
const payload = action.payload;
expect(action.type).toBe(UpdateSearchTab.REQUEST);
expect(payload.selectedTab).toBe(selectedTab);
});
}); });
describe('reducer', () => { describe('reducer', () => {
...@@ -92,8 +144,9 @@ describe('search ducks', () => { ...@@ -92,8 +144,9 @@ describe('search ducks', () => {
it('should handle SearchAll.REQUEST', () => { it('should handle SearchAll.REQUEST', () => {
const term = 'testSearch'; const term = 'testSearch';
const options = {}; const resource = ResourceType.table;
expect(reducer(testState, searchAll(term, options))).toEqual({ const pageIndex = 0;
expect(reducer(testState, searchAll(term, resource, pageIndex))).toEqual({
...testState, ...testState,
search_term: term, search_term: term,
isLoading: true, isLoading: true,
...@@ -101,7 +154,7 @@ describe('search ducks', () => { ...@@ -101,7 +154,7 @@ describe('search ducks', () => {
}); });
it('should handle SearchAll.SUCCESS', () => { it('should handle SearchAll.SUCCESS', () => {
expect(reducer(testState, searchAllSuccess(expectedSearchResults))).toEqual({ expect(reducer(testState, searchAllSuccess(expectedSearchAllResults))).toEqual({
...initialState, ...initialState,
...expectedSearchResults, ...expectedSearchResults,
isLoading: false, isLoading: false,
...@@ -120,7 +173,7 @@ describe('search ducks', () => { ...@@ -120,7 +173,7 @@ describe('search ducks', () => {
}); });
it('should handle SearchResource.REQUEST', () => { it('should handle SearchResource.REQUEST', () => {
expect(reducer(testState, searchResource(ResourceType.table, 'test', 0))).toEqual({ expect(reducer(testState, searchResource('test', ResourceType.table, 0))).toEqual({
...initialState, ...initialState,
isLoading: true, isLoading: true,
}); });
...@@ -140,6 +193,14 @@ describe('search ducks', () => { ...@@ -140,6 +193,14 @@ describe('search ducks', () => {
search_term: testState.search_term, search_term: testState.search_term,
}); });
}); });
it('should handle UpdateSearchTab.REQUEST', () => {
const selectedTab = ResourceType.user;
expect(reducer(testState, updateSearchTab(selectedTab))).toEqual({
...testState,
selectedTab,
});
});
}); });
describe('sagas', () => { describe('sagas', () => {
...@@ -166,7 +227,7 @@ describe('search ducks', () => { ...@@ -166,7 +227,7 @@ describe('search ducks', () => {
});*/ });*/
it('handles request error', () => { it('handles request error', () => {
testSaga(searchAllWorker, searchAll('test', {})) testSaga(searchAllWorker, searchAll('test', ResourceType.table, 0))
.next().throw(new Error()).put(searchAllFailure()) .next().throw(new Error()).put(searchAllFailure())
.next().isDone(); .next().isDone();
}); });
...@@ -185,14 +246,14 @@ describe('search ducks', () => { ...@@ -185,14 +246,14 @@ describe('search ducks', () => {
const pageIndex = 0; const pageIndex = 0;
const resource = ResourceType.table; const resource = ResourceType.table;
const term = 'test'; const term = 'test';
testSaga(searchResourceWorker, searchResource(resource, term, pageIndex)) testSaga(searchResourceWorker, searchResource(term, resource, pageIndex))
.next().call(API.searchResource, pageIndex, resource, term) .next().call(API.searchResource, pageIndex, resource, term)
.next(expectedSearchResults).put(searchResourceSuccess(expectedSearchResults)) .next(expectedSearchResults).put(searchResourceSuccess(expectedSearchResults))
.next().isDone(); .next().isDone();
}); });
it('handles request error', () => { it('handles request error', () => {
testSaga(searchResourceWorker, searchResource(ResourceType.table, 'test', 0)) testSaga(searchResourceWorker, searchResource('test', ResourceType.table, 0))
.next().throw(new Error()).put(searchResourceFailure()) .next().throw(new Error()).put(searchResourceFailure())
.next().isDone(); .next().isDone();
}); });
......
...@@ -2,7 +2,6 @@ import { ...@@ -2,7 +2,6 @@ import {
DashboardResource, DashboardResource,
Resource, Resource,
ResourceType, ResourceType,
SearchAllOptions,
TableResource, TableResource,
UserResource, UserResource,
} from 'interfaces'; } from 'interfaces';
...@@ -18,7 +17,12 @@ export type UserSearchResults = SearchResults<UserResource>; ...@@ -18,7 +17,12 @@ export type UserSearchResults = SearchResults<UserResource>;
export interface SearchResponsePayload { export interface SearchResponsePayload {
search_term: string; search_term: string;
isLoading: boolean; dashboards?: DashboardSearchResults;
tables?: TableSearchResults;
users?: UserSearchResults;
};
export interface SearchAllResponsePayload extends SearchResponsePayload {
selectedTab: ResourceType;
dashboards: DashboardSearchResults; dashboards: DashboardSearchResults;
tables: TableSearchResults; tables: TableSearchResults;
users: UserSearchResults; users: UserSearchResults;
...@@ -32,14 +36,15 @@ export enum SearchAll { ...@@ -32,14 +36,15 @@ export enum SearchAll {
}; };
export interface SearchAllRequest { export interface SearchAllRequest {
payload: { payload: {
options: SearchAllOptions; resource: ResourceType;
pageIndex: number;
term: string; term: string;
}; };
type: SearchAll.REQUEST; type: SearchAll.REQUEST;
}; };
export interface SearchAllResponse { export interface SearchAllResponse {
type: SearchAll.SUCCESS | SearchAll.FAILURE; type: SearchAll.SUCCESS | SearchAll.FAILURE;
payload?: SearchResponsePayload; payload?: SearchAllResponsePayload;
}; };
export interface SearchAllReset { export interface SearchAllReset {
type: SearchAll.RESET; type: SearchAll.RESET;
...@@ -62,3 +67,13 @@ export interface SearchResourceResponse { ...@@ -62,3 +67,13 @@ export interface SearchResourceResponse {
type: SearchResource.SUCCESS | SearchResource.FAILURE; type: SearchResource.SUCCESS | SearchResource.FAILURE;
payload?: SearchResponsePayload; payload?: SearchResponsePayload;
}; };
export enum UpdateSearchTab {
REQUEST = 'amundsen/search/UPDATE_SEARCH_TAB_REQUEST',
}
export interface UpdateSearchTabRequest {
type: UpdateSearchTab.REQUEST;
payload: {
selectedTab: ResourceType;
}
}
...@@ -54,6 +54,7 @@ const globalState: GlobalState = { ...@@ -54,6 +54,7 @@ const globalState: GlobalState = {
], ],
search: { search: {
search_term: 'testName', search_term: 'testName',
selectedTab: ResourceType.table,
isLoading: false, isLoading: false,
dashboards: { dashboards: {
page_index: 0, page_index: 0,
......
...@@ -26,7 +26,7 @@ export function getMockRouterProps<P>(data: P, location: Partial<History.Locatio ...@@ -26,7 +26,7 @@ export function getMockRouterProps<P>(data: P, location: Partial<History.Locatio
action: 'POP', action: 'POP',
location: mockLocation, location: mockLocation,
push: () => {}, push: () => {},
replace: null, replace: () => {},
go: null, go: null,
goBack: null, goBack: null,
goForward: null, goForward: null,
......
export interface SearchAllOptions {
dashboardIndex?: number;
tableIndex?: number;
userIndex?: number;
}
...@@ -2,7 +2,6 @@ export * from './Announcements'; ...@@ -2,7 +2,6 @@ export * from './Announcements';
export * from './Enums'; export * from './Enums';
export * from './Feedback'; export * from './Feedback';
export * from './Resources'; export * from './Resources';
export * from './Search';
export * from './TableMetadata'; export * from './TableMetadata';
export * from './Tags'; export * from './Tags';
export * from './User'; export * from './User';
...@@ -6517,7 +6517,7 @@ ...@@ -6517,7 +6517,7 @@
}, },
"tar": { "tar": {
"version": "4.4.1", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.1.tgz", "resolved": false,
"integrity": "sha512-O+v1r9yN4tOsvl90p5HAP4AEqbYhx4036AGMm075fH9F8Qwi3oJ+v4u50FkT/KkvywNGtwkk0zRI+8eYm1X/xg==", "integrity": "sha512-O+v1r9yN4tOsvl90p5HAP4AEqbYhx4036AGMm075fH9F8Qwi3oJ+v4u50FkT/KkvywNGtwkk0zRI+8eYm1X/xg==",
"optional": true, "optional": true,
"requires": { "requires": {
...@@ -13306,7 +13306,7 @@ ...@@ -13306,7 +13306,7 @@
"react-avatar": { "react-avatar": {
"version": "2.5.1", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/react-avatar/-/react-avatar-2.5.1.tgz", "resolved": "https://registry.npmjs.org/react-avatar/-/react-avatar-2.5.1.tgz",
"integrity": "sha512-bwH5pWY6uxaKZt+IZBfD+SU3Dpy3FaKbmAzrOI4N8SATUPLXOdGaJHWUl6Vl8hHSwWSsoLh/m7xYHdnn0lofZw==", "integrity": "sha1-W76MHQpSWT1GCPs9hinamV7rcJU=",
"requires": { "requires": {
"babel-runtime": ">=5.0.0", "babel-runtime": ">=5.0.0",
"is-retina": "^1.0.3", "is-retina": "^1.0.3",
......
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