import { useEffect, useMemo, useState, useRef } from 'react'; import classNames from 'classnames'; import { min, splitEvery } from 'ramda'; import { faCheck as checkIcon, faRobot as botIcon } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { UncontrolledTooltip } from 'reactstrap'; import { SimplePaginator } from '../common/SimplePaginator'; import { SearchField } from '../utils/SearchField'; import { determineOrderDir, Order, sortList } from '../utils/helpers/ordering'; import { prettify } from '../utils/helpers/numbers'; import { Time } from '../utils/dates/Time'; import { TableOrderIcon } from '../utils/table/TableOrderIcon'; import { MediaMatcher } from '../utils/types'; import { NormalizedOrphanVisit, NormalizedVisit } from './types'; import './VisitsTable.scss'; export interface VisitsTableProps { visits: NormalizedVisit[]; selectedVisits?: NormalizedVisit[]; setSelectedVisits: (visits: NormalizedVisit[]) => void; matchMedia?: MediaMatcher; isOrphanVisits?: boolean; } type OrderableFields = 'date' | 'country' | 'city' | 'browser' | 'os' | 'referer' | 'visitedUrl' | 'potentialBot'; type VisitsOrder = Order; const PAGE_SIZE = 20; const visitMatchesSearch = ({ browser, os, referer, country, city, ...rest }: NormalizedVisit, searchTerm: string) => `${browser} ${os} ${referer} ${country} ${city} ${(rest as NormalizedOrphanVisit).visitedUrl}`.toLowerCase().includes( searchTerm.toLowerCase(), ); const searchVisits = (searchTerm: string, visits: NormalizedVisit[]) => visits.filter((visit) => visitMatchesSearch(visit, searchTerm)); const sortVisits = (order: VisitsOrder, visits: NormalizedVisit[]) => sortList(visits, order as any); const calculateVisits = (allVisits: NormalizedVisit[], searchTerm: string | undefined, order: VisitsOrder) => { const filteredVisits = searchTerm ? searchVisits(searchTerm, allVisits) : [...allVisits]; const sortedVisits = sortVisits(order, filteredVisits); const total = sortedVisits.length; const visitsGroups = splitEvery(PAGE_SIZE, sortedVisits); return { visitsGroups, total }; }; export const VisitsTable = ({ visits, selectedVisits = [], setSelectedVisits, matchMedia = window.matchMedia, isOrphanVisits = false, }: VisitsTableProps) => { const headerCellsClass = 'visits-table__header-cell visits-table__sticky'; const matchMobile = () => matchMedia('(max-width: 767px)').matches; const [isMobileDevice, setIsMobileDevice] = useState(matchMobile()); const [searchTerm, setSearchTerm] = useState(undefined); const [order, setOrder] = useState({}); const resultSet = useMemo(() => calculateVisits(visits, searchTerm, order), [searchTerm, order]); const isFirstLoad = useRef(true); const [page, setPage] = useState(1); const end = page * PAGE_SIZE; const start = end - PAGE_SIZE; const fullSizeColSpan = 8 + Number(isOrphanVisits); const orderByColumn = (field: OrderableFields) => () => setOrder({ field, dir: determineOrderDir(field, order.field, order.dir) }); const renderOrderIcon = (field: OrderableFields) => ; useEffect(() => { const listener = () => setIsMobileDevice(matchMobile()); window.addEventListener('resize', listener); return () => window.removeEventListener('resize', listener); }, []); useEffect(() => { setPage(1); !isFirstLoad.current && setSelectedVisits([]); isFirstLoad.current = false; }, [searchTerm]); return (
{isOrphanVisits && ( )} {!resultSet.visitsGroups[page - 1]?.length && ( )} {resultSet.visitsGroups[page - 1]?.map((visit, index) => { const isSelected = selectedVisits.includes(visit); return ( setSelectedVisits( isSelected ? selectedVisits.filter((v) => v !== visit) : [...selectedVisits, visit], )} > {isOrphanVisits && } ); })} {resultSet.total > PAGE_SIZE && ( )}
setSelectedVisits( selectedVisits.length < resultSet.total ? resultSet.visitsGroups.flat() : [], )} > 0 })} /> {renderOrderIcon('potentialBot')} Date {renderOrderIcon('date')} Country {renderOrderIcon('country')} City {renderOrderIcon('city')} Browser {renderOrderIcon('browser')} OS {renderOrderIcon('os')} Referrer {renderOrderIcon('referer')} Visited URL {renderOrderIcon('visitedUrl')}
No visits found with current filtering
{isSelected && } {visit.potentialBot && ( <> Potentially a visit from a bot or crawler )} {visit.country} {visit.city} {visit.browser} {visit.os} {visit.referer}{(visit as NormalizedOrphanVisit).visitedUrl}
Visits {prettify(start + 1)} to{' '} {prettify(min(end, resultSet.total))} of{' '} {prettify(resultSet.total)}
); };