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';
|
|
|
|
import { HealthBar } from '../HealthBar';
|
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';
|
|
|
|
import { Loading } from '../Loading';
|
2023-09-15 16:03:09 +00:00
|
|
|
import dayjs from 'dayjs';
|
|
|
|
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-09-01 16:52:43 +00:00
|
|
|
|
2023-10-06 07:04:55 +00:00
|
|
|
export const WebsiteOverview: React.FC<{
|
2023-09-10 07:55:04 +00:00
|
|
|
website: WebsiteInfo;
|
2023-10-06 07:04:55 +00:00
|
|
|
actions?: React.ReactNode;
|
2023-09-10 07:55:04 +00:00
|
|
|
}> = React.memo((props) => {
|
2023-10-08 10:51:47 +00:00
|
|
|
const { startDate, endDate, unit } = useGlobalRangeDate();
|
2023-09-15 16:03:09 +00:00
|
|
|
|
2023-09-19 11:29:28 +00:00
|
|
|
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
|
|
|
|
);
|
2023-09-17 06:41:50 +00:00
|
|
|
|
|
|
|
const handleRefresh = useEvent(async () => {
|
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">
|
2023-09-10 07:55:04 +00:00
|
|
|
<span className="mr-2" title={props.website.domain ?? ''}>
|
|
|
|
{props.website.name}
|
|
|
|
</span>
|
2023-09-01 16:52:43 +00:00
|
|
|
|
|
|
|
<HealthBar
|
2023-09-10 07:55:04 +00:00
|
|
|
beats={Array.from({ length: 13 }).map(() => ({
|
|
|
|
status: 'health',
|
|
|
|
}))}
|
2023-09-01 16:52:43 +00:00
|
|
|
/>
|
2023-09-28 10:01:04 +00:00
|
|
|
|
|
|
|
<div className="ml-4 text-base font-normal">
|
|
|
|
<WebsiteOnlineCount
|
|
|
|
workspaceId={props.website.workspaceId}
|
|
|
|
websiteId={props.website.id}
|
|
|
|
/>
|
|
|
|
</div>
|
2023-09-01 16:52:43 +00:00
|
|
|
</div>
|
|
|
|
|
2023-10-06 07:04:55 +00:00
|
|
|
<div>{props.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
|
|
|
|
2023-10-08 10:43:31 +00:00
|
|
|
<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"
|
|
|
|
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) => {
|
2023-09-23 12:44:26 +00:00
|
|
|
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: {
|
2023-09-23 12:44:26 +00:00
|
|
|
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
|
|
|
},
|
2023-09-23 12:44:26 +00:00
|
|
|
} as 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';
|