diff --git a/src/visits/VisitsStats.tsx b/src/visits/VisitsStats.tsx index 8e5da643..d94d14bb 100644 --- a/src/visits/VisitsStats.tsx +++ b/src/visits/VisitsStats.tsx @@ -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) => void; @@ -42,7 +43,6 @@ interface VisitsNavLinkProps { icon: IconDefinition; } -type HighlightableProps = 'referer' | 'country' | 'city'; type Section = 'byTime' | 'byContext' | 'byLocation' | 'list'; const sections: Record = { @@ -52,14 +52,6 @@ const sections: Record = { 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 = ({ subPath, title, icon, to }) => ( @@ -94,7 +86,7 @@ const VisitsStats: FC = ( () => 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 = ( selectedBar = undefined; setHighlightedVisits(selectedVisits); }; - const highlightVisitsForProp = (prop: HighlightableProps) => (value: string) => { + const highlightVisitsForProp = (prop: HighlightableProps) => (value: string) => { const newSelectedBar = `${prop}_${value}`; if (selectedBar === newSelectedBar) { @@ -112,7 +104,7 @@ const VisitsStats: FC = ( 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 = (
)} diff --git a/src/visits/VisitsTable.tsx b/src/visits/VisitsTable.tsx index e5bcd849..9eb0eef2 100644 --- a/src/visits/VisitsTable.tsx +++ b/src/visits/VisitsTable.tsx @@ -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 = ({ )} - {resultSet.visitsGroups[page - 1] && resultSet.visitsGroups[page - 1].map((visit, index) => { + {resultSet.visitsGroups[page - 1]?.map((visit, index) => { const isSelected = selectedVisits.includes(visit); return ( diff --git a/src/visits/services/VisitsParser.ts b/src/visits/services/VisitsParser.ts index 824fb796..9cef1faa 100644 --- a/src/visits/services/VisitsParser.ts +++ b/src/visits/services/VisitsParser.ts @@ -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 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 => { diff --git a/src/visits/types/helpers.ts b/src/visits/types/helpers.ts index fdc45733..d2691504 100644 --- a/src/visits/types/helpers.ts +++ b/src/visits/types/helpers.ts @@ -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 NormalizedOrphanVisit + ? ('referer' | 'country' | 'city' | 'visitedUrl') + : ('referer' | 'country' | 'city'); + +export const highlightedVisitsToStats = ( + highlightedVisits: T[], + property: HighlightableProps, +): 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); diff --git a/src/visits/types/index.ts b/src/visits/types/index.ts index bb8f7f68..0e2879d5 100644 --- a/src/visits/types/index.ts +++ b/src/visits/types/index.ts @@ -90,4 +90,5 @@ export interface VisitsStats { countries: Stats; cities: Stats; citiesForMap: Record; + visitedUrls: Stats; }