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 = {
branches: 75,
functions: 80,
lines: 80,
statements: 80,
statements: 85,
},
'./js/fixtures': {
branches: 100,
......
......@@ -10,14 +10,14 @@ import { SEARCH_BREADCRUMB_TEXT } from './constants';
import MyBookmarks from 'components/common/Bookmark/MyBookmarks';
import Breadcrumb from 'components/common/Breadcrumb';
import PopularTables from 'components/common/PopularTables';
import { SearchAllReset } from 'ducks/search/types';
import { searchReset } from 'ducks/search/reducer';
import { resetSearchState } from 'ducks/search/reducer';
import { UpdateSearchStateReset } from 'ducks/search/types';
import SearchBar from 'components/common/SearchBar';
import TagsList from 'components/common/TagsList';
export interface DispatchFromProps {
searchReset: () => SearchAllReset;
searchReset: () => UpdateSearchStateReset;
}
export type HomePageProps = DispatchFromProps & RouteComponentProps<any>;
......@@ -62,7 +62,9 @@ export class HomePage extends React.Component<HomePageProps> {
}
export const mapDispatchToProps = (dispatch: any) => {
return bindActionCreators({ searchReset } , dispatch);
return bindActionCreators({
searchReset: () => resetSearchState(),
}, dispatch);
};
export default connect<DispatchFromProps>(null, mapDispatchToProps)(HomePage);
......@@ -15,7 +15,7 @@ import { getMockRouterProps } from 'fixtures/mockRouter';
describe('HomePage', () => {
const setup = (propOverrides?: Partial<HomePageProps>) => {
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 props: HomePageProps = {
......
......@@ -5,24 +5,24 @@ import { connect } from 'react-redux';
import { TABLE_RESOURCE_TITLE, USER_RESOURCE_TITLE } from 'components/SearchPage/constants';
import AppConfig from 'config/config';
import { GlobalState } from 'ducks/rootReducer';
import { updateSearchState } from 'ducks/search/reducer';
import {
DashboardSearchResults,
SetResourceRequest,
TableSearchResults,
UpdateSearchStateRequest,
UserSearchResults
} from 'ducks/search/types';
import { ResourceType } from 'interfaces/Resources';
import { setResource } from 'ducks/search/reducer';
export interface StateFromProps {
selectedTab: ResourceType,
resource: ResourceType,
tables: TableSearchResults;
dashboards: DashboardSearchResults;
users: UserSearchResults;
}
export interface DispatchFromProps {
setResource: (resource: ResourceType) => SetResourceRequest;
setResource: (resource: ResourceType) => UpdateSearchStateRequest;
}
export type ResourceSelectorProps = StateFromProps & DispatchFromProps;
......@@ -50,7 +50,7 @@ export class ResourceSelector extends React.Component<ResourceSelectorProps > {
type="radio"
name="resource"
value={ option.type }
checked={ this.props.selectedTab === option.type }
checked={ this.props.resource === option.type }
onChange={ this.onChange }
/>
<span className="subtitle-2">{ option.label }</span>
......@@ -88,7 +88,7 @@ export class ResourceSelector extends React.Component<ResourceSelectorProps > {
export const mapStateToProps = (state: GlobalState) => {
return {
selectedTab: state.search.selectedTab,
resource: state.search.resource,
tables: state.search.tables,
users: state.search.users,
dashboards: state.search.dashboards,
......@@ -96,7 +96,9 @@ export const mapStateToProps = (state: GlobalState) => {
};
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);
......@@ -16,7 +16,7 @@ import { ResourceType } from 'interfaces/Resources';
describe('ResourceSelector', () => {
const setup = (propOverrides?: Partial<ResourceSelectorProps>) => {
const props = {
selectedTab: ResourceType.table,
resource: ResourceType.table,
tables: globalState.search.tables,
users: globalState.search.users,
dashboards: globalState.search.dashboards,
......@@ -42,7 +42,7 @@ describe('ResourceSelector', () => {
expect(inputProps.type).toEqual("radio");
expect(inputProps.name).toEqual("resource");
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);
});
......@@ -121,8 +121,8 @@ describe('mapStateToProps', () => {
result = mapStateToProps(globalState);
});
it('sets selectedTab on the props', () => {
expect(result.selectedTab).toEqual(globalState.search.selectedTab);
it('sets resource on the props', () => {
expect(result.resource).toEqual(globalState.search.resource);
});
it('sets tables on the props', () => {
......
......@@ -3,7 +3,7 @@ import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
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';
......@@ -22,8 +22,7 @@ interface StateFromProps {
}
interface DispatchFromProps {
clearFilterByCategory: (categoryId: string) => ClearFilterRequest;
updateFilterByCategory: (categoryId: string, value: FilterOptions) => UpdateFilterRequest;
updateFilter: (categoryId: string, checkedValues: FilterOptions | undefined) => UpdateFilterRequest;
}
export type CheckBoxFilterProps = OwnProps & DispatchFromProps & StateFromProps;
......@@ -64,10 +63,10 @@ export class CheckBoxFilter extends React.Component<CheckBoxFilterProps> {
}
if (Object.keys(checkedValues).length === 0) {
this.props.clearFilterByCategory(categoryId);
this.props.updateFilter(categoryId, undefined);
}
else {
this.props.updateFilterByCategory(categoryId, checkedValues);
this.props.updateFilter(categoryId, checkedValues);
}
};
......@@ -83,7 +82,7 @@ export class CheckBoxFilter extends React.Component<CheckBoxFilterProps> {
export const mapStateToProps = (state: GlobalState, ownProps: OwnProps) => {
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) {
filterValues = {};
}
......@@ -95,8 +94,7 @@ export const mapStateToProps = (state: GlobalState, ownProps: OwnProps) => {
export const mapDispatchToProps = (dispatch: any) => {
return bindActionCreators({
clearFilterByCategory,
updateFilterByCategory,
updateFilter: (categoryId: string, checkedValues: FilterOptions | undefined) => updateFilterByCategory({ categoryId, value: checkedValues }),
}, dispatch);
};
......
......@@ -27,8 +27,7 @@ describe('CheckBoxFilter', () => {
checkedValues: {
'hive': true,
},
clearFilterByCategory: jest.fn(),
updateFilterByCategory: jest.fn(),
updateFilter: jest.fn(),
...propOverrides
};
const wrapper = shallow<CheckBoxFilter>(<CheckBoxFilter {...props} />);
......@@ -74,32 +73,30 @@ describe('CheckBoxFilter', () => {
let wrapper;
let mockEvent;
let clearCategorySpy;
let updateCategorySpy;
let updateFilterSpy;
beforeAll(() => {
const setupResult = setup();
props = setupResult.props;
wrapper = setupResult.wrapper;
clearCategorySpy = jest.spyOn(props, 'clearFilterByCategory');
updateCategorySpy = jest.spyOn(props, 'updateFilterByCategory');
updateFilterSpy = jest.spyOn(props, 'updateFilter');
})
it('calls props.clearFilterByCategory if no items will be checked', () => {
clearCategorySpy.mockClear();
it('calls props.updateFilter if no items will be checked', () => {
updateFilterSpy.mockClear();
mockEvent = { target: { name: mockCategoryId, value: 'hive', checked: false }};
wrapper.instance().onCheckboxChange(mockEvent);
expect(clearCategorySpy).toHaveBeenCalledWith(mockCategoryId)
expect(updateFilterSpy).toHaveBeenCalledWith(mockCategoryId, undefined)
});
it('calls props.updateFilterByCategory with expected parameters', () => {
updateCategorySpy.mockClear();
it('calls props.updateFilter with expected parameters', () => {
updateFilterSpy.mockClear();
mockEvent = { target: { name: mockCategoryId, value: 'bigquery', checked: true}};
const expectedCheckedValues = {
...props.checkedValues,
'bigquery': true
}
wrapper.instance().onCheckboxChange(mockEvent);
expect(updateCategorySpy).toHaveBeenCalledWith(mockCategoryId, expectedCheckedValues)
expect(updateFilterSpy).toHaveBeenCalledWith(mockCategoryId, expectedCheckedValues)
});
});
......@@ -133,7 +130,7 @@ describe('CheckBoxFilter', () => {
...globalState,
search: {
...globalState.search,
selectedTab: ResourceType.table,
resource: ResourceType.table,
filters: {
[ResourceType.table]: {
[mockCategoryId]: mockFilters
......@@ -146,7 +143,7 @@ describe('CheckBoxFilter', () => {
...globalState,
search: {
...globalState.search,
selectedTab: ResourceType.user,
resource: ResourceType.user,
filters: {
[ResourceType.table]: {}
}
......@@ -183,12 +180,8 @@ describe('CheckBoxFilter', () => {
result = mapDispatchToProps(dispatch);
});
it('sets clearFilterByCategory on the props', () => {
expect(result.clearFilterByCategory).toBeInstanceOf(Function);
});
it('sets updateFilterByCategory on the props', () => {
expect(result.updateFilterByCategory).toBeInstanceOf(Function);
it('sets updateFilter on the props', () => {
expect(result.updateFilter).toBeInstanceOf(Function);
});
});
});
......@@ -2,7 +2,7 @@ import * as React from 'react';
import { bindActionCreators } from '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';
......@@ -28,7 +28,7 @@ export interface StateFromProps {
};
export interface DispatchFromProps {
clearFilterByCategory: (categoryId: string) => ClearFilterRequest;
clearFilter: (categoryId: string) => UpdateFilterRequest;
};
export type FilterSectionProps = OwnProps & DispatchFromProps & StateFromProps;
......@@ -39,7 +39,7 @@ export class FilterSection extends React.Component<FilterSectionProps> {
}
onClearFilter = () => {
this.props.clearFilterByCategory(this.props.categoryId);
this.props.clearFilter(this.props.categoryId);
}
renderFilterComponent = () => {
......@@ -93,7 +93,7 @@ export class FilterSection extends React.Component<FilterSectionProps> {
export const mapStateToProps = (state: GlobalState, ownProps: OwnProps) => {
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;
if (filterValue && ownProps.type === FilterType.CHECKBOX_SELECT) {
Object.keys(filterValue).forEach(key => {
......@@ -110,7 +110,7 @@ export const mapStateToProps = (state: GlobalState, ownProps: OwnProps) => {
export const mapDispatchToProps = (dispatch: any) => {
return bindActionCreators({
clearFilterByCategory,
clearFilter: (categoryId: string) => updateFilterByCategory({ categoryId, value: undefined }),
}, dispatch);
};
......
......@@ -2,7 +2,6 @@ import * as React from 'react';
import { shallow } from 'enzyme';
import { GlobalState } from 'ducks/rootReducer';
import { clearFilterByCategory } from 'ducks/search/filters/reducer';
import { FilterSection, FilterSectionProps, mapDispatchToProps, mapStateToProps } from '../';
......@@ -19,7 +18,7 @@ describe('FilterSection', () => {
categoryId: 'testId',
hasValue: true,
title: 'Category',
clearFilterByCategory: jest.fn(),
clearFilter: jest.fn(),
type: FilterType.INPUT_SELECT,
...propOverrides
};
......@@ -36,10 +35,10 @@ describe('FilterSection', () => {
const setupResult = setup();
props = setupResult.props;
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();
expect(clearFilterSpy).toHaveBeenCalledWith(props.categoryId);
})
......@@ -109,7 +108,7 @@ describe('FilterSection', () => {
...globalState,
search: {
...globalState.search,
selectedTab: ResourceType.table,
resource: ResourceType.table,
filters: {
[ResourceType.table]: {
'database': { 'hive': true },
......@@ -123,7 +122,7 @@ describe('FilterSection', () => {
...globalState,
search: {
...globalState.search,
selectedTab: ResourceType.user,
resource: ResourceType.user,
filters: {
[ResourceType.table]: {}
}
......@@ -175,8 +174,8 @@ describe('FilterSection', () => {
result = mapDispatchToProps(dispatch);
});
it('sets clearFilterByCategory on the props', () => {
expect(result.clearFilterByCategory).toBeInstanceOf(Function);
it('sets clearFilter on the props', () => {
expect(result.clearFilter).toBeInstanceOf(Function);
});
});
});
......@@ -2,7 +2,7 @@ import * as React from 'react';
import { bindActionCreators } from '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';
......@@ -17,8 +17,7 @@ interface StateFromProps {
}
interface DispatchFromProps {
clearFilterByCategory: (categoryId: string) => ClearFilterRequest;
updateFilterByCategory: (categoryId: string, value: string) => UpdateFilterRequest;
updateFilter: (categoryId: string, value: string | undefined) => UpdateFilterRequest;
}
export type InputFilterProps = StateFromProps & DispatchFromProps & OwnProps;
......@@ -46,10 +45,10 @@ export class InputFilter extends React.Component<InputFilterProps, InputFilterSt
onApplyChanges = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if(!!this.state.value) {
this.props.updateFilterByCategory(this.props.categoryId, this.state.value);
this.props.updateFilter(this.props.categoryId, this.state.value);
}
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
export const mapStateToProps = (state: GlobalState, ownProps: OwnProps) => {
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 {
value: value || '',
}
......@@ -90,8 +89,7 @@ export const mapStateToProps = (state: GlobalState, ownProps: OwnProps) => {
export const mapDispatchToProps = (dispatch: any) => {
return bindActionCreators({
clearFilterByCategory,
updateFilterByCategory,
updateFilter: (categoryId: string, value: string | undefined) => updateFilterByCategory({ categoryId, value }),
}, dispatch);
};
......
......@@ -6,7 +6,6 @@ import { InputFilter, InputFilterProps, mapDispatchToProps, mapStateToProps } fr
import { APPLY_BTN_TEXT } from '../../constants';
import { GlobalState } from 'ducks/rootReducer';
import { clearFilterByCategory, updateFilterByCategory } from 'ducks/search/filters/reducer';
import globalState from 'fixtures/globalState';
......@@ -19,8 +18,7 @@ describe('InputFilter', () => {
const props: InputFilterProps = {
categoryId: 'schema',
value: 'schema_name',
clearFilterByCategory: jest.fn(),
updateFilterByCategory: jest.fn(),
updateFilter: jest.fn(),
...propOverrides
};
const wrapper = shallow<InputFilter>(<InputFilter {...props} />);
......@@ -78,29 +76,27 @@ describe('InputFilter', () => {
let props;
let wrapper;
let clearCategorySpy;
let updateCategorySpy;
let updateFilterSpy;
beforeAll(() => {
const setupResult = setup();
props = setupResult.props;
wrapper = setupResult.wrapper;
clearCategorySpy = jest.spyOn(props, 'clearFilterByCategory');
updateCategorySpy = jest.spyOn(props, 'updateFilterByCategory');
updateFilterSpy = jest.spyOn(props, 'updateFilter');
});
it('calls props.clearFilterByCategory if state.value is falsy', () => {
clearCategorySpy.mockClear();
it('calls props.updateFilter if state.value is falsy', () => {
updateFilterSpy.mockClear();
wrapper.setState({ value: '' });
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', () => {
updateCategorySpy.mockClear();
it('calls props.updateFilter if state.value has a truthy value', () => {
updateFilterSpy.mockClear();
const mockValue = 'hello';
wrapper.setState({ value: mockValue });
wrapper.instance().onApplyChanges({ preventDefault: jest.fn() });
expect(updateCategorySpy).toHaveBeenCalledWith(props.categoryId, mockValue)
expect(updateFilterSpy).toHaveBeenCalledWith(props.categoryId, mockValue);
});
});
......@@ -162,7 +158,7 @@ describe('InputFilter', () => {
...globalState,
search: {
...globalState.search,
selectedTab: ResourceType.table,
resource: ResourceType.table,
filters: {
[ResourceType.table]: {
[mockCategoryId]: mockFilters
......@@ -175,7 +171,7 @@ describe('InputFilter', () => {
...globalState,
search: {
...globalState.search,
selectedTab: ResourceType.user,
resource: ResourceType.user,
filters: {
[ResourceType.table]: {}
}
......@@ -212,12 +208,8 @@ describe('InputFilter', () => {
result = mapDispatchToProps(dispatch);
});
it('sets clearFilterByCategory on the props', () => {
expect(result.clearFilterByCategory).toBeInstanceOf(Function);
});
it('sets updateFilterByCategory on the props', () => {
expect(result.updateFilterByCategory).toBeInstanceOf(Function);
it('sets updateFilter on the props', () => {
expect(result.updateFilter).toBeInstanceOf(Function);
});
});
});
......@@ -63,7 +63,7 @@ export class SearchFilter extends React.Component<SearchFilterProps> {
};
export const mapStateToProps = (state: GlobalState) => {
const resourceType = state.search.selectedTab;
const resourceType = state.search.resource;
const filterCategories = getFilterConfigByResource(resourceType);
const filterSections = [];
......
......@@ -156,7 +156,7 @@ describe('mapStateToProps', () => {
...globalState,
search: {
...globalState.search,
selectedTab: ResourceType.table,
resource: ResourceType.table,
filters: {
[ResourceType.table]: {
[mockSchemaId]: mockSchemaValue,
......@@ -170,10 +170,10 @@ describe('mapStateToProps', () => {
let getFilterConfigByResourceSpy;
let result;
it('calls getFilterConfigByResource with selectedTab', () => {
it('calls getFilterConfigByResource with resource', () => {
getFilterConfigByResourceSpy = jest.spyOn(ConfigUtils, 'getFilterConfigByResource').mockReturnValue(MOCK_CATEGORY_CONFIG);
mapStateToProps(mockStateWithFilters);
expect(getFilterConfigByResourceSpy).toHaveBeenCalledWith(mockStateWithFilters.search.selectedTab);
expect(getFilterConfigByResourceSpy).toHaveBeenCalledWith(mockStateWithFilters.search.resource);
});
it('sets expected filterSections on the result', () => {
......
......@@ -12,18 +12,17 @@ import SearchFilter from './SearchFilter';
import SearchPanel from './SearchPanel';
import { GlobalState } from 'ducks/rootReducer';
import { setPageIndex, setResource, urlDidUpdate } from 'ducks/search/reducer';
import { submitSearchResource, urlDidUpdate } from 'ducks/search/reducer';
import {
DashboardSearchResults,
SearchResults,
SetPageIndexRequest,
SetResourceRequest,
SubmitSearchResourceRequest,
TableSearchResults,
UrlDidUpdateRequest,
UserSearchResults,
} from 'ducks/search/types';
import { Resource, ResourceType } from 'interfaces';
import { Resource, ResourceType, SearchType } from 'interfaces';
// TODO: Use css-modules instead of 'import'
import './styles.scss';
......@@ -43,7 +42,7 @@ import {
export interface StateFromProps {
hasFilters: boolean;
searchTerm: string;
selectedTab: ResourceType;
resource: ResourceType;
isLoading: boolean;
tables: TableSearchResults;
dashboards: DashboardSearchResults;
......@@ -51,8 +50,7 @@ export interface StateFromProps {
}
export interface DispatchFromProps {
setResource: (resource: ResourceType) => SetResourceRequest;
setPageIndex: (pageIndex: number) => SetPageIndexRequest;
setPageIndex: (pageIndex: number) => SubmitSearchResourceRequest;
urlDidUpdate: (urlSearch: UrlSearch) => UrlDidUpdateRequest;
}
......@@ -76,7 +74,7 @@ export class SearchPage extends React.Component<SearchPageProps> {
}
renderSearchResults = () => {
switch(this.props.selectedTab) {
switch(this.props.resource) {
case ResourceType.table:
return this.getTabContent(this.props.tables, ResourceType.table);
case ResourceType.user:
......@@ -183,11 +181,11 @@ export class SearchPage extends React.Component<SearchPageProps> {
}
export const mapStateToProps = (state: GlobalState) => {
const resourceFilters = state.search.filters[state.search.selectedTab];
const resourceFilters = state.search.filters[state.search.resource];
return {
hasFilters: resourceFilters && Object.keys(resourceFilters).length > 0,
searchTerm: state.search.search_term,
selectedTab: state.search.selectedTab,
resource: state.search.resource,
isLoading: state.search.isLoading,
tables: state.search.tables,
users: state.search.users,
......@@ -196,7 +194,10 @@ export const mapStateToProps = (state: GlobalState) => {
};
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);
......@@ -35,12 +35,11 @@ describe('SearchPage', () => {
const props: SearchPageProps = {
hasFilters: false,
searchTerm: globalState.search.search_term,
selectedTab: ResourceType.table,
resource: ResourceType.table,
isLoading: false,
dashboards: globalState.search.dashboards,
tables: globalState.search.tables,
users: globalState.search.users,
setResource: jest.fn(),
setPageIndex: jest.fn(),
urlDidUpdate: jest.fn(),
...routerProps,
......@@ -55,7 +54,7 @@ describe('SearchPage', () => {
let wrapper;
beforeAll(() => {
const setupResult = setup(null, {
search: '/search?searchTerm=testName&selectedTab=table&pageIndex=1',
search: '/search?searchTerm=testName&resource=table&pageIndex=1',
});
props = setupResult.props;
wrapper = setupResult.wrapper;
......@@ -74,7 +73,7 @@ describe('SearchPage', () => {
let mockPrevProps;
beforeAll(() => {
const setupResult = setup(null, {
search: '/search?searchTerm=testName&selectedTab=table&pageIndex=1',
search: '/search?searchTerm=testName&resource=table&pageIndex=1',
});
props = setupResult.props;
wrapper = setupResult.wrapper;
......@@ -82,7 +81,7 @@ describe('SearchPage', () => {
mockPrevProps = {
searchTerm: 'previous',
location: {
search: '/search?searchTerm=previous&selectedTab=table&pageIndex=0',
search: '/search?searchTerm=previous&resource=table&pageIndex=0',
pathname: 'mockstr',
state: jest.fn(),
hash: 'mockstr',
......@@ -224,7 +223,7 @@ describe('SearchPage', () => {
describe('renderSearchResults', () => {
it('renders the correct content for table resources', () => {
const { props, wrapper } = setup({
selectedTab: ResourceType.table
resource: ResourceType.table
});
const getTabContentSpy = jest.spyOn(wrapper.instance(), 'getTabContent');
shallow(wrapper.instance().renderSearchResults());
......@@ -233,7 +232,7 @@ describe('SearchPage', () => {
it('renders the correct content for user resources', () => {
const { props, wrapper } = setup({
selectedTab: ResourceType.user
resource: ResourceType.user
});
const getTabContentSpy = jest.spyOn(wrapper.instance(), 'getTabContent');
shallow(wrapper.instance().renderSearchResults());
......@@ -242,16 +241,16 @@ describe('SearchPage', () => {
it('renders the correct content for dashboard resources', () => {
const { props, wrapper } = setup({
selectedTab: ResourceType.dashboard
resource: ResourceType.dashboard
});
const getTabContentSpy = jest.spyOn(wrapper.instance(), 'getTabContent');
shallow(wrapper.instance().renderSearchResults());
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({
selectedTab: null
resource: null
});
const renderedSearchResults = wrapper.instance().renderSearchResults();
expect(renderedSearchResults).toBe(null);
......@@ -313,10 +312,6 @@ describe('mapDispatchToProps', () => {
result = mapDispatchToProps(dispatch);
});
it('sets setResource on the props', () => {
expect(result.setResource).toBeInstanceOf(Function);
});
it('sets setPageIndex on the props', () => {
expect(result.setPageIndex).toBeInstanceOf(Function);
});
......@@ -336,8 +331,8 @@ describe('mapStateToProps', () => {
expect(result.searchTerm).toEqual(globalState.search.search_term);
});
it('sets selectedTab on the props', () => {
expect(result.selectedTab).toEqual(globalState.search.selectedTab);
it('sets resource on the props', () => {
expect(result.resource).toEqual(globalState.search.resource);
});
it('sets isLoading on the props', () => {
......@@ -361,7 +356,7 @@ describe('mapStateToProps', () => {
const testState = {
...globalState
};
testState.search.selectedTab = ResourceType.user;
testState.search.resource = ResourceType.user;
testState.search.filters = defaultEmptyFilters;
result = mapStateToProps(testState);
expect(result.hasFilters).toBeFalsy();
......@@ -371,7 +366,7 @@ describe('mapStateToProps', () => {
const testState = {
...globalState
};
testState.search.selectedTab = ResourceType.table;
testState.search.resource = ResourceType.table;
testState.search.filters = datasetFilterExample;
result = mapStateToProps(testState);
expect(result.hasFilters).toBe(true);
......
import * as React from 'react';
import { bindActionCreators } from '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 './styles.scss';
......@@ -14,7 +15,7 @@ interface OwnProps {
}
export interface DispatchFromProps {
searchTag: (tagName: string) => SetSearchInputRequest;
searchTag: (tagName: string) => SubmitSearchResourceRequest;
}
export type TagInfoProps = OwnProps & DispatchFromProps;
......@@ -60,9 +61,14 @@ export class TagInfo extends React.Component<TagInfoProps> {
export const mapDispatchToProps = (dispatch: any) => {
return bindActionCreators({
/* Note: Pattern intentionally isolates component from extraneous hardcoded parameters */
/* Note: This will have to be extended to all resources that support tags */
searchTag: (tagName: string) => setSearchInputByResource({ 'tag': tagName }, ResourceType.table, 0, '')
searchTag: (tagName: string) => submitSearchResource({
resourceFilters: { 'tag': tagName },
resource: ResourceType.table,
pageIndex: 0,
searchTerm: '',
searchType: SearchType.FILTER,
updateUrl: true,
})
}, dispatch);
};
......
......@@ -5,8 +5,8 @@ import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { GlobalState } from 'ducks/rootReducer';
import { clearSearch, submitSearch, getInlineResultsDebounce, selectInlineResult } from 'ducks/search/reducer';
import { ClearSearchRequest, SubmitSearchRequest, InlineSearchRequest, InlineSearchSelect } from 'ducks/search/types';
import { submitSearch, getInlineResultsDebounce, selectInlineResult } from 'ducks/search/reducer';
import { SubmitSearchRequest, InlineSearchRequest, InlineSearchSelect } from 'ducks/search/types';
import { ResourceType } from 'interfaces';
......@@ -25,7 +25,7 @@ export interface StateFromProps {
}
export interface DispatchFromProps {
clearSearch?: () => ClearSearchRequest;
clearSearch?: () => SubmitSearchRequest;
submitSearch: (searchTerm: string) => SubmitSearchRequest;
onInputChange: (term: string) => InlineSearchRequest;
onSelectInlineResult: (resourceType: ResourceType, searchTerm: string, updateUrl: boolean) => InlineSearchSelect;
......@@ -190,8 +190,8 @@ export const mapDispatchToProps = (dispatch: any, ownProps) => {
const updateStateOnClear = ownProps.history.location.pathname === '/search';
return bindActionCreators({
clearSearch: updateStateOnClear ? clearSearch : null,
submitSearch: (searchTerm: string) => { return submitSearch(searchTerm, useFilters) },
clearSearch: updateStateOnClear ? () => submitSearch({ useFilters, searchTerm: '' }) : null,
submitSearch: (searchTerm: string) => submitSearch({ searchTerm, useFilters }),
onInputChange: getInlineResultsDebounce,
onSelectInlineResult: selectInlineResult
}, dispatch);
......
......@@ -23,20 +23,20 @@ import { createIssueWatcher, getIssuesWatcher } from './issue/sagas';
import { getPopularTablesWatcher } from './popularTables/sagas';
// Search
import {
clearSearchWatcher,
filterWatcher,
filterWatcher2,
inlineSearchWatcher,
inlineSearchWatcherDebounce,
loadPreviousSearchWatcher,
searchAllWatcher,
searchResourceWatcher,
setPageIndexWatcher,
setResourceWatcher,
selectInlineResultsWatcher,
submitSearchWatcher,
submitSearchResourceWatcher,
updateSearchStateWatcher,
urlDidUpdateWatcher
} from './search/sagas';
import {
filterWatcher,
} from './search/filters/sagas';
// TableDetail
import { updateTableOwnerWatcher } from './tableMetadata/owners/sagas';
......@@ -70,22 +70,20 @@ export default function* rootSaga() {
submitNotificationWatcher(),
// FeedbackForm
submitFeedbackWatcher(),
// Issues
getIssuesWatcher(),
createIssueWatcher(),
// Issues
getIssuesWatcher(),
createIssueWatcher(),
// Search
clearSearchWatcher(),
filterWatcher(),
filterWatcher2(),
inlineSearchWatcher(),
inlineSearchWatcherDebounce(),
loadPreviousSearchWatcher(),
searchAllWatcher(),
searchResourceWatcher(),
selectInlineResultsWatcher(),
setPageIndexWatcher(),
setResourceWatcher(),
submitSearchWatcher(),
submitSearchResourceWatcher(),
updateSearchStateWatcher(),
urlDidUpdateWatcher(),
// PopularTables
getPopularTablesWatcher(),
......
import { SubmitSearchResource, SubmitSearchResourceRequest } from 'ducks/search/types';
import { ResourceType } from 'interfaces';
import { filterFromObj } from 'ducks/utilMethods';
/* ACTION TYPES */
export enum UpdateSearchFilter {
CLEAR_ALL = 'amundsen/search/filter/CLEAR_ALL',
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;
REQUEST = 'amundsen/search/filter/UPDATE_SEARCH_FILTER_REQUEST',
};
export interface SetSearchInputRequest {
payload: {
filters: ResourceFilterReducerState;
pageIndex?: number;
resourceType: ResourceType;
term?: string;
};
type: UpdateSearchFilter.SET_BY_RESOURCE;
};
export type UpdateFilterPayload = {
categoryId: string;
value: string | FilterOptions | undefined;
}
export interface UpdateFilterRequest {
payload: {
categoryId: string;
value: string | FilterOptions;
};
type: UpdateSearchFilter.UPDATE_CATEGORY;
payload: UpdateFilterPayload;
type: UpdateSearchFilter.REQUEST;
};
/* ACTIONS */
export function clearAllFilters(): ClearAllFiltersRequest {
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 {
export function updateFilterByCategory({ categoryId, value }: UpdateFilterPayload): UpdateFilterRequest {
return {
payload: {
categoryId,
value
},
type: UpdateSearchFilter.UPDATE_CATEGORY,
type: UpdateSearchFilter.REQUEST,
};
};
......@@ -98,31 +43,17 @@ export const initialFilterState: FilterReducerState = {
[ResourceType.table]: initialTableFilterState,
};
export default function reducer(state: FilterReducerState = initialFilterState, action, resourceType: ResourceType): FilterReducerState {
const resourceFilters = state[resourceType];
const { payload, type } = action;
switch (type) {
case UpdateSearchFilter.CLEAR_ALL:
return initialFilterState;
case UpdateSearchFilter.CLEAR_CATEGORY:
return {
...state,
[resourceType]: filterFromObj(resourceFilters, [payload.categoryId])
};
case UpdateSearchFilter.SET_BY_RESOURCE:
return {
...state,
[payload.resourceType]: payload.filters
};
case UpdateSearchFilter.UPDATE_CATEGORY:
return {
...state,
[resourceType]: {
...resourceFilters,
[payload.categoryId]: payload.value
}
};
export default function reducer(state: FilterReducerState = initialFilterState, action): FilterReducerState {
switch (action.type) {
case SubmitSearchResource.REQUEST:
const { payload } = <SubmitSearchResourceRequest>action;
if (payload.resource && payload.resourceFilters) {
return {
...state,
[payload.resource]: payload.resourceFilters
};
}
return state;
default:
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';
import { Search as UrlSearch } from 'history';
import filterReducer, { initialFilterState, UpdateSearchFilter, FilterReducerState } from './filters/reducer';
import filterReducer, { initialFilterState, FilterReducerState } from './filters/reducer';
import {
DashboardSearchResults,
SearchAll,
SearchAllRequest,
SearchAllReset,
SearchAllResponse,
SearchAllResponsePayload,
SearchResource,
......@@ -24,18 +23,24 @@ import {
InlineSearchUpdate,
TableSearchResults,
UserSearchResults,
ClearSearch,
ClearSearchRequest,
SubmitSearchRequest,
SubmitSearch,
SetResourceRequest,
SetResource,
SetPageIndexRequest, SetPageIndex, LoadPreviousSearchRequest, LoadPreviousSearch, UrlDidUpdateRequest, UrlDidUpdate,
SubmitSearchResourcePayload,
SubmitSearchResourceRequest,
SubmitSearchResource,
LoadPreviousSearchRequest,
LoadPreviousSearch,
UpdateSearchStateRequest,
UpdateSearchStateReset,
UpdateSearchStatePayload,
UpdateSearchState,
UrlDidUpdateRequest,
UrlDidUpdate
} from './types';
export interface SearchReducerState {
search_term: string;
selectedTab: ResourceType;
resource: ResourceType;
isLoading: boolean;
dashboards: DashboardSearchResults;
tables: TableSearchResults;
......@@ -108,6 +113,7 @@ export function getInlineResultsSuccess(inlineResults: InlineSearchResponsePaylo
export function getInlineResultsFailure(): InlineSearchResponse {
return { type: InlineSearch.FAILURE };
};
export function selectInlineResult(resourceType: ResourceType, searchTerm: string, updateUrl: boolean = false): InlineSearchSelect {
return {
payload: {
......@@ -118,6 +124,7 @@ export function selectInlineResult(resourceType: ResourceType, searchTerm: strin
type: InlineSearch.SELECT
};
};
export function updateFromInlineResult(data: InlineSearchUpdatePayload): InlineSearchUpdate {
return {
payload: data,
......@@ -125,36 +132,33 @@ export function updateFromInlineResult(data: InlineSearchUpdatePayload): InlineS
};
};
export function searchReset(): SearchAllReset {
return {
type: SearchAll.RESET,
};
};
export function submitSearch(searchTerm: string, useFilters: boolean = false): SubmitSearchRequest {
export function submitSearch({ searchTerm, useFilters } : { searchTerm: string, useFilters: boolean }): SubmitSearchRequest {
return {
payload: { searchTerm, useFilters },
type: SubmitSearch.REQUEST,
};
};
export function clearSearch(): ClearSearchRequest {
export function submitSearchResource({ resourceFilters, pageIndex, searchTerm, resource, searchType, updateUrl } : SubmitSearchResourcePayload): SubmitSearchResourceRequest {
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 {
payload: { resource, updateUrl },
type: SetResource.REQUEST,
payload: {
filters,
resource,
updateUrl,
},
type: UpdateSearchState.REQUEST,
};
};
export function setPageIndex(pageIndex: number, updateUrl: boolean = true): SetPageIndexRequest {
export function resetSearchState(): UpdateSearchStateReset {
return {
payload: { pageIndex, updateUrl },
type: SetPageIndex.REQUEST,
type: UpdateSearchState.RESET,
};
};
......@@ -164,14 +168,13 @@ export function loadPreviousSearch(): LoadPreviousSearchRequest {
};
};
export function urlDidUpdate(urlSearch: UrlSearch): UrlDidUpdateRequest{
export function urlDidUpdate(urlSearch: UrlSearch): UrlDidUpdateRequest {
return {
payload: { urlSearch },
type: UrlDidUpdate.REQUEST,
};
};
/* REDUCER */
export const initialInlineResultsState = {
isLoading: false,
......@@ -189,7 +192,7 @@ export const initialInlineResultsState = {
export const initialState: SearchReducerState = {
search_term: '',
isLoading: false,
selectedTab: ResourceType.table,
resource: ResourceType.table,
dashboards: {
page_index: 0,
results: [],
......@@ -211,28 +214,29 @@ export const initialState: SearchReducerState = {
export default function reducer(state: SearchReducerState = initialState, action): SearchReducerState {
switch (action.type) {
case UpdateSearchFilter.SET_BY_RESOURCE:
case SubmitSearch.REQUEST:
return {
...state,
search_term: action.payload.term,
filters: filterReducer(state.filters, action, state.selectedTab),
isLoading: true,
search_term: action.payload.searchTerm,
}
case UpdateSearchFilter.CLEAR_ALL:
case SubmitSearchResource.REQUEST:
return {
...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 UpdateSearchFilter.UPDATE_CATEGORY:
case UpdateSearchState.REQUEST:
const payload = action.payload;
return {
...state,
isLoading: true,
filters: filterReducer(state.filters, action, state.selectedTab),
filters: payload.filters || state.filters,
resource: payload.resource || state.resource,
}
case SearchAll.RESET:
return initialState;
case UpdateSearchState.RESET:
return initialState;
case SearchAll.REQUEST:
// updates search term to reflect action
return {
...state,
inlineResults: {
......@@ -273,16 +277,11 @@ export default function reducer(state: SearchReducerState = initialState, action
...initialState,
search_term: state.search_term,
};
case SetResource.REQUEST:
return {
...state,
selectedTab: (<SetResourceRequest>action).payload.resource
};
case InlineSearch.UPDATE:
const { searchTerm, selectedTab, tables, users } = (<InlineSearchUpdate>action).payload;
const { searchTerm, resource, tables, users } = (<InlineSearchUpdate>action).payload;
return {
...state,
selectedTab,
resource,
tables,
users,
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 {
UserResource,
} from 'interfaces';
import {
FilterReducerState,
ResourceFilterReducerState,
} from 'ducks/search/filters/reducer';
export interface SearchResults<T extends Resource> {
page_index: number;
total_results: number;
......@@ -25,7 +30,7 @@ export interface SearchResponsePayload {
users?: UserSearchResults;
};
export interface SearchAllResponsePayload extends SearchResponsePayload {
selectedTab: ResourceType;
resource: ResourceType;
dashboards: DashboardSearchResults;
tables: TableSearchResults;
users: UserSearchResults;
......@@ -36,7 +41,7 @@ export interface InlineSearchResponsePayload {
};
export interface InlineSearchUpdatePayload {
searchTerm: string;
selectedTab: ResourceType;
resource: ResourceType;
tables: TableSearchResults;
users: UserSearchResults;
};
......@@ -46,7 +51,6 @@ export enum SearchAll {
REQUEST = 'amundsen/search/SEARCH_ALL_REQUEST',
SUCCESS = 'amundsen/search/SEARCH_ALL_SUCCESS',
FAILURE = 'amundsen/search/SEARCH_ALL_FAILURE',
RESET = 'amundsen/search/SEARCH_ALL_RESET',
};
export interface SearchAllRequest {
payload: {
......@@ -62,10 +66,6 @@ export interface SearchAllResponse {
type: SearchAll.SUCCESS | SearchAll.FAILURE;
payload?: SearchAllResponsePayload;
};
export interface SearchAllReset {
type: SearchAll.RESET;
};
export enum SearchResource {
REQUEST = 'amundsen/search/SEARCH_RESOURCE_REQUEST',
......@@ -124,42 +124,44 @@ export enum SubmitSearch {
export interface SubmitSearchRequest {
payload: {
searchTerm: string;
useFilters?: boolean;
useFilters: boolean;
};
type: SubmitSearch.REQUEST;
};
export enum ClearSearch {
REQUEST = 'amundsen/search/CLEAR_SEARCH_REQUEST',
export enum SubmitSearchResource {
REQUEST = 'amundsen/search/SUBMIT_SEARCH_RESOURCE_REQUEST',
};
export interface ClearSearchRequest {
type: ClearSearch.REQUEST;
export type SubmitSearchResourcePayload = {
pageIndex: number;
searchType: SearchType;
updateUrl?: boolean;
resourceFilters?: ResourceFilterReducerState;
searchTerm?: string;
resource?: ResourceType;
}
export interface SubmitSearchResourceRequest {
payload: SubmitSearchResourcePayload;
type: SubmitSearchResource.REQUEST;
};
export enum SetResource {
REQUEST = 'amundsen/search/SET_RESOURCE_REQUEST',
};
export interface SetResourceRequest {
payload: {
resource: ResourceType;
updateUrl: boolean;
};
type: SetResource.REQUEST;
export enum UpdateSearchState {
REQUEST = 'amundsen/search/UPDATE_SEARCH_STATE',
RESET = 'amundsen/search/RESET_SEARCH_STATE',
};
export enum SetPageIndex {
REQUEST = 'amundsen/search/SET_PAGE_INDEX_REQUEST',
export type UpdateSearchStatePayload = {
filters?: FilterReducerState;
resource?: ResourceType;
updateUrl?: boolean;
}
export interface UpdateSearchStateRequest {
payload?: UpdateSearchStatePayload;
type: UpdateSearchState.REQUEST;
};
export interface SetPageIndexRequest {
payload: {
pageIndex: number;
updateUrl: boolean;
};
type: SetPageIndex.REQUEST;
export interface UpdateSearchStateReset {
type: UpdateSearchState.RESET;
};
export enum LoadPreviousSearch {
REQUEST = 'amundsen/search/LOAD_PREVIOUS_SEARCH_REQUEST',
};
......@@ -167,7 +169,6 @@ export interface LoadPreviousSearchRequest {
type: LoadPreviousSearch.REQUEST;
};
export enum UrlDidUpdate {
REQUEST = 'amundsen/search/URL_DID_UPDATE_REQUEST',
};
......
......@@ -13,7 +13,7 @@ can be the combination of multiple responses.
*/
export const getPageIndex = (state: Partial<SearchReducerState>, resource?: ResourceType) => {
resource = resource || state.selectedTab;
resource = resource || state.resource;
switch(resource) {
case ResourceType.table:
return state.tables.page_index;
......
......@@ -66,7 +66,7 @@ const globalState: GlobalState = {
],
search: {
search_term: 'testName',
selectedTab: ResourceType.table,
resource: ResourceType.table,
isLoading: false,
dashboards: {
page_index: 0,
......
......@@ -31,4 +31,5 @@ We use [Jest](https://jestjs.io/) as our test framework. We leverage utility met
#### 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.
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