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 { supportsBotVisits } from '../utils/helpers/features'; import { SelectedServer } from '../servers/data'; import { Time } from '../utils/Time'; import { TableOrderIcon } from '../utils/table/TableOrderIcon'; import { NormalizedOrphanVisit, NormalizedVisit } from './types'; import './VisitsTable.scss'; export interface VisitsTableProps { visits: NormalizedVisit[]; selectedVisits?: NormalizedVisit[]; setSelectedVisits: (visits: NormalizedVisit[]) => void; matchMedia?: (query: string) => MediaQueryList; isOrphanVisits?: boolean; selectedServer: SelectedServer; } 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 }; }; const VisitsTable = ({ visits, selectedVisits = [], setSelectedVisits, selectedServer, 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 supportsBots = supportsBotVisits(selectedServer); const fullSizeColSpan = 7 + Number(supportsBots) + 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 ( {supportsBots && ( )} {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 ], )} > {supportsBots && ( )} {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)}
); }; export default VisitsTable;