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 React from 'react';
import { formatNumber } from '../../utils/common';
import { useGlobalStateStore } from '../../store/global';
interface MetricCardProps {
value?: number;
prev?: number;
change?: number;
label: string;
reverseColors?: boolean;
@ -13,12 +15,16 @@ interface MetricCardProps {
export const MetricCard: React.FC<MetricCardProps> = React.memo((props) => {
const {
value = 0,
prev = 0,
change = 0,
label,
reverseColors = false,
format = formatNumber,
hideComparison = false,
} = props;
const showPreviousPeriod = useGlobalStateStore(
(state) => state.showPreviousPeriod
);
return (
<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)}
</div>
<div className="flex items-center whitespace-nowrap font-bold">
<span className="mr-2">{label}</span>
{~~change !== 0 && !hideComparison && (
<span className="mr-2 capitalize">{label}</span>
{change !== 0 && !hideComparison && (
<Tag color={change * (reverseColors ? -1 : 1) >= 0 ? 'green' : 'red'}>
{change > 0 && '+'}
{format(change)}
</Tag>
)}
</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>
);
});

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

View File

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

View File

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

View File

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

View File

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