Unverified Commit 14cf1ff3 authored by Marcos Iglesias's avatar Marcos Iglesias Committed by GitHub

feat: Update ListItem Loading State into Shimmering loader (#484)

* Adds shimmer loader to Bookmarks

* Adds loader for popular tables

* Shimmering loader on results page

* Abstracting Shimering Resource Loader

* Adding tests

* Formatting

* Reusing RESULTS_PER_PAGE on the number of shimmering row items

* Reusing RESULTS_PER_PAGE on the number of shimmering row items
parent 7fdaf8db
export const SEARCH_BREADCRUMB_TEXT = 'Advanced Search'; export const SEARCH_BREADCRUMB_TEXT = 'Advanced Search';
export const HOMEPAGE_TITLE = 'Amundsen Homepage'; export const HOMEPAGE_TITLE = 'Amundsen Homepage';
export const TAGS_TITLE = 'Browse Tags';
...@@ -13,7 +13,11 @@ import { resetSearchState } from 'ducks/search/reducer'; ...@@ -13,7 +13,11 @@ import { resetSearchState } from 'ducks/search/reducer';
import { UpdateSearchStateReset } from 'ducks/search/types'; import { UpdateSearchStateReset } from 'ducks/search/types';
import SearchBar from 'components/common/SearchBar'; import SearchBar from 'components/common/SearchBar';
import TagsList from 'components/common/TagsList'; import TagsList from 'components/common/TagsList';
import { SEARCH_BREADCRUMB_TEXT, HOMEPAGE_TITLE } from './constants'; import {
SEARCH_BREADCRUMB_TEXT,
HOMEPAGE_TITLE,
TAGS_TITLE,
} from './constants';
export interface DispatchFromProps { export interface DispatchFromProps {
searchReset: () => UpdateSearchStateReset; searchReset: () => UpdateSearchStateReset;
...@@ -45,7 +49,7 @@ export class HomePage extends React.Component<HomePageProps> { ...@@ -45,7 +49,7 @@ export class HomePage extends React.Component<HomePageProps> {
id="browse-tags-header" id="browse-tags-header"
className="title-1 browse-tags-header" className="title-1 browse-tags-header"
> >
Browse Tags {TAGS_TITLE}
</div> </div>
<TagsList /> <TagsList />
</div> </div>
......
...@@ -6,7 +6,6 @@ import { shallow } from 'enzyme'; ...@@ -6,7 +6,6 @@ import { shallow } from 'enzyme';
import { ResourceType } from 'interfaces'; import { ResourceType } from 'interfaces';
import LoadingSpinner from 'components/common/LoadingSpinner';
import ResourceSelector from 'components/SearchPage/ResourceSelector'; import ResourceSelector from 'components/SearchPage/ResourceSelector';
import SearchFilter from 'components/SearchPage/SearchFilter'; import SearchFilter from 'components/SearchPage/SearchFilter';
import SearchPanel from 'components/SearchPage/SearchPanel'; import SearchPanel from 'components/SearchPage/SearchPanel';
...@@ -37,39 +36,37 @@ import { ...@@ -37,39 +36,37 @@ import {
SearchPageProps, SearchPageProps,
} from '.'; } from '.';
describe('SearchPage', () => { const setup = (
const setStateSpy = jest.spyOn(SearchPage.prototype, 'setState'); propOverrides?: Partial<SearchPageProps>,
const setup = ( location?: Partial<History.Location>
propOverrides?: Partial<SearchPageProps>, ) => {
location?: Partial<History.Location> const routerProps = getMockRouterProps<any>(null, location);
) => { const props: SearchPageProps = {
const routerProps = getMockRouterProps<any>(null, location); hasFilters: false,
const props: SearchPageProps = { searchTerm: globalState.search.search_term,
hasFilters: false, resource: ResourceType.table,
searchTerm: globalState.search.search_term, isLoading: false,
resource: ResourceType.table, dashboards: globalState.search.dashboards,
isLoading: false, tables: globalState.search.tables,
dashboards: globalState.search.dashboards, users: globalState.search.users,
tables: globalState.search.tables, setPageIndex: jest.fn(),
users: globalState.search.users, urlDidUpdate: jest.fn(),
setPageIndex: jest.fn(), ...routerProps,
urlDidUpdate: jest.fn(), ...propOverrides,
...routerProps,
...propOverrides,
};
const wrapper = shallow<SearchPage>(<SearchPage {...props} />);
return { props, wrapper };
}; };
const wrapper = shallow<SearchPage>(<SearchPage {...props} />);
return { props, wrapper };
};
describe('SearchPage', () => {
describe('componentDidMount', () => { describe('componentDidMount', () => {
let props; let props;
let wrapper; let wrapper;
beforeAll(() => { beforeAll(() => {
const setupResult = setup(null, { ({ props, wrapper } = setup(null, {
search: '/search?searchTerm=testName&resource=table&pageIndex=1', search: '/search?searchTerm=testName&resource=table&pageIndex=1',
}); }));
props = setupResult.props;
wrapper = setupResult.wrapper;
}); });
it('calls getUrlParams and getGlobalStateParams', () => { it('calls getUrlParams and getGlobalStateParams', () => {
...@@ -81,15 +78,12 @@ describe('SearchPage', () => { ...@@ -81,15 +78,12 @@ describe('SearchPage', () => {
describe('componentDidUpdate', () => { describe('componentDidUpdate', () => {
let props; let props;
let wrapper; let wrapper;
let mockPrevProps; let mockPrevProps;
beforeAll(() => { beforeAll(() => {
const setupResult = setup(null, { ({ props, wrapper } = setup(null, {
search: '/search?searchTerm=testName&resource=table&pageIndex=1', search: '/search?searchTerm=testName&resource=table&pageIndex=1',
}); }));
props = setupResult.props;
wrapper = setupResult.wrapper;
mockPrevProps = { mockPrevProps = {
searchTerm: 'previous', searchTerm: 'previous',
location: { location: {
...@@ -116,8 +110,9 @@ describe('SearchPage', () => { ...@@ -116,8 +110,9 @@ describe('SearchPage', () => {
describe('generateTabLabel', () => { describe('generateTabLabel', () => {
let wrapper; let wrapper;
beforeAll(() => { beforeAll(() => {
wrapper = setup().wrapper; ({ wrapper } = setup());
}); });
it('returns correct text for ResourceType.table', () => { it('returns correct text for ResourceType.table', () => {
...@@ -171,7 +166,7 @@ describe('SearchPage', () => { ...@@ -171,7 +166,7 @@ describe('SearchPage', () => {
}; };
}); });
it('if there is a searchTerm ', () => { it('if there is a searchTerm ', () => {
const { props, wrapper } = setup({ searchTerm: 'data' }); const { wrapper } = setup({ searchTerm: 'data' });
content = shallow( content = shallow(
wrapper.instance().getTabContent(testResults, ResourceType.table) wrapper.instance().getTabContent(testResults, ResourceType.table)
); );
...@@ -180,7 +175,7 @@ describe('SearchPage', () => { ...@@ -180,7 +175,7 @@ describe('SearchPage', () => {
}); });
it('if no searchTerm but there are filters active', () => { it('if no searchTerm but there are filters active', () => {
const { props, wrapper } = setup({ searchTerm: '', hasFilters: true }); const { wrapper } = setup({ searchTerm: '', hasFilters: true });
content = shallow( content = shallow(
wrapper.instance().getTabContent(testResults, ResourceType.table) wrapper.instance().getTabContent(testResults, ResourceType.table)
); );
...@@ -191,7 +186,7 @@ describe('SearchPage', () => { ...@@ -191,7 +186,7 @@ describe('SearchPage', () => {
describe('if page index is out of bounds', () => { describe('if page index is out of bounds', () => {
it('renders expected page index error message', () => { it('renders expected page index error message', () => {
const { props, wrapper } = setup(); const { wrapper } = setup();
const testResults = { const testResults = {
page_index: 2, page_index: 2,
results: [], results: [],
...@@ -210,6 +205,7 @@ describe('SearchPage', () => { ...@@ -210,6 +205,7 @@ describe('SearchPage', () => {
let props; let props;
let wrapper; let wrapper;
let generateInfoTextMockResults; let generateInfoTextMockResults;
beforeAll(() => { beforeAll(() => {
const setupResult = setup({ searchTerm: '' }); const setupResult = setup({ searchTerm: '' });
props = setupResult.props; props = setupResult.props;
...@@ -246,15 +242,19 @@ describe('SearchPage', () => { ...@@ -246,15 +242,19 @@ describe('SearchPage', () => {
describe('renderContent', () => { describe('renderContent', () => {
it('renders search results when given search term', () => { it('renders search results when given search term', () => {
const { props, wrapper } = setup({ searchTerm: 'test' }); const { wrapper } = setup({ searchTerm: 'test' });
expect(wrapper.instance().renderContent()).toEqual( expect(wrapper.instance().renderContent()).toEqual(
wrapper.instance().renderSearchResults() wrapper.instance().renderSearchResults()
); );
}); });
it('renders loading spinner when in loading state', () => { it('renders shimmering loader when in loading state', () => {
const { props, wrapper } = setup({ isLoading: true }); const { wrapper } = setup({ isLoading: true });
expect(wrapper.instance().renderContent()).toEqual(<LoadingSpinner />); const expected = 1;
const actual = wrapper.find('ShimmeringResourceLoader').length;
expect(actual).toEqual(expected);
}); });
}); });
...@@ -296,7 +296,7 @@ describe('SearchPage', () => { ...@@ -296,7 +296,7 @@ describe('SearchPage', () => {
}); });
it('renders null for an invalid resource', () => { it('renders null for an invalid resource', () => {
const { props, wrapper } = setup({ const { wrapper } = setup({
resource: null, resource: null,
}); });
const renderedSearchResults = wrapper.instance().renderSearchResults(); const renderedSearchResults = wrapper.instance().renderSearchResults();
...@@ -307,14 +307,14 @@ describe('SearchPage', () => { ...@@ -307,14 +307,14 @@ describe('SearchPage', () => {
describe('render', () => { describe('render', () => {
describe('DocumentTitle', () => { describe('DocumentTitle', () => {
it('renders correct title if there is a search term', () => { it('renders correct title if there is a search term', () => {
const { props, wrapper } = setup({ searchTerm: 'test search' }); const { wrapper } = setup({ searchTerm: 'test search' });
expect(wrapper.find(DocumentTitle).props()).toMatchObject({ expect(wrapper.find(DocumentTitle).props()).toMatchObject({
title: `test search${DOCUMENT_TITLE_SUFFIX}`, title: `test search${DOCUMENT_TITLE_SUFFIX}`,
}); });
}); });
it('does not render DocumentTitle if searchTerm is empty string', () => { it('does not render DocumentTitle if searchTerm is empty string', () => {
const { props, wrapper } = setup({ searchTerm: '' }); const { wrapper } = setup({ searchTerm: '' });
expect(wrapper.find(DocumentTitle).exists()).toBeFalsy(); expect(wrapper.find(DocumentTitle).exists()).toBeFalsy();
}); });
}); });
...@@ -331,13 +331,11 @@ describe('SearchPage', () => { ...@@ -331,13 +331,11 @@ describe('SearchPage', () => {
}); });
describe('renders a SearchPanel', () => { describe('renders a SearchPanel', () => {
let props;
let wrapper; let wrapper;
let searchPanel; let searchPanel;
beforeAll(() => { beforeAll(() => {
const setupResult = setup(); ({ wrapper } = setup());
props = setupResult.props;
wrapper = setupResult.wrapper;
searchPanel = wrapper.find(SearchPanel); searchPanel = wrapper.find(SearchPanel);
}); });
it('renders a search panel', () => { it('renders a search panel', () => {
......
...@@ -7,6 +7,7 @@ import { Search as UrlSearch } from 'history'; ...@@ -7,6 +7,7 @@ import { Search as UrlSearch } from 'history';
import LoadingSpinner from 'components/common/LoadingSpinner'; import LoadingSpinner from 'components/common/LoadingSpinner';
import PaginatedApiResourceList from 'components/common/ResourceList/PaginatedApiResourceList'; import PaginatedApiResourceList from 'components/common/ResourceList/PaginatedApiResourceList';
import ShimmeringResourceLoader from 'components/common/ShimmeringResourceLoader';
import { GlobalState } from 'ducks/rootReducer'; import { GlobalState } from 'ducks/rootReducer';
import { submitSearchResource, urlDidUpdate } from 'ducks/search/reducer'; import { submitSearchResource, urlDidUpdate } from 'ducks/search/reducer';
...@@ -157,8 +158,9 @@ export class SearchPage extends React.Component<SearchPageProps> { ...@@ -157,8 +158,9 @@ export class SearchPage extends React.Component<SearchPageProps> {
renderContent = () => { renderContent = () => {
if (this.props.isLoading) { if (this.props.isLoading) {
return <LoadingSpinner />; return <ShimmeringResourceLoader numItems={RESULTS_PER_PAGE} />;
} }
return this.renderSearchResults(); return this.renderSearchResults();
}; };
......
...@@ -152,7 +152,6 @@ describe('MyBookmarks', () => { ...@@ -152,7 +152,6 @@ describe('MyBookmarks', () => {
describe('generateTabInfo', () => { describe('generateTabInfo', () => {
let tabInfoArray; let tabInfoArray;
let props;
let wrapper; let wrapper;
let generateTabContentSpy; let generateTabContentSpy;
let generateTabKeySpy; let generateTabKeySpy;
...@@ -160,8 +159,8 @@ describe('MyBookmarks', () => { ...@@ -160,8 +159,8 @@ describe('MyBookmarks', () => {
beforeAll(() => { beforeAll(() => {
const setupResult = setup(); const setupResult = setup();
props = setupResult.props;
wrapper = setupResult.wrapper; wrapper = setupResult.wrapper;
generateTabContentSpy = jest generateTabContentSpy = jest
.spyOn(wrapper.instance(), 'generateTabContent') .spyOn(wrapper.instance(), 'generateTabContent')
.mockImplementation((input) => `${input}Content`); .mockImplementation((input) => `${input}Content`);
...@@ -240,17 +239,20 @@ describe('MyBookmarks', () => { ...@@ -240,17 +239,20 @@ describe('MyBookmarks', () => {
}); });
describe('render', () => { describe('render', () => {
let props;
let wrapper; let wrapper;
beforeAll(() => { beforeAll(() => {
const setupResult = setup(); const setupResult = setup();
props = setupResult.props;
wrapper = setupResult.wrapper; wrapper = setupResult.wrapper;
}); });
it('renders nothing until ready', () => { it('renders a shimmer loader until ready', () => {
const { props, wrapper } = setup({ isLoaded: false }); const { wrapper } = setup({ isLoaded: false });
expect(wrapper.html()).toBeFalsy(); const expected = 1;
const actual = wrapper.find('ShimmeringResourceLoader').length;
expect(actual).toEqual(expected);
}); });
it('renders the correct title', () => { it('renders the correct title', () => {
...@@ -258,10 +260,12 @@ describe('MyBookmarks', () => { ...@@ -258,10 +260,12 @@ describe('MyBookmarks', () => {
}); });
it('renders a TabsComponent with correct props', () => { it('renders a TabsComponent with correct props', () => {
const generateTabKeySpy = jest jest
.spyOn(wrapper.instance(), 'generateTabKey') .spyOn(wrapper.instance(), 'generateTabKey')
.mockImplementation((input) => `${input}Key`); .mockImplementation((input) => `${input}Key`);
wrapper.instance().forceUpdate(); wrapper.instance().forceUpdate();
expect(wrapper.find(TabsComponent).props()).toMatchObject({ expect(wrapper.find(TabsComponent).props()).toMatchObject({
tabs: wrapper.instance().generateTabInfo(), tabs: wrapper.instance().generateTabInfo(),
defaultTab: 'tableKey', defaultTab: 'tableKey',
......
...@@ -10,6 +10,7 @@ import { ...@@ -10,6 +10,7 @@ import {
} from 'config/config-utils'; } from 'config/config-utils';
import PaginatedResourceList from 'components/common/ResourceList/PaginatedResourceList'; import PaginatedResourceList from 'components/common/ResourceList/PaginatedResourceList';
import TabsComponent from 'components/common/TabsComponent'; import TabsComponent from 'components/common/TabsComponent';
import ShimmeringResourceLoader from 'components/common/ShimmeringResourceLoader';
import { import {
BOOKMARK_TITLE, BOOKMARK_TITLE,
BOOKMARKS_PER_PAGE, BOOKMARKS_PER_PAGE,
...@@ -27,9 +28,11 @@ export type MyBookmarksProps = StateFromProps; ...@@ -27,9 +28,11 @@ export type MyBookmarksProps = StateFromProps;
export class MyBookmarks extends React.Component<MyBookmarksProps> { export class MyBookmarks extends React.Component<MyBookmarksProps> {
generateTabContent = (resource: ResourceType) => { generateTabContent = (resource: ResourceType) => {
const bookmarks = this.props.myBookmarks[resource]; const bookmarks = this.props.myBookmarks[resource];
if (!bookmarks) { if (!bookmarks) {
return null; return null;
} }
return ( return (
<PaginatedResourceList <PaginatedResourceList
allItems={bookmarks} allItems={bookmarks}
...@@ -46,9 +49,11 @@ export class MyBookmarks extends React.Component<MyBookmarksProps> { ...@@ -46,9 +49,11 @@ export class MyBookmarks extends React.Component<MyBookmarksProps> {
generateTabTitle = (resource: ResourceType): string => { generateTabTitle = (resource: ResourceType): string => {
const bookmarks = this.props.myBookmarks[resource]; const bookmarks = this.props.myBookmarks[resource];
if (!bookmarks) { if (!bookmarks) {
return ''; return '';
} }
return `${getDisplayNameByResource(resource)} (${bookmarks.length})`; return `${getDisplayNameByResource(resource)} (${bookmarks.length})`;
}; };
...@@ -73,17 +78,21 @@ export class MyBookmarks extends React.Component<MyBookmarksProps> { ...@@ -73,17 +78,21 @@ export class MyBookmarks extends React.Component<MyBookmarksProps> {
}; };
render() { render() {
if (!this.props.isLoaded) { let content = <ShimmeringResourceLoader numItems={BOOKMARKS_PER_PAGE} />;
return null;
}
return ( if (this.props.isLoaded) {
<div className="bookmark-list"> content = (
<div className="title-1">{BOOKMARK_TITLE}</div>
<TabsComponent <TabsComponent
tabs={this.generateTabInfo()} tabs={this.generateTabInfo()}
defaultTab={this.generateTabKey(ResourceType.table)} defaultTab={this.generateTabKey(ResourceType.table)}
/> />
);
}
return (
<div className="bookmark-list">
<div className="title-1">{BOOKMARK_TITLE}</div>
{content}
</div> </div>
); );
} }
......
...@@ -17,26 +17,28 @@ import { ...@@ -17,26 +17,28 @@ import {
mapDispatchToProps, mapDispatchToProps,
} from '.'; } from '.';
describe('PopularTables', () => { const setup = (propOverrides?: Partial<PopularTablesProps>) => {
const setup = (propOverrides?: Partial<PopularTablesProps>) => { const props: PopularTablesProps = {
const props: PopularTablesProps = { isLoaded: false,
popularTables: jest.fn() as any, popularTables: jest.fn() as any,
getPopularTables: jest.fn(), getPopularTables: jest.fn(),
...propOverrides, ...propOverrides,
};
const wrapper = shallow<PopularTables>(<PopularTables {...props} />);
return { props, wrapper };
}; };
const wrapper = shallow<PopularTables>(<PopularTables {...props} />);
return { props, wrapper };
};
describe('PopularTables', () => {
let wrapper; let wrapper;
let props; let props;
describe('componentDidMount', () => { describe('componentDidMount', () => {
let getPopularTablesSpy; let getPopularTablesSpy;
beforeAll(() => { beforeAll(() => {
const setupResult = setup(); ({ wrapper, props } = setup());
wrapper = setupResult.wrapper;
props = setupResult.props;
getPopularTablesSpy = jest.spyOn(props, 'getPopularTables'); getPopularTablesSpy = jest.spyOn(props, 'getPopularTables');
}); });
...@@ -46,14 +48,11 @@ describe('PopularTables', () => { ...@@ -46,14 +48,11 @@ describe('PopularTables', () => {
}); });
describe('mapStateToProps', () => { describe('mapStateToProps', () => {
let result;
beforeAll(() => {
result = mapStateToProps(globalState);
});
it('sets popularTables on the props', () => { it('sets popularTables on the props', () => {
expect(result.popularTables).toEqual(globalState.popularTables); const actual = mapStateToProps(globalState).popularTables;
const expected = globalState.popularTables.popularTables;
expect(actual).toEqual(expected);
}); });
}); });
...@@ -72,14 +71,14 @@ describe('PopularTables', () => { ...@@ -72,14 +71,14 @@ describe('PopularTables', () => {
describe('render', () => { describe('render', () => {
beforeAll(() => { beforeAll(() => {
const setupResult = setup(); ({ wrapper, props } = setup());
wrapper = setupResult.wrapper;
props = setupResult.props;
}); });
it('renders correct label for content', () => { it('renders correct label for content', () => {
expect(wrapper.children().find('label').text()).toEqual( const actual = wrapper.children().find('label').text();
POPULAR_TABLES_LABEL const expected = POPULAR_TABLES_LABEL;
);
expect(actual).toEqual(expected);
}); });
it('renders InfoButton with correct props', () => { it('renders InfoButton with correct props', () => {
...@@ -88,13 +87,32 @@ describe('PopularTables', () => { ...@@ -88,13 +87,32 @@ describe('PopularTables', () => {
}); });
}); });
it('renders PaginatedResourceList with correct props', () => { describe('when loading', () => {
expect( it('renders loading state', () => {
wrapper.children().find(PaginatedResourceList).props() const actual = wrapper.find('ShimmeringResourceLoader').length;
).toMatchObject({ const expected = 1;
allItems: props.popularTables,
itemsPerPage: POPULAR_TABLES_PER_PAGE, expect(actual).toEqual(expected);
source: POPULAR_TABLES_SOURCE_NAME, });
});
describe('when loaded', () => {
beforeAll(() => {
({ wrapper, props } = setup({
isLoaded: true,
popularTables: globalState.popularTables.popularTables,
}));
});
it('renders PaginatedResourceList with correct props', () => {
const actual = wrapper.children().find(PaginatedResourceList).props();
const expected = {
allItems: props.popularTables,
itemsPerPage: POPULAR_TABLES_PER_PAGE,
source: POPULAR_TABLES_SOURCE_NAME,
};
expect(actual).toMatchObject(expected);
}); });
}); });
}); });
......
import * as React from 'react'; import * as React from 'react';
// TODO: Use css-modules instead of 'import'
import './styles.scss';
import { TableResource } from 'interfaces'; import { TableResource } from 'interfaces';
import InfoButton from 'components/common/InfoButton'; import InfoButton from 'components/common/InfoButton';
import PaginatedResourceList from 'components/common/ResourceList/PaginatedResourceList'; import PaginatedResourceList from 'components/common/ResourceList/PaginatedResourceList';
import ShimmeringResourceLoader from 'components/common/ShimmeringResourceLoader';
import { getPopularTables } from 'ducks/popularTables/reducer'; import { getPopularTables } from 'ducks/popularTables/reducer';
import { GetPopularTablesRequest } from 'ducks/popularTables/types'; import { GetPopularTablesRequest } from 'ducks/popularTables/types';
...@@ -20,8 +18,11 @@ import { ...@@ -20,8 +18,11 @@ import {
POPULAR_TABLES_PER_PAGE, POPULAR_TABLES_PER_PAGE,
} from './constants'; } from './constants';
import './styles.scss';
export interface StateFromProps { export interface StateFromProps {
popularTables: TableResource[]; popularTables: TableResource[];
isLoaded: boolean;
} }
export interface DispatchFromProps { export interface DispatchFromProps {
...@@ -36,24 +37,36 @@ export class PopularTables extends React.Component<PopularTablesProps> { ...@@ -36,24 +37,36 @@ export class PopularTables extends React.Component<PopularTablesProps> {
} }
render() { render() {
const { popularTables, isLoaded } = this.props;
let content = (
<ShimmeringResourceLoader numItems={POPULAR_TABLES_PER_PAGE} />
);
if (isLoaded) {
content = (
<PaginatedResourceList
allItems={popularTables}
itemsPerPage={POPULAR_TABLES_PER_PAGE}
source={POPULAR_TABLES_SOURCE_NAME}
/>
);
}
return ( return (
<> <>
<div className="popular-tables-header"> <div className="popular-tables-header">
<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>
<PaginatedResourceList {content}
allItems={this.props.popularTables}
itemsPerPage={POPULAR_TABLES_PER_PAGE}
source={POPULAR_TABLES_SOURCE_NAME}
/>
</> </>
); );
} }
} }
export const mapStateToProps = (state: GlobalState) => { export const mapStateToProps = (state: GlobalState) => {
return { return {
popularTables: state.popularTables, popularTables: state.popularTables.popularTables,
isLoaded: state.popularTables.popularTablesIsLoaded,
}; };
}; };
......
...@@ -26,6 +26,7 @@ class PaginatedApiResourceList extends React.Component< ...@@ -26,6 +26,7 @@ class PaginatedApiResourceList extends React.Component<
onPagination = (rawPageNum: number) => { onPagination = (rawPageNum: number) => {
const activePage = rawPageNum - 1; const activePage = rawPageNum - 1;
this.props.onPagination(activePage); this.props.onPagination(activePage);
}; };
......
import * as React from 'react';
import { mount } from 'enzyme';
import ShimmeringResourceLoader, {
ShimmeringResourceItem,
ShimmeringResourceLoaderProps,
} from '.';
const setup = (propOverrides?: Partial<ShimmeringResourceLoaderProps>) => {
const props: ShimmeringResourceLoaderProps = {
...propOverrides,
};
const wrapper = mount<ShimmeringResourceLoaderProps>(
<ShimmeringResourceLoader {...props} />
);
return { props, wrapper };
};
describe('ShimmeringResourceLoader', () => {
let wrapper;
describe('render', () => {
beforeAll(() => {
({ wrapper } = setup());
});
it('renders a container', () => {
const actual = wrapper.find('.shimmer-resource-loader').length;
const expected = 1;
expect(actual).toEqual(expected);
});
it('renders three items by default', () => {
const actual = wrapper.find(ShimmeringResourceItem).length;
const expected = 3;
expect(actual).toEqual(expected);
});
describe('when passing a numItems value', () => {
it('renders as many items as requested', () => {
const expected = 5;
({ wrapper } = setup({ numItems: expected }));
const actual = wrapper.find(ShimmeringResourceItem).length;
expect(actual).toEqual(expected);
});
});
});
});
import * as React from 'react';
import * as times from 'lodash/times';
import './styles.scss';
const DEFAULT_REPETITION = 3;
export const ShimmeringResourceItem: React.SFC = () => {
return (
<div className="shimmer-resource-loader-item media">
<div className="media-left media-middle">
<div className="shimmer-resource-circle is-shimmer-animated" />
</div>
<div className="media-body">
<div className="shimmer-resource-line shimmer-resource-line--1 is-shimmer-animated" />
<div className="shimmer-resource-line shimmer-resource-line--2 is-shimmer-animated" />
</div>
</div>
);
};
export interface ShimmeringResourceLoaderProps {
numItems?: number;
}
const ShimmeringResourceLoader: React.SFC<ShimmeringResourceLoaderProps> = ({
numItems = DEFAULT_REPETITION,
}: ShimmeringResourceLoaderProps) => {
return (
<div className="shimmer-resource-loader">
{times(numItems, (idx) => (
<ShimmeringResourceItem key={idx} />
))}
</div>
);
};
export default ShimmeringResourceLoader;
@import 'variables';
$shimmer-loader-lines: 1, 2;
$shimmer-loader-text-height: 16px;
$shimmer-loader-circle-size: 24px;
$shimmer-loader-border-size: 1px;
.shimmer-resource-loader-item {
padding: $spacer-3;
border-top: $shimmer-loader-border-size solid $stroke;
border-bottom: $shimmer-loader-border-size solid $stroke;
margin-bottom: -$shimmer-loader-border-size;
margin-top: 0;
& .media-left {
padding-right: $spacer-2;
}
}
.shimmer-resource-circle {
height: $shimmer-loader-circle-size;
width: $shimmer-loader-circle-size;
border-radius: $shimmer-loader-circle-size;
}
.shimmer-resource-line {
margin-bottom: $spacer-2;
height: $shimmer-loader-text-height;
&:last-child {
margin-bottom: 0;
}
}
@each $line in $shimmer-loader-lines {
.shimmer-resource-line--#{$line} {
width: percentage(random(100) / 100);
}
}
...@@ -9,11 +9,11 @@ import * as API from '../v0'; ...@@ -9,11 +9,11 @@ import * as API from '../v0';
jest.mock('axios'); jest.mock('axios');
describe('getPopularTables', () => { describe('getPopularTables', () => {
let axiosMock;
let expectedTables: TableResource[]; let expectedTables: TableResource[];
let mockGetResponse: AxiosResponse<API.PopularTablesAPI>; let mockGetResponse: AxiosResponse<API.PopularTablesAPI>;
beforeAll(() => { beforeAll(() => {
expectedTables = globalState.popularTables; expectedTables = globalState.popularTables.popularTables;
mockGetResponse = { mockGetResponse = {
data: { data: {
results: expectedTables, results: expectedTables,
...@@ -24,7 +24,7 @@ describe('getPopularTables', () => { ...@@ -24,7 +24,7 @@ describe('getPopularTables', () => {
headers: {}, headers: {},
config: {}, config: {},
}; };
axiosMock = jest jest
.spyOn(axios, 'get') .spyOn(axios, 'get')
.mockImplementation(() => Promise.resolve(mockGetResponse)); .mockImplementation(() => Promise.resolve(mockGetResponse));
}); });
......
...@@ -4,25 +4,23 @@ import { TableResource } from 'interfaces'; ...@@ -4,25 +4,23 @@ import { TableResource } from 'interfaces';
import globalState from 'fixtures/globalState'; import globalState from 'fixtures/globalState';
import * as API from '../api/v0'; import * as API from './api/v0';
import reducer, { import reducer, {
getPopularTables, getPopularTables,
getPopularTablesFailure, getPopularTablesFailure,
getPopularTablesSuccess, getPopularTablesSuccess,
PopularTablesReducerState, PopularTablesReducerState,
} from '../reducer'; } from './reducer';
import { getPopularTablesWorker, getPopularTablesWatcher } from '../sagas'; import { getPopularTablesWorker, getPopularTablesWatcher } from './sagas';
import { import { GetPopularTables } from './types';
GetPopularTables,
GetPopularTablesRequest,
GetPopularTablesResponse,
} from '../types';
describe('popularTables ducks', () => { describe('popularTables ducks', () => {
let expectedTables: TableResource[]; let expectedTables: TableResource[];
beforeAll(() => { beforeAll(() => {
expectedTables = globalState.popularTables; expectedTables = globalState.popularTables.popularTables;
}); });
describe('actions', () => { describe('actions', () => {
it('getPopularTables - returns the action to get popular tables', () => { it('getPopularTables - returns the action to get popular tables', () => {
const action = getPopularTables(); const action = getPopularTables();
...@@ -46,21 +44,34 @@ describe('popularTables ducks', () => { ...@@ -46,21 +44,34 @@ describe('popularTables ducks', () => {
describe('reducer', () => { describe('reducer', () => {
let testState: PopularTablesReducerState; let testState: PopularTablesReducerState;
beforeAll(() => { beforeAll(() => {
testState = []; testState = {
popularTablesIsLoaded: false,
popularTables: [],
};
}); });
it('should return the existing state if action is not handled', () => { it('should return the existing state if action is not handled', () => {
expect(reducer(testState, { type: 'INVALID.ACTION' })).toEqual(testState); expect(reducer(testState, { type: 'INVALID.ACTION' })).toEqual(testState);
}); });
it('should handle GetPopularTables.SUCCESS', () => { it('should handle GetPopularTables.SUCCESS', () => {
expect( const expected = expectedTables;
reducer(testState, getPopularTablesSuccess(expectedTables)) const actual = reducer(testState, getPopularTablesSuccess(expectedTables))
).toEqual(expectedTables); .popularTables;
expect(actual).toEqual(expected);
}); });
it('should handle GetPopularTables.FAILURE', () => { it('should handle GetPopularTables.FAILURE', () => {
expect(reducer(testState, getPopularTablesFailure())).toEqual([]); const expected = {
popularTables: [],
popularTablesIsLoaded: true,
};
const actual = reducer(testState, getPopularTablesFailure());
expect(actual).toEqual(expected);
}); });
}); });
......
...@@ -20,18 +20,33 @@ export function getPopularTablesSuccess( ...@@ -20,18 +20,33 @@ export function getPopularTablesSuccess(
} }
/* REDUCER */ /* REDUCER */
export type PopularTablesReducerState = TableResource[]; export interface PopularTablesReducerState {
popularTables: TableResource[];
popularTablesIsLoaded: boolean;
}
const initialState: PopularTablesReducerState = []; const initialState: PopularTablesReducerState = {
popularTables: [],
popularTablesIsLoaded: false,
};
export default function reducer( export default function reducer(
state: PopularTablesReducerState = initialState, state: PopularTablesReducerState = initialState,
action action
): PopularTablesReducerState { ): PopularTablesReducerState {
switch (action.type) { switch (action.type) {
case GetPopularTables.REQUEST:
return {
...state,
...initialState,
};
case GetPopularTables.SUCCESS: case GetPopularTables.SUCCESS:
case GetPopularTables.FAILURE: case GetPopularTables.FAILURE:
return (<GetPopularTablesResponse>action).payload.tables; return {
...state,
popularTables: (<GetPopularTablesResponse>action).payload.tables,
popularTablesIsLoaded: true,
};
default: default:
return state; return state;
} }
......
...@@ -73,26 +73,29 @@ const globalState: GlobalState = { ...@@ -73,26 +73,29 @@ const globalState: GlobalState = {
requestIsOpen: false, requestIsOpen: false,
sendState: SendingState.IDLE, sendState: SendingState.IDLE,
}, },
popularTables: [ popularTables: {
{ popularTablesIsLoaded: true,
cluster: 'testCluster', popularTables: [
database: 'testDatabase', {
description: 'I have a lot of users', cluster: 'testCluster',
key: 'testDatabase://testCluster.testSchema/testName', database: 'testDatabase',
name: 'testName', description: 'I have a lot of users',
schema: 'testSchema', key: 'testDatabase://testCluster.testSchema/testName',
type: ResourceType.table, name: 'testName',
}, schema: 'testSchema',
{ type: ResourceType.table,
cluster: 'testCluster', },
database: 'testDatabase', {
description: 'I also have a lot of users', cluster: 'testCluster',
key: 'testDatabase://testCluster.testSchema/otherName', database: 'testDatabase',
name: 'otherName', description: 'I also have a lot of users',
schema: 'testSchema', key: 'testDatabase://testCluster.testSchema/otherName',
type: ResourceType.table, name: 'otherName',
}, schema: 'testSchema',
], type: ResourceType.table,
},
],
},
search: { search: {
search_term: 'testName', search_term: 'testName',
resource: ResourceType.table, resource: ResourceType.table,
......
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