mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 17:40:23 +03:00
Merge pull request #447 from acelaya-forks/feature/visits-filter-reducer
Feature/visits filter reducer
This commit is contained in:
commit
b3e79f4219
15 changed files with 80 additions and 63 deletions
|
@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
* [#430](https://github.com/shlinkio/shlink-web-client/pull/430) Added support to set new and existing short URLs as crawlable, when consuming Shlink >=2.7.
|
* [#430](https://github.com/shlinkio/shlink-web-client/pull/430) Added support to set new and existing short URLs as crawlable, when consuming Shlink >=2.7.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
* [#442](https://github.com/shlinkio/shlink-web-client/pull/442) Visits filtering now goes through the corresponding reducer.
|
||||||
* [#337](https://github.com/shlinkio/shlink-web-client/pull/337) Replaced moment.js with date-fns.
|
* [#337](https://github.com/shlinkio/shlink-web-client/pull/337) Replaced moment.js with date-fns.
|
||||||
* [#360](https://github.com/shlinkio/shlink-web-client/pull/360) Changed component used to generate a tags selector, switching from `react-tagsinput`, which is no longer maintained, to `react-tag-autocomplete`.
|
* [#360](https://github.com/shlinkio/shlink-web-client/pull/360) Changed component used to generate a tags selector, switching from `react-tagsinput`, which is no longer maintained, to `react-tag-autocomplete`.
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,7 @@ export interface ShlinkVisitsParams {
|
||||||
itemsPerPage?: number;
|
itemsPerPage?: number;
|
||||||
startDate?: string;
|
startDate?: string;
|
||||||
endDate?: string;
|
endDate?: string;
|
||||||
|
excludeBots?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ShlinkShortUrlData extends ShortUrlMeta {
|
export interface ShlinkShortUrlData extends ShortUrlMeta {
|
||||||
|
|
|
@ -4,12 +4,13 @@ import { ShlinkVisitsParams } from '../api/types';
|
||||||
import { Topics } from '../mercure/helpers/Topics';
|
import { Topics } from '../mercure/helpers/Topics';
|
||||||
import VisitsStats from './VisitsStats';
|
import VisitsStats from './VisitsStats';
|
||||||
import { OrphanVisitsHeader } from './OrphanVisitsHeader';
|
import { OrphanVisitsHeader } from './OrphanVisitsHeader';
|
||||||
import { NormalizedVisit, VisitsInfo } from './types';
|
import { NormalizedVisit, OrphanVisitType, VisitsInfo, VisitsParams } from './types';
|
||||||
import { VisitsExporter } from './services/VisitsExporter';
|
import { VisitsExporter } from './services/VisitsExporter';
|
||||||
import { CommonVisitsProps } from './types/CommonVisitsProps';
|
import { CommonVisitsProps } from './types/CommonVisitsProps';
|
||||||
|
import { toApiParams } from './types/helpers';
|
||||||
|
|
||||||
export interface OrphanVisitsProps extends CommonVisitsProps, RouteComponentProps {
|
export interface OrphanVisitsProps extends CommonVisitsProps, RouteComponentProps {
|
||||||
getOrphanVisits: (params: ShlinkVisitsParams) => void;
|
getOrphanVisits: (params?: ShlinkVisitsParams, orphanVisitsType?: OrphanVisitType) => void;
|
||||||
orphanVisits: VisitsInfo;
|
orphanVisits: VisitsInfo;
|
||||||
cancelGetOrphanVisits: () => void;
|
cancelGetOrphanVisits: () => void;
|
||||||
}
|
}
|
||||||
|
@ -24,10 +25,11 @@ export const OrphanVisits = ({ exportVisits }: VisitsExporter) => boundToMercure
|
||||||
selectedServer,
|
selectedServer,
|
||||||
}: OrphanVisitsProps) => {
|
}: OrphanVisitsProps) => {
|
||||||
const exportCsv = (visits: NormalizedVisit[]) => exportVisits('orphan_visits.csv', visits);
|
const exportCsv = (visits: NormalizedVisit[]) => exportVisits('orphan_visits.csv', visits);
|
||||||
|
const loadVisits = (params: VisitsParams) => getOrphanVisits(toApiParams(params), params.filter?.orphanVisitsType);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VisitsStats
|
<VisitsStats
|
||||||
getVisits={getOrphanVisits}
|
getVisits={loadVisits}
|
||||||
cancelGetVisits={cancelGetOrphanVisits}
|
cancelGetVisits={cancelGetOrphanVisits}
|
||||||
visitsInfo={orphanVisits}
|
visitsInfo={orphanVisits}
|
||||||
baseUrl={url}
|
baseUrl={url}
|
||||||
|
|
|
@ -9,8 +9,9 @@ import { ShortUrlVisits as ShortUrlVisitsState } from './reducers/shortUrlVisits
|
||||||
import ShortUrlVisitsHeader from './ShortUrlVisitsHeader';
|
import ShortUrlVisitsHeader from './ShortUrlVisitsHeader';
|
||||||
import VisitsStats from './VisitsStats';
|
import VisitsStats from './VisitsStats';
|
||||||
import { VisitsExporter } from './services/VisitsExporter';
|
import { VisitsExporter } from './services/VisitsExporter';
|
||||||
import { NormalizedVisit } from './types';
|
import { NormalizedVisit, VisitsParams } from './types';
|
||||||
import { CommonVisitsProps } from './types/CommonVisitsProps';
|
import { CommonVisitsProps } from './types/CommonVisitsProps';
|
||||||
|
import { toApiParams } from './types/helpers';
|
||||||
|
|
||||||
export interface ShortUrlVisitsProps extends CommonVisitsProps, RouteComponentProps<{ shortCode: string }> {
|
export interface ShortUrlVisitsProps extends CommonVisitsProps, RouteComponentProps<{ shortCode: string }> {
|
||||||
getShortUrlVisits: (shortCode: string, query?: ShlinkVisitsParams) => void;
|
getShortUrlVisits: (shortCode: string, query?: ShlinkVisitsParams) => void;
|
||||||
|
@ -34,7 +35,7 @@ const ShortUrlVisits = ({ exportVisits }: VisitsExporter) => boundToMercureHub((
|
||||||
}: ShortUrlVisitsProps) => {
|
}: ShortUrlVisitsProps) => {
|
||||||
const { shortCode } = params;
|
const { shortCode } = params;
|
||||||
const { domain } = parseQuery<{ domain?: string }>(search);
|
const { domain } = parseQuery<{ domain?: string }>(search);
|
||||||
const loadVisits = (params: Partial<ShlinkVisitsParams>) => getShortUrlVisits(shortCode, { ...params, domain });
|
const loadVisits = (params: VisitsParams) => getShortUrlVisits(shortCode, { ...toApiParams(params), domain });
|
||||||
const exportCsv = (visits: NormalizedVisit[]) => exportVisits(
|
const exportCsv = (visits: NormalizedVisit[]) => exportVisits(
|
||||||
`short-url_${shortUrlDetail.shortUrl?.shortUrl.replace(/https?:\/\//g, '')}_visits.csv`,
|
`short-url_${shortUrlDetail.shortUrl?.shortUrl.replace(/https?:\/\//g, '')}_visits.csv`,
|
||||||
visits,
|
visits,
|
||||||
|
|
|
@ -9,9 +9,10 @@ import VisitsStats from './VisitsStats';
|
||||||
import { VisitsExporter } from './services/VisitsExporter';
|
import { VisitsExporter } from './services/VisitsExporter';
|
||||||
import { NormalizedVisit } from './types';
|
import { NormalizedVisit } from './types';
|
||||||
import { CommonVisitsProps } from './types/CommonVisitsProps';
|
import { CommonVisitsProps } from './types/CommonVisitsProps';
|
||||||
|
import { toApiParams } from './types/helpers';
|
||||||
|
|
||||||
export interface TagVisitsProps extends CommonVisitsProps, RouteComponentProps<{ tag: string }> {
|
export interface TagVisitsProps extends CommonVisitsProps, RouteComponentProps<{ tag: string }> {
|
||||||
getTagVisits: (tag: string, query: any) => void;
|
getTagVisits: (tag: string, query?: ShlinkVisitsParams) => void;
|
||||||
tagVisits: TagVisitsState;
|
tagVisits: TagVisitsState;
|
||||||
cancelGetTagVisits: () => void;
|
cancelGetTagVisits: () => void;
|
||||||
}
|
}
|
||||||
|
@ -26,7 +27,7 @@ const TagVisits = (colorGenerator: ColorGenerator, { exportVisits }: VisitsExpor
|
||||||
selectedServer,
|
selectedServer,
|
||||||
}: TagVisitsProps) => {
|
}: TagVisitsProps) => {
|
||||||
const { tag } = params;
|
const { tag } = params;
|
||||||
const loadVisits = (params: ShlinkVisitsParams) => getTagVisits(tag, params);
|
const loadVisits = (params: ShlinkVisitsParams) => getTagVisits(tag, toApiParams(params));
|
||||||
const exportCsv = (visits: NormalizedVisit[]) => exportVisits(`tag_${tag}_visits.csv`, visits);
|
const exportCsv = (visits: NormalizedVisit[]) => exportVisits(`tag_${tag}_visits.csv`, visits);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -9,8 +9,6 @@ import { Location } from 'history';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
|
import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
|
||||||
import Message from '../utils/Message';
|
import Message from '../utils/Message';
|
||||||
import { formatIsoDate } from '../utils/helpers/date';
|
|
||||||
import { ShlinkVisitsParams } from '../api/types';
|
|
||||||
import { DateInterval, DateRange, intervalToDateRange } from '../utils/dates/types';
|
import { DateInterval, DateRange, intervalToDateRange } from '../utils/dates/types';
|
||||||
import { Result } from '../utils/Result';
|
import { Result } from '../utils/Result';
|
||||||
import { ShlinkApiError } from '../api/ShlinkApiError';
|
import { ShlinkApiError } from '../api/ShlinkApiError';
|
||||||
|
@ -21,15 +19,15 @@ import SortableBarGraph from './helpers/SortableBarGraph';
|
||||||
import GraphCard from './helpers/GraphCard';
|
import GraphCard from './helpers/GraphCard';
|
||||||
import LineChartCard from './helpers/LineChartCard';
|
import LineChartCard from './helpers/LineChartCard';
|
||||||
import VisitsTable from './VisitsTable';
|
import VisitsTable from './VisitsTable';
|
||||||
import { NormalizedOrphanVisit, NormalizedVisit, VisitsInfo } from './types';
|
import { NormalizedOrphanVisit, NormalizedVisit, VisitsFilter, VisitsInfo, VisitsParams } from './types';
|
||||||
import OpenMapModalBtn from './helpers/OpenMapModalBtn';
|
import OpenMapModalBtn from './helpers/OpenMapModalBtn';
|
||||||
import { processStatsFromVisits } from './services/VisitsParser';
|
import { normalizeVisits, processStatsFromVisits } from './services/VisitsParser';
|
||||||
import { VisitsFilter, VisitsFilterDropdown } from './helpers/VisitsFilterDropdown';
|
import { VisitsFilterDropdown } from './helpers/VisitsFilterDropdown';
|
||||||
import { HighlightableProps, highlightedVisitsToStats, normalizeAndFilterVisits } from './types/helpers';
|
import { HighlightableProps, highlightedVisitsToStats } from './types/helpers';
|
||||||
import './VisitsStats.scss';
|
import './VisitsStats.scss';
|
||||||
|
|
||||||
export interface VisitsStatsProps {
|
export interface VisitsStatsProps {
|
||||||
getVisits: (params: Partial<ShlinkVisitsParams>) => void;
|
getVisits: (params: VisitsParams) => void;
|
||||||
visitsInfo: VisitsInfo;
|
visitsInfo: VisitsInfo;
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
selectedServer: SelectedServer;
|
selectedServer: SelectedServer;
|
||||||
|
@ -95,7 +93,7 @@ const VisitsStats: FC<VisitsStatsProps> = ({
|
||||||
return !subPath ? `${baseUrl}${query}` : `${baseUrl}${subPath}${query}`;
|
return !subPath ? `${baseUrl}${query}` : `${baseUrl}${subPath}${query}`;
|
||||||
};
|
};
|
||||||
const { visits, loading, loadingLarge, error, errorData, progress } = visitsInfo;
|
const { visits, loading, loadingLarge, error, errorData, progress } = visitsInfo;
|
||||||
const normalizedVisits = useMemo(() => normalizeAndFilterVisits(visits, visitsFilter), [ visits, visitsFilter ]);
|
const normalizedVisits = useMemo(() => normalizeVisits(visits), [ visits ]);
|
||||||
const { os, browsers, referrers, countries, cities, citiesForMap, visitedUrls } = useMemo(
|
const { os, browsers, referrers, countries, cities, citiesForMap, visitedUrls } = useMemo(
|
||||||
() => processStatsFromVisits(normalizedVisits),
|
() => processStatsFromVisits(normalizedVisits),
|
||||||
[ normalizedVisits ],
|
[ normalizedVisits ],
|
||||||
|
@ -122,10 +120,8 @@ const VisitsStats: FC<VisitsStatsProps> = ({
|
||||||
|
|
||||||
useEffect(() => cancelGetVisits, []);
|
useEffect(() => cancelGetVisits, []);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { startDate, endDate } = dateRange;
|
getVisits({ dateRange, filter: visitsFilter });
|
||||||
|
}, [ dateRange, visitsFilter ]);
|
||||||
getVisits({ startDate: formatIsoDate(startDate) ?? undefined, endDate: formatIsoDate(endDate) ?? undefined });
|
|
||||||
}, [ dateRange ]);
|
|
||||||
|
|
||||||
const renderVisitsContent = () => {
|
const renderVisitsContent = () => {
|
||||||
if (loadingLarge) {
|
if (loadingLarge) {
|
||||||
|
|
|
@ -1,13 +1,8 @@
|
||||||
import { DropdownItem, DropdownItemProps } from 'reactstrap'; // eslint-disable-line import/named
|
import { DropdownItem, DropdownItemProps } from 'reactstrap'; // eslint-disable-line import/named
|
||||||
import { OrphanVisitType } from '../types';
|
import { OrphanVisitType, VisitsFilter } from '../types';
|
||||||
import { DropdownBtn } from '../../utils/DropdownBtn';
|
import { DropdownBtn } from '../../utils/DropdownBtn';
|
||||||
import { hasValue } from '../../utils/utils';
|
import { hasValue } from '../../utils/utils';
|
||||||
|
|
||||||
export interface VisitsFilter {
|
|
||||||
orphanVisitsType?: OrphanVisitType | undefined;
|
|
||||||
excludeBots?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface VisitsFilterDropdownProps {
|
interface VisitsFilterDropdownProps {
|
||||||
onChange: (filters: VisitsFilter) => void;
|
onChange: (filters: VisitsFilter) => void;
|
||||||
selected?: VisitsFilter;
|
selected?: VisitsFilter;
|
||||||
|
@ -26,7 +21,7 @@ export const VisitsFilterDropdown = (
|
||||||
const { orphanVisitsType, excludeBots = false } = selected;
|
const { orphanVisitsType, excludeBots = false } = selected;
|
||||||
const propsForOrphanVisitsTypeItem = (type: OrphanVisitType): DropdownItemProps => ({
|
const propsForOrphanVisitsTypeItem = (type: OrphanVisitType): DropdownItemProps => ({
|
||||||
active: orphanVisitsType === type,
|
active: orphanVisitsType === type,
|
||||||
onClick: () => onChange({ ...selected, orphanVisitsType: type }),
|
onClick: () => onChange({ ...selected, orphanVisitsType: type === selected?.orphanVisitsType ? undefined : type }),
|
||||||
});
|
});
|
||||||
const onBotsClick = () => onChange({ ...selected, excludeBots: !selected?.excludeBots });
|
const onBotsClick = () => onChange({ ...selected, excludeBots: !selected?.excludeBots });
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,17 @@
|
||||||
import { Action, Dispatch } from 'redux';
|
import { Action, Dispatch } from 'redux';
|
||||||
import { Visit, VisitsInfo, VisitsLoadFailedAction, VisitsLoadProgressChangedAction } from '../types';
|
import {
|
||||||
|
OrphanVisit,
|
||||||
|
OrphanVisitType,
|
||||||
|
Visit,
|
||||||
|
VisitsInfo,
|
||||||
|
VisitsLoadFailedAction,
|
||||||
|
VisitsLoadProgressChangedAction,
|
||||||
|
} from '../types';
|
||||||
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
|
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
|
||||||
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||||
import { GetState } from '../../container/types';
|
import { GetState } from '../../container/types';
|
||||||
|
import { ShlinkVisitsParams } from '../../api/types';
|
||||||
|
import { isOrphanVisit } from '../types/helpers';
|
||||||
import { getVisitsWithLoader } from './common';
|
import { getVisitsWithLoader } from './common';
|
||||||
import { CREATE_VISITS, CreateVisitsAction } from './visitCreation';
|
import { CREATE_VISITS, CreateVisitsAction } from './visitCreation';
|
||||||
|
|
||||||
|
@ -48,12 +57,20 @@ export default buildReducer<VisitsInfo, OrphanVisitsCombinedAction>({
|
||||||
},
|
},
|
||||||
}, initialState);
|
}, initialState);
|
||||||
|
|
||||||
export const getOrphanVisits = (buildShlinkApiClient: ShlinkApiClientBuilder) => (query = {}) => async (
|
const matchesType = (visit: OrphanVisit, orphanVisitsType?: OrphanVisitType) =>
|
||||||
dispatch: Dispatch,
|
!orphanVisitsType || orphanVisitsType === visit.type;
|
||||||
getState: GetState,
|
|
||||||
) => {
|
export const getOrphanVisits = (buildShlinkApiClient: ShlinkApiClientBuilder) => (
|
||||||
|
query: ShlinkVisitsParams = {},
|
||||||
|
orphanVisitsType?: OrphanVisitType,
|
||||||
|
) => async (dispatch: Dispatch, getState: GetState) => {
|
||||||
const { getOrphanVisits } = buildShlinkApiClient(getState);
|
const { getOrphanVisits } = buildShlinkApiClient(getState);
|
||||||
const visitsLoader = async (page: number, itemsPerPage: number) => getOrphanVisits({ ...query, page, itemsPerPage });
|
const visitsLoader = async (page: number, itemsPerPage: number) => getOrphanVisits({ ...query, page, itemsPerPage })
|
||||||
|
.then((result) => {
|
||||||
|
const visits = result.data.filter((visit) => isOrphanVisit(visit) && matchesType(visit, orphanVisitsType));
|
||||||
|
|
||||||
|
return { ...result, data: visits };
|
||||||
|
});
|
||||||
const shouldCancel = () => getState().orphanVisits.cancelLoad;
|
const shouldCancel = () => getState().orphanVisits.cancelLoad;
|
||||||
const actionMap = {
|
const actionMap = {
|
||||||
start: GET_ORPHAN_VISITS_START,
|
start: GET_ORPHAN_VISITS_START,
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { ShortUrlIdentifier } from '../../short-urls/data';
|
||||||
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
|
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
|
||||||
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||||
import { GetState } from '../../container/types';
|
import { GetState } from '../../container/types';
|
||||||
import { OptionalString } from '../../utils/utils';
|
import { ShlinkVisitsParams } from '../../api/types';
|
||||||
import { getVisitsWithLoader } from './common';
|
import { getVisitsWithLoader } from './common';
|
||||||
import { CREATE_VISITS, CreateVisitsAction } from './visitCreation';
|
import { CREATE_VISITS, CreateVisitsAction } from './visitCreation';
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ export default buildReducer<ShortUrlVisits, ShortUrlVisitsCombinedAction>({
|
||||||
|
|
||||||
export const getShortUrlVisits = (buildShlinkApiClient: ShlinkApiClientBuilder) => (
|
export const getShortUrlVisits = (buildShlinkApiClient: ShlinkApiClientBuilder) => (
|
||||||
shortCode: string,
|
shortCode: string,
|
||||||
query: { domain?: OptionalString } = {},
|
query: ShlinkVisitsParams = {},
|
||||||
) => async (dispatch: Dispatch, getState: GetState) => {
|
) => async (dispatch: Dispatch, getState: GetState) => {
|
||||||
const { getShortUrlVisits } = buildShlinkApiClient(getState);
|
const { getShortUrlVisits } = buildShlinkApiClient(getState);
|
||||||
const visitsLoader = async (page: number, itemsPerPage: number) => getShortUrlVisits(
|
const visitsLoader = async (page: number, itemsPerPage: number) => getShortUrlVisits(
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { Visit, VisitsInfo, VisitsLoadFailedAction, VisitsLoadProgressChangedAct
|
||||||
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
|
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
|
||||||
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||||
import { GetState } from '../../container/types';
|
import { GetState } from '../../container/types';
|
||||||
|
import { ShlinkVisitsParams } from '../../api/types';
|
||||||
import { getVisitsWithLoader } from './common';
|
import { getVisitsWithLoader } from './common';
|
||||||
import { CREATE_VISITS, CreateVisitsAction } from './visitCreation';
|
import { CREATE_VISITS, CreateVisitsAction } from './visitCreation';
|
||||||
|
|
||||||
|
@ -56,10 +57,10 @@ export default buildReducer<TagVisits, TagsVisitsCombinedAction>({
|
||||||
},
|
},
|
||||||
}, initialState);
|
}, initialState);
|
||||||
|
|
||||||
export const getTagVisits = (buildShlinkApiClient: ShlinkApiClientBuilder) => (tag: string, query = {}) => async (
|
export const getTagVisits = (buildShlinkApiClient: ShlinkApiClientBuilder) => (
|
||||||
dispatch: Dispatch,
|
tag: string,
|
||||||
getState: GetState,
|
query: ShlinkVisitsParams = {},
|
||||||
) => {
|
) => async (dispatch: Dispatch, getState: GetState) => {
|
||||||
const { getTagVisits } = buildShlinkApiClient(getState);
|
const { getTagVisits } = buildShlinkApiClient(getState);
|
||||||
const visitsLoader = async (page: number, itemsPerPage: number) => getTagVisits(
|
const visitsLoader = async (page: number, itemsPerPage: number) => getTagVisits(
|
||||||
tag,
|
tag,
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { countBy, filter, groupBy, pipe, prop } from 'ramda';
|
import { countBy, groupBy, pipe, prop } from 'ramda';
|
||||||
import { normalizeVisits } from '../services/VisitsParser';
|
import { formatIsoDate } from '../../utils/helpers/date';
|
||||||
import { VisitsFilter } from '../helpers/VisitsFilterDropdown';
|
import { ShlinkVisitsParams } from '../../api/types';
|
||||||
import { hasValue } from '../../utils/utils';
|
import { CreateVisit, NormalizedOrphanVisit, NormalizedVisit, OrphanVisit, Stats, Visit, VisitsParams } from './index';
|
||||||
import { Visit, OrphanVisit, CreateVisit, NormalizedVisit, NormalizedOrphanVisit, Stats } from './index';
|
|
||||||
|
|
||||||
export const isOrphanVisit = (visit: Visit): visit is OrphanVisit => visit.hasOwnProperty('visitedUrl');
|
export const isOrphanVisit = (visit: Visit): visit is OrphanVisit => visit.hasOwnProperty('visitedUrl');
|
||||||
|
|
||||||
|
@ -29,19 +28,10 @@ export const highlightedVisitsToStats = <T extends NormalizedVisit>(
|
||||||
property: HighlightableProps<T>,
|
property: HighlightableProps<T>,
|
||||||
): Stats => countBy(prop(property) as any, highlightedVisits);
|
): Stats => countBy(prop(property) as any, highlightedVisits);
|
||||||
|
|
||||||
export const normalizeAndFilterVisits = (visits: Visit[], filters: VisitsFilter) => pipe(
|
export const toApiParams = ({ page, itemsPerPage, filter, dateRange }: VisitsParams): ShlinkVisitsParams => {
|
||||||
normalizeVisits,
|
const startDate = (dateRange?.startDate && formatIsoDate(dateRange?.startDate)) ?? undefined;
|
||||||
filter((normalizedVisit: NormalizedVisit) => {
|
const endDate = (dateRange?.endDate && formatIsoDate(dateRange?.endDate)) ?? undefined;
|
||||||
if (!hasValue(filters)) {
|
const excludeBots = filter?.excludeBots || undefined; // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { orphanVisitsType, excludeBots } = filters;
|
return { page, itemsPerPage, startDate, endDate, excludeBots };
|
||||||
|
};
|
||||||
if (orphanVisitsType && orphanVisitsType !== (normalizedVisit as NormalizedOrphanVisit).type) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return !(excludeBots && normalizedVisit.potentialBot);
|
|
||||||
}),
|
|
||||||
)(visits);
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Action } from 'redux';
|
import { Action } from 'redux';
|
||||||
import { ShortUrl } from '../../short-urls/data';
|
import { ShortUrl } from '../../short-urls/data';
|
||||||
import { ProblemDetailsError } from '../../api/types';
|
import { ProblemDetailsError } from '../../api/types';
|
||||||
|
import { DateRange } from '../../utils/dates/types';
|
||||||
|
|
||||||
export interface VisitsInfo {
|
export interface VisitsInfo {
|
||||||
visits: Visit[];
|
visits: Visit[];
|
||||||
|
@ -94,3 +95,15 @@ export interface VisitsStats {
|
||||||
citiesForMap: Record<string, CityStats>;
|
citiesForMap: Record<string, CityStats>;
|
||||||
visitedUrls: Stats;
|
visitedUrls: Stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface VisitsFilter {
|
||||||
|
orphanVisitsType?: OrphanVisitType | undefined;
|
||||||
|
excludeBots?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VisitsParams {
|
||||||
|
page?: number;
|
||||||
|
itemsPerPage?: number;
|
||||||
|
dateRange?: DateRange;
|
||||||
|
filter?: VisitsFilter;
|
||||||
|
}
|
||||||
|
|
|
@ -37,7 +37,6 @@ describe('<OrphanVisits />', () => {
|
||||||
|
|
||||||
expect(stats).toHaveLength(1);
|
expect(stats).toHaveLength(1);
|
||||||
expect(header).toHaveLength(1);
|
expect(header).toHaveLength(1);
|
||||||
expect(stats.prop('getVisits')).toEqual(getOrphanVisits);
|
|
||||||
expect(stats.prop('cancelGetVisits')).toEqual(cancelGetOrphanVisits);
|
expect(stats.prop('cancelGetVisits')).toEqual(cancelGetOrphanVisits);
|
||||||
expect(stats.prop('visitsInfo')).toEqual(orphanVisits);
|
expect(stats.prop('visitsInfo')).toEqual(orphanVisits);
|
||||||
expect(stats.prop('baseUrl')).toEqual('the_base_url');
|
expect(stats.prop('baseUrl')).toEqual('the_base_url');
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { shallow, ShallowWrapper } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { DropdownItem } from 'reactstrap';
|
import { DropdownItem } from 'reactstrap';
|
||||||
import { OrphanVisitType } from '../../../src/visits/types';
|
import { OrphanVisitType, VisitsFilter } from '../../../src/visits/types';
|
||||||
import { VisitsFilter, VisitsFilterDropdown } from '../../../src/visits/helpers/VisitsFilterDropdown';
|
import { VisitsFilterDropdown } from '../../../src/visits/helpers/VisitsFilterDropdown';
|
||||||
|
|
||||||
describe('<VisitsFilterDropdown />', () => {
|
describe('<VisitsFilterDropdown />', () => {
|
||||||
let wrapper: ShallowWrapper;
|
let wrapper: ShallowWrapper;
|
||||||
|
|
|
@ -110,9 +110,9 @@ describe('orphanVisitsReducer', () => {
|
||||||
[ undefined ],
|
[ undefined ],
|
||||||
[{}],
|
[{}],
|
||||||
])('dispatches start and success when promise is resolved', async (query) => {
|
])('dispatches start and success when promise is resolved', async (query) => {
|
||||||
const visits = visitsMocks;
|
const visits = visitsMocks.map((visit) => ({ ...visit, visitedUrl: '' }));
|
||||||
const ShlinkApiClient = buildApiClientMock(Promise.resolve({
|
const ShlinkApiClient = buildApiClientMock(Promise.resolve({
|
||||||
data: visitsMocks,
|
data: visits,
|
||||||
pagination: {
|
pagination: {
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pagesCount: 1,
|
pagesCount: 1,
|
||||||
|
|
Loading…
Reference in a new issue