feat: add previous period in website overview
This commit is contained in:
parent
835d7ff43d
commit
8ff5db80e2
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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) =>
|
||||||
|
@ -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,
|
||||||
}));
|
}));
|
||||||
|
@ -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({
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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);
|
||||||
}),
|
}),
|
||||||
|
Loading…
Reference in New Issue
Block a user