Added graph with orphan visits grouped by visited URL

This commit is contained in:
Alejandro Celaya 2021-03-28 20:56:16 +02:00
parent d6bb718672
commit f0a04ced75
5 changed files with 56 additions and 26 deletions

View file

@ -1,4 +1,4 @@
import { countBy, filter, isEmpty, pipe, prop, propEq, values } from 'ramda';
import { isEmpty, propEq, values } from 'ramda';
import { useState, useEffect, useMemo, FC } from 'react';
import { Button, Card, Nav, NavLink, Progress, Row } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@ -19,11 +19,12 @@ import SortableBarGraph from './helpers/SortableBarGraph';
import GraphCard from './helpers/GraphCard';
import LineChartCard from './helpers/LineChartCard';
import VisitsTable from './VisitsTable';
import { NormalizedOrphanVisit, NormalizedVisit, OrphanVisitType, Stats, Visit, VisitsInfo } from './types';
import { NormalizedOrphanVisit, NormalizedVisit, OrphanVisitType, VisitsInfo } from './types';
import OpenMapModalBtn from './helpers/OpenMapModalBtn';
import { normalizeVisits, processStatsFromVisits } from './services/VisitsParser';
import { processStatsFromVisits } from './services/VisitsParser';
import { OrphanVisitTypeDropdown } from './helpers/OrphanVisitTypeDropdown';
import './VisitsStats.scss';
import { HighlightableProps, highlightedVisitsToStats, normalizeAndFilterVisits } from './types/helpers';
export interface VisitsStatsProps {
getVisits: (params: Partial<ShlinkVisitsParams>) => void;
@ -42,7 +43,6 @@ interface VisitsNavLinkProps {
icon: IconDefinition;
}
type HighlightableProps = 'referer' | 'country' | 'city';
type Section = 'byTime' | 'byContext' | 'byLocation' | 'list';
const sections: Record<Section, VisitsNavLinkProps> = {
@ -52,14 +52,6 @@ const sections: Record<Section, VisitsNavLinkProps> = {
list: { title: 'List', subPath: '/list', icon: faList },
};
const highlightedVisitsToStats = (highlightedVisits: NormalizedVisit[], property: HighlightableProps): Stats =>
countBy(prop(property), highlightedVisits);
const normalizeAndFilterVisits = (visits: Visit[], type: OrphanVisitType | undefined) => pipe(
normalizeVisits,
filter((normalizedVisit) => type === undefined || (normalizedVisit as NormalizedOrphanVisit).type === type),
)(visits);
let selectedBar: string | undefined;
const VisitsNavLink: FC<VisitsNavLinkProps & { to: string }> = ({ subPath, title, icon, to }) => (
@ -94,7 +86,7 @@ const VisitsStats: FC<VisitsStatsProps> = (
() => normalizeAndFilterVisits(visits, orphanVisitType),
[ visits, orphanVisitType ],
);
const { os, browsers, referrers, countries, cities, citiesForMap } = useMemo(
const { os, browsers, referrers, countries, cities, citiesForMap, visitedUrls } = useMemo(
() => processStatsFromVisits(normalizedVisits),
[ normalizedVisits ],
);
@ -104,7 +96,7 @@ const VisitsStats: FC<VisitsStatsProps> = (
selectedBar = undefined;
setHighlightedVisits(selectedVisits);
};
const highlightVisitsForProp = (prop: HighlightableProps) => (value: string) => {
const highlightVisitsForProp = (prop: HighlightableProps<NormalizedOrphanVisit>) => (value: string) => {
const newSelectedBar = `${prop}_${value}`;
if (selectedBar === newSelectedBar) {
@ -112,7 +104,7 @@ const VisitsStats: FC<VisitsStatsProps> = (
setHighlightedLabel(undefined);
selectedBar = undefined;
} else {
setHighlightedVisits(normalizedVisits.filter(propEq(prop, value)));
setHighlightedVisits((normalizedVisits as NormalizedOrphanVisit[]).filter(propEq(prop, value)));
setHighlightedLabel(value);
selectedBar = newSelectedBar;
}
@ -198,11 +190,14 @@ const VisitsStats: FC<VisitsStatsProps> = (
<div className="mt-4 col-lg-6">
<SortableBarGraph
title="Visited URLs"
stats={{}}
stats={visitedUrls}
highlightedLabel={highlightedLabel}
highlightedStats={highlightedVisitsToStats(highlightedVisits, 'visitedUrl')}
sortingItems={{
visitedUrl: 'Visited URL',
amount: 'Visits amount',
}}
onClick={highlightVisitsForProp('visitedUrl')}
/>
</div>
)}

View file

@ -93,11 +93,8 @@ const VisitsTable = ({
useEffect(() => {
setPage(1);
if (isFirstLoad.current) {
isFirstLoad.current = false;
} else {
setSelectedVisits([]);
}
!isFirstLoad.current && setSelectedVisits([]);
isFirstLoad.current = false;
}, [ searchTerm ]);
return (
@ -157,7 +154,7 @@ const VisitsTable = ({
</td>
</tr>
)}
{resultSet.visitsGroups[page - 1] && resultSet.visitsGroups[page - 1].map((visit, index) => {
{resultSet.visitsGroups[page - 1]?.map((visit, index) => {
const isSelected = selectedVisits.includes(visit);
return (

View file

@ -2,7 +2,7 @@ import { isNil, map } from 'ramda';
import { extractDomain, parseUserAgent } from '../../utils/helpers/visits';
import { hasValue } from '../../utils/utils';
import { CityStats, NormalizedVisit, Stats, Visit, VisitsStats } from '../types';
import { isOrphanVisit } from '../types/helpers';
import { isNormalizedOrphanVisit, isOrphanVisit } from '../types/helpers';
const visitHasProperty = (visit: NormalizedVisit, propertyName: keyof NormalizedVisit) =>
!isNil(visit) && hasValue(visit[propertyName]);
@ -54,6 +54,16 @@ const updateCitiesForMapForVisit = (citiesForMapStats: Record<string, CityStats>
citiesForMapStats[city] = currentCity;
};
const updateVisitedUrlsForVisit = (visitedUrlsStats: Stats, visit: NormalizedVisit) => {
if (!isNormalizedOrphanVisit(visit)) {
return;
}
const { visitedUrl } = visit;
visitedUrlsStats[visitedUrl] = (visitedUrlsStats[visitedUrl] || 0) + 1;
};
export const processStatsFromVisits = (visits: NormalizedVisit[]) => visits.reduce(
(stats: VisitsStats, visit: NormalizedVisit) => {
// We mutate the original object because it has a big performance impact when large data sets are processed
@ -63,10 +73,11 @@ export const processStatsFromVisits = (visits: NormalizedVisit[]) => visits.redu
updateCountriesStatsForVisit(stats.countries, visit);
updateCitiesStatsForVisit(stats.cities, visit);
updateCitiesForMapForVisit(stats.citiesForMap, visit);
updateVisitedUrlsForVisit(stats.visitedUrls, visit);
return stats;
},
{ os: {}, browsers: {}, referrers: {}, countries: {}, cities: {}, citiesForMap: {} },
{ os: {}, browsers: {}, referrers: {}, countries: {}, cities: {}, citiesForMap: {}, visitedUrls: {} },
);
export const normalizeVisits = map((visit: Visit): NormalizedVisit => {

View file

@ -1,8 +1,20 @@
import { groupBy, pipe } from 'ramda';
import { Visit, OrphanVisit, CreateVisit } from './index';
import { countBy, filter, groupBy, pipe, prop } from 'ramda';
import { normalizeVisits } from '../services/VisitsParser';
import {
Visit,
OrphanVisit,
CreateVisit,
NormalizedVisit,
NormalizedOrphanVisit,
Stats,
OrphanVisitType,
} from './index';
export const isOrphanVisit = (visit: Visit): visit is OrphanVisit => visit.hasOwnProperty('visitedUrl');
export const isNormalizedOrphanVisit = (visit: NormalizedVisit): visit is NormalizedOrphanVisit =>
visit.hasOwnProperty('visitedUrl');
export interface GroupedNewVisits {
orphanVisits: CreateVisit[];
regularVisits: CreateVisit[];
@ -13,3 +25,17 @@ export const groupNewVisitsByType = pipe(
// @ts-expect-error Type declaration on groupBy is not correct. It can return undefined props
(result): GroupedNewVisits => ({ orphanVisits: [], regularVisits: [], ...result }),
);
export type HighlightableProps<T extends NormalizedVisit> = T extends NormalizedOrphanVisit
? ('referer' | 'country' | 'city' | 'visitedUrl')
: ('referer' | 'country' | 'city');
export const highlightedVisitsToStats = <T extends NormalizedVisit>(
highlightedVisits: T[],
property: HighlightableProps<T>,
): Stats => countBy(prop(property) as any, highlightedVisits);
export const normalizeAndFilterVisits = (visits: Visit[], type: OrphanVisitType | undefined) => pipe(
normalizeVisits,
filter((normalizedVisit) => type === undefined || (normalizedVisit as NormalizedOrphanVisit).type === type),
)(visits);

View file

@ -90,4 +90,5 @@ export interface VisitsStats {
countries: Stats;
cities: Stats;
citiesForMap: Record<string, CityStats>;
visitedUrls: Stats;
}