mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 17:40:23 +03:00
Improved visits section so that charts are grouped in sub tabs
This commit is contained in:
parent
a013d40bf1
commit
c74355e363
9 changed files with 196 additions and 139 deletions
|
@ -92,3 +92,17 @@ body,
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
background-color: $mainColor;
|
background-color: $mainColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-xs-block {
|
||||||
|
@media (max-width: $xsMax) {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-md-block {
|
||||||
|
@media (max-width: $mdMax) {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,12 +1,5 @@
|
||||||
@import '../utils/base';
|
@import '../utils/base';
|
||||||
|
|
||||||
.create-short-url__save-btn {
|
|
||||||
@media (max-width: $xsMax) {
|
|
||||||
width: 100%;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.create-short-url .form-group:last-child,
|
.create-short-url .form-group:last-child,
|
||||||
.create-short-url p:last-child {
|
.create-short-url p:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|
|
@ -197,7 +197,7 @@ const CreateShortUrl = (
|
||||||
outline
|
outline
|
||||||
color="primary"
|
color="primary"
|
||||||
disabled={shortUrlCreationResult.saving || isEmpty(shortUrlCreation.longUrl)}
|
disabled={shortUrlCreationResult.saving || isEmpty(shortUrlCreation.longUrl)}
|
||||||
className="create-short-url__save-btn"
|
className="btn-xs-block"
|
||||||
>
|
>
|
||||||
{shortUrlCreationResult.saving ? 'Creating...' : 'Create'}
|
{shortUrlCreationResult.saving ? 'Creating...' : 'Create'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
20
src/visits/VisitsStats.scss
Normal file
20
src/visits/VisitsStats.scss
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
@import '../utils/base';
|
||||||
|
|
||||||
|
.visits-stats__nav-link {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
padding-bottom: calc(.5rem - 3px) !important;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
color: #5d6778;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visits-stats__nav-link:hover {
|
||||||
|
color: $mainColor !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visits-stats__nav-link.active {
|
||||||
|
border-color: $mainColor;
|
||||||
|
background-color: white !important;
|
||||||
|
color: $mainColor !important;
|
||||||
|
}
|
|
@ -1,14 +1,13 @@
|
||||||
import { isEmpty, propEq, values } from 'ramda';
|
import { isEmpty, propEq, values } from 'ramda';
|
||||||
import { useState, useEffect, useMemo, FC } from 'react';
|
import { useState, useEffect, useMemo, FC } from 'react';
|
||||||
import { Button, Card, Collapse, Progress } from 'reactstrap';
|
import { Button, Card, Nav, NavLink, Progress } from 'reactstrap';
|
||||||
import classNames from 'classnames';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faChevronDown as chevronDown } from '@fortawesome/free-solid-svg-icons';
|
import { faCalendarAlt, faMapMarkedAlt, faList, faChartPie } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { IconDefinition } from '@fortawesome/fontawesome-common-types';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import DateRangeRow from '../utils/DateRangeRow';
|
import DateRangeRow from '../utils/DateRangeRow';
|
||||||
import Message from '../utils/Message';
|
import Message from '../utils/Message';
|
||||||
import { formatDate } from '../utils/helpers/date';
|
import { formatDate } from '../utils/helpers/date';
|
||||||
import { useToggle } from '../utils/helpers/hooks';
|
|
||||||
import { ShlinkVisitsParams } from '../utils/services/types';
|
import { ShlinkVisitsParams } from '../utils/services/types';
|
||||||
import SortableBarGraph from './helpers/SortableBarGraph';
|
import SortableBarGraph from './helpers/SortableBarGraph';
|
||||||
import GraphCard from './helpers/GraphCard';
|
import GraphCard from './helpers/GraphCard';
|
||||||
|
@ -17,15 +16,23 @@ import VisitsTable from './VisitsTable';
|
||||||
import { NormalizedVisit, Stats, VisitsInfo } from './types';
|
import { NormalizedVisit, Stats, VisitsInfo } from './types';
|
||||||
import OpenMapModalBtn from './helpers/OpenMapModalBtn';
|
import OpenMapModalBtn from './helpers/OpenMapModalBtn';
|
||||||
import { normalizeVisits, processStatsFromVisits } from './services/VisitsParser';
|
import { normalizeVisits, processStatsFromVisits } from './services/VisitsParser';
|
||||||
|
import './VisitsStats.scss';
|
||||||
|
|
||||||
export interface VisitsStatsProps {
|
export interface VisitsStatsProps {
|
||||||
matchMedia?: (query: string) => MediaQueryList;
|
|
||||||
getVisits: (params: Partial<ShlinkVisitsParams>) => void;
|
getVisits: (params: Partial<ShlinkVisitsParams>) => void;
|
||||||
visitsInfo: VisitsInfo;
|
visitsInfo: VisitsInfo;
|
||||||
cancelGetVisits: () => void;
|
cancelGetVisits: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type HighlightableProps = 'referer' | 'country' | 'city';
|
type HighlightableProps = 'referer' | 'country' | 'city';
|
||||||
|
type Section = 'byTime' | 'byContext' | 'byLocation' | 'list';
|
||||||
|
|
||||||
|
const sections: Record<Section, { title: string; icon: IconDefinition }> = {
|
||||||
|
byTime: { title: 'By time', icon: faCalendarAlt },
|
||||||
|
byContext: { title: 'By context', icon: faChartPie },
|
||||||
|
byLocation: { title: 'By location', icon: faMapMarkedAlt },
|
||||||
|
list: { title: 'List', icon: faList },
|
||||||
|
};
|
||||||
|
|
||||||
const highlightedVisitsToStats = (
|
const highlightedVisitsToStats = (
|
||||||
highlightedVisits: NormalizedVisit[],
|
highlightedVisits: NormalizedVisit[],
|
||||||
|
@ -42,19 +49,15 @@ const highlightedVisitsToStats = (
|
||||||
const format = formatDate();
|
const format = formatDate();
|
||||||
let selectedBar: string | undefined;
|
let selectedBar: string | undefined;
|
||||||
|
|
||||||
const VisitsStats: FC<VisitsStatsProps> = (
|
const VisitsStats: FC<VisitsStatsProps> = ({ children, visitsInfo, getVisits, cancelGetVisits }) => {
|
||||||
{ children, visitsInfo, getVisits, cancelGetVisits, matchMedia = window.matchMedia },
|
|
||||||
) => {
|
|
||||||
const [ startDate, setStartDate ] = useState<moment.Moment | null>(null);
|
const [ startDate, setStartDate ] = useState<moment.Moment | null>(null);
|
||||||
const [ endDate, setEndDate ] = useState<moment.Moment | null>(null);
|
const [ endDate, setEndDate ] = useState<moment.Moment | null>(null);
|
||||||
const [ showTable, toggleTable ] = useToggle();
|
|
||||||
const [ tableIsSticky, , setSticky, unsetSticky ] = useToggle();
|
|
||||||
const [ highlightedVisits, setHighlightedVisits ] = useState<NormalizedVisit[]>([]);
|
const [ highlightedVisits, setHighlightedVisits ] = useState<NormalizedVisit[]>([]);
|
||||||
const [ highlightedLabel, setHighlightedLabel ] = useState<string | undefined>();
|
const [ highlightedLabel, setHighlightedLabel ] = useState<string | undefined>();
|
||||||
const [ isMobileDevice, setIsMobileDevice ] = useState(false);
|
const [ activeSection, setActiveSection ] = useState<Section>('byTime');
|
||||||
|
const onSectionChange = (section: Section) => () => setActiveSection(section);
|
||||||
|
|
||||||
const { visits, loading, loadingLarge, error, progress } = visitsInfo;
|
const { visits, loading, loadingLarge, error, progress } = visitsInfo;
|
||||||
const showTableControls = !loading && visits.length > 0;
|
|
||||||
const normalizedVisits = useMemo(() => normalizeVisits(visits), [ visits ]);
|
const normalizedVisits = useMemo(() => normalizeVisits(visits), [ visits ]);
|
||||||
const { os, browsers, referrers, countries, cities, citiesForMap } = useMemo(
|
const { os, browsers, referrers, countries, cities, citiesForMap } = useMemo(
|
||||||
() => processStatsFromVisits(normalizedVisits),
|
() => processStatsFromVisits(normalizedVisits),
|
||||||
|
@ -62,7 +65,6 @@ const VisitsStats: FC<VisitsStatsProps> = (
|
||||||
);
|
);
|
||||||
const mapLocations = values(citiesForMap);
|
const mapLocations = values(citiesForMap);
|
||||||
|
|
||||||
const determineIsMobileDevice = () => setIsMobileDevice(matchMedia('(max-width: 991px)').matches);
|
|
||||||
const setSelectedVisits = (selectedVisits: NormalizedVisit[]) => {
|
const setSelectedVisits = (selectedVisits: NormalizedVisit[]) => {
|
||||||
selectedBar = undefined;
|
selectedBar = undefined;
|
||||||
setHighlightedVisits(selectedVisits);
|
setHighlightedVisits(selectedVisits);
|
||||||
|
@ -81,15 +83,7 @@ const VisitsStats: FC<VisitsStatsProps> = (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => () => cancelGetVisits(), []);
|
||||||
determineIsMobileDevice();
|
|
||||||
window.addEventListener('resize', determineIsMobileDevice);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelGetVisits();
|
|
||||||
window.removeEventListener('resize', determineIsMobileDevice);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getVisits({ startDate: format(startDate) ?? undefined, endDate: format(endDate) ?? undefined });
|
getVisits({ startDate: format(startDate) ?? undefined, endDate: format(endDate) ?? undefined });
|
||||||
}, [ startDate, endDate ]);
|
}, [ startDate, endDate ]);
|
||||||
|
@ -121,67 +115,106 @@ const VisitsStats: FC<VisitsStatsProps> = (
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<>
|
||||||
<div className="col-12 mt-4">
|
<Card className="p-0 mt-4 overflow-hidden" body>
|
||||||
<LineChartCard
|
<Nav className="visits-stats__nav" pills justified>
|
||||||
title="Visits during time"
|
{Object.entries(sections).map(
|
||||||
visits={normalizedVisits}
|
([ section, { title, icon }]) => (
|
||||||
highlightedVisits={highlightedVisits}
|
<NavLink
|
||||||
highlightedLabel={highlightedLabel}
|
key={section}
|
||||||
setSelectedVisits={setSelectedVisits}
|
active={activeSection === section}
|
||||||
/>
|
className="visits-stats__nav-link"
|
||||||
|
onClick={onSectionChange(section as Section)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={icon} />
|
||||||
|
<span className="ml-2 d-none d-sm-inline">{title}</span>
|
||||||
|
</NavLink>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</Nav>
|
||||||
|
</Card>
|
||||||
|
<div className="row">
|
||||||
|
{activeSection === 'byTime' && (
|
||||||
|
<div className="col-12 mt-4">
|
||||||
|
<LineChartCard
|
||||||
|
title="Visits during time"
|
||||||
|
visits={normalizedVisits}
|
||||||
|
highlightedVisits={highlightedVisits}
|
||||||
|
highlightedLabel={highlightedLabel}
|
||||||
|
setSelectedVisits={setSelectedVisits}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{activeSection === 'byContext' && (
|
||||||
|
<>
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{activeSection === 'byLocation' && (
|
||||||
|
<>
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{activeSection === 'list' && (
|
||||||
|
<div className="col-12">
|
||||||
|
<VisitsTable
|
||||||
|
visits={normalizedVisits}
|
||||||
|
selectedVisits={highlightedVisits}
|
||||||
|
setSelectedVisits={setSelectedVisits}
|
||||||
|
isSticky
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<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>
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -200,47 +233,21 @@ const VisitsStats: FC<VisitsStatsProps> = (
|
||||||
onEndDateChange={setEndDate}
|
onEndDateChange={setEndDate}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-lg-5 col-xl-6 mt-4 mt-lg-0">
|
{visits.length > 0 && (
|
||||||
{showTableControls && (
|
<div className="col-lg-5 col-xl-6 mt-4 mt-lg-0">
|
||||||
<span className={classNames({ row: isMobileDevice })}>
|
<Button
|
||||||
<span className={classNames({ 'col-6': isMobileDevice })}>
|
outline
|
||||||
<Button outline color="primary" block={isMobileDevice} onClick={toggleTable}>
|
disabled={highlightedVisits.length === 0}
|
||||||
{showTable ? 'Hide' : 'Show'} table
|
className="btn-md-block"
|
||||||
<FontAwesomeIcon icon={chevronDown} rotation={showTable ? 180 : undefined} className="ml-2" />
|
onClick={() => setSelectedVisits([])}
|
||||||
</Button>
|
>
|
||||||
</span>
|
Reset selection {highlightedVisits.length > 0 && <>({highlightedVisits.length})</>}
|
||||||
<span className={classNames({ 'col-6': isMobileDevice, 'ml-2': !isMobileDevice })}>
|
</Button>
|
||||||
<Button
|
</div>
|
||||||
outline
|
)}
|
||||||
disabled={highlightedVisits.length === 0}
|
|
||||||
block={isMobileDevice}
|
|
||||||
onClick={() => setSelectedVisits([])}
|
|
||||||
>
|
|
||||||
Reset selection
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{showTableControls && (
|
|
||||||
<Collapse
|
|
||||||
isOpen={showTable}
|
|
||||||
// Enable stickiness only when there's no CSS animation, to avoid weird rendering effects
|
|
||||||
onEntered={setSticky}
|
|
||||||
onExiting={unsetSticky}
|
|
||||||
>
|
|
||||||
<VisitsTable
|
|
||||||
visits={normalizedVisits}
|
|
||||||
selectedVisits={highlightedVisits}
|
|
||||||
setSelectedVisits={setSelectedVisits}
|
|
||||||
isSticky={tableIsSticky}
|
|
||||||
/>
|
|
||||||
</Collapse>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
{renderVisitsContent()}
|
{renderVisitsContent()}
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState, useRef } from 'react';
|
||||||
import Moment from 'react-moment';
|
import Moment from 'react-moment';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { min, splitEvery } from 'ramda';
|
import { min, splitEvery } from 'ramda';
|
||||||
|
@ -68,6 +68,7 @@ const VisitsTable = ({
|
||||||
const [ searchTerm, setSearchTerm ] = useState<string | undefined>(undefined);
|
const [ searchTerm, setSearchTerm ] = useState<string | undefined>(undefined);
|
||||||
const [ order, setOrder ] = useState<Order>({ field: undefined, dir: undefined });
|
const [ order, setOrder ] = useState<Order>({ field: undefined, dir: undefined });
|
||||||
const resultSet = useMemo(() => calculateVisits(visits, searchTerm, order), [ searchTerm, order ]);
|
const resultSet = useMemo(() => calculateVisits(visits, searchTerm, order), [ searchTerm, order ]);
|
||||||
|
const isFirstLoad = useRef(true);
|
||||||
|
|
||||||
const [ page, setPage ] = useState(1);
|
const [ page, setPage ] = useState(1);
|
||||||
const end = page * PAGE_SIZE;
|
const end = page * PAGE_SIZE;
|
||||||
|
@ -91,7 +92,12 @@ const VisitsTable = ({
|
||||||
}, []);
|
}, []);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPage(1);
|
setPage(1);
|
||||||
setSelectedVisits([]);
|
|
||||||
|
if (isFirstLoad.current) {
|
||||||
|
isFirstLoad.current = false;
|
||||||
|
} else {
|
||||||
|
setSelectedVisits([]);
|
||||||
|
}
|
||||||
}, [ searchTerm ]);
|
}, [ searchTerm ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useRef } from 'react';
|
import { useState } from 'react';
|
||||||
import { Doughnut, HorizontalBar } from 'react-chartjs-2';
|
import { Doughnut, HorizontalBar } from 'react-chartjs-2';
|
||||||
import { keys, values } from 'ramda';
|
import { keys, values } from 'ramda';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
@ -118,7 +118,7 @@ const DefaultChart = (
|
||||||
}, { ...stats }),
|
}, { ...stats }),
|
||||||
);
|
);
|
||||||
const highlightedData = statsAreDefined(highlightedStats) ? fillTheGaps(highlightedStats, labels) : undefined;
|
const highlightedData = statsAreDefined(highlightedStats) ? fillTheGaps(highlightedStats, labels) : undefined;
|
||||||
const chartRef = useRef<HorizontalBar | Doughnut>();
|
const [ chartRef, setChartRef ] = useState<HorizontalBar | Doughnut | undefined>()
|
||||||
|
|
||||||
const options: ChartOptions = {
|
const options: ChartOptions = {
|
||||||
legend: { display: false },
|
legend: { display: false },
|
||||||
|
@ -156,7 +156,7 @@ const DefaultChart = (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className={classNames('col-sm-12', { 'col-md-7': !isBarChart })}>
|
<div className={classNames('col-sm-12', { 'col-md-7': !isBarChart })}>
|
||||||
<Component
|
<Component
|
||||||
ref={chartRef as any}
|
ref={(element) => setChartRef(element ?? undefined)}
|
||||||
key={height}
|
key={height}
|
||||||
data={graphData}
|
data={graphData}
|
||||||
options={options}
|
options={options}
|
||||||
|
@ -166,7 +166,7 @@ const DefaultChart = (
|
||||||
</div>
|
</div>
|
||||||
{!isBarChart && (
|
{!isBarChart && (
|
||||||
<div className="col-sm-12 col-md-5">
|
<div className="col-sm-12 col-md-5">
|
||||||
{chartRef.current?.chartInstance.generateLegend()}
|
{chartRef?.chartInstance.generateLegend()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,6 +4,6 @@
|
||||||
height: 300px !important;
|
height: 300px !important;
|
||||||
|
|
||||||
@media (min-width: $mdMin) {
|
@media (min-width: $mdMin) {
|
||||||
height: 350px !important;
|
height: 400px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { shallow, ShallowWrapper } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { Card, Progress } from 'reactstrap';
|
import { Card, NavLink, Progress } from 'reactstrap';
|
||||||
import { Mock } from 'ts-mockery';
|
import { Mock } from 'ts-mockery';
|
||||||
import VisitStats from '../../src/visits/VisitsStats';
|
import VisitStats from '../../src/visits/VisitsStats';
|
||||||
import Message from '../../src/utils/Message';
|
import Message from '../../src/utils/Message';
|
||||||
|
@ -7,6 +7,8 @@ import GraphCard from '../../src/visits/helpers/GraphCard';
|
||||||
import SortableBarGraph from '../../src/visits/helpers/SortableBarGraph';
|
import SortableBarGraph from '../../src/visits/helpers/SortableBarGraph';
|
||||||
import DateRangeRow from '../../src/utils/DateRangeRow';
|
import DateRangeRow from '../../src/utils/DateRangeRow';
|
||||||
import { Visit, VisitsInfo } from '../../src/visits/types';
|
import { Visit, VisitsInfo } from '../../src/visits/types';
|
||||||
|
import LineChartCard from '../../src/visits/helpers/LineChartCard';
|
||||||
|
import VisitsTable from '../../src/visits/VisitsTable';
|
||||||
|
|
||||||
describe('<VisitStats />', () => {
|
describe('<VisitStats />', () => {
|
||||||
const visits = [ Mock.all<Visit>(), Mock.all<Visit>(), Mock.all<Visit>() ];
|
const visits = [ Mock.all<Visit>(), Mock.all<Visit>(), Mock.all<Visit>() ];
|
||||||
|
@ -20,7 +22,6 @@ describe('<VisitStats />', () => {
|
||||||
getVisits={getVisitsMock}
|
getVisits={getVisitsMock}
|
||||||
visitsInfo={Mock.of<VisitsInfo>(visitsInfo)}
|
visitsInfo={Mock.of<VisitsInfo>(visitsInfo)}
|
||||||
cancelGetVisits={() => {}}
|
cancelGetVisits={() => {}}
|
||||||
matchMedia={() => Mock.of<MediaQueryList>({ matches: false })}
|
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -66,12 +67,24 @@ describe('<VisitStats />', () => {
|
||||||
expect(message.html()).toContain('There are no visits matching current filter :(');
|
expect(message.html()).toContain('There are no visits matching current filter :(');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders all graphics when visits are properly loaded', () => {
|
it.each([
|
||||||
|
[ 0, 1, 0 ],
|
||||||
|
[ 1, 3, 0 ],
|
||||||
|
[ 2, 2, 0 ],
|
||||||
|
[ 3, 0, 1 ],
|
||||||
|
])('renders expected amount of graphics based on active section', (navIndex, expectedGraphics, expectedTables) => {
|
||||||
const wrapper = createComponent({ loading: false, error: false, visits });
|
const wrapper = createComponent({ loading: false, error: false, visits });
|
||||||
|
const nav = wrapper.find(NavLink).at(navIndex);
|
||||||
|
|
||||||
|
nav.simulate('click');
|
||||||
|
|
||||||
const graphs = wrapper.find(GraphCard);
|
const graphs = wrapper.find(GraphCard);
|
||||||
const sortableBarGraphs = wrapper.find(SortableBarGraph);
|
const sortableBarGraphs = wrapper.find(SortableBarGraph);
|
||||||
|
const lineChart = wrapper.find(LineChartCard);
|
||||||
|
const table = wrapper.find(VisitsTable);
|
||||||
|
|
||||||
expect(graphs.length + sortableBarGraphs.length).toEqual(5);
|
expect(graphs.length + sortableBarGraphs.length + lineChart.length).toEqual(expectedGraphics);
|
||||||
|
expect(table).toHaveLength(expectedTables);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reloads visits when selected dates change', () => {
|
it('reloads visits when selected dates change', () => {
|
||||||
|
@ -88,6 +101,10 @@ describe('<VisitStats />', () => {
|
||||||
|
|
||||||
it('holds the map button content generator on cities graph extraHeaderContent', () => {
|
it('holds the map button content generator on cities graph extraHeaderContent', () => {
|
||||||
const wrapper = createComponent({ loading: false, error: false, visits });
|
const wrapper = createComponent({ loading: false, error: false, visits });
|
||||||
|
const locationNav = wrapper.find(NavLink).at(2);
|
||||||
|
|
||||||
|
locationNav.simulate('click');
|
||||||
|
|
||||||
const citiesGraph = wrapper.find(SortableBarGraph).find('[title="Cities"]');
|
const citiesGraph = wrapper.find(SortableBarGraph).find('[title="Cities"]');
|
||||||
const extraHeaderContent = citiesGraph.prop('extraHeaderContent');
|
const extraHeaderContent = citiesGraph.prop('extraHeaderContent');
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue