tianji/src/client/components/website/WebsiteOverview.tsx

243 lines
6.4 KiB
TypeScript
Raw Normal View History

2023-10-08 10:43:31 +00:00
import { Button, message, Spin } from 'antd';
2023-09-15 16:03:09 +00:00
import React, { useMemo } from 'react';
import { Column, ColumnConfig } from '@ant-design/charts';
2023-10-06 07:04:55 +00:00
import { SyncOutlined } from '@ant-design/icons';
2023-10-06 14:08:15 +00:00
import { DateFilter } from '../DateFilter';
2023-09-15 16:03:09 +00:00
import {
2023-09-19 11:29:28 +00:00
StatsItemType,
2023-09-15 16:03:09 +00:00
useWorkspaceWebsitePageview,
2023-09-19 11:29:28 +00:00
useWorkspaceWebsiteStats,
2023-09-15 16:03:09 +00:00
WebsiteInfo,
2023-10-06 14:08:15 +00:00
} from '../../api/model/website';
2023-09-15 16:03:09 +00:00
import {
DateUnit,
formatDate,
formatDateWithUnit,
getDateArray,
2023-10-06 14:08:15 +00:00
} from '../../utils/date';
import { useEvent } from '../../hooks/useEvent';
import { MetricCard } from '../MetricCard';
import { formatNumber, formatShortTime } from '../../utils/common';
import { useTheme } from '../../hooks/useTheme';
import { WebsiteOnlineCount } from '../WebsiteOnlineCount';
2023-10-08 10:43:31 +00:00
import { useGlobalRangeDate } from '../../hooks/useGlobalRangeDate';
2023-10-14 16:51:03 +00:00
import { MonitorHealthBar } from '../monitor/MonitorHealthBar';
import { useNavigate } from 'react-router';
2023-09-01 16:52:43 +00:00
2023-10-06 07:04:55 +00:00
export const WebsiteOverview: React.FC<{
website: WebsiteInfo;
showDateFilter?: boolean;
2023-10-06 07:04:55 +00:00
actions?: React.ReactNode;
}> = React.memo((props) => {
const { website, showDateFilter = false, actions } = props;
const { startDate, endDate, unit, refresh } = useGlobalRangeDate();
const navigate = useNavigate();
2023-09-15 16:03:09 +00:00
2023-09-19 11:29:28 +00:00
const {
pageviews,
sessions,
isLoading: isLoadingPageview,
refetch: refetchPageview,
} = useWorkspaceWebsitePageview(
website.workspaceId,
website.id,
2023-09-19 11:29:28 +00:00
startDate.unix() * 1000,
endDate.unix() * 1000,
unit
);
const {
stats,
isLoading: isLoadingStats,
refetch: refetchStats,
} = useWorkspaceWebsiteStats(
website.workspaceId,
website.id,
2023-09-19 11:29:28 +00:00
startDate.unix() * 1000,
endDate.unix() * 1000,
unit
);
2023-09-17 06:41:50 +00:00
const handleRefresh = useEvent(async () => {
refresh();
2023-09-19 11:29:28 +00:00
await Promise.all([refetchPageview(), refetchStats()]);
2023-09-17 06:41:50 +00:00
message.success('Refreshed');
});
2023-09-15 16:03:09 +00:00
const chartData = useMemo(() => {
2023-09-16 08:12:19 +00:00
const pageviewsArr = getDateArray(pageviews, startDate, endDate, unit);
const sessionsArr = getDateArray(sessions, startDate, endDate, unit);
return [
...pageviewsArr.map((item) => ({ ...item, type: 'pageview' })),
...sessionsArr.map((item) => ({ ...item, type: 'session' })),
];
}, [pageviews, sessions, unit]);
2023-09-15 16:03:09 +00:00
2023-10-08 10:43:31 +00:00
const loading = isLoadingPageview || isLoadingStats;
2023-09-15 16:03:09 +00:00
2023-09-01 16:52:43 +00:00
return (
2023-10-08 10:43:31 +00:00
<Spin spinning={loading}>
2023-09-01 16:52:43 +00:00
<div className="flex">
<div className="flex flex-1 text-2xl font-bold items-center">
<span className="mr-2" title={website.domain ?? ''}>
{website.name}
</span>
2023-09-01 16:52:43 +00:00
{website.monitorId && (
<div
className="cursor-pointer"
onClick={() => navigate(`/monitor/${website.monitorId}`)}
>
<MonitorHealthBar monitorId={website.monitorId} />
</div>
2023-10-14 16:51:03 +00:00
)}
2023-09-28 10:01:04 +00:00
<div className="ml-4 text-base font-normal">
<WebsiteOnlineCount
workspaceId={website.workspaceId}
websiteId={website.id}
2023-09-28 10:01:04 +00:00
/>
</div>
2023-09-01 16:52:43 +00:00
</div>
<div>{actions}</div>
2023-09-01 16:52:43 +00:00
</div>
<div className="flex mb-10 flex-wrap">
2023-09-19 11:29:28 +00:00
{stats && <MetricsBar stats={stats} />}
2023-09-01 16:52:43 +00:00
<div className="flex items-center gap-2 justify-end w-full lg:w-1/3">
2023-09-17 06:41:50 +00:00
<Button
size="large"
icon={<SyncOutlined />}
onClick={handleRefresh}
/>
2023-09-01 16:52:43 +00:00
{showDateFilter && (
<div>
<DateFilter />
</div>
)}
2023-09-01 16:52:43 +00:00
</div>
</div>
<div>
2023-09-15 16:03:09 +00:00
<StatsChart data={chartData} unit={unit} />
2023-09-01 16:52:43 +00:00
</div>
2023-10-08 10:43:31 +00:00
</Spin>
2023-09-01 16:52:43 +00:00
);
});
2023-10-06 07:04:55 +00:00
WebsiteOverview.displayName = 'WebsiteOverview';
2023-09-01 16:52:43 +00:00
2023-09-19 11:29:28 +00:00
export const MetricsBar: React.FC<{
stats: {
bounces: StatsItemType;
pageviews: StatsItemType;
totaltime: StatsItemType;
uniques: StatsItemType;
};
2023-09-01 16:52:43 +00:00
}> = React.memo((props) => {
2023-09-19 11:29:28 +00:00
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,
};
2023-09-01 16:52:43 +00:00
return (
2023-09-19 11:29:28 +00:00
<div className="flex gap-5 flex-wrap w-full lg:w-2/3">
<MetricCard
label="Views"
value={pageviews.value}
change={pageviews.change}
/>
<MetricCard
label="Visitors"
value={uniques.value}
change={uniques.change}
/>
<MetricCard
label="Bounce rate"
2023-11-02 16:30:36 +00:00
reverseColors={true}
2023-09-19 11:29:28 +00:00
value={uniques.value ? (num / uniques.value) * 100 : 0}
change={
uniques.value && uniques.change
? (num / uniques.value) * 100 -
(Math.min(diffs.uniques, diffs.bounces) / diffs.uniques) *
100 || 0
: 0
}
format={(n) => formatNumber(n) + '%'}
/>
<MetricCard
label="Average visit time"
value={
totaltime.value && pageviews.value
? totaltime.value / (pageviews.value - bounces.value)
: 0
}
change={
totaltime.value && pageviews.value
? (diffs.totaltime / (diffs.pageviews - diffs.bounces) -
totaltime.value / (pageviews.value - bounces.value)) *
-1 || 0
: 0
}
format={(n) =>
`${n < 0 ? '-' : ''}${formatShortTime(
Math.abs(~~n),
['m', 's'],
' '
)}`
}
/>
2023-09-01 16:52:43 +00:00
</div>
);
});
2023-09-19 11:29:28 +00:00
MetricsBar.displayName = 'MetricsBar';
2023-09-01 16:52:43 +00:00
2023-09-15 16:03:09 +00:00
export const StatsChart: React.FC<{
2023-09-16 08:12:19 +00:00
data: { x: string; y: number; type: string }[];
2023-09-15 16:03:09 +00:00
unit: DateUnit;
}> = React.memo((props) => {
const { colors } = useTheme();
const config = useMemo(
() =>
({
data: props.data,
isStack: true,
xField: 'x',
yField: 'y',
seriesField: 'type',
2023-09-15 16:03:09 +00:00
label: {
position: 'middle' as const,
style: {
fill: '#FFFFFF',
opacity: 0.6,
},
},
tooltip: {
title: (t) => formatDate(t),
},
color: [colors.chart.pv, colors.chart.uv],
xAxis: {
label: {
autoHide: true,
autoRotate: false,
formatter: (text) => formatDateWithUnit(text, props.unit),
},
2023-09-15 16:03:09 +00:00
},
} satisfies ColumnConfig),
2023-09-15 16:03:09 +00:00
[props.data, props.unit]
);
2023-09-01 16:52:43 +00:00
return <Column {...config} />;
});
2023-09-15 16:03:09 +00:00
StatsChart.displayName = 'StatsChart';