feat: add previous period in website overview

This commit is contained in:
moonrailgun 2024-01-31 01:22:01 +08:00
parent 835d7ff43d
commit 8ff5db80e2
6 changed files with 67 additions and 30 deletions

View File

@ -1,9 +1,11 @@
import { Tag } from 'antd'; import { Tag } from 'antd';
import React from 'react'; import React from 'react';
import { formatNumber } from '../../utils/common'; import { formatNumber } from '../../utils/common';
import { useGlobalStateStore } from '../../store/global';
interface MetricCardProps { interface MetricCardProps {
value?: number; value?: number;
prev?: number;
change?: number; change?: number;
label: string; label: string;
reverseColors?: boolean; reverseColors?: boolean;
@ -13,12 +15,16 @@ interface MetricCardProps {
export const MetricCard: React.FC<MetricCardProps> = React.memo((props) => { export const MetricCard: React.FC<MetricCardProps> = React.memo((props) => {
const { const {
value = 0, value = 0,
prev = 0,
change = 0, change = 0,
label, label,
reverseColors = false, reverseColors = false,
format = formatNumber, format = formatNumber,
hideComparison = false, hideComparison = false,
} = props; } = props;
const showPreviousPeriod = useGlobalStateStore(
(state) => state.showPreviousPeriod
);
return ( return (
<div className="flex flex-col justify-center min-w-[140px] min-h-[90px]"> <div className="flex flex-col justify-center min-w-[140px] min-h-[90px]">
@ -26,14 +32,25 @@ export const MetricCard: React.FC<MetricCardProps> = React.memo((props) => {
{format(value)} {format(value)}
</div> </div>
<div className="flex items-center whitespace-nowrap font-bold"> <div className="flex items-center whitespace-nowrap font-bold">
<span className="mr-2">{label}</span> <span className="mr-2 capitalize">{label}</span>
{~~change !== 0 && !hideComparison && ( {change !== 0 && !hideComparison && (
<Tag color={change * (reverseColors ? -1 : 1) >= 0 ? 'green' : 'red'}> <Tag color={change * (reverseColors ? -1 : 1) >= 0 ? 'green' : 'red'}>
{change > 0 && '+'} {change > 0 && '+'}
{format(change)} {format(change)}
</Tag> </Tag>
)} )}
</div> </div>
{showPreviousPeriod && (
<div className="mt-2 lg:mt-4 opacity-60">
<div className="flex items-center whitespace-nowrap font-bold text-4xl">
{format(prev)}
</div>
<div className="flex items-center whitespace-nowrap font-bold">
<span className="mr-2">Previous {label}</span>
</div>
</div>
)}
</div> </div>
); );
}); });

View File

@ -1,4 +1,4 @@
import { Button, message, Spin } from 'antd'; import { Button, message, Spin, Switch } from 'antd';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { Column, ColumnConfig } from '@ant-design/charts'; import { Column, ColumnConfig } from '@ant-design/charts';
import { SyncOutlined } from '@ant-design/icons'; import { SyncOutlined } from '@ant-design/icons';
@ -23,6 +23,7 @@ import { MonitorHealthBar } from '../monitor/MonitorHealthBar';
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
import { AppRouterOutput, trpc } from '../../api/trpc'; import { AppRouterOutput, trpc } from '../../api/trpc';
import { getUserTimezone } from '../../api/model/user'; import { getUserTimezone } from '../../api/model/user';
import { useGlobalStateStore } from '../../store/global';
export const WebsiteOverview: React.FC<{ export const WebsiteOverview: React.FC<{
website: WebsiteInfo; website: WebsiteInfo;
@ -32,6 +33,9 @@ export const WebsiteOverview: React.FC<{
const { website, showDateFilter = false, actions } = props; const { website, showDateFilter = false, actions } = props;
const { startDate, endDate, unit, refresh } = useGlobalRangeDate(); const { startDate, endDate, unit, refresh } = useGlobalRangeDate();
const navigate = useNavigate(); const navigate = useNavigate();
const showPreviousPeriod = useGlobalStateStore(
(state) => state.showPreviousPeriod
);
const { const {
pageviews, pageviews,
@ -113,7 +117,19 @@ export const WebsiteOverview: React.FC<{
<div className="flex mb-10 flex-wrap justify-between"> <div className="flex mb-10 flex-wrap justify-between">
<div className="flex-1">{stats && <MetricsBar stats={stats} />}</div> <div className="flex-1">{stats && <MetricsBar stats={stats} />}</div>
<div className="flex items-center gap-2 justify-end w-full lg:w-1/3"> <div className="flex items-center gap-2 justify-end flex-wrap w-full lg:w-1/3">
<div className="mr-2">
<Switch
checked={showPreviousPeriod}
onChange={(checked) =>
useGlobalStateStore.setState({
showPreviousPeriod: checked,
})
}
/>
<span className="ml-1">Previous period</span>
</div>
<Button <Button
size="large" size="large"
icon={<SyncOutlined />} icon={<SyncOutlined />}
@ -140,51 +156,51 @@ export const MetricsBar: React.FC<{
stats: AppRouterOutput['website']['stats']; stats: AppRouterOutput['website']['stats'];
}> = React.memo((props) => { }> = React.memo((props) => {
const { pageviews, uniques, bounces, totaltime } = props.stats || {}; const { pageviews, uniques, bounces, totaltime } = props.stats || {};
const num = Math.min(uniques.value, bounces.value); const bouncesNum = Math.min(uniques.value, bounces.value) / uniques.value;
const diffs = { const prevBouncesNum = Math.min(uniques.prev, bounces.prev) / uniques.prev;
pageviews: pageviews.value - pageviews.change,
uniques: uniques.value - uniques.change,
bounces: bounces.value - bounces.change,
totaltime: totaltime.value - totaltime.change,
};
return ( return (
<div className="flex gap-5 flex-wrap w-full"> <div className="flex gap-5 flex-wrap w-full">
<MetricCard <MetricCard
label="Views" label="views"
value={pageviews.value} value={pageviews.value}
change={pageviews.change} prev={pageviews.prev}
change={pageviews.value - pageviews.prev}
/> />
<MetricCard <MetricCard
label="Visitors" label="visitors"
value={uniques.value} value={uniques.value}
change={uniques.change} prev={uniques.prev}
change={uniques.value - uniques.prev}
/> />
<MetricCard <MetricCard
label="Bounce rate" label="bounce rate"
reverseColors={true} reverseColors={true}
value={uniques.value ? (num / uniques.value) * 100 : 0} value={uniques.value ? bouncesNum * 100 : 0}
prev={uniques.prev ? prevBouncesNum * 100 : 0}
change={ change={
uniques.value && uniques.change uniques.value && uniques.prev
? (num / uniques.value) * 100 - ? bouncesNum * 100 - prevBouncesNum * 100 || 0
(Math.min(diffs.uniques, diffs.bounces) / diffs.uniques) *
100 || 0
: 0 : 0
} }
format={(n) => formatNumber(n) + '%'} format={(n) => formatNumber(n) + '%'}
/> />
<MetricCard <MetricCard
label="Average visit time" label="average visit time"
value={ value={
totaltime.value && pageviews.value totaltime.value && pageviews.value
? totaltime.value / (pageviews.value - bounces.value) ? totaltime.value / (pageviews.value - bounces.value)
: 0 : 0
} }
prev={
totaltime.prev && pageviews.prev
? totaltime.prev / (pageviews.prev - bounces.prev)
: 0
}
change={ change={
totaltime.value && pageviews.value totaltime.value && pageviews.value
? (diffs.totaltime / (diffs.pageviews - diffs.bounces) - ? totaltime.value / (pageviews.value - bounces.value) -
totaltime.value / (pageviews.value - bounces.value)) * totaltime.prev / (pageviews.prev - bounces.prev) || 0
-1 || 0
: 0 : 0
} }
format={(n) => format={(n) =>

View File

@ -18,10 +18,12 @@ interface GlobalState {
dateRange: DateRange; dateRange: DateRange;
startDate: Dayjs | null; startDate: Dayjs | null;
endDate: Dayjs | null; endDate: Dayjs | null;
showPreviousPeriod: boolean;
} }
export const useGlobalStateStore = create<GlobalState>(() => ({ export const useGlobalStateStore = create<GlobalState>(() => ({
dateRange: DateRange.Last24Hours, dateRange: DateRange.Last24Hours,
startDate: null, startDate: null,
endDate: null, endDate: null,
showPreviousPeriod: false,
})); }));

View File

@ -15,7 +15,7 @@ export const websiteFilterSchema = z.object({
const websiteStatsItemType = z.object({ const websiteStatsItemType = z.object({
value: z.number(), value: z.number(),
change: z.number(), prev: z.number(),
}); });
export const websiteStatsSchema = z.object({ export const websiteStatsSchema = z.object({

View File

@ -91,7 +91,7 @@ export const http: MonitorProvider<{
return diff; return diff;
} catch (err) { } catch (err) {
logger.error('run monitor http error', String(err)); logger.error(`run monitor(${monitor.id}) http error`, String(err));
return -1; return -1;
} }
}, },

View File

@ -178,12 +178,14 @@ export const websiteRouter = router({
]); ]);
const stats = Object.keys(metrics[0]).reduce((obj, key) => { const stats = Object.keys(metrics[0]).reduce((obj, key) => {
const current = Number(metrics[0][key]) || 0;
const prev = Number(prevPeriod[0][key]) || 0;
obj[key] = { obj[key] = {
value: Number(metrics[0][key]) || 0, value: current,
change: Number(metrics[0][key]) - Number(prevPeriod[0][key]) || 0, prev,
}; };
return obj; return obj;
}, {} as Record<string, { value: number; change: number }>); }, {} as Record<string, { value: number; prev: number }>);
return websiteStatsSchema.parse(stats); return websiteStatsSchema.parse(stats);
}), }),