Move date range selection into redux.

This commit is contained in:
Mike Cao 2020-08-31 14:11:30 -07:00
parent d06c077019
commit c5cb19a3bf
21 changed files with 141 additions and 138 deletions

View File

@ -3,10 +3,8 @@ import classNames from 'classnames';
import WebsiteChart from 'components/metrics/WebsiteChart';
import WorldMap from 'components/common/WorldMap';
import Page from 'components/layout/Page';
import WebsiteHeader from 'components/metrics/WebsiteHeader';
import MenuLayout from 'components/layout/MenuLayout';
import Button from 'components/common/Button';
import { getDateRange } from 'lib/date';
import Arrow from 'assets/arrow-right.svg';
import styles from './WebsiteDetails.module.css';
import PagesTable from './metrics/PagesTable';
@ -20,15 +18,12 @@ import EventsChart from './metrics/EventsChart';
import useFetch from 'hooks/useFetch';
import Loading from 'components/common/Loading';
export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' }) {
export default function WebsiteDetails({ websiteId }) {
const { data } = useFetch(`/api/website/${websiteId}`);
const [chartLoaded, setChartLoaded] = useState(false);
const [countryData, setCountryData] = useState();
const [eventsData, setEventsData] = useState();
const [expand, setExpand] = useState();
const [refresh, setRefresh] = useState(0);
const [dateRange, setDateRange] = useState(getDateRange(defaultDateRange));
const { startDate, endDate, unit } = dateRange;
const { data } = useFetch(`/api/website/${websiteId}`);
const BackButton = () => (
<Button
@ -50,23 +45,12 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
{ label: 'Browsers', value: 'browser', component: BrowsersTable },
{ label: 'Operating system', value: 'os', component: OSTable },
{ label: 'Devices', value: 'device', component: DevicesTable },
{
label: 'Countries',
value: 'country',
component: props => <CountriesTable {...props} onDataLoad={data => setCountryData(data)} />,
},
{ label: 'Countries', value: 'country', component: CountriesTable },
{ label: 'Events', value: 'event', component: EventsTable },
];
const dataProps = {
websiteId,
startDate,
endDate,
unit,
};
const tableProps = {
...dataProps,
websiteId,
websiteDomain: data?.domain,
limit: 10,
onExpand: handleExpand,
@ -79,11 +63,9 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
}
function handleDataLoad() {
if (!chartLoaded) setTimeout(() => setChartLoaded(true), 300);
if (!chartLoaded) {
setTimeout(() => setChartLoaded(true), 300);
}
function handleDateChange(values) {
setTimeout(() => setDateRange(values), 300);
}
function handleExpand(value) {
@ -94,10 +76,6 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
setExpand(getSelectedMenuOption(value));
}
function handleRefresh() {
setRefresh(state => state + 1);
}
if (!data) {
return null;
}
@ -106,17 +84,11 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
<Page>
<div className="row">
<div className={classNames(styles.chart, 'col')}>
<WebsiteHeader
<WebsiteChart
websiteId={websiteId}
title={data.name}
onRefresh={handleRefresh}
showLink={false}
/>
<WebsiteChart
key={refresh}
websiteId={websiteId}
onDataLoad={handleDataLoad}
onDateChange={handleDateChange}
showLink={false}
stickyHeader
/>
</div>
@ -158,7 +130,7 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
<EventsTable {...tableProps} onDataLoad={setEventsData} />
</div>
<div className="col-12 col-md-12 col-lg-8 pt-5 pb-5">
<EventsChart {...dataProps} />
<EventsChart websiteId={websiteId} />
</div>
</div>
</>

View File

@ -1,6 +1,5 @@
import React from 'react';
import { useRouter } from 'next/router';
import WebsiteHeader from 'components/metrics/WebsiteHeader';
import WebsiteChart from 'components/metrics/WebsiteChart';
import Page from 'components/layout/Page';
import Button from 'components/common/Button';
@ -21,8 +20,7 @@ export default function WebsiteList() {
<Page>
{data.map(({ website_id, name }) => (
<div key={website_id} className={styles.website}>
<WebsiteHeader websiteId={website_id} title={name} showLink />
<WebsiteChart websiteId={website_id} />
<WebsiteChart websiteId={website_id} title={name} showLink />
</div>
))}
{data.length === 0 && (

View File

@ -20,10 +20,12 @@ export default function DropDown({
setShowMenu(state => !state);
}
function handleSelect(value, e) {
function handleSelect(selected, e) {
e.stopPropagation();
setShowMenu(false);
onChange(value);
if (selected !== value) {
onChange(selected);
}
}
useDocumentClick(e => {

View File

@ -1,7 +1,18 @@
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { setDateRange } from 'redux/actions/websites';
import Button from './Button';
import Refresh from 'assets/redo.svg';
export default function RefreshButton({ onClick }) {
return <Button icon={<Refresh />} size="small" onClick={onClick} />;
export default function RefreshButton({ websiteId }) {
const dispatch = useDispatch();
const dateRange = useSelector(state => state.websites[websiteId]?.dateRange);
function handleClick() {
if (dateRange) {
dispatch(setDateRange(websiteId, { ...dateRange }));
}
}
return <Button icon={<Refresh />} size="small" onClick={handleClick} />;
}

View File

@ -2,15 +2,13 @@ import React from 'react';
import MetricsTable from './MetricsTable';
import { browserFilter } from 'lib/filters';
export default function BrowsersTable({ websiteId, startDate, endDate, limit, onExpand }) {
export default function BrowsersTable({ websiteId, limit, onExpand }) {
return (
<MetricsTable
title="Browsers"
type="browser"
metric="Visitors"
websiteId={websiteId}
startDate={startDate}
endDate={endDate}
limit={limit}
dataFilter={browserFilter}
onExpand={onExpand}

View File

@ -2,22 +2,13 @@ import React from 'react';
import MetricsTable from './MetricsTable';
import { countryFilter, percentFilter } from 'lib/filters';
export default function CountriesTable({
websiteId,
startDate,
endDate,
limit,
onDataLoad,
onExpand,
}) {
export default function CountriesTable({ websiteId, limit, onDataLoad = () => {}, onExpand }) {
return (
<MetricsTable
title="Countries"
type="country"
metric="Visitors"
websiteId={websiteId}
startDate={startDate}
endDate={endDate}
limit={limit}
dataFilter={countryFilter}
onDataLoad={data => onDataLoad(percentFilter(data))}

View File

@ -2,15 +2,13 @@ import React from 'react';
import MetricsTable from './MetricsTable';
import { deviceFilter } from 'lib/filters';
export default function DevicesTable({ websiteId, startDate, endDate, limit, onExpand }) {
export default function DevicesTable({ websiteId, limit, onExpand }) {
return (
<MetricsTable
title="Devices"
type="device"
metric="Visitors"
websiteId={websiteId}
startDate={startDate}
endDate={endDate}
limit={limit}
dataFilter={deviceFilter}
onExpand={onExpand}

View File

@ -1,8 +1,9 @@
import React, { useMemo } from 'react';
import tinycolor from 'tinycolor2';
import BarChart from './BarChart';
import { getTimezone, getDateArray, getDateLength } from 'lib/date';
import { getTimezone, getDateArray, getDateLength, getDateRange } from 'lib/date';
import useFetch from 'hooks/useFetch';
import { useSelector } from 'react-redux';
const COLORS = [
'#2680eb',
@ -15,13 +16,19 @@ const COLORS = [
'#85d044',
];
export default function EventsChart({ websiteId, startDate, endDate, unit }) {
const { data } = useFetch(`/api/website/${websiteId}/events`, {
export default function EventsChart({ websiteId, defaultDateRange = '7day' }) {
const dateRange = useSelector(state => state.websites[websiteId]?.dateRange);
const { startDate, endDate, unit, modified } = dateRange || getDateRange(defaultDateRange);
const { data } = useFetch(
`/api/website/${websiteId}/events`,
{
start_at: +startDate,
end_at: +endDate,
unit,
tz: getTimezone(),
});
},
{ update: [modified] },
);
const datasets = useMemo(() => {
if (!data) return [];

View File

@ -2,22 +2,13 @@ import React from 'react';
import MetricsTable from './MetricsTable';
import styles from './EventsTable.module.css';
export default function EventsTable({
websiteId,
startDate,
endDate,
limit,
onExpand,
onDataLoad,
}) {
export default function EventsTable({ websiteId, limit, onExpand, onDataLoad }) {
return (
<MetricsTable
title="Events"
type="event"
metric="Actions"
websiteId={websiteId}
startDate={startDate}
endDate={endDate}
limit={limit}
renderLabel={({ x }) => <Label value={x} />}
onExpand={onExpand}

View File

@ -5,12 +5,22 @@ import Loading from 'components/common/Loading';
import useFetch from 'hooks/useFetch';
import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
import styles from './MetricsBar.module.css';
import { useSelector } from 'react-redux';
import { getDateRange } from '../../lib/date';
export default function MetricsBar({ websiteId, startDate, endDate, className }) {
const { data } = useFetch(`/api/website/${websiteId}/metrics`, {
export default function MetricsBar({ websiteId, className, defaultDateRange = '7day' }) {
const dateRange = useSelector(state => state.websites[websiteId]?.dateRange);
const { startDate, endDate, modified } = dateRange || getDateRange(defaultDateRange);
const { data } = useFetch(
`/api/website/${websiteId}/metrics`,
{
start_at: +startDate,
end_at: +endDate,
});
},
{
update: [modified],
},
);
const [format, setFormat] = useState(true);
const formatFunc = format ? formatLongNumber : formatNumber;

View File

@ -1,4 +1,5 @@
import React, { useState, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { FixedSizeList } from 'react-window';
import { useSpring, animated, config } from 'react-spring';
import classNames from 'classnames';
@ -9,14 +10,13 @@ import Arrow from 'assets/arrow-right.svg';
import { percentFilter } from 'lib/filters';
import { formatNumber, formatLongNumber } from 'lib/format';
import styles from './MetricsTable.module.css';
import { getDateRange } from '../../lib/date';
export default function MetricsTable({
title,
metric,
websiteId,
websiteDomain,
startDate,
endDate,
title,
metric,
type,
className,
dataFilter,
@ -24,9 +24,12 @@ export default function MetricsTable({
limit,
headerComponent,
renderLabel,
defaultDateRange = '7day',
onDataLoad = () => {},
onExpand = () => {},
}) {
const dateRange = useSelector(state => state.websites[websiteId]?.dateRange);
const { startDate, endDate, modified } = dateRange || getDateRange(defaultDateRange);
const { data } = useFetch(
`/api/website/${websiteId}/rankings`,
{
@ -35,7 +38,7 @@ export default function MetricsTable({
end_at: +endDate,
domain: websiteDomain,
},
{ onDataLoad },
{ onDataLoad, delay: 300, update: [modified] },
);
const [format, setFormat] = useState(true);
const formatFunc = format ? formatLongNumber : formatNumber;

View File

@ -2,15 +2,13 @@ import React from 'react';
import MetricsTable from './MetricsTable';
import { osFilter } from 'lib/filters';
export default function OSTable({ websiteId, startDate, endDate, limit, onExpand }) {
export default function OSTable({ websiteId, limit, onExpand }) {
return (
<MetricsTable
title="Operating System"
type="os"
metric="Visitors"
websiteId={websiteId}
startDate={startDate}
endDate={endDate}
limit={limit}
dataFilter={osFilter}
onExpand={onExpand}

View File

@ -3,14 +3,7 @@ import MetricsTable from './MetricsTable';
import { urlFilter } from 'lib/filters';
import ButtonGroup from '../common/ButtonGroup';
export default function PagesTable({
websiteId,
websiteDomain,
startDate,
endDate,
limit,
onExpand,
}) {
export default function PagesTable({ websiteId, websiteDomain, limit, onExpand }) {
const [filter, setFilter] = useState('Combined');
return (
@ -20,8 +13,6 @@ export default function PagesTable({
metric="Views"
headerComponent={limit ? null : <FilterButtons selected={filter} onClick={setFilter} />}
websiteId={websiteId}
startDate={startDate}
endDate={endDate}
limit={limit}
dataFilter={urlFilter}
filterOptions={{ domain: websiteDomain, raw: filter === 'Raw' }}

View File

@ -1,9 +1,8 @@
import React from 'react';
import CheckVisible from 'components/helpers/CheckVisible';
import BarChart from './BarChart';
import { getDateLength } from '../../lib/date';
export default function PageviewsChart({ websiteId, data, startDate, endDate, unit, className }) {
export default function PageviewsChart({ websiteId, data, unit, records, className }) {
const handleUpdate = chart => {
const {
data: { datasets },
@ -44,7 +43,7 @@ export default function PageviewsChart({ websiteId, data, startDate, endDate, un
},
]}
unit={unit}
records={getDateLength(startDate, endDate, unit)}
records={records}
animationDuration={visible ? 300 : 0}
onUpdate={handleUpdate}
/>

View File

@ -12,8 +12,10 @@ const options = {
export default function QuickButtons({ value, onChange }) {
const selectedItem = Object.keys(options).find(key => options[key] === value);
function handleClick(value) {
onChange(getDateRange(options[value]));
function handleClick(selected) {
if (options[selected] !== value) {
onChange(getDateRange(options[selected]));
}
}
return (

View File

@ -1,16 +1,9 @@
import React, { useState } from 'react';
import MetricsTable from './MetricsTable';
import { refFilter } from 'lib/filters';
import ButtonGroup from '../common/ButtonGroup';
import ButtonGroup from 'components/common/ButtonGroup';
export default function Referrers({
websiteId,
websiteDomain,
startDate,
endDate,
limit,
onExpand = () => {},
}) {
export default function ReferrersTable({ websiteId, websiteDomain, limit, onExpand = () => {} }) {
const [filter, setFilter] = useState('Combined');
const renderLink = ({ x: url }) => {
@ -31,8 +24,6 @@ export default function Referrers({
headerComponent={limit ? null : <FilterButtons selected={filter} onClick={setFilter} />}
websiteId={websiteId}
websiteDomain={websiteDomain}
startDate={startDate}
endDate={endDate}
limit={limit}
dataFilter={refFilter}
filterOptions={{

View File

@ -1,4 +1,5 @@
import React, { useState, useMemo } from 'react';
import React, { useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import classNames from 'classnames';
import PageviewsChart from './PageviewsChart';
import MetricsBar from './MetricsBar';
@ -6,18 +7,23 @@ import QuickButtons from './QuickButtons';
import DateFilter from 'components/common/DateFilter';
import StickyHeader from 'components/helpers/StickyHeader';
import useFetch from 'hooks/useFetch';
import { getDateArray, getDateRange, getTimezone } from 'lib/date';
import { getDateArray, getDateLength, getDateRange, getTimezone } from 'lib/date';
import { setDateRange } from 'redux/actions/websites';
import styles from './WebsiteChart.module.css';
import WebsiteHeader from './WebsiteHeader';
export default function WebsiteChart({
websiteId,
title,
defaultDateRange = '7day',
stickyHeader = false,
showLink = false,
onDataLoad = () => {},
onDateChange = () => {},
}) {
const [dateRange, setDateRange] = useState(getDateRange(defaultDateRange));
const { startDate, endDate, unit, value } = dateRange;
const dispatch = useDispatch();
const dateRange = useSelector(state => state.websites[websiteId]?.dateRange);
const { startDate, endDate, unit, value, modified } = dateRange || getDateRange(defaultDateRange);
const { data } = useFetch(
`/api/website/${websiteId}/pageviews`,
{
@ -26,7 +32,7 @@ export default function WebsiteChart({
unit,
tz: getTimezone(),
},
{ onDataLoad },
{ onDataLoad, update: [modified] },
);
const [pageviews, uniques] = useMemo(() => {
@ -40,24 +46,19 @@ export default function WebsiteChart({
}, [data]);
function handleDateChange(values) {
setDateRange(values);
onDateChange(values);
dispatch(setDateRange(websiteId, values));
}
return (
<>
<WebsiteHeader websiteId={websiteId} title={title} showLink={showLink} />
<div className={classNames(styles.header, 'row')}>
<StickyHeader
className={classNames(styles.metrics, 'col row')}
stickyClassName={styles.sticky}
enabled={stickyHeader}
>
<MetricsBar
className="col-12 col-md-9 col-lg-10"
websiteId={websiteId}
startDate={startDate}
endDate={endDate}
/>
<MetricsBar className="col-12 col-md-9 col-lg-10" websiteId={websiteId} />
<DateFilter
className="col-12 col-md-3 col-lg-2"
value={value}
@ -67,7 +68,12 @@ export default function WebsiteChart({
</div>
<div className="row">
<div className="col">
<PageviewsChart websiteId={websiteId} data={{ pageviews, uniques }} unit={unit} />
<PageviewsChart
websiteId={websiteId}
data={{ pageviews, uniques }}
unit={unit}
records={getDateLength(startDate, endDate, unit)}
/>
<QuickButtons value={value} onChange={handleDateChange} />
</div>
</div>

View File

@ -8,7 +8,7 @@ import styles from './WebsiteHeader.module.css';
import RefreshButton from '../common/RefreshButton';
import ButtonLayout from '../layout/ButtonLayout';
export default function WebsiteHeader({ websiteId, title, showLink = false, onRefresh }) {
export default function WebsiteHeader({ websiteId, title, showLink = false }) {
const router = useRouter();
return (
@ -16,7 +16,7 @@ export default function WebsiteHeader({ websiteId, title, showLink = false, onRe
<div className={styles.title}>{title}</div>
<ActiveUsers className={styles.active} websiteId={websiteId} />
<ButtonLayout>
<RefreshButton onClick={onRefresh} />
<RefreshButton websiteId={websiteId} />
{showLink && (
<Button
icon={<Arrow />}

View File

@ -23,9 +23,9 @@ export default function useFetch(url, params = {}, options = {}) {
useEffect(() => {
if (url) {
const { interval } = options;
const { interval, delay = 0 } = options;
loadData();
setTimeout(() => loadData(), delay);
const id = interval ? setInterval(() => loadData(), interval) : null;

34
redux/actions/websites.js Normal file
View File

@ -0,0 +1,34 @@
import { createSlice } from '@reduxjs/toolkit';
import produce from 'immer';
const websites = createSlice({
name: 'user',
initialState: {},
reducers: {
updateWebsites(state, action) {
state = action.payload;
return state;
},
},
});
export const { updateWebsites } = websites.actions;
export default websites.reducer;
export function setDateRange(websiteId, dateRange) {
return (dispatch, getState) => {
const state = getState();
let { websites = {} } = state;
websites = produce(websites, draft => {
if (!draft[websiteId]) {
draft[websiteId] = {};
}
dateRange.modified = Date.now();
draft[websiteId].dateRange = dateRange;
});
return dispatch(updateWebsites(websites));
};
}

View File

@ -1,4 +1,5 @@
import { combineReducers } from 'redux';
import user from './actions/user';
import websites from './actions/websites';
export default combineReducers({ user });
export default combineReducers({ user, websites });