mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-10 18:27:25 +03:00
Added graph with orphan visits grouped by visited URL
This commit is contained in:
parent
d6bb718672
commit
f0a04ced75
5 changed files with 56 additions and 26 deletions
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -90,4 +90,5 @@ export interface VisitsStats {
|
|||
countries: Stats;
|
||||
cities: Stats;
|
||||
citiesForMap: Record<string, CityStats>;
|
||||
visitedUrls: Stats;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue