Progress check-in for date compare.

This commit is contained in:
Mike Cao 2024-05-23 19:35:29 -07:00
parent 24af06f3aa
commit 8cf7985dac
25 changed files with 181 additions and 61 deletions

View File

@ -7,11 +7,11 @@ import styles from './DateRangeSetting.module.css';
export function DateRangeSetting() {
const { formatMessage, labels } = useMessages();
const [dateRange, setDateRange] = useDateRange();
const { dateRange, saveDateRange } = useDateRange();
const { value } = dateRange;
const handleChange = (value: string | DateRange) => setDateRange(value);
const handleReset = () => setDateRange(DEFAULT_DATE_RANGE);
const handleChange = (value: string | DateRange) => saveDateRange(value);
const handleReset = () => saveDateRange(DEFAULT_DATE_RANGE);
return (
<Flexbox gap={10} width={300}>

View File

@ -4,17 +4,33 @@ import { getDateArray } from 'lib/date';
import useWebsitePageviews from 'components/hooks/queries/useWebsitePageviews';
import { useDateRange } from 'components/hooks';
export function WebsiteChart({ websiteId }: { websiteId: string }) {
const [dateRange] = useDateRange(websiteId);
export function WebsiteChart({
websiteId,
compareMode = false,
}: {
websiteId: string;
compareMode: boolean;
}) {
const { dateRange, dateCompare } = useDateRange(websiteId);
const { startDate, endDate, unit } = dateRange;
const { data, isLoading } = useWebsitePageviews(websiteId);
const { data, isLoading } = useWebsitePageviews(websiteId, compareMode ? dateCompare : undefined);
const { pageviews, sessions, compare } = (data || {}) as any;
const chartData = useMemo(() => {
if (data) {
return {
pageviews: getDateArray(data.pageviews, startDate, endDate, unit),
sessions: getDateArray(data.sessions, startDate, endDate, unit),
const result = {
pageviews: getDateArray(pageviews, startDate, endDate, unit),
sessions: getDateArray(sessions, startDate, endDate, unit),
};
if (compare) {
result['compare'] = {
pageviews: result.pageviews.map(({ x }, i) => ({ x, y: compare.pageviews[i].y })),
sessions: result.sessions.map(({ x }, i) => ({ x, y: compare.sessions[i].y })),
};
}
return result;
}
return { pageviews: [], sessions: [] };
}, [data, startDate, endDate, unit]);

View File

@ -21,7 +21,9 @@ export function WebsiteFilterButton({
const { formatMessage, labels } = useMessages();
const { renderUrl, router } = useNavigation();
const { fields } = useFields();
const [{ startDate, endDate }] = useDateRange(websiteId);
const {
dateRange: { startDate, endDate },
} = useDateRange(websiteId);
const handleAddFilter = ({ name, operator, value }) => {
const prefix = OPERATOR_PREFIXES[operator];

View File

@ -1,4 +1,3 @@
import { useState } from 'react';
import classNames from 'classnames';
import { useMessages, useSticky } from 'components/hooks';
import WebsiteDateFilter from 'components/input/WebsiteDateFilter';
@ -9,6 +8,7 @@ import WebsiteFilterButton from './WebsiteFilterButton';
import useWebsiteStats from 'components/hooks/queries/useWebsiteStats';
import styles from './WebsiteMetricsBar.module.css';
import { Dropdown, Item } from 'react-basics';
import useStore, { setWebsiteDateCompare } from 'store/websites';
export function WebsiteMetricsBar({
websiteId,
@ -22,9 +22,12 @@ export function WebsiteMetricsBar({
compareMode?: boolean;
}) {
const { formatMessage, labels } = useMessages();
const [compare, setCompare] = useState('prev');
const dateCompare = useStore(state => state[websiteId]?.dateCompare);
const { ref, isSticky } = useSticky({ enabled: sticky });
const { data, isLoading, isFetched, error } = useWebsiteStats(websiteId, compare);
const { data, isLoading, isFetched, error } = useWebsiteStats(
websiteId,
compareMode && dateCompare,
);
const { pageviews, visitors, visits, bounces, totaltime } = data || {};
@ -106,10 +109,10 @@ export function WebsiteMetricsBar({
<Dropdown
className={styles.dropdown}
items={items}
value={compare}
value={dateCompare || 'prev'}
renderValue={value => items.find(i => i.value === value)?.label}
alignment="end"
onChange={(e: any) => setCompare(e)}
onChange={(value: any) => setWebsiteDateCompare(websiteId, value)}
>
{items.map(({ label, value }) => (
<Item key={value}>{label}</Item>

View File

@ -21,7 +21,7 @@ export function WebsiteComparePage({ websiteId }) {
<WebsiteHeader websiteId={websiteId} />
<FilterTags websiteId={websiteId} params={params} />
<WebsiteMetricsBar websiteId={websiteId} compareMode={true} />
<WebsiteChart websiteId={websiteId} />
<WebsiteChart websiteId={websiteId} compareMode={true} />
</>
);
}

View File

@ -7,7 +7,7 @@ import styles from './EventDataMetricsBar.module.css';
export function EventDataMetricsBar({ websiteId }: { websiteId: string }) {
const { formatMessage, labels } = useMessages();
const { get, useQuery } = useApi();
const [dateRange] = useDateRange(websiteId);
const { dateRange } = useDateRange(websiteId);
const { startDate, endDate } = dateRange;
const { data, error, isLoading, isFetched } = useQuery({

View File

@ -6,7 +6,7 @@ import { useDateRange, useApi, useNavigation } from 'components/hooks';
import styles from './WebsiteEventData.module.css';
function useData(websiteId: string, event: string) {
const [dateRange] = useDateRange(websiteId);
const { dateRange } = useDateRange(websiteId);
const { startDate, endDate } = dateRange;
const { get, useQuery } = useApi();
const { data, error, isLoading } = useQuery({

View File

@ -26,7 +26,7 @@ export function BarChart(props: BarChartProps) {
stacked = false,
} = props;
const options = useMemo(() => {
const options: any = useMemo(() => {
return {
scales: {
x: {

View File

@ -79,15 +79,19 @@ export function Chart({
};
const updateChart = (data: any) => {
chart.current.data.datasets.forEach((dataset: { data: any }, index: string | number) => {
if (data?.datasets[index]) {
dataset.data = data?.datasets[index]?.data;
if (data.datasets.length === chart.current.data.datasets.length) {
chart.current.data.datasets.forEach((dataset: { data: any }, index: string | number) => {
if (data?.datasets[index]) {
dataset.data = data?.datasets[index]?.data;
if (chart.current.legend.legendItems[index]) {
chart.current.legend.legendItems[index].text = data?.datasets[index]?.label;
if (chart.current.legend.legendItems[index]) {
chart.current.legend.legendItems[index].text = data?.datasets[index]?.label;
}
}
}
});
});
} else {
chart.current.data.datasets = data.datasets;
}
chart.current.options = options;

View File

@ -4,14 +4,15 @@ import { useFilterParams } from '..//useFilterParams';
export function useWebsitePageviews(
websiteId: string,
compare?: string,
options?: Omit<UseQueryOptions, 'queryKey' | 'queryFn'>,
) {
const { get, useQuery } = useApi();
const params = useFilterParams(websiteId);
return useQuery({
queryKey: ['websites:pageviews', { websiteId, ...params }],
queryFn: () => get(`/websites/${websiteId}/pageviews`, params),
queryKey: ['websites:pageviews', { websiteId, ...params, compare }],
queryFn: () => get(`/websites/${websiteId}/pageviews`, { ...params, compare }),
enabled: !!websiteId,
...options,
});

View File

@ -1,19 +1,25 @@
import { getMinimumUnit, parseDateRange } from 'lib/date';
import { setItem } from 'next-basics';
import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE } from 'lib/constants';
import websiteStore, { setWebsiteDateRange } from 'store/websites';
import { DATE_RANGE_CONFIG, DEFAULT_DATE_COMPARE, DEFAULT_DATE_RANGE } from 'lib/constants';
import websiteStore, { setWebsiteDateRange, setWebsiteDateCompare } from 'store/websites';
import appStore, { setDateRange } from 'store/app';
import { DateRange } from 'lib/types';
import { useLocale } from './useLocale';
import { useApi } from './queries/useApi';
export function useDateRange(websiteId?: string): [DateRange, (value: string | DateRange) => void] {
export function useDateRange(websiteId?: string): {
dateRange: DateRange;
saveDateRange: (value: string | DateRange) => void;
dateCompare: string;
saveDateCompare: (value: string) => void;
} {
const { get } = useApi();
const { locale } = useLocale();
const websiteConfig = websiteStore(state => state[websiteId]?.dateRange);
const defaultConfig = DEFAULT_DATE_RANGE;
const globalConfig = appStore(state => state.dateRange);
const dateRange = parseDateRange(websiteConfig || globalConfig || defaultConfig, locale);
const dateCompare = websiteStore(state => state[websiteId]?.dateCompare || DEFAULT_DATE_COMPARE);
const saveDateRange = async (value: DateRange | string) => {
if (websiteId) {
@ -45,7 +51,11 @@ export function useDateRange(websiteId?: string): [DateRange, (value: string | D
}
};
return [dateRange, saveDateRange];
const saveDateCompare = (value: string) => {
setWebsiteDateCompare(websiteId, value);
};
return { dateRange, saveDateRange, dateCompare, saveDateCompare };
}
export default useDateRange;

View File

@ -4,7 +4,7 @@ import { useTimezone } from './useTimezone';
import { zonedTimeToUtc } from 'date-fns-tz';
export function useFilterParams(websiteId: string) {
const [dateRange] = useDateRange(websiteId);
const { dateRange } = useDateRange(websiteId);
const { startDate, endDate, unit } = dateRange;
const { timezone } = useTimezone();
const {

View File

@ -12,7 +12,7 @@ export function RefreshButton({
isLoading?: boolean;
}) {
const { formatMessage, labels } = useMessages();
const [dateRange] = useDateRange(websiteId);
const { dateRange } = useDateRange(websiteId);
function handleClick() {
if (!isLoading && dateRange) {

View File

@ -8,17 +8,17 @@ import { DateRange } from 'lib/types';
export function WebsiteDateFilter({ websiteId }: { websiteId: string }) {
const { dir } = useLocale();
const [dateRange, setDateRange] = useDateRange(websiteId);
const { dateRange, saveDateRange } = useDateRange(websiteId);
const { value, startDate, endDate, offset } = dateRange;
const disableForward =
value === 'all' || isAfter(getOffsetDateRange(dateRange, 1).startDate, new Date());
const handleChange = (value: string | DateRange) => {
setDateRange(value);
saveDateRange(value);
};
const handleIncrement = (increment: number) => {
setDateRange(getOffsetDateRange(dateRange, increment));
saveDateRange(getOffsetDateRange(dateRange, increment));
};
return (

View File

@ -13,7 +13,9 @@ export interface EventsChartProps {
}
export function EventsChart({ websiteId, className }: EventsChartProps) {
const [{ startDate, endDate, unit }] = useDateRange(websiteId);
const {
dateRange: { startDate, endDate, unit },
} = useDateRange(websiteId);
const { locale } = useLocale();
const { data, isLoading } = useWebsiteEvents(websiteId);

View File

@ -24,7 +24,7 @@ export function FilterTags({
}) {
const { formatMessage, labels } = useMessages();
const { formatValue } = useFormat();
const [dateRange] = useDateRange(websiteId);
const { dateRange } = useDateRange(websiteId);
const {
router,
renderUrl,

View File

@ -5,13 +5,9 @@
min-width: 150px;
}
.card.compare {
gap: 10px;
}
.card.compare .change {
font-size: 16px;
padding: 5px 10px;
margin: 10px 0;
}
.card:first-child {
@ -23,7 +19,7 @@
}
.value {
font-size: 40px;
font-size: 36px;
font-weight: 700;
white-space: nowrap;
color: var(--base900);
@ -46,7 +42,7 @@
gap: 5px;
font-size: 13px;
font-weight: 700;
padding: 0 5px;
padding: 0.1em 0.5em;
border-radius: 5px;
color: var(--base500);
align-self: flex-start;

View File

@ -48,7 +48,7 @@ export const MetricCard = ({
[styles.hide]: ~~change === 0,
})}
>
<Icon rotate={positive ? -45 : 45} size={showPrevious ? 'sm' : 'xs'}>
<Icon rotate={positive || reverseColors ? -45 : 45} size={showPrevious ? 'md' : 'xs'}>
<Icons.ArrowRight />
</Icon>
<animated.span title={changeProps?.x as any}>

View File

@ -5,8 +5,12 @@ import { renderDateLabels } from 'lib/charts';
export interface PageviewsChartProps extends BarChartProps {
data: {
sessions: any[];
pageviews: any[];
sessions: any[];
compare?: {
pageviews: any[];
sessions: any[];
};
};
unit: string;
isLoading?: boolean;
@ -36,7 +40,25 @@ export function PageviewsChart({ data, unit, isLoading, ...props }: PageviewsCha
borderWidth: 1,
...colors.chart.views,
},
],
data.compare
? {
type: 'line',
label: formatMessage(labels.visitors),
data: data.compare.pageviews,
borderWidth: 2,
borderColor: '#f15bb5',
}
: null,
data.compare
? {
type: 'line',
label: formatMessage(labels.visits),
data: data.compare.sessions,
borderWidth: 2,
borderColor: '#9b5de5',
}
: null,
].filter(n => n),
};
}, [data, locale]);

View File

@ -20,6 +20,7 @@ export const DEFAULT_DATE_RANGE = '24hour';
export const DEFAULT_WEBSITE_LIMIT = 10;
export const DEFAULT_RESET_DATE = '2000-01-01';
export const DEFAULT_PAGE_SIZE = 10;
export const DEFAULT_DATE_COMPARE = 'prev';
export const REALTIME_RANGE = 30;
export const REALTIME_INTERVAL = 5000;

View File

@ -326,3 +326,13 @@ export function getDateLength(startDate: Date, endDate: Date, unit: string | num
const { diff } = DATE_FUNCTIONS[unit];
return diff(endDate, startDate) + 1;
}
export function getCompareDate(compare: string, startDate: Date, endDate: Date) {
if (compare === 'yoy') {
return { startDate: subYears(startDate, 1), endDate: subYears(endDate, 1) };
}
const diff = differenceInMinutes(endDate, startDate);
return { startDate: subMinutes(startDate, diff), endDate: subMinutes(endDate, diff) };
}

View File

@ -1,3 +1,4 @@
import * as yup from 'yup';
import { canViewWebsite } from 'lib/auth';
import { useAuth, useCors, useValidate } from 'lib/middleware';
import { getRequestFilters, getRequestDateRange } from 'lib/request';
@ -5,6 +6,8 @@ import { NextApiRequestQueryBody, WebsitePageviews } from 'lib/types';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { getPageviewStats, getSessionStats } from 'queries';
import { TimezoneTest, UnitTypeTest } from 'lib/yup';
import { getCompareDate } from 'lib/date';
export interface WebsitePageviewRequestQuery {
websiteId: string;
@ -21,10 +24,9 @@ export interface WebsitePageviewRequestQuery {
country?: string;
region: string;
city?: string;
compare?: string;
}
import { TimezoneTest, UnitTypeTest } from 'lib/yup';
import * as yup from 'yup';
const schema = {
GET: yup.object().shape({
websiteId: yup.string().uuid().required(),
@ -41,6 +43,7 @@ const schema = {
country: yup.string(),
region: yup.string(),
city: yup.string(),
compare: yup.string(),
}),
};
@ -52,7 +55,7 @@ export default async (
await useAuth(req, res);
await useValidate(schema, req, res);
const { websiteId, timezone } = req.query;
const { websiteId, timezone, compare } = req.query;
if (req.method === 'GET') {
if (!(await canViewWebsite(req.auth, websiteId))) {
@ -74,6 +77,40 @@ export default async (
getSessionStats(websiteId, filters),
]);
if (compare) {
const { startDate: compareStartDate, endDate: compareEndDate } = getCompareDate(
compare,
startDate,
endDate,
);
const [comparePageviews, compareSessions] = await Promise.all([
getPageviewStats(websiteId, {
...filters,
startDate: compareStartDate,
endDate: compareEndDate,
}),
getSessionStats(websiteId, {
...filters,
startDate: compareStartDate,
endDate: compareEndDate,
}),
]);
return ok(res, {
pageviews,
sessions,
startDate,
endDate,
compare: {
pageviews: comparePageviews,
sessions: compareSessions,
startDate: compareStartDate,
endDate: compareEndDate,
},
});
}
return ok(res, { pageviews, sessions });
}

View File

@ -1,4 +1,4 @@
import { subMinutes, differenceInMinutes } from 'date-fns';
import * as yup from 'yup';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { canViewWebsite } from 'lib/auth';
@ -6,6 +6,7 @@ import { useAuth, useCors, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody, WebsiteStats } from 'lib/types';
import { getRequestFilters, getRequestDateRange } from 'lib/request';
import { getWebsiteStats } from 'queries';
import { getCompareDate } from 'lib/date';
export interface WebsiteStatsRequestQuery {
websiteId: string;
@ -25,7 +26,6 @@ export interface WebsiteStatsRequestQuery {
compare?: string;
}
import * as yup from 'yup';
const schema = {
GET: yup.object().shape({
websiteId: yup.string().uuid().required(),
@ -54,7 +54,7 @@ export default async (
await useAuth(req, res);
await useValidate(schema, req, res);
const { websiteId } = req.query;
const { websiteId, compare } = req.query;
if (req.method === 'GET') {
if (!(await canViewWebsite(req.auth, websiteId))) {
@ -62,9 +62,11 @@ export default async (
}
const { startDate, endDate } = await getRequestDateRange(req);
const diff = differenceInMinutes(endDate, startDate);
const prevStartDate = subMinutes(startDate, diff);
const prevEndDate = subMinutes(endDate, diff);
const { startDate: compareStartDate, endDate: compareEndDate } = getCompareDate(
compare,
startDate,
endDate,
);
const filters = getRequestFilters(req);
@ -72,8 +74,8 @@ export default async (
const prevPeriod = await getWebsiteStats(websiteId, {
...filters,
startDate: prevStartDate,
endDate: prevEndDate,
startDate: compareStartDate,
endDate: compareEndDate,
});
const stats = Object.keys(metrics[0]).reduce((obj, key) => {

View File

@ -67,7 +67,7 @@ async function clickhouseQuery(
`,
params,
).then(result => {
return Object.values(result).map(a => {
return Object.values(result).map((a: any) => {
return { x: a.x, y: Number(a.y) };
});
});

View File

@ -18,4 +18,18 @@ export function setWebsiteDateRange(websiteId: string, dateRange: DateRange) {
);
}
export function setWebsiteDateCompare(websiteId: string, dateCompare: string) {
store.setState(
produce(state => {
if (!state[websiteId]) {
state[websiteId] = {};
}
state[websiteId].dateCompare = dateCompare;
return state;
}),
);
}
export default store;