mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-24 01:48:18 +03:00
Improved VisitsTable performance by memoizing visits lists
This commit is contained in:
parent
06b63d1af2
commit
bd4255108d
4 changed files with 23 additions and 26 deletions
|
@ -35,8 +35,8 @@ const Message = ({ children, loading = false, noMargin = false, type = 'default'
|
||||||
<Card className={cardClasses} body>
|
<Card className={cardClasses} body>
|
||||||
<h3 className={classNames('text-center mb-0', getTextClassForType(type))}>
|
<h3 className={classNames('text-center mb-0', getTextClassForType(type))}>
|
||||||
{loading && <FontAwesomeIcon icon={preloader} spin />}
|
{loading && <FontAwesomeIcon icon={preloader} spin />}
|
||||||
{loading && !children && <span className="ml-2">Loading...</span>}
|
{loading && <span className="ml-2">{children || 'Loading...'}</span>}
|
||||||
{children}
|
{!loading && children}
|
||||||
</h3>
|
</h3>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
const TEN_ROUNDING_NUMBER = 10;
|
const TEN_ROUNDING_NUMBER = 10;
|
||||||
const { ceil } = Math;
|
const { ceil } = Math;
|
||||||
|
|
||||||
|
const formatter = new Intl.NumberFormat('en-US');
|
||||||
|
|
||||||
|
export const prettify = (number) => formatter.format(number);
|
||||||
|
|
||||||
export const roundTen = (number) => ceil(number / TEN_ROUNDING_NUMBER) * TEN_ROUNDING_NUMBER;
|
export const roundTen = (number) => ceil(number / TEN_ROUNDING_NUMBER) * TEN_ROUNDING_NUMBER;
|
||||||
|
|
|
@ -78,6 +78,7 @@ const ShortUrlVisits = (
|
||||||
render() {
|
render() {
|
||||||
const { shortUrlVisits, shortUrlDetail } = this.props;
|
const { shortUrlVisits, shortUrlDetail } = this.props;
|
||||||
const { visits, loading, loadingLarge, error } = shortUrlVisits;
|
const { visits, loading, loadingLarge, error } = shortUrlVisits;
|
||||||
|
const showTableControls = !loading && visits.length > 0;
|
||||||
|
|
||||||
const renderVisitsContent = () => {
|
const renderVisitsContent = () => {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
|
@ -166,7 +167,7 @@ const ShortUrlVisits = (
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-lg-4 col-xl-6 mt-4 mt-lg-0">
|
<div className="col-lg-4 col-xl-6 mt-4 mt-lg-0">
|
||||||
{visits.length > 0 && (
|
{showTableControls && (
|
||||||
<Button
|
<Button
|
||||||
outline
|
outline
|
||||||
block={this.state.isMobileDevice}
|
block={this.state.isMobileDevice}
|
||||||
|
@ -179,7 +180,7 @@ const ShortUrlVisits = (
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{!loading && visits.length > 0 && (
|
{showTableControls && (
|
||||||
<Collapse
|
<Collapse
|
||||||
isOpen={this.state.showTable}
|
isOpen={this.state.showTable}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Moment from 'react-moment';
|
import Moment from 'react-moment';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
@ -13,6 +13,7 @@ import SimplePaginator from '../common/SimplePaginator';
|
||||||
import SearchField from '../utils/SearchField';
|
import SearchField from '../utils/SearchField';
|
||||||
import { browserFromUserAgent, extractDomain, osFromUserAgent } from '../utils/helpers/visits';
|
import { browserFromUserAgent, extractDomain, osFromUserAgent } from '../utils/helpers/visits';
|
||||||
import { determineOrderDir } from '../utils/utils';
|
import { determineOrderDir } from '../utils/utils';
|
||||||
|
import { prettify } from '../utils/helpers/numbers';
|
||||||
import { visitType } from './reducers/shortUrlVisits';
|
import { visitType } from './reducers/shortUrlVisits';
|
||||||
import './VisitsTable.scss';
|
import './VisitsTable.scss';
|
||||||
|
|
||||||
|
@ -56,39 +57,30 @@ const normalizeVisits = map(({ userAgent, date, referer, visitLocation }) => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const VisitsTable = ({ visits, onVisitSelected, isSticky = false, matchMedia = window.matchMedia }) => {
|
const VisitsTable = ({ visits, onVisitSelected, isSticky = false, matchMedia = window.matchMedia }) => {
|
||||||
const allVisits = normalizeVisits(visits);
|
|
||||||
const headerCellsClass = classNames('visits-table__header-cell', {
|
const headerCellsClass = classNames('visits-table__header-cell', {
|
||||||
'visits-table__sticky': isSticky,
|
'visits-table__sticky': isSticky,
|
||||||
});
|
});
|
||||||
const matchMobile = () => matchMedia('(max-width: 767px)').matches;
|
const matchMobile = () => matchMedia('(max-width: 767px)').matches;
|
||||||
|
|
||||||
const [ selectedVisit, setSelectedVisit ] = useState(undefined);
|
const [ selectedVisit, setSelectedVisit ] = useState(undefined);
|
||||||
|
const [ isMobileDevice, setIsMobileDevice ] = useState(matchMobile());
|
||||||
const [ page, setPage ] = useState(1);
|
const [ page, setPage ] = useState(1);
|
||||||
const [ searchTerm, setSearchTerm ] = useState(undefined);
|
const [ searchTerm, setSearchTerm ] = useState(undefined);
|
||||||
const [ order, setOrder ] = useState({ field: undefined, dir: undefined });
|
const [ order, setOrder ] = useState({ field: undefined, dir: undefined });
|
||||||
const [ currentPage, setCurrentPageVisits ] = useState(calculateVisits(allVisits, page, searchTerm, order));
|
const allVisits = useMemo(() => normalizeVisits(visits), [ visits ]);
|
||||||
const [ isMobileDevice, setIsMobileDevice ] = useState(matchMobile());
|
const currentPage = useMemo(() => calculateVisits(allVisits, page, searchTerm, order), [ page, searchTerm, order ]);
|
||||||
|
|
||||||
const orderByColumn = (field) => () => setOrder({ field, dir: determineOrderDir(field, order.field, order.dir) });
|
const orderByColumn = (field) => () => setOrder({ field, dir: determineOrderDir(field, order.field, order.dir) });
|
||||||
const renderOrderIcon = (field) => {
|
const renderOrderIcon = (field) => order.dir && order.field === field && (
|
||||||
if (!order.dir || order.field !== field) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={order.dir === 'ASC' ? caretUpIcon : caretDownIcon}
|
icon={order.dir === 'ASC' ? caretUpIcon : caretDownIcon}
|
||||||
className="visits-table__header-icon"
|
className="visits-table__header-icon"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onVisitSelected && onVisitSelected(selectedVisit);
|
onVisitSelected && onVisitSelected(selectedVisit);
|
||||||
}, [ selectedVisit ]);
|
}, [ selectedVisit ]);
|
||||||
useEffect(() => {
|
|
||||||
setCurrentPageVisits(calculateVisits(allVisits, page, searchTerm, order));
|
|
||||||
}, [ page, searchTerm, order ]);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const listener = () => setIsMobileDevice(matchMobile());
|
const listener = () => setIsMobileDevice(matchMobile());
|
||||||
|
|
||||||
|
@ -188,9 +180,9 @@ const VisitsTable = ({ visits, onVisitSelected, isSticky = false, matchMedia = w
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
Visits <b>{currentPage.start + 1}</b> to{' '}
|
Visits <b>{prettify(currentPage.start + 1)}</b> to{' '}
|
||||||
<b>{min(currentPage.end, currentPage.total)}</b> of{' '}
|
<b>{prettify(min(currentPage.end, currentPage.total))}</b> of{' '}
|
||||||
<b>{currentPage.total}</b>
|
<b>{prettify(currentPage.total)}</b>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue