Unverified Commit 670cbdfc authored by Daniel's avatar Daniel Committed by GitHub

Create Generic Paginated Resource List (#193)

- Created a generic ResourceList with optional Pagination
- Renamed 'BookmarkList' to 'MyBookmarks'
- Converted usages in 'PopularTables', 'SearchPage', and 'MyBookmarks'
parent 7c06fdf7
...@@ -6,7 +6,7 @@ import { RouteComponentProps } from 'react-router'; ...@@ -6,7 +6,7 @@ import { RouteComponentProps } from 'react-router';
// TODO: Use css-modules instead of 'import' // TODO: Use css-modules instead of 'import'
import './styles.scss'; import './styles.scss';
import BookmarkList from 'components/common/Bookmark/BookmarkList'; import MyBookmarks from 'components/common/Bookmark/MyBookmarks';
import PopularTables from 'components/common/PopularTables'; import PopularTables from 'components/common/PopularTables';
import { SearchAllReset } from 'ducks/search/types'; import { SearchAllReset } from 'ducks/search/types';
import { searchReset } from 'ducks/search/reducer'; import { searchReset } from 'ducks/search/reducer';
...@@ -35,7 +35,7 @@ export class HomePage extends React.Component<HomePageProps> { ...@@ -35,7 +35,7 @@ export class HomePage extends React.Component<HomePageProps> {
<div className="col-xs-12 col-md-offset-1 col-md-10"> <div className="col-xs-12 col-md-offset-1 col-md-10">
<SearchBar /> <SearchBar />
<div className="home-element-container"> <div className="home-element-container">
<BookmarkList /> <MyBookmarks />
</div> </div>
<div className="home-element-container"> <div className="home-element-container">
<PopularTables /> <PopularTables />
......
...@@ -5,7 +5,7 @@ import { shallow } from 'enzyme'; ...@@ -5,7 +5,7 @@ import { shallow } from 'enzyme';
import { mapDispatchToProps, HomePage, HomePageProps } from '../'; import { mapDispatchToProps, HomePage, HomePageProps } from '../';
import SearchBar from 'components/SearchPage/SearchBar'; import SearchBar from 'components/SearchPage/SearchBar';
import BookmarkList from 'components/common/Bookmark/BookmarkList'; import MyBookmarks from 'components/common/Bookmark/MyBookmarks';
import PopularTables from 'components/common/PopularTables'; import PopularTables from 'components/common/PopularTables';
describe('HomePage', () => { describe('HomePage', () => {
...@@ -49,7 +49,7 @@ describe('HomePage', () => { ...@@ -49,7 +49,7 @@ describe('HomePage', () => {
describe('render', () => { describe('render', () => {
it('contains Searchbar, BookmarkList, and PopularTables', () => { it('contains Searchbar, BookmarkList, and PopularTables', () => {
expect(wrapper.contains(<SearchBar />)); expect(wrapper.contains(<SearchBar />));
expect(wrapper.contains(<BookmarkList />)); expect(wrapper.contains(<MyBookmarks />));
expect(wrapper.contains(<PopularTables />)); expect(wrapper.contains(<PopularTables />));
}); });
}); });
......
...@@ -7,7 +7,7 @@ import AppConfig from 'config/config'; ...@@ -7,7 +7,7 @@ import AppConfig from 'config/config';
import { LinkConfig } from 'config/config-types'; import { LinkConfig } from 'config/config-types';
import { GlobalState } from 'ducks/rootReducer'; import { GlobalState } from 'ducks/rootReducer';
import { LoggedInUser } from 'ducks/user/types'; import { LoggedInUser } from 'ducks/user/types';
import { logClick } from "ducks/utilMethods"; import { logClick } from 'ducks/utilMethods';
import './styles.scss'; import './styles.scss';
......
import * as React from 'react';
import ResourceListItem from 'components/common/ResourceListItem';
import { Resource } from 'interfaces';
export interface SearchListProps {
results?: Resource[];
params?: SearchListParams;
}
export interface SearchListParams {
source?: string;
paginationStartIndex?: number;
}
const SearchList: React.SFC<SearchListProps> = ({ results, params }) => {
const { source, paginationStartIndex } = params;
return (
<ul className="list-group">
{
results.map((resource, index) => {
const logging = { source, index: paginationStartIndex + index };
return <ResourceListItem item={ resource } logging={ logging } key={ index } />;
})
}
</ul>
);
};
SearchList.defaultProps = {
results: [],
params: {},
};
export default SearchList;
import * as React from 'react';
import { shallow } from 'enzyme';
import { Resource, ResourceType } from 'interfaces';
import ResourceListItem from 'components/common/ResourceListItem';
import SearchList, { SearchListProps, SearchListParams } from '../';
describe('SearchList', () => {
const setup = (propOverrides?: Partial<SearchListProps>) => {
const props: SearchListProps = {
results: [
{ type: ResourceType.table },
{ type: ResourceType.user },
],
params: {
source: 'testSource',
paginationStartIndex: 0,
},
...propOverrides
};
const wrapper = shallow(<SearchList {...props} />);
return { props, wrapper };
};
describe('render', () => {
let props;
let wrapper;
beforeAll(() => {
const setupResult = setup();
props = setupResult.props;
wrapper = setupResult.wrapper;
});
it('renders unordered list', () => {
expect(wrapper.type()).toEqual('ul');
});
it('renders unordered list w/ correct className', () => {
expect(wrapper.props()).toMatchObject({
className: 'list-group',
});
});
it('renders a ResourceListItem for each result', () => {
const content = wrapper.find(ResourceListItem);
expect(content.length).toEqual(props.results.length);
});
it('passes correct props to each ResourceListItem', () => {
const content = wrapper.find(ResourceListItem);
content.forEach((contentItem, index) => {
expect(contentItem.props()).toMatchObject({
item: props.results[index],
logging: {
source: props.params.source,
index: props.params.paginationStartIndex + index,
}
})
});
});
});
});
...@@ -3,14 +3,14 @@ import { connect } from 'react-redux'; ...@@ -3,14 +3,14 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import * as DocumentTitle from 'react-document-title'; import * as DocumentTitle from 'react-document-title';
import * as qs from 'simple-query-string'; import * as qs from 'simple-query-string';
import Pagination from 'react-js-pagination';
import { RouteComponentProps } from 'react-router'; import { RouteComponentProps } from 'react-router';
import SearchBar from './SearchBar'; import SearchBar from './SearchBar';
import SearchList from './SearchList';
import LoadingSpinner from 'components/common/LoadingSpinner'; import LoadingSpinner from 'components/common/LoadingSpinner';
import { ResourceType } from 'interfaces';
import InfoButton from 'components/common/InfoButton'; import InfoButton from 'components/common/InfoButton';
import { ResourceType, TableResource } from 'interfaces'; import ResourceList from 'components/common/ResourceList';
import TabsComponent from 'components/common/Tabs'; import TabsComponent from 'components/common/Tabs';
import { GlobalState } from 'ducks/rootReducer'; import { GlobalState } from 'ducks/rootReducer';
...@@ -30,7 +30,6 @@ import './styles.scss'; ...@@ -30,7 +30,6 @@ import './styles.scss';
import { import {
DOCUMENT_TITLE_SUFFIX, DOCUMENT_TITLE_SUFFIX,
PAGE_INDEX_ERROR_MESSAGE, PAGE_INDEX_ERROR_MESSAGE,
PAGINATION_PAGE_RANGE,
RESULTS_PER_PAGE, RESULTS_PER_PAGE,
SEARCH_ERROR_MESSAGE_INFIX, SEARCH_ERROR_MESSAGE_INFIX,
SEARCH_ERROR_MESSAGE_PREFIX, SEARCH_ERROR_MESSAGE_PREFIX,
...@@ -134,8 +133,7 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState ...@@ -134,8 +133,7 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState
return 0; return 0;
}; };
onPaginationChange = (pageNumber: number): void => { onPaginationChange = (index: number): void => {
const index = pageNumber - 1;
this.props.searchResource(this.state.selectedTab, this.props.searchTerm, index); this.props.searchResource(this.state.selectedTab, this.props.searchTerm, index);
this.updatePageUrl(this.props.searchTerm, this.state.selectedTab, index); this.updatePageUrl(this.props.searchTerm, this.state.selectedTab, index);
}; };
...@@ -209,19 +207,14 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState ...@@ -209,19 +207,14 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState
<label>{ title }</label> <label>{ title }</label>
<InfoButton infoText={SEARCH_INFO_TEXT}/> <InfoButton infoText={SEARCH_INFO_TEXT}/>
</div> </div>
<SearchList results={ results.results } params={ {source: SEARCH_SOURCE_NAME, paginationStartIndex: 0 } }/> <ResourceList
<div className="search-pagination-component"> slicedItems={ results.results }
{ slicedItemsCount={ total_results }
total_results > RESULTS_PER_PAGE && source={ SEARCH_SOURCE_NAME }
<Pagination itemsPerPage={ RESULTS_PER_PAGE }
activePage={ page_index + 1 } activePage={ page_index }
itemsCountPerPage={ RESULTS_PER_PAGE } onPagination={ this.onPaginationChange }
totalItemsCount={ total_results }
pageRangeDisplayed={ PAGINATION_PAGE_RANGE }
onChange={ this.onPaginationChange }
/> />
}
</div>
</div> </div>
); );
}; };
......
import * as React from 'react'; import * as React from 'react';
import * as DocumentTitle from 'react-document-title'; import * as DocumentTitle from 'react-document-title';
import Pagination from 'react-js-pagination';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
...@@ -23,11 +22,11 @@ import InfoButton from 'components/common/InfoButton'; ...@@ -23,11 +22,11 @@ import InfoButton from 'components/common/InfoButton';
import TabsComponent from 'components/common/Tabs'; import TabsComponent from 'components/common/Tabs';
import SearchBar from '../SearchBar'; import SearchBar from '../SearchBar';
import SearchList from '../SearchList';
import globalState from 'fixtures/globalState';
import LoadingSpinner from 'components/common/LoadingSpinner'; import LoadingSpinner from 'components/common/LoadingSpinner';
import BookmarkList from 'components/common/Bookmark/BookmarkList';
import ResourceList from 'components/common/ResourceList';
import globalState from 'fixtures/globalState';
describe('SearchPage', () => { describe('SearchPage', () => {
const setStateSpy = jest.spyOn(SearchPage.prototype, 'setState'); const setStateSpy = jest.spyOn(SearchPage.prototype, 'setState');
...@@ -449,11 +448,11 @@ describe('SearchPage', () => { ...@@ -449,11 +448,11 @@ describe('SearchPage', () => {
}); });
it('calls props.searchResource with correct parameters', () => { it('calls props.searchResource with correct parameters', () => {
expect(searchResourceSpy).toHaveBeenCalledWith(wrapper.state().selectedTab, props.searchTerm, testIndex - 1); expect(searchResourceSpy).toHaveBeenCalledWith(wrapper.state().selectedTab, props.searchTerm, testIndex);
}); });
it('calls updatePageUrl with correct parameters', () => { it('calls updatePageUrl with correct parameters', () => {
expect(updatePageUrlSpy).toHaveBeenCalledWith(props.searchTerm, wrapper.state().selectedTab, testIndex - 1); expect(updatePageUrlSpy).toHaveBeenCalledWith(props.searchTerm, wrapper.state().selectedTab, testIndex);
}); });
}); });
...@@ -563,21 +562,7 @@ describe('SearchPage', () => { ...@@ -563,21 +562,7 @@ describe('SearchPage', () => {
}); });
}); });
it('renders SearchList with correct props', () => { it('renders ResourceList with correct props', () => {
expect(content.children().find(SearchList).props()).toMatchObject({
results: props.tables.results,
params: {
source: SEARCH_SOURCE_NAME,
paginationStartIndex: 0,
},
});
});
it('does not render Pagination if total_results <= RESULTS_PER_PAGE', () => {
expect(content.children().find(Pagination).exists()).toBeFalsy()
});
it('renders Pagination with correct props if total_results > RESULTS_PER_PAGE', () => {
const { props, wrapper } = setup(); const { props, wrapper } = setup();
const testResults = { const testResults = {
page_index: 0, page_index: 0,
...@@ -585,12 +570,14 @@ describe('SearchPage', () => { ...@@ -585,12 +570,14 @@ describe('SearchPage', () => {
total_results: 11, total_results: 11,
}; };
content = shallow(wrapper.instance().getTabContent(testResults, TABLE_RESOURCE_TITLE)); content = shallow(wrapper.instance().getTabContent(testResults, TABLE_RESOURCE_TITLE));
expect(content.children().find(Pagination).props()).toMatchObject({
activePage: 1, expect(content.children().find(ResourceList).props()).toMatchObject({
itemsCountPerPage: RESULTS_PER_PAGE, activePage: 0,
totalItemsCount: 11, slicedItems: testResults.results,
pageRangeDisplayed: PAGINATION_PAGE_RANGE, slicedItemsCount: testResults.total_results,
onChange: wrapper.instance().onPaginationChange, itemsPerPage: RESULTS_PER_PAGE,
onPagination: wrapper.instance().onPaginationChange,
source: SEARCH_SOURCE_NAME,
}); });
}); });
}); });
......
export const ITEMS_PER_PAGE = 4;
export const PAGINATION_PAGE_RANGE = 10;
export const BOOKMARK_TITLE = "My Bookmarks"; export const BOOKMARK_TITLE = "My Bookmarks";
export const BOOKMARKS_PER_PAGE = 4;
export const EMPTY_BOOKMARK_MESSAGE = "You don't have any bookmarks. Use the star icon to save a bookmark."; export const EMPTY_BOOKMARK_MESSAGE = "You don't have any bookmarks. Use the star icon to save a bookmark.";
export const MY_BOOKMARKS_SOURCE_NAME = "bookmarks";
import * as React from 'react'; import * as React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Pagination from 'react-js-pagination'; import { GlobalState } from 'ducks/rootReducer';
import { GlobalState } from "ducks/rootReducer";
import './styles.scss' import './styles.scss'
import { Bookmark } from 'interfaces'; import { Bookmark } from 'interfaces';
import ResourceListItem from "components/common/ResourceListItem";
import { import {
ITEMS_PER_PAGE,
EMPTY_BOOKMARK_MESSAGE,
PAGINATION_PAGE_RANGE,
BOOKMARK_TITLE, BOOKMARK_TITLE,
} from "./constants"; BOOKMARKS_PER_PAGE,
EMPTY_BOOKMARK_MESSAGE,
MY_BOOKMARKS_SOURCE_NAME,
} from './constants';
import ResourceList from 'components/common/ResourceList';
interface StateFromProps { interface StateFromProps {
myBookmarks: Bookmark[]; myBookmarks: Bookmark[];
isLoaded: boolean; isLoaded: boolean;
} }
export type BookmarkListProps = StateFromProps; export type MyBookmarksProps = StateFromProps;
interface BookmarkListState {
activePage: number;
}
export class BookmarkList extends React.Component<BookmarkListProps, BookmarkListState> { export class MyBookmarks extends React.Component<MyBookmarksProps> {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { activePage: 0 };
} }
onPaginationChange = (pageNumber: number) => {
const index = pageNumber - 1;
this.setState({ activePage: index });
};
render() { render() {
if (!this.props.isLoaded) { if (!this.props.isLoaded) {
return null; return null;
} }
const totalBookmarks = this.props.myBookmarks.length; const bookmarksLength = this.props.myBookmarks.length;
const startIndex = this.state.activePage * ITEMS_PER_PAGE;
const displayedBookmarks = this.props.myBookmarks.slice(startIndex, startIndex + ITEMS_PER_PAGE);
if (totalBookmarks === 0) {
return ( return (
<div className="bookmark-list"> <div className="bookmark-list">
<div className="title-1">{ BOOKMARK_TITLE }</div> <div className="title-1">{ BOOKMARK_TITLE }</div>
{
bookmarksLength === 0 &&
<div className="empty-message body-placeholder"> <div className="empty-message body-placeholder">
{ EMPTY_BOOKMARK_MESSAGE } { EMPTY_BOOKMARK_MESSAGE }
</div> </div>
</div>
)
}
return (
<div className="bookmark-list">
<div className="title-1">{ BOOKMARK_TITLE }</div>
<ul className="list-group">
{
displayedBookmarks.map((resource, index) => {
const logging = { index: index + this.state.activePage * ITEMS_PER_PAGE , source: 'Bookmarks' };
return <ResourceListItem item={ resource } logging={ logging } key={ index } />;
})
} }
</ul>
{ {
totalBookmarks > ITEMS_PER_PAGE && bookmarksLength !== 0 &&
<div className="pagination-container"> <ResourceList
<Pagination allItems={ this.props.myBookmarks }
activePage={ this.state.activePage + 1 } source={ MY_BOOKMARKS_SOURCE_NAME }
itemsCountPerPage={ ITEMS_PER_PAGE } itemsPerPage={ BOOKMARKS_PER_PAGE }
totalItemsCount={ totalBookmarks }
pageRangeDisplayed={ PAGINATION_PAGE_RANGE }
onChange={ this.onPaginationChange }
/> />
</div>
} }
</div> </div>
) );
} }
} }
...@@ -91,4 +60,4 @@ export const mapStateToProps = (state: GlobalState) => { ...@@ -91,4 +60,4 @@ export const mapStateToProps = (state: GlobalState) => {
}; };
}; };
export default connect<StateFromProps>(mapStateToProps)(BookmarkList); export default connect<StateFromProps>(mapStateToProps)(MyBookmarks);
...@@ -7,8 +7,7 @@ ...@@ -7,8 +7,7 @@
margin-bottom: 32px; margin-bottom: 32px;
} }
.empty-message, .empty-message {
.pagination-container {
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
......
...@@ -4,20 +4,20 @@ import { shallow } from 'enzyme'; ...@@ -4,20 +4,20 @@ import { shallow } from 'enzyme';
import globalState from 'fixtures/globalState'; import globalState from 'fixtures/globalState';
import { ResourceType } from 'interfaces'; import { ResourceType } from 'interfaces';
import Pagination from 'react-js-pagination'; import { MyBookmarks, MyBookmarksProps, mapStateToProps } from '../';
import ResourceListItem from 'components/common/ResourceListItem' import ResourceList from 'components/common/ResourceList';
import { BookmarkList, BookmarkListProps, mapStateToProps } from "../";
import { import {
ITEMS_PER_PAGE,
EMPTY_BOOKMARK_MESSAGE,
BOOKMARK_TITLE, BOOKMARK_TITLE,
BOOKMARKS_PER_PAGE,
EMPTY_BOOKMARK_MESSAGE,
MY_BOOKMARKS_SOURCE_NAME,
} from '../constants'; } from '../constants';
describe('BookmarkList', () => { describe('MyBookmarks', () => {
const setStateSpy = jest.spyOn(BookmarkList.prototype, 'setState'); const setStateSpy = jest.spyOn(MyBookmarks.prototype, 'setState');
const setup = (propOverrides?: Partial<BookmarkListProps>) => { const setup = (propOverrides?: Partial<MyBookmarksProps>) => {
const props: BookmarkListProps = { const props: MyBookmarksProps = {
myBookmarks: [ myBookmarks: [
{ {
key: 'bookmark-1', key: 'bookmark-1',
...@@ -77,20 +77,10 @@ describe('BookmarkList', () => { ...@@ -77,20 +77,10 @@ describe('BookmarkList', () => {
isLoaded: true, isLoaded: true,
...propOverrides ...propOverrides
}; };
const wrapper = shallow<BookmarkList>(<BookmarkList {...props} />); const wrapper = shallow<MyBookmarks>(<MyBookmarks {...props} />);
return { props, wrapper }; return { props, wrapper };
}; };
describe('onPaginationChange', () => {
it('reduces the page index by 1 and updates state', () => {
const { props, wrapper } = setup();
const pageNumber = 3;
wrapper.instance().onPaginationChange(pageNumber);
expect(setStateSpy).toHaveBeenCalledWith({ activePage: pageNumber - 1 });
});
});
describe('Render', () => { describe('Render', () => {
it('Renders nothing until ready', () => { it('Renders nothing until ready', () => {
const { props, wrapper } = setup({ isLoaded: false }); const { props, wrapper } = setup({ isLoaded: false });
...@@ -107,29 +97,14 @@ describe('BookmarkList', () => { ...@@ -107,29 +97,14 @@ describe('BookmarkList', () => {
expect(wrapper.find('.empty-message').text()).toEqual(EMPTY_BOOKMARK_MESSAGE); expect(wrapper.find('.empty-message').text()).toEqual(EMPTY_BOOKMARK_MESSAGE);
}); });
it('Renders at most ITEMS_PER_PAGE bookmarks at once', () => { it('Renders ResourceList with the correct props', () => {
const { props, wrapper } = setup();
expect(wrapper.find(ResourceListItem).length).toEqual(ITEMS_PER_PAGE)
});
it('Renders a pagination widget when there are more than ITEMS_PER_PAGE bookmarks', () => {
const { props, wrapper } = setup(); const { props, wrapper } = setup();
expect(wrapper.find(Pagination).exists()).toBe(true)
});
it('Hides a pagination widget when there are fewer than ITEMS_PER_PAGE bookmarks', () => { expect(wrapper.children().find(ResourceList).props()).toMatchObject({
const { props, wrapper } = setup({ allItems: props.myBookmarks,
myBookmarks: [{ itemsPerPage: BOOKMARKS_PER_PAGE,
key: 'bookmark-1', source: MY_BOOKMARKS_SOURCE_NAME,
type: ResourceType.table,
cluster: 'cluster',
database: 'database',
description: 'description',
name: 'name',
schema_name: 'schema_name',
}]
}); });
expect(wrapper.find(Pagination).exists()).toBe(false)
}); });
}); });
}); });
......
...@@ -2,3 +2,4 @@ ...@@ -2,3 +2,4 @@
export const POPULAR_TABLES_INFO_TEXT = 'These are some of the most commonly accessed tables within your organization.'; export const POPULAR_TABLES_INFO_TEXT = 'These are some of the most commonly accessed tables within your organization.';
export const POPULAR_TABLES_LABEL = 'Popular Tables'; export const POPULAR_TABLES_LABEL = 'Popular Tables';
export const POPULAR_TABLES_SOURCE_NAME = 'popular_tables'; export const POPULAR_TABLES_SOURCE_NAME = 'popular_tables';
export const POPULAR_TABLES_PER_PAGE = 4;
...@@ -3,9 +3,14 @@ import * as React from 'react'; ...@@ -3,9 +3,14 @@ import * as React from 'react';
// TODO: Use css-modules instead of 'import' // TODO: Use css-modules instead of 'import'
import './styles.scss'; import './styles.scss';
import { POPULAR_TABLES_LABEL, POPULAR_TABLES_INFO_TEXT, POPULAR_TABLES_SOURCE_NAME } from './constants'; import {
POPULAR_TABLES_LABEL,
POPULAR_TABLES_INFO_TEXT,
POPULAR_TABLES_SOURCE_NAME,
POPULAR_TABLES_PER_PAGE
} from './constants';
import InfoButton from 'components/common/InfoButton'; import InfoButton from 'components/common/InfoButton';
import SearchList from 'components/SearchPage/SearchList'; import ResourceList from 'components/common/ResourceList';
import { getPopularTables } from 'ducks/popularTables/reducer'; import { getPopularTables } from 'ducks/popularTables/reducer';
import { GetPopularTablesRequest, TableResource } from 'ducks/popularTables/types'; import { GetPopularTablesRequest, TableResource } from 'ducks/popularTables/types';
...@@ -39,12 +44,10 @@ export class PopularTables extends React.Component<PopularTablesProps> { ...@@ -39,12 +44,10 @@ export class PopularTables extends React.Component<PopularTablesProps> {
<label className="title-1">{POPULAR_TABLES_LABEL}</label> <label className="title-1">{POPULAR_TABLES_LABEL}</label>
<InfoButton infoText={POPULAR_TABLES_INFO_TEXT} /> <InfoButton infoText={POPULAR_TABLES_INFO_TEXT} />
</div> </div>
<SearchList <ResourceList
results={this.props.popularTables} allItems={ this.props.popularTables }
params={{ source={ POPULAR_TABLES_SOURCE_NAME }
source: POPULAR_TABLES_SOURCE_NAME, itemsPerPage={ POPULAR_TABLES_PER_PAGE }
paginationStartIndex: 0,
}}
/> />
</> </>
); );
......
...@@ -4,10 +4,11 @@ import { shallow } from 'enzyme'; ...@@ -4,10 +4,11 @@ import { shallow } from 'enzyme';
import { import {
POPULAR_TABLES_INFO_TEXT, POPULAR_TABLES_INFO_TEXT,
POPULAR_TABLES_LABEL, POPULAR_TABLES_LABEL,
POPULAR_TABLES_PER_PAGE,
POPULAR_TABLES_SOURCE_NAME, POPULAR_TABLES_SOURCE_NAME,
} from '../constants'; } from '../constants';
import InfoButton from 'components/common/InfoButton'; import InfoButton from 'components/common/InfoButton';
import SearchList from 'components/SearchPage/SearchList'; import ResourceList from 'components/common/ResourceList';
import globalState from 'fixtures/globalState'; import globalState from 'fixtures/globalState';
import { PopularTables, PopularTablesProps, mapStateToProps, mapDispatchToProps } from '..'; import { PopularTables, PopularTablesProps, mapStateToProps, mapDispatchToProps } from '..';
...@@ -80,13 +81,11 @@ describe('PopularTables', () => { ...@@ -80,13 +81,11 @@ describe('PopularTables', () => {
}); });
}); });
it('renders SearchList with correct props', () => { it('renders ResourceList with correct props', () => {
expect(wrapper.children().find(SearchList).props()).toMatchObject({ expect(wrapper.children().find(ResourceList).props()).toMatchObject({
results: props.popularTables, allItems: props.popularTables,
params: { itemsPerPage: POPULAR_TABLES_PER_PAGE,
source: POPULAR_TABLES_SOURCE_NAME, source: POPULAR_TABLES_SOURCE_NAME,
paginationStartIndex: 0,
},
}); });
}); });
}); });
......
export const ITEMS_PER_PAGE = 4;
export const PAGINATION_PAGE_RANGE = 10;
import * as React from 'react';
import Pagination from 'react-js-pagination';
import ResourceListItem from 'components/common/ResourceListItem';
import { Resource } from 'interfaces';
import { ITEMS_PER_PAGE, PAGINATION_PAGE_RANGE } from './constants';
export interface ResourceListProps {
source: string;
paginate?: boolean;
itemsPerPage?: number;
// Choose to use either 'allItems' vs 'slicedItems' depending on if you're passing the entire list
// of items vs a pre-sliced section of all items.
allItems?: Resource[];
// 'slicedItems' and 'slicedItemsCount' should be used together
slicedItems?: Resource[];
slicedItemsCount?: number;
// 'onPagination' and 'activePage' should be used together
onPagination?: (pageNumber: number) => void;
activePage?: number;
}
interface ResourceListState {
activePage: number;
}
class ResourceList extends React.Component<ResourceListProps, ResourceListState> {
public static defaultProps: Partial<ResourceListProps> = {
paginate: true,
itemsPerPage: ITEMS_PER_PAGE,
};
constructor(props) {
super(props);
this.state = { activePage: 0 };
}
onPagination = (rawPageNum: number) => {
const activePage = rawPageNum - 1;
if (this.props.onPagination !== undefined) {
// activePage is managed externally via 'props'
this.props.onPagination(activePage);
} else {
// activePage is managed internally via 'state'.
this.setState({ activePage });
}
};
render() {
const { allItems, slicedItems, itemsPerPage, paginate, source } = this.props;
const activePage = this.props.activePage !== undefined ? this.props.activePage : this.state.activePage;
const itemsCount = this.props.slicedItemsCount || allItems.length;
const startIndex = itemsPerPage * activePage;
let itemsToRender = slicedItems || allItems;
if (paginate && allItems) {
itemsToRender = allItems.slice(startIndex, startIndex + itemsPerPage);
}
return (
<>
<ul className="list-group">
{
itemsToRender.map((item, idx) => {
const logging = { source, index: startIndex + idx };
return <ResourceListItem item={ item } logging={ logging } key={ idx } />;
})
}
</ul>
{
paginate &&
itemsCount > itemsPerPage &&
<div className="text-center">
<Pagination
activePage={ activePage + 1 }
itemsCountPerPage={ itemsPerPage }
totalItemsCount={ itemsCount }
pageRangeDisplayed={ PAGINATION_PAGE_RANGE }
onChange={ this.onPagination }
/>
</div>
}
</>
);
}
}
export default ResourceList;
import * as React from 'react';
import Pagination from 'react-js-pagination';
import { shallow } from 'enzyme';
import { ResourceType } from 'interfaces';
import ResourceListItem from 'components/common/ResourceListItem/index';
import ResourceList, { ResourceListProps } from '../';
describe('ResourceList', () => {
const setStateSpy = jest.spyOn(ResourceList.prototype, 'setState');
const setupAllItems = (propOverrides?: Partial<ResourceListProps>) => {
const props: ResourceListProps = {
allItems: [
{ type: ResourceType.table },
{ type: ResourceType.table },
{ type: ResourceType.table },
{ type: ResourceType.table },
{ type: ResourceType.table },
{ type: ResourceType.table },
],
itemsPerPage: 4,
source: 'testSource',
...propOverrides
};
const wrapper = shallow<ResourceList>(<ResourceList {...props} />);
return { props, wrapper };
};
const setupSlicedItems = (propOverrides?: Partial<ResourceListProps>) => {
const props: ResourceListProps = {
source: 'testSource',
activePage: 3,
slicedItems: [
{ type: ResourceType.table },
{ type: ResourceType.table },
{ type: ResourceType.table },
{ type: ResourceType.table },
],
slicedItemsCount: 40,
itemsPerPage: 4,
onPagination: jest.fn(),
...propOverrides
};
const wrapper = shallow<ResourceList>(<ResourceList {...props} />);
return { props, wrapper };
};
describe('render with no pagination', () => {
let props;
let wrapper;
beforeAll(() => {
const setupResult = setupAllItems({ paginate: false });
props = setupResult.props;
wrapper = setupResult.wrapper;
});
it('should render all items', () => {
const items = wrapper.find(ResourceListItem);
expect(items.length).toEqual(props.allItems.length);
});
it('passes correct props to each ResourceListItem', () => {
const items = wrapper.find(ResourceListItem);
items.forEach((contentItem, idx) => {
expect(contentItem.props()).toMatchObject({
item: props.allItems[idx],
logging: {
source: props.source,
index: idx,
}
})
});
});
});
describe('render allItems', () => {
let props;
let wrapper;
beforeAll(() => {
const setupResult = setupAllItems();
props = setupResult.props;
wrapper = setupResult.wrapper;
});
it('renders at most itemsPerPage ResourceListItems', () => {
const items = wrapper.find(ResourceListItem);
expect(items.length).toEqual(props.itemsPerPage);
});
it('renders a pagination widget when there are more than itemsPerPage items', () => {
expect(wrapper.find(Pagination).exists()).toBe(true)
});
it('hides a pagination widget when there are fewer than itemsPerPage items', () => {
const { props, wrapper } = setupAllItems({
itemsPerPage: 20,
});
expect(wrapper.find(Pagination).exists()).toBe(false)
});
});
describe('render slicedItems', () => {
let props;
let wrapper;
beforeAll(() => {
const setupResult = setupSlicedItems();
props = setupResult.props;
wrapper = setupResult.wrapper;
});
it('renders all slicedItems', () => {
const items = wrapper.find(ResourceListItem);
expect(items.length).toEqual(props.slicedItems.length);
});
it('renders pagination widget when slicedItemsCount is greater than itemsPerPage', () => {
expect(wrapper.find(Pagination).exists()).toBe(true);
});
it('hides pagination widget when slicedItemsCount is less than itemsPerPage', () => {
const { props, wrapper } = setupAllItems({
itemsPerPage: 20,
});
expect(wrapper.find(Pagination).exists()).toBe(false);
});
});
describe('onPagination', () => {
it('calls the onPagination prop when it exists', () => {
const setupResult = setupSlicedItems();
const wrapper = setupResult.wrapper;
const props = setupResult.props;
const onPaginationSpy = jest.spyOn(props, 'onPagination');
wrapper.instance().onPagination(3);
expect(onPaginationSpy).toHaveBeenCalledWith(2);
});
it('calls setState when onPagination prop does not exist', () => {
setStateSpy.mockClear();
const setupResult = setupSlicedItems({ onPagination: undefined });
const wrapper = setupResult.wrapper;
wrapper.instance().onPagination(3);
expect(setStateSpy).toHaveBeenCalledWith({ activePage: 2 });
});
});
});
...@@ -5,7 +5,7 @@ import { LoggingParams } from '../types'; ...@@ -5,7 +5,7 @@ import { LoggingParams } from '../types';
import { TableResource } from 'interfaces'; import { TableResource } from 'interfaces';
import BookmarkIcon from "components/common/Bookmark/BookmarkIcon"; import BookmarkIcon from 'components/common/Bookmark/BookmarkIcon';
export interface TableListItemProps { export interface TableListItemProps {
table: TableResource; table: TableResource;
......
...@@ -13,7 +13,7 @@ export interface ListItemProps { ...@@ -13,7 +13,7 @@ export interface ListItemProps {
item: Resource; item: Resource;
} }
export default class ResourceListItem extends React.Component<ListItemProps, {}> { export default class ResourceListItem extends React.Component<ListItemProps> {
constructor(props) { constructor(props) {
super(props); super(props);
} }
......
...@@ -16,7 +16,7 @@ import Footer from './components/Footer'; ...@@ -16,7 +16,7 @@ import Footer from './components/Footer';
import HomePage from './components/HomePage' import HomePage from './components/HomePage'
import NavBar from './components/NavBar'; import NavBar from './components/NavBar';
import NotFoundPage from './components/NotFoundPage'; import NotFoundPage from './components/NotFoundPage';
import Preloader from "components/common/Preloader"; import Preloader from 'components/common/Preloader';
import ProfilePage from './components/ProfilePage'; import ProfilePage from './components/ProfilePage';
import SearchPage from './components/SearchPage'; import SearchPage from './components/SearchPage';
import TableDetail from './components/TableDetail'; import TableDetail from './components/TableDetail';
......
...@@ -1074,13 +1074,15 @@ ...@@ -1074,13 +1074,15 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
"dev": true "dev": true,
"optional": true
}, },
"is-glob": { "is-glob": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
"integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"is-extglob": "^1.0.0" "is-extglob": "^1.0.0"
} }
...@@ -4947,7 +4949,8 @@ ...@@ -4947,7 +4949,8 @@
"ansi-regex": { "ansi-regex": {
"version": "2.1.1", "version": "2.1.1",
"resolved": false, "resolved": false,
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"optional": true
}, },
"aproba": { "aproba": {
"version": "1.2.0", "version": "1.2.0",
...@@ -4968,12 +4971,14 @@ ...@@ -4968,12 +4971,14 @@
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"resolved": false, "resolved": false,
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"optional": true
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"resolved": false, "resolved": false,
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"optional": true,
"requires": { "requires": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
...@@ -4988,17 +4993,20 @@ ...@@ -4988,17 +4993,20 @@
"code-point-at": { "code-point-at": {
"version": "1.1.0", "version": "1.1.0",
"resolved": false, "resolved": false,
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
"optional": true
}, },
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": false, "resolved": false,
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"optional": true
}, },
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"resolved": false, "resolved": false,
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
"optional": true
}, },
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
...@@ -5115,7 +5123,8 @@ ...@@ -5115,7 +5123,8 @@
"inherits": { "inherits": {
"version": "2.0.3", "version": "2.0.3",
"resolved": false, "resolved": false,
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
"optional": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.5",
...@@ -5127,6 +5136,7 @@ ...@@ -5127,6 +5136,7 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": false, "resolved": false,
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"optional": true,
"requires": { "requires": {
"number-is-nan": "^1.0.0" "number-is-nan": "^1.0.0"
} }
...@@ -5141,6 +5151,7 @@ ...@@ -5141,6 +5151,7 @@
"version": "3.0.4", "version": "3.0.4",
"resolved": false, "resolved": false,
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"optional": true,
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
...@@ -5148,12 +5159,14 @@ ...@@ -5148,12 +5159,14 @@
"minimist": { "minimist": {
"version": "0.0.8", "version": "0.0.8",
"resolved": false, "resolved": false,
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"optional": true
}, },
"minipass": { "minipass": {
"version": "2.2.4", "version": "2.2.4",
"resolved": false, "resolved": false,
"integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==",
"optional": true,
"requires": { "requires": {
"safe-buffer": "^5.1.1", "safe-buffer": "^5.1.1",
"yallist": "^3.0.0" "yallist": "^3.0.0"
...@@ -5172,6 +5185,7 @@ ...@@ -5172,6 +5185,7 @@
"version": "0.5.1", "version": "0.5.1",
"resolved": false, "resolved": false,
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"optional": true,
"requires": { "requires": {
"minimist": "0.0.8" "minimist": "0.0.8"
} }
...@@ -5252,7 +5266,8 @@ ...@@ -5252,7 +5266,8 @@
"number-is-nan": { "number-is-nan": {
"version": "1.0.1", "version": "1.0.1",
"resolved": false, "resolved": false,
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
"optional": true
}, },
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
...@@ -5264,6 +5279,7 @@ ...@@ -5264,6 +5279,7 @@
"version": "1.4.0", "version": "1.4.0",
"resolved": false, "resolved": false,
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"optional": true,
"requires": { "requires": {
"wrappy": "1" "wrappy": "1"
} }
...@@ -5349,7 +5365,8 @@ ...@@ -5349,7 +5365,8 @@
"safe-buffer": { "safe-buffer": {
"version": "5.1.1", "version": "5.1.1",
"resolved": false, "resolved": false,
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==",
"optional": true
}, },
"safer-buffer": { "safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
...@@ -5385,6 +5402,7 @@ ...@@ -5385,6 +5402,7 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": false, "resolved": false,
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"optional": true,
"requires": { "requires": {
"code-point-at": "^1.0.0", "code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0", "is-fullwidth-code-point": "^1.0.0",
...@@ -5404,6 +5422,7 @@ ...@@ -5404,6 +5422,7 @@
"version": "3.0.1", "version": "3.0.1",
"resolved": false, "resolved": false,
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"optional": true,
"requires": { "requires": {
"ansi-regex": "^2.0.0" "ansi-regex": "^2.0.0"
} }
...@@ -5447,12 +5466,14 @@ ...@@ -5447,12 +5466,14 @@
"wrappy": { "wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": false, "resolved": false,
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"optional": true
}, },
"yallist": { "yallist": {
"version": "3.0.2", "version": "3.0.2",
"resolved": false, "resolved": false,
"integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=",
"optional": true
} }
} }
}, },
...@@ -11782,7 +11803,7 @@ ...@@ -11782,7 +11803,7 @@
"react-avatar": { "react-avatar": {
"version": "2.5.1", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/react-avatar/-/react-avatar-2.5.1.tgz", "resolved": "https://registry.npmjs.org/react-avatar/-/react-avatar-2.5.1.tgz",
"integrity": "sha512-bwH5pWY6uxaKZt+IZBfD+SU3Dpy3FaKbmAzrOI4N8SATUPLXOdGaJHWUl6Vl8hHSwWSsoLh/m7xYHdnn0lofZw==", "integrity": "sha1-W76MHQpSWT1GCPs9hinamV7rcJU=",
"requires": { "requires": {
"babel-runtime": ">=5.0.0", "babel-runtime": ">=5.0.0",
"is-retina": "^1.0.3", "is-retina": "^1.0.3",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment