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');
};