shlink-web-client/src/visits/VisitsStats.tsx

271 lines
9.6 KiB
TypeScript
Raw Normal View History

2020-09-05 09:49:18 +03:00
import { isEmpty, propEq, values } from 'ramda';
2020-11-14 00:44:26 +03:00
import { useState, useEffect, useMemo, FC } from 'react';
import { Button, Card, Nav, NavLink, Progress } from 'reactstrap';
2020-09-05 09:49:18 +03:00
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCalendarAlt, faMapMarkedAlt, faList, faChartPie } from '@fortawesome/free-solid-svg-icons';
import { IconDefinition } from '@fortawesome/fontawesome-common-types';
2020-12-20 21:28:09 +03:00
import { Route, Switch, NavLink as RouterNavLink, Redirect } from 'react-router-dom';
import { Location } from 'history';
import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
2020-09-05 09:49:18 +03:00
import Message from '../utils/Message';
import { formatIsoDate } from '../utils/helpers/date';
2020-09-05 09:49:18 +03:00
import { ShlinkVisitsParams } from '../utils/services/types';
import { DateInterval, DateRange, intervalToDateRange } from '../utils/dates/types';
2020-09-05 09:49:18 +03:00
import SortableBarGraph from './helpers/SortableBarGraph';
import GraphCard from './helpers/GraphCard';
import LineChartCard from './helpers/LineChartCard';
import VisitsTable from './VisitsTable';
import { NormalizedVisit, Stats, VisitsInfo } from './types';
import OpenMapModalBtn from './helpers/OpenMapModalBtn';
import { normalizeVisits, processStatsFromVisits } from './services/VisitsParser';
import './VisitsStats.scss';
2020-09-05 09:49:18 +03:00
export interface VisitsStatsProps {
getVisits: (params: Partial<ShlinkVisitsParams>) => void;
visitsInfo: VisitsInfo;
cancelGetVisits: () => void;
2020-12-20 21:28:09 +03:00
baseUrl: string;
domain?: string;
2020-09-05 09:49:18 +03:00
}
type HighlightableProps = 'referer' | 'country' | 'city';
type Section = 'byTime' | 'byContext' | 'byLocation' | 'list';
2020-12-20 21:28:09 +03:00
const sections: Record<Section, { title: string; subPath?: string; icon: IconDefinition }> = {
byTime: { title: 'By time', icon: faCalendarAlt },
2020-12-20 21:28:09 +03:00
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 },
};
2020-09-05 09:49:18 +03:00
const highlightedVisitsToStats = (
highlightedVisits: NormalizedVisit[],
prop: HighlightableProps,
): Stats => highlightedVisits.reduce<Stats>((acc, highlightedVisit) => {
if (!acc[highlightedVisit[prop]]) {
acc[highlightedVisit[prop]] = 0;
}
acc[highlightedVisit[prop]] += 1;
return acc;
}, {});
let selectedBar: string | undefined;
const initialInterval: DateInterval = 'last30Days';
2020-09-05 09:49:18 +03:00
2020-12-20 21:28:09 +03:00
const VisitsStats: FC<VisitsStatsProps> = ({ children, visitsInfo, getVisits, cancelGetVisits, baseUrl, domain }) => {
const [ dateRange, setDateRange ] = useState<DateRange>(intervalToDateRange(initialInterval));
2020-09-05 09:49:18 +03:00
const [ highlightedVisits, setHighlightedVisits ] = useState<NormalizedVisit[]>([]);
const [ highlightedLabel, setHighlightedLabel ] = useState<string | undefined>();
2020-12-20 21:28:09 +03:00
const buildSectionUrl = (subPath?: string) => {
const query = domain ? `?domain=${domain}` : '';
return !subPath ? `${baseUrl}${query}` : `${baseUrl}/${subPath}${query}`;
};
2020-09-05 09:49:18 +03:00
const { visits, loading, loadingLarge, error, progress } = visitsInfo;
const normalizedVisits = useMemo(() => normalizeVisits(visits), [ visits ]);
const { os, browsers, referrers, countries, cities, citiesForMap } = 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.filter(propEq(prop, value)));
setHighlightedLabel(value);
selectedBar = newSelectedBar;
}
};
useEffect(() => cancelGetVisits, []);
2020-09-05 09:49:18 +03:00
useEffect(() => {
const { startDate, endDate } = dateRange;
getVisits({ startDate: formatIsoDate(startDate) ?? undefined, endDate: formatIsoDate(endDate) ?? undefined });
}, [ dateRange ]);
2020-09-05 09:49:18 +03:00
const renderVisitsContent = () => {
if (loadingLarge) {
return (
<Message loading>
This is going to take a while... :S
<Progress value={progress} striped={progress === 100} className="mt-3" />
</Message>
);
}
if (loading) {
return <Message loading />;
}
if (error) {
return (
<Card className="mt-4" body inverse color="danger">
An error occurred while loading visits :(
</Card>
);
}
if (isEmpty(visits)) {
return <Message>There are no visits matching current filter :(</Message>;
}
return (
<>
<Card className="visits-stats__nav p-0 mt-4 overflow-hidden" body>
<Nav pills justified>
{Object.entries(sections).map(
2020-12-20 21:28:09 +03:00
([ section, { title, icon, subPath }]) => (
<NavLink
key={section}
2020-12-20 21:28:09 +03:00
tag={RouterNavLink}
className="visits-stats__nav-link"
2020-12-20 21:28:09 +03:00
to={buildSectionUrl(subPath)}
isActive={(_: null, { pathname }: Location) =>
(!subPath && pathname.endsWith('/visits')) || (subPath && pathname.endsWith(subPath))
}
>
<FontAwesomeIcon icon={icon} />
<span className="ml-2 d-none d-sm-inline">{title}</span>
</NavLink>
),
)}
</Nav>
</Card>
<div className="row">
2020-12-20 21:28:09 +03:00
<Switch>
<Route exact path={baseUrl}>
<div className="col-12 mt-4">
<LineChartCard
title="Visits during time"
visits={normalizedVisits}
highlightedVisits={highlightedVisits}
highlightedLabel={highlightedLabel}
setSelectedVisits={setSelectedVisits}
/>
</div>
</Route>
<Route exact path={`${baseUrl}/by-context`}>
<div className="col-xl-4 col-lg-6 mt-4">
<GraphCard title="Operating systems" stats={os} />
</div>
<div className="col-xl-4 col-lg-6 mt-4">
<GraphCard title="Browsers" stats={browsers} />
</div>
<div className="col-xl-4 mt-4">
<SortableBarGraph
title="Referrers"
stats={referrers}
withPagination={false}
highlightedStats={highlightedVisitsToStats(highlightedVisits, 'referer')}
highlightedLabel={highlightedLabel}
sortingItems={{
name: 'Referrer name',
amount: 'Visits amount',
}}
onClick={highlightVisitsForProp('referer')}
/>
</div>
2020-12-20 21:28:09 +03:00
</Route>
<Route exact path={`${baseUrl}/by-location`}>
<div className="col-lg-6 mt-4">
<SortableBarGraph
title="Countries"
stats={countries}
highlightedStats={highlightedVisitsToStats(highlightedVisits, 'country')}
highlightedLabel={highlightedLabel}
sortingItems={{
name: 'Country name',
amount: 'Visits amount',
}}
onClick={highlightVisitsForProp('country')}
/>
</div>
<div className="col-lg-6 mt-4">
<SortableBarGraph
title="Cities"
stats={cities}
highlightedStats={highlightedVisitsToStats(highlightedVisits, 'city')}
highlightedLabel={highlightedLabel}
extraHeaderContent={(activeCities: string[]) =>
mapLocations.length > 0 &&
<OpenMapModalBtn modalTitle="Cities" locations={mapLocations} activeCities={activeCities} />
}
sortingItems={{
name: 'City name',
amount: 'Visits amount',
}}
onClick={highlightVisitsForProp('city')}
/>
</div>
2020-12-20 21:28:09 +03:00
</Route>
<Route exact path={`${baseUrl}/list`}>
<div className="col-12">
<VisitsTable
visits={normalizedVisits}
selectedVisits={highlightedVisits}
setSelectedVisits={setSelectedVisits}
/>
</div>
</Route>
<Redirect to={baseUrl} />
</Switch>
2020-09-05 09:49:18 +03:00
</div>
</>
2020-09-05 09:49:18 +03:00
);
};
return (
2020-11-14 00:44:26 +03:00
<>
2020-09-05 09:49:18 +03:00
{children}
<section className="mt-4">
<div className="row flex-md-row-reverse">
<div className="col-lg-7 col-xl-6">
<DateRangeSelector
2020-09-05 09:49:18 +03:00
disabled={loading}
initialDateRange={initialInterval}
defaultText="All visits"
onDatesChange={setDateRange}
2020-09-05 09:49:18 +03:00
/>
</div>
{visits.length > 0 && (
<div className="col-lg-5 col-xl-6 mt-4 mt-lg-0">
<Button
outline
disabled={highlightedVisits.length === 0}
className="btn-md-block"
onClick={() => setSelectedVisits([])}
>
Reset selection {highlightedVisits.length > 0 && <>({highlightedVisits.length})</>}
</Button>
</div>
)}
2020-09-05 09:49:18 +03:00
</div>
</section>
<section>
{renderVisitsContent()}
</section>
2020-11-14 00:44:26 +03:00
</>
2020-09-05 09:49:18 +03:00
);
};
export default VisitsStats;