mirror of https://github.com/msgbyte/tailchat
feat(admin): add analytics page
- activeGroupTop5 - activeUserTop5 - largeGroupTop5 - fileStorageUserTop5pull/105/merge
parent
288f5a61e8
commit
39e0b2cee7
@ -0,0 +1,203 @@
|
||||
import fileSize from 'filesize';
|
||||
import React from 'react';
|
||||
import {
|
||||
Card,
|
||||
Grid,
|
||||
Tooltip,
|
||||
Typography,
|
||||
useAsync,
|
||||
useTranslation,
|
||||
} from 'tushan';
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
ResponsiveContainer,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from 'tushan/chart';
|
||||
import { request } from '../../request';
|
||||
|
||||
export const TailchatAnalytics: React.FC = React.memo(() => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Grid.Row gutter={4}>
|
||||
<Grid.Col md={12}>
|
||||
<Card>
|
||||
<Typography.Title heading={4}>
|
||||
{t('custom.analytics.activeGroupTop5')}
|
||||
</Typography.Title>
|
||||
|
||||
<ActiveGroupChart />
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col md={12}>
|
||||
<Card>
|
||||
<Typography.Title heading={4}>
|
||||
{t('custom.analytics.activeUserTop5')}
|
||||
</Typography.Title>
|
||||
|
||||
<ActiveUserChart />
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
|
||||
<Grid.Row gutter={4} style={{ marginTop: 8 }}>
|
||||
<Grid.Col md={12}>
|
||||
<Card>
|
||||
<Typography.Title heading={4}>
|
||||
{t('custom.analytics.largeGroupTop5')}
|
||||
</Typography.Title>
|
||||
|
||||
<LargeGroupChart />
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col md={12}>
|
||||
<Card>
|
||||
<Typography.Title heading={4}>
|
||||
{t('custom.analytics.fileStorageUserTop5')}
|
||||
</Typography.Title>
|
||||
|
||||
<FileStorageChart />
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
TailchatAnalytics.displayName = 'TailchatAnalytics';
|
||||
|
||||
const ActiveGroupChart: React.FC = React.memo(() => {
|
||||
const { value } = useAsync(async () => {
|
||||
const { data } = await request.get<{
|
||||
activeGroups: {
|
||||
groupId: string;
|
||||
groupName: string;
|
||||
messageCount: number;
|
||||
}[];
|
||||
}>('/analytics/activeGroups');
|
||||
|
||||
return data.activeGroups;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height={320}>
|
||||
<BarChart
|
||||
data={value}
|
||||
layout="vertical"
|
||||
maxBarSize={40}
|
||||
margin={{ left: 60 }}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="messageCount" type="number" />
|
||||
<YAxis dataKey="groupName" type="category" />
|
||||
<Tooltip />
|
||||
<Bar dataKey="messageCount" fill="#8884d8" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
});
|
||||
ActiveGroupChart.displayName = 'ActiveGroupChart';
|
||||
|
||||
const ActiveUserChart: React.FC = React.memo(() => {
|
||||
const { value } = useAsync(async () => {
|
||||
const { data } = await request.get<{
|
||||
activeUsers: {
|
||||
groupId: string;
|
||||
groupName: string;
|
||||
messageCount: number;
|
||||
}[];
|
||||
}>('/analytics/activeUsers');
|
||||
|
||||
return data.activeUsers;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height={320}>
|
||||
<BarChart
|
||||
data={value}
|
||||
layout="vertical"
|
||||
maxBarSize={40}
|
||||
margin={{ left: 60 }}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="messageCount" type="number" />
|
||||
<YAxis dataKey="userName" type="category" />
|
||||
<Tooltip />
|
||||
<Bar dataKey="messageCount" fill="#8884d8" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
});
|
||||
ActiveUserChart.displayName = 'ActiveUserChart';
|
||||
|
||||
const LargeGroupChart: React.FC = React.memo(() => {
|
||||
const { value } = useAsync(async () => {
|
||||
const { data } = await request.get<{
|
||||
largeGroups: {
|
||||
name: string;
|
||||
memberCount: number;
|
||||
}[];
|
||||
}>('/analytics/largeGroups');
|
||||
|
||||
return data.largeGroups;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height={320}>
|
||||
<BarChart
|
||||
data={value}
|
||||
layout="vertical"
|
||||
maxBarSize={40}
|
||||
margin={{ left: 60 }}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="memberCount" type="number" />
|
||||
<YAxis dataKey="name" type="category" />
|
||||
<Tooltip />
|
||||
<Bar dataKey="memberCount" fill="#8884d8" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
});
|
||||
LargeGroupChart.displayName = 'LargeGroupChart';
|
||||
|
||||
const FileStorageChart: React.FC = React.memo(() => {
|
||||
const { value } = useAsync(async () => {
|
||||
const { data } = await request.get<{
|
||||
fileStorageUserTop: {
|
||||
userId: string;
|
||||
userName: string;
|
||||
fileStorageTotal: number;
|
||||
}[];
|
||||
}>('/analytics/fileStorageUserTop');
|
||||
|
||||
return data.fileStorageUserTop;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height={320}>
|
||||
<BarChart
|
||||
data={value}
|
||||
layout="vertical"
|
||||
maxBarSize={40}
|
||||
margin={{ left: 60 }}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="fileStorageTotal"
|
||||
type="number"
|
||||
tickFormatter={(val) => fileSize(val)}
|
||||
/>
|
||||
<YAxis dataKey="userName" type="category" />
|
||||
<Tooltip />
|
||||
<Bar dataKey="fileStorageTotal" fill="#8884d8" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
});
|
||||
FileStorageChart.displayName = 'FileStorageChart';
|
@ -0,0 +1,218 @@
|
||||
import { Router } from 'express';
|
||||
import { auth } from '../middleware/auth';
|
||||
import messageModel from '../../../../models/chat/message';
|
||||
import groupModel from '../../../../models/group/group';
|
||||
import fileModel from '../../../../models/file';
|
||||
import dayjs from 'dayjs';
|
||||
import { db } from 'tailchat-server-sdk';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/activeGroups', auth(), async (req, res) => {
|
||||
// 返回最近7天的最活跃的群组
|
||||
const day = 7;
|
||||
const aggregateRes: { _id: string; count: number }[] = await messageModel
|
||||
.aggregate([
|
||||
{
|
||||
$match: {
|
||||
createdAt: {
|
||||
$gte: dayjs().subtract(day, 'd').startOf('d').toDate(),
|
||||
$lt: dayjs().endOf('d').toDate(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$groupId' as any,
|
||||
count: {
|
||||
$sum: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
count: -1,
|
||||
},
|
||||
},
|
||||
{
|
||||
$limit: 5,
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: 'groups',
|
||||
localField: '_id',
|
||||
foreignField: '_id',
|
||||
as: 'groupInfo',
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
groupId: '$_id',
|
||||
messageCount: '$count',
|
||||
groupName: {
|
||||
$arrayElemAt: ['$groupInfo.name', 0],
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
.exec();
|
||||
|
||||
const activeGroups = aggregateRes;
|
||||
|
||||
res.json({ activeGroups });
|
||||
});
|
||||
|
||||
router.get('/activeUsers', auth(), async (req, res) => {
|
||||
// 返回最近7天的最活跃的用户
|
||||
const day = 7;
|
||||
const aggregateRes: { _id: string; count: number }[] = await messageModel
|
||||
.aggregate([
|
||||
{
|
||||
$match: {
|
||||
author: {
|
||||
$ne: new db.Types.ObjectId('000000000000000000000000'),
|
||||
},
|
||||
createdAt: {
|
||||
$gte: dayjs().subtract(day, 'd').startOf('d').toDate(),
|
||||
$lt: dayjs().endOf('d').toDate(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$author' as any,
|
||||
count: {
|
||||
$sum: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
count: -1,
|
||||
},
|
||||
},
|
||||
{
|
||||
$limit: 5,
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: 'users',
|
||||
localField: '_id',
|
||||
foreignField: '_id',
|
||||
as: 'userInfo',
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
userId: '$_id',
|
||||
messageCount: '$count',
|
||||
userName: {
|
||||
$concat: [
|
||||
{
|
||||
$arrayElemAt: ['$userInfo.nickname', 0],
|
||||
},
|
||||
'#',
|
||||
{
|
||||
$arrayElemAt: ['$userInfo.discriminator', 0],
|
||||
},
|
||||
],
|
||||
// $arrayElemAt: ['$userInfo.nickname', 0],
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
.exec();
|
||||
|
||||
const activeUsers = aggregateRes;
|
||||
|
||||
res.json({ activeUsers });
|
||||
});
|
||||
|
||||
router.get('/largeGroups', auth(), async (req, res) => {
|
||||
// 返回最大的 5 个群组
|
||||
const limit = 5;
|
||||
const aggregateRes: { _id: string; count: number }[] = await groupModel
|
||||
.aggregate([
|
||||
{
|
||||
$project: {
|
||||
name: 1,
|
||||
memberCount: {
|
||||
$size: '$members',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
memberCount: -1,
|
||||
},
|
||||
},
|
||||
{
|
||||
$limit: limit,
|
||||
},
|
||||
])
|
||||
.exec();
|
||||
|
||||
const largeGroups = aggregateRes;
|
||||
|
||||
res.json({ largeGroups });
|
||||
});
|
||||
|
||||
router.get('/fileStorageUserTop', auth(), async (req, res) => {
|
||||
// 返回最大的 5 个群组
|
||||
const limit = 5;
|
||||
const aggregateRes: { _id: string; count: number }[] = await fileModel
|
||||
.aggregate([
|
||||
{
|
||||
$group: {
|
||||
_id: '$userId',
|
||||
total: {
|
||||
$sum: '$size',
|
||||
},
|
||||
} as any,
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
total: -1,
|
||||
},
|
||||
},
|
||||
{
|
||||
$limit: limit,
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: 'users',
|
||||
localField: '_id',
|
||||
foreignField: '_id',
|
||||
as: 'userInfo',
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
userId: '$_id',
|
||||
fileStorageTotal: '$total',
|
||||
userName: {
|
||||
$concat: [
|
||||
{
|
||||
$arrayElemAt: ['$userInfo.nickname', 0],
|
||||
},
|
||||
'#',
|
||||
{
|
||||
$arrayElemAt: ['$userInfo.discriminator', 0],
|
||||
},
|
||||
],
|
||||
// $arrayElemAt: ['$userInfo.nickname', 0],
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
.exec();
|
||||
|
||||
const fileStorageUserTop = aggregateRes;
|
||||
|
||||
res.json({ fileStorageUserTop });
|
||||
});
|
||||
|
||||
export { router as analyticsRouter };
|
Loading…
Reference in New Issue