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 = {