[MM-58017] Allow plugins to use more visualization types for admin statistics (#27188)
* Allow plugins to use more visualization types for admin statistics * Fix translations failure in test * Review types and better tests
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
1ec2de4a95
Коммит
d2c3710265
@@ -0,0 +1,247 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import SystemAnalytics from 'components/analytics/system_analytics';
|
||||
|
||||
import {renderWithContext, screen} from 'tests/react_testing_utils';
|
||||
import Constants from 'utils/constants';
|
||||
|
||||
const StatTypes = Constants.StatTypes;
|
||||
|
||||
global.ResizeObserver = jest.fn().mockImplementation(() => ({
|
||||
observe: jest.fn(),
|
||||
unobserve: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('components/analytics/system_analytics/system_analytics.tsx', () => {
|
||||
const baseProps = {
|
||||
stats: null,
|
||||
license: {
|
||||
IsLicensed: 'true',
|
||||
Cloud: 'true',
|
||||
},
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
entities: {
|
||||
general: {
|
||||
license: {
|
||||
IsLicensed: 'true',
|
||||
Cloud: 'true',
|
||||
},
|
||||
config: {
|
||||
TelemetryId: 'test123',
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: 'current_user_id',
|
||||
profiles: {
|
||||
current_user_id: {roles: 'system_admin'},
|
||||
},
|
||||
},
|
||||
admin: {
|
||||
analytics: {},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
siteStatsHandlers: {},
|
||||
},
|
||||
};
|
||||
|
||||
test('no data', () => {
|
||||
renderWithContext(<SystemAnalytics {...baseProps}/>, initialState, {useMockedStore: true});
|
||||
|
||||
expect(screen.getByTestId('totalPosts')).toHaveTextContent('Loading...');
|
||||
expect(screen.queryByTestId('totalPostsLineChart')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('system data', () => {
|
||||
const state = {
|
||||
...initialState,
|
||||
entities: {
|
||||
...initialState.entities,
|
||||
admin: {
|
||||
analytics: {
|
||||
[StatTypes.TOTAL_POSTS]: 45,
|
||||
[StatTypes.POST_PER_DAY]: [
|
||||
{
|
||||
name: '2024-05-20',
|
||||
value: 45,
|
||||
},
|
||||
{
|
||||
name: '2024-05-21',
|
||||
value: 45,
|
||||
},
|
||||
{
|
||||
name: '2024-05-22',
|
||||
value: 45,
|
||||
},
|
||||
],
|
||||
[StatTypes.TOTAL_PUBLIC_CHANNELS]: 4545,
|
||||
[StatTypes.TOTAL_PRIVATE_GROUPS]: 45,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithContext(<SystemAnalytics {...baseProps}/>, state, {useMockedStore: true});
|
||||
|
||||
expect(screen.getByTestId('totalPosts')).toHaveTextContent('45');
|
||||
expect(screen.getByTestId('totalPostsLineChart')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('plugins data', async () => {
|
||||
const totalPlaybooksID = 'total_playbooks';
|
||||
const totalPlaybookRunsID = 'total_playbook_runs';
|
||||
const playbooksStats = {
|
||||
playbook_count: {
|
||||
id: 'total_playbooks',
|
||||
icon: 'fa-book',
|
||||
name:
|
||||
<FormattedMessage
|
||||
id={totalPlaybooksID}
|
||||
defaultMessage='Total Playbooks'
|
||||
/>,
|
||||
value: 45,
|
||||
},
|
||||
playbook_run_count: {
|
||||
id: 'total_runs',
|
||||
icon: 'fa-list-alt',
|
||||
name:
|
||||
<FormattedMessage
|
||||
id={totalPlaybookRunsID}
|
||||
defaultMessage='Total Runs'
|
||||
/>,
|
||||
value: 45,
|
||||
},
|
||||
};
|
||||
|
||||
const callsStats = {
|
||||
calls_count: {
|
||||
visualizationType: 'count',
|
||||
name: 'Total Calls',
|
||||
id: 'total_calls',
|
||||
icon: 'fa-phone',
|
||||
value: 1000,
|
||||
},
|
||||
calls_sessions_count: {
|
||||
visualizationType: 'count',
|
||||
name: 'Total Calls Sessions',
|
||||
id: 'total_calls_sessions',
|
||||
icon: 'fa-phone',
|
||||
value: 10000,
|
||||
},
|
||||
calls_per_day: {
|
||||
visualizationType: 'line_chart',
|
||||
name: 'Calls per day',
|
||||
id: 'calls_per_day',
|
||||
value: {
|
||||
labels: [
|
||||
'2024-05-18',
|
||||
'2024-05-19',
|
||||
'2024-05-20',
|
||||
'2024-05-21',
|
||||
'2024-05-22',
|
||||
'2024-05-23',
|
||||
'2024-05-24',
|
||||
'2024-05-26',
|
||||
'2024-05-27',
|
||||
'2024-05-28',
|
||||
],
|
||||
datasets: [{
|
||||
label: '',
|
||||
fillColor: 'rgba(151,187,205,0.2)',
|
||||
borderColor: 'rgba(151,187,205,1)',
|
||||
pointBackgroundColor: 'rgba(151,187,205,1)',
|
||||
pointBorderColor: '#fff',
|
||||
pointHoverBackgroundColor: '#fff',
|
||||
pointHoverBorderColor: 'rgba(151,187,205,1)',
|
||||
data: [
|
||||
10,
|
||||
45,
|
||||
60,
|
||||
45,
|
||||
25,
|
||||
20,
|
||||
40,
|
||||
45,
|
||||
100,
|
||||
150,
|
||||
],
|
||||
}],
|
||||
},
|
||||
},
|
||||
calls_per_channel: {
|
||||
visualizationType: 'doughnut_chart',
|
||||
name: 'Calls per channel',
|
||||
id: 'calls_per_channel',
|
||||
value: {
|
||||
labels: [
|
||||
'Public',
|
||||
'Private',
|
||||
'Direct',
|
||||
'Group',
|
||||
],
|
||||
datasets: [{
|
||||
data: [100, 45, 45, 100],
|
||||
backgroundColor: ['#46BFBD', '#FDB45C', '#3CB470', '#502D86'],
|
||||
hoverBackgroundColor: ['#5AD3D1', '#FFC870', '#3CB470', '#502D86'],
|
||||
}],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const state = {
|
||||
...initialState,
|
||||
entities: {
|
||||
...initialState.entities,
|
||||
admin: {
|
||||
analytics: {
|
||||
[StatTypes.TOTAL_POSTS]: 45,
|
||||
[StatTypes.POST_PER_DAY]: [
|
||||
{
|
||||
name: '2024-05-20',
|
||||
value: 45,
|
||||
},
|
||||
{
|
||||
name: '2024-05-21',
|
||||
value: 45,
|
||||
},
|
||||
{
|
||||
name: '2024-05-22',
|
||||
value: 45,
|
||||
},
|
||||
],
|
||||
[StatTypes.TOTAL_PUBLIC_CHANNELS]: 4545,
|
||||
[StatTypes.TOTAL_PRIVATE_GROUPS]: 45,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
siteStatsHandlers: {
|
||||
'com.mattermost.calls': () => callsStats,
|
||||
'com.mattermost.playbooks': () => playbooksStats,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithContext(<SystemAnalytics {...baseProps}/>, state, {useMockedStore: true});
|
||||
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
expect(screen.getByTestId('totalPosts')).toHaveTextContent('45');
|
||||
expect(screen.getByTestId('totalPostsLineChart')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('com.mattermost.playbooks.playbook_count')).toHaveTextContent('45');
|
||||
expect(screen.getByTestId('com.mattermost.playbooks.playbook_run_count')).toHaveTextContent('45');
|
||||
|
||||
expect(screen.getByTestId('com.mattermost.calls.calls_count')).toHaveTextContent('1000');
|
||||
expect(screen.getByTestId('com.mattermost.calls.calls_sessions_count')).toHaveTextContent('10000');
|
||||
|
||||
expect(screen.getByText('Calls per channel')).toBeInTheDocument();
|
||||
expect(screen.getByText('Calls per day')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import React from 'react';
|
||||
import {FormattedMessage, defineMessages} from 'react-intl';
|
||||
|
||||
import type {AnalyticsRow, PluginAnalyticsRow, IndexedPluginAnalyticsRow, AnalyticsState} from '@mattermost/types/admin';
|
||||
import {AnalyticsVisualizationType} from '@mattermost/types/admin';
|
||||
import type {ClientLicense} from '@mattermost/types/config';
|
||||
|
||||
import * as AdminActions from 'actions/admin_actions.jsx';
|
||||
@@ -392,21 +393,48 @@ export default class SystemAnalytics extends React.PureComponent<Props, State> {
|
||||
);
|
||||
|
||||
// Extract plugin stats that should be displayed and pass them to widget
|
||||
const pluginSiteStats = (
|
||||
<>
|
||||
{Object.entries(this.state.pluginSiteStats).map(([key, stat]) =>
|
||||
(
|
||||
<StatisticCount
|
||||
id={key}
|
||||
key={'pluginstat.' + key}
|
||||
title={stat.name}
|
||||
icon={stat.icon}
|
||||
count={stat.value}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</>
|
||||
);
|
||||
const pluginCounts = [];
|
||||
const pluginLineCharts = [];
|
||||
const pluginDoughnutCharts = [];
|
||||
|
||||
for (const [key, stat] of Object.entries(this.state.pluginSiteStats)) {
|
||||
switch (stat.visualizationType) {
|
||||
case AnalyticsVisualizationType.LineChart:
|
||||
pluginLineCharts.push((
|
||||
<LineChart
|
||||
id={key}
|
||||
key={'pluginstat.' + key}
|
||||
title={stat.name}
|
||||
data={stat.value}
|
||||
width={740}
|
||||
height={225}
|
||||
/>
|
||||
));
|
||||
break;
|
||||
case AnalyticsVisualizationType.DoughnutChart:
|
||||
pluginDoughnutCharts.push((
|
||||
<DoughnutChart
|
||||
key={'pluginstat.' + key}
|
||||
title={stat.name}
|
||||
data={stat.value}
|
||||
width={300}
|
||||
height={225}
|
||||
/>
|
||||
));
|
||||
break;
|
||||
case AnalyticsVisualizationType.Count:
|
||||
default:
|
||||
pluginCounts.push((
|
||||
<StatisticCount
|
||||
id={key}
|
||||
key={'pluginstat.' + key}
|
||||
title={stat.name}
|
||||
icon={stat.icon!}
|
||||
count={stat.value}
|
||||
/>
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let systemCards;
|
||||
if (isLicensed) {
|
||||
@@ -448,12 +476,14 @@ export default class SystemAnalytics extends React.PureComponent<Props, State> {
|
||||
{dailyActiveUsers}
|
||||
{monthlyActiveUsers}
|
||||
{advancedStats}
|
||||
{pluginSiteStats}
|
||||
{pluginCounts}
|
||||
</div>
|
||||
{advancedGraphs}
|
||||
{pluginDoughnutCharts}
|
||||
{postTotalGraph}
|
||||
{botPostTotalGraph}
|
||||
{activeUserGraph}
|
||||
{pluginLineCharts}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -114,11 +114,18 @@ export type IndexedPluginAnalyticsRow = {
|
||||
[key: string]: PluginAnalyticsRow;
|
||||
}
|
||||
|
||||
export enum AnalyticsVisualizationType {
|
||||
Count = 'count',
|
||||
LineChart = 'line_chart',
|
||||
DoughnutChart = 'doughnut_chart',
|
||||
}
|
||||
|
||||
export type PluginAnalyticsRow = {
|
||||
id: string;
|
||||
name: React.ReactNode;
|
||||
icon: string;
|
||||
value: number;
|
||||
icon?: string;
|
||||
value: any;
|
||||
visualizationType?: AnalyticsVisualizationType;
|
||||
};
|
||||
|
||||
export type SchemaMigration = {
|
||||
|
||||
Ссылка в новой задаче
Block a user