From 7ccbb922de62adce72a4df8e7d67597bd16aa139 Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Tue, 19 Sep 2023 19:29:28 +0800 Subject: [PATCH] feat: add metric bar data --- index.html | 2 +- src/client/api/model/website.ts | 64 ++++++++++- src/client/components/MetricCard.tsx | 40 +++++++ src/client/components/WebsiteOverview.tsx | 130 +++++++++++++++------- src/client/utils/common.ts | 36 ++++++ src/server/model/workspace.ts | 46 +++++++- src/server/router/workspace.ts | 90 ++++++++++++++- src/server/utils/prisma.ts | 6 + 8 files changed, 365 insertions(+), 49 deletions(-) create mode 100644 src/client/components/MetricCard.tsx create mode 100644 src/client/utils/common.ts diff --git a/index.html b/index.html index bd4282a..b3d805c 100644 --- a/index.html +++ b/index.html @@ -12,7 +12,7 @@ diff --git a/src/client/api/model/website.ts b/src/client/api/model/website.ts index 6875a91..0df7a11 100644 --- a/src/client/api/model/website.ts +++ b/src/client/api/model/website.ts @@ -1,4 +1,5 @@ import { useQuery } from '@tanstack/react-query'; +import { DateUnit } from '../../utils/date'; import { queryClient } from '../cache'; import { request } from '../request'; import { getUserTimezone } from './user'; @@ -126,15 +127,16 @@ export function useWorkspaceWebsitePageview( workspaceId: string, websiteId: string, startAt: number, - endAt: number + endAt: number, + unit: DateUnit ) { const { data, isLoading, refetch } = useQuery( - ['websitePageview', { workspaceId, websiteId }], + ['websitePageview', { workspaceId, websiteId, startAt, endAt }], () => { return getWorkspaceWebsitePageview(workspaceId, websiteId, { startAt, endAt, - unit: 'hour', + unit, timezone: getUserTimezone(), }); } @@ -147,3 +149,59 @@ export function useWorkspaceWebsitePageview( refetch, }; } + +export interface StatsItemType { + value: number; + change: number; +} +export async function getWorkspaceWebsiteStats( + workspaceId: string, + websiteId: string, + filter: Record +): Promise<{ + bounces: StatsItemType; + pageviews: StatsItemType; + totaltime: StatsItemType; + uniques: StatsItemType; +}> { + const { data } = await request.get( + `/api/workspace/${workspaceId}/website/${websiteId}/stats`, + { + params: { + ...filter, + }, + } + ); + + return data.stats; +} + +export function useWorkspaceWebsiteStats( + workspaceId: string, + websiteId: string, + startAt: number, + endAt: number, + unit: DateUnit +) { + const { + data: stats, + isLoading, + refetch, + } = useQuery( + ['websiteStats', { workspaceId, websiteId, startAt, endAt }], + () => { + return getWorkspaceWebsiteStats(workspaceId, websiteId, { + startAt, + endAt, + unit, + timezone: getUserTimezone(), + }); + } + ); + + return { + stats, + isLoading, + refetch, + }; +} diff --git a/src/client/components/MetricCard.tsx b/src/client/components/MetricCard.tsx new file mode 100644 index 0000000..c9e058a --- /dev/null +++ b/src/client/components/MetricCard.tsx @@ -0,0 +1,40 @@ +import { Tag } from 'antd'; +import React from 'react'; +import { formatNumber } from '../utils/common'; + +interface MetricCardProps { + value?: number; + change?: number; + label: string; + reverseColors?: boolean; + format?: (n: number) => string; + hideComparison?: boolean; +} +export const MetricCard: React.FC = React.memo((props) => { + const { + value = 0, + change = 0, + label, + reverseColors = false, + format = formatNumber, + hideComparison = false, + } = props; + + return ( +
+
+ {format(value)} +
+
+ {label} + {~~change !== 0 && !hideComparison && ( + = 0 ? 'green' : 'red'}> + {change > 0 && '+'} + {format(change)} + + )} +
+
+ ); +}); +MetricCard.displayName = 'MetricCard'; diff --git a/src/client/components/WebsiteOverview.tsx b/src/client/components/WebsiteOverview.tsx index 6615c61..3b624f9 100644 --- a/src/client/components/WebsiteOverview.tsx +++ b/src/client/components/WebsiteOverview.tsx @@ -5,7 +5,9 @@ import { ArrowRightOutlined, SyncOutlined } from '@ant-design/icons'; import { DateFilter } from './DateFilter'; import { HealthBar } from './HealthBar'; import { + StatsItemType, useWorkspaceWebsitePageview, + useWorkspaceWebsiteStats, useWorspaceWebsites, WebsiteInfo, } from '../api/model/website'; @@ -18,6 +20,8 @@ import { getDateArray, } from '../utils/date'; import { useEvent } from '../hooks/useEvent'; +import { MetricCard } from './MetricCard'; +import { formatNumber, formatShortTime } from '../utils/common'; interface WebsiteOverviewProps { workspaceId: string; @@ -48,16 +52,33 @@ const WebsiteOverviewItem: React.FC<{ const startDate = dayjs().subtract(1, 'day').add(1, unit).startOf(unit); const endDate = dayjs().endOf(unit); - const { pageviews, sessions, isLoading, refetch } = - useWorkspaceWebsitePageview( - props.website.workspaceId, - props.website.id, - startDate.unix() * 1000, - endDate.unix() * 1000 - ); + const { + pageviews, + sessions, + isLoading: isLoadingPageview, + refetch: refetchPageview, + } = useWorkspaceWebsitePageview( + props.website.workspaceId, + props.website.id, + startDate.unix() * 1000, + endDate.unix() * 1000, + unit + ); + + const { + stats, + isLoading: isLoadingStats, + refetch: refetchStats, + } = useWorkspaceWebsiteStats( + props.website.workspaceId, + props.website.id, + startDate.unix() * 1000, + endDate.unix() * 1000, + unit + ); const handleRefresh = useEvent(async () => { - await refetch(); + await Promise.all([refetchPageview(), refetchStats()]); message.success('Refreshed'); }); @@ -71,7 +92,7 @@ const WebsiteOverviewItem: React.FC<{ ]; }, [pageviews, sessions, unit]); - if (isLoading) { + if (isLoadingPageview || isLoadingStats) { return ; } @@ -98,17 +119,7 @@ const WebsiteOverviewItem: React.FC<{
-
- - - - -
+ {stats && }