Unverified Commit 7c06fdf7 authored by Ryan Lieu's avatar Ryan Lieu Committed by GitHub

Home page component (#190)

* new home page component

* separated home page

* searchbar fix

* separated search bar into separate component

* fixed breadcrumbs to go back to proper url

* cleaned up consolelogs, cleaned up home page component

* addressed comments

* addressed more comments'

* more code styling fixes

* removed unecessary files, fixed styling for home page to include larger margins

* making breadcrumb an isolated component

* gave breadcrumbs default props, fixed styling, removed unecessary API call on componentDidMount

* added tests

* fixed homepage tests, cleaned up code

* removed extraneous import for homepage test

* fixed breadcrumb/test issues?

* cleaned up breadcrumb code

* fixed breadcrumb logic

* removed default props

* one last breadcrumb fix
parent e4c02787
import * as React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { RouteComponentProps } from 'react-router';
// TODO: Use css-modules instead of 'import'
import './styles.scss';
import BookmarkList from 'components/common/Bookmark/BookmarkList';
import PopularTables from 'components/common/PopularTables';
import { SearchAllReset } from 'ducks/search/types';
import { searchReset } from 'ducks/search/reducer';
import SearchBar from 'components/SearchPage/SearchBar';
export interface DispatchFromProps {
searchReset: () => SearchAllReset;
}
export type HomePageProps = DispatchFromProps & RouteComponentProps<any>;
export class HomePage extends React.Component<HomePageProps> {
constructor(props) {
super(props);
}
componentDidMount() {
this.props.searchReset();
}
render() {
return (
<div className="container home-page">
<div className="row">
<div className="col-xs-12 col-md-offset-1 col-md-10">
<SearchBar />
<div className="home-element-container">
<BookmarkList />
</div>
<div className="home-element-container">
<PopularTables />
</div>
</div>
</div>
</div>
);
}
}
export const mapDispatchToProps = (dispatch: any) => {
return bindActionCreators({ searchReset } , dispatch);
};
export default connect<DispatchFromProps>(null, mapDispatchToProps)(HomePage);
@import 'variables';
.home-page {
.home-element-container {
margin-top: 64px;
}
@media (max-width: $screen-sm-max) {
.home-element-container {
margin-top: 32px;
}
}
}
import * as React from 'react';
import { shallow } from 'enzyme';
import { mapDispatchToProps, HomePage, HomePageProps } from '../';
import SearchBar from 'components/SearchPage/SearchBar';
import BookmarkList from 'components/common/Bookmark/BookmarkList';
import PopularTables from 'components/common/PopularTables';
describe('HomePage', () => {
const setup = (propOverrides?: Partial<HomePageProps>) => {
const props: HomePageProps = {
searchReset: jest.fn(),
history: {
length: 2,
action: "POP",
location: jest.fn() as any,
push: jest.fn(),
replace: jest.fn(),
go: jest.fn(),
goBack: jest.fn(),
goForward: jest.fn(),
block: jest.fn(),
createHref: jest.fn(),
listen: jest.fn(),
},
location: {
search: '/search?searchTerm=testName&selectedTab=table&pageIndex=1',
pathname: 'mockstr',
state: jest.fn(),
hash: 'mockstr',
},
match: jest.fn() as any,
staticContext: jest.fn() as any,
...propOverrides
};
const wrapper = shallow<HomePage>(<HomePage {...props} />)
return { props, wrapper };
};
let props;
let wrapper;
beforeAll(() => {
const setupResult = setup();
props = setupResult.props;
wrapper = setupResult.wrapper;
});
describe('render', () => {
it('contains Searchbar, BookmarkList, and PopularTables', () => {
expect(wrapper.contains(<SearchBar />));
expect(wrapper.contains(<BookmarkList />));
expect(wrapper.contains(<PopularTables />));
});
});
describe('componentDidMount', () => {
it('calls searchReset', () => {
const searchResetSpy = jest.spyOn(props, 'searchReset');
wrapper.instance().componentDidMount();
expect(searchResetSpy).toHaveBeenCalled;
});
});
});
describe('mapDispatchToProps', () => {
let dispatch;
let result;
beforeAll(() => {
dispatch = jest.fn(() => Promise.resolve());
result = mapDispatchToProps(dispatch);
});
it('sets searchReset on the props', () => {
expect(result.searchReset).toBeInstanceOf(Function);
});
});
......@@ -10,7 +10,7 @@ const NotFoundPage: React.SFC<any> = () => {
return (
<DocumentTitle title="404 Page Not Found - Amundsen">
<div className="container not-found-page">
<Breadcrumb path='/' text='Home'/>
<Breadcrumb path="/" text="Home" />
<h1>404 Page Not Found</h1>
<img className="icon icon-alert"/>
</div>
......
......@@ -86,7 +86,8 @@ export class ProfilePage extends React.Component<ProfilePageProps> {
<div className="container profile-page">
<div className="row">
<div className="col-xs-12 col-md-offset-1 col-md-10">
<Breadcrumb path='/' text='Search Results'/>
{/* remove hardcode to home when this page is ready for production */}
<Breadcrumb path="/" text="Home" />
<div className="profile-header">
<div id="profile-avatar" className="profile-avatar">
{
......
......@@ -95,7 +95,7 @@ describe('ProfilePage', () => {
it('renders Breadcrumb with correct props', () => {
expect(wrapper.find(Breadcrumb).props()).toMatchObject({
path: '/',
text: 'Search Results',
text: 'Home',
});
});
......
import * as React from 'react';
import { connect } from 'react-redux';
import { RouteComponentProps, withRouter } from 'react-router';
// TODO: Use css-modules instead of 'import'
import './styles.scss';
......@@ -11,24 +13,28 @@ import {
SYNTAX_ERROR_PREFIX,
SYNTAX_ERROR_SPACING_SUFFIX,
} from './constants';
import { GlobalState } from 'ducks/rootReducer';
export interface SearchBarProps {
handleValueSubmit: (term: string) => void;
export interface StateFromProps {
searchTerm: string;
}
export interface OwnProps {
placeholder?: string;
searchTerm?: string;
subText?: string;
}
export type SearchBarProps = StateFromProps & OwnProps & RouteComponentProps<any>;
interface SearchBarState {
subTextClassName: string;
searchTerm: string;
subText: string;
}
class SearchBar extends React.Component<SearchBarProps, SearchBarState> {
export class SearchBar extends React.Component<SearchBarProps, SearchBarState> {
public static defaultProps: Partial<SearchBarProps> = {
placeholder: PLACEHOLDER_DEFAULT,
searchTerm: '',
subText: SUBTEXT_DEFAULT,
};
......@@ -54,7 +60,8 @@ class SearchBar extends React.Component<SearchBarProps, SearchBarState> {
handleValueSubmit = (event: React.FormEvent<HTMLFormElement>) : void => {
event.preventDefault();
if (this.isFormValid()) {
this.props.handleValueSubmit(this.state.searchTerm);
const pathName = `/search?searchTerm=${this.state.searchTerm}&selectedTab=table&pageIndex=0`;
this.props.history.push(pathName);
}
};
......@@ -111,4 +118,10 @@ class SearchBar extends React.Component<SearchBarProps, SearchBarState> {
}
}
export default SearchBar;
export const mapStateToProps = (state: GlobalState) => {
return {
searchTerm: state.search.search_term,
};
};
export default connect<StateFromProps>(mapStateToProps, null)(withRouter(SearchBar));
......@@ -2,7 +2,7 @@ import * as React from 'react';
import { shallow } from 'enzyme';
import SearchBar, { SearchBarProps } from '../';
import { mapStateToProps, SearchBar, SearchBarProps } from '../';
import {
ERROR_CLASSNAME,
SUBTEXT_DEFAULT,
......@@ -10,6 +10,7 @@ import {
SYNTAX_ERROR_PREFIX,
SYNTAX_ERROR_SPACING_SUFFIX,
} from '../constants';
import globalState from 'fixtures/globalState';
describe('SearchBar', () => {
const valueChangeMockEvent = { target: { value: 'Data Resources' } };
......@@ -18,7 +19,28 @@ describe('SearchBar', () => {
const setup = (propOverrides?: Partial<SearchBarProps>) => {
const props: SearchBarProps = {
handleValueSubmit: jest.fn(),
searchTerm: '',
history: {
length: 2,
action: "POP",
location: jest.fn() as any,
push: jest.fn(),
replace: jest.fn(),
go: jest.fn(),
goBack: jest.fn(),
goForward: jest.fn(),
block: jest.fn(),
createHref: jest.fn(),
listen: jest.fn(),
},
location: {
search: '/search?searchTerm=testName&selectedTab=table&pageIndex=1',
pathname: 'mockstr',
state: jest.fn(),
hash: 'mockstr',
},
match: jest.fn() as any,
staticContext: jest.fn() as any,
...propOverrides
};
const wrapper = shallow<SearchBar>(<SearchBar {...props} />)
......@@ -81,14 +103,14 @@ describe('SearchBar', () => {
it('submits with correct props if isFormValid()', () => {
// @ts-ignore: mocked events throw type errors
wrapper.instance().handleValueSubmit(submitMockEvent);
expect(props.handleValueSubmit).toHaveBeenCalledWith(wrapper.state().searchTerm);
expect(props.history.push).toHaveBeenCalledWith(`/search?searchTerm=${wrapper.state().searchTerm}&selectedTab=table&pageIndex=0`);
});
it('does not submit if !isFormValid()', () => {
const { props, wrapper } = setup({ searchTerm: 'tag:tag1 tag:tag2' });
// @ts-ignore: mocked events throw type errors
wrapper.instance().handleValueSubmit(submitMockEvent);
expect(props.handleValueSubmit).not.toHaveBeenCalled();
expect(props.history.push).not.toHaveBeenCalled();
});
});
......@@ -222,3 +244,14 @@ describe('SearchBar', () => {
});
});
});
describe('mapStateToProps', () => {
let result;
beforeAll(() => {
result = mapStateToProps(globalState);
});
it('sets searchTerm on the props', () => {
expect(result.searchTerm).toEqual(globalState.search.search_term);
});
});
......@@ -9,9 +9,6 @@ import { RouteComponentProps } from 'react-router';
import SearchBar from './SearchBar';
import SearchList from './SearchList';
import LoadingSpinner from 'components/common/LoadingSpinner';
import PopularTables from 'components/common/PopularTables';
import BookmarkList from 'components/common/Bookmark/BookmarkList'
import InfoButton from 'components/common/InfoButton';
import { ResourceType, TableResource } from 'interfaces';
import TabsComponent from 'components/common/Tabs';
......@@ -78,7 +75,7 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState
const { searchTerm, pageIndex, selectedTab } = params;
const { term, index, currentTab } = this.getSanitizedUrlParams(searchTerm, pageIndex, selectedTab);
this.setState({ selectedTab: currentTab });
if (term !== "") {
if (term !== '') {
this.props.searchAll(term, this.createSearchOptions(index, currentTab));
if (currentTab !== selectedTab || pageIndex !== index) {
this.updatePageUrl(term, currentTab, index);
......@@ -137,10 +134,6 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState
return 0;
};
onSearchBarSubmit = (searchTerm: string): void => {
this.updatePageUrl(searchTerm, this.state.selectedTab,0);
};
onPaginationChange = (pageNumber: number): void => {
const index = pageNumber - 1;
this.props.searchResource(this.state.selectedTab, this.props.searchTerm, index);
......@@ -158,15 +151,6 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState
this.props.history.push(pathName);
};
renderPopularTables = () => {
return (
<div className="search-list-container">
<BookmarkList />
<PopularTables />
</div>
)
};
renderSearchResults = () => {
const tabConfig = [
{
......@@ -246,10 +230,7 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState
if (this.props.isLoading) {
return (<LoadingSpinner/>);
}
if (this.props.searchTerm.length > 0) {
return this.renderSearchResults();
}
return this.renderPopularTables();
return this.renderSearchResults();
};
render() {
......@@ -258,7 +239,7 @@ export class SearchPage extends React.Component<SearchPageProps, SearchPageState
<div className="container search-page">
<div className="row">
<div className="col-xs-12 col-md-offset-1 col-md-10">
<SearchBar handleValueSubmit={ this.onSearchBarSubmit } searchTerm={ searchTerm }/>
<SearchBar />
{ this.renderContent() }
</div>
</div>
......
......@@ -28,7 +28,6 @@ import SearchList from '../SearchList';
import globalState from 'fixtures/globalState';
import LoadingSpinner from 'components/common/LoadingSpinner';
import BookmarkList from 'components/common/Bookmark/BookmarkList';
import PopularTables from 'components/common/PopularTables';
describe('SearchPage', () => {
const setStateSpy = jest.spyOn(SearchPage.prototype, 'setState');
......@@ -431,26 +430,6 @@ describe('SearchPage', () => {
});
});
describe('onSearchBarSubmit', () => {
let props;
let wrapper;
let updatePageUrlSpy;
beforeAll(() => {
const setupResult = setup();
props = setupResult.props;
wrapper = setupResult.wrapper;
updatePageUrlSpy = jest.spyOn(wrapper.instance(), 'updatePageUrl');
wrapper.instance().onSearchBarSubmit('searchTerm');
});
it('call updatePageUrl with correct parameters', () => {
expect(updatePageUrlSpy).toHaveBeenCalledWith('searchTerm', wrapper.state().selectedTab, 0);
});
});
describe('onPaginationChange', () => {
const testIndex = 10;
let props;
......@@ -618,11 +597,6 @@ describe('SearchPage', () => {
});
describe('renderContent', () => {
it('renders popular tables if searchTerm is empty', () => {
const {props, wrapper} = setup({ searchTerm: '' });
expect(wrapper.instance().renderContent()).toEqual(wrapper.instance().renderPopularTables());
});
it('renders search results when given search term', () => {
const {props, wrapper} = setup({ searchTerm: 'test' });
expect(wrapper.instance().renderContent()).toEqual(wrapper.instance().renderSearchResults());
......@@ -634,15 +608,6 @@ describe('SearchPage', () => {
});
});
describe('renderPopularTables', () => {
it('renders bookmark list and popular tables', () => {
const {props, wrapper} = setup();
wrapper.instance().renderPopularTables();
expect(wrapper.contains(<BookmarkList />));
expect(wrapper.contains(<PopularTables />));
});
});
describe('renderSearchResults', () => {
it('renders TabsComponent with correct props', () => {
const { props, wrapper } = setup({ searchTerm: 'test search' });
......@@ -680,10 +645,7 @@ describe('SearchPage', () => {
it('renders SearchBar with correct props', () => {
const { props, wrapper } = setup();
expect(wrapper.find(SearchBar).props()).toMatchObject({
handleValueSubmit: wrapper.instance().onSearchBarSubmit,
searchTerm: props.searchTerm,
});
expect(wrapper.find(SearchBar).exists()).toBeTruthy();
});
it('calls renderSearchResults if searchTerm is not empty string', () => {
......@@ -692,13 +654,6 @@ describe('SearchPage', () => {
wrapper.setProps(props);
expect(renderSearchResultsSpy).toHaveBeenCalled();
});
it('calls renderPopularTables is searchTerm is empty string', () => {
const { props, wrapper } = setup({ searchTerm: '' });
const renderPopularTablesSpy = jest.spyOn(wrapper.instance(), 'renderPopularTables');
wrapper.setProps(props);
expect(renderPopularTablesSpy).toHaveBeenCalled();
});
});
});
......
......@@ -118,7 +118,6 @@ export class TableDetail extends React.Component<TableDetailProps & RouteCompone
const params = qs.parse(this.props.location.search);
const searchIndex = params['index'];
const source = params['source'];
/* update the url stored in the browser history to remove params used for logging purposes */
if (searchIndex !== undefined) {
window.history.replaceState({}, '', `${window.location.origin}${window.location.pathname}`);
......@@ -331,14 +330,14 @@ export class TableDetail extends React.Component<TableDetailProps & RouteCompone
} else if (this.state.statusCode === 500) {
innerContent = (
<div className="container error-label">
<Breadcrumb path='/' text='Search Results'/>
<Breadcrumb />
<label className="d-block m-auto">Something went wrong...</label>
</div>
)
} else {
innerContent = (
<div className="container table-detail">
<Breadcrumb path='/' text='Search Results'/>
<Breadcrumb />
<div className="row">
<div className="detail-header col-xs-12 col-md-7 col-lg-8">
<h1 className="detail-header-text">
......
import * as React from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import './styles.scss';
import { GlobalState } from 'ducks/rootReducer';
export interface BreadcrumbProps {
path: string;
text: string;
export interface OwnProps {
path?: string;
text?: string;
}
const Breadcrumb: React.SFC<BreadcrumbProps> = ({ path, text }) => {
export interface StateFromProps {
searchTerm: string;
}
export type BreadcrumbProps = OwnProps & StateFromProps;
export const Breadcrumb: React.SFC<BreadcrumbProps> = (props) => {
let path = props.path;
let text = props.text;
if (!path && !text) {
path = '/';
text = 'Home';
if (props.searchTerm) {
path = `/search?searchTerm=${props.searchTerm}&selectedTab=table&pageIndex=0`
text = 'Search Results'
}
}
return (
<div className="amundsen-breadcrumb">
<Link to={path}>
......@@ -21,9 +39,10 @@ const Breadcrumb: React.SFC<BreadcrumbProps> = ({ path, text }) => {
);
};
Breadcrumb.defaultProps = {
path: '/',
text: 'Home',
export const mapStateToProps = (state: GlobalState) => {
return {
searchTerm: state.search.search_term,
};
};
export default Breadcrumb;
export default connect<StateFromProps>(mapStateToProps, null)(Breadcrumb);
......@@ -3,29 +3,70 @@ import * as React from 'react';
import { shallow } from 'enzyme';
import { Link } from 'react-router-dom';
import Breadcrumb, { BreadcrumbProps } from '../';
import { Breadcrumb, BreadcrumbProps } from '../';
describe('Breadcrumb', () => {
let props: BreadcrumbProps;
let subject;
let props: BreadcrumbProps;
let subject;
describe('render', () => {
beforeEach(() => {
props = {
path: 'testPath',
text: 'testText',
};
subject = shallow(<Breadcrumb {...props} />);
props = {
path: 'testPath',
text: 'testText',
searchTerm: '',
};
subject = shallow(<Breadcrumb {...props} />);
});
describe('render', () => {
it('renders Link with correct path', () => {
expect(subject.find(Link).props()).toMatchObject({
to: props.path,
});
});
it('renders Link with correct path', () => {
expect(subject.find(Link).props()).toMatchObject({
to: props.path,
});
});
it('renders button with correct text within the Link', () => {
expect(subject.find(Link).find('button').text()).toEqual(props.text);
});
});
describe('render with existing searchTerm', () => {
beforeEach(() => {
props = {
searchTerm: 'testTerm',
};
subject = shallow(<Breadcrumb {...props} />);
});
it('renders Link with correct path', () => {
expect(subject.find(Link).props()).toMatchObject({
to: '/search?searchTerm=testTerm&selectedTab=table&pageIndex=0',
});
});
it('renders button with correct text within the Link', () => {
expect(subject.find(Link).find('button').text()).toEqual('Search Results');
});
});
describe('render with existing searchTerm and prop overrides', () => {
beforeEach(() => {
props = {
path: 'testPath',
text: 'testText',
searchTerm: 'testTerm',
};
subject = shallow(<Breadcrumb {...props} />);
});
it('renders Link with correct path', () => {
expect(subject.find(Link).props()).toMatchObject({
to: 'testPath',
});
});
it('renders button with correct text within the Link', () => {
expect(subject.find(Link).find('button').text()).toEqual(props.text);
});
it('renders button with correct text within the Link', () => {
expect(subject.find(Link).find('button').text()).toEqual('testText');
});
});
});
......@@ -2,6 +2,7 @@ import {
SearchAll,
SearchAllOptions,
SearchAllRequest,
SearchAllReset,
SearchAllResponse,
SearchResource,
SearchResourceRequest,
......@@ -12,7 +13,7 @@ import {
} from './types';
import { ResourceType } from 'interfaces';
export type SearchReducerAction = SearchAllResponse | SearchResourceResponse | SearchAllRequest | SearchResourceRequest;
export type SearchReducerAction = SearchAllResponse | SearchResourceResponse | SearchAllRequest | SearchResourceRequest | SearchAllReset;
export interface SearchReducerState {
search_term: string;
......@@ -39,6 +40,12 @@ export function searchResource(resource: ResourceType, term: string, pageIndex:
};
}
export function searchReset(): SearchAllReset {
return {
type: SearchAll.RESET,
};
}
const initialState: SearchReducerState = {
search_term: '',
isLoading: false,
......@@ -62,6 +69,8 @@ const initialState: SearchReducerState = {
export default function reducer(state: SearchReducerState = initialState, action: SearchReducerAction): SearchReducerState {
switch (action.type) {
// Updates search term to reflect action
case SearchAll.RESET:
return initialState;
case SearchAll.ACTION:
return {
...state,
......
......@@ -30,6 +30,7 @@ export enum SearchAll {
ACTION = 'amundsen/search/SEARCH_ALL',
SUCCESS = 'amundsen/search/SEARCH_ALL_SUCCESS',
FAILURE = 'amundsen/search/SEARCH_ALL_FAILURE',
RESET = 'amundsen/search/SEARCH_ALL_RESET',
}
export interface SearchAllOptions {
......@@ -49,6 +50,9 @@ export interface SearchAllResponse {
payload?: SearchReducerState;
}
export interface SearchAllReset {
type: SearchAll.RESET;
}
/* searchResource - Search a single resource type */
export enum SearchResource {
......
......@@ -13,11 +13,12 @@ import AnnouncementPage from './components/AnnouncementPage';
import BrowsePage from './components/BrowsePage';
import Feedback from './components/Feedback';
import Footer from './components/Footer';
import HomePage from './components/HomePage'
import NavBar from './components/NavBar';
import NotFoundPage from './components/NotFoundPage';
import Preloader from "components/common/Preloader";
import ProfilePage from './components/ProfilePage';
import SearchPage from './components/SearchPage';
import SearchPage from './components/SearchPage';
import TableDetail from './components/TableDetail';
import rootReducer from './ducks/rootReducer';
......@@ -43,7 +44,7 @@ ReactDOM.render(
<Route path="/search" component={SearchPage} />
<Route path="/user/:userId" component={ProfilePage} />
<Route path="/404" component={NotFoundPage} />
<Route path="/" component={SearchPage} />
<Route path="/" component={HomePage} />
</Switch>
<Feedback />
<Footer />
......
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