diff --git a/webapp/channels/src/components/analytics/system_analytics/system_analytics.test.tsx b/webapp/channels/src/components/analytics/system_analytics/system_analytics.test.tsx
new file mode 100644
index 0000000000..0f6f15a303
--- /dev/null
+++ b/webapp/channels/src/components/analytics/system_analytics/system_analytics.test.tsx
@@ -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(, 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(, 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:
+ ,
+ value: 45,
+ },
+ playbook_run_count: {
+ id: 'total_runs',
+ icon: 'fa-list-alt',
+ name:
+ ,
+ 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(, 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();
+ });
+});
diff --git a/webapp/channels/src/components/analytics/system_analytics/system_analytics.tsx b/webapp/channels/src/components/analytics/system_analytics/system_analytics.tsx
index 3f3e19ef5b..7ab69f141f 100644
--- a/webapp/channels/src/components/analytics/system_analytics/system_analytics.tsx
+++ b/webapp/channels/src/components/analytics/system_analytics/system_analytics.tsx
@@ -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 {
);
// Extract plugin stats that should be displayed and pass them to widget
- const pluginSiteStats = (
- <>
- {Object.entries(this.state.pluginSiteStats).map(([key, stat]) =>
- (
-
- ),
- )}
- >
- );
+ const pluginCounts = [];
+ const pluginLineCharts = [];
+ const pluginDoughnutCharts = [];
+
+ for (const [key, stat] of Object.entries(this.state.pluginSiteStats)) {
+ switch (stat.visualizationType) {
+ case AnalyticsVisualizationType.LineChart:
+ pluginLineCharts.push((
+
+ ));
+ break;
+ case AnalyticsVisualizationType.DoughnutChart:
+ pluginDoughnutCharts.push((
+
+ ));
+ break;
+ case AnalyticsVisualizationType.Count:
+ default:
+ pluginCounts.push((
+
+ ));
+ }
+ }
let systemCards;
if (isLicensed) {
@@ -448,12 +476,14 @@ export default class SystemAnalytics extends React.PureComponent {
{dailyActiveUsers}
{monthlyActiveUsers}
{advancedStats}
- {pluginSiteStats}
+ {pluginCounts}
{advancedGraphs}
+ {pluginDoughnutCharts}
{postTotalGraph}
{botPostTotalGraph}
{activeUserGraph}
+ {pluginLineCharts}
diff --git a/webapp/platform/types/src/admin.ts b/webapp/platform/types/src/admin.ts
index 2e2df211ac..3ec0753592 100644
--- a/webapp/platform/types/src/admin.ts
+++ b/webapp/platform/types/src/admin.ts
@@ -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 = {