mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 17:40:23 +03:00
Implemented domain visits section
This commit is contained in:
parent
932dec3bde
commit
05254326cb
8 changed files with 176 additions and 7 deletions
|
@ -56,6 +56,10 @@ export default class ShlinkApiClient {
|
||||||
this.performRequest<{ visits: ShlinkVisits }>(`/tags/${tag}/visits`, 'GET', query)
|
this.performRequest<{ visits: ShlinkVisits }>(`/tags/${tag}/visits`, 'GET', query)
|
||||||
.then(({ data }) => data.visits);
|
.then(({ data }) => data.visits);
|
||||||
|
|
||||||
|
public readonly getDomainVisits = async (domain: string, query?: Omit<ShlinkVisitsParams, 'domain'>): Promise<ShlinkVisits> =>
|
||||||
|
this.performRequest<{ visits: ShlinkVisits }>(`/domains/${domain}/visits`, 'GET', query)
|
||||||
|
.then(({ data }) => data.visits);
|
||||||
|
|
||||||
public readonly getOrphanVisits = async (query?: Omit<ShlinkVisitsParams, 'domain'>): Promise<ShlinkVisits> =>
|
public readonly getOrphanVisits = async (query?: Omit<ShlinkVisitsParams, 'domain'>): Promise<ShlinkVisits> =>
|
||||||
this.performRequest<{ visits: ShlinkVisits }>('/visits/orphan', 'GET', query)
|
this.performRequest<{ visits: ShlinkVisits }>('/visits/orphan', 'GET', query)
|
||||||
.then(({ data }) => data.visits);
|
.then(({ data }) => data.visits);
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { DomainsList } from '../domains/reducers/domainsList';
|
||||||
import { VisitsOverview } from '../visits/reducers/visitsOverview';
|
import { VisitsOverview } from '../visits/reducers/visitsOverview';
|
||||||
import { VisitsInfo } from '../visits/types';
|
import { VisitsInfo } from '../visits/types';
|
||||||
import { Sidebar } from '../common/reducers/sidebar';
|
import { Sidebar } from '../common/reducers/sidebar';
|
||||||
|
import { DomainVisits } from '../visits/reducers/domainVisits';
|
||||||
|
|
||||||
export interface ShlinkState {
|
export interface ShlinkState {
|
||||||
servers: ServersMap;
|
servers: ServersMap;
|
||||||
|
@ -25,6 +26,7 @@ export interface ShlinkState {
|
||||||
shortUrlEdition: ShortUrlEdition;
|
shortUrlEdition: ShortUrlEdition;
|
||||||
shortUrlVisits: ShortUrlVisits;
|
shortUrlVisits: ShortUrlVisits;
|
||||||
tagVisits: TagVisits;
|
tagVisits: TagVisits;
|
||||||
|
domainVisits: DomainVisits;
|
||||||
orphanVisits: VisitsInfo;
|
orphanVisits: VisitsInfo;
|
||||||
nonOrphanVisits: VisitsInfo;
|
nonOrphanVisits: VisitsInfo;
|
||||||
shortUrlDetail: ShortUrlDetail;
|
shortUrlDetail: ShortUrlDetail;
|
||||||
|
|
|
@ -27,14 +27,17 @@ export const DomainDropdown: FC<DomainDropdownProps> = ({ domain, editDomainRedi
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownBtnMenu isOpen={isOpen} toggle={toggle}>
|
<DropdownBtnMenu isOpen={isOpen} toggle={toggle}>
|
||||||
<DropdownItem disabled={!canBeEdited} onClick={!canBeEdited ? undefined : toggleModal}>
|
|
||||||
<FontAwesomeIcon fixedWidth icon={editIcon} /> Edit redirects
|
|
||||||
</DropdownItem>
|
|
||||||
{withVisits && (
|
{withVisits && (
|
||||||
<DropdownItem tag={Link} to={`/server/${serverId}/domain/${isDefault ? 'DEFAULT' : domain.domain}/visits`}>
|
<DropdownItem
|
||||||
|
tag={Link}
|
||||||
|
to={`/server/${serverId}/domain/${domain.domain}${domain.isDefault ? '_DEFAULT' : ''}/visits`}
|
||||||
|
>
|
||||||
<FontAwesomeIcon icon={pieChartIcon} fixedWidth /> Visit stats
|
<FontAwesomeIcon icon={pieChartIcon} fixedWidth /> Visit stats
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
)}
|
)}
|
||||||
|
<DropdownItem disabled={!canBeEdited} onClick={!canBeEdited ? undefined : toggleModal}>
|
||||||
|
<FontAwesomeIcon fixedWidth icon={editIcon} /> Edit redirects
|
||||||
|
</DropdownItem>
|
||||||
|
|
||||||
<EditDomainRedirectsModal
|
<EditDomainRedirectsModal
|
||||||
domain={domain}
|
domain={domain}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import shortUrlDeletionReducer from '../short-urls/reducers/shortUrlDeletion';
|
||||||
import shortUrlEditionReducer from '../short-urls/reducers/shortUrlEdition';
|
import shortUrlEditionReducer from '../short-urls/reducers/shortUrlEdition';
|
||||||
import shortUrlVisitsReducer from '../visits/reducers/shortUrlVisits';
|
import shortUrlVisitsReducer from '../visits/reducers/shortUrlVisits';
|
||||||
import tagVisitsReducer from '../visits/reducers/tagVisits';
|
import tagVisitsReducer from '../visits/reducers/tagVisits';
|
||||||
|
import domainVisitsReducer from '../visits/reducers/domainVisits';
|
||||||
import orphanVisitsReducer from '../visits/reducers/orphanVisits';
|
import orphanVisitsReducer from '../visits/reducers/orphanVisits';
|
||||||
import nonOrphanVisitsReducer from '../visits/reducers/nonOrphanVisits';
|
import nonOrphanVisitsReducer from '../visits/reducers/nonOrphanVisits';
|
||||||
import shortUrlDetailReducer from '../short-urls/reducers/shortUrlDetail';
|
import shortUrlDetailReducer from '../short-urls/reducers/shortUrlDetail';
|
||||||
|
@ -30,6 +31,7 @@ export default combineReducers<ShlinkState>({
|
||||||
shortUrlEdition: shortUrlEditionReducer,
|
shortUrlEdition: shortUrlEditionReducer,
|
||||||
shortUrlVisits: shortUrlVisitsReducer,
|
shortUrlVisits: shortUrlVisitsReducer,
|
||||||
tagVisits: tagVisitsReducer,
|
tagVisits: tagVisitsReducer,
|
||||||
|
domainVisits: domainVisitsReducer,
|
||||||
orphanVisits: orphanVisitsReducer,
|
orphanVisits: orphanVisitsReducer,
|
||||||
nonOrphanVisits: nonOrphanVisitsReducer,
|
nonOrphanVisits: nonOrphanVisitsReducer,
|
||||||
shortUrlDetail: shortUrlDetailReducer,
|
shortUrlDetail: shortUrlDetailReducer,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { isNil } from 'ramda';
|
import { isNil } from 'ramda';
|
||||||
import { ShortUrl } from '../data';
|
import { ShortUrl } from '../data';
|
||||||
import { OptionalString } from '../../utils/utils';
|
import { OptionalString } from '../../utils/utils';
|
||||||
|
import { DEFAULT_DOMAIN } from '../../visits/reducers/domainVisits';
|
||||||
|
|
||||||
export const shortUrlMatches = (shortUrl: ShortUrl, shortCode: string, domain: OptionalString): boolean => {
|
export const shortUrlMatches = (shortUrl: ShortUrl, shortCode: string, domain: OptionalString): boolean => {
|
||||||
if (isNil(domain)) {
|
if (isNil(domain)) {
|
||||||
|
@ -9,3 +10,11 @@ export const shortUrlMatches = (shortUrl: ShortUrl, shortCode: string, domain: O
|
||||||
|
|
||||||
return shortUrl.shortCode === shortCode && shortUrl.domain === domain;
|
return shortUrl.shortCode === shortCode && shortUrl.domain === domain;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const domainMatches = (shortUrl: ShortUrl, domain: string): boolean => {
|
||||||
|
if (!shortUrl.domain && domain === DEFAULT_DOMAIN) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return shortUrl.domain === domain;
|
||||||
|
};
|
||||||
|
|
|
@ -1,3 +1,46 @@
|
||||||
import { FC } from 'react';
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { CommonVisitsProps } from './types/CommonVisitsProps';
|
||||||
|
import { ShlinkVisitsParams } from '../api/types';
|
||||||
|
import { DomainVisits as DomainVisitsState } from './reducers/domainVisits';
|
||||||
|
import { ReportExporter } from '../common/services/ReportExporter';
|
||||||
|
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
||||||
|
import { Topics } from '../mercure/helpers/Topics';
|
||||||
|
import { useGoBack } from '../utils/helpers/hooks';
|
||||||
|
import { toApiParams } from './types/helpers';
|
||||||
|
import { NormalizedVisit } from './types';
|
||||||
|
import VisitsStats from './VisitsStats';
|
||||||
|
import VisitsHeader from './VisitsHeader';
|
||||||
|
|
||||||
export const DomainVisits = (): FC => () => <span>DomainVisits</span>;
|
export interface DomainVisitsProps extends CommonVisitsProps {
|
||||||
|
getDomainVisits: (domain: string, query?: ShlinkVisitsParams, doIntervalFallback?: boolean) => void;
|
||||||
|
domainVisits: DomainVisitsState;
|
||||||
|
cancelGetDomainVisits: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DomainVisits = ({ exportVisits }: ReportExporter) => boundToMercureHub(({
|
||||||
|
getDomainVisits,
|
||||||
|
domainVisits,
|
||||||
|
cancelGetDomainVisits,
|
||||||
|
settings,
|
||||||
|
selectedServer,
|
||||||
|
}: DomainVisitsProps) => {
|
||||||
|
const goBack = useGoBack();
|
||||||
|
const { domain = '' } = useParams();
|
||||||
|
const [authority, domainId = authority] = domain.split('_');
|
||||||
|
const loadVisits = (params: ShlinkVisitsParams, doIntervalFallback?: boolean) =>
|
||||||
|
getDomainVisits(domainId, toApiParams(params), doIntervalFallback);
|
||||||
|
const exportCsv = (visits: NormalizedVisit[]) => exportVisits(`domain_${authority}_visits.csv`, visits);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VisitsStats
|
||||||
|
getVisits={loadVisits}
|
||||||
|
cancelGetVisits={cancelGetDomainVisits}
|
||||||
|
visitsInfo={domainVisits}
|
||||||
|
settings={settings}
|
||||||
|
exportCsv={exportCsv}
|
||||||
|
selectedServer={selectedServer}
|
||||||
|
>
|
||||||
|
<VisitsHeader goBack={goBack} visits={domainVisits.visits} title={`"${authority}" visits`} />
|
||||||
|
</VisitsStats>
|
||||||
|
);
|
||||||
|
}, () => [Topics.visits]);
|
||||||
|
|
98
src/visits/reducers/domainVisits.ts
Normal file
98
src/visits/reducers/domainVisits.ts
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
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';
|
||||||
|
import { domainMatches } from '../../short-urls/helpers';
|
||||||
|
|
||||||
|
export const GET_DOMAIN_VISITS_START = 'shlink/domainVisits/GET_DOMAIN_VISITS_START';
|
||||||
|
export const GET_DOMAIN_VISITS_ERROR = 'shlink/domainVisits/GET_DOMAIN_VISITS_ERROR';
|
||||||
|
export const GET_DOMAIN_VISITS = 'shlink/domainVisits/GET_DOMAIN_VISITS';
|
||||||
|
export const GET_DOMAIN_VISITS_LARGE = 'shlink/domainVisits/GET_DOMAIN_VISITS_LARGE';
|
||||||
|
export const GET_DOMAIN_VISITS_CANCEL = 'shlink/domainVisits/GET_DOMAIN_VISITS_CANCEL';
|
||||||
|
export const GET_DOMAIN_VISITS_PROGRESS_CHANGED = 'shlink/domainVisits/GET_DOMAIN_VISITS_PROGRESS_CHANGED';
|
||||||
|
export const GET_DOMAIN_VISITS_FALLBACK_TO_INTERVAL = 'shlink/domainVisits/GET_DOMAIN_VISITS_FALLBACK_TO_INTERVAL';
|
||||||
|
|
||||||
|
export const DEFAULT_DOMAIN = 'DEFAULT';
|
||||||
|
|
||||||
|
export interface DomainVisits extends VisitsInfo {
|
||||||
|
domain: string;
|
||||||
|
domainId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DomainVisitsAction extends Action<string> {
|
||||||
|
visits: Visit[];
|
||||||
|
domain: string;
|
||||||
|
query?: ShlinkVisitsParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
type DomainVisitsCombinedAction = DomainVisitsAction
|
||||||
|
& VisitsLoadProgressChangedAction
|
||||||
|
& VisitsFallbackIntervalAction
|
||||||
|
& CreateVisitsAction
|
||||||
|
& ApiErrorAction;
|
||||||
|
|
||||||
|
const initialState: DomainVisits = {
|
||||||
|
visits: [],
|
||||||
|
domain: '',
|
||||||
|
domainId: '',
|
||||||
|
loading: false,
|
||||||
|
loadingLarge: false,
|
||||||
|
error: false,
|
||||||
|
cancelLoad: false,
|
||||||
|
progress: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default buildReducer<DomainVisits, DomainVisitsCombinedAction>({
|
||||||
|
[GET_DOMAIN_VISITS_START]: () => ({ ...initialState, loading: true }),
|
||||||
|
[GET_DOMAIN_VISITS_ERROR]: (_, { errorData }) => ({ ...initialState, error: true, errorData }),
|
||||||
|
[GET_DOMAIN_VISITS]: (state, { visits, domain, query }) => (
|
||||||
|
{ ...state, visits, domain, query, loading: false, error: false }
|
||||||
|
),
|
||||||
|
[GET_DOMAIN_VISITS_LARGE]: (state) => ({ ...state, loadingLarge: true }),
|
||||||
|
[GET_DOMAIN_VISITS_CANCEL]: (state) => ({ ...state, cancelLoad: true }),
|
||||||
|
[GET_DOMAIN_VISITS_PROGRESS_CHANGED]: (state, { progress }) => ({ ...state, progress }),
|
||||||
|
[GET_DOMAIN_VISITS_FALLBACK_TO_INTERVAL]: (state, { fallbackInterval }) => ({ ...state, fallbackInterval }),
|
||||||
|
[CREATE_VISITS]: (state, { createdVisits }) => {
|
||||||
|
const { domain, visits, query = {} } = state;
|
||||||
|
const { startDate, endDate } = query;
|
||||||
|
const newVisits = createdVisits
|
||||||
|
.filter(({ shortUrl, visit }) =>
|
||||||
|
shortUrl && domainMatches(shortUrl, domain) && isBetween(visit.date, startDate, endDate))
|
||||||
|
.map(({ visit }) => visit);
|
||||||
|
|
||||||
|
return { ...state, visits: [...newVisits, ...visits] };
|
||||||
|
},
|
||||||
|
}, initialState);
|
||||||
|
|
||||||
|
export const getDomainVisits = (buildShlinkApiClient: ShlinkApiClientBuilder) => (
|
||||||
|
domain: string,
|
||||||
|
query: ShlinkVisitsParams = {},
|
||||||
|
doIntervalFallback = false,
|
||||||
|
) => async (dispatch: Dispatch, getState: GetState) => {
|
||||||
|
const { getDomainVisits: getVisits } = buildShlinkApiClient(getState);
|
||||||
|
const visitsLoader = async (page: number, itemsPerPage: number) => getVisits(
|
||||||
|
domain,
|
||||||
|
{ ...query, page, itemsPerPage },
|
||||||
|
);
|
||||||
|
const lastVisitLoader = lastVisitLoaderForLoader(doIntervalFallback, async (params) => getVisits(domain, params));
|
||||||
|
const shouldCancel = () => getState().domainVisits.cancelLoad;
|
||||||
|
const extraFinishActionData: Partial<DomainVisitsAction> = { domain, query };
|
||||||
|
const actionMap = {
|
||||||
|
start: GET_DOMAIN_VISITS_START,
|
||||||
|
large: GET_DOMAIN_VISITS_LARGE,
|
||||||
|
finish: GET_DOMAIN_VISITS,
|
||||||
|
error: GET_DOMAIN_VISITS_ERROR,
|
||||||
|
progress: GET_DOMAIN_VISITS_PROGRESS_CHANGED,
|
||||||
|
fallbackToInterval: GET_DOMAIN_VISITS_FALLBACK_TO_INTERVAL,
|
||||||
|
};
|
||||||
|
|
||||||
|
return getVisitsWithLoader(visitsLoader, lastVisitLoader, extraFinishActionData, actionMap, dispatch, shouldCancel);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cancelGetDomainVisits = buildActionCreator(GET_DOMAIN_VISITS_CANCEL);
|
|
@ -7,6 +7,7 @@ import { OrphanVisits } from '../OrphanVisits';
|
||||||
import { NonOrphanVisits } from '../NonOrphanVisits';
|
import { NonOrphanVisits } from '../NonOrphanVisits';
|
||||||
import { cancelGetShortUrlVisits, getShortUrlVisits } from '../reducers/shortUrlVisits';
|
import { cancelGetShortUrlVisits, getShortUrlVisits } from '../reducers/shortUrlVisits';
|
||||||
import { cancelGetTagVisits, getTagVisits } from '../reducers/tagVisits';
|
import { cancelGetTagVisits, getTagVisits } from '../reducers/tagVisits';
|
||||||
|
import { cancelGetDomainVisits, getDomainVisits } from '../reducers/domainVisits';
|
||||||
import { cancelGetOrphanVisits, getOrphanVisits } from '../reducers/orphanVisits';
|
import { cancelGetOrphanVisits, getOrphanVisits } from '../reducers/orphanVisits';
|
||||||
import { cancelGetNonOrphanVisits, getNonOrphanVisits } from '../reducers/nonOrphanVisits';
|
import { cancelGetNonOrphanVisits, getNonOrphanVisits } from '../reducers/nonOrphanVisits';
|
||||||
import { ConnectDecorator } from '../../container/types';
|
import { ConnectDecorator } from '../../container/types';
|
||||||
|
@ -30,7 +31,11 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
['getTagVisits', 'cancelGetTagVisits', 'createNewVisits', 'loadMercureInfo'],
|
['getTagVisits', 'cancelGetTagVisits', 'createNewVisits', 'loadMercureInfo'],
|
||||||
));
|
));
|
||||||
|
|
||||||
bottle.serviceFactory('DomainVisits', DomainVisits);
|
bottle.serviceFactory('DomainVisits', DomainVisits, 'ReportExporter');
|
||||||
|
bottle.decorator('DomainVisits', connect(
|
||||||
|
['domainVisits', 'mercureInfo', 'settings', 'selectedServer'],
|
||||||
|
['getDomainVisits', 'cancelGetDomainVisits', 'createNewVisits', 'loadMercureInfo'],
|
||||||
|
));
|
||||||
|
|
||||||
bottle.serviceFactory('OrphanVisits', OrphanVisits, 'ReportExporter');
|
bottle.serviceFactory('OrphanVisits', OrphanVisits, 'ReportExporter');
|
||||||
bottle.decorator('OrphanVisits', connect(
|
bottle.decorator('OrphanVisits', connect(
|
||||||
|
@ -54,6 +59,9 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
bottle.serviceFactory('getTagVisits', getTagVisits, 'buildShlinkApiClient');
|
bottle.serviceFactory('getTagVisits', getTagVisits, 'buildShlinkApiClient');
|
||||||
bottle.serviceFactory('cancelGetTagVisits', () => cancelGetTagVisits);
|
bottle.serviceFactory('cancelGetTagVisits', () => cancelGetTagVisits);
|
||||||
|
|
||||||
|
bottle.serviceFactory('getDomainVisits', getDomainVisits, 'buildShlinkApiClient');
|
||||||
|
bottle.serviceFactory('cancelGetDomainVisits', () => cancelGetDomainVisits);
|
||||||
|
|
||||||
bottle.serviceFactory('getOrphanVisits', getOrphanVisits, 'buildShlinkApiClient');
|
bottle.serviceFactory('getOrphanVisits', getOrphanVisits, 'buildShlinkApiClient');
|
||||||
bottle.serviceFactory('cancelGetOrphanVisits', () => cancelGetOrphanVisits);
|
bottle.serviceFactory('cancelGetOrphanVisits', () => cancelGetOrphanVisits);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue