[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 {FormattedMessage, defineMessages} from 'react-intl';
|
||||||
|
|
||||||
import type {AnalyticsRow, PluginAnalyticsRow, IndexedPluginAnalyticsRow, AnalyticsState} from '@mattermost/types/admin';
|
import type {AnalyticsRow, PluginAnalyticsRow, IndexedPluginAnalyticsRow, AnalyticsState} from '@mattermost/types/admin';
|
||||||
|
import {AnalyticsVisualizationType} from '@mattermost/types/admin';
|
||||||
import type {ClientLicense} from '@mattermost/types/config';
|
import type {ClientLicense} from '@mattermost/types/config';
|
||||||
|
|
||||||
import * as AdminActions from 'actions/admin_actions.jsx';
|
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
|
// Extract plugin stats that should be displayed and pass them to widget
|
||||||
const pluginSiteStats = (
|
const pluginCounts = [];
|
||||||
<>
|
const pluginLineCharts = [];
|
||||||
{Object.entries(this.state.pluginSiteStats).map(([key, stat]) =>
|
const pluginDoughnutCharts = [];
|
||||||
(
|
|
||||||
<StatisticCount
|
for (const [key, stat] of Object.entries(this.state.pluginSiteStats)) {
|
||||||
id={key}
|
switch (stat.visualizationType) {
|
||||||
key={'pluginstat.' + key}
|
case AnalyticsVisualizationType.LineChart:
|
||||||
title={stat.name}
|
pluginLineCharts.push((
|
||||||
icon={stat.icon}
|
<LineChart
|
||||||
count={stat.value}
|
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;
|
let systemCards;
|
||||||
if (isLicensed) {
|
if (isLicensed) {
|
||||||
@@ -448,12 +476,14 @@ export default class SystemAnalytics extends React.PureComponent<Props, State> {
|
|||||||
{dailyActiveUsers}
|
{dailyActiveUsers}
|
||||||
{monthlyActiveUsers}
|
{monthlyActiveUsers}
|
||||||
{advancedStats}
|
{advancedStats}
|
||||||
{pluginSiteStats}
|
{pluginCounts}
|
||||||
</div>
|
</div>
|
||||||
{advancedGraphs}
|
{advancedGraphs}
|
||||||
|
{pluginDoughnutCharts}
|
||||||
{postTotalGraph}
|
{postTotalGraph}
|
||||||
{botPostTotalGraph}
|
{botPostTotalGraph}
|
||||||
{activeUserGraph}
|
{activeUserGraph}
|
||||||
|
{pluginLineCharts}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -114,11 +114,18 @@ export type IndexedPluginAnalyticsRow = {
|
|||||||
[key: string]: PluginAnalyticsRow;
|
[key: string]: PluginAnalyticsRow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum AnalyticsVisualizationType {
|
||||||
|
Count = 'count',
|
||||||
|
LineChart = 'line_chart',
|
||||||
|
DoughnutChart = 'doughnut_chart',
|
||||||
|
}
|
||||||
|
|
||||||
export type PluginAnalyticsRow = {
|
export type PluginAnalyticsRow = {
|
||||||
id: string;
|
id: string;
|
||||||
name: React.ReactNode;
|
name: React.ReactNode;
|
||||||
icon: string;
|
icon?: string;
|
||||||
value: number;
|
value: any;
|
||||||
|
visualizationType?: AnalyticsVisualizationType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SchemaMigration = {
|
export type SchemaMigration = {
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user