mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-12-11 18:41:12 -06:00
Added graph with orphan visits grouped by visited URL
This commit is contained in:
parent
d6bb718672
commit
f0a04ced75
@ -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 { useState, useEffect, useMemo, FC } from 'react';
|
||||||
import { Button, Card, Nav, NavLink, Progress, Row } from 'reactstrap';
|
import { Button, Card, Nav, NavLink, Progress, Row } from 'reactstrap';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
@ -19,11 +19,12 @@ 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, OrphanVisitType, Stats, Visit, VisitsInfo } from './types';
|
import { NormalizedOrphanVisit, NormalizedVisit, OrphanVisitType, VisitsInfo } from './types';
|
||||||
import OpenMapModalBtn from './helpers/OpenMapModalBtn';
|
import OpenMapModalBtn from './helpers/OpenMapModalBtn';
|
||||||
import { normalizeVisits, processStatsFromVisits } from './services/VisitsParser';
|
import { processStatsFromVisits } from './services/VisitsParser';
|
||||||
import { OrphanVisitTypeDropdown } from './helpers/OrphanVisitTypeDropdown';
|
import { OrphanVisitTypeDropdown } from './helpers/OrphanVisitTypeDropdown';
|
||||||
import './VisitsStats.scss';
|
import './VisitsStats.scss';
|
||||||
|
import { HighlightableProps, highlightedVisitsToStats, normalizeAndFilterVisits } from './types/helpers';
|
||||||
|
|
||||||
export interface VisitsStatsProps {
|
export interface VisitsStatsProps {
|
||||||
getVisits: (params: Partial<ShlinkVisitsParams>) => void;
|
getVisits: (params: Partial<ShlinkVisitsParams>) => void;
|
||||||
@ -42,7 +43,6 @@ interface VisitsNavLinkProps {
|
|||||||
icon: IconDefinition;
|
icon: IconDefinition;
|
||||||
}
|
}
|
||||||
|
|
||||||
type HighlightableProps = 'referer' | 'country' | 'city';
|
|
||||||
type Section = 'byTime' | 'byContext' | 'byLocation' | 'list';
|
type Section = 'byTime' | 'byContext' | 'byLocation' | 'list';
|
||||||
|
|
||||||
const sections: Record<Section, VisitsNavLinkProps> = {
|
const sections: Record<Section, VisitsNavLinkProps> = {
|
||||||
@ -52,14 +52,6 @@ const sections: Record<Section, VisitsNavLinkProps> = {
|
|||||||
list: { title: 'List', subPath: '/list', icon: faList },
|
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;
|
let selectedBar: string | undefined;
|
||||||
|
|
||||||
const VisitsNavLink: FC<VisitsNavLinkProps & { to: string }> = ({ subPath, title, icon, to }) => (
|
const VisitsNavLink: FC<VisitsNavLinkProps & { to: string }> = ({ subPath, title, icon, to }) => (
|
||||||
@ -94,7 +86,7 @@ const VisitsStats: FC<VisitsStatsProps> = (
|
|||||||
() => normalizeAndFilterVisits(visits, orphanVisitType),
|
() => normalizeAndFilterVisits(visits, orphanVisitType),
|
||||||
[ visits, orphanVisitType ],
|
[ visits, orphanVisitType ],
|
||||||
);
|
);
|
||||||
const { os, browsers, referrers, countries, cities, citiesForMap } = useMemo(
|
const { os, browsers, referrers, countries, cities, citiesForMap, visitedUrls } = useMemo(
|
||||||
() => processStatsFromVisits(normalizedVisits),
|
() => processStatsFromVisits(normalizedVisits),
|
||||||
[ normalizedVisits ],
|
[ normalizedVisits ],
|
||||||
);
|
);
|
||||||
@ -104,7 +96,7 @@ const VisitsStats: FC<VisitsStatsProps> = (
|
|||||||
selectedBar = undefined;
|
selectedBar = undefined;
|
||||||
setHighlightedVisits(selectedVisits);
|
setHighlightedVisits(selectedVisits);
|
||||||
};
|
};
|
||||||
const highlightVisitsForProp = (prop: HighlightableProps) => (value: string) => {
|
const highlightVisitsForProp = (prop: HighlightableProps<NormalizedOrphanVisit>) => (value: string) => {
|
||||||
const newSelectedBar = `${prop}_${value}`;
|
const newSelectedBar = `${prop}_${value}`;
|
||||||
|
|
||||||
if (selectedBar === newSelectedBar) {
|
if (selectedBar === newSelectedBar) {
|
||||||
@ -112,7 +104,7 @@ const VisitsStats: FC<VisitsStatsProps> = (
|
|||||||
setHighlightedLabel(undefined);
|
setHighlightedLabel(undefined);
|
||||||
selectedBar = undefined;
|
selectedBar = undefined;
|
||||||
} else {
|
} else {
|
||||||
setHighlightedVisits(normalizedVisits.filter(propEq(prop, value)));
|
setHighlightedVisits((normalizedVisits as NormalizedOrphanVisit[]).filter(propEq(prop, value)));
|
||||||
setHighlightedLabel(value);
|
setHighlightedLabel(value);
|
||||||
selectedBar = newSelectedBar;
|
selectedBar = newSelectedBar;
|
||||||
}
|
}
|
||||||
@ -198,11 +190,14 @@ const VisitsStats: FC<VisitsStatsProps> = (
|
|||||||
<div className="mt-4 col-lg-6">
|
<div className="mt-4 col-lg-6">
|
||||||
<SortableBarGraph
|
<SortableBarGraph
|
||||||
title="Visited URLs"
|
title="Visited URLs"
|
||||||
stats={{}}
|
stats={visitedUrls}
|
||||||
|
highlightedLabel={highlightedLabel}
|
||||||
|
highlightedStats={highlightedVisitsToStats(highlightedVisits, 'visitedUrl')}
|
||||||
sortingItems={{
|
sortingItems={{
|
||||||
visitedUrl: 'Visited URL',
|
visitedUrl: 'Visited URL',
|
||||||
amount: 'Visits amount',
|
amount: 'Visits amount',
|
||||||
}}
|
}}
|
||||||
|
onClick={highlightVisitsForProp('visitedUrl')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -93,11 +93,8 @@ const VisitsTable = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPage(1);
|
setPage(1);
|
||||||
|
|
||||||
if (isFirstLoad.current) {
|
!isFirstLoad.current && setSelectedVisits([]);
|
||||||
isFirstLoad.current = false;
|
isFirstLoad.current = false;
|
||||||
} else {
|
|
||||||
setSelectedVisits([]);
|
|
||||||
}
|
|
||||||
}, [ searchTerm ]);
|
}, [ searchTerm ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -157,7 +154,7 @@ const VisitsTable = ({
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
{resultSet.visitsGroups[page - 1] && resultSet.visitsGroups[page - 1].map((visit, index) => {
|
{resultSet.visitsGroups[page - 1]?.map((visit, index) => {
|
||||||
const isSelected = selectedVisits.includes(visit);
|
const isSelected = selectedVisits.includes(visit);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { isNil, map } from 'ramda';
|
|||||||
import { extractDomain, parseUserAgent } from '../../utils/helpers/visits';
|
import { extractDomain, parseUserAgent } from '../../utils/helpers/visits';
|
||||||
import { hasValue } from '../../utils/utils';
|
import { hasValue } from '../../utils/utils';
|
||||||
import { CityStats, NormalizedVisit, Stats, Visit, VisitsStats } from '../types';
|
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) =>
|
const visitHasProperty = (visit: NormalizedVisit, propertyName: keyof NormalizedVisit) =>
|
||||||
!isNil(visit) && hasValue(visit[propertyName]);
|
!isNil(visit) && hasValue(visit[propertyName]);
|
||||||
@ -54,6 +54,16 @@ const updateCitiesForMapForVisit = (citiesForMapStats: Record<string, CityStats>
|
|||||||
citiesForMapStats[city] = currentCity;
|
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(
|
export const processStatsFromVisits = (visits: NormalizedVisit[]) => visits.reduce(
|
||||||
(stats: VisitsStats, visit: NormalizedVisit) => {
|
(stats: VisitsStats, visit: NormalizedVisit) => {
|
||||||
// We mutate the original object because it has a big performance impact when large data sets are processed
|
// 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);
|
updateCountriesStatsForVisit(stats.countries, visit);
|
||||||
updateCitiesStatsForVisit(stats.cities, visit);
|
updateCitiesStatsForVisit(stats.cities, visit);
|
||||||
updateCitiesForMapForVisit(stats.citiesForMap, visit);
|
updateCitiesForMapForVisit(stats.citiesForMap, visit);
|
||||||
|
updateVisitedUrlsForVisit(stats.visitedUrls, visit);
|
||||||
|
|
||||||
return stats;
|
return stats;
|
||||||
},
|
},
|
||||||
{ os: {}, browsers: {}, referrers: {}, countries: {}, cities: {}, citiesForMap: {} },
|
{ os: {}, browsers: {}, referrers: {}, countries: {}, cities: {}, citiesForMap: {}, visitedUrls: {} },
|
||||||
);
|
);
|
||||||
|
|
||||||
export const normalizeVisits = map((visit: Visit): NormalizedVisit => {
|
export const normalizeVisits = map((visit: Visit): NormalizedVisit => {
|
||||||
|
|||||||
@ -1,8 +1,20 @@
|
|||||||
import { groupBy, pipe } from 'ramda';
|
import { countBy, filter, groupBy, pipe, prop } from 'ramda';
|
||||||
import { Visit, OrphanVisit, CreateVisit } from './index';
|
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 isOrphanVisit = (visit: Visit): visit is OrphanVisit => visit.hasOwnProperty('visitedUrl');
|
||||||
|
|
||||||
|
export const isNormalizedOrphanVisit = (visit: NormalizedVisit): visit is NormalizedOrphanVisit =>
|
||||||
|
visit.hasOwnProperty('visitedUrl');
|
||||||
|
|
||||||
export interface GroupedNewVisits {
|
export interface GroupedNewVisits {
|
||||||
orphanVisits: CreateVisit[];
|
orphanVisits: CreateVisit[];
|
||||||
regularVisits: 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
|
// @ts-expect-error Type declaration on groupBy is not correct. It can return undefined props
|
||||||
(result): GroupedNewVisits => ({ orphanVisits: [], regularVisits: [], ...result }),
|
(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;
|
countries: Stats;
|
||||||
cities: Stats;
|
cities: Stats;
|
||||||
citiesForMap: Record<string, CityStats>;
|
citiesForMap: Record<string, CityStats>;
|
||||||
|
visitedUrls: Stats;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user