mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-10 18:27:25 +03:00
Converted ShortUrlVisits in functional component
This commit is contained in:
parent
8a486d991b
commit
310831a26a
2 changed files with 86 additions and 86 deletions
|
@ -1,5 +1,5 @@
|
|||
import { isEmpty, mapObjIndexed, values } from 'ramda';
|
||||
import React from 'react';
|
||||
import { isEmpty, values } from 'ramda';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button, Card, Collapse } from 'reactstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
import qs from 'qs';
|
||||
|
@ -8,6 +8,7 @@ import { faChevronDown as chevronDown } from '@fortawesome/free-solid-svg-icons'
|
|||
import DateRangeRow from '../utils/DateRangeRow';
|
||||
import Message from '../utils/Message';
|
||||
import { formatDate } from '../utils/helpers/date';
|
||||
import { useToggle } from '../utils/helpers/hooks';
|
||||
import SortableBarGraph from './SortableBarGraph';
|
||||
import { shortUrlVisitsType } from './reducers/shortUrlVisits';
|
||||
import VisitsHeader from './VisitsHeader';
|
||||
|
@ -15,71 +16,74 @@ import GraphCard from './GraphCard';
|
|||
import { shortUrlDetailType } from './reducers/shortUrlDetail';
|
||||
import VisitsTable from './VisitsTable';
|
||||
|
||||
const propTypes = {
|
||||
match: PropTypes.shape({
|
||||
params: PropTypes.object,
|
||||
}),
|
||||
location: PropTypes.shape({
|
||||
search: PropTypes.string,
|
||||
}),
|
||||
getShortUrlVisits: PropTypes.func,
|
||||
shortUrlVisits: shortUrlVisitsType,
|
||||
getShortUrlDetail: PropTypes.func,
|
||||
shortUrlDetail: shortUrlDetailType,
|
||||
cancelGetShortUrlVisits: PropTypes.func,
|
||||
matchMedia: PropTypes.func,
|
||||
};
|
||||
|
||||
const highlightedVisitToStats = (highlightedVisit, prop) => highlightedVisit && { [highlightedVisit[prop]]: 1 };
|
||||
const format = formatDate();
|
||||
let memoizationId;
|
||||
let timeWhenMounted;
|
||||
|
||||
const ShortUrlVisits = (
|
||||
{ processStatsFromVisits },
|
||||
OpenMapModalBtn
|
||||
) => class ShortUrlVisits extends React.PureComponent {
|
||||
static propTypes = {
|
||||
match: PropTypes.shape({
|
||||
params: PropTypes.object,
|
||||
}),
|
||||
location: PropTypes.shape({
|
||||
search: PropTypes.string,
|
||||
}),
|
||||
getShortUrlVisits: PropTypes.func,
|
||||
shortUrlVisits: shortUrlVisitsType,
|
||||
getShortUrlDetail: PropTypes.func,
|
||||
shortUrlDetail: shortUrlDetailType,
|
||||
cancelGetShortUrlVisits: PropTypes.func,
|
||||
matchMedia: PropTypes.func,
|
||||
};
|
||||
const ShortUrlVisits = ({ processStatsFromVisits }, OpenMapModalBtn) => {
|
||||
const ShortUrlVisitsComp = ({
|
||||
match,
|
||||
location,
|
||||
shortUrlVisits,
|
||||
shortUrlDetail,
|
||||
getShortUrlVisits,
|
||||
getShortUrlDetail,
|
||||
cancelGetShortUrlVisits,
|
||||
matchMedia = window.matchMedia,
|
||||
}) => {
|
||||
const [ startDate, setStartDate ] = useState(undefined);
|
||||
const [ endDate, setEndDate ] = useState(undefined);
|
||||
const [ showTable, toggleTable ] = useToggle();
|
||||
const [ tableIsSticky, , setSticky, unsetSticky ] = useToggle();
|
||||
const [ highlightedVisit, setHighlightedVisit ] = useState(undefined);
|
||||
const [ isMobileDevice, setIsMobileDevice ] = useState(false);
|
||||
const determineIsMobileDevice = () => setIsMobileDevice(matchMedia('(max-width: 991px)').matches);
|
||||
|
||||
state = {
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
showTable: false,
|
||||
tableIsSticky: false,
|
||||
isMobileDevice: false,
|
||||
highlightedVisit: undefined,
|
||||
};
|
||||
|
||||
loadVisits = (loadDetail = false) => {
|
||||
const { match: { params }, location: { search }, getShortUrlVisits, getShortUrlDetail } = this.props;
|
||||
const { params } = match;
|
||||
const { shortCode } = params;
|
||||
const { startDate, endDate } = mapObjIndexed(formatDate(), this.state);
|
||||
const { search } = location;
|
||||
const { domain } = qs.parse(search, { ignoreQueryPrefix: true });
|
||||
|
||||
// While the "page" is loaded, use the timestamp + filtering dates as memoization IDs for stats calculations
|
||||
this.memoizationId = `${this.timeWhenMounted}_${shortCode}_${startDate}_${endDate}`;
|
||||
getShortUrlVisits(shortCode, { startDate, endDate, domain });
|
||||
const loadVisits = () => {
|
||||
const start = format(startDate);
|
||||
const end = format(endDate);
|
||||
|
||||
if (loadDetail) {
|
||||
// While the "page" is loaded, use the timestamp + filtering dates as memoization IDs for stats calculations
|
||||
memoizationId = `${timeWhenMounted}_${shortCode}_${start}_${end}`;
|
||||
getShortUrlVisits(shortCode, { startDate: start, endDate: end, domain });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
timeWhenMounted = new Date().getTime();
|
||||
getShortUrlDetail(shortCode, domain);
|
||||
}
|
||||
};
|
||||
determineIsMobileDevice();
|
||||
window.addEventListener('resize', determineIsMobileDevice);
|
||||
|
||||
setIsMobileDevice = () => {
|
||||
const { matchMedia = window.matchMedia } = this.props;
|
||||
return () => {
|
||||
cancelGetShortUrlVisits();
|
||||
window.removeEventListener('resize', determineIsMobileDevice);
|
||||
};
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
loadVisits();
|
||||
}, [ startDate, endDate ]);
|
||||
|
||||
this.setState({ isMobileDevice: matchMedia('(max-width: 991px)').matches });
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.timeWhenMounted = new Date().getTime();
|
||||
this.loadVisits(true);
|
||||
this.setIsMobileDevice();
|
||||
window.addEventListener('resize', this.setIsMobileDevice);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.cancelGetShortUrlVisits();
|
||||
window.removeEventListener('resize', this.setIsMobileDevice);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { shortUrlVisits, shortUrlDetail } = this.props;
|
||||
const { visits, loading, loadingLarge, error } = shortUrlVisits;
|
||||
const showTableControls = !loading && visits.length > 0;
|
||||
|
||||
|
@ -103,7 +107,7 @@ const ShortUrlVisits = (
|
|||
}
|
||||
|
||||
const { os, browsers, referrers, countries, cities, citiesForMap } = processStatsFromVisits(
|
||||
{ id: this.memoizationId, visits }
|
||||
{ id: memoizationId, visits }
|
||||
);
|
||||
const mapLocations = values(citiesForMap);
|
||||
|
||||
|
@ -120,7 +124,7 @@ const ShortUrlVisits = (
|
|||
title="Referrers"
|
||||
stats={referrers}
|
||||
withPagination={false}
|
||||
highlightedStats={highlightedVisitToStats(this.state.highlightedVisit, 'referer')}
|
||||
highlightedStats={highlightedVisitToStats(highlightedVisit, 'referer')}
|
||||
sortingItems={{
|
||||
name: 'Referrer name',
|
||||
amount: 'Visits amount',
|
||||
|
@ -131,7 +135,7 @@ const ShortUrlVisits = (
|
|||
<SortableBarGraph
|
||||
title="Countries"
|
||||
stats={countries}
|
||||
highlightedStats={highlightedVisitToStats(this.state.highlightedVisit, 'country')}
|
||||
highlightedStats={highlightedVisitToStats(highlightedVisit, 'country')}
|
||||
sortingItems={{
|
||||
name: 'Country name',
|
||||
amount: 'Visits amount',
|
||||
|
@ -142,10 +146,10 @@ const ShortUrlVisits = (
|
|||
<SortableBarGraph
|
||||
title="Cities"
|
||||
stats={cities}
|
||||
highlightedStats={highlightedVisitToStats(this.state.highlightedVisit, 'city')}
|
||||
highlightedStats={highlightedVisitToStats(highlightedVisit, 'city')}
|
||||
extraHeaderContent={(activeCities) =>
|
||||
mapLocations.length > 0 &&
|
||||
<OpenMapModalBtn modalTitle="Cities" locations={mapLocations} activeCities={activeCities} />
|
||||
<OpenMapModalBtn modalTitle="Cities" locations={mapLocations} activeCities={activeCities} />
|
||||
}
|
||||
sortingItems={{
|
||||
name: 'City name',
|
||||
|
@ -156,7 +160,6 @@ const ShortUrlVisits = (
|
|||
</div>
|
||||
);
|
||||
};
|
||||
const setDate = (dateField) => (date) => this.setState({ [dateField]: date }, this.loadVisits);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
|
@ -166,20 +169,21 @@ const ShortUrlVisits = (
|
|||
<div className="row flex-md-row-reverse">
|
||||
<div className="col-lg-8 col-xl-6">
|
||||
<DateRangeRow
|
||||
startDate={this.state.startDate}
|
||||
endDate={this.state.endDate}
|
||||
onStartDateChange={setDate('startDate')}
|
||||
onEndDateChange={setDate('endDate')}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-lg-4 col-xl-6 mt-4 mt-lg-0">
|
||||
{showTableControls && (
|
||||
<Button
|
||||
outline
|
||||
block={this.state.isMobileDevice}
|
||||
onClick={() => this.setState(({ showTable }) => ({ showTable: !showTable }))}
|
||||
block={isMobileDevice}
|
||||
onClick={toggleTable}
|
||||
>
|
||||
Show table <FontAwesomeIcon icon={chevronDown} rotation={this.state.showTable ? 180 : undefined} />
|
||||
{showTable ? 'Hide' : 'Show'} table{' '}
|
||||
<FontAwesomeIcon icon={chevronDown} rotation={showTable ? 180 : undefined} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
@ -188,17 +192,13 @@ const ShortUrlVisits = (
|
|||
|
||||
{showTableControls && (
|
||||
<Collapse
|
||||
isOpen={this.state.showTable}
|
||||
isOpen={showTable}
|
||||
|
||||
// Enable stickiness only when there's no CSS animation, to avoid weird rendering effects
|
||||
onEntered={() => this.setState({ tableIsSticky: true })}
|
||||
onExiting={() => this.setState({ tableIsSticky: false })}
|
||||
onEntered={setSticky}
|
||||
onExiting={unsetSticky}
|
||||
>
|
||||
<VisitsTable
|
||||
visits={visits}
|
||||
isSticky={this.state.tableIsSticky}
|
||||
onVisitSelected={(highlightedVisit) => this.setState({ highlightedVisit })}
|
||||
/>
|
||||
<VisitsTable visits={visits} isSticky={tableIsSticky} onVisitSelected={setHighlightedVisit} />
|
||||
</Collapse>
|
||||
)}
|
||||
|
||||
|
@ -207,7 +207,11 @@ const ShortUrlVisits = (
|
|||
</section>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ShortUrlVisitsComp.propTypes = propTypes;
|
||||
|
||||
return ShortUrlVisitsComp;
|
||||
};
|
||||
|
||||
export default ShortUrlVisits;
|
||||
|
|
|
@ -38,10 +38,7 @@ describe('<ShortUrlVisits />', () => {
|
|||
return wrapper;
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
getShortUrlVisitsMock.mockReset();
|
||||
wrapper && wrapper.unmount();
|
||||
});
|
||||
afterEach(() => wrapper && wrapper.unmount());
|
||||
|
||||
it('renders a preloader when visits are loading', () => {
|
||||
const wrapper = createComponent({ loading: true, visits: [] });
|
||||
|
@ -91,9 +88,8 @@ describe('<ShortUrlVisits />', () => {
|
|||
dateRange.simulate('endDateChange', '2016-01-02T00:00:00+01:00');
|
||||
dateRange.simulate('endDateChange', '2016-01-03T00:00:00+01:00');
|
||||
|
||||
expect(getShortUrlVisitsMock).toHaveBeenCalledTimes(4);
|
||||
expect(wrapper.state('startDate')).toEqual('2016-01-01T00:00:00+01:00');
|
||||
expect(wrapper.state('endDate')).toEqual('2016-01-03T00:00:00+01:00');
|
||||
expect(wrapper.find(DateRangeRow).prop('startDate')).toEqual('2016-01-01T00:00:00+01:00');
|
||||
expect(wrapper.find(DateRangeRow).prop('endDate')).toEqual('2016-01-03T00:00:00+01:00');
|
||||
});
|
||||
|
||||
it('holds the map button content generator on cities graph extraHeaderContent', () => {
|
||||
|
|
Loading…
Reference in a new issue