Unverified Commit e8893c1a authored by Tamika Tannis's avatar Tamika Tannis Committed by GitHub

Consolidate some areas of search application state workflow (#405)

* clearSearch consolidated with submitSearch

* Remove unused 'setResource' logic from SearchPage

* WIP

* WIP: Consolidate filter actions

* selectedTab --> resource

* Code cleanup

* More code cleanup

* Add back/split up some tests

* Update a type

* Cleanup + add more tests

* ts-ignore

* Cleanup doc wording; Searches when url updates should be LOAD_URL type
parent f51b2aa3
...@@ -16,7 +16,7 @@ module.exports = { ...@@ -16,7 +16,7 @@ module.exports = {
branches: 75, branches: 75,
functions: 80, functions: 80,
lines: 80, lines: 80,
statements: 80, statements: 85,
}, },
'./js/fixtures': { './js/fixtures': {
branches: 100, branches: 100,
......
...@@ -10,14 +10,14 @@ import { SEARCH_BREADCRUMB_TEXT } from './constants'; ...@@ -10,14 +10,14 @@ import { SEARCH_BREADCRUMB_TEXT } from './constants';
import MyBookmarks from 'components/common/Bookmark/MyBookmarks'; import MyBookmarks from 'components/common/Bookmark/MyBookmarks';
import Breadcrumb from 'components/common/Breadcrumb'; import Breadcrumb from 'components/common/Breadcrumb';
import PopularTables from 'components/common/PopularTables'; import PopularTables from 'components/common/PopularTables';
import { SearchAllReset } from 'ducks/search/types'; import { resetSearchState } from 'ducks/search/reducer';
import { searchReset } from 'ducks/search/reducer'; import { UpdateSearchStateReset } from 'ducks/search/types';
import SearchBar from 'components/common/SearchBar'; import SearchBar from 'components/common/SearchBar';
import TagsList from 'components/common/TagsList'; import TagsList from 'components/common/TagsList';
export interface DispatchFromProps { export interface DispatchFromProps {
searchReset: () => SearchAllReset; searchReset: () => UpdateSearchStateReset;
} }
export type HomePageProps = DispatchFromProps & RouteComponentProps<any>; export type HomePageProps = DispatchFromProps & RouteComponentProps<any>;
...@@ -62,7 +62,9 @@ export class HomePage extends React.Component<HomePageProps> { ...@@ -62,7 +62,9 @@ export class HomePage extends React.Component<HomePageProps> {
} }
export const mapDispatchToProps = (dispatch: any) => { export const mapDispatchToProps = (dispatch: any) => {
return bindActionCreators({ searchReset } , dispatch); return bindActionCreators({
searchReset: () => resetSearchState(),
}, dispatch);
}; };
export default connect<DispatchFromProps>(null, mapDispatchToProps)(HomePage); export default connect<DispatchFromProps>(null, mapDispatchToProps)(HomePage);
...@@ -15,7 +15,7 @@ import { getMockRouterProps } from 'fixtures/mockRouter'; ...@@ -15,7 +15,7 @@ import { getMockRouterProps } from 'fixtures/mockRouter';
describe('HomePage', () => { describe('HomePage', () => {
const setup = (propOverrides?: Partial<HomePageProps>) => { const setup = (propOverrides?: Partial<HomePageProps>) => {
const mockLocation = { const mockLocation = {
search: '/search?searchTerm=testName&selectedTab=table&pageIndex=1', search: '/search?searchTerm=testName&resource=table&pageIndex=1',
}; };
const routerProps = getMockRouterProps<any>(null, mockLocation); const routerProps = getMockRouterProps<any>(null, mockLocation);
const props: HomePageProps = { const props: HomePageProps = {
......
...@@ -5,24 +5,24 @@ import { connect } from 'react-redux'; ...@@ -5,24 +5,24 @@ import { connect } from 'react-redux';
import { TABLE_RESOURCE_TITLE, USER_RESOURCE_TITLE } from 'components/SearchPage/constants'; import { TABLE_RESOURCE_TITLE, USER_RESOURCE_TITLE } from 'components/SearchPage/constants';
import AppConfig from 'config/config'; import AppConfig from 'config/config';
import { GlobalState } from 'ducks/rootReducer'; import { GlobalState } from 'ducks/rootReducer';
import { updateSearchState } from 'ducks/search/reducer';
import { import {
DashboardSearchResults, DashboardSearchResults,
SetResourceRequest,
TableSearchResults, TableSearchResults,
UpdateSearchStateRequest,
UserSearchResults UserSearchResults
} from 'ducks/search/types'; } from 'ducks/search/types';
import { ResourceType } from 'interfaces/Resources'; import { ResourceType } from 'interfaces/Resources';
import { setResource } from 'ducks/search/reducer';
export interface StateFromProps { export interface StateFromProps {
selectedTab: ResourceType, resource: ResourceType,
tables: TableSearchResults; tables: TableSearchResults;
dashboards: DashboardSearchResults; dashboards: DashboardSearchResults;
users: UserSearchResults; users: UserSearchResults;
} }
export interface DispatchFromProps { export interface DispatchFromProps {
setResource: (resource: ResourceType) => SetResourceRequest; setResource: (resource: ResourceType) => UpdateSearchStateRequest;
} }
export type ResourceSelectorProps = StateFromProps & DispatchFromProps; export type ResourceSelectorProps = StateFromProps & DispatchFromProps;
...@@ -50,7 +50,7 @@ export class ResourceSelector extends React.Component<ResourceSelectorProps > { ...@@ -50,7 +50,7 @@ export class ResourceSelector extends React.Component<ResourceSelectorProps > {
type="radio" type="radio"
name="resource" name="resource"
value={ option.type } value={ option.type }
checked={ this.props.selectedTab === option.type } checked={ this.props.resource === option.type }
onChange={ this.onChange } onChange={ this.onChange }
/> />
<span className="subtitle-2">{ option.label }</span> <span className="subtitle-2">{ option.label }</span>
...@@ -88,7 +88,7 @@ export class ResourceSelector extends React.Component<ResourceSelectorProps > { ...@@ -88,7 +88,7 @@ export class ResourceSelector extends React.Component<ResourceSelectorProps > {
export const mapStateToProps = (state: GlobalState) => { export const mapStateToProps = (state: GlobalState) => {
return { return {
selectedTab: state.search.selectedTab, resource: state.search.resource,
tables: state.search.tables, tables: state.search.tables,
users: state.search.users, users: state.search.users,
dashboards: state.search.dashboards, dashboards: state.search.dashboards,
...@@ -96,7 +96,9 @@ export const mapStateToProps = (state: GlobalState) => { ...@@ -96,7 +96,9 @@ export const mapStateToProps = (state: GlobalState) => {
}; };
export const mapDispatchToProps = (dispatch: any) => { export const mapDispatchToProps = (dispatch: any) => {
return bindActionCreators({ setResource }, dispatch); return bindActionCreators({
setResource: (resource: ResourceType) => updateSearchState({ resource, updateUrl: true }),
}, dispatch);
}; };
export default connect<StateFromProps, DispatchFromProps>(mapStateToProps, mapDispatchToProps)(ResourceSelector); export default connect<StateFromProps, DispatchFromProps>(mapStateToProps, mapDispatchToProps)(ResourceSelector);
...@@ -16,7 +16,7 @@ import { ResourceType } from 'interfaces/Resources'; ...@@ -16,7 +16,7 @@ import { ResourceType } from 'interfaces/Resources';
describe('ResourceSelector', () => { describe('ResourceSelector', () => {
const setup = (propOverrides?: Partial<ResourceSelectorProps>) => { const setup = (propOverrides?: Partial<ResourceSelectorProps>) => {
const props = { const props = {
selectedTab: ResourceType.table, resource: ResourceType.table,
tables: globalState.search.tables, tables: globalState.search.tables,
users: globalState.search.users, users: globalState.search.users,
dashboards: globalState.search.dashboards, dashboards: globalState.search.dashboards,
...@@ -42,7 +42,7 @@ describe('ResourceSelector', () => { ...@@ -42,7 +42,7 @@ describe('ResourceSelector', () => {
expect(inputProps.type).toEqual("radio"); expect(inputProps.type).toEqual("radio");
expect(inputProps.name).toEqual("resource"); expect(inputProps.name).toEqual("resource");
expect(inputProps.value).toEqual(radioConfig.type); expect(inputProps.value).toEqual(radioConfig.type);
expect(inputProps.checked).toEqual(props.selectedTab === radioConfig.type); expect(inputProps.checked).toEqual(props.resource === radioConfig.type);
expect(inputProps.onChange).toEqual(instance.onChange); expect(inputProps.onChange).toEqual(instance.onChange);
}); });
...@@ -121,8 +121,8 @@ describe('mapStateToProps', () => { ...@@ -121,8 +121,8 @@ describe('mapStateToProps', () => {
result = mapStateToProps(globalState); result = mapStateToProps(globalState);
}); });
it('sets selectedTab on the props', () => { it('sets resource on the props', () => {
expect(result.selectedTab).toEqual(globalState.search.selectedTab); expect(result.resource).toEqual(globalState.search.resource);
}); });
it('sets tables on the props', () => { it('sets tables on the props', () => {
......
...@@ -3,7 +3,7 @@ import { bindActionCreators } from 'redux'; ...@@ -3,7 +3,7 @@ import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { GlobalState } from 'ducks/rootReducer'; import { GlobalState } from 'ducks/rootReducer';
import { clearFilterByCategory, updateFilterByCategory, ClearFilterRequest, UpdateFilterRequest, FilterOptions } from 'ducks/search/filters/reducer'; import { updateFilterByCategory, UpdateFilterRequest, FilterOptions } from 'ducks/search/filters/reducer';
import CheckBoxItem from 'components/common/Inputs/CheckBoxItem'; import CheckBoxItem from 'components/common/Inputs/CheckBoxItem';
...@@ -22,8 +22,7 @@ interface StateFromProps { ...@@ -22,8 +22,7 @@ interface StateFromProps {
} }
interface DispatchFromProps { interface DispatchFromProps {
clearFilterByCategory: (categoryId: string) => ClearFilterRequest; updateFilter: (categoryId: string, checkedValues: FilterOptions | undefined) => UpdateFilterRequest;
updateFilterByCategory: (categoryId: string, value: FilterOptions) => UpdateFilterRequest;
} }
export type CheckBoxFilterProps = OwnProps & DispatchFromProps & StateFromProps; export type CheckBoxFilterProps = OwnProps & DispatchFromProps & StateFromProps;
...@@ -64,10 +63,10 @@ export class CheckBoxFilter extends React.Component<CheckBoxFilterProps> { ...@@ -64,10 +63,10 @@ export class CheckBoxFilter extends React.Component<CheckBoxFilterProps> {
} }
if (Object.keys(checkedValues).length === 0) { if (Object.keys(checkedValues).length === 0) {
this.props.clearFilterByCategory(categoryId); this.props.updateFilter(categoryId, undefined);
} }
else { else {
this.props.updateFilterByCategory(categoryId, checkedValues); this.props.updateFilter(categoryId, checkedValues);
} }
}; };
...@@ -83,7 +82,7 @@ export class CheckBoxFilter extends React.Component<CheckBoxFilterProps> { ...@@ -83,7 +82,7 @@ export class CheckBoxFilter extends React.Component<CheckBoxFilterProps> {
export const mapStateToProps = (state: GlobalState, ownProps: OwnProps) => { export const mapStateToProps = (state: GlobalState, ownProps: OwnProps) => {
const filterState = state.search.filters; const filterState = state.search.filters;
let filterValues = filterState[state.search.selectedTab] ? filterState[state.search.selectedTab][ownProps.categoryId] : {}; let filterValues = filterState[state.search.resource] ? filterState[state.search.resource][ownProps.categoryId] : {};
if (!filterValues) { if (!filterValues) {
filterValues = {}; filterValues = {};
} }
...@@ -95,8 +94,7 @@ export const mapStateToProps = (state: GlobalState, ownProps: OwnProps) => { ...@@ -95,8 +94,7 @@ export const mapStateToProps = (state: GlobalState, ownProps: OwnProps) => {
export const mapDispatchToProps = (dispatch: any) => { export const mapDispatchToProps = (dispatch: any) => {
return bindActionCreators({ return bindActionCreators({
clearFilterByCategory, updateFilter: (categoryId: string, checkedValues: FilterOptions | undefined) => updateFilterByCategory({ categoryId, value: checkedValues }),
updateFilterByCategory,
}, dispatch); }, dispatch);
}; };
......
...@@ -27,8 +27,7 @@ describe('CheckBoxFilter', () => { ...@@ -27,8 +27,7 @@ describe('CheckBoxFilter', () => {
checkedValues: { checkedValues: {
'hive': true, 'hive': true,
}, },
clearFilterByCategory: jest.fn(), updateFilter: jest.fn(),
updateFilterByCategory: jest.fn(),
...propOverrides ...propOverrides
}; };
const wrapper = shallow<CheckBoxFilter>(<CheckBoxFilter {...props} />); const wrapper = shallow<CheckBoxFilter>(<CheckBoxFilter {...props} />);
...@@ -74,32 +73,30 @@ describe('CheckBoxFilter', () => { ...@@ -74,32 +73,30 @@ describe('CheckBoxFilter', () => {
let wrapper; let wrapper;
let mockEvent; let mockEvent;
let clearCategorySpy; let updateFilterSpy;
let updateCategorySpy;
beforeAll(() => { beforeAll(() => {
const setupResult = setup(); const setupResult = setup();
props = setupResult.props; props = setupResult.props;
wrapper = setupResult.wrapper; wrapper = setupResult.wrapper;
clearCategorySpy = jest.spyOn(props, 'clearFilterByCategory'); updateFilterSpy = jest.spyOn(props, 'updateFilter');
updateCategorySpy = jest.spyOn(props, 'updateFilterByCategory');
}) })
it('calls props.clearFilterByCategory if no items will be checked', () => { it('calls props.updateFilter if no items will be checked', () => {
clearCategorySpy.mockClear(); updateFilterSpy.mockClear();
mockEvent = { target: { name: mockCategoryId, value: 'hive', checked: false }}; mockEvent = { target: { name: mockCategoryId, value: 'hive', checked: false }};
wrapper.instance().onCheckboxChange(mockEvent); wrapper.instance().onCheckboxChange(mockEvent);
expect(clearCategorySpy).toHaveBeenCalledWith(mockCategoryId) expect(updateFilterSpy).toHaveBeenCalledWith(mockCategoryId, undefined)
}); });
it('calls props.updateFilterByCategory with expected parameters', () => { it('calls props.updateFilter with expected parameters', () => {
updateCategorySpy.mockClear(); updateFilterSpy.mockClear();
mockEvent = { target: { name: mockCategoryId, value: 'bigquery', checked: true}}; mockEvent = { target: { name: mockCategoryId, value: 'bigquery', checked: true}};
const expectedCheckedValues = { const expectedCheckedValues = {
...props.checkedValues, ...props.checkedValues,
'bigquery': true 'bigquery': true
} }
wrapper.instance().onCheckboxChange(mockEvent); wrapper.instance().onCheckboxChange(mockEvent);
expect(updateCategorySpy).toHaveBeenCalledWith(mockCategoryId, expectedCheckedValues) expect(updateFilterSpy).toHaveBeenCalledWith(mockCategoryId, expectedCheckedValues)
}); });
}); });
...@@ -133,7 +130,7 @@ describe('CheckBoxFilter', () => { ...@@ -133,7 +130,7 @@ describe('CheckBoxFilter', () => {
...globalState, ...globalState,
search: { search: {
...globalState.search, ...globalState.search,
selectedTab: ResourceType.table, resource: ResourceType.table,
filters: { filters: {
[ResourceType.table]: { [ResourceType.table]: {
[mockCategoryId]: mockFilters [mockCategoryId]: mockFilters
...@@ -146,7 +143,7 @@ describe('CheckBoxFilter', () => { ...@@ -146,7 +143,7 @@ describe('CheckBoxFilter', () => {
...globalState, ...globalState,
search: { search: {
...globalState.search, ...globalState.search,
selectedTab: ResourceType.user, resource: ResourceType.user,
filters: { filters: {
[ResourceType.table]: {} [ResourceType.table]: {}
} }
...@@ -183,12 +180,8 @@ describe('CheckBoxFilter', () => { ...@@ -183,12 +180,8 @@ describe('CheckBoxFilter', () => {
result = mapDispatchToProps(dispatch); result = mapDispatchToProps(dispatch);
}); });
it('sets clearFilterByCategory on the props', () => { it('sets updateFilter on the props', () => {
expect(result.clearFilterByCategory).toBeInstanceOf(Function); expect(result.updateFilter).toBeInstanceOf(Function);
});
it('sets updateFilterByCategory on the props', () => {
expect(result.updateFilterByCategory).toBeInstanceOf(Function);
}); });
}); });
}); });
...@@ -2,7 +2,7 @@ import * as React from 'react'; ...@@ -2,7 +2,7 @@ import * as React from 'react';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { clearFilterByCategory, ClearFilterRequest } from 'ducks/search/filters/reducer'; import { updateFilterByCategory, UpdateFilterRequest } from 'ducks/search/filters/reducer';
import { CLEAR_BTN_TEXT } from '../constants'; import { CLEAR_BTN_TEXT } from '../constants';
...@@ -28,7 +28,7 @@ export interface StateFromProps { ...@@ -28,7 +28,7 @@ export interface StateFromProps {
}; };
export interface DispatchFromProps { export interface DispatchFromProps {
clearFilterByCategory: (categoryId: string) => ClearFilterRequest; clearFilter: (categoryId: string) => UpdateFilterRequest;
}; };
export type FilterSectionProps = OwnProps & DispatchFromProps & StateFromProps; export type FilterSectionProps = OwnProps & DispatchFromProps & StateFromProps;
...@@ -39,7 +39,7 @@ export class FilterSection extends React.Component<FilterSectionProps> { ...@@ -39,7 +39,7 @@ export class FilterSection extends React.Component<FilterSectionProps> {
} }
onClearFilter = () => { onClearFilter = () => {
this.props.clearFilterByCategory(this.props.categoryId); this.props.clearFilter(this.props.categoryId);
} }
renderFilterComponent = () => { renderFilterComponent = () => {
...@@ -93,7 +93,7 @@ export class FilterSection extends React.Component<FilterSectionProps> { ...@@ -93,7 +93,7 @@ export class FilterSection extends React.Component<FilterSectionProps> {
export const mapStateToProps = (state: GlobalState, ownProps: OwnProps) => { export const mapStateToProps = (state: GlobalState, ownProps: OwnProps) => {
const filterState = state.search.filters; const filterState = state.search.filters;
const filterValue = filterState[state.search.selectedTab] ? filterState[state.search.selectedTab][ownProps.categoryId] : null; const filterValue = filterState[state.search.resource] ? filterState[state.search.resource][ownProps.categoryId] : null;
let hasValue = false; let hasValue = false;
if (filterValue && ownProps.type === FilterType.CHECKBOX_SELECT) { if (filterValue && ownProps.type === FilterType.CHECKBOX_SELECT) {
Object.keys(filterValue).forEach(key => { Object.keys(filterValue).forEach(key => {
...@@ -110,7 +110,7 @@ export const mapStateToProps = (state: GlobalState, ownProps: OwnProps) => { ...@@ -110,7 +110,7 @@ export const mapStateToProps = (state: GlobalState, ownProps: OwnProps) => {
export const mapDispatchToProps = (dispatch: any) => { export const mapDispatchToProps = (dispatch: any) => {
return bindActionCreators({ return bindActionCreators({
clearFilterByCategory, clearFilter: (categoryId: string) => updateFilterByCategory({ categoryId, value: undefined }),
}, dispatch); }, dispatch);
}; };
......
...@@ -2,7 +2,6 @@ import * as React from 'react'; ...@@ -2,7 +2,6 @@ import * as React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import { GlobalState } from 'ducks/rootReducer'; import { GlobalState } from 'ducks/rootReducer';
import { clearFilterByCategory } from 'ducks/search/filters/reducer';
import { FilterSection, FilterSectionProps, mapDispatchToProps, mapStateToProps } from '../'; import { FilterSection, FilterSectionProps, mapDispatchToProps, mapStateToProps } from '../';
...@@ -19,7 +18,7 @@ describe('FilterSection', () => { ...@@ -19,7 +18,7 @@ describe('FilterSection', () => {
categoryId: 'testId', categoryId: 'testId',
hasValue: true, hasValue: true,
title: 'Category', title: 'Category',
clearFilterByCategory: jest.fn(), clearFilter: jest.fn(),
type: FilterType.INPUT_SELECT, type: FilterType.INPUT_SELECT,
...propOverrides ...propOverrides
}; };
...@@ -36,10 +35,10 @@ describe('FilterSection', () => { ...@@ -36,10 +35,10 @@ describe('FilterSection', () => {
const setupResult = setup(); const setupResult = setup();
props = setupResult.props; props = setupResult.props;
wrapper = setupResult.wrapper; wrapper = setupResult.wrapper;
clearFilterSpy = jest.spyOn(props, 'clearFilterByCategory'); clearFilterSpy = jest.spyOn(props, 'clearFilter');
}); });
it('calls props.clearFilterByCategory with props.categoryId', () => { it('calls props.clearFilter with props.categoryId', () => {
wrapper.instance().onClearFilter(); wrapper.instance().onClearFilter();
expect(clearFilterSpy).toHaveBeenCalledWith(props.categoryId); expect(clearFilterSpy).toHaveBeenCalledWith(props.categoryId);
}) })
...@@ -109,7 +108,7 @@ describe('FilterSection', () => { ...@@ -109,7 +108,7 @@ describe('FilterSection', () => {
...globalState, ...globalState,
search: { search: {
...globalState.search, ...globalState.search,
selectedTab: ResourceType.table, resource: ResourceType.table,
filters: { filters: {
[ResourceType.table]: { [ResourceType.table]: {
'database': { 'hive': true }, 'database': { 'hive': true },
...@@ -123,7 +122,7 @@ describe('FilterSection', () => { ...@@ -123,7 +122,7 @@ describe('FilterSection', () => {
...globalState, ...globalState,
search: { search: {
...globalState.search, ...globalState.search,
selectedTab: ResourceType.user, resource: ResourceType.user,
filters: { filters: {
[ResourceType.table]: {} [ResourceType.table]: {}
} }
...@@ -175,8 +174,8 @@ describe('FilterSection', () => { ...@@ -175,8 +174,8 @@ describe('FilterSection', () => {
result = mapDispatchToProps(dispatch); result = mapDispatchToProps(dispatch);
}); });
it('sets clearFilterByCategory on the props', () => { it('sets clearFilter on the props', () => {
expect(result.clearFilterByCategory).toBeInstanceOf(Function); expect(result.clearFilter).toBeInstanceOf(Function);
}); });
}); });
}); });
...@@ -2,7 +2,7 @@ import * as React from 'react'; ...@@ -2,7 +2,7 @@ import * as React from 'react';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { clearFilterByCategory, updateFilterByCategory, ClearFilterRequest, UpdateFilterRequest } from 'ducks/search/filters/reducer'; import { updateFilterByCategory, UpdateFilterRequest } from 'ducks/search/filters/reducer';
import { APPLY_BTN_TEXT } from '../constants'; import { APPLY_BTN_TEXT } from '../constants';
...@@ -17,8 +17,7 @@ interface StateFromProps { ...@@ -17,8 +17,7 @@ interface StateFromProps {
} }
interface DispatchFromProps { interface DispatchFromProps {
clearFilterByCategory: (categoryId: string) => ClearFilterRequest; updateFilter: (categoryId: string, value: string | undefined) => UpdateFilterRequest;
updateFilterByCategory: (categoryId: string, value: string) => UpdateFilterRequest;
} }
export type InputFilterProps = StateFromProps & DispatchFromProps & OwnProps; export type InputFilterProps = StateFromProps & DispatchFromProps & OwnProps;
...@@ -46,10 +45,10 @@ export class InputFilter extends React.Component<InputFilterProps, InputFilterSt ...@@ -46,10 +45,10 @@ export class InputFilter extends React.Component<InputFilterProps, InputFilterSt
onApplyChanges = (event: React.FormEvent<HTMLFormElement>) => { onApplyChanges = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
if(!!this.state.value) { if(!!this.state.value) {
this.props.updateFilterByCategory(this.props.categoryId, this.state.value); this.props.updateFilter(this.props.categoryId, this.state.value);
} }
else { else {
this.props.clearFilterByCategory(this.props.categoryId); this.props.updateFilter(this.props.categoryId, undefined);
} }
}; };
...@@ -82,7 +81,7 @@ export class InputFilter extends React.Component<InputFilterProps, InputFilterSt ...@@ -82,7 +81,7 @@ export class InputFilter extends React.Component<InputFilterProps, InputFilterSt
export const mapStateToProps = (state: GlobalState, ownProps: OwnProps) => { export const mapStateToProps = (state: GlobalState, ownProps: OwnProps) => {
const filterState = state.search.filters; const filterState = state.search.filters;
const value = filterState[state.search.selectedTab] ? filterState[state.search.selectedTab][ownProps.categoryId] : ''; const value = filterState[state.search.resource] ? filterState[state.search.resource][ownProps.categoryId] : '';
return { return {
value: value || '', value: value || '',
} }
...@@ -90,8 +89,7 @@ export const mapStateToProps = (state: GlobalState, ownProps: OwnProps) => { ...@@ -90,8 +89,7 @@ export const mapStateToProps = (state: GlobalState, ownProps: OwnProps) => {
export const mapDispatchToProps = (dispatch: any) => { export const mapDispatchToProps = (dispatch: any) => {
return bindActionCreators({ return bindActionCreators({
clearFilterByCategory, updateFilter: (categoryId: string, value: string | undefined) => updateFilterByCategory({ categoryId, value }),
updateFilterByCategory,
}, dispatch); }, dispatch);
}; };
......
...@@ -6,7 +6,6 @@ import { InputFilter, InputFilterProps, mapDispatchToProps, mapStateToProps } fr ...@@ -6,7 +6,6 @@ import { InputFilter, InputFilterProps, mapDispatchToProps, mapStateToProps } fr
import { APPLY_BTN_TEXT } from '../../constants'; import { APPLY_BTN_TEXT } from '../../constants';
import { GlobalState } from 'ducks/rootReducer'; import { GlobalState } from 'ducks/rootReducer';
import { clearFilterByCategory, updateFilterByCategory } from 'ducks/search/filters/reducer';
import globalState from 'fixtures/globalState'; import globalState from 'fixtures/globalState';
...@@ -19,8 +18,7 @@ describe('InputFilter', () => { ...@@ -19,8 +18,7 @@ describe('InputFilter', () => {
const props: InputFilterProps = { const props: InputFilterProps = {
categoryId: 'schema', categoryId: 'schema',
value: 'schema_name', value: 'schema_name',
clearFilterByCategory: jest.fn(), updateFilter: jest.fn(),
updateFilterByCategory: jest.fn(),
...propOverrides ...propOverrides
}; };
const wrapper = shallow<InputFilter>(<InputFilter {...props} />); const wrapper = shallow<InputFilter>(<InputFilter {...props} />);
...@@ -78,29 +76,27 @@ describe('InputFilter', () => { ...@@ -78,29 +76,27 @@ describe('InputFilter', () => {
let props; let props;
let wrapper; let wrapper;
let clearCategorySpy; let updateFilterSpy;
let updateCategorySpy;
beforeAll(() => { beforeAll(() => {
const setupResult = setup(); const setupResult = setup();
props = setupResult.props; props = setupResult.props;
wrapper = setupResult.wrapper; wrapper = setupResult.wrapper;
clearCategorySpy = jest.spyOn(props, 'clearFilterByCategory'); updateFilterSpy = jest.spyOn(props, 'updateFilter');
updateCategorySpy = jest.spyOn(props, 'updateFilterByCategory');
}); });
it('calls props.clearFilterByCategory if state.value is falsy', () => { it('calls props.updateFilter if state.value is falsy', () => {
clearCategorySpy.mockClear(); updateFilterSpy.mockClear();
wrapper.setState({ value: '' }); wrapper.setState({ value: '' });
wrapper.instance().onApplyChanges({ preventDefault: jest.fn() }); wrapper.instance().onApplyChanges({ preventDefault: jest.fn() });
expect(clearCategorySpy).toHaveBeenCalledWith(props.categoryId); expect(updateFilterSpy).toHaveBeenCalledWith(props.categoryId, undefined);
}); });
it('calls props.updateFilterByCategory if state.value has a truthy value', () => { it('calls props.updateFilter if state.value has a truthy value', () => {
updateCategorySpy.mockClear(); updateFilterSpy.mockClear();
const mockValue = 'hello'; const mockValue = 'hello';
wrapper.setState({ value: mockValue }); wrapper.setState({ value: mockValue });
wrapper.instance().onApplyChanges({ preventDefault: jest.fn() }); wrapper.instance().onApplyChanges({ preventDefault: jest.fn() });
expect(updateCategorySpy).toHaveBeenCalledWith(props.categoryId, mockValue) expect(updateFilterSpy).toHaveBeenCalledWith(props.categoryId, mockValue);
}); });
}); });
...@@ -162,7 +158,7 @@ describe('InputFilter', () => { ...@@ -162,7 +158,7 @@ describe('InputFilter', () => {
...globalState, ...globalState,
search: { search: {
...globalState.search, ...globalState.search,
selectedTab: ResourceType.table, resource: ResourceType.table,
filters: { filters: {
[ResourceType.table]: { [ResourceType.table]: {
[mockCategoryId]: mockFilters [mockCategoryId]: mockFilters
...@@ -175,7 +171,7 @@ describe('InputFilter', () => { ...@@ -175,7 +171,7 @@ describe('InputFilter', () => {
...globalState, ...globalState,
search: { search: {
...globalState.search, ...globalState.search,
selectedTab: ResourceType.user, resource: ResourceType.user,
filters: { filters: {
[ResourceType.table]: {} [ResourceType.table]: {}
} }
...@@ -212,12 +208,8 @@ describe('InputFilter', () => { ...@@ -212,12 +208,8 @@ describe('InputFilter', () => {
result = mapDispatchToProps(dispatch); result = mapDispatchToProps(dispatch);
}); });
it('sets clearFilterByCategory on the props', () => { it('sets updateFilter on the props', () => {
expect(result.clearFilterByCategory).toBeInstanceOf(Function); expect(result.updateFilter).toBeInstanceOf(Function);
});
it('sets updateFilterByCategory on the props', () => {
expect(result.updateFilterByCategory).toBeInstanceOf(Function);
}); });
}); });
}); });
...@@ -63,7 +63,7 @@ export class SearchFilter extends React.Component<SearchFilterProps> { ...@@ -63,7 +63,7 @@ export class SearchFilter extends React.Component<SearchFilterProps> {
}; };
export const mapStateToProps = (state: GlobalState) => { export const mapStateToProps = (state: GlobalState) => {
const resourceType = state.search.selectedTab; const resourceType = state.search.resource;
const filterCategories = getFilterConfigByResource(resourceType); const filterCategories = getFilterConfigByResource(resourceType);
const filterSections = []; const filterSections = [];
......
...@@ -156,7 +156,7 @@ describe('mapStateToProps', () => { ...@@ -156,7 +156,7 @@ describe('mapStateToProps', () => {
...globalState, ...globalState,
search: { search: {
...globalState.search, ...globalState.search,
selectedTab: ResourceType.table, resource: ResourceType.table,
filters: { filters: {
[ResourceType.table]: { [ResourceType.table]: {
[mockSchemaId]: mockSchemaValue, [mockSchemaId]: mockSchemaValue,
...@@ -170,10 +170,10 @@ describe('mapStateToProps', () => { ...@@ -170,10 +170,10 @@ describe('mapStateToProps', () => {
let getFilterConfigByResourceSpy; let getFilterConfigByResourceSpy;
let result; let result;
it('calls getFilterConfigByResource with selectedTab', () => { it('calls getFilterConfigByResource with resource', () => {
getFilterConfigByResourceSpy = jest.spyOn(ConfigUtils, 'getFilterConfigByResource').mockReturnValue(MOCK_CATEGORY_CONFIG); getFilterConfigByResourceSpy = jest.spyOn(ConfigUtils, 'getFilterConfigByResource').mockReturnValue(MOCK_CATEGORY_CONFIG);
mapStateToProps(mockStateWithFilters); mapStateToProps(mockStateWithFilters);
expect(getFilterConfigByResourceSpy).toHaveBeenCalledWith(mockStateWithFilters.search.selectedTab); expect(getFilterConfigByResourceSpy).toHaveBeenCalledWith(mockStateWithFilters.search.resource);
}); });
it('sets expected filterSections on the result', () => { it('sets expected filterSections on the result', () => {
......
...@@ -12,18 +12,17 @@ import SearchFilter from './SearchFilter'; ...@@ -12,18 +12,17 @@ import SearchFilter from './SearchFilter';
import SearchPanel from './SearchPanel'; import SearchPanel from './SearchPanel';
import { GlobalState } from 'ducks/rootReducer'; import { GlobalState } from 'ducks/rootReducer';
import { setPageIndex, setResource, urlDidUpdate } from 'ducks/search/reducer'; import { submitSearchResource, urlDidUpdate } from 'ducks/search/reducer';
import { import {
DashboardSearchResults, DashboardSearchResults,
SearchResults, SearchResults,
SetPageIndexRequest, SubmitSearchResourceRequest,
SetResourceRequest,
TableSearchResults, TableSearchResults,
UrlDidUpdateRequest, UrlDidUpdateRequest,
UserSearchResults, UserSearchResults,
} from 'ducks/search/types'; } from 'ducks/search/types';
import { Resource, ResourceType } from 'interfaces'; import { Resource, ResourceType, SearchType } from 'interfaces';
// TODO: Use css-modules instead of 'import' // TODO: Use css-modules instead of 'import'
import './styles.scss'; import './styles.scss';
...@@ -43,7 +42,7 @@ import { ...@@ -43,7 +42,7 @@ import {
export interface StateFromProps { export interface StateFromProps {
hasFilters: boolean; hasFilters: boolean;
searchTerm: string; searchTerm: string;
selectedTab: ResourceType; resource: ResourceType;
isLoading: boolean; isLoading: boolean;
tables: TableSearchResults; tables: TableSearchResults;
dashboards: DashboardSearchResults; dashboards: DashboardSearchResults;
...@@ -51,8 +50,7 @@ export interface StateFromProps { ...@@ -51,8 +50,7 @@ export interface StateFromProps {
} }
export interface DispatchFromProps { export interface DispatchFromProps {
setResource: (resource: ResourceType) => SetResourceRequest; setPageIndex: (pageIndex: number) => SubmitSearchResourceRequest;
setPageIndex: (pageIndex: number) => SetPageIndexRequest;
urlDidUpdate: (urlSearch: UrlSearch) => UrlDidUpdateRequest; urlDidUpdate: (urlSearch: UrlSearch) => UrlDidUpdateRequest;
} }
...@@ -76,7 +74,7 @@ export class SearchPage extends React.Component<SearchPageProps> { ...@@ -76,7 +74,7 @@ export class SearchPage extends React.Component<SearchPageProps> {
} }
renderSearchResults = () => { renderSearchResults = () => {
switch(this.props.selectedTab) { switch(this.props.resource) {
case ResourceType.table: case ResourceType.table:
return this.getTabContent(this.props.tables, ResourceType.table); return this.getTabContent(this.props.tables, ResourceType.table);
case ResourceType.user: case ResourceType.user:
...@@ -183,11 +181,11 @@ export class SearchPage extends React.Component<SearchPageProps> { ...@@ -183,11 +181,11 @@ export class SearchPage extends React.Component<SearchPageProps> {
} }
export const mapStateToProps = (state: GlobalState) => { export const mapStateToProps = (state: GlobalState) => {
const resourceFilters = state.search.filters[state.search.selectedTab]; const resourceFilters = state.search.filters[state.search.resource];
return { return {
hasFilters: resourceFilters && Object.keys(resourceFilters).length > 0, hasFilters: resourceFilters && Object.keys(resourceFilters).length > 0,
searchTerm: state.search.search_term, searchTerm: state.search.search_term,
selectedTab: state.search.selectedTab, resource: state.search.resource,
isLoading: state.search.isLoading, isLoading: state.search.isLoading,
tables: state.search.tables, tables: state.search.tables,
users: state.search.users, users: state.search.users,
...@@ -196,7 +194,10 @@ export const mapStateToProps = (state: GlobalState) => { ...@@ -196,7 +194,10 @@ export const mapStateToProps = (state: GlobalState) => {
}; };
export const mapDispatchToProps = (dispatch: any) => { export const mapDispatchToProps = (dispatch: any) => {
return bindActionCreators({ setResource, setPageIndex, urlDidUpdate }, dispatch); return bindActionCreators({
urlDidUpdate,
setPageIndex: (pageIndex: number) => submitSearchResource({ pageIndex, searchType: SearchType.PAGINATION, updateUrl: true }),
}, dispatch);
}; };
export default connect<StateFromProps, DispatchFromProps>(mapStateToProps, mapDispatchToProps)(SearchPage); export default connect<StateFromProps, DispatchFromProps>(mapStateToProps, mapDispatchToProps)(SearchPage);
...@@ -35,12 +35,11 @@ describe('SearchPage', () => { ...@@ -35,12 +35,11 @@ describe('SearchPage', () => {
const props: SearchPageProps = { const props: SearchPageProps = {
hasFilters: false, hasFilters: false,
searchTerm: globalState.search.search_term, searchTerm: globalState.search.search_term,
selectedTab: ResourceType.table, resource: 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,
setResource: jest.fn(),
setPageIndex: jest.fn(), setPageIndex: jest.fn(),
urlDidUpdate: jest.fn(), urlDidUpdate: jest.fn(),
...routerProps, ...routerProps,
...@@ -55,7 +54,7 @@ describe('SearchPage', () => { ...@@ -55,7 +54,7 @@ describe('SearchPage', () => {
let wrapper; let wrapper;
beforeAll(() => { beforeAll(() => {
const setupResult = setup(null, { const setupResult = setup(null, {
search: '/search?searchTerm=testName&selectedTab=table&pageIndex=1', search: '/search?searchTerm=testName&resource=table&pageIndex=1',
}); });
props = setupResult.props; props = setupResult.props;
wrapper = setupResult.wrapper; wrapper = setupResult.wrapper;
...@@ -74,7 +73,7 @@ describe('SearchPage', () => { ...@@ -74,7 +73,7 @@ describe('SearchPage', () => {
let mockPrevProps; let mockPrevProps;
beforeAll(() => { beforeAll(() => {
const setupResult = setup(null, { const setupResult = setup(null, {
search: '/search?searchTerm=testName&selectedTab=table&pageIndex=1', search: '/search?searchTerm=testName&resource=table&pageIndex=1',
}); });
props = setupResult.props; props = setupResult.props;
wrapper = setupResult.wrapper; wrapper = setupResult.wrapper;
...@@ -82,7 +81,7 @@ describe('SearchPage', () => { ...@@ -82,7 +81,7 @@ describe('SearchPage', () => {
mockPrevProps = { mockPrevProps = {
searchTerm: 'previous', searchTerm: 'previous',
location: { location: {
search: '/search?searchTerm=previous&selectedTab=table&pageIndex=0', search: '/search?searchTerm=previous&resource=table&pageIndex=0',
pathname: 'mockstr', pathname: 'mockstr',
state: jest.fn(), state: jest.fn(),
hash: 'mockstr', hash: 'mockstr',
...@@ -224,7 +223,7 @@ describe('SearchPage', () => { ...@@ -224,7 +223,7 @@ describe('SearchPage', () => {
describe('renderSearchResults', () => { describe('renderSearchResults', () => {
it('renders the correct content for table resources', () => { it('renders the correct content for table resources', () => {
const { props, wrapper } = setup({ const { props, wrapper } = setup({
selectedTab: ResourceType.table resource: ResourceType.table
}); });
const getTabContentSpy = jest.spyOn(wrapper.instance(), 'getTabContent'); const getTabContentSpy = jest.spyOn(wrapper.instance(), 'getTabContent');
shallow(wrapper.instance().renderSearchResults()); shallow(wrapper.instance().renderSearchResults());
...@@ -233,7 +232,7 @@ describe('SearchPage', () => { ...@@ -233,7 +232,7 @@ describe('SearchPage', () => {
it('renders the correct content for user resources', () => { it('renders the correct content for user resources', () => {
const { props, wrapper } = setup({ const { props, wrapper } = setup({
selectedTab: ResourceType.user resource: ResourceType.user
}); });
const getTabContentSpy = jest.spyOn(wrapper.instance(), 'getTabContent'); const getTabContentSpy = jest.spyOn(wrapper.instance(), 'getTabContent');
shallow(wrapper.instance().renderSearchResults()); shallow(wrapper.instance().renderSearchResults());
...@@ -242,16 +241,16 @@ describe('SearchPage', () => { ...@@ -242,16 +241,16 @@ describe('SearchPage', () => {
it('renders the correct content for dashboard resources', () => { it('renders the correct content for dashboard resources', () => {
const { props, wrapper } = setup({ const { props, wrapper } = setup({
selectedTab: ResourceType.dashboard resource: ResourceType.dashboard
}); });
const getTabContentSpy = jest.spyOn(wrapper.instance(), 'getTabContent'); const getTabContentSpy = jest.spyOn(wrapper.instance(), 'getTabContent');
shallow(wrapper.instance().renderSearchResults()); shallow(wrapper.instance().renderSearchResults());
expect(getTabContentSpy).toHaveBeenCalledWith(props.dashboards, ResourceType.dashboard); expect(getTabContentSpy).toHaveBeenCalledWith(props.dashboards, ResourceType.dashboard);
}); });
it('renders null for an invalid selectedTab', () => { it('renders null for an invalid resource', () => {
const { props, wrapper } = setup({ const { props, wrapper } = setup({
selectedTab: null resource: null
}); });
const renderedSearchResults = wrapper.instance().renderSearchResults(); const renderedSearchResults = wrapper.instance().renderSearchResults();
expect(renderedSearchResults).toBe(null); expect(renderedSearchResults).toBe(null);
...@@ -313,10 +312,6 @@ describe('mapDispatchToProps', () => { ...@@ -313,10 +312,6 @@ describe('mapDispatchToProps', () => {
result = mapDispatchToProps(dispatch); result = mapDispatchToProps(dispatch);
}); });
it('sets setResource on the props', () => {
expect(result.setResource).toBeInstanceOf(Function);
});
it('sets setPageIndex on the props', () => { it('sets setPageIndex on the props', () => {
expect(result.setPageIndex).toBeInstanceOf(Function); expect(result.setPageIndex).toBeInstanceOf(Function);
}); });
...@@ -336,8 +331,8 @@ describe('mapStateToProps', () => { ...@@ -336,8 +331,8 @@ describe('mapStateToProps', () => {
expect(result.searchTerm).toEqual(globalState.search.search_term); expect(result.searchTerm).toEqual(globalState.search.search_term);
}); });
it('sets selectedTab on the props', () => { it('sets resource on the props', () => {
expect(result.selectedTab).toEqual(globalState.search.selectedTab); expect(result.resource).toEqual(globalState.search.resource);
}); });
it('sets isLoading on the props', () => { it('sets isLoading on the props', () => {
...@@ -361,7 +356,7 @@ describe('mapStateToProps', () => { ...@@ -361,7 +356,7 @@ describe('mapStateToProps', () => {
const testState = { const testState = {
...globalState ...globalState
}; };
testState.search.selectedTab = ResourceType.user; testState.search.resource = ResourceType.user;
testState.search.filters = defaultEmptyFilters; testState.search.filters = defaultEmptyFilters;
result = mapStateToProps(testState); result = mapStateToProps(testState);
expect(result.hasFilters).toBeFalsy(); expect(result.hasFilters).toBeFalsy();
...@@ -371,7 +366,7 @@ describe('mapStateToProps', () => { ...@@ -371,7 +366,7 @@ describe('mapStateToProps', () => {
const testState = { const testState = {
...globalState ...globalState
}; };
testState.search.selectedTab = ResourceType.table; testState.search.resource = ResourceType.table;
testState.search.filters = datasetFilterExample; testState.search.filters = datasetFilterExample;
result = mapStateToProps(testState); result = mapStateToProps(testState);
expect(result.hasFilters).toBe(true); expect(result.hasFilters).toBe(true);
......
import * as React from 'react'; import * as React from 'react';
import { bindActionCreators } from 'redux' import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { ResourceType, Tag } from 'interfaces'; import { ResourceType, Tag, SearchType } from 'interfaces';
import { setSearchInputByResource, SetSearchInputRequest } from 'ducks/search/filters/reducer'; import { submitSearchResource } from 'ducks/search/reducer';
import { SubmitSearchResourceRequest } from 'ducks/search/types';
import { logClick } from 'ducks/utilMethods'; import { logClick } from 'ducks/utilMethods';
import './styles.scss'; import './styles.scss';
...@@ -14,7 +15,7 @@ interface OwnProps { ...@@ -14,7 +15,7 @@ interface OwnProps {
} }
export interface DispatchFromProps { export interface DispatchFromProps {
searchTag: (tagName: string) => SetSearchInputRequest; searchTag: (tagName: string) => SubmitSearchResourceRequest;
} }
export type TagInfoProps = OwnProps & DispatchFromProps; export type TagInfoProps = OwnProps & DispatchFromProps;
...@@ -60,9 +61,14 @@ export class TagInfo extends React.Component<TagInfoProps> { ...@@ -60,9 +61,14 @@ export class TagInfo extends React.Component<TagInfoProps> {
export const mapDispatchToProps = (dispatch: any) => { export const mapDispatchToProps = (dispatch: any) => {
return bindActionCreators({ return bindActionCreators({
/* Note: Pattern intentionally isolates component from extraneous hardcoded parameters */ searchTag: (tagName: string) => submitSearchResource({
/* Note: This will have to be extended to all resources that support tags */ resourceFilters: { 'tag': tagName },
searchTag: (tagName: string) => setSearchInputByResource({ 'tag': tagName }, ResourceType.table, 0, '') resource: ResourceType.table,
pageIndex: 0,
searchTerm: '',
searchType: SearchType.FILTER,
updateUrl: true,
})
}, dispatch); }, dispatch);
}; };
......
...@@ -5,8 +5,8 @@ import { withRouter } from 'react-router-dom'; ...@@ -5,8 +5,8 @@ import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { GlobalState } from 'ducks/rootReducer'; import { GlobalState } from 'ducks/rootReducer';
import { clearSearch, submitSearch, getInlineResultsDebounce, selectInlineResult } from 'ducks/search/reducer'; import { submitSearch, getInlineResultsDebounce, selectInlineResult } from 'ducks/search/reducer';
import { ClearSearchRequest, SubmitSearchRequest, InlineSearchRequest, InlineSearchSelect } from 'ducks/search/types'; import { SubmitSearchRequest, InlineSearchRequest, InlineSearchSelect } from 'ducks/search/types';
import { ResourceType } from 'interfaces'; import { ResourceType } from 'interfaces';
...@@ -25,7 +25,7 @@ export interface StateFromProps { ...@@ -25,7 +25,7 @@ export interface StateFromProps {
} }
export interface DispatchFromProps { export interface DispatchFromProps {
clearSearch?: () => ClearSearchRequest; clearSearch?: () => SubmitSearchRequest;
submitSearch: (searchTerm: string) => SubmitSearchRequest; submitSearch: (searchTerm: string) => SubmitSearchRequest;
onInputChange: (term: string) => InlineSearchRequest; onInputChange: (term: string) => InlineSearchRequest;
onSelectInlineResult: (resourceType: ResourceType, searchTerm: string, updateUrl: boolean) => InlineSearchSelect; onSelectInlineResult: (resourceType: ResourceType, searchTerm: string, updateUrl: boolean) => InlineSearchSelect;
...@@ -190,8 +190,8 @@ export const mapDispatchToProps = (dispatch: any, ownProps) => { ...@@ -190,8 +190,8 @@ export const mapDispatchToProps = (dispatch: any, ownProps) => {
const updateStateOnClear = ownProps.history.location.pathname === '/search'; const updateStateOnClear = ownProps.history.location.pathname === '/search';
return bindActionCreators({ return bindActionCreators({
clearSearch: updateStateOnClear ? clearSearch : null, clearSearch: updateStateOnClear ? () => submitSearch({ useFilters, searchTerm: '' }) : null,
submitSearch: (searchTerm: string) => { return submitSearch(searchTerm, useFilters) }, submitSearch: (searchTerm: string) => submitSearch({ searchTerm, useFilters }),
onInputChange: getInlineResultsDebounce, onInputChange: getInlineResultsDebounce,
onSelectInlineResult: selectInlineResult onSelectInlineResult: selectInlineResult
}, dispatch); }, dispatch);
......
...@@ -23,20 +23,20 @@ import { createIssueWatcher, getIssuesWatcher } from './issue/sagas'; ...@@ -23,20 +23,20 @@ import { createIssueWatcher, getIssuesWatcher } from './issue/sagas';
import { getPopularTablesWatcher } from './popularTables/sagas'; import { getPopularTablesWatcher } from './popularTables/sagas';
// Search // Search
import { import {
clearSearchWatcher,
filterWatcher,
filterWatcher2,
inlineSearchWatcher, inlineSearchWatcher,
inlineSearchWatcherDebounce, inlineSearchWatcherDebounce,
loadPreviousSearchWatcher, loadPreviousSearchWatcher,
searchAllWatcher, searchAllWatcher,
searchResourceWatcher, searchResourceWatcher,
setPageIndexWatcher,
setResourceWatcher,
selectInlineResultsWatcher, selectInlineResultsWatcher,
submitSearchWatcher, submitSearchWatcher,
submitSearchResourceWatcher,
updateSearchStateWatcher,
urlDidUpdateWatcher urlDidUpdateWatcher
} from './search/sagas'; } from './search/sagas';
import {
filterWatcher,
} from './search/filters/sagas';
// TableDetail // TableDetail
import { updateTableOwnerWatcher } from './tableMetadata/owners/sagas'; import { updateTableOwnerWatcher } from './tableMetadata/owners/sagas';
...@@ -70,22 +70,20 @@ export default function* rootSaga() { ...@@ -70,22 +70,20 @@ export default function* rootSaga() {
submitNotificationWatcher(), submitNotificationWatcher(),
// FeedbackForm // FeedbackForm
submitFeedbackWatcher(), submitFeedbackWatcher(),
// Issues // Issues
getIssuesWatcher(), getIssuesWatcher(),
createIssueWatcher(), createIssueWatcher(),
// Search // Search
clearSearchWatcher(),
filterWatcher(), filterWatcher(),
filterWatcher2(),
inlineSearchWatcher(), inlineSearchWatcher(),
inlineSearchWatcherDebounce(), inlineSearchWatcherDebounce(),
loadPreviousSearchWatcher(), loadPreviousSearchWatcher(),
searchAllWatcher(), searchAllWatcher(),
searchResourceWatcher(), searchResourceWatcher(),
selectInlineResultsWatcher(), selectInlineResultsWatcher(),
setPageIndexWatcher(),
setResourceWatcher(),
submitSearchWatcher(), submitSearchWatcher(),
submitSearchResourceWatcher(),
updateSearchStateWatcher(),
urlDidUpdateWatcher(), urlDidUpdateWatcher(),
// PopularTables // PopularTables
getPopularTablesWatcher(), getPopularTablesWatcher(),
......
import { SubmitSearchResource, SubmitSearchResourceRequest } from 'ducks/search/types';
import { ResourceType } from 'interfaces'; import { ResourceType } from 'interfaces';
import { filterFromObj } from 'ducks/utilMethods';
/* ACTION TYPES */ /* ACTION TYPES */
export enum UpdateSearchFilter { export enum UpdateSearchFilter {
CLEAR_ALL = 'amundsen/search/filter/CLEAR_ALL', REQUEST = 'amundsen/search/filter/UPDATE_SEARCH_FILTER_REQUEST',
CLEAR_CATEGORY = 'amundsen/search/filter/CLEAR_CATEGORY',
SET_BY_RESOURCE = 'amundsen/search/filter/SET_BY_RESOURCE',
UPDATE_CATEGORY = 'amundsen/search/filter/UPDATE_CATEGORY',
};
interface ClearAllFiltersRequest {
type: UpdateSearchFilter.CLEAR_ALL;
};
export interface ClearFilterRequest {
payload: {
categoryId: string;
};
type: UpdateSearchFilter.CLEAR_CATEGORY;
}; };
export type UpdateFilterPayload = {
export interface SetSearchInputRequest { categoryId: string;
payload: { value: string | FilterOptions | undefined;
filters: ResourceFilterReducerState; }
pageIndex?: number;
resourceType: ResourceType;
term?: string;
};
type: UpdateSearchFilter.SET_BY_RESOURCE;
};
export interface UpdateFilterRequest { export interface UpdateFilterRequest {
payload: { payload: UpdateFilterPayload;
categoryId: string; type: UpdateSearchFilter.REQUEST;
value: string | FilterOptions;
};
type: UpdateSearchFilter.UPDATE_CATEGORY;
}; };
/* ACTIONS */ /* ACTIONS */
export function clearAllFilters(): ClearAllFiltersRequest { export function updateFilterByCategory({ categoryId, value }: UpdateFilterPayload): UpdateFilterRequest {
return {
type: UpdateSearchFilter.CLEAR_ALL,
};
};
export function clearFilterByCategory(categoryId: string): ClearFilterRequest {
return {
payload: {
categoryId,
},
type: UpdateSearchFilter.CLEAR_CATEGORY,
};
};
export function setSearchInputByResource(filters: ResourceFilterReducerState,
resourceType: ResourceType,
pageIndex?: number,
term?: string): SetSearchInputRequest {
return {
payload: {
filters,
pageIndex,
resourceType,
term
},
type: UpdateSearchFilter.SET_BY_RESOURCE,
};
};
export function updateFilterByCategory(categoryId: string, value: string | FilterOptions): UpdateFilterRequest {
return { return {
payload: { payload: {
categoryId, categoryId,
value value
}, },
type: UpdateSearchFilter.UPDATE_CATEGORY, type: UpdateSearchFilter.REQUEST,
}; };
}; };
...@@ -98,31 +43,17 @@ export const initialFilterState: FilterReducerState = { ...@@ -98,31 +43,17 @@ export const initialFilterState: FilterReducerState = {
[ResourceType.table]: initialTableFilterState, [ResourceType.table]: initialTableFilterState,
}; };
export default function reducer(state: FilterReducerState = initialFilterState, action, resourceType: ResourceType): FilterReducerState { export default function reducer(state: FilterReducerState = initialFilterState, action): FilterReducerState {
const resourceFilters = state[resourceType]; switch (action.type) {
const { payload, type } = action; case SubmitSearchResource.REQUEST:
const { payload } = <SubmitSearchResourceRequest>action;
switch (type) { if (payload.resource && payload.resourceFilters) {
case UpdateSearchFilter.CLEAR_ALL: return {
return initialFilterState; ...state,
case UpdateSearchFilter.CLEAR_CATEGORY: [payload.resource]: payload.resourceFilters
return { };
...state, }
[resourceType]: filterFromObj(resourceFilters, [payload.categoryId]) return state;
};
case UpdateSearchFilter.SET_BY_RESOURCE:
return {
...state,
[payload.resourceType]: payload.filters
};
case UpdateSearchFilter.UPDATE_CATEGORY:
return {
...state,
[resourceType]: {
...resourceFilters,
[payload.categoryId]: payload.value
}
};
default: default:
return state; return state;
}; };
......
import { SagaIterator } from 'redux-saga';
import { takeEvery, put, select } from 'redux-saga/effects';
import { SearchType } from 'interfaces';
import {
submitSearchResource,
} from 'ducks/search/reducer';
import { getSearchState } from 'ducks/search/utils';
import { filterFromObj } from 'ducks/utilMethods';
import {
UpdateSearchFilter,
UpdateFilterRequest,
} from './reducer';
/**
* Listens to actions triggers by user updates to the filter state..
*/
export function* filterWatcher(): SagaIterator {
/*
TODO: If we want to minimize api calls on checkbox quick-select,
we will have to debounce and accumulate filter updates elsewhere.
To be revisited when we have more checkbox filters
*/
yield takeEvery(UpdateSearchFilter.REQUEST, filterWorker);
};
/*
* Generates new filter shape from action payload.
* Then executes a search on current resource based with new filters and current search state values.
*/
export function* filterWorker(action: UpdateFilterRequest): SagaIterator {
const { categoryId, value } = action.payload;
const state = yield select(getSearchState);
const { search_term, resource, filters } = state;
let resourceFilters = {
...filters[resource],
}
if (value === undefined) {
resourceFilters = filterFromObj(resourceFilters, [categoryId])
}
else {
resourceFilters[categoryId] = value;
}
yield put(submitSearchResource({
resource,
resourceFilters,
searchTerm: search_term,
pageIndex: 0,
searchType: SearchType.FILTER,
updateUrl: true,
}));
};
import { ResourceType } from 'interfaces';
import reducer, {
clearAllFilters,
clearFilterByCategory,
setSearchInputByResource,
updateFilterByCategory,
initialFilterState,
FilterReducerState,
UpdateSearchFilter,
} from '../reducer';
describe('filters ducks', () => {
describe('actions', () => {
it('clearAllFilters - returns the action to clear all filters', () => {
const action = clearAllFilters();
expect(action.type).toBe(UpdateSearchFilter.CLEAR_ALL);
});
it('clearFilterByCategory - returns the action to clear the filters for a given category', () => {
const testCategory = 'category';
const action = clearFilterByCategory(testCategory);
const { payload } = action;
expect(action.type).toBe(UpdateSearchFilter.CLEAR_CATEGORY);
expect(payload.categoryId).toBe(testCategory);
});
it('setSearchInputByResource - returns the action to set all search input for a given resource', () => {;
const testResource = ResourceType.table;
const testFilters = { 'column': 'column_name' }
const testIndex = 0;
const testTerm = 'test';
const action = setSearchInputByResource(testFilters, testResource, testIndex, testTerm);
const { payload } = action;
expect(action.type).toBe(UpdateSearchFilter.SET_BY_RESOURCE);
expect(payload.resourceType).toBe(testResource);
expect(payload.filters).toBe(testFilters);
expect(payload.pageIndex).toBe(testIndex);
expect(payload.term).toBe(testTerm);
});
it('updateFilterByCategory - returns the action to update the filters for a given category', () => {;
const testCategory = 'column';
const testValue = 'column_name';
const action = updateFilterByCategory(testCategory, testValue);
const { payload } = action;
expect(action.type).toBe(UpdateSearchFilter.UPDATE_CATEGORY);
expect(payload.categoryId).toBe(testCategory);
expect(payload.value).toBe(testValue);
});
});
describe('reducer', () => {
let testState: FilterReducerState;
beforeAll(() => {
testState = {
[ResourceType.table]: {
'column': 'column_name'
}
}
});
it('should return the existing state if action is not handled', () => {
expect(reducer(testState, { type: 'INVALID.ACTION' }, ResourceType.table)).toBe(testState);
});
it('returns initial reducer state on UpdateSearchFilter.CLEAR_ALL', () => {
expect(reducer(testState, clearAllFilters(), ResourceType.table)).toBe(initialFilterState);
});
it('removes the given category from the current resource filter state on UpdateSearchFilter.CLEAR_CATEGORY', () => {
const givenResource = ResourceType.table;
const givenCategory = 'column';
expect(testState[givenResource][givenCategory]).toBeTruthy();
const result = reducer(testState, clearFilterByCategory(givenCategory), givenResource);
expect(result[givenResource][givenCategory]).toBe(undefined);
});
it('sets the given filters on the given resource on UpdateSearchFilter.SET_BY_RESOURCE', () => {
const givenResource = ResourceType.table;
const givenFilters = {
'column': 'column_name',
'schema': 'schema_name',
'database': { 'testDb': true }
}
const result = reducer(initialFilterState, setSearchInputByResource(givenFilters, givenResource), givenResource);
expect(result[givenResource]).toBe(givenFilters);
});
it('sets the given category on the filter state to the given value on UpdateSearchFilter.UPDATE_CATEGORY', () => {
const givenResource = ResourceType.table;
const givenCategory = 'database';
const givenValue = { 'testDb': true }
const result = reducer(initialFilterState, updateFilterByCategory(givenCategory, givenValue), givenResource);
expect(result[givenResource][givenCategory]).toBe(givenValue);
});
});
});
import { ResourceType, SearchType } from 'interfaces';
import { submitSearchResource } from 'ducks/search/reducer';
import reducer, {
updateFilterByCategory,
initialFilterState,
FilterReducerState,
UpdateSearchFilter,
} from '../reducer';
describe('filters reducer', () => {
describe('actions', () => {
it('updateFilterByCategory - returns the action to update the filters for a given category', () => {;
const testCategory = 'column';
const testValue = 'column_name';
const action = updateFilterByCategory({ categoryId: testCategory, value: testValue });
const { payload } = action;
expect(action.type).toBe(UpdateSearchFilter.REQUEST);
expect(payload.categoryId).toBe(testCategory);
expect(payload.value).toBe(testValue);
});
});
describe('reducer', () => {
let testState: FilterReducerState;
beforeAll(() => {
testState = {
[ResourceType.table]: {
'column': 'column_name'
}
}
});
it('should return the existing state if action is not handled', () => {
expect(reducer(testState, { type: 'INVALID.ACTION' })).toBe(testState);
});
describe('handles SubmitSearchResource.REQUEST', () => {
it('updates the filter state if request contains filter information', () => {
const givenResource = ResourceType.table;
const givenFilters = {'database': { 'testDb': true }}
const result = reducer(initialFilterState, submitSearchResource({
resource: givenResource,
resourceFilters: givenFilters,
searchTerm: 'test',
pageIndex: 0,
searchType: SearchType.FILTER,
updateUrl: true,
}));
expect(result[givenResource]).toBe(givenFilters);
});
it('does not update the filter state if request does not contains filter information', () => {
const givenResource = ResourceType.table;
const givenFilters = {'database': { 'testDb': true }}
const result = reducer(testState, submitSearchResource({
pageIndex: 1,
searchType: SearchType.PAGINATION,
updateUrl: true,
}));
expect(result).toBe(testState);
});
})
});
});
import { testSaga } from 'redux-saga-test-plan';
import { takeEvery } from 'redux-saga/effects';
import { submitSearchResource } from 'ducks/search/reducer';
import * as SearchUtils from 'ducks/search/utils';
import globalState from 'fixtures/globalState';
import { datasetFilterExample } from 'fixtures/search/filters';
import { ResourceType, SearchType } from 'interfaces';
import {
updateFilterByCategory,
UpdateSearchFilter,
} from '../reducer';
import * as Sagas from '../sagas';
describe('filter sagas', () => {
describe('filterWatcher', () => {
it('debounces update filter actions with filterWorker', () => {
testSaga(Sagas.filterWatcher)
.next()
.is(takeEvery(UpdateSearchFilter.REQUEST, Sagas.filterWorker))
.next().isDone();
});
});
describe('filterWorker', () => {
let mockSearchStateWithFilters;
beforeAll(() => {
mockSearchStateWithFilters = {
...globalState.search,
filters: datasetFilterExample,
};
});
/* TODO: Library has issue rectifying {} vs [Object] */
/*it('puts expected actions for clearing a filter', () => {
testSaga(Sagas.filterWorker, updateFilterByCategory({ categoryId: 'database', value: undefined }))
.next().select(SearchUtils.getSearchState).next(mockSearchStateWithFilters)
.put(submitSearchResource({
resource: mockSearchStateWithFilters.resource,
resourceFilters: {},
searchTerm: mockSearchStateWithFilters.search_term,
pageIndex: 0,
searchType: SearchType.FILTER,
updateUrl: true,
}))
.next().isDone();
});*/
it('puts expected actions for updating a filter', () => {
const testCategoryId = 'database';
const testValue = { 'hive': true };
testSaga(Sagas.filterWorker, updateFilterByCategory({ categoryId: testCategoryId, value: testValue }))
.next().select(SearchUtils.getSearchState).next(globalState.search)
.put(submitSearchResource({
resource: mockSearchStateWithFilters.resource,
resourceFilters: { [testCategoryId]: testValue },
searchTerm: mockSearchStateWithFilters.search_term,
pageIndex: 0,
searchType: SearchType.FILTER,
updateUrl: true,
}))
.next().isDone();
});
});
});
...@@ -2,13 +2,12 @@ import { ResourceType, SearchType} from 'interfaces'; ...@@ -2,13 +2,12 @@ import { ResourceType, SearchType} from 'interfaces';
import { Search as UrlSearch } from 'history'; import { Search as UrlSearch } from 'history';
import filterReducer, { initialFilterState, UpdateSearchFilter, FilterReducerState } from './filters/reducer'; import filterReducer, { initialFilterState, FilterReducerState } from './filters/reducer';
import { import {
DashboardSearchResults, DashboardSearchResults,
SearchAll, SearchAll,
SearchAllRequest, SearchAllRequest,
SearchAllReset,
SearchAllResponse, SearchAllResponse,
SearchAllResponsePayload, SearchAllResponsePayload,
SearchResource, SearchResource,
...@@ -24,18 +23,24 @@ import { ...@@ -24,18 +23,24 @@ import {
InlineSearchUpdate, InlineSearchUpdate,
TableSearchResults, TableSearchResults,
UserSearchResults, UserSearchResults,
ClearSearch,
ClearSearchRequest,
SubmitSearchRequest, SubmitSearchRequest,
SubmitSearch, SubmitSearch,
SetResourceRequest, SubmitSearchResourcePayload,
SetResource, SubmitSearchResourceRequest,
SetPageIndexRequest, SetPageIndex, LoadPreviousSearchRequest, LoadPreviousSearch, UrlDidUpdateRequest, UrlDidUpdate, SubmitSearchResource,
LoadPreviousSearchRequest,
LoadPreviousSearch,
UpdateSearchStateRequest,
UpdateSearchStateReset,
UpdateSearchStatePayload,
UpdateSearchState,
UrlDidUpdateRequest,
UrlDidUpdate
} from './types'; } from './types';
export interface SearchReducerState { export interface SearchReducerState {
search_term: string; search_term: string;
selectedTab: ResourceType; resource: ResourceType;
isLoading: boolean; isLoading: boolean;
dashboards: DashboardSearchResults; dashboards: DashboardSearchResults;
tables: TableSearchResults; tables: TableSearchResults;
...@@ -108,6 +113,7 @@ export function getInlineResultsSuccess(inlineResults: InlineSearchResponsePaylo ...@@ -108,6 +113,7 @@ export function getInlineResultsSuccess(inlineResults: InlineSearchResponsePaylo
export function getInlineResultsFailure(): InlineSearchResponse { export function getInlineResultsFailure(): InlineSearchResponse {
return { type: InlineSearch.FAILURE }; return { type: InlineSearch.FAILURE };
}; };
export function selectInlineResult(resourceType: ResourceType, searchTerm: string, updateUrl: boolean = false): InlineSearchSelect { export function selectInlineResult(resourceType: ResourceType, searchTerm: string, updateUrl: boolean = false): InlineSearchSelect {
return { return {
payload: { payload: {
...@@ -118,6 +124,7 @@ export function selectInlineResult(resourceType: ResourceType, searchTerm: strin ...@@ -118,6 +124,7 @@ export function selectInlineResult(resourceType: ResourceType, searchTerm: strin
type: InlineSearch.SELECT type: InlineSearch.SELECT
}; };
}; };
export function updateFromInlineResult(data: InlineSearchUpdatePayload): InlineSearchUpdate { export function updateFromInlineResult(data: InlineSearchUpdatePayload): InlineSearchUpdate {
return { return {
payload: data, payload: data,
...@@ -125,36 +132,33 @@ export function updateFromInlineResult(data: InlineSearchUpdatePayload): InlineS ...@@ -125,36 +132,33 @@ export function updateFromInlineResult(data: InlineSearchUpdatePayload): InlineS
}; };
}; };
export function searchReset(): SearchAllReset { export function submitSearch({ searchTerm, useFilters } : { searchTerm: string, useFilters: boolean }): SubmitSearchRequest {
return {
type: SearchAll.RESET,
};
};
export function submitSearch(searchTerm: string, useFilters: boolean = false): SubmitSearchRequest {
return { return {
payload: { searchTerm, useFilters }, payload: { searchTerm, useFilters },
type: SubmitSearch.REQUEST, type: SubmitSearch.REQUEST,
}; };
}; };
export function clearSearch(): ClearSearchRequest { export function submitSearchResource({ resourceFilters, pageIndex, searchTerm, resource, searchType, updateUrl } : SubmitSearchResourcePayload): SubmitSearchResourceRequest {
return { return {
type: ClearSearch.REQUEST, payload: { resourceFilters, pageIndex, searchTerm, resource, searchType, updateUrl },
type: SubmitSearchResource.REQUEST,
}; };
}; };
export function setResource(resource: ResourceType, updateUrl: boolean = true): SetResourceRequest { export function updateSearchState({ filters, resource, updateUrl }: UpdateSearchStatePayload): UpdateSearchStateRequest {
return { return {
payload: { resource, updateUrl }, payload: {
type: SetResource.REQUEST, filters,
resource,
updateUrl,
},
type: UpdateSearchState.REQUEST,
}; };
}; };
export function resetSearchState(): UpdateSearchStateReset {
export function setPageIndex(pageIndex: number, updateUrl: boolean = true): SetPageIndexRequest {
return { return {
payload: { pageIndex, updateUrl }, type: UpdateSearchState.RESET,
type: SetPageIndex.REQUEST,
}; };
}; };
...@@ -164,14 +168,13 @@ export function loadPreviousSearch(): LoadPreviousSearchRequest { ...@@ -164,14 +168,13 @@ export function loadPreviousSearch(): LoadPreviousSearchRequest {
}; };
}; };
export function urlDidUpdate(urlSearch: UrlSearch): UrlDidUpdateRequest{ export function urlDidUpdate(urlSearch: UrlSearch): UrlDidUpdateRequest {
return { return {
payload: { urlSearch }, payload: { urlSearch },
type: UrlDidUpdate.REQUEST, type: UrlDidUpdate.REQUEST,
}; };
}; };
/* REDUCER */ /* REDUCER */
export const initialInlineResultsState = { export const initialInlineResultsState = {
isLoading: false, isLoading: false,
...@@ -189,7 +192,7 @@ export const initialInlineResultsState = { ...@@ -189,7 +192,7 @@ export const initialInlineResultsState = {
export const initialState: SearchReducerState = { export const initialState: SearchReducerState = {
search_term: '', search_term: '',
isLoading: false, isLoading: false,
selectedTab: ResourceType.table, resource: ResourceType.table,
dashboards: { dashboards: {
page_index: 0, page_index: 0,
results: [], results: [],
...@@ -211,28 +214,29 @@ export const initialState: SearchReducerState = { ...@@ -211,28 +214,29 @@ export const initialState: SearchReducerState = {
export default function reducer(state: SearchReducerState = initialState, action): SearchReducerState { export default function reducer(state: SearchReducerState = initialState, action): SearchReducerState {
switch (action.type) { switch (action.type) {
case UpdateSearchFilter.SET_BY_RESOURCE: case SubmitSearch.REQUEST:
return { return {
...state, ...state,
search_term: action.payload.term, isLoading: true,
filters: filterReducer(state.filters, action, state.selectedTab), search_term: action.payload.searchTerm,
} }
case UpdateSearchFilter.CLEAR_ALL: case SubmitSearchResource.REQUEST:
return { return {
...state, ...state,
filters: filterReducer(state.filters, action, state.selectedTab), isLoading: true,
filters: filterReducer(state.filters, action),
search_term: action.payload.searchTerm || state.search_term,
} }
case UpdateSearchFilter.CLEAR_CATEGORY: case UpdateSearchState.REQUEST:
case UpdateSearchFilter.UPDATE_CATEGORY: const payload = action.payload;
return { return {
...state, ...state,
isLoading: true, filters: payload.filters || state.filters,
filters: filterReducer(state.filters, action, state.selectedTab), resource: payload.resource || state.resource,
} }
case SearchAll.RESET: case UpdateSearchState.RESET:
return initialState; return initialState;
case SearchAll.REQUEST: case SearchAll.REQUEST:
// updates search term to reflect action
return { return {
...state, ...state,
inlineResults: { inlineResults: {
...@@ -273,16 +277,11 @@ export default function reducer(state: SearchReducerState = initialState, action ...@@ -273,16 +277,11 @@ export default function reducer(state: SearchReducerState = initialState, action
...initialState, ...initialState,
search_term: state.search_term, search_term: state.search_term,
}; };
case SetResource.REQUEST:
return {
...state,
selectedTab: (<SetResourceRequest>action).payload.resource
};
case InlineSearch.UPDATE: case InlineSearch.UPDATE:
const { searchTerm, selectedTab, tables, users } = (<InlineSearchUpdate>action).payload; const { searchTerm, resource, tables, users } = (<InlineSearchUpdate>action).payload;
return { return {
...state, ...state,
selectedTab, resource,
tables, tables,
users, users,
search_term: searchTerm, search_term: searchTerm,
......
import { DEFAULT_RESOURCE_TYPE, ResourceType } from 'interfaces';
import * as SearchUtils from 'ducks/search/utils';
import globalState from 'fixtures/globalState';
const searchState = globalState.search;
describe('search utils', () => {
describe('getSearchState', () => {
it('returns the search state', () => {
const result = SearchUtils.getSearchState(globalState);
expect(result).toEqual(searchState);
});
});
describe('getPageIndex', () => {
const mockState = {
...searchState,
resource: ResourceType.dashboard,
dashboards: {
...searchState.dashboards,
page_index: 1,
},
tables: {
...searchState.tables,
page_index: 2,
},
users: {
...searchState.users,
page_index: 3,
}
};
it('given ResourceType.dashboard, returns page_index for dashboards', () => {
expect(SearchUtils.getPageIndex(mockState, ResourceType.dashboard)).toEqual(mockState.dashboards.page_index);
});
it('given ResourceType.table, returns page_index for table', () => {
expect(SearchUtils.getPageIndex(mockState, ResourceType.table)).toEqual(mockState.tables.page_index);
});
it('given ResourceType.user, returns page_index for users', () => {
expect(SearchUtils.getPageIndex(mockState, ResourceType.user)).toEqual(mockState.users.page_index);
});
it('given no resource, returns page_index for the selected resource', () => {
const resourceToUse = mockState[mockState.resource + 's'];
expect(SearchUtils.getPageIndex(mockState)).toEqual(resourceToUse.page_index);
});
it('returns 0 if not given a supported ResourceType', () => {
// @ts-ignore: cover default case
expect(SearchUtils.getPageIndex(mockState, 'not valid input')).toEqual(0);
});
});
describe('autoSelectResource', () => {
const emptyMockState = {
...searchState,
dashboards: {
...searchState.dashboards,
total_results: 0,
},
tables: {
...searchState.tables,
total_results: 0,
},
users: {
...searchState.users,
total_results: 0,
}
};
it('returns the DEFAULT_RESOURCE_TYPE when search results are empty', () => {
expect(SearchUtils.autoSelectResource(emptyMockState)).toEqual(DEFAULT_RESOURCE_TYPE);
});
it('prefers `table` over `user` and `dashboard`', () => {
const mockState = { ...emptyMockState };
mockState.tables.total_results = 10;
mockState.users.total_results = 10;
mockState.dashboards.total_results = 10;
expect(SearchUtils.autoSelectResource(mockState)).toEqual(ResourceType.table);
});
it('prefers `user` over `dashboard`', () => {
const mockState = { ...emptyMockState };
mockState.tables.total_results = 0;
mockState.users.total_results = 10;
mockState.dashboards.total_results = 10;
expect(SearchUtils.autoSelectResource(mockState)).toEqual(ResourceType.user);
});
it('returns `dashboard` if there are dashboards but no other results', () => {
const mockState = { ...emptyMockState };
mockState.tables.total_results = 0;
mockState.users.total_results = 0;
mockState.dashboards.total_results = 10;
expect(SearchUtils.autoSelectResource(mockState)).toEqual(ResourceType.dashboard);
});
});
});
...@@ -9,6 +9,11 @@ import { ...@@ -9,6 +9,11 @@ import {
UserResource, UserResource,
} from 'interfaces'; } from 'interfaces';
import {
FilterReducerState,
ResourceFilterReducerState,
} from 'ducks/search/filters/reducer';
export interface SearchResults<T extends Resource> { export interface SearchResults<T extends Resource> {
page_index: number; page_index: number;
total_results: number; total_results: number;
...@@ -25,7 +30,7 @@ export interface SearchResponsePayload { ...@@ -25,7 +30,7 @@ export interface SearchResponsePayload {
users?: UserSearchResults; users?: UserSearchResults;
}; };
export interface SearchAllResponsePayload extends SearchResponsePayload { export interface SearchAllResponsePayload extends SearchResponsePayload {
selectedTab: ResourceType; resource: ResourceType;
dashboards: DashboardSearchResults; dashboards: DashboardSearchResults;
tables: TableSearchResults; tables: TableSearchResults;
users: UserSearchResults; users: UserSearchResults;
...@@ -36,7 +41,7 @@ export interface InlineSearchResponsePayload { ...@@ -36,7 +41,7 @@ export interface InlineSearchResponsePayload {
}; };
export interface InlineSearchUpdatePayload { export interface InlineSearchUpdatePayload {
searchTerm: string; searchTerm: string;
selectedTab: ResourceType; resource: ResourceType;
tables: TableSearchResults; tables: TableSearchResults;
users: UserSearchResults; users: UserSearchResults;
}; };
...@@ -46,7 +51,6 @@ export enum SearchAll { ...@@ -46,7 +51,6 @@ export enum SearchAll {
REQUEST = 'amundsen/search/SEARCH_ALL_REQUEST', REQUEST = 'amundsen/search/SEARCH_ALL_REQUEST',
SUCCESS = 'amundsen/search/SEARCH_ALL_SUCCESS', SUCCESS = 'amundsen/search/SEARCH_ALL_SUCCESS',
FAILURE = 'amundsen/search/SEARCH_ALL_FAILURE', FAILURE = 'amundsen/search/SEARCH_ALL_FAILURE',
RESET = 'amundsen/search/SEARCH_ALL_RESET',
}; };
export interface SearchAllRequest { export interface SearchAllRequest {
payload: { payload: {
...@@ -62,10 +66,6 @@ export interface SearchAllResponse { ...@@ -62,10 +66,6 @@ export interface SearchAllResponse {
type: SearchAll.SUCCESS | SearchAll.FAILURE; type: SearchAll.SUCCESS | SearchAll.FAILURE;
payload?: SearchAllResponsePayload; payload?: SearchAllResponsePayload;
}; };
export interface SearchAllReset {
type: SearchAll.RESET;
};
export enum SearchResource { export enum SearchResource {
REQUEST = 'amundsen/search/SEARCH_RESOURCE_REQUEST', REQUEST = 'amundsen/search/SEARCH_RESOURCE_REQUEST',
...@@ -124,42 +124,44 @@ export enum SubmitSearch { ...@@ -124,42 +124,44 @@ export enum SubmitSearch {
export interface SubmitSearchRequest { export interface SubmitSearchRequest {
payload: { payload: {
searchTerm: string; searchTerm: string;
useFilters?: boolean; useFilters: boolean;
}; };
type: SubmitSearch.REQUEST; type: SubmitSearch.REQUEST;
}; };
export enum ClearSearch { export enum SubmitSearchResource {
REQUEST = 'amundsen/search/CLEAR_SEARCH_REQUEST', REQUEST = 'amundsen/search/SUBMIT_SEARCH_RESOURCE_REQUEST',
}; };
export interface ClearSearchRequest { export type SubmitSearchResourcePayload = {
type: ClearSearch.REQUEST; pageIndex: number;
searchType: SearchType;
updateUrl?: boolean;
resourceFilters?: ResourceFilterReducerState;
searchTerm?: string;
resource?: ResourceType;
}
export interface SubmitSearchResourceRequest {
payload: SubmitSearchResourcePayload;
type: SubmitSearchResource.REQUEST;
}; };
export enum SetResource { export enum UpdateSearchState {
REQUEST = 'amundsen/search/SET_RESOURCE_REQUEST', REQUEST = 'amundsen/search/UPDATE_SEARCH_STATE',
}; RESET = 'amundsen/search/RESET_SEARCH_STATE',
export interface SetResourceRequest {
payload: {
resource: ResourceType;
updateUrl: boolean;
};
type: SetResource.REQUEST;
}; };
export type UpdateSearchStatePayload = {
filters?: FilterReducerState;
export enum SetPageIndex { resource?: ResourceType;
REQUEST = 'amundsen/search/SET_PAGE_INDEX_REQUEST', updateUrl?: boolean;
}
export interface UpdateSearchStateRequest {
payload?: UpdateSearchStatePayload;
type: UpdateSearchState.REQUEST;
}; };
export interface SetPageIndexRequest { export interface UpdateSearchStateReset {
payload: { type: UpdateSearchState.RESET;
pageIndex: number;
updateUrl: boolean;
};
type: SetPageIndex.REQUEST;
}; };
export enum LoadPreviousSearch { export enum LoadPreviousSearch {
REQUEST = 'amundsen/search/LOAD_PREVIOUS_SEARCH_REQUEST', REQUEST = 'amundsen/search/LOAD_PREVIOUS_SEARCH_REQUEST',
}; };
...@@ -167,7 +169,6 @@ export interface LoadPreviousSearchRequest { ...@@ -167,7 +169,6 @@ export interface LoadPreviousSearchRequest {
type: LoadPreviousSearch.REQUEST; type: LoadPreviousSearch.REQUEST;
}; };
export enum UrlDidUpdate { export enum UrlDidUpdate {
REQUEST = 'amundsen/search/URL_DID_UPDATE_REQUEST', REQUEST = 'amundsen/search/URL_DID_UPDATE_REQUEST',
}; };
......
...@@ -13,7 +13,7 @@ can be the combination of multiple responses. ...@@ -13,7 +13,7 @@ can be the combination of multiple responses.
*/ */
export const getPageIndex = (state: Partial<SearchReducerState>, resource?: ResourceType) => { export const getPageIndex = (state: Partial<SearchReducerState>, resource?: ResourceType) => {
resource = resource || state.selectedTab; resource = resource || state.resource;
switch(resource) { switch(resource) {
case ResourceType.table: case ResourceType.table:
return state.tables.page_index; return state.tables.page_index;
......
...@@ -66,7 +66,7 @@ const globalState: GlobalState = { ...@@ -66,7 +66,7 @@ const globalState: GlobalState = {
], ],
search: { search: {
search_term: 'testName', search_term: 'testName',
selectedTab: ResourceType.table, resource: ResourceType.table,
isLoading: false, isLoading: false,
dashboards: { dashboards: {
page_index: 0, page_index: 0,
......
...@@ -31,4 +31,5 @@ We use [Jest](https://jestjs.io/) as our test framework. We leverage utility met ...@@ -31,4 +31,5 @@ We use [Jest](https://jestjs.io/) as our test framework. We leverage utility met
#### Redux #### Redux
1. Because the majority of Redux code consists of functions, we unit test those methods as usual and ensure the functions produce the expected output for any given input. See Redux's documentation on testing [action creators](https://redux.js.org/recipes/writing-tests#action-creators), [async action creators](https://redux.js.org/recipes/writing-tests#async-action-creators), and [reducers](https://redux.js.org/recipes/writing-tests#reducers), or check out examples in our code. 1. Because the majority of Redux code consists of functions, we unit test those methods as usual and ensure the functions produce the expected output for any given input. See Redux's documentation on testing [action creators](https://redux.js.org/recipes/writing-tests#action-creators), [async action creators](https://redux.js.org/recipes/writing-tests#async-action-creators), and [reducers](https://redux.js.org/recipes/writing-tests#reducers), or check out examples in our code.
2. `redux-saga` generator functions can be tested by iterating through it step-by-step and running assertions at each step, or by executing the entire saga and running assertions on the side effects. See redux-saga's documentation on [testing sagas](https://redux-saga.js.org/docs/advanced/Testing.html) for a wider breadth of examples. 2. Unless an action creator includes any logic other than returning the action, unit testing the reducer and saga middleware logic is sufficient and provides the most value.
3. `redux-saga` generator functions can be tested by iterating through it step-by-step and running assertions at each step, or by executing the entire saga and running assertions on the side effects. See redux-saga's documentation on [testing sagas](https://redux-saga.js.org/docs/advanced/Testing.html) for a wider breadth of examples.
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