[AI assisted]: Improve system console statistics performance (#29899)
```release-note NONE ``` Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
6046a304b2
Коммит
ae9e6174e5
@@ -19,23 +19,6 @@ export function formatChannelDoughtnutData(totalPublic: any, totalPrivate: any)
|
||||
return channelTypeData;
|
||||
}
|
||||
|
||||
export function formatPostDoughtnutData(filePosts: any, hashtagPosts: any, totalPosts: any) {
|
||||
const postTypeData = {
|
||||
labels: [
|
||||
Utils.localizeMessage({id: 'analytics.system.totalFilePosts', defaultMessage: 'Posts with Files'}),
|
||||
Utils.localizeMessage({id: 'analytics.system.totalHashtagPosts', defaultMessage: 'Posts with Hashtags'}),
|
||||
Utils.localizeMessage({id: 'analytics.system.textPosts', defaultMessage: 'Posts with Text-only'}),
|
||||
],
|
||||
datasets: [{
|
||||
data: [filePosts, hashtagPosts, (totalPosts - filePosts - hashtagPosts)],
|
||||
backgroundColor: ['#46BFBD', '#F7464A', '#FDB45C'],
|
||||
hoverBackgroundColor: ['#5AD3D1', '#FF5A5E', '#FFC870'],
|
||||
}],
|
||||
};
|
||||
|
||||
return postTypeData;
|
||||
}
|
||||
|
||||
export function formatPostsPerDayData(labels: string[], data: any) {
|
||||
const chartData = {
|
||||
labels: [] as string[],
|
||||
|
||||
@@ -41,4 +41,19 @@ describe('components/analytics/statistic_count.tsx', () => {
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should apply formatter function when provided', () => {
|
||||
const mockFormatter = (value: number) => `${value}%`;
|
||||
const wrapper = shallow(
|
||||
<StatisticCount
|
||||
title='Test'
|
||||
icon='test-icon'
|
||||
count={42}
|
||||
id='test-stat'
|
||||
formatter={mockFormatter}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-testid="test-stat"]').text()).toBe('42%');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ type Props = {
|
||||
id?: string;
|
||||
children?: React.ReactNode;
|
||||
status?: 'warning' | 'error';
|
||||
formatter?: (value: number) => string;
|
||||
}
|
||||
|
||||
const StatisticCount = ({
|
||||
@@ -22,6 +23,7 @@ const StatisticCount = ({
|
||||
id,
|
||||
children,
|
||||
status,
|
||||
formatter,
|
||||
}: Props) => {
|
||||
const loading = (
|
||||
<FormattedMessage
|
||||
@@ -30,6 +32,9 @@ const StatisticCount = ({
|
||||
/>
|
||||
);
|
||||
|
||||
const result = formatter ? formatter(count ?? 0) : count;
|
||||
const displayValue = typeof count === 'undefined' || isNaN(count) ? loading : result;
|
||||
|
||||
return (
|
||||
<div className='grid-statistics__card'>
|
||||
<div
|
||||
@@ -57,7 +62,7 @@ const StatisticCount = ({
|
||||
'team_statistics--error': status === 'error',
|
||||
})}
|
||||
>
|
||||
{typeof count === 'undefined' || isNaN(count) ? loading : count}
|
||||
{displayValue}
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
details {
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||
border-radius: 4px;
|
||||
background-color: var(--center-channel-bg);
|
||||
}
|
||||
|
||||
summary {
|
||||
position: relative;
|
||||
padding: 12px 12px 12px 28px;
|
||||
margin: -12px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: background 0.15s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background: rgba(var(--center-channel-color-rgb), 0.04);
|
||||
}
|
||||
|
||||
&::marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 12px;
|
||||
content: '►';
|
||||
font-size: 10px;
|
||||
transform: rotate(0deg);
|
||||
transition: transform 0.15s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
details[open] {
|
||||
|
||||
summary {
|
||||
border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||
margin-bottom: 0;
|
||||
|
||||
&::before {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.row:last-child .total-count {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {fireEvent} from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
@@ -59,7 +60,7 @@ describe('components/analytics/system_analytics/system_analytics.tsx', () => {
|
||||
expect(screen.queryByTestId('totalPostsLineChart')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('system data', () => {
|
||||
test('system data', async () => {
|
||||
const state = {
|
||||
...initialState,
|
||||
entities: {
|
||||
@@ -90,6 +91,11 @@ describe('components/analytics/system_analytics/system_analytics.tsx', () => {
|
||||
|
||||
renderWithContext(<SystemAnalytics {...baseProps}/>, state, {useMockedStore: true});
|
||||
|
||||
const detailsElement = screen.getByText('Load Advanced Statistics');
|
||||
fireEvent.click(detailsElement);
|
||||
|
||||
await screen.findByTestId('totalPostsLineChart');
|
||||
|
||||
expect(screen.getByTestId('totalPosts')).toHaveTextContent('45');
|
||||
expect(screen.getByTestId('totalPostsLineChart')).toBeInTheDocument();
|
||||
});
|
||||
@@ -233,6 +239,11 @@ describe('components/analytics/system_analytics/system_analytics.tsx', () => {
|
||||
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
const detailsElement = screen.getByText('Load Advanced Statistics');
|
||||
fireEvent.click(detailsElement);
|
||||
|
||||
await screen.findByTestId('totalPostsLineChart');
|
||||
|
||||
expect(screen.getByTestId('totalPosts')).toHaveTextContent('45');
|
||||
expect(screen.getByTestId('totalPostsLineChart')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('com.mattermost.playbooks.playbook_count')).toHaveTextContent('45');
|
||||
|
||||
@@ -8,6 +8,8 @@ import type {AnalyticsRow, PluginAnalyticsRow, IndexedPluginAnalyticsRow, Analyt
|
||||
import {AnalyticsVisualizationType} from '@mattermost/types/admin';
|
||||
import type {ClientLicense} from '@mattermost/types/config';
|
||||
|
||||
import {getFormattedFileSize} from 'mattermost-redux/utils/file_utils';
|
||||
|
||||
import * as AdminActions from 'actions/admin_actions.jsx';
|
||||
|
||||
import ActivatedUserCard from 'components/analytics/activated_users_card';
|
||||
@@ -16,6 +18,8 @@ import AdminHeader from 'components/widgets/admin_console/admin_header';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
|
||||
import './analytics.scss';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import DoughnutChart from '../doughnut_chart';
|
||||
@@ -23,7 +27,6 @@ import {
|
||||
formatPostsPerDayData,
|
||||
formatUsersWithPostsPerDayData,
|
||||
formatChannelDoughtnutData,
|
||||
formatPostDoughtnutData,
|
||||
synchronizeChartLabels,
|
||||
} from '../format';
|
||||
import LineChart from '../line_chart';
|
||||
@@ -40,6 +43,7 @@ type Props = {
|
||||
|
||||
type State = {
|
||||
pluginSiteStats: Record<string, PluginAnalyticsRow>;
|
||||
lineChartsDataLoaded: boolean;
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -59,6 +63,8 @@ const messages = defineMessages({
|
||||
totalChannels: {id: 'analytics.system.totalChannels', defaultMessage: 'Total Channels'},
|
||||
dailyActiveUsers: {id: 'analytics.system.dailyActiveUsers', defaultMessage: 'Daily Active Users'},
|
||||
monthlyActiveUsers: {id: 'analytics.system.monthlyActiveUsers', defaultMessage: 'Monthly Active Users'},
|
||||
totalFiles: {id: 'analytics.system.totalFiles', defaultMessage: 'Total Files'},
|
||||
totalFilesSize: {id: 'analytics.system.totalFilesSize', defaultMessage: 'Total Files Size'},
|
||||
});
|
||||
|
||||
export const searchableStrings = [
|
||||
@@ -78,18 +84,18 @@ export const searchableStrings = [
|
||||
messages.totalChannels,
|
||||
messages.dailyActiveUsers,
|
||||
messages.monthlyActiveUsers,
|
||||
messages.totalFiles,
|
||||
messages.totalFilesSize,
|
||||
];
|
||||
|
||||
export default class SystemAnalytics extends React.PureComponent<Props, State> {
|
||||
state = {
|
||||
pluginSiteStats: {} as Record<string, PluginAnalyticsRow>,
|
||||
lineChartsDataLoaded: false,
|
||||
};
|
||||
|
||||
public async componentDidMount() {
|
||||
AdminActions.getStandardAnalytics();
|
||||
AdminActions.getPostsPerDayAnalytics();
|
||||
AdminActions.getBotPostsPerDayAnalytics();
|
||||
AdminActions.getUsersPerDayAnalytics();
|
||||
|
||||
if (this.props.isLicensed) {
|
||||
AdminActions.getAdvancedAnalytics();
|
||||
@@ -97,6 +103,24 @@ export default class SystemAnalytics extends React.PureComponent<Props, State> {
|
||||
this.fetchPluginStats();
|
||||
}
|
||||
|
||||
private loadLineChartData = async () => {
|
||||
await Promise.allSettled([
|
||||
AdminActions.getPostsPerDayAnalytics(),
|
||||
AdminActions.getBotPostsPerDayAnalytics(),
|
||||
AdminActions.getUsersPerDayAnalytics(),
|
||||
]);
|
||||
this.setState({lineChartsDataLoaded: true});
|
||||
};
|
||||
|
||||
private handleLineChartsToggle = (e: React.MouseEvent<HTMLDetailsElement>) => {
|
||||
const details = e.currentTarget;
|
||||
const isExpanding = details.open;
|
||||
|
||||
if (isExpanding && !this.state.lineChartsDataLoaded) {
|
||||
this.loadLineChartData();
|
||||
}
|
||||
};
|
||||
|
||||
// fetchPluginStats does a call for each one of the registered handlers,
|
||||
// wait and set the data in the state
|
||||
private async fetchPluginStats() {
|
||||
@@ -223,6 +247,8 @@ export default class SystemAnalytics extends React.PureComponent<Props, State> {
|
||||
let commandCount;
|
||||
let incomingCount;
|
||||
let outgoingCount;
|
||||
let totalFiles;
|
||||
let totalFilesSize;
|
||||
if (this.props.isLicensed) {
|
||||
sessionCount = (
|
||||
<StatisticCount
|
||||
@@ -262,6 +288,25 @@ export default class SystemAnalytics extends React.PureComponent<Props, State> {
|
||||
/>
|
||||
);
|
||||
|
||||
totalFiles = (
|
||||
<StatisticCount
|
||||
id='totalFiles'
|
||||
title={<FormattedMessage {...messages.totalFiles}/>}
|
||||
icon='fa-files-o'
|
||||
count={this.getStatValue(stats[StatTypes.TOTAL_FILE_COUNT])}
|
||||
/>
|
||||
);
|
||||
|
||||
totalFilesSize = (
|
||||
<StatisticCount
|
||||
id='totalFilesSize'
|
||||
title={<FormattedMessage {...messages.totalFilesSize}/>}
|
||||
icon='fa-files-o'
|
||||
count={this.getStatValue(stats[StatTypes.TOTAL_FILE_SIZE])}
|
||||
formatter={getFormattedFileSize}
|
||||
/>
|
||||
);
|
||||
|
||||
advancedStats = (
|
||||
<>
|
||||
<StatisticCount
|
||||
@@ -289,20 +334,6 @@ export default class SystemAnalytics extends React.PureComponent<Props, State> {
|
||||
);
|
||||
|
||||
const channelTypeData = formatChannelDoughtnutData(stats[StatTypes.TOTAL_PUBLIC_CHANNELS], stats[StatTypes.TOTAL_PRIVATE_GROUPS]);
|
||||
const postTypeData = formatPostDoughtnutData(stats[StatTypes.TOTAL_FILE_POSTS], stats[StatTypes.TOTAL_HASHTAG_POSTS], stats[StatTypes.TOTAL_POSTS]);
|
||||
|
||||
let postTypeGraph;
|
||||
if (stats[StatTypes.TOTAL_POSTS] !== -1) {
|
||||
postTypeGraph = (
|
||||
<DoughnutChart
|
||||
title={<FormattedMessage {...messages.postTypes}/>
|
||||
}
|
||||
data={postTypeData}
|
||||
width={300}
|
||||
height={225}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
advancedGraphs = (
|
||||
<div className='row'>
|
||||
@@ -313,7 +344,6 @@ export default class SystemAnalytics extends React.PureComponent<Props, State> {
|
||||
width={300}
|
||||
height={225}
|
||||
/>
|
||||
{postTypeGraph}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -401,25 +431,33 @@ export default class SystemAnalytics extends React.PureComponent<Props, State> {
|
||||
switch (stat.visualizationType) {
|
||||
case AnalyticsVisualizationType.LineChart:
|
||||
pluginLineCharts.push((
|
||||
<LineChart
|
||||
id={key}
|
||||
<div
|
||||
className='row'
|
||||
key={'pluginstat.' + key}
|
||||
title={stat.name}
|
||||
data={stat.value}
|
||||
width={740}
|
||||
height={225}
|
||||
/>
|
||||
>
|
||||
<LineChart
|
||||
id={key}
|
||||
title={stat.name}
|
||||
data={stat.value}
|
||||
width={740}
|
||||
height={225}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
break;
|
||||
case AnalyticsVisualizationType.DoughnutChart:
|
||||
pluginDoughnutCharts.push((
|
||||
<DoughnutChart
|
||||
<div
|
||||
className='row'
|
||||
key={'pluginstat.' + key}
|
||||
title={stat.name}
|
||||
data={stat.value}
|
||||
width={300}
|
||||
height={225}
|
||||
/>
|
||||
>
|
||||
<DoughnutChart
|
||||
title={stat.name}
|
||||
data={stat.value}
|
||||
width={300}
|
||||
height={225}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
break;
|
||||
case AnalyticsVisualizationType.Count:
|
||||
@@ -449,6 +487,8 @@ export default class SystemAnalytics extends React.PureComponent<Props, State> {
|
||||
{commandCount}
|
||||
{incomingCount}
|
||||
{outgoingCount}
|
||||
{totalFiles}
|
||||
{totalFilesSize}
|
||||
</>
|
||||
);
|
||||
} else if (!isLicensed) {
|
||||
@@ -480,10 +520,23 @@ export default class SystemAnalytics extends React.PureComponent<Props, State> {
|
||||
</div>
|
||||
{advancedGraphs}
|
||||
{pluginDoughnutCharts}
|
||||
{postTotalGraph}
|
||||
{botPostTotalGraph}
|
||||
{activeUserGraph}
|
||||
{pluginLineCharts}
|
||||
<details
|
||||
onToggle={this.handleLineChartsToggle}
|
||||
data-testid='details-expander'
|
||||
>
|
||||
<summary>
|
||||
<FormattedMessage
|
||||
id='analytics.system.perDayStatistics'
|
||||
defaultMessage='Load Advanced Statistics'
|
||||
/>
|
||||
</summary>
|
||||
<>
|
||||
{postTotalGraph}
|
||||
{botPostTotalGraph}
|
||||
{activeUserGraph}
|
||||
</>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2895,18 +2895,18 @@
|
||||
"analytics.system.infoAndSkippedIntensiveQueries1": "Use data for only the chosen team. Exclude posts in direct message channels that are not tied to a team.",
|
||||
"analytics.system.infoAndSkippedIntensiveQueries2": "To maximize performance, some statistics are disabled. You can <link>re-enable them in config.json</link>.",
|
||||
"analytics.system.monthlyActiveUsers": "Monthly Active Users",
|
||||
"analytics.system.perDayStatistics": "Load Advanced Statistics",
|
||||
"analytics.system.postTypes": "Posts, Files and Hashtags",
|
||||
"analytics.system.privateGroups": "Private Channels",
|
||||
"analytics.system.publicChannels": "Public Channels",
|
||||
"analytics.system.seatsPurchased": "Licensed Seats",
|
||||
"analytics.system.skippedIntensiveQueries": "To maximize performance, some statistics are disabled. You can <link>re-enable them in config.json</link>.",
|
||||
"analytics.system.textPosts": "Posts with Text-only",
|
||||
"analytics.system.title": "System Statistics",
|
||||
"analytics.system.totalBotPosts": "Total Posts from Bots",
|
||||
"analytics.system.totalChannels": "Total Channels",
|
||||
"analytics.system.totalCommands": "Total Commands",
|
||||
"analytics.system.totalFilePosts": "Posts with Files",
|
||||
"analytics.system.totalHashtagPosts": "Posts with Hashtags",
|
||||
"analytics.system.totalFiles": "Total Files",
|
||||
"analytics.system.totalFilesSize": "Total Files Size",
|
||||
"analytics.system.totalIncomingWebhooks": "Incoming Webhooks",
|
||||
"analytics.system.totalMasterDbConnections": "Master DB Conns",
|
||||
"analytics.system.totalOutgoingWebhooks": "Outgoing Webhooks",
|
||||
|
||||
@@ -10,8 +10,6 @@ export default keyMirror({
|
||||
TOTAL_PRIVATE_GROUPS: null,
|
||||
TOTAL_POSTS: null,
|
||||
TOTAL_TEAMS: null,
|
||||
TOTAL_FILE_POSTS: null,
|
||||
TOTAL_HASHTAG_POSTS: null,
|
||||
TOTAL_IHOOKS: null,
|
||||
TOTAL_OHOOKS: null,
|
||||
TOTAL_COMMANDS: null,
|
||||
@@ -27,5 +25,7 @@ export default keyMirror({
|
||||
DAILY_ACTIVE_USERS: null,
|
||||
MONTHLY_ACTIVE_USERS: null,
|
||||
REGISTERED_USERS: null,
|
||||
TOTAL_FILE_COUNT: null,
|
||||
TOTAL_FILE_SIZE: null,
|
||||
});
|
||||
|
||||
|
||||
@@ -218,12 +218,6 @@ export function convertAnalyticsRowsToStats(data: AnalyticsRow[], name: string):
|
||||
case 'monthly_active_users':
|
||||
key = Stats.MONTHLY_ACTIVE_USERS;
|
||||
break;
|
||||
case 'file_post_count':
|
||||
key = Stats.TOTAL_FILE_POSTS;
|
||||
break;
|
||||
case 'hashtag_post_count':
|
||||
key = Stats.TOTAL_HASHTAG_POSTS;
|
||||
break;
|
||||
case 'incoming_webhook_count':
|
||||
key = Stats.TOTAL_IHOOKS;
|
||||
break;
|
||||
@@ -239,6 +233,12 @@ export function convertAnalyticsRowsToStats(data: AnalyticsRow[], name: string):
|
||||
case 'registered_users':
|
||||
key = Stats.REGISTERED_USERS;
|
||||
break;
|
||||
case 'total_file_count':
|
||||
key = Stats.TOTAL_FILE_COUNT;
|
||||
break;
|
||||
case 'total_file_size':
|
||||
key = Stats.TOTAL_FILE_SIZE;
|
||||
break;
|
||||
}
|
||||
|
||||
if (key) {
|
||||
|
||||
@@ -7,8 +7,7 @@ import {Client4} from 'mattermost-redux/client';
|
||||
|
||||
import {Files, General} from '../constants';
|
||||
|
||||
export function getFormattedFileSize(file: FileInfo): string {
|
||||
const bytes = file.size;
|
||||
export function getFormattedFileSize(bytes: number): string {
|
||||
const fileSizes = [
|
||||
['TB', 1024 * 1024 * 1024 * 1024],
|
||||
['GB', 1024 * 1024 * 1024],
|
||||
|
||||
@@ -824,8 +824,6 @@ export const StatTypes = keyMirror({
|
||||
TOTAL_PRIVATE_GROUPS: null,
|
||||
TOTAL_POSTS: null,
|
||||
TOTAL_TEAMS: null,
|
||||
TOTAL_FILE_POSTS: null,
|
||||
TOTAL_HASHTAG_POSTS: null,
|
||||
TOTAL_IHOOKS: null,
|
||||
TOTAL_OHOOKS: null,
|
||||
TOTAL_COMMANDS: null,
|
||||
@@ -840,6 +838,8 @@ export const StatTypes = keyMirror({
|
||||
TOTAL_READ_DB_CONNECTIONS: null,
|
||||
DAILY_ACTIVE_USERS: null,
|
||||
MONTHLY_ACTIVE_USERS: null,
|
||||
TOTAL_FILE_COUNT: null,
|
||||
TOTAL_FILE_SIZE: null,
|
||||
});
|
||||
|
||||
export const SearchTypes = keyMirror({
|
||||
|
||||
@@ -87,13 +87,13 @@ export type AnalyticsState = {
|
||||
TOTAL_READ_DB_CONNECTIONS?: number;
|
||||
DAILY_ACTIVE_USERS?: number;
|
||||
MONTHLY_ACTIVE_USERS?: number;
|
||||
TOTAL_FILE_POSTS?: number;
|
||||
TOTAL_HASHTAG_POSTS?: number;
|
||||
TOTAL_IHOOKS?: number;
|
||||
TOTAL_OHOOKS?: number;
|
||||
TOTAL_COMMANDS?: number;
|
||||
TOTAL_SESSIONS?: number;
|
||||
REGISTERED_USERS?: number;
|
||||
TOTAL_FILE_COUNT?: number;
|
||||
TOTAL_FILE_SIZE?: number;
|
||||
}
|
||||
|
||||
export type ClusterInfo = {
|
||||
|
||||
Ссылка в новой задаче
Block a user