From af3b38a3738e0ee4028720de046dbd21d185fd4f Mon Sep 17 00:00:00 2001
From: actiontech-zihan
Date: Tue, 23 Jun 2026 07:16:34 +0000
Subject: [PATCH 1/8] feat(ee): add user activity report page
Add EE-only user activity dashboard with KPI cards, trend charts, module and
hourly distribution, user ranking table, API client, routes, and QuickActions entry.
---
packages/base/src/locale/en-US/dmsMenu.ts | 1 +
packages/base/src/locale/en-US/index.ts | 4 +-
.../base/src/locale/en-US/userActivity.ts | 45 +
packages/base/src/locale/zh-CN/dmsMenu.ts | 1 +
packages/base/src/locale/zh-CN/index.ts | 4 +-
.../base/src/locale/zh-CN/userActivity.ts | 45 +
.../page/Nav/SideMenu/QuickActions/index.tsx | 10 +-
.../__snapshots__/index.test.tsx.snap | 888 ++++++++++++++++++
.../UserActivity/__tests__/index.test.tsx | 26 +
.../components/DailyTrendChart/index.tsx | 77 ++
.../HourlyDistributionChart/index.tsx | 65 ++
.../ModuleDistributionChart/index.tsx | 69 ++
.../components/SummaryCards/index.tsx | 67 ++
.../components/UserRankingTable/index.tsx | 108 +++
packages/base/src/page/UserActivity/index.tsx | 192 ++++
packages/base/src/page/UserActivity/style.ts | 37 +
packages/base/src/page/UserActivity/utils.ts | 50 +
packages/base/src/router/router.base.tsx | 7 +
.../__snapshots__/router.base.test.tsx.snap | 21 +
.../base/src/router/test/router.base.test.tsx | 9 +
packages/dms-kit/src/data/routePaths.ts | 1 +
packages/shared/lib/api/base/index.ts | 1 +
.../api/base/service/UserActivity/index.d.ts | 94 ++
.../api/base/service/UserActivity/index.ts | 84 ++
.../shared/lib/testUtil/mockApi/base/index.ts | 1 +
.../mockApi/base/userActivity/data.ts | 83 ++
.../mockApi/base/userActivity/index.ts | 70 ++
27 files changed, 2057 insertions(+), 3 deletions(-)
create mode 100644 packages/base/src/locale/en-US/userActivity.ts
create mode 100644 packages/base/src/locale/zh-CN/userActivity.ts
create mode 100644 packages/base/src/page/UserActivity/__tests__/__snapshots__/index.test.tsx.snap
create mode 100644 packages/base/src/page/UserActivity/__tests__/index.test.tsx
create mode 100644 packages/base/src/page/UserActivity/components/DailyTrendChart/index.tsx
create mode 100644 packages/base/src/page/UserActivity/components/HourlyDistributionChart/index.tsx
create mode 100644 packages/base/src/page/UserActivity/components/ModuleDistributionChart/index.tsx
create mode 100644 packages/base/src/page/UserActivity/components/SummaryCards/index.tsx
create mode 100644 packages/base/src/page/UserActivity/components/UserRankingTable/index.tsx
create mode 100644 packages/base/src/page/UserActivity/index.tsx
create mode 100644 packages/base/src/page/UserActivity/style.ts
create mode 100644 packages/base/src/page/UserActivity/utils.ts
create mode 100644 packages/shared/lib/api/base/service/UserActivity/index.d.ts
create mode 100644 packages/shared/lib/api/base/service/UserActivity/index.ts
create mode 100644 packages/shared/lib/testUtil/mockApi/base/userActivity/data.ts
create mode 100644 packages/shared/lib/testUtil/mockApi/base/userActivity/index.ts
diff --git a/packages/base/src/locale/en-US/dmsMenu.ts b/packages/base/src/locale/en-US/dmsMenu.ts
index 80b7b23499..c6be6b10eb 100644
--- a/packages/base/src/locale/en-US/dmsMenu.ts
+++ b/packages/base/src/locale/en-US/dmsMenu.ts
@@ -60,6 +60,7 @@ export default {
title: 'Global settings',
userCenter: 'User center',
reportStatistics: 'Report statistics',
+ userActivity: 'User activity',
viewRule: 'View rule',
ruleManage: 'Rule management',
system: 'System settings',
diff --git a/packages/base/src/locale/en-US/index.ts b/packages/base/src/locale/en-US/index.ts
index ec027a1ea6..7f7a81d75e 100644
--- a/packages/base/src/locale/en-US/index.ts
+++ b/packages/base/src/locale/en-US/index.ts
@@ -14,6 +14,7 @@ import dmsDataExport from './dmsDataExport';
import dmsGlobalDataSource from './dmsGlobalDataSource';
import resourceOverview from './resourceOverview';
import availabilityZone from './availabilityZone';
+import userActivity from './userActivity';
// eslint-disable-next-line import/no-anonymous-default-export
export default {
@@ -33,6 +34,7 @@ export default {
dmsDataExport,
dmsGlobalDataSource,
resourceOverview,
- availabilityZone
+ availabilityZone,
+ userActivity
}
};
diff --git a/packages/base/src/locale/en-US/userActivity.ts b/packages/base/src/locale/en-US/userActivity.ts
new file mode 100644
index 0000000000..4cbf96bb64
--- /dev/null
+++ b/packages/base/src/locale/en-US/userActivity.ts
@@ -0,0 +1,45 @@
+// eslint-disable-next-line import/no-anonymous-default-export
+export default {
+ title: 'User activity',
+ statDate: 'Stat date',
+ dateRange: 'Trend range',
+ refresh: 'Refresh',
+ summary: {
+ dau: 'Daily active users',
+ requestCount: 'Total requests',
+ avgRequestPerUser: 'Avg requests per user',
+ errorRate: 'Error rate',
+ errorCount: 'Error requests',
+ peakHour: 'Peak hour',
+ peakHourRequests: 'Peak hour requests'
+ },
+ dailyTrend: {
+ title: 'Daily activity trend',
+ dau: 'DAU',
+ requestCount: 'Requests',
+ errorCount: 'Errors',
+ emptyText: 'No data in selected range'
+ },
+ moduleDistribution: {
+ title: 'Module distribution',
+ requestCount: 'Requests',
+ percent: 'Share',
+ emptyText: 'No module data for selected date'
+ },
+ hourlyDistribution: {
+ title: '24-hour distribution',
+ hour: 'Hour',
+ requestCount: 'Requests',
+ activeUsers: 'Active users',
+ emptyText: 'No hourly data for selected date'
+ },
+ userRanking: {
+ title: 'User activity ranking',
+ userName: 'User',
+ activeDays: 'Active days',
+ requestCount: 'Requests',
+ topModule: 'Top module',
+ lastActiveAt: 'Last active',
+ emptyText: 'No user activity in selected range'
+ }
+};
diff --git a/packages/base/src/locale/zh-CN/dmsMenu.ts b/packages/base/src/locale/zh-CN/dmsMenu.ts
index 232d307dd4..f19c8b75f7 100644
--- a/packages/base/src/locale/zh-CN/dmsMenu.ts
+++ b/packages/base/src/locale/zh-CN/dmsMenu.ts
@@ -65,6 +65,7 @@ export default {
title: '全局设置',
userCenter: '用户中心',
reportStatistics: '报表统计',
+ userActivity: '用户使用情况',
viewRule: '查看规则',
ruleManage: '规则管理',
system: '系统设置',
diff --git a/packages/base/src/locale/zh-CN/index.ts b/packages/base/src/locale/zh-CN/index.ts
index e748dcbd27..f507c7af11 100644
--- a/packages/base/src/locale/zh-CN/index.ts
+++ b/packages/base/src/locale/zh-CN/index.ts
@@ -14,6 +14,7 @@ import dmsAccount from './dmsAccount';
import dmsDataExport from './dmsDataExport';
import dmsGlobalDataSource from './dmsGlobalDataSource';
import resourceOverview from './resourceOverview';
+import userActivity from './userActivity';
import availabilityZone from './availabilityZone';
// eslint-disable-next-line import/no-anonymous-default-export
export default {
@@ -33,6 +34,7 @@ export default {
dmsDataExport,
dmsGlobalDataSource,
resourceOverview,
- availabilityZone
+ availabilityZone,
+ userActivity
}
};
diff --git a/packages/base/src/locale/zh-CN/userActivity.ts b/packages/base/src/locale/zh-CN/userActivity.ts
new file mode 100644
index 0000000000..9ecc5216c6
--- /dev/null
+++ b/packages/base/src/locale/zh-CN/userActivity.ts
@@ -0,0 +1,45 @@
+// eslint-disable-next-line import/no-anonymous-default-export
+export default {
+ title: '用户使用情况',
+ statDate: '统计日期',
+ dateRange: '趋势区间',
+ refresh: '刷新',
+ summary: {
+ dau: '日活跃用户',
+ requestCount: '请求总数',
+ avgRequestPerUser: '人均请求数',
+ errorRate: '错误率',
+ errorCount: '错误请求数',
+ peakHour: '高峰时段',
+ peakHourRequests: '高峰请求数'
+ },
+ dailyTrend: {
+ title: '日活跃趋势',
+ dau: '日活跃用户',
+ requestCount: '请求数',
+ errorCount: '错误数',
+ emptyText: '所选区间暂无数据'
+ },
+ moduleDistribution: {
+ title: '模块访问分布',
+ requestCount: '请求数',
+ percent: '占比',
+ emptyText: '所选日期暂无模块访问数据'
+ },
+ hourlyDistribution: {
+ title: '24 小时访问分布',
+ hour: '时段',
+ requestCount: '请求数',
+ activeUsers: '活跃用户数',
+ emptyText: '所选日期暂无时段数据'
+ },
+ userRanking: {
+ title: '用户活跃排行',
+ userName: '用户名',
+ activeDays: '活跃天数',
+ requestCount: '请求数',
+ topModule: '最常用模块',
+ lastActiveAt: '最近活跃时间',
+ emptyText: '所选区间暂无用户活跃数据'
+ }
+};
diff --git a/packages/base/src/page/Nav/SideMenu/QuickActions/index.tsx b/packages/base/src/page/Nav/SideMenu/QuickActions/index.tsx
index a7d089de1f..5fce494308 100644
--- a/packages/base/src/page/Nav/SideMenu/QuickActions/index.tsx
+++ b/packages/base/src/page/Nav/SideMenu/QuickActions/index.tsx
@@ -5,7 +5,8 @@ import {
SignalFilled,
ProfileSquareFilled,
RingOutlined,
- BookMarkOutlined
+ BookMarkOutlined,
+ UserCircleFilled
} from '@actiontech/icons';
import { QuickActionsStyleWrapper } from '../style';
import { BasicToolTip, EmptyBox } from '@actiontech/dms-kit';
@@ -86,6 +87,13 @@ const QuickActions: React.FC = ({
icon: ,
permission: PERMISSIONS.PAGES.SQLE.REPORT_STATISTICS
},
+ {
+ key: 'user-activity',
+ title: t('dmsMenu.globalSettings.userActivity'),
+ path: ROUTE_PATHS.BASE.USER_ACTIVITY,
+ icon: ,
+ permission: PERMISSIONS.PAGES.BASE.RESOURCE_OVERVIEW
+ },
{
key: 'view-rule',
title: t('dmsMenu.globalSettings.viewRule'),
diff --git a/packages/base/src/page/UserActivity/__tests__/__snapshots__/index.test.tsx.snap b/packages/base/src/page/UserActivity/__tests__/__snapshots__/index.test.tsx.snap
new file mode 100644
index 0000000000..13e2ef514a
--- /dev/null
+++ b/packages/base/src/page/UserActivity/__tests__/__snapshots__/index.test.tsx.snap
@@ -0,0 +1,888 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`base/page/UserActivity render init snap 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+ 日活跃用户
+
+
+
+
+ 3
+
+
+
+
+
+
+
+
+
+
+
+
+ 请求总数
+
+
+
+
+ 13
+
+
+
+
+
+
+
+
+
+
+
+
+ 人均请求数
+
+
+
+
+ 4
+
+
+ .33
+
+
+
+
+
+
+
+
+
+
+
+
+ 错误率
+
+
+
+ 7.69%
+
+
+
+
+
+
+
+
+
+
+
+ 错误请求数
+
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+ 高峰时段
+
+
+
+ 09:00-10:00
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/packages/base/src/page/UserActivity/__tests__/index.test.tsx b/packages/base/src/page/UserActivity/__tests__/index.test.tsx
new file mode 100644
index 0000000000..159d97bfd7
--- /dev/null
+++ b/packages/base/src/page/UserActivity/__tests__/index.test.tsx
@@ -0,0 +1,26 @@
+import { baseSuperRender } from '../../../testUtils/superRender';
+import UserActivity from '../index';
+import userActivity from '@actiontech/shared/lib/testUtil/mockApi/base/userActivity';
+import { mockUseCurrentUser } from '@actiontech/shared/lib/testUtil/mockHook/mockUseCurrentUser';
+import { act, cleanup } from '@testing-library/react';
+
+describe('base/page/UserActivity', () => {
+ beforeEach(() => {
+ userActivity.mockAllApi();
+ jest.useFakeTimers();
+ mockUseCurrentUser();
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ cleanup();
+ });
+
+ it('render init snap', async () => {
+ const { container } = baseSuperRender();
+ await act(async () => {
+ jest.advanceTimersByTime(3000);
+ });
+ expect(container).toMatchSnapshot();
+ });
+});
diff --git a/packages/base/src/page/UserActivity/components/DailyTrendChart/index.tsx b/packages/base/src/page/UserActivity/components/DailyTrendChart/index.tsx
new file mode 100644
index 0000000000..346ea83be8
--- /dev/null
+++ b/packages/base/src/page/UserActivity/components/DailyTrendChart/index.tsx
@@ -0,0 +1,77 @@
+import { useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Line, LineConfig } from '@ant-design/plots';
+import ChartWrapper from 'sqle/src/components/ChartCom/ChartWrapper';
+import { useChangeTheme } from '@actiontech/shared/lib/features';
+import useThemeStyleData from '../../../../hooks/useThemeStyleData';
+import { IUserActivityDailyTrendItem } from '@actiontech/shared/lib/api/base/service/UserActivity/index.d';
+
+type DailyTrendChartProps = {
+ loading?: boolean;
+ errorMessage?: string;
+ data?: IUserActivityDailyTrendItem[];
+ onRefresh?: () => void;
+};
+
+const DailyTrendChart = ({
+ loading,
+ errorMessage,
+ data,
+ onRefresh
+}: DailyTrendChartProps) => {
+ const { t } = useTranslation();
+ const { currentTheme } = useChangeTheme();
+ const { sqleTheme } = useThemeStyleData();
+
+ const chartData = useMemo(() => {
+ const result: Array<{
+ stat_date: string;
+ value: number;
+ category: string;
+ }> = [];
+ data?.forEach((item) => {
+ result.push({
+ stat_date: item.stat_date ?? '',
+ value: item.dau ?? 0,
+ category: t('userActivity.dailyTrend.dau')
+ });
+ result.push({
+ stat_date: item.stat_date ?? '',
+ value: item.request_count ?? 0,
+ category: t('userActivity.dailyTrend.requestCount')
+ });
+ });
+ return result;
+ }, [data, t]);
+
+ const config: LineConfig = {
+ data: chartData,
+ xField: 'stat_date',
+ yField: 'value',
+ seriesField: 'category',
+ color: sqleTheme.statistics.rectColor,
+ smooth: true,
+ legend: {
+ position: 'top'
+ },
+ xAxis: {
+ label: {
+ autoRotate: true
+ }
+ }
+ };
+
+ return (
+
+
+
+ );
+};
+
+export default DailyTrendChart;
diff --git a/packages/base/src/page/UserActivity/components/HourlyDistributionChart/index.tsx b/packages/base/src/page/UserActivity/components/HourlyDistributionChart/index.tsx
new file mode 100644
index 0000000000..1244276ed1
--- /dev/null
+++ b/packages/base/src/page/UserActivity/components/HourlyDistributionChart/index.tsx
@@ -0,0 +1,65 @@
+import { useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Column, ColumnConfig } from '@ant-design/plots';
+import ChartWrapper from 'sqle/src/components/ChartCom/ChartWrapper';
+import { useChangeTheme } from '@actiontech/shared/lib/features';
+import useThemeStyleData from '../../../../hooks/useThemeStyleData';
+import { IUserActivityHourlyDistributionItem } from '@actiontech/shared/lib/api/base/service/UserActivity/index.d';
+import { formatHourLabel } from '../../utils';
+
+type HourlyDistributionChartProps = {
+ loading?: boolean;
+ errorMessage?: string;
+ data?: IUserActivityHourlyDistributionItem[];
+ onRefresh?: () => void;
+};
+
+const HourlyDistributionChart = ({
+ loading,
+ errorMessage,
+ data,
+ onRefresh
+}: HourlyDistributionChartProps) => {
+ const { t } = useTranslation();
+ const { currentTheme } = useChangeTheme();
+ const { sharedTheme } = useThemeStyleData();
+
+ const chartData = useMemo(
+ () =>
+ data?.map((item) => ({
+ hour_label: formatHourLabel(item.stat_hour),
+ request_count: item.request_count ?? 0,
+ active_users: item.active_users ?? 0
+ })) ?? [],
+ [data]
+ );
+
+ const config: ColumnConfig = {
+ data: chartData,
+ xField: 'hour_label',
+ yField: 'request_count',
+ color: sharedTheme.uiToken.colorPrimary,
+ meta: {
+ hour_label: {
+ alias: t('userActivity.hourlyDistribution.hour')
+ },
+ request_count: {
+ alias: t('userActivity.hourlyDistribution.requestCount')
+ }
+ }
+ };
+
+ return (
+
+
+
+ );
+};
+
+export default HourlyDistributionChart;
diff --git a/packages/base/src/page/UserActivity/components/ModuleDistributionChart/index.tsx b/packages/base/src/page/UserActivity/components/ModuleDistributionChart/index.tsx
new file mode 100644
index 0000000000..c39cb0e377
--- /dev/null
+++ b/packages/base/src/page/UserActivity/components/ModuleDistributionChart/index.tsx
@@ -0,0 +1,69 @@
+import { useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Column, ColumnConfig } from '@ant-design/plots';
+import ChartWrapper from 'sqle/src/components/ChartCom/ChartWrapper';
+import { useChangeTheme } from '@actiontech/shared/lib/features';
+import useThemeStyleData from '../../../../hooks/useThemeStyleData';
+import { IUserActivityModuleDistributionItem } from '@actiontech/shared/lib/api/base/service/UserActivity/index.d';
+import { floatRound } from '@actiontech/shared/lib/utils/Math';
+
+type ModuleDistributionChartProps = {
+ loading?: boolean;
+ errorMessage?: string;
+ data?: IUserActivityModuleDistributionItem[];
+ onRefresh?: () => void;
+};
+
+const ModuleDistributionChart = ({
+ loading,
+ errorMessage,
+ data,
+ onRefresh
+}: ModuleDistributionChartProps) => {
+ const { t } = useTranslation();
+ const { currentTheme } = useChangeTheme();
+ const { sharedTheme } = useThemeStyleData();
+
+ const chartData = useMemo(
+ () =>
+ data?.map((item) => ({
+ module_name: item.module_name || item.module_code,
+ request_count: item.request_count ?? 0,
+ percent: `${floatRound((item.percent ?? 0) * 100, 2)}%`
+ })) ?? [],
+ [data]
+ );
+
+ const config: ColumnConfig = {
+ data: chartData,
+ xField: 'module_name',
+ yField: 'request_count',
+ color: sharedTheme.uiToken.colorPrimary,
+ label: {
+ position: 'top',
+ formatter: (datum: { percent?: string }) => datum.percent ?? ''
+ },
+ meta: {
+ module_name: {
+ alias: t('userActivity.moduleDistribution.title')
+ },
+ request_count: {
+ alias: t('userActivity.moduleDistribution.requestCount')
+ }
+ }
+ };
+
+ return (
+
+
+
+ );
+};
+
+export default ModuleDistributionChart;
diff --git a/packages/base/src/page/UserActivity/components/SummaryCards/index.tsx b/packages/base/src/page/UserActivity/components/SummaryCards/index.tsx
new file mode 100644
index 0000000000..bcbbfa51b0
--- /dev/null
+++ b/packages/base/src/page/UserActivity/components/SummaryCards/index.tsx
@@ -0,0 +1,67 @@
+import { Row, Col, Card, Statistic, Spin } from 'antd';
+import { useTranslation } from 'react-i18next';
+import { IUserActivitySummary } from '@actiontech/shared/lib/api/base/service/UserActivity/index.d';
+import {
+ formatAvgRequest,
+ formatCount,
+ formatErrorRate,
+ formatPeakHour
+} from '../../utils';
+
+type SummaryCardsProps = {
+ loading?: boolean;
+ data?: IUserActivitySummary;
+};
+
+const SummaryCards = ({ loading, data }: SummaryCardsProps) => {
+ const { t } = useTranslation();
+
+ const items = [
+ {
+ key: 'dau',
+ title: t('userActivity.summary.dau'),
+ value: formatCount(data?.dau)
+ },
+ {
+ key: 'requestCount',
+ title: t('userActivity.summary.requestCount'),
+ value: formatCount(data?.request_count)
+ },
+ {
+ key: 'avgRequestPerUser',
+ title: t('userActivity.summary.avgRequestPerUser'),
+ value: formatAvgRequest(data?.avg_request_per_user)
+ },
+ {
+ key: 'errorRate',
+ title: t('userActivity.summary.errorRate'),
+ value: formatErrorRate(data?.error_rate)
+ },
+ {
+ key: 'errorCount',
+ title: t('userActivity.summary.errorCount'),
+ value: formatCount(data?.error_count)
+ },
+ {
+ key: 'peakHour',
+ title: t('userActivity.summary.peakHour'),
+ value: formatPeakHour(data?.peak_hour)
+ }
+ ];
+
+ return (
+
+
+ {items.map((item) => (
+
+
+
+
+
+ ))}
+
+
+ );
+};
+
+export default SummaryCards;
diff --git a/packages/base/src/page/UserActivity/components/UserRankingTable/index.tsx b/packages/base/src/page/UserActivity/components/UserRankingTable/index.tsx
new file mode 100644
index 0000000000..5f6de75a81
--- /dev/null
+++ b/packages/base/src/page/UserActivity/components/UserRankingTable/index.tsx
@@ -0,0 +1,108 @@
+import { useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Card } from 'antd';
+import {
+ ActiontechTable,
+ ActiontechTableWrapper,
+ useTableRequestError,
+ useTableRequestParams
+} from '@actiontech/shared/lib/components/ActiontechTable';
+import { DmsApi } from '@actiontech/shared/lib/api';
+import { useRequest } from 'ahooks';
+import { IUserActivityUserItem } from '@actiontech/shared/lib/api/base/service/UserActivity/index.d';
+import { IGetUserActivityUsersV1Params } from '@actiontech/shared/lib/api/base/service/UserActivity/index.d';
+import { formatTime } from '@actiontech/shared/lib/utils/Common';
+
+type UserRankingTableProps = {
+ filterDateFrom: string;
+ filterDateTo: string;
+};
+
+const UserRankingTable = ({
+ filterDateFrom,
+ filterDateTo
+}: UserRankingTableProps) => {
+ const { t } = useTranslation();
+ const { requestErrorMessage, handleTableRequestError } =
+ useTableRequestError();
+
+ const { tableChange, pagination } = useTableRequestParams<
+ IUserActivityUserItem,
+ IGetUserActivityUsersV1Params
+ >();
+
+ const { data, loading } = useRequest(
+ () =>
+ handleTableRequestError(
+ DmsApi.UserActivityService.GetUserActivityUsersV1({
+ filter_date_from: filterDateFrom,
+ filter_date_to: filterDateTo,
+ page_index: pagination.page_index,
+ page_size: pagination.page_size
+ })
+ ),
+ {
+ refreshDeps: [
+ filterDateFrom,
+ filterDateTo,
+ pagination.page_index,
+ pagination.page_size
+ ]
+ }
+ );
+
+ const columns = useMemo(
+ () => [
+ {
+ dataIndex: 'user_name',
+ title: t('userActivity.userRanking.userName')
+ },
+ {
+ dataIndex: 'active_days',
+ title: t('userActivity.userRanking.activeDays')
+ },
+ {
+ dataIndex: 'request_count',
+ title: t('userActivity.userRanking.requestCount')
+ },
+ {
+ dataIndex: 'top_module_name',
+ title: t('userActivity.userRanking.topModule'),
+ render: (_: string, record: IUserActivityUserItem) =>
+ record.top_module_name || record.top_module_code || '-'
+ },
+ {
+ dataIndex: 'last_active_at',
+ title: t('userActivity.userRanking.lastActiveAt'),
+ render: (value?: string) => (value ? formatTime(value) : '-')
+ }
+ ],
+ [t]
+ );
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default UserRankingTable;
diff --git a/packages/base/src/page/UserActivity/index.tsx b/packages/base/src/page/UserActivity/index.tsx
new file mode 100644
index 0000000000..7ea0965d2f
--- /dev/null
+++ b/packages/base/src/page/UserActivity/index.tsx
@@ -0,0 +1,192 @@
+import { useMemo, useState } from 'react';
+import { Button, Col, DatePicker, Row } from 'antd';
+import type { Dayjs } from 'dayjs';
+import dayjs from 'dayjs';
+import { useTranslation } from 'react-i18next';
+import { useRequest } from 'ahooks';
+import { RefreshOutlined } from '@actiontech/icons';
+import { DmsApi } from '@actiontech/shared/lib/api';
+import { ResponseCode } from '@actiontech/shared/lib/enum';
+import CardWrapper from 'sqle/src/components/CardWrapper';
+import { UserActivityStyleWrapper } from './style';
+import SummaryCards from './components/SummaryCards';
+import DailyTrendChart from './components/DailyTrendChart';
+import ModuleDistributionChart from './components/ModuleDistributionChart';
+import HourlyDistributionChart from './components/HourlyDistributionChart';
+import UserRankingTable from './components/UserRankingTable';
+import { DATE_FORMAT, getDefaultStatDate, getTrendDateRange } from './utils';
+
+const UserActivity = () => {
+ const { t } = useTranslation();
+ const [statDate, setStatDate] = useState(getDefaultStatDate());
+ const [trendRange, setTrendRange] = useState<[Dayjs, Dayjs]>(() => {
+ const [from, to] = getTrendDateRange(getDefaultStatDate());
+ return [dayjs(from), dayjs(to)];
+ });
+
+ const statDateStr = useMemo(() => statDate.format(DATE_FORMAT), [statDate]);
+ const trendFrom = useMemo(
+ () => trendRange[0].format(DATE_FORMAT),
+ [trendRange]
+ );
+ const trendTo = useMemo(
+ () => trendRange[1].format(DATE_FORMAT),
+ [trendRange]
+ );
+
+ const {
+ data: summary,
+ loading: summaryLoading,
+ refresh: refreshSummary
+ } = useRequest(
+ () =>
+ DmsApi.UserActivityService.GetUserActivitySummaryV1({
+ stat_date: statDateStr
+ }).then((res) => {
+ if (res.data.code === ResponseCode.SUCCESS) {
+ return res.data.data;
+ }
+ throw new Error(res.data.message);
+ }),
+ { refreshDeps: [statDateStr] }
+ );
+
+ const {
+ data: dailyTrend,
+ loading: dailyTrendLoading,
+ error: dailyTrendError,
+ refresh: refreshDailyTrend
+ } = useRequest(
+ () =>
+ DmsApi.UserActivityService.GetUserActivityDailyTrendV1({
+ filter_date_from: trendFrom,
+ filter_date_to: trendTo
+ }).then((res) => {
+ if (res.data.code === ResponseCode.SUCCESS) {
+ return res.data.data ?? [];
+ }
+ throw new Error(res.data.message);
+ }),
+ { refreshDeps: [trendFrom, trendTo] }
+ );
+
+ const {
+ data: moduleDistribution,
+ loading: moduleLoading,
+ error: moduleError,
+ refresh: refreshModule
+ } = useRequest(
+ () =>
+ DmsApi.UserActivityService.GetUserActivityModuleDistributionV1({
+ stat_date: statDateStr
+ }).then((res) => {
+ if (res.data.code === ResponseCode.SUCCESS) {
+ return res.data.data ?? [];
+ }
+ throw new Error(res.data.message);
+ }),
+ { refreshDeps: [statDateStr] }
+ );
+
+ const {
+ data: hourlyDistribution,
+ loading: hourlyLoading,
+ error: hourlyError,
+ refresh: refreshHourly
+ } = useRequest(
+ () =>
+ DmsApi.UserActivityService.GetUserActivityHourlyDistributionV1({
+ stat_date: statDateStr
+ }).then((res) => {
+ if (res.data.code === ResponseCode.SUCCESS) {
+ return res.data.data ?? [];
+ }
+ throw new Error(res.data.message);
+ }),
+ { refreshDeps: [statDateStr] }
+ );
+
+ const refreshAll = () => {
+ refreshSummary();
+ refreshDailyTrend();
+ refreshModule();
+ refreshHourly();
+ };
+
+ return (
+
+
+
{t('userActivity.title')}
+
+ {t('userActivity.statDate')}
+ {
+ if (!value) {
+ return;
+ }
+ setStatDate(value);
+ const [from, to] = getTrendDateRange(value);
+ setTrendRange([dayjs(from), dayjs(to)]);
+ }}
+ />
+ {t('userActivity.dateRange')}
+ {
+ if (!values?.[0] || !values?.[1]) {
+ return;
+ }
+ setTrendRange([values[0], values[1]]);
+ }}
+ />
+ } onClick={refreshAll}>
+ {t('userActivity.refresh')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default UserActivity;
diff --git a/packages/base/src/page/UserActivity/style.ts b/packages/base/src/page/UserActivity/style.ts
new file mode 100644
index 0000000000..34693de5fb
--- /dev/null
+++ b/packages/base/src/page/UserActivity/style.ts
@@ -0,0 +1,37 @@
+import { styled } from '@mui/material/styles';
+
+export const UserActivityStyleWrapper = styled('section')`
+ &.user-activity-page {
+ .page-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 16px;
+
+ .page-title {
+ font-size: 20px;
+ font-weight: 600;
+ line-height: 28px;
+ }
+
+ .page-filters {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ flex-wrap: wrap;
+ }
+ }
+
+ .summary-card {
+ height: 100%;
+ }
+
+ .chart-section {
+ margin-top: 16px;
+ }
+
+ .chart-card {
+ height: 100%;
+ }
+ }
+`;
diff --git a/packages/base/src/page/UserActivity/utils.ts b/packages/base/src/page/UserActivity/utils.ts
new file mode 100644
index 0000000000..7ee940274c
--- /dev/null
+++ b/packages/base/src/page/UserActivity/utils.ts
@@ -0,0 +1,50 @@
+import dayjs, { Dayjs } from 'dayjs';
+import { floatRound } from '@actiontech/shared/lib/utils/Math';
+import { formatParamsBySeparator } from '@actiontech/shared/lib/utils/Tool';
+
+export const DATE_FORMAT = 'YYYY-MM-DD';
+
+export const getDefaultStatDate = () => dayjs().subtract(1, 'day');
+
+export const getTrendDateRange = (statDate: Dayjs): [string, string] => {
+ const end = statDate;
+ const start = statDate.subtract(6, 'day');
+ return [start.format(DATE_FORMAT), end.format(DATE_FORMAT)];
+};
+
+export const formatPeakHour = (hour?: number) => {
+ if (hour === undefined || hour === null) {
+ return '-';
+ }
+ const start = `${String(hour).padStart(2, '0')}:00`;
+ const end = `${String((hour + 1) % 24).padStart(2, '0')}:00`;
+ return `${start}-${end}`;
+};
+
+export const formatErrorRate = (rate?: number) => {
+ if (rate === undefined || rate === null) {
+ return '-';
+ }
+ return `${floatRound(rate * 100, 2)}%`;
+};
+
+export const formatAvgRequest = (value?: number) => {
+ if (value === undefined || value === null) {
+ return '-';
+ }
+ return floatRound(value, 2).toString();
+};
+
+export const formatCount = (value?: number) => {
+ if (value === undefined || value === null) {
+ return '-';
+ }
+ return formatParamsBySeparator(value);
+};
+
+export const formatHourLabel = (hour?: number) => {
+ if (hour === undefined || hour === null) {
+ return '';
+ }
+ return `${String(hour).padStart(2, '0')}:00`;
+};
diff --git a/packages/base/src/router/router.base.tsx b/packages/base/src/router/router.base.tsx
index 59c1b3ea32..4f58759df7 100644
--- a/packages/base/src/router/router.base.tsx
+++ b/packages/base/src/router/router.base.tsx
@@ -36,6 +36,7 @@ const GlobalAddDataSource = React.lazy(
);
const ResourceOverview = React.lazy(() => import('../page/ResourceOverview'));
+const UserActivity = React.lazy(() => import('../page/UserActivity'));
const AvailabilityZoneList = React.lazy(
() => import('../page/AvailabilityZone/List')
@@ -179,6 +180,12 @@ export const BaseGlobalRouterConfig: RouterConfigItem[] = [
key: 'resourceOverview',
element: ,
permission: PERMISSIONS.PAGES.BASE.RESOURCE_OVERVIEW
+ },
+ {
+ path: ROUTE_PATHS.BASE.USER_ACTIVITY,
+ key: 'userActivity',
+ element: ,
+ permission: PERMISSIONS.PAGES.BASE.RESOURCE_OVERVIEW
}
// #endif
];
diff --git a/packages/base/src/router/test/__snapshots__/router.base.test.tsx.snap b/packages/base/src/router/test/__snapshots__/router.base.test.tsx.snap
index a66d7e4a55..d06b13f57f 100644
--- a/packages/base/src/router/test/__snapshots__/router.base.test.tsx.snap
+++ b/packages/base/src/router/test/__snapshots__/router.base.test.tsx.snap
@@ -134,6 +134,13 @@ exports[`base/router-base-ee render base route data snap 1`] = `
"key": "resourceOverview",
"path": "/resource-overview",
},
+ {
+ "element":
+ userActivity
+
,
+ "key": "userActivity",
+ "path": "/user-activity",
+ },
{
"children": [
{
@@ -1197,3 +1204,17 @@ exports[`base/router-base-ee render base router when version is ee render route
+
+