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`] = ` +
+
+
+
+`; 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]]); + }} + /> + +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+ ); +}; + +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 `; + +exports[`base/router-base-ee render base router when version is ee render route userActivity render router userActivity 1`] = ` + +
+
+
+ userActivity +
+
+
+ +`; diff --git a/packages/base/src/router/test/router.base.test.tsx b/packages/base/src/router/test/router.base.test.tsx index dbdc1bf434..1d52ee76c4 100644 --- a/packages/base/src/router/test/router.base.test.tsx +++ b/packages/base/src/router/test/router.base.test.tsx @@ -203,6 +203,15 @@ describe('base/router-base-ee', () => { }); }); + describe('render route userActivity', () => { + it('render router userActivity', async () => { + const { baseElement } = customRender(['/user-activity']); + + expect(screen.getByText('userActivity')).toBeInTheDocument(); + expect(baseElement).toMatchSnapshot(); + }); + }); + describe('render route syncDataSource', () => { it('render route syncDataSourceList', async () => { const { baseElement } = customRender([ diff --git a/packages/dms-kit/src/data/routePaths.ts b/packages/dms-kit/src/data/routePaths.ts index 61d8fb01d7..a79e2874c9 100644 --- a/packages/dms-kit/src/data/routePaths.ts +++ b/packages/dms-kit/src/data/routePaths.ts @@ -121,6 +121,7 @@ export const ROUTE_PATHS = { } }, RESOURCE_OVERVIEW: '/resource-overview', + USER_ACTIVITY: '/user-activity', AVAILABILITY_ZONE: '/availability-zone' }, SQLE: { diff --git a/packages/shared/lib/api/base/index.ts b/packages/shared/lib/api/base/index.ts index 220befc3e1..e81ee8f17a 100644 --- a/packages/shared/lib/api/base/index.ts +++ b/packages/shared/lib/api/base/index.ts @@ -22,6 +22,7 @@ export { default as ResourceOverviewService } from './service/ResourceOverview'; export { default as RoleService } from './service/Role'; export { default as SMSService } from './service/SMS'; export { default as SessionService } from './service/Session'; +export { default as UserActivityService } from './service/UserActivity'; export { default as UserService } from './service/User'; export { default as UserGroupService } from './service/UserGroup'; export { default as WebhookService } from './service/Webhook'; diff --git a/packages/shared/lib/api/base/service/UserActivity/index.d.ts b/packages/shared/lib/api/base/service/UserActivity/index.d.ts new file mode 100644 index 0000000000..ce264fec76 --- /dev/null +++ b/packages/shared/lib/api/base/service/UserActivity/index.d.ts @@ -0,0 +1,94 @@ +export interface IUserActivitySummary { + dau?: number; + request_count?: number; + avg_request_per_user?: number; + error_count?: number; + error_rate?: number; + peak_hour?: number; + peak_hour_requests?: number; +} + +export interface IGetUserActivitySummaryV1Params { + stat_date: string; +} + +export interface IGetUserActivitySummaryV1Return { + code?: number; + message?: string; + data?: IUserActivitySummary; +} + +export interface IUserActivityDailyTrendItem { + stat_date?: string; + dau?: number; + request_count?: number; + error_count?: number; +} + +export interface IGetUserActivityDailyTrendV1Params { + filter_date_from: string; + filter_date_to: string; +} + +export interface IGetUserActivityDailyTrendV1Return { + code?: number; + message?: string; + data?: IUserActivityDailyTrendItem[]; +} + +export interface IUserActivityModuleDistributionItem { + module_code?: string; + module_name?: string; + request_count?: number; + percent?: number; +} + +export interface IGetUserActivityModuleDistributionV1Params { + stat_date: string; +} + +export interface IGetUserActivityModuleDistributionV1Return { + code?: number; + message?: string; + data?: IUserActivityModuleDistributionItem[]; +} + +export interface IUserActivityHourlyDistributionItem { + stat_hour?: number; + request_count?: number; + active_users?: number; +} + +export interface IGetUserActivityHourlyDistributionV1Params { + stat_date: string; +} + +export interface IGetUserActivityHourlyDistributionV1Return { + code?: number; + message?: string; + data?: IUserActivityHourlyDistributionItem[]; +} + +export interface IUserActivityUserItem { + user_uid?: string; + user_name?: string; + active_days?: number; + request_count?: number; + top_module_code?: string; + top_module_name?: string; + last_active_at?: string; +} + +export interface IGetUserActivityUsersV1Params { + filter_date_from: string; + filter_date_to: string; + page_index: number; + page_size: number; +} + +export interface IGetUserActivityUsersV1Return { + code?: number; + message?: string; + data?: IUserActivityUserItem[]; + total_nums?: number; +} diff --git a/packages/shared/lib/api/base/service/UserActivity/index.ts b/packages/shared/lib/api/base/service/UserActivity/index.ts new file mode 100644 index 0000000000..71ce92eef8 --- /dev/null +++ b/packages/shared/lib/api/base/service/UserActivity/index.ts @@ -0,0 +1,84 @@ +/* tslint:disable no-identical-functions */ +/* tslint:disable no-useless-cast */ +/* tslint:disable no-unnecessary-type-assertion */ +/* tslint:disable no-big-function */ +/* tslint:disable no-duplicate-string */ +import ServiceBase from '../Service.base'; +import { AxiosRequestConfig } from 'axios'; + +import { + IGetUserActivityDailyTrendV1Params, + IGetUserActivityDailyTrendV1Return, + IGetUserActivityHourlyDistributionV1Params, + IGetUserActivityHourlyDistributionV1Return, + IGetUserActivityModuleDistributionV1Params, + IGetUserActivityModuleDistributionV1Return, + IGetUserActivitySummaryV1Params, + IGetUserActivitySummaryV1Return, + IGetUserActivityUsersV1Params, + IGetUserActivityUsersV1Return +} from './index.d'; + +class UserActivityService extends ServiceBase { + public GetUserActivitySummaryV1( + params: IGetUserActivitySummaryV1Params, + options?: AxiosRequestConfig + ) { + const paramsData = this.cloneDeep(params); + return this.get( + '/v1/dms/statistic/user_activity/summary', + paramsData, + options + ); + } + + public GetUserActivityDailyTrendV1( + params: IGetUserActivityDailyTrendV1Params, + options?: AxiosRequestConfig + ) { + const paramsData = this.cloneDeep(params); + return this.get( + '/v1/dms/statistic/user_activity/daily_trend', + paramsData, + options + ); + } + + public GetUserActivityModuleDistributionV1( + params: IGetUserActivityModuleDistributionV1Params, + options?: AxiosRequestConfig + ) { + const paramsData = this.cloneDeep(params); + return this.get( + '/v1/dms/statistic/user_activity/module_distribution', + paramsData, + options + ); + } + + public GetUserActivityHourlyDistributionV1( + params: IGetUserActivityHourlyDistributionV1Params, + options?: AxiosRequestConfig + ) { + const paramsData = this.cloneDeep(params); + return this.get( + '/v1/dms/statistic/user_activity/hourly_distribution', + paramsData, + options + ); + } + + public GetUserActivityUsersV1( + params: IGetUserActivityUsersV1Params, + options?: AxiosRequestConfig + ) { + const paramsData = this.cloneDeep(params); + return this.get( + '/v1/dms/statistic/user_activity/users', + paramsData, + options + ); + } +} + +export default new UserActivityService(); diff --git a/packages/shared/lib/testUtil/mockApi/base/index.ts b/packages/shared/lib/testUtil/mockApi/base/index.ts index fd2646599c..0d93d3e7b5 100644 --- a/packages/shared/lib/testUtil/mockApi/base/index.ts +++ b/packages/shared/lib/testUtil/mockApi/base/index.ts @@ -7,6 +7,7 @@ export { default as global } from './global'; export { default as member } from './member'; export { default as project } from './project'; export { default as resourceOverview } from './resourceOverview'; +export { default as userActivity } from './userActivity'; export { default as sms } from './sms'; export { default as syncTaskList } from './syncTaskList'; export { default as system } from './system'; diff --git a/packages/shared/lib/testUtil/mockApi/base/userActivity/data.ts b/packages/shared/lib/testUtil/mockApi/base/userActivity/data.ts new file mode 100644 index 0000000000..b6db0a7cc2 --- /dev/null +++ b/packages/shared/lib/testUtil/mockApi/base/userActivity/data.ts @@ -0,0 +1,83 @@ +import { IUserActivitySummary } from '../../../../api/base/service/UserActivity/index.d'; + +export const mockUserActivitySummary: IUserActivitySummary = { + dau: 3, + request_count: 13, + avg_request_per_user: 4.33, + error_count: 1, + error_rate: 1 / 13, + peak_hour: 9, + peak_hour_requests: 4 +}; + +export const mockUserActivityDailyTrend = [ + { + stat_date: '2026-06-20', + dau: 2, + request_count: 5, + error_count: 0 + }, + { + stat_date: '2026-06-21', + dau: 2, + request_count: 6, + error_count: 0 + }, + { + stat_date: '2026-06-22', + dau: 3, + request_count: 13, + error_count: 1 + } +]; + +export const mockUserActivityModuleDistribution = [ + { + module_code: 'WORKBENCH', + module_name: 'SQL工作台', + request_count: 4, + percent: 4 / 13 + }, + { + module_code: 'WORKFLOW', + module_name: 'SQL工单', + request_count: 2, + percent: 2 / 13 + }, + { + module_code: 'SQL_AUDIT', + module_name: '快捷审核', + request_count: 2, + percent: 2 / 13 + } +]; + +export const mockUserActivityHourlyDistribution = Array.from( + { length: 24 }, + (_, hour) => ({ + stat_hour: hour, + request_count: hour === 9 ? 4 : hour < 3 ? 1 : 0, + active_users: hour === 9 ? 2 : hour < 3 ? 1 : 0 + }) +); + +export const mockUserActivityUsers = [ + { + user_uid: 'u1', + user_name: 'admin', + active_days: 3, + request_count: 6, + top_module_code: 'WORKBENCH', + top_module_name: 'SQL工作台', + last_active_at: '2026-06-22T09:30:00Z' + }, + { + user_uid: 'u2', + user_name: 'tester', + active_days: 2, + request_count: 4, + top_module_code: 'WORKFLOW', + top_module_name: 'SQL工单', + last_active_at: '2026-06-22T08:15:00Z' + } +]; diff --git a/packages/shared/lib/testUtil/mockApi/base/userActivity/index.ts b/packages/shared/lib/testUtil/mockApi/base/userActivity/index.ts new file mode 100644 index 0000000000..777a63efa4 --- /dev/null +++ b/packages/shared/lib/testUtil/mockApi/base/userActivity/index.ts @@ -0,0 +1,70 @@ +import { createSpySuccessResponse, MockSpyApy } from '../../common'; +import { UserActivityService } from '../../../../api/base'; +import { + mockUserActivityDailyTrend, + mockUserActivityHourlyDistribution, + mockUserActivityModuleDistribution, + mockUserActivitySummary, + mockUserActivityUsers +} from './data'; + +class MockUserActivityApi implements MockSpyApy { + public mockAllApi(): void { + this.getUserActivitySummaryV1(); + this.getUserActivityDailyTrendV1(); + this.getUserActivityModuleDistributionV1(); + this.getUserActivityHourlyDistributionV1(); + this.getUserActivityUsersV1(); + } + + public getUserActivitySummaryV1() { + const spy = jest.spyOn(UserActivityService, 'GetUserActivitySummaryV1'); + spy.mockImplementation(() => + createSpySuccessResponse({ data: mockUserActivitySummary }) + ); + return spy; + } + + public getUserActivityDailyTrendV1() { + const spy = jest.spyOn(UserActivityService, 'GetUserActivityDailyTrendV1'); + spy.mockImplementation(() => + createSpySuccessResponse({ data: mockUserActivityDailyTrend }) + ); + return spy; + } + + public getUserActivityModuleDistributionV1() { + const spy = jest.spyOn( + UserActivityService, + 'GetUserActivityModuleDistributionV1' + ); + spy.mockImplementation(() => + createSpySuccessResponse({ data: mockUserActivityModuleDistribution }) + ); + return spy; + } + + public getUserActivityHourlyDistributionV1() { + const spy = jest.spyOn( + UserActivityService, + 'GetUserActivityHourlyDistributionV1' + ); + spy.mockImplementation(() => + createSpySuccessResponse({ data: mockUserActivityHourlyDistribution }) + ); + return spy; + } + + public getUserActivityUsersV1() { + const spy = jest.spyOn(UserActivityService, 'GetUserActivityUsersV1'); + spy.mockImplementation(() => + createSpySuccessResponse({ + data: mockUserActivityUsers, + total_nums: mockUserActivityUsers.length + }) + ); + return spy; + } +} + +export default new MockUserActivityApi(); From f86e6ee8d02a3debe76b044914354d55a130ce46 Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Tue, 23 Jun 2026 07:39:42 +0000 Subject: [PATCH 2/8] fix(ee): constrain user activity chart panel height to prevent infinite page growth --- .../__snapshots__/index.test.tsx.snap | 736 +++++++++--------- packages/base/src/page/UserActivity/index.tsx | 58 +- packages/base/src/page/UserActivity/style.ts | 15 + 3 files changed, 424 insertions(+), 385 deletions(-) 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 index 13e2ef514a..55b17f7930 100644 --- a/packages/base/src/page/UserActivity/__tests__/__snapshots__/index.test.tsx.snap +++ b/packages/base/src/page/UserActivity/__tests__/__snapshots__/index.test.tsx.snap @@ -3,7 +3,7 @@ exports[`base/page/UserActivity render init snap 1`] = `
@@ -440,43 +444,47 @@ exports[`base/page/UserActivity render init snap 1`] = ` style="padding: 8px 8px 8px 8px;" >
-
-
- 模块访问分布 +
+ 模块访问分布 +
-
-
-
-
-
+ +
+ class="chart-box" + > +
+
-
-
+
+
@@ -485,43 +493,47 @@ exports[`base/page/UserActivity render init snap 1`] = ` style="padding: 8px 8px 8px 8px;" >
-
-
- 24 小时访问分布 +
+ 24 小时访问分布 +
-
-
-
-
-
+ +
+ class="chart-box" + > +
+
-
-
+
+
@@ -530,337 +542,277 @@ exports[`base/page/UserActivity render init snap 1`] = ` style="padding: 8px 8px 8px 8px;" >
- 用户活跃排行 +
+ 用户活跃排行 +
-
-
- - - - - - - - - - - - - + - - - - - - - - - + + + - - - - + + + + + + - SQL工单 - - + + + + + + - 2026-06-22 16:15:00 - - - -
- 用户名 - - 活跃天数 - - 请求数 - - 最常用模块 - - 最近活跃时间 -
-
+
-   - - - -
+
-   - - - -
+
-   - - - -
+
-   - - - -
+
-   - - -
- admin - - 3 - - 6 - - SQL工作台 - - 2026-06-22 17:30:00 -
- tester - - 2 - +
+   +
+
+
+   +
+
+
+   +
+
+
+   +
+
+
+   +
+
+ admin + + 3 + + 6 + + SQL工作台 + + 2026-06-22 17:30:00 +
+ + tester + + + 2 + + + 4 + + + SQL工单 + + + 2026-06-22 16:15:00 + + + + +
-
-
    -
  • - - 共 2 条数据 - -
  • -
  • - -
  • -
  • - - 1 - -
  • -
  • - -
  • -
  • -
  • -
    - - - - 20 条/页 + -
    -
  • +
  • + + 1 + +
  • +
  • +
- - + + +
  • + +
  • + +
    diff --git a/packages/base/src/page/UserActivity/index.tsx b/packages/base/src/page/UserActivity/index.tsx index 7ea0965d2f..0fcd8de7ae 100644 --- a/packages/base/src/page/UserActivity/index.tsx +++ b/packages/base/src/page/UserActivity/index.tsx @@ -152,37 +152,45 @@ const UserActivity = () => { - - - +
    + + + +
    - - - +
    + + + +
    - - - +
    + + + +
    - +
    + +
    diff --git a/packages/base/src/page/UserActivity/style.ts b/packages/base/src/page/UserActivity/style.ts index 34693de5fb..f6d54ba43b 100644 --- a/packages/base/src/page/UserActivity/style.ts +++ b/packages/base/src/page/UserActivity/style.ts @@ -28,10 +28,25 @@ export const UserActivityStyleWrapper = styled('section')` .chart-section { margin-top: 16px; + + .chart-panel { + width: 100%; + height: 360px; + } + + .ranking-panel { + width: 100%; + height: 360px; + } } .chart-card { height: 100%; + + .ant-card-body { + height: calc(100% - 57px); + overflow: auto; + } } } `; From 2f7be86e2b32cd7688e07300ed74821ee4416874 Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Tue, 23 Jun 2026 08:08:04 +0000 Subject: [PATCH 3/8] fix(ee): improve user activity charts and default to today Default stat date to today, pad chart axes, fill 24-hour slots, and show uid fallback in user ranking when name is missing. --- .../__snapshots__/index.test.tsx.snap | 16 +++++++------- .../components/DailyTrendChart/index.tsx | 8 ++++++- .../HourlyDistributionChart/index.tsx | 22 ++++++++++--------- .../ModuleDistributionChart/index.tsx | 11 ++++++++++ .../components/UserRankingTable/index.tsx | 4 +++- packages/base/src/page/UserActivity/style.ts | 12 ++++++++-- packages/base/src/page/UserActivity/utils.ts | 22 ++++++++++++++++++- 7 files changed, 72 insertions(+), 23 deletions(-) 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 index 55b17f7930..3d63a14e15 100644 --- a/packages/base/src/page/UserActivity/__tests__/__snapshots__/index.test.tsx.snap +++ b/packages/base/src/page/UserActivity/__tests__/__snapshots__/index.test.tsx.snap @@ -3,7 +3,7 @@ exports[`base/page/UserActivity render init snap 1`] = `
    @@ -478,7 +478,7 @@ exports[`base/page/UserActivity render init snap 1`] = ` class="chart-box" >
    @@ -527,7 +527,7 @@ exports[`base/page/UserActivity render init snap 1`] = ` class="chart-box" >
    diff --git a/packages/base/src/page/UserActivity/components/DailyTrendChart/index.tsx b/packages/base/src/page/UserActivity/components/DailyTrendChart/index.tsx index 346ea83be8..9782db800f 100644 --- a/packages/base/src/page/UserActivity/components/DailyTrendChart/index.tsx +++ b/packages/base/src/page/UserActivity/components/DailyTrendChart/index.tsx @@ -5,6 +5,7 @@ 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'; +import { USER_ACTIVITY_CHART_PADDING } from '../../utils'; type DailyTrendChartProps = { loading?: boolean; @@ -51,12 +52,17 @@ const DailyTrendChart = ({ seriesField: 'category', color: sqleTheme.statistics.rectColor, smooth: true, + appendPadding: USER_ACTIVITY_CHART_PADDING, legend: { position: 'top' }, xAxis: { label: { - autoRotate: true + autoRotate: true, + autoHide: false, + style: { + fontSize: 11 + } } } }; diff --git a/packages/base/src/page/UserActivity/components/HourlyDistributionChart/index.tsx b/packages/base/src/page/UserActivity/components/HourlyDistributionChart/index.tsx index 1244276ed1..3ba8135082 100644 --- a/packages/base/src/page/UserActivity/components/HourlyDistributionChart/index.tsx +++ b/packages/base/src/page/UserActivity/components/HourlyDistributionChart/index.tsx @@ -5,7 +5,7 @@ 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'; +import { buildFullHourlyChartData, USER_ACTIVITY_CHART_PADDING } from '../../utils'; type HourlyDistributionChartProps = { loading?: boolean; @@ -24,21 +24,23 @@ const HourlyDistributionChart = ({ 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 chartData = useMemo(() => buildFullHourlyChartData(data), [data]); const config: ColumnConfig = { data: chartData, xField: 'hour_label', yField: 'request_count', + appendPadding: USER_ACTIVITY_CHART_PADDING, color: sharedTheme.uiToken.colorPrimary, + xAxis: { + label: { + autoRotate: false, + autoHide: false, + style: { + fontSize: 11 + } + } + }, meta: { hour_label: { alias: t('userActivity.hourlyDistribution.hour') diff --git a/packages/base/src/page/UserActivity/components/ModuleDistributionChart/index.tsx b/packages/base/src/page/UserActivity/components/ModuleDistributionChart/index.tsx index c39cb0e377..9b642f17d8 100644 --- a/packages/base/src/page/UserActivity/components/ModuleDistributionChart/index.tsx +++ b/packages/base/src/page/UserActivity/components/ModuleDistributionChart/index.tsx @@ -6,6 +6,7 @@ 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'; +import { USER_ACTIVITY_CHART_PADDING } from '../../utils'; type ModuleDistributionChartProps = { loading?: boolean; @@ -38,7 +39,17 @@ const ModuleDistributionChart = ({ data: chartData, xField: 'module_name', yField: 'request_count', + appendPadding: USER_ACTIVITY_CHART_PADDING, color: sharedTheme.uiToken.colorPrimary, + xAxis: { + label: { + autoRotate: true, + autoHide: false, + style: { + fontSize: 11 + } + } + }, label: { position: 'top', formatter: (datum: { percent?: string }) => datum.percent ?? '' diff --git a/packages/base/src/page/UserActivity/components/UserRankingTable/index.tsx b/packages/base/src/page/UserActivity/components/UserRankingTable/index.tsx index 5f6de75a81..1d324dea42 100644 --- a/packages/base/src/page/UserActivity/components/UserRankingTable/index.tsx +++ b/packages/base/src/page/UserActivity/components/UserRankingTable/index.tsx @@ -55,7 +55,9 @@ const UserRankingTable = ({ () => [ { dataIndex: 'user_name', - title: t('userActivity.userRanking.userName') + title: t('userActivity.userRanking.userName'), + render: (_: string, record: IUserActivityUserItem) => + record.user_name || record.user_uid || '-' }, { dataIndex: 'active_days', diff --git a/packages/base/src/page/UserActivity/style.ts b/packages/base/src/page/UserActivity/style.ts index f6d54ba43b..282769fec7 100644 --- a/packages/base/src/page/UserActivity/style.ts +++ b/packages/base/src/page/UserActivity/style.ts @@ -31,12 +31,20 @@ export const UserActivityStyleWrapper = styled('section')` .chart-panel { width: 100%; - height: 360px; + height: 400px; + + .card-cont { + overflow: visible; + } + + .chart-wrapper .chart-box { + min-height: 280px; + } } .ranking-panel { width: 100%; - height: 360px; + height: 400px; } } diff --git a/packages/base/src/page/UserActivity/utils.ts b/packages/base/src/page/UserActivity/utils.ts index 7ee940274c..86b3f7dca4 100644 --- a/packages/base/src/page/UserActivity/utils.ts +++ b/packages/base/src/page/UserActivity/utils.ts @@ -4,7 +4,12 @@ import { formatParamsBySeparator } from '@actiontech/shared/lib/utils/Tool'; export const DATE_FORMAT = 'YYYY-MM-DD'; -export const getDefaultStatDate = () => dayjs().subtract(1, 'day'); +/** 图表底部留白,避免横轴标签被裁切 */ +export const USER_ACTIVITY_CHART_PADDING: [number, number, number, number] = [ + 16, 16, 52, 16 +]; + +export const getDefaultStatDate = () => dayjs(); export const getTrendDateRange = (statDate: Dayjs): [string, string] => { const end = statDate; @@ -48,3 +53,18 @@ export const formatHourLabel = (hour?: number) => { } return `${String(hour).padStart(2, '0')}:00`; }; + +export const buildFullHourlyChartData = ( + data?: Array<{ stat_hour?: number; request_count?: number }> +) => { + const hourMap = new Map(); + data?.forEach((item) => { + if (item.stat_hour !== undefined && item.stat_hour !== null) { + hourMap.set(item.stat_hour, item.request_count ?? 0); + } + }); + return Array.from({ length: 24 }, (_, hour) => ({ + hour_label: formatHourLabel(hour), + request_count: hourMap.get(hour) ?? 0 + })); +}; From 5f2359b4e166a1af031d78d28e729d34c9961043 Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Tue, 23 Jun 2026 08:32:03 +0000 Subject: [PATCH 4/8] fix(ee): refine user activity page access, charts and ranking table Restrict page to global admins, improve chart layout and 24-hour axis, hide pagination when ranking fits on a single page. --- .../page/Nav/SideMenu/QuickActions/index.tsx | 2 +- .../__snapshots__/index.test.tsx.snap | 158 +----------------- .../components/DailyTrendChart/index.tsx | 1 + .../HourlyDistributionChart/index.tsx | 21 ++- .../ModuleDistributionChart/index.tsx | 9 +- .../components/UserRankingTable/index.tsx | 5 +- packages/base/src/page/UserActivity/style.ts | 15 +- packages/base/src/page/UserActivity/utils.ts | 23 ++- packages/base/src/router/router.base.tsx | 2 +- .../__snapshots__/index.test.ts.snap | 9 + .../usePermission/permissionManifest.ts | 5 + .../lib/features/usePermission/permissions.ts | 3 +- 12 files changed, 76 insertions(+), 177 deletions(-) diff --git a/packages/base/src/page/Nav/SideMenu/QuickActions/index.tsx b/packages/base/src/page/Nav/SideMenu/QuickActions/index.tsx index 5fce494308..118205c3a0 100644 --- a/packages/base/src/page/Nav/SideMenu/QuickActions/index.tsx +++ b/packages/base/src/page/Nav/SideMenu/QuickActions/index.tsx @@ -92,7 +92,7 @@ const QuickActions: React.FC = ({ title: t('dmsMenu.globalSettings.userActivity'), path: ROUTE_PATHS.BASE.USER_ACTIVITY, icon: , - permission: PERMISSIONS.PAGES.BASE.RESOURCE_OVERVIEW + permission: PERMISSIONS.PAGES.BASE.USER_ACTIVITY }, { key: 'view-rule', 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 index 3d63a14e15..c7e167d09d 100644 --- a/packages/base/src/page/UserActivity/__tests__/__snapshots__/index.test.tsx.snap +++ b/packages/base/src/page/UserActivity/__tests__/__snapshots__/index.test.tsx.snap @@ -3,7 +3,7 @@ exports[`base/page/UserActivity render init snap 1`] = `
    -
      -
    • - - 共 2 条数据 - -
    • -
    • - -
    • -
    • - - 1 - -
    • -
    • - -
    • -
    • - -
    • -
    diff --git a/packages/base/src/page/UserActivity/components/DailyTrendChart/index.tsx b/packages/base/src/page/UserActivity/components/DailyTrendChart/index.tsx index 9782db800f..04fa75909d 100644 --- a/packages/base/src/page/UserActivity/components/DailyTrendChart/index.tsx +++ b/packages/base/src/page/UserActivity/components/DailyTrendChart/index.tsx @@ -60,6 +60,7 @@ const DailyTrendChart = ({ label: { autoRotate: true, autoHide: false, + offset: 10, style: { fontSize: 11 } diff --git a/packages/base/src/page/UserActivity/components/HourlyDistributionChart/index.tsx b/packages/base/src/page/UserActivity/components/HourlyDistributionChart/index.tsx index 3ba8135082..78020a6d50 100644 --- a/packages/base/src/page/UserActivity/components/HourlyDistributionChart/index.tsx +++ b/packages/base/src/page/UserActivity/components/HourlyDistributionChart/index.tsx @@ -5,7 +5,11 @@ 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 { buildFullHourlyChartData, USER_ACTIVITY_CHART_PADDING } from '../../utils'; +import { + buildFullHourlyChartData, + USER_ACTIVITY_CHART_PADDING, + USER_ACTIVITY_HOUR_LABELS +} from '../../utils'; type HourlyDistributionChartProps = { loading?: boolean; @@ -31,18 +35,25 @@ const HourlyDistributionChart = ({ xField: 'hour_label', yField: 'request_count', appendPadding: USER_ACTIVITY_CHART_PADDING, + columnWidthRatio: 0.55, color: sharedTheme.uiToken.colorPrimary, xAxis: { label: { - autoRotate: false, + autoRotate: true, autoHide: false, + rotate: -0.75, + offset: 10, style: { - fontSize: 11 + fontSize: 10, + textAlign: 'center' } - } + }, + tickLine: null }, meta: { hour_label: { + type: 'cat', + values: USER_ACTIVITY_HOUR_LABELS, alias: t('userActivity.hourlyDistribution.hour') }, request_count: { @@ -55,7 +66,7 @@ const HourlyDistributionChart = ({ diff --git a/packages/base/src/page/UserActivity/components/ModuleDistributionChart/index.tsx b/packages/base/src/page/UserActivity/components/ModuleDistributionChart/index.tsx index 9b642f17d8..beb40d82bf 100644 --- a/packages/base/src/page/UserActivity/components/ModuleDistributionChart/index.tsx +++ b/packages/base/src/page/UserActivity/components/ModuleDistributionChart/index.tsx @@ -40,15 +40,19 @@ const ModuleDistributionChart = ({ xField: 'module_name', yField: 'request_count', appendPadding: USER_ACTIVITY_CHART_PADDING, + columnWidthRatio: 0.5, color: sharedTheme.uiToken.colorPrimary, xAxis: { label: { autoRotate: true, autoHide: false, + rotate: -0.75, + offset: 10, style: { - fontSize: 11 + fontSize: 10 } - } + }, + tickLine: null }, label: { position: 'top', @@ -56,6 +60,7 @@ const ModuleDistributionChart = ({ }, meta: { module_name: { + type: 'cat', alias: t('userActivity.moduleDistribution.title') }, request_count: { diff --git a/packages/base/src/page/UserActivity/components/UserRankingTable/index.tsx b/packages/base/src/page/UserActivity/components/UserRankingTable/index.tsx index 1d324dea42..1f85649961 100644 --- a/packages/base/src/page/UserActivity/components/UserRankingTable/index.tsx +++ b/packages/base/src/page/UserActivity/components/UserRankingTable/index.tsx @@ -95,10 +95,13 @@ const UserRankingTable = ({ columns={columns} loading={loading} errorMessage={requestErrorMessage} + isPaginationFixed={false} pagination={{ total: data?.total ?? 0, current: pagination.page_index, - pageSize: pagination.page_size + pageSize: pagination.page_size, + hideOnSinglePage: true, + showSizeChanger: (data?.total ?? 0) > 20 }} onChange={tableChange} /> diff --git a/packages/base/src/page/UserActivity/style.ts b/packages/base/src/page/UserActivity/style.ts index 282769fec7..0eb0ee24d1 100644 --- a/packages/base/src/page/UserActivity/style.ts +++ b/packages/base/src/page/UserActivity/style.ts @@ -1,4 +1,5 @@ import { styled } from '@mui/material/styles'; +import { USER_ACTIVITY_CHART_HEIGHT } from './utils'; export const UserActivityStyleWrapper = styled('section')` &.user-activity-page { @@ -31,20 +32,26 @@ export const UserActivityStyleWrapper = styled('section')` .chart-panel { width: 100%; - height: 400px; + height: 440px; .card-cont { overflow: visible; + height: calc(100% - 48px); } - .chart-wrapper .chart-box { - min-height: 280px; + .chart-wrapper { + height: ${USER_ACTIVITY_CHART_HEIGHT}px; + + .chart-box { + height: ${USER_ACTIVITY_CHART_HEIGHT}px; + min-height: ${USER_ACTIVITY_CHART_HEIGHT}px; + } } } .ranking-panel { width: 100%; - height: 400px; + height: 440px; } } diff --git a/packages/base/src/page/UserActivity/utils.ts b/packages/base/src/page/UserActivity/utils.ts index 86b3f7dca4..a8839e577c 100644 --- a/packages/base/src/page/UserActivity/utils.ts +++ b/packages/base/src/page/UserActivity/utils.ts @@ -4,11 +4,25 @@ import { formatParamsBySeparator } from '@actiontech/shared/lib/utils/Tool'; export const DATE_FORMAT = 'YYYY-MM-DD'; +/** 图表绘图区固定高度,避免横轴被父容器裁切 */ +export const USER_ACTIVITY_CHART_HEIGHT = 300; + /** 图表底部留白,避免横轴标签被裁切 */ export const USER_ACTIVITY_CHART_PADDING: [number, number, number, number] = [ - 16, 16, 52, 16 + 16, 16, 64, 16 ]; +export const formatHourLabel = (hour?: number) => { + if (hour === undefined || hour === null) { + return ''; + } + return `${String(hour).padStart(2, '0')}:00`; +}; + +export const USER_ACTIVITY_HOUR_LABELS = Array.from({ length: 24 }, (_, hour) => + formatHourLabel(hour) +); + export const getDefaultStatDate = () => dayjs(); export const getTrendDateRange = (statDate: Dayjs): [string, string] => { @@ -47,13 +61,6 @@ export const formatCount = (value?: number) => { return formatParamsBySeparator(value); }; -export const formatHourLabel = (hour?: number) => { - if (hour === undefined || hour === null) { - return ''; - } - return `${String(hour).padStart(2, '0')}:00`; -}; - export const buildFullHourlyChartData = ( data?: Array<{ stat_hour?: number; request_count?: number }> ) => { diff --git a/packages/base/src/router/router.base.tsx b/packages/base/src/router/router.base.tsx index 4f58759df7..dac74eb119 100644 --- a/packages/base/src/router/router.base.tsx +++ b/packages/base/src/router/router.base.tsx @@ -185,7 +185,7 @@ export const BaseGlobalRouterConfig: RouterConfigItem[] = [ path: ROUTE_PATHS.BASE.USER_ACTIVITY, key: 'userActivity', element: , - permission: PERMISSIONS.PAGES.BASE.RESOURCE_OVERVIEW + permission: PERMISSIONS.PAGES.BASE.USER_ACTIVITY } // #endif ]; diff --git a/packages/shared/lib/features/usePermission/__tests__/__snapshots__/index.test.ts.snap b/packages/shared/lib/features/usePermission/__tests__/__snapshots__/index.test.ts.snap index c787d96a4f..621a68b2f0 100644 --- a/packages/shared/lib/features/usePermission/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/shared/lib/features/usePermission/__tests__/__snapshots__/index.test.ts.snap @@ -266,6 +266,7 @@ exports[`usePermission should match snapshot 1`] = ` "RESOURCE_OVERVIEW": "page:resource_overview", "SYNC_DATA_SOURCE": "page:sync_data_source", "SYSTEM_SETTING": "page:system_setting", + "USER_ACTIVITY": "page:user_activity", "USER_CENTER": "page:user_center", }, "SQLE": { @@ -2062,6 +2063,14 @@ exports[`usePermission should match snapshot 2`] = ` ], "type": "page", }, + "page:user_activity": { + "id": "page:user_activity", + "role": [ + "admin", + "systemAdministrator", + ], + "type": "page", + }, "page:user_center": { "id": "page:user_center", "role": [ diff --git a/packages/shared/lib/features/usePermission/permissionManifest.ts b/packages/shared/lib/features/usePermission/permissionManifest.ts index c03cd88333..abf2e274cb 100644 --- a/packages/shared/lib/features/usePermission/permissionManifest.ts +++ b/packages/shared/lib/features/usePermission/permissionManifest.ts @@ -225,6 +225,11 @@ export const PERMISSION_MANIFEST: Record< SystemRole.certainProjectManager ] }, + [PERMISSIONS.PAGES.BASE.USER_ACTIVITY]: { + id: PERMISSIONS.PAGES.BASE.USER_ACTIVITY, + type: 'page', + role: [SystemRole.admin, SystemRole.systemAdministrator] + }, [PERMISSIONS.PAGES.SQLE.PROJECT_RULE_TEMPLATE]: { id: PERMISSIONS.PAGES.SQLE.PROJECT_RULE_TEMPLATE, type: 'page', diff --git a/packages/shared/lib/features/usePermission/permissions.ts b/packages/shared/lib/features/usePermission/permissions.ts index a4a2cb68da..1369dd12bb 100644 --- a/packages/shared/lib/features/usePermission/permissions.ts +++ b/packages/shared/lib/features/usePermission/permissions.ts @@ -8,7 +8,8 @@ export const PERMISSIONS = { SYSTEM_SETTING: 'page:system_setting', RESOURCE_OVERVIEW: 'page:resource_overview', MEMBER: 'page:member', - DB_SERVICE: 'page:db_service' + DB_SERVICE: 'page:db_service', + USER_ACTIVITY: 'page:user_activity' }, SQLE: { OPERATION_RECORD: 'page:operation_record', From 8de9e529502b35ce2ccbb4a0abc1dd6c14470f79 Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Wed, 24 Jun 2026 03:03:31 +0000 Subject: [PATCH 5/8] Rename user activity ranking column to top business module. Align the table label with backend logic that excludes session and bootstrap modules when picking each user's primary module. --- packages/base/src/locale/en-US/userActivity.ts | 2 +- packages/base/src/locale/zh-CN/userActivity.ts | 2 +- .../__tests__/__snapshots__/index.test.tsx.snap | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/base/src/locale/en-US/userActivity.ts b/packages/base/src/locale/en-US/userActivity.ts index 4cbf96bb64..1baff864d9 100644 --- a/packages/base/src/locale/en-US/userActivity.ts +++ b/packages/base/src/locale/en-US/userActivity.ts @@ -38,7 +38,7 @@ export default { userName: 'User', activeDays: 'Active days', requestCount: 'Requests', - topModule: 'Top module', + topModule: 'Top business module', lastActiveAt: 'Last active', emptyText: 'No user activity in selected range' } diff --git a/packages/base/src/locale/zh-CN/userActivity.ts b/packages/base/src/locale/zh-CN/userActivity.ts index 9ecc5216c6..401ea81fc9 100644 --- a/packages/base/src/locale/zh-CN/userActivity.ts +++ b/packages/base/src/locale/zh-CN/userActivity.ts @@ -38,7 +38,7 @@ export default { userName: '用户名', activeDays: '活跃天数', requestCount: '请求数', - topModule: '最常用模块', + topModule: '最常用业务模块', lastActiveAt: '最近活跃时间', emptyText: '所选区间暂无用户活跃数据' } 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 index c7e167d09d..2c8ed2dba9 100644 --- a/packages/base/src/page/UserActivity/__tests__/__snapshots__/index.test.tsx.snap +++ b/packages/base/src/page/UserActivity/__tests__/__snapshots__/index.test.tsx.snap @@ -30,8 +30,8 @@ exports[`base/page/UserActivity render init snap 1`] = ` placeholder="请选择日期" readonly="" size="12" - title="2026-06-23" - value="2026-06-23" + title="2026-06-24" + value="2026-06-24" />
    - 最常用模块 + 最常用业务模块 Date: Wed, 12 Aug 2026 03:11:42 +0000 Subject: [PATCH 6/8] fix(ee): align user activity page with mainline module layout Import ResponseCode, table helpers and utils from @actiontech/dms-kit after they moved out of @actiontech/shared, update QuickActions assertions for the added shortcut, and pin the system time in the page test so the default "today" stat date no longer drifts the snapshot. --- .../__snapshots__/index.test.tsx.snap | 58 +++++++++++++++++++ .../QuickActions/__tests__/index.test.tsx | 16 +++-- .../__snapshots__/index.test.tsx.snap | 1 + .../UserActivity/__tests__/index.test.tsx | 3 +- .../ModuleDistributionChart/index.tsx | 2 +- .../components/UserRankingTable/index.tsx | 6 +- packages/base/src/page/UserActivity/index.tsx | 7 ++- packages/base/src/page/UserActivity/utils.ts | 3 +- 8 files changed, 83 insertions(+), 13 deletions(-) diff --git a/packages/base/src/page/Nav/SideMenu/QuickActions/__tests__/__snapshots__/index.test.tsx.snap b/packages/base/src/page/Nav/SideMenu/QuickActions/__tests__/__snapshots__/index.test.tsx.snap index 2ae8225681..b1adc533f9 100644 --- a/packages/base/src/page/Nav/SideMenu/QuickActions/__tests__/__snapshots__/index.test.tsx.snap +++ b/packages/base/src/page/Nav/SideMenu/QuickActions/__tests__/__snapshots__/index.test.tsx.snap @@ -72,6 +72,35 @@ exports[`base/Nav/QuickActions render navigate event 1`] = `
    +
    +
    +
    +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    + + + +
    +
    +
    +
    { const { baseElement } = customRender(); expect(baseElement).toMatchSnapshot(); const actions = getAllBySelector('.action-item'); - expect(actions).toHaveLength(4); + expect(actions).toHaveLength(5); fireEvent.click(actions[0]); await act(async () => jest.advanceTimersByTime(0)); @@ -189,7 +189,7 @@ describe('base/Nav/QuickActions', () => { expect(navigateSpy).toHaveBeenCalledTimes(3); expect(navigateSpy).toHaveBeenNthCalledWith( 3, - ROUTE_PATHS.SQLE.RULE.index.path + ROUTE_PATHS.BASE.USER_ACTIVITY ); fireEvent.click(actions[3]); @@ -197,6 +197,14 @@ describe('base/Nav/QuickActions', () => { expect(navigateSpy).toHaveBeenCalledTimes(4); expect(navigateSpy).toHaveBeenNthCalledWith( 4, + ROUTE_PATHS.SQLE.RULE.index.path + ); + + fireEvent.click(actions[4]); + await act(async () => jest.advanceTimersByTime(0)); + expect(navigateSpy).toHaveBeenCalledTimes(5); + expect(navigateSpy).toHaveBeenNthCalledWith( + 5, ROUTE_PATHS.SQLE.KNOWLEDGE.index.path ); }); @@ -212,7 +220,7 @@ describe('base/Nav/QuickActions', () => { pathname: ROUTE_PATHS.SQLE.KNOWLEDGE.index.path }); customRender(); - expect(getAllBySelector('.action-item')[3]).toHaveClass( + expect(getAllBySelector('.action-item')[4]).toHaveClass( 'action-item-active' ); @@ -221,7 +229,7 @@ describe('base/Nav/QuickActions', () => { pathname: `${ROUTE_PATHS.SQLE.KNOWLEDGE.index.path}/${ROUTE_PATHS.SQLE.KNOWLEDGE.refined.path}` }); customRender(); - expect(getAllBySelector('.action-item')[3]).toHaveClass( + expect(getAllBySelector('.action-item')[4]).toHaveClass( 'action-item-active' ); }); 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 index 2c8ed2dba9..a8df89ca35 100644 --- a/packages/base/src/page/UserActivity/__tests__/__snapshots__/index.test.tsx.snap +++ b/packages/base/src/page/UserActivity/__tests__/__snapshots__/index.test.tsx.snap @@ -157,6 +157,7 @@ exports[`base/page/UserActivity render init snap 1`] = ` > diff --git a/packages/base/src/page/UserActivity/__tests__/index.test.tsx b/packages/base/src/page/UserActivity/__tests__/index.test.tsx index 159d97bfd7..4c462f0022 100644 --- a/packages/base/src/page/UserActivity/__tests__/index.test.tsx +++ b/packages/base/src/page/UserActivity/__tests__/index.test.tsx @@ -7,7 +7,8 @@ import { act, cleanup } from '@testing-library/react'; describe('base/page/UserActivity', () => { beforeEach(() => { userActivity.mockAllApi(); - jest.useFakeTimers(); + // 页面默认统计日期取当天,固定系统时间避免快照随运行日期漂移 + jest.useFakeTimers().setSystemTime(new Date(2026, 5, 24, 10, 0, 0)); mockUseCurrentUser(); }); diff --git a/packages/base/src/page/UserActivity/components/ModuleDistributionChart/index.tsx b/packages/base/src/page/UserActivity/components/ModuleDistributionChart/index.tsx index beb40d82bf..2290763e31 100644 --- a/packages/base/src/page/UserActivity/components/ModuleDistributionChart/index.tsx +++ b/packages/base/src/page/UserActivity/components/ModuleDistributionChart/index.tsx @@ -5,7 +5,7 @@ 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'; +import { floatRound } from '@actiontech/dms-kit'; import { USER_ACTIVITY_CHART_PADDING } from '../../utils'; type ModuleDistributionChartProps = { diff --git a/packages/base/src/page/UserActivity/components/UserRankingTable/index.tsx b/packages/base/src/page/UserActivity/components/UserRankingTable/index.tsx index 1f85649961..3dc71d57c2 100644 --- a/packages/base/src/page/UserActivity/components/UserRankingTable/index.tsx +++ b/packages/base/src/page/UserActivity/components/UserRankingTable/index.tsx @@ -5,13 +5,13 @@ import { ActiontechTable, ActiontechTableWrapper, useTableRequestError, - useTableRequestParams -} from '@actiontech/shared/lib/components/ActiontechTable'; + useTableRequestParams, + formatTime +} from '@actiontech/dms-kit'; 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; diff --git a/packages/base/src/page/UserActivity/index.tsx b/packages/base/src/page/UserActivity/index.tsx index 0fcd8de7ae..787d6b4fdc 100644 --- a/packages/base/src/page/UserActivity/index.tsx +++ b/packages/base/src/page/UserActivity/index.tsx @@ -6,7 +6,7 @@ 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 { ResponseCode } from '@actiontech/dms-kit'; import CardWrapper from 'sqle/src/components/CardWrapper'; import { UserActivityStyleWrapper } from './style'; import SummaryCards from './components/SummaryCards'; @@ -189,7 +189,10 @@ const UserActivity = () => {
    - +
    diff --git a/packages/base/src/page/UserActivity/utils.ts b/packages/base/src/page/UserActivity/utils.ts index a8839e577c..00dd517f02 100644 --- a/packages/base/src/page/UserActivity/utils.ts +++ b/packages/base/src/page/UserActivity/utils.ts @@ -1,6 +1,5 @@ import dayjs, { Dayjs } from 'dayjs'; -import { floatRound } from '@actiontech/shared/lib/utils/Math'; -import { formatParamsBySeparator } from '@actiontech/shared/lib/utils/Tool'; +import { floatRound, formatParamsBySeparator } from '@actiontech/dms-kit'; export const DATE_FORMAT = 'YYYY-MM-DD'; From cbaf29e7255de55058cb49827b6d2d80ac827245 Mon Sep 17 00:00:00 2001 From: actiontech-zihan Date: Wed, 12 Aug 2026 03:29:40 +0000 Subject: [PATCH 7/8] test(nav): refresh App and SideMenu snapshots for user activity entry The new quick action shortcut renders in both CE and EE side menus, so the affected snapshots gain the entry markup. --- .../base/src/__snapshots__/App.test.tsx.snap | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/base/src/__snapshots__/App.test.tsx.snap b/packages/base/src/__snapshots__/App.test.tsx.snap index ff42a054f9..9fb71089bd 100644 --- a/packages/base/src/__snapshots__/App.test.tsx.snap +++ b/packages/base/src/__snapshots__/App.test.tsx.snap @@ -1363,6 +1363,35 @@ exports[`App render App when token is existed 1`] = `
    +
    +
    +
    +
    + + + +
    +
    +
    +
    Date: Wed, 12 Aug 2026 03:46:14 +0000 Subject: [PATCH 8/8] fix(ee): gate the user activity quick action behind the ee build flag The report route already lives inside an ee conditional block, so without the same guard the CE build renders a shortcut that navigates to a missing route. --- packages/base/src/page/Nav/SideMenu/QuickActions/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/base/src/page/Nav/SideMenu/QuickActions/index.tsx b/packages/base/src/page/Nav/SideMenu/QuickActions/index.tsx index 118205c3a0..e7737f1d68 100644 --- a/packages/base/src/page/Nav/SideMenu/QuickActions/index.tsx +++ b/packages/base/src/page/Nav/SideMenu/QuickActions/index.tsx @@ -1,12 +1,12 @@ import { // #if [ee] TodoListOutlined, + UserCircleFilled, // #endif SignalFilled, ProfileSquareFilled, RingOutlined, - BookMarkOutlined, - UserCircleFilled + BookMarkOutlined } from '@actiontech/icons'; import { QuickActionsStyleWrapper } from '../style'; import { BasicToolTip, EmptyBox } from '@actiontech/dms-kit'; @@ -87,6 +87,7 @@ const QuickActions: React.FC = ({ icon: , permission: PERMISSIONS.PAGES.SQLE.REPORT_STATISTICS }, + // #if [ee] { key: 'user-activity', title: t('dmsMenu.globalSettings.userActivity'), @@ -94,6 +95,7 @@ const QuickActions: React.FC = ({ icon: , permission: PERMISSIONS.PAGES.BASE.USER_ACTIVITY }, + // #endif { key: 'view-rule', title: t('dmsMenu.globalSettings.viewRule'),