From 4a80f224d885bca78bdc88e09712a9a83e0e024c Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 5 Feb 2022 13:53:07 +0100 Subject: [PATCH] Created components and reducer to handle non-orphan visits --- src/common/services/provideServices.ts | 1 + src/visits/NonOrphanVisits.tsx | 44 +++++++++++++ src/visits/NonOrphanVisitsHeader.tsx | 14 ++++ src/visits/reducers/nonOrphanVisits.ts | 88 ++++++++++++++++++++++++++ src/visits/reducers/orphanVisits.ts | 2 +- src/visits/services/provideServices.ts | 17 ++++- 6 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 src/visits/NonOrphanVisits.tsx create mode 100644 src/visits/NonOrphanVisitsHeader.tsx create mode 100644 src/visits/reducers/nonOrphanVisits.ts diff --git a/src/common/services/provideServices.ts b/src/common/services/provideServices.ts index 9d51ccdb..ab1f58b1 100644 --- a/src/common/services/provideServices.ts +++ b/src/common/services/provideServices.ts @@ -41,6 +41,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter: 'ShortUrlVisits', 'TagVisits', 'OrphanVisits', + 'NonOrphanVisits', 'ServerError', 'Overview', 'EditShortUrl', diff --git a/src/visits/NonOrphanVisits.tsx b/src/visits/NonOrphanVisits.tsx new file mode 100644 index 00000000..947111ff --- /dev/null +++ b/src/visits/NonOrphanVisits.tsx @@ -0,0 +1,44 @@ +import { RouteComponentProps } from 'react-router'; +import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; +import { ShlinkVisitsParams } from '../api/types'; +import { Topics } from '../mercure/helpers/Topics'; +import VisitsStats from './VisitsStats'; +import { NormalizedVisit, VisitsInfo, VisitsParams } from './types'; +import { VisitsExporter } from './services/VisitsExporter'; +import { CommonVisitsProps } from './types/CommonVisitsProps'; +import { toApiParams } from './types/helpers'; +import { NonOrphanVisitsHeader } from './NonOrphanVisitsHeader'; + +export interface NonOrphanVisitsProps extends CommonVisitsProps, RouteComponentProps { + getNonOrphanVisits: (params?: ShlinkVisitsParams, doIntervalFallback?: boolean) => void; + nonOrphanVisits: VisitsInfo; + cancelGetNonOrphanVisits: () => void; +} + +export const NonOrphanVisits = ({ exportVisits }: VisitsExporter) => boundToMercureHub(({ + history: { goBack }, + match: { url }, + getNonOrphanVisits, + nonOrphanVisits, + cancelGetNonOrphanVisits, + settings, + selectedServer, +}: NonOrphanVisitsProps) => { + const exportCsv = (visits: NormalizedVisit[]) => exportVisits('non_orphan_visits.csv', visits); + const loadVisits = (params: VisitsParams, doIntervalFallback?: boolean) => + getNonOrphanVisits(toApiParams(params), doIntervalFallback); + + return ( + + + + ); +}, () => [ Topics.visits ]); diff --git a/src/visits/NonOrphanVisitsHeader.tsx b/src/visits/NonOrphanVisitsHeader.tsx new file mode 100644 index 00000000..a361defe --- /dev/null +++ b/src/visits/NonOrphanVisitsHeader.tsx @@ -0,0 +1,14 @@ +import VisitsHeader from './VisitsHeader'; +import { VisitsInfo } from './types'; +import './ShortUrlVisitsHeader.scss'; + +interface NonOrphanVisitsHeaderProps { + nonOrphanVisits: VisitsInfo; + goBack: () => void; +} + +export const NonOrphanVisitsHeader = ({ nonOrphanVisits, goBack }: NonOrphanVisitsHeaderProps) => { + const { visits } = nonOrphanVisits; + + return ; +}; diff --git a/src/visits/reducers/nonOrphanVisits.ts b/src/visits/reducers/nonOrphanVisits.ts new file mode 100644 index 00000000..2ce9fa9d --- /dev/null +++ b/src/visits/reducers/nonOrphanVisits.ts @@ -0,0 +1,88 @@ +import { Action, Dispatch } from 'redux'; +import { + Visit, + VisitsFallbackIntervalAction, + VisitsInfo, + VisitsLoadProgressChangedAction, +} from '../types'; +import { buildActionCreator, buildReducer } from '../../utils/helpers/redux'; +import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; +import { GetState } from '../../container/types'; +import { ShlinkVisitsParams } from '../../api/types'; +import { ApiErrorAction } from '../../api/types/actions'; +import { isBetween } from '../../utils/helpers/date'; +import { getVisitsWithLoader, lastVisitLoaderForLoader } from './common'; +import { CREATE_VISITS, CreateVisitsAction } from './visitCreation'; + +/* eslint-disable padding-line-between-statements */ +export const GET_NON_ORPHAN_VISITS_START = 'shlink/orphanVisits/GET_NON_ORPHAN_VISITS_START'; +export const GET_NON_ORPHAN_VISITS_ERROR = 'shlink/orphanVisits/GET_NON_ORPHAN_VISITS_ERROR'; +export const GET_NON_ORPHAN_VISITS = 'shlink/orphanVisits/GET_NON_ORPHAN_VISITS'; +export const GET_NON_ORPHAN_VISITS_LARGE = 'shlink/orphanVisits/GET_NON_ORPHAN_VISITS_LARGE'; +export const GET_NON_ORPHAN_VISITS_CANCEL = 'shlink/orphanVisits/GET_NON_ORPHAN_VISITS_CANCEL'; +export const GET_NON_ORPHAN_VISITS_PROGRESS_CHANGED = 'shlink/orphanVisits/GET_NON_ORPHAN_VISITS_PROGRESS_CHANGED'; +export const GET_NON_ORPHAN_VISITS_FALLBACK_TO_INTERVAL = 'shlink/orphanVisits/GET_NON_ORPHAN_VISITS_FALLBACK_TO_INTERVAL'; +/* eslint-enable padding-line-between-statements */ + +export interface NonOrphanVisitsAction extends Action { + visits: Visit[]; + query?: ShlinkVisitsParams; +} + +type NonOrphanVisitsCombinedAction = NonOrphanVisitsAction +& VisitsLoadProgressChangedAction +& VisitsFallbackIntervalAction +& CreateVisitsAction +& ApiErrorAction; + +const initialState: VisitsInfo = { + visits: [], + loading: false, + loadingLarge: false, + error: false, + cancelLoad: false, + progress: 0, +}; + +export default buildReducer({ + [GET_NON_ORPHAN_VISITS_START]: () => ({ ...initialState, loading: true }), + [GET_NON_ORPHAN_VISITS_ERROR]: (_, { errorData }) => ({ ...initialState, error: true, errorData }), + [GET_NON_ORPHAN_VISITS]: (state, { visits, query }) => ({ ...state, visits, query, loading: false, error: false }), + [GET_NON_ORPHAN_VISITS_LARGE]: (state) => ({ ...state, loadingLarge: true }), + [GET_NON_ORPHAN_VISITS_CANCEL]: (state) => ({ ...state, cancelLoad: true }), + [GET_NON_ORPHAN_VISITS_PROGRESS_CHANGED]: (state, { progress }) => ({ ...state, progress }), + [GET_NON_ORPHAN_VISITS_FALLBACK_TO_INTERVAL]: (state, { fallbackInterval }) => ({ ...state, fallbackInterval }), + [CREATE_VISITS]: (state, { createdVisits }) => { + const { visits, query = {} } = state; + const { startDate, endDate } = query; + const newVisits = createdVisits + .filter(({ visit }) => isBetween(visit.date, startDate, endDate)) + .map(({ visit }) => visit); + + return { ...state, visits: [ ...newVisits, ...visits ] }; + }, +}, initialState); + +export const getNonOrphanVisits = (buildShlinkApiClient: ShlinkApiClientBuilder) => ( + query: ShlinkVisitsParams = {}, + doIntervalFallback = false, +) => async (dispatch: Dispatch, getState: GetState) => { + const { getNonOrphanVisits } = buildShlinkApiClient(getState); + const visitsLoader = async (page: number, itemsPerPage: number) => + getNonOrphanVisits({ ...query, page, itemsPerPage }); + const lastVisitLoader = lastVisitLoaderForLoader(doIntervalFallback, getNonOrphanVisits); + const shouldCancel = () => getState().orphanVisits.cancelLoad; + const extraFinishActionData: Partial = { query }; + const actionMap = { + start: GET_NON_ORPHAN_VISITS_START, + large: GET_NON_ORPHAN_VISITS_LARGE, + finish: GET_NON_ORPHAN_VISITS, + error: GET_NON_ORPHAN_VISITS_ERROR, + progress: GET_NON_ORPHAN_VISITS_PROGRESS_CHANGED, + fallbackToInterval: GET_NON_ORPHAN_VISITS_FALLBACK_TO_INTERVAL, + }; + + return getVisitsWithLoader(visitsLoader, lastVisitLoader, extraFinishActionData, actionMap, dispatch, shouldCancel); +}; + +export const cancelGetNonOrphanVisits = buildActionCreator(GET_NON_ORPHAN_VISITS_CANCEL); diff --git a/src/visits/reducers/orphanVisits.ts b/src/visits/reducers/orphanVisits.ts index bcb06d8a..03a93cf6 100644 --- a/src/visits/reducers/orphanVisits.ts +++ b/src/visits/reducers/orphanVisits.ts @@ -59,7 +59,7 @@ export default buildReducer({ const { visits, query = {} } = state; const { startDate, endDate } = query; const newVisits = createdVisits - .filter(({ visit }) => isBetween(visit.date, startDate, endDate)) + .filter(({ visit, shortUrl }) => !shortUrl && isBetween(visit.date, startDate, endDate)) .map(({ visit }) => visit); return { ...state, visits: [ ...newVisits, ...visits ] }; diff --git a/src/visits/services/provideServices.ts b/src/visits/services/provideServices.ts index 9eb3f5eb..626d1ee1 100644 --- a/src/visits/services/provideServices.ts +++ b/src/visits/services/provideServices.ts @@ -1,12 +1,14 @@ import Bottle from 'bottlejs'; -import ShortUrlVisits from '../ShortUrlVisits'; -import { cancelGetShortUrlVisits, getShortUrlVisits } from '../reducers/shortUrlVisits'; import MapModal from '../helpers/MapModal'; import { createNewVisits } from '../reducers/visitCreation'; +import ShortUrlVisits from '../ShortUrlVisits'; import TagVisits from '../TagVisits'; -import { cancelGetTagVisits, getTagVisits } from '../reducers/tagVisits'; import { OrphanVisits } from '../OrphanVisits'; +import { NonOrphanVisits } from '../NonOrphanVisits'; +import { cancelGetShortUrlVisits, getShortUrlVisits } from '../reducers/shortUrlVisits'; +import { cancelGetTagVisits, getTagVisits } from '../reducers/tagVisits'; import { cancelGetOrphanVisits, getOrphanVisits } from '../reducers/orphanVisits'; +import { cancelGetNonOrphanVisits, getNonOrphanVisits } from '../reducers/nonOrphanVisits'; import { ConnectDecorator } from '../../container/types'; import { loadVisitsOverview } from '../reducers/visitsOverview'; import * as visitsParser from './VisitsParser'; @@ -34,6 +36,12 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { [ 'getOrphanVisits', 'cancelGetOrphanVisits', 'createNewVisits', 'loadMercureInfo' ], )); + bottle.serviceFactory('NonOrphanVisits', NonOrphanVisits, 'VisitsExporter'); + bottle.decorator('NonOrphanVisits', connect( + [ 'nonOrphanVisits', 'mercureInfo', 'settings', 'selectedServer' ], + [ 'getNonOrphanVisits', 'cancelGetNonOrphanVisits', 'createNewVisits', 'loadMercureInfo' ], + )); + // Services bottle.serviceFactory('VisitsParser', () => visitsParser); bottle.service('VisitsExporter', VisitsExporter, 'window', 'csvjson'); @@ -48,6 +56,9 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.serviceFactory('getOrphanVisits', getOrphanVisits, 'buildShlinkApiClient'); bottle.serviceFactory('cancelGetOrphanVisits', () => cancelGetOrphanVisits); + bottle.serviceFactory('getNonOrphanVisits', getNonOrphanVisits, 'buildShlinkApiClient'); + bottle.serviceFactory('cancelGetNonOrphanVisits', () => cancelGetNonOrphanVisits); + bottle.serviceFactory('createNewVisits', () => createNewVisits); bottle.serviceFactory('loadVisitsOverview', loadVisitsOverview, 'buildShlinkApiClient'); };