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'; import { faCalendarAlt, faMapMarkedAlt, faList, faChartPie, faFileDownload } from '@fortawesome/free-solid-svg-icons'; import { IconDefinition } from '@fortawesome/fontawesome-common-types'; import { Route, Switch, NavLink as RouterNavLink, Redirect } from 'react-router-dom'; import { Location } from 'history'; import classNames from 'classnames'; import { DateRangeSelector } from '../utils/dates/DateRangeSelector'; import Message from '../utils/Message'; import { DateInterval, DateRange, intervalToDateRange } from '../utils/dates/types'; import { Result } from '../utils/Result'; import { ShlinkApiError } from '../api/ShlinkApiError'; import { Settings } from '../settings/reducers/settings'; import { SelectedServer } from '../servers/data'; import { supportsBotVisits } from '../utils/helpers/features'; import SortableBarGraph from './helpers/SortableBarGraph'; import GraphCard from './helpers/GraphCard'; import LineChartCard from './helpers/LineChartCard'; import VisitsTable from './VisitsTable'; import { NormalizedOrphanVisit, NormalizedVisit, VisitsFilter, VisitsInfo, VisitsParams } from './types'; import OpenMapModalBtn from './helpers/OpenMapModalBtn'; import { normalizeVisits, processStatsFromVisits } from './services/VisitsParser'; import { VisitsFilterDropdown } from './helpers/VisitsFilterDropdown'; import { HighlightableProps, highlightedVisitsToStats } from './types/helpers'; import './VisitsStats.scss'; export interface VisitsStatsProps { getVisits: (params: VisitsParams) => void; visitsInfo: VisitsInfo; settings: Settings; selectedServer: SelectedServer; cancelGetVisits: () => void; baseUrl: string; domain?: string; exportCsv: (visits: NormalizedVisit[]) => void; isOrphanVisits?: boolean; } interface VisitsNavLinkProps { title: string; subPath: string; icon: IconDefinition; } type Section = 'byTime' | 'byContext' | 'byLocation' | 'list'; const sections: Record = { byTime: { title: 'By time', subPath: '', icon: faCalendarAlt }, byContext: { title: 'By context', subPath: '/by-context', icon: faChartPie }, byLocation: { title: 'By location', subPath: '/by-location', icon: faMapMarkedAlt }, list: { title: 'List', subPath: '/list', icon: faList }, }; let selectedBar: string | undefined; const VisitsNavLink: FC = ({ subPath, title, icon, to }) => ( pathname.endsWith(`visits${subPath}`)} replace > {title} ); const VisitsStats: FC = ({ children, visitsInfo, getVisits, cancelGetVisits, baseUrl, domain, settings, exportCsv, selectedServer, isOrphanVisits = false, }) => { const initialInterval: DateInterval = settings.visits?.defaultInterval ?? 'last30Days'; const [ dateRange, setDateRange ] = useState(intervalToDateRange(initialInterval)); const [ highlightedVisits, setHighlightedVisits ] = useState([]); const [ highlightedLabel, setHighlightedLabel ] = useState(); const [ visitsFilter, setVisitsFilter ] = useState({}); const botsSupported = supportsBotVisits(selectedServer); const buildSectionUrl = (subPath?: string) => { const query = domain ? `?domain=${domain}` : ''; return !subPath ? `${baseUrl}${query}` : `${baseUrl}${subPath}${query}`; }; const { visits, loading, loadingLarge, error, errorData, progress } = visitsInfo; const normalizedVisits = useMemo(() => normalizeVisits(visits), [ visits ]); const { os, browsers, referrers, countries, cities, citiesForMap, visitedUrls } = useMemo( () => processStatsFromVisits(normalizedVisits), [ normalizedVisits ], ); const mapLocations = values(citiesForMap); const setSelectedVisits = (selectedVisits: NormalizedVisit[]) => { selectedBar = undefined; setHighlightedVisits(selectedVisits); }; const highlightVisitsForProp = (prop: HighlightableProps) => (value: string) => { const newSelectedBar = `${prop}_${value}`; if (selectedBar === newSelectedBar) { setHighlightedVisits([]); setHighlightedLabel(undefined); selectedBar = undefined; } else { setHighlightedVisits((normalizedVisits as NormalizedOrphanVisit[]).filter(propEq(prop, value))); setHighlightedLabel(value); selectedBar = newSelectedBar; } }; useEffect(() => cancelGetVisits, []); useEffect(() => { getVisits({ dateRange, filter: visitsFilter }); }, [ dateRange, visitsFilter ]); const renderVisitsContent = () => { if (loadingLarge) { return ( This is going to take a while... :S ); } if (loading) { return ; } if (error) { return ( ); } if (isEmpty(visits)) { return There are no visits matching current filter :(; } return ( <>
{isOrphanVisits && (
)}
mapLocations.length > 0 && } sortingItems={{ name: 'City name', amount: 'Visits amount', }} onClick={highlightVisitsForProp('city')} />
); }; return ( <> {children}
{visits.length > 0 && (
)}
{renderVisitsContent()}
); }; export default VisitsStats;