diff --git a/src/visits/ShortUrlVisits.js b/src/visits/ShortUrlVisits.js index f7979d19..cd14bc22 100644 --- a/src/visits/ShortUrlVisits.js +++ b/src/visits/ShortUrlVisits.js @@ -53,10 +53,12 @@ const ShortUrlVisits = ({ processStatsFromVisits }) => class ShortUrlVisits exte const { shortUrlVisits, shortUrlDetail } = this.props; const renderVisitsContent = () => { - const { visits, loading, error } = shortUrlVisits; + const { visits, loading, loadingLarge, error } = shortUrlVisits; if (loading) { - return Loading...; + const message = loadingLarge ? 'This is going to take a while... :S' : 'Loading...'; + + return {message}; } if (error) { diff --git a/src/visits/reducers/shortUrlVisits.js b/src/visits/reducers/shortUrlVisits.js index 32a86f47..3d97d4df 100644 --- a/src/visits/reducers/shortUrlVisits.js +++ b/src/visits/reducers/shortUrlVisits.js @@ -1,9 +1,11 @@ import PropTypes from 'prop-types'; +import { flatten, range, splitEvery } from 'ramda'; /* eslint-disable padding-line-between-statements, newline-after-var */ export const GET_SHORT_URL_VISITS_START = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_START'; export const GET_SHORT_URL_VISITS_ERROR = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_ERROR'; export const GET_SHORT_URL_VISITS = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS'; +export const GET_SHORT_URL_VISITS_LARGE = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_LARGE'; /* eslint-enable padding-line-between-statements, newline-after-var */ export const shortUrlVisitsType = PropTypes.shape({ @@ -15,6 +17,7 @@ export const shortUrlVisitsType = PropTypes.shape({ const initialState = { visits: [], loading: false, + loadingLarge: false, error: false, }; @@ -24,19 +27,27 @@ export default function reducer(state = initialState, action) { return { ...state, loading: true, + loadingLarge: false, }; case GET_SHORT_URL_VISITS_ERROR: return { ...state, loading: false, + loadingLarge: false, error: true, }; case GET_SHORT_URL_VISITS: return { visits: action.visits, loading: false, + loadingLarge: false, error: false, }; + case GET_SHORT_URL_VISITS_LARGE: + return { + ...state, + loadingLarge: true, + }; default: return state; } @@ -58,9 +69,36 @@ export const getShortUrlVisits = (buildShlinkApiClient) => (shortCode, dates) => return data; } - return data.concat(await loadVisits(page + 1)); + // If there are more pages, make requests in blocks of 4 + const parallelRequestsCount = 4; + const parallelStartingPage = 2; + const pagesRange = range(parallelStartingPage, pagination.pagesCount + 1); + const pagesBlocks = splitEvery(parallelRequestsCount, pagesRange); + + if (pagination.pagesCount - 1 > parallelRequestsCount) { + dispatch({ type: GET_SHORT_URL_VISITS_LARGE }); + } + + return data.concat(await loadPagesBlocks(pagesBlocks)); }; + const loadPagesBlocks = async (pagesBlocks, index = 0) => { + const data = await loadVisitsInParallel(pagesBlocks[index]); + + if (index < pagesBlocks.length - 1) { + return data.concat(await loadPagesBlocks(pagesBlocks, index + 1)); + } + + return data; + }; + + const loadVisitsInParallel = (pages) => + Promise.all(pages.map(async (page) => { + const { data } = await getShortUrlVisits(shortCode, { ...dates, page, itemsPerPage }); + + return data; + })).then(flatten); + try { const visits = await loadVisits(); diff --git a/test/visits/ShortUrlVisits.test.js b/test/visits/ShortUrlVisits.test.js index a24e8645..8f792852 100644 --- a/test/visits/ShortUrlVisits.test.js +++ b/test/visits/ShortUrlVisits.test.js @@ -43,7 +43,7 @@ describe('', () => { } }); - it('Renders a preloader when visits are loading', () => { + it('renders a preloader when visits are loading', () => { const wrapper = createComponent({ loading: true }); const loadingMessage = wrapper.find(MutedMessage); @@ -51,6 +51,14 @@ describe('', () => { expect(loadingMessage.html()).toContain('Loading...'); }); + it('renders a warning when loading large amounts of visits', () => { + const wrapper = createComponent({ loading: true, loadingLarge: true }); + const loadingMessage = wrapper.find(MutedMessage); + + expect(loadingMessage).toHaveLength(1); + expect(loadingMessage.html()).toContain('This is going to take a while... :S'); + }); + it('renders an error message when visits could not be loaded', () => { const wrapper = createComponent({ loading: false, error: true }); const errorMessage = wrapper.find(Card); diff --git a/test/visits/reducers/shortUrlVisits.test.js b/test/visits/reducers/shortUrlVisits.test.js index 676490e7..93e8fbe4 100644 --- a/test/visits/reducers/shortUrlVisits.test.js +++ b/test/visits/reducers/shortUrlVisits.test.js @@ -4,6 +4,7 @@ import reducer, { GET_SHORT_URL_VISITS_START, GET_SHORT_URL_VISITS_ERROR, GET_SHORT_URL_VISITS, + GET_SHORT_URL_VISITS_LARGE, } from '../../../src/visits/reducers/shortUrlVisits'; describe('shortUrlVisitsReducer', () => { @@ -15,6 +16,13 @@ describe('shortUrlVisitsReducer', () => { expect(loading).toEqual(true); }); + it('returns loadingLarge on GET_SHORT_URL_VISITS_LARGE', () => { + const state = reducer({ loadingLarge: false }, { type: GET_SHORT_URL_VISITS_LARGE }); + const { loadingLarge } = state; + + expect(loadingLarge).toEqual(true); + }); + it('stops loading and returns error on GET_SHORT_URL_VISITS_ERROR', () => { const state = reducer({ loading: true, error: false }, { type: GET_SHORT_URL_VISITS_ERROR }); const { loading, error } = state;