mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-10 18:27:25 +03:00
Created page for tag visit stats
This commit is contained in:
parent
18e18f533b
commit
bfbb21e1cc
17 changed files with 291 additions and 79 deletions
|
@ -17,7 +17,16 @@ const propTypes = {
|
|||
selectedServer: serverType,
|
||||
};
|
||||
|
||||
const MenuLayout = (TagsList, ShortUrls, AsideMenu, CreateShortUrl, ShortUrlVisits, ShlinkVersions, ServerError) => {
|
||||
const MenuLayout = (
|
||||
TagsList,
|
||||
ShortUrls,
|
||||
AsideMenu,
|
||||
CreateShortUrl,
|
||||
ShortUrlVisits,
|
||||
TagVisits,
|
||||
ShlinkVersions,
|
||||
ServerError
|
||||
) => {
|
||||
const MenuLayoutComp = ({ match, location, selectedServer }) => {
|
||||
const [ sidebarVisible, toggleSidebar, showSidebar, hideSidebar ] = useToggle();
|
||||
const { params: { serverId } } = match;
|
||||
|
@ -61,7 +70,7 @@ const MenuLayout = (TagsList, ShortUrls, AsideMenu, CreateShortUrl, ShortUrlVisi
|
|||
<Route exact path="/server/:serverId/list-short-urls/:page" component={ShortUrls} />
|
||||
<Route exact path="/server/:serverId/create-short-url" component={CreateShortUrl} />
|
||||
<Route exact path="/server/:serverId/short-code/:shortCode/visits" component={ShortUrlVisits} />
|
||||
{/* <Route exact path="/server/:serverId/tag/:tag/visits" component={TagVisits} /> */}
|
||||
<Route exact path="/server/:serverId/tag/:tag/visits" component={TagVisits} />
|
||||
<Route exact path="/server/:serverId/manage-tags" component={TagsList} />
|
||||
<Route
|
||||
render={() => <NotFound to={`/server/${serverId}/list-short-urls/1`}>List short URLs</NotFound>}
|
||||
|
|
|
@ -27,6 +27,7 @@ const provideServices = (bottle, connect, withRouter) => {
|
|||
'AsideMenu',
|
||||
'CreateShortUrl',
|
||||
'ShortUrlVisits',
|
||||
'TagVisits',
|
||||
'ShlinkVersions',
|
||||
'ServerError'
|
||||
);
|
||||
|
|
|
@ -9,6 +9,7 @@ import shortUrlTagsReducer from '../short-urls/reducers/shortUrlTags';
|
|||
import shortUrlMetaReducer from '../short-urls/reducers/shortUrlMeta';
|
||||
import shortUrlEditionReducer from '../short-urls/reducers/shortUrlEdition';
|
||||
import shortUrlVisitsReducer from '../visits/reducers/shortUrlVisits';
|
||||
import tagVisitsReducer from '../visits/reducers/tagVisits';
|
||||
import shortUrlDetailReducer from '../visits/reducers/shortUrlDetail';
|
||||
import tagsListReducer from '../tags/reducers/tagsList';
|
||||
import tagDeleteReducer from '../tags/reducers/tagDelete';
|
||||
|
@ -27,6 +28,7 @@ export default combineReducers({
|
|||
shortUrlMeta: shortUrlMetaReducer,
|
||||
shortUrlEdition: shortUrlEditionReducer,
|
||||
shortUrlVisits: shortUrlVisitsReducer,
|
||||
tagVisits: tagVisitsReducer,
|
||||
shortUrlDetail: shortUrlDetailReducer,
|
||||
tagsList: tagsListReducer,
|
||||
tagDelete: tagDeleteReducer,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { handleActions } from 'redux-actions';
|
||||
import { assoc, assocPath, reject } from 'ramda';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CREATE_SHORT_URL_VISIT } from '../../visits/reducers/shortUrlVisits';
|
||||
import { shortUrlMatches } from '../helpers';
|
||||
import { CREATE_VISIT } from '../../visits/reducers/visitCreation';
|
||||
import { SHORT_URL_TAGS_EDITED } from './shortUrlTags';
|
||||
import { SHORT_URL_DELETED } from './shortUrlDeletion';
|
||||
import { SHORT_URL_META_EDITED, shortUrlMetaType } from './shortUrlMeta';
|
||||
|
@ -50,7 +50,7 @@ export default handleActions({
|
|||
[SHORT_URL_TAGS_EDITED]: setPropFromActionOnMatchingShortUrl('tags'),
|
||||
[SHORT_URL_META_EDITED]: setPropFromActionOnMatchingShortUrl('meta'),
|
||||
[SHORT_URL_EDITED]: setPropFromActionOnMatchingShortUrl('longUrl'),
|
||||
[CREATE_SHORT_URL_VISIT]: (state, { shortUrl: { shortCode, domain, visitsCount } }) => assocPath(
|
||||
[CREATE_VISIT]: (state, { shortUrl: { shortCode, domain, visitsCount } }) => assocPath(
|
||||
[ 'shortUrls', 'data' ],
|
||||
state.shortUrls && state.shortUrls.data && state.shortUrls.data.map(
|
||||
(shortUrl) => shortUrlMatches(shortUrl, shortCode, domain)
|
||||
|
|
|
@ -36,6 +36,10 @@ export default class ShlinkApiClient {
|
|||
this._performRequest(`/short-urls/${shortCode}/visits`, 'GET', query)
|
||||
.then((resp) => resp.data.visits);
|
||||
|
||||
getTagVisits = (tag, query) =>
|
||||
this._performRequest(`/tags/${tag}/visits`, 'GET', query)
|
||||
.then((resp) => resp.data.visits);
|
||||
|
||||
getShortUrl = (shortCode, domain) =>
|
||||
this._performRequest(`/short-urls/${shortCode}`, 'GET', { domain })
|
||||
.then((resp) => resp.data);
|
||||
|
|
65
src/visits/TagVisits.js
Normal file
65
src/visits/TagVisits.js
Normal file
|
@ -0,0 +1,65 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MercureInfoType } from '../mercure/reducers/mercureInfo';
|
||||
import { SettingsType } from '../settings/reducers/settings';
|
||||
import { bindToMercureTopic } from '../mercure/helpers';
|
||||
import { TagVisitsType } from './reducers/tagVisits';
|
||||
|
||||
const propTypes = {
|
||||
history: PropTypes.shape({
|
||||
goBack: PropTypes.func,
|
||||
}),
|
||||
match: PropTypes.shape({
|
||||
params: PropTypes.object,
|
||||
}),
|
||||
getTagVisits: PropTypes.func,
|
||||
tagVisits: TagVisitsType,
|
||||
cancelGetTagVisits: PropTypes.func,
|
||||
createNewVisit: PropTypes.func,
|
||||
loadMercureInfo: PropTypes.func,
|
||||
mercureInfo: MercureInfoType,
|
||||
settings: SettingsType,
|
||||
};
|
||||
|
||||
const TagVisits = (VisitsStats) => {
|
||||
const TagVisitsComp = ({
|
||||
history,
|
||||
match,
|
||||
getTagVisits,
|
||||
tagVisits,
|
||||
cancelGetTagVisits,
|
||||
createNewVisit,
|
||||
loadMercureInfo,
|
||||
mercureInfo,
|
||||
settings: { realTimeUpdates },
|
||||
}) => {
|
||||
const { params } = match;
|
||||
const { tag } = params;
|
||||
const loadVisits = (dates) => getTagVisits(tag, dates);
|
||||
|
||||
console.log(history);
|
||||
|
||||
useEffect(
|
||||
bindToMercureTopic(
|
||||
mercureInfo,
|
||||
realTimeUpdates,
|
||||
'https://shlink.io/new-visit',
|
||||
createNewVisit,
|
||||
loadMercureInfo
|
||||
),
|
||||
[ mercureInfo ],
|
||||
);
|
||||
|
||||
return (
|
||||
<VisitsStats getVisits={loadVisits} cancelGetVisits={cancelGetTagVisits} visitsInfo={tagVisits}>
|
||||
<span>{tag} - {tagVisits.visits.length}</span>
|
||||
</VisitsStats>
|
||||
);
|
||||
};
|
||||
|
||||
TagVisitsComp.propTypes = propTypes;
|
||||
|
||||
return TagVisitsComp;
|
||||
};
|
||||
|
||||
export default TagVisits;
|
|
@ -17,7 +17,7 @@ import { VisitsInfoType } from './types';
|
|||
const propTypes = {
|
||||
children: PropTypes.node,
|
||||
getVisits: PropTypes.func,
|
||||
visitsInfo: VisitsInfoType, // TODO VisitsInfo type
|
||||
visitsInfo: VisitsInfoType,
|
||||
cancelGetVisits: PropTypes.func,
|
||||
matchMedia: PropTypes.func,
|
||||
};
|
||||
|
|
56
src/visits/reducers/common.js
Normal file
56
src/visits/reducers/common.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { flatten, prop, range, splitEvery } from 'ramda';
|
||||
|
||||
const ITEMS_PER_PAGE = 5000;
|
||||
const isLastPage = ({ currentPage, pagesCount }) => currentPage >= pagesCount;
|
||||
|
||||
export const getVisitsWithLoader = async (visitsLoader, extraFinishActionData, actionMap, dispatch, getState) => {
|
||||
dispatch({ type: actionMap.start });
|
||||
|
||||
const loadVisits = async (page = 1) => {
|
||||
const { pagination, data } = await visitsLoader(page, ITEMS_PER_PAGE);
|
||||
|
||||
// If pagination was not returned, then this is an old shlink version. Just return data
|
||||
if (!pagination || isLastPage(pagination)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// 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: actionMap.large });
|
||||
}
|
||||
|
||||
return data.concat(await loadPagesBlocks(pagesBlocks));
|
||||
};
|
||||
|
||||
const loadPagesBlocks = async (pagesBlocks, index = 0) => {
|
||||
const { shortUrlVisits: { cancelLoad } } = getState();
|
||||
|
||||
if (cancelLoad) {
|
||||
return [];
|
||||
}
|
||||
|
||||
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((page) => visitsLoader(page, ITEMS_PER_PAGE).then(prop('data')))).then(flatten);
|
||||
|
||||
try {
|
||||
const visits = await loadVisits();
|
||||
|
||||
dispatch({ ...extraFinishActionData, visits, type: actionMap.finish });
|
||||
} catch (e) {
|
||||
dispatch({ type: actionMap.error });
|
||||
}
|
||||
};
|
|
@ -1,8 +1,9 @@
|
|||
import { createAction, handleActions } from 'redux-actions';
|
||||
import PropTypes from 'prop-types';
|
||||
import { flatten, prop, range, splitEvery } from 'ramda';
|
||||
import { shortUrlMatches } from '../../short-urls/helpers';
|
||||
import { VisitType } from '../types';
|
||||
import { getVisitsWithLoader } from './common';
|
||||
import { CREATE_VISIT } from './visitCreation';
|
||||
|
||||
/* eslint-disable padding-line-between-statements */
|
||||
export const GET_SHORT_URL_VISITS_START = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_START';
|
||||
|
@ -10,7 +11,6 @@ export const GET_SHORT_URL_VISITS_ERROR = 'shlink/shortUrlVisits/GET_SHORT_URL_V
|
|||
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';
|
||||
export const GET_SHORT_URL_VISITS_CANCEL = 'shlink/shortUrlVisits/GET_SHORT_URL_VISITS_CANCEL';
|
||||
export const CREATE_SHORT_URL_VISIT = 'shlink/shortUrlVisits/CREATE_SHORT_URL_VISIT';
|
||||
/* eslint-enable padding-line-between-statements */
|
||||
|
||||
export const shortUrlVisitsType = PropTypes.shape({ // TODO Should extend from VisitInfoType
|
||||
|
@ -18,6 +18,7 @@ export const shortUrlVisitsType = PropTypes.shape({ // TODO Should extend from V
|
|||
shortCode: PropTypes.string,
|
||||
domain: PropTypes.string,
|
||||
loading: PropTypes.bool,
|
||||
loadingLarge: PropTypes.bool,
|
||||
error: PropTypes.bool,
|
||||
});
|
||||
|
||||
|
@ -56,7 +57,7 @@ export default handleActions({
|
|||
}),
|
||||
[GET_SHORT_URL_VISITS_LARGE]: (state) => ({ ...state, loadingLarge: true }),
|
||||
[GET_SHORT_URL_VISITS_CANCEL]: (state) => ({ ...state, cancelLoad: true }),
|
||||
[CREATE_SHORT_URL_VISIT]: (state, { shortUrl, visit }) => { // eslint-disable-line object-shorthand
|
||||
[CREATE_VISIT]: (state, { shortUrl, visit }) => { // eslint-disable-line object-shorthand
|
||||
const { shortCode, domain, visits } = state;
|
||||
|
||||
if (!shortUrlMatches(shortUrl, shortCode, domain)) {
|
||||
|
@ -67,65 +68,18 @@ export default handleActions({
|
|||
},
|
||||
}, initialState);
|
||||
|
||||
export const getShortUrlVisits = (buildShlinkApiClient) => (shortCode, query = {}) => async (dispatch, getState) => {
|
||||
dispatch({ type: GET_SHORT_URL_VISITS_START });
|
||||
export const getShortUrlVisits = (buildShlinkApiClient) => (shortCode, query = {}) => (dispatch, getState) => {
|
||||
const { getShortUrlVisits } = buildShlinkApiClient(getState);
|
||||
const itemsPerPage = 5000;
|
||||
const isLastPage = ({ currentPage, pagesCount }) => currentPage >= pagesCount;
|
||||
|
||||
const loadVisits = async (page = 1) => {
|
||||
const { pagination, data } = await getShortUrlVisits(shortCode, { ...query, page, itemsPerPage });
|
||||
|
||||
// If pagination was not returned, then this is an older shlink version. Just return data
|
||||
if (!pagination || isLastPage(pagination)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// 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 visitsLoader = (page, itemsPerPage) => getShortUrlVisits(shortCode, { ...query, page, itemsPerPage });
|
||||
const extraFinishActionData = { shortCode, domain: query.domain };
|
||||
const actionMap = {
|
||||
start: GET_SHORT_URL_VISITS_START,
|
||||
large: GET_SHORT_URL_VISITS_LARGE,
|
||||
finish: GET_SHORT_URL_VISITS,
|
||||
error: GET_SHORT_URL_VISITS_ERROR,
|
||||
};
|
||||
|
||||
const loadPagesBlocks = async (pagesBlocks, index = 0) => {
|
||||
const { shortUrlVisits: { cancelLoad } } = getState();
|
||||
|
||||
if (cancelLoad) {
|
||||
return [];
|
||||
}
|
||||
|
||||
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(
|
||||
(page) =>
|
||||
getShortUrlVisits(shortCode, { ...query, page, itemsPerPage })
|
||||
.then(prop('data'))
|
||||
)).then(flatten);
|
||||
|
||||
try {
|
||||
const visits = await loadVisits();
|
||||
|
||||
dispatch({ visits, shortCode, domain: query.domain, type: GET_SHORT_URL_VISITS });
|
||||
} catch (e) {
|
||||
dispatch({ type: GET_SHORT_URL_VISITS_ERROR });
|
||||
}
|
||||
return getVisitsWithLoader(visitsLoader, extraFinishActionData, actionMap, dispatch, getState);
|
||||
};
|
||||
|
||||
export const cancelGetShortUrlVisits = createAction(GET_SHORT_URL_VISITS_CANCEL);
|
||||
|
||||
export const createNewVisit = ({ shortUrl, visit }) => ({ shortUrl, visit, type: CREATE_SHORT_URL_VISIT });
|
||||
|
|
81
src/visits/reducers/tagVisits.js
Normal file
81
src/visits/reducers/tagVisits.js
Normal file
|
@ -0,0 +1,81 @@
|
|||
import { createAction, handleActions } from 'redux-actions';
|
||||
import PropTypes from 'prop-types';
|
||||
import { VisitType } from '../types';
|
||||
import { getVisitsWithLoader } from './common';
|
||||
import { CREATE_VISIT } from './visitCreation';
|
||||
|
||||
/* eslint-disable padding-line-between-statements */
|
||||
export const GET_TAG_VISITS_START = 'shlink/tagVisits/GET_TAG_VISITS_START';
|
||||
export const GET_TAG_VISITS_ERROR = 'shlink/tagVisits/GET_TAG_VISITS_ERROR';
|
||||
export const GET_TAG_VISITS = 'shlink/tagVisits/GET_TAG_VISITS';
|
||||
export const GET_TAG_VISITS_LARGE = 'shlink/tagVisits/GET_TAG_VISITS_LARGE';
|
||||
export const GET_TAG_VISITS_CANCEL = 'shlink/tagVisits/GET_TAG_VISITS_CANCEL';
|
||||
/* eslint-enable padding-line-between-statements */
|
||||
|
||||
export const TagVisitsType = PropTypes.shape({ // TODO Should extend from VisitInfoType
|
||||
visits: PropTypes.arrayOf(VisitType),
|
||||
tag: PropTypes.string,
|
||||
loading: PropTypes.bool,
|
||||
loadingLarge: PropTypes.bool,
|
||||
error: PropTypes.bool,
|
||||
});
|
||||
|
||||
const initialState = {
|
||||
visits: [],
|
||||
tag: '',
|
||||
loading: false,
|
||||
loadingLarge: false,
|
||||
error: false,
|
||||
cancelLoad: false,
|
||||
};
|
||||
|
||||
export default handleActions({
|
||||
[GET_TAG_VISITS_START]: (state) => ({
|
||||
...state,
|
||||
loading: true,
|
||||
loadingLarge: false,
|
||||
cancelLoad: false,
|
||||
}),
|
||||
[GET_TAG_VISITS_ERROR]: (state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
loadingLarge: false,
|
||||
error: true,
|
||||
cancelLoad: false,
|
||||
}),
|
||||
[GET_TAG_VISITS]: (state, { visits, tag }) => ({
|
||||
visits,
|
||||
tag,
|
||||
loading: false,
|
||||
loadingLarge: false,
|
||||
error: false,
|
||||
cancelLoad: false,
|
||||
}),
|
||||
[GET_TAG_VISITS_LARGE]: (state) => ({ ...state, loadingLarge: true }),
|
||||
[GET_TAG_VISITS_CANCEL]: (state) => ({ ...state, cancelLoad: true }),
|
||||
[CREATE_VISIT]: (state, { shortUrl, visit }) => { // eslint-disable-line object-shorthand
|
||||
const { tag, visits } = state;
|
||||
|
||||
if (!shortUrl.tags.includes(tag)) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return { ...state, visits: [ ...visits, visit ] };
|
||||
},
|
||||
}, initialState);
|
||||
|
||||
export const getTagVisits = (buildShlinkApiClient) => (tag, query = {}) => (dispatch, getState) => {
|
||||
const { getTagVisits } = buildShlinkApiClient(getState);
|
||||
const visitsLoader = (page, itemsPerPage) => getTagVisits(tag, { ...query, page, itemsPerPage });
|
||||
const extraFinishActionData = { tag };
|
||||
const actionMap = {
|
||||
start: GET_TAG_VISITS_START,
|
||||
large: GET_TAG_VISITS_LARGE,
|
||||
finish: GET_TAG_VISITS,
|
||||
error: GET_TAG_VISITS_ERROR,
|
||||
};
|
||||
|
||||
return getVisitsWithLoader(visitsLoader, extraFinishActionData, actionMap, dispatch, getState);
|
||||
};
|
||||
|
||||
export const cancelGetTagVisits = createAction(GET_TAG_VISITS_CANCEL);
|
3
src/visits/reducers/visitCreation.js
Normal file
3
src/visits/reducers/visitCreation.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export const CREATE_VISIT = 'shlink/visitCreation/CREATE_VISIT';
|
||||
|
||||
export const createNewVisit = ({ shortUrl, visit }) => ({ shortUrl, visit, type: CREATE_VISIT });
|
|
@ -1,9 +1,12 @@
|
|||
import ShortUrlVisits from '../ShortUrlVisits';
|
||||
import { cancelGetShortUrlVisits, createNewVisit, getShortUrlVisits } from '../reducers/shortUrlVisits';
|
||||
import { cancelGetShortUrlVisits, getShortUrlVisits } from '../reducers/shortUrlVisits';
|
||||
import { getShortUrlDetail } from '../reducers/shortUrlDetail';
|
||||
import OpenMapModalBtn from '../helpers/OpenMapModalBtn';
|
||||
import MapModal from '../helpers/MapModal';
|
||||
import VisitsStats from '../VisitsStats';
|
||||
import { createNewVisit } from '../reducers/visitCreation';
|
||||
import { cancelGetTagVisits, getTagVisits } from '../reducers/tagVisits';
|
||||
import TagVisits from '../TagVisits';
|
||||
import * as visitsParser from './VisitsParser';
|
||||
|
||||
const provideServices = (bottle, connect) => {
|
||||
|
@ -16,6 +19,11 @@ const provideServices = (bottle, connect) => {
|
|||
[ 'shortUrlVisits', 'shortUrlDetail', 'mercureInfo', 'settings' ],
|
||||
[ 'getShortUrlVisits', 'getShortUrlDetail', 'cancelGetShortUrlVisits', 'createNewVisit', 'loadMercureInfo' ]
|
||||
));
|
||||
bottle.serviceFactory('TagVisits', TagVisits, 'VisitsStats');
|
||||
bottle.decorator('TagVisits', connect(
|
||||
[ 'tagVisits', 'mercureInfo', 'settings' ],
|
||||
[ 'getTagVisits', 'cancelGetTagVisits', 'createNewVisit', 'loadMercureInfo' ]
|
||||
));
|
||||
|
||||
// Services
|
||||
bottle.serviceFactory('VisitsParser', () => visitsParser);
|
||||
|
@ -24,6 +32,10 @@ const provideServices = (bottle, connect) => {
|
|||
bottle.serviceFactory('getShortUrlVisits', getShortUrlVisits, 'buildShlinkApiClient');
|
||||
bottle.serviceFactory('getShortUrlDetail', getShortUrlDetail, 'buildShlinkApiClient');
|
||||
bottle.serviceFactory('cancelGetShortUrlVisits', () => cancelGetShortUrlVisits);
|
||||
|
||||
bottle.serviceFactory('getTagVisits', getTagVisits, 'buildShlinkApiClient');
|
||||
bottle.serviceFactory('cancelGetTagVisits', () => cancelGetTagVisits);
|
||||
|
||||
bottle.serviceFactory('createNewVisit', () => createNewVisit);
|
||||
};
|
||||
|
||||
|
|
|
@ -19,5 +19,6 @@ export const VisitType = PropTypes.shape({
|
|||
export const VisitsInfoType = PropTypes.shape({
|
||||
visits: PropTypes.arrayOf(VisitType),
|
||||
loading: PropTypes.bool,
|
||||
loadingLarge: PropTypes.bool,
|
||||
error: PropTypes.bool,
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@ import reducer, {
|
|||
import { SHORT_URL_TAGS_EDITED } from '../../../src/short-urls/reducers/shortUrlTags';
|
||||
import { SHORT_URL_DELETED } from '../../../src/short-urls/reducers/shortUrlDeletion';
|
||||
import { SHORT_URL_META_EDITED } from '../../../src/short-urls/reducers/shortUrlMeta';
|
||||
import { CREATE_SHORT_URL_VISIT } from '../../../src/visits/reducers/shortUrlVisits';
|
||||
import { CREATE_VISIT } from '../../../src/visits/reducers/visitCreation';
|
||||
|
||||
describe('shortUrlsListReducer', () => {
|
||||
describe('reducer', () => {
|
||||
|
@ -103,7 +103,7 @@ describe('shortUrlsListReducer', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('updates visits count on CREATE_SHORT_URL_VISIT', () => {
|
||||
it('updates visits count on CREATE_VISIT', () => {
|
||||
const shortCode = 'abc123';
|
||||
const shortUrl = {
|
||||
shortCode,
|
||||
|
@ -119,7 +119,7 @@ describe('shortUrlsListReducer', () => {
|
|||
},
|
||||
};
|
||||
|
||||
expect(reducer(state, { type: CREATE_SHORT_URL_VISIT, shortUrl })).toEqual({
|
||||
expect(reducer(state, { type: CREATE_VISIT, shortUrl })).toEqual({
|
||||
shortUrls: {
|
||||
data: [
|
||||
{ shortCode, domain: 'example.com', visitsCount: 5 },
|
||||
|
|
|
@ -71,6 +71,28 @@ describe('ShlinkApiClient', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getTagVisits', () => {
|
||||
it('properly returns tag visits', async () => {
|
||||
const expectedVisits = [ 'foo', 'bar' ];
|
||||
const axiosSpy = jest.fn(createAxiosMock({
|
||||
data: {
|
||||
visits: {
|
||||
data: expectedVisits,
|
||||
},
|
||||
},
|
||||
}));
|
||||
const { getTagVisits } = new ShlinkApiClient(axiosSpy);
|
||||
|
||||
const actualVisits = await getTagVisits('foo', {});
|
||||
|
||||
expect({ data: expectedVisits }).toEqual(actualVisits);
|
||||
expect(axiosSpy).toHaveBeenCalledWith(expect.objectContaining({
|
||||
url: '/tags/foo/visits',
|
||||
method: 'GET',
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getShortUrl', () => {
|
||||
it.each(shortCodesWithDomainCombinations)('properly returns short URL', async (shortCode, domain) => {
|
||||
const expectedShortUrl = { foo: 'bar' };
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import reducer, {
|
||||
getShortUrlVisits,
|
||||
cancelGetShortUrlVisits,
|
||||
createNewVisit,
|
||||
GET_SHORT_URL_VISITS_START,
|
||||
GET_SHORT_URL_VISITS_ERROR,
|
||||
GET_SHORT_URL_VISITS,
|
||||
GET_SHORT_URL_VISITS_LARGE,
|
||||
GET_SHORT_URL_VISITS_CANCEL,
|
||||
CREATE_SHORT_URL_VISIT,
|
||||
} from '../../../src/visits/reducers/shortUrlVisits';
|
||||
import { CREATE_VISIT } from '../../../src/visits/reducers/visitCreation';
|
||||
|
||||
describe('shortUrlVisitsReducer', () => {
|
||||
describe('reducer', () => {
|
||||
|
@ -54,7 +53,7 @@ describe('shortUrlVisitsReducer', () => {
|
|||
it.each([
|
||||
[{ shortCode: 'abc123' }, [{}, {}, {}]],
|
||||
[{ shortCode: 'def456' }, [{}, {}]],
|
||||
])('appends a new visit on CREATE_SHORT_URL_VISIT', (state, expectedVisits) => {
|
||||
])('appends a new visit on CREATE_VISIT', (state, expectedVisits) => {
|
||||
const shortUrl = {
|
||||
shortCode: 'abc123',
|
||||
};
|
||||
|
@ -63,7 +62,7 @@ describe('shortUrlVisitsReducer', () => {
|
|||
visits: [{}, {}],
|
||||
};
|
||||
|
||||
const { visits } = reducer(prevState, { type: CREATE_SHORT_URL_VISIT, shortUrl, visit: {} });
|
||||
const { visits } = reducer(prevState, { type: CREATE_VISIT, shortUrl, visit: {} });
|
||||
|
||||
expect(visits).toEqual(expectedVisits);
|
||||
});
|
||||
|
@ -138,11 +137,4 @@ describe('shortUrlVisitsReducer', () => {
|
|||
it('just returns the action with proper type', () =>
|
||||
expect(cancelGetShortUrlVisits()).toEqual({ type: GET_SHORT_URL_VISITS_CANCEL }));
|
||||
});
|
||||
|
||||
describe('createNewVisit', () => {
|
||||
it('just returns the action with proper type', () =>
|
||||
expect(createNewVisit({ shortUrl: {}, visit: {} })).toEqual(
|
||||
{ type: CREATE_SHORT_URL_VISIT, shortUrl: {}, visit: {} }
|
||||
));
|
||||
});
|
||||
});
|
||||
|
|
10
test/visits/reducers/visitCreation.test.js
Normal file
10
test/visits/reducers/visitCreation.test.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { CREATE_VISIT, createNewVisit } from '../../../src/visits/reducers/visitCreation';
|
||||
|
||||
describe('visitCreationReducer', () => {
|
||||
describe('createNewVisit', () => {
|
||||
it('just returns the action with proper type', () =>
|
||||
expect(createNewVisit({ shortUrl: {}, visit: {} })).toEqual(
|
||||
{ type: CREATE_VISIT, shortUrl: {}, visit: {} }
|
||||
));
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue