From 9ab5a209e9131e838cb1dc5136f848027b78e0f5 Mon Sep 17 00:00:00 2001 From: Marcos Defendi Date: Tue, 24 Dec 2019 12:34:02 -0300 Subject: [PATCH] Added a button to download admin server info --- app/api/server/v1/stats.js | 50 ++--- app/models/server/raw/Statistics.js | 14 ++ app/models/server/raw/index.js | 3 + app/statistics/server/functions/get.js | 165 ----------------- .../server/functions/getLastStatistics.js | 14 ++ .../server/functions/getStatistics.js | 26 +++ app/statistics/server/functions/save.js | 9 - app/statistics/server/index.js | 6 +- app/statistics/server/lib/statistics.js | 173 ++++++++++++++++++ .../server/methods/getStatistics.js | 17 +- app/statistics/server/statisticsNamespace.js | 1 - .../components/admin/info/InformationPage.js | 8 +- .../admin/info/InformationPage.stories.js | 7 + .../components/admin/info/InformationRoute.js | 16 +- client/contexts/ServerContext.js | 6 + client/helpers/download.js | 15 ++ client/providers/ServerProvider.js | 16 ++ packages/rocketchat-i18n/i18n/en.i18n.json | 1 + packages/rocketchat-i18n/i18n/pt-BR.i18n.json | 1 + tests/end-to-end/api/19-statistics.js | 85 +++++++++ 20 files changed, 403 insertions(+), 230 deletions(-) create mode 100644 app/models/server/raw/Statistics.js delete mode 100644 app/statistics/server/functions/get.js create mode 100644 app/statistics/server/functions/getLastStatistics.js create mode 100644 app/statistics/server/functions/getStatistics.js delete mode 100644 app/statistics/server/functions/save.js create mode 100644 app/statistics/server/lib/statistics.js delete mode 100644 app/statistics/server/statisticsNamespace.js create mode 100644 client/helpers/download.js create mode 100644 tests/end-to-end/api/19-statistics.js diff --git a/app/api/server/v1/stats.js b/app/api/server/v1/stats.js index 2f74a2092947e..bd6d1fa2db6b5 100644 --- a/app/api/server/v1/stats.js +++ b/app/api/server/v1/stats.js @@ -1,48 +1,30 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasPermission } from '../../../authorization'; -import { Statistics } from '../../../models'; import { API } from '../api'; +import { getStatistics, getLastStatistics } from '../../../statistics/server'; API.v1.addRoute('statistics', { authRequired: true }, { get() { - let refresh = false; - if (typeof this.queryParams.refresh !== 'undefined' && this.queryParams.refresh === 'true') { - refresh = true; - } - - let stats; - Meteor.runAsUser(this.userId, () => { - stats = Meteor.call('getStatistics', refresh); - }); - - return API.v1.success({ - statistics: stats, - }); + const { refresh } = this.requestParams(); + return API.v1.success(Promise.await(getLastStatistics({ + userId: this.userId, + refresh: refresh && refresh === 'true', + }))); }, }); API.v1.addRoute('statistics.list', { authRequired: true }, { get() { - if (!hasPermission(this.userId, 'view-statistics')) { - return API.v1.unauthorized(); - } - const { offset, count } = this.getPaginationItems(); const { sort, fields, query } = this.parseJsonQuery(); - const statistics = Statistics.find(query, { - sort: sort || { name: 1 }, - skip: offset, - limit: count, - fields, - }).fetch(); - - return API.v1.success({ - statistics, - count: statistics.length, - offset, - total: Statistics.find(query).count(), - }); + return API.v1.success(Promise.await(getStatistics({ + userId: this.userId, + query, + pagination: { + offset, + count, + sort, + fields, + }, + }))); }, }); diff --git a/app/models/server/raw/Statistics.js b/app/models/server/raw/Statistics.js new file mode 100644 index 0000000000000..15b3cf39404a0 --- /dev/null +++ b/app/models/server/raw/Statistics.js @@ -0,0 +1,14 @@ +import { BaseRaw } from './BaseRaw'; + +export class StatisticsRaw extends BaseRaw { + async findLast() { + const options = { + sort: { + createdAt: -1, + }, + limit: 1, + }; + const records = await this.find({}, options).toArray(); + return records && records[0]; + } +} diff --git a/app/models/server/raw/index.js b/app/models/server/raw/index.js index 481fa39a15ab1..042900293a67c 100644 --- a/app/models/server/raw/index.js +++ b/app/models/server/raw/index.js @@ -44,6 +44,8 @@ import CustomUserStatusModel from '../models/CustomUserStatus'; import { CustomUserStatusRaw } from './CustomUserStatus'; import LivechatAgentActivityModel from '../models/LivechatAgentActivity'; import { LivechatAgentActivityRaw } from './LivechatAgentActivity'; +import StatisticsModel from '../models/Statistics'; +import { StatisticsRaw } from './Statistics'; export const Permissions = new PermissionsRaw(PermissionsModel.model.rawCollection()); export const Roles = new RolesRaw(RolesModel.model.rawCollection()); @@ -68,3 +70,4 @@ export const OAuthApps = new OAuthAppsRaw(OAuthAppsModel.model.rawCollection()); export const CustomSounds = new CustomSoundsRaw(CustomSoundsModel.model.rawCollection()); export const CustomUserStatus = new CustomUserStatusRaw(CustomUserStatusModel.model.rawCollection()); export const LivechatAgentActivity = new LivechatAgentActivityRaw(LivechatAgentActivityModel.model.rawCollection()); +export const Statistics = new StatisticsRaw(StatisticsModel.model.rawCollection()); diff --git a/app/statistics/server/functions/get.js b/app/statistics/server/functions/get.js deleted file mode 100644 index 847995bf23804..0000000000000 --- a/app/statistics/server/functions/get.js +++ /dev/null @@ -1,165 +0,0 @@ -import os from 'os'; - -import _ from 'underscore'; -import { Meteor } from 'meteor/meteor'; -import { InstanceStatus } from 'meteor/konecty:multiple-instances-status'; - -import { - Sessions, - Settings, - Users, - Rooms, - Subscriptions, - Uploads, - Messages, - LivechatVisitors, - Integrations, -} from '../../../models/server'; -import { settings } from '../../../settings/server'; -import { Info, getMongoInfo } from '../../../utils/server'; -import { Migrations } from '../../../migrations/server'; -import { statistics } from '../statisticsNamespace'; -import { Apps } from '../../../apps/server'; -import { getStatistics as federationGetStatistics } from '../../../federation/server/functions/dashboard'; - -const wizardFields = [ - 'Organization_Type', - 'Industry', - 'Size', - 'Country', - 'Language', - 'Server_Type', - 'Register_Server', -]; - -statistics.get = function _getStatistics() { - const statistics = {}; - - // Setup Wizard - statistics.wizard = {}; - wizardFields.forEach((field) => { - const record = Settings.findOne(field); - if (record) { - const wizardField = field.replace(/_/g, '').replace(field[0], field[0].toLowerCase()); - statistics.wizard[wizardField] = record.value; - } - }); - - // Version - statistics.uniqueId = settings.get('uniqueID'); - if (Settings.findOne('uniqueID')) { - statistics.installedAt = Settings.findOne('uniqueID').createdAt; - } - - if (Info) { - statistics.version = Info.version; - statistics.tag = Info.tag; - statistics.branch = Info.branch; - } - - // User statistics - statistics.totalUsers = Meteor.users.find().count(); - statistics.activeUsers = Meteor.users.find({ active: true }).count(); - statistics.nonActiveUsers = statistics.totalUsers - statistics.activeUsers; - statistics.onlineUsers = Meteor.users.find({ statusConnection: 'online' }).count(); - statistics.awayUsers = Meteor.users.find({ statusConnection: 'away' }).count(); - statistics.totalConnectedUsers = statistics.onlineUsers + statistics.awayUsers; - statistics.offlineUsers = statistics.totalUsers - statistics.onlineUsers - statistics.awayUsers; - - // Room statistics - statistics.totalRooms = Rooms.find().count(); - statistics.totalChannels = Rooms.findByType('c').count(); - statistics.totalPrivateGroups = Rooms.findByType('p').count(); - statistics.totalDirect = Rooms.findByType('d').count(); - statistics.totalLivechat = Rooms.findByType('l').count(); - statistics.totalDiscussions = Rooms.countDiscussions(); - statistics.totalThreads = Messages.countThreads(); - - // livechat visitors - statistics.totalLivechatVisitors = LivechatVisitors.find().count(); - - // livechat agents - statistics.totalLivechatAgents = Users.findAgents().count(); - - // livechat enabled - statistics.livechatEnabled = settings.get('Livechat_enabled'); - - // Message statistics - statistics.totalMessages = Messages.find().count(); - statistics.totalChannelMessages = _.reduce(Rooms.findByType('c', { fields: { msgs: 1 } }).fetch(), function _countChannelMessages(num, room) { return num + room.msgs; }, 0); - statistics.totalPrivateGroupMessages = _.reduce(Rooms.findByType('p', { fields: { msgs: 1 } }).fetch(), function _countPrivateGroupMessages(num, room) { return num + room.msgs; }, 0); - statistics.totalDirectMessages = _.reduce(Rooms.findByType('d', { fields: { msgs: 1 } }).fetch(), function _countDirectMessages(num, room) { return num + room.msgs; }, 0); - statistics.totalLivechatMessages = _.reduce(Rooms.findByType('l', { fields: { msgs: 1 } }).fetch(), function _countLivechatMessages(num, room) { return num + room.msgs; }, 0); - - // Federation statistics - const federationOverviewData = federationGetStatistics(); - - statistics.federatedServers = federationOverviewData.numberOfServers; - statistics.federatedUsers = federationOverviewData.numberOfFederatedUsers; - - statistics.lastLogin = Users.getLastLogin(); - statistics.lastMessageSentAt = Messages.getLastTimestamp(); - statistics.lastSeenSubscription = Subscriptions.getLastSeen(); - - statistics.os = { - type: os.type(), - platform: os.platform(), - arch: os.arch(), - release: os.release(), - uptime: os.uptime(), - loadavg: os.loadavg(), - totalmem: os.totalmem(), - freemem: os.freemem(), - cpus: os.cpus(), - }; - - statistics.process = { - nodeVersion: process.version, - pid: process.pid, - uptime: process.uptime(), - }; - - statistics.deploy = { - method: process.env.DEPLOY_METHOD || 'tar', - platform: process.env.DEPLOY_PLATFORM || 'selfinstall', - }; - - statistics.uploadsTotal = Uploads.find().count(); - const [result] = Promise.await(Uploads.model.rawCollection().aggregate([{ $group: { _id: 'total', total: { $sum: '$size' } } }]).toArray()); - statistics.uploadsTotalSize = result ? result.total : 0; - - statistics.migration = Migrations._getControl(); - statistics.instanceCount = InstanceStatus.getCollection().find({ _updatedAt: { $gt: new Date(Date.now() - process.uptime() * 1000 - 2000) } }).count(); - - const { oplogEnabled, mongoVersion, mongoStorageEngine } = getMongoInfo(); - statistics.oplogEnabled = oplogEnabled; - statistics.mongoVersion = mongoVersion; - statistics.mongoStorageEngine = mongoStorageEngine; - - statistics.uniqueUsersOfYesterday = Sessions.getUniqueUsersOfYesterday(); - statistics.uniqueUsersOfLastMonth = Sessions.getUniqueUsersOfLastMonth(); - statistics.uniqueDevicesOfYesterday = Sessions.getUniqueDevicesOfYesterday(); - statistics.uniqueDevicesOfLastMonth = Sessions.getUniqueDevicesOfLastMonth(); - statistics.uniqueOSOfYesterday = Sessions.getUniqueOSOfYesterday(); - statistics.uniqueOSOfLastMonth = Sessions.getUniqueOSOfLastMonth(); - - statistics.apps = { - engineVersion: Info.marketplaceApiVersion, - enabled: Apps.isEnabled(), - totalInstalled: Apps.isInitialized() && Apps.getManager().get().length, - totalActive: Apps.isInitialized() && Apps.getManager().get({ enabled: true }).length, - }; - - const integrations = Integrations.find().fetch(); - - statistics.integrations = { - totalIntegrations: integrations.length, - totalIncoming: integrations.filter((integration) => integration.type === 'webhook-incoming').length, - totalIncomingActive: integrations.filter((integration) => integration.enabled === true && integration.type === 'webhook-incoming').length, - totalOutgoing: integrations.filter((integration) => integration.type === 'webhook-outgoing').length, - totalOutgoingActive: integrations.filter((integration) => integration.enabled === true && integration.type === 'webhook-outgoing').length, - totalWithScriptEnabled: integrations.filter((integration) => integration.scriptEnabled === true).length, - }; - - return statistics; -}; diff --git a/app/statistics/server/functions/getLastStatistics.js b/app/statistics/server/functions/getLastStatistics.js new file mode 100644 index 0000000000000..8231bd8b4b10b --- /dev/null +++ b/app/statistics/server/functions/getLastStatistics.js @@ -0,0 +1,14 @@ +import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { statistics } from '../lib/statistics'; +import { Statistics } from '../../../models/server/raw'; + +export async function getLastStatistics({ userId, refresh }) { + if (!await hasPermissionAsync(userId, 'view-statistics')) { + throw new Error('error-not-allowed'); + } + + if (refresh) { + return statistics.save(); + } + return Statistics.findLast(); +} diff --git a/app/statistics/server/functions/getStatistics.js b/app/statistics/server/functions/getStatistics.js new file mode 100644 index 0000000000000..0c801453012a3 --- /dev/null +++ b/app/statistics/server/functions/getStatistics.js @@ -0,0 +1,26 @@ +import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { Statistics } from '../../../models/server/raw'; + +export async function getStatistics({ userId, query = {}, pagination: { offset, count, sort, fields } }) { + if (!await hasPermissionAsync(userId, 'view-statistics')) { + throw new Error('error-not-allowed'); + } + + const cursor = Statistics.find(query, { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + fields, + }); + + const total = await cursor.count(); + + const statistics = await cursor.toArray(); + + return { + statistics, + count: statistics.length, + offset, + total, + }; +} diff --git a/app/statistics/server/functions/save.js b/app/statistics/server/functions/save.js deleted file mode 100644 index 506dd61e3e127..0000000000000 --- a/app/statistics/server/functions/save.js +++ /dev/null @@ -1,9 +0,0 @@ -import { Statistics } from '../../../models'; -import { statistics } from '../statisticsNamespace'; - -statistics.save = function() { - const rcStatistics = statistics.get(); - rcStatistics.createdAt = new Date(); - Statistics.insert(rcStatistics); - return rcStatistics; -}; diff --git a/app/statistics/server/index.js b/app/statistics/server/index.js index 7d5d1f16ea38d..29f45ead81b48 100644 --- a/app/statistics/server/index.js +++ b/app/statistics/server/index.js @@ -1,6 +1,6 @@ -import './functions/get'; -import './functions/save'; import './methods/getStatistics'; import './startup/monitor'; -export { statistics } from './statisticsNamespace'; +export { statistics } from './lib/statistics'; +export { getLastStatistics } from './functions/getLastStatistics'; +export { getStatistics } from './functions/getStatistics'; diff --git a/app/statistics/server/lib/statistics.js b/app/statistics/server/lib/statistics.js new file mode 100644 index 0000000000000..54390f133ed57 --- /dev/null +++ b/app/statistics/server/lib/statistics.js @@ -0,0 +1,173 @@ +import os from 'os'; + +import _ from 'underscore'; +import { Meteor } from 'meteor/meteor'; +import { InstanceStatus } from 'meteor/konecty:multiple-instances-status'; + +import { + Sessions, + Settings, + Users, + Rooms, + Subscriptions, + Uploads, + Messages, + LivechatVisitors, + Integrations, + Statistics, +} from '../../../models/server'; +import { settings } from '../../../settings/server'; +import { Info, getMongoInfo } from '../../../utils/server'; +import { Migrations } from '../../../migrations/server'; +import { Apps } from '../../../apps/server'; +import { getStatistics as federationGetStatistics } from '../../../federation/server/functions/dashboard'; + +const wizardFields = [ + 'Organization_Type', + 'Industry', + 'Size', + 'Country', + 'Language', + 'Server_Type', + 'Register_Server', +]; + +export const statistics = { + get: function _getStatistics() { + const statistics = {}; + + // Setup Wizard + statistics.wizard = {}; + wizardFields.forEach((field) => { + const record = Settings.findOne(field); + if (record) { + const wizardField = field.replace(/_/g, '').replace(field[0], field[0].toLowerCase()); + statistics.wizard[wizardField] = record.value; + } + }); + + // Version + statistics.uniqueId = settings.get('uniqueID'); + if (Settings.findOne('uniqueID')) { + statistics.installedAt = Settings.findOne('uniqueID').createdAt; + } + + if (Info) { + statistics.version = Info.version; + statistics.tag = Info.tag; + statistics.branch = Info.branch; + } + + // User statistics + statistics.totalUsers = Meteor.users.find().count(); + statistics.activeUsers = Meteor.users.find({ active: true }).count(); + statistics.nonActiveUsers = statistics.totalUsers - statistics.activeUsers; + statistics.onlineUsers = Meteor.users.find({ statusConnection: 'online' }).count(); + statistics.awayUsers = Meteor.users.find({ statusConnection: 'away' }).count(); + statistics.totalConnectedUsers = statistics.onlineUsers + statistics.awayUsers; + statistics.offlineUsers = statistics.totalUsers - statistics.onlineUsers - statistics.awayUsers; + + // Room statistics + statistics.totalRooms = Rooms.find().count(); + statistics.totalChannels = Rooms.findByType('c').count(); + statistics.totalPrivateGroups = Rooms.findByType('p').count(); + statistics.totalDirect = Rooms.findByType('d').count(); + statistics.totalLivechat = Rooms.findByType('l').count(); + statistics.totalDiscussions = Rooms.countDiscussions(); + statistics.totalThreads = Messages.countThreads(); + + // livechat visitors + statistics.totalLivechatVisitors = LivechatVisitors.find().count(); + + // livechat agents + statistics.totalLivechatAgents = Users.findAgents().count(); + + // livechat enabled + statistics.livechatEnabled = settings.get('Livechat_enabled'); + + // Message statistics + statistics.totalMessages = Messages.find().count(); + statistics.totalChannelMessages = _.reduce(Rooms.findByType('c', { fields: { msgs: 1 } }).fetch(), function _countChannelMessages(num, room) { return num + room.msgs; }, 0); + statistics.totalPrivateGroupMessages = _.reduce(Rooms.findByType('p', { fields: { msgs: 1 } }).fetch(), function _countPrivateGroupMessages(num, room) { return num + room.msgs; }, 0); + statistics.totalDirectMessages = _.reduce(Rooms.findByType('d', { fields: { msgs: 1 } }).fetch(), function _countDirectMessages(num, room) { return num + room.msgs; }, 0); + statistics.totalLivechatMessages = _.reduce(Rooms.findByType('l', { fields: { msgs: 1 } }).fetch(), function _countLivechatMessages(num, room) { return num + room.msgs; }, 0); + + // Federation statistics + const federationOverviewData = federationGetStatistics(); + + statistics.federatedServers = federationOverviewData.numberOfServers; + statistics.federatedUsers = federationOverviewData.numberOfFederatedUsers; + + statistics.lastLogin = Users.getLastLogin(); + statistics.lastMessageSentAt = Messages.getLastTimestamp(); + statistics.lastSeenSubscription = Subscriptions.getLastSeen(); + + statistics.os = { + type: os.type(), + platform: os.platform(), + arch: os.arch(), + release: os.release(), + uptime: os.uptime(), + loadavg: os.loadavg(), + totalmem: os.totalmem(), + freemem: os.freemem(), + cpus: os.cpus(), + }; + + statistics.process = { + nodeVersion: process.version, + pid: process.pid, + uptime: process.uptime(), + }; + + statistics.deploy = { + method: process.env.DEPLOY_METHOD || 'tar', + platform: process.env.DEPLOY_PLATFORM || 'selfinstall', + }; + + statistics.uploadsTotal = Uploads.find().count(); + const [result] = Promise.await(Uploads.model.rawCollection().aggregate([{ $group: { _id: 'total', total: { $sum: '$size' } } }]).toArray()); + statistics.uploadsTotalSize = result ? result.total : 0; + + statistics.migration = Migrations._getControl(); + statistics.instanceCount = InstanceStatus.getCollection().find({ _updatedAt: { $gt: new Date(Date.now() - process.uptime() * 1000 - 2000) } }).count(); + + const { oplogEnabled, mongoVersion, mongoStorageEngine } = getMongoInfo(); + statistics.oplogEnabled = oplogEnabled; + statistics.mongoVersion = mongoVersion; + statistics.mongoStorageEngine = mongoStorageEngine; + + statistics.uniqueUsersOfYesterday = Sessions.getUniqueUsersOfYesterday(); + statistics.uniqueUsersOfLastMonth = Sessions.getUniqueUsersOfLastMonth(); + statistics.uniqueDevicesOfYesterday = Sessions.getUniqueDevicesOfYesterday(); + statistics.uniqueDevicesOfLastMonth = Sessions.getUniqueDevicesOfLastMonth(); + statistics.uniqueOSOfYesterday = Sessions.getUniqueOSOfYesterday(); + statistics.uniqueOSOfLastMonth = Sessions.getUniqueOSOfLastMonth(); + + statistics.apps = { + engineVersion: Info.marketplaceApiVersion, + enabled: Apps.isEnabled(), + totalInstalled: Apps.isInitialized() && Apps.getManager().get().length, + totalActive: Apps.isInitialized() && Apps.getManager().get({ enabled: true }).length, + }; + + const integrations = Integrations.find().fetch(); + + statistics.integrations = { + totalIntegrations: integrations.length, + totalIncoming: integrations.filter((integration) => integration.type === 'webhook-incoming').length, + totalIncomingActive: integrations.filter((integration) => integration.enabled === true && integration.type === 'webhook-incoming').length, + totalOutgoing: integrations.filter((integration) => integration.type === 'webhook-outgoing').length, + totalOutgoingActive: integrations.filter((integration) => integration.enabled === true && integration.type === 'webhook-outgoing').length, + totalWithScriptEnabled: integrations.filter((integration) => integration.scriptEnabled === true).length, + }; + + return statistics; + }, + save() { + const rcStatistics = statistics.get(); + rcStatistics.createdAt = new Date(); + Statistics.insert(rcStatistics); + return rcStatistics; + }, +}; diff --git a/app/statistics/server/methods/getStatistics.js b/app/statistics/server/methods/getStatistics.js index e5bfbf6305e3f..3bd17de4a229f 100644 --- a/app/statistics/server/methods/getStatistics.js +++ b/app/statistics/server/methods/getStatistics.js @@ -1,22 +1,15 @@ import { Meteor } from 'meteor/meteor'; -import { hasPermission } from '../../../authorization'; -import { Statistics } from '../../../models'; -import { statistics } from '../statisticsNamespace'; +import { getLastStatistics } from '../functions/getLastStatistics'; Meteor.methods({ getStatistics(refresh) { if (!Meteor.userId()) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getStatistics' }); } - - if (hasPermission(Meteor.userId(), 'view-statistics') !== true) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'getStatistics' }); - } - - if (refresh) { - return statistics.save(); - } - return Statistics.findLast(); + return Promise.await(getLastStatistics({ + userId: Meteor.userId(), + refresh, + })); }, }); diff --git a/app/statistics/server/statisticsNamespace.js b/app/statistics/server/statisticsNamespace.js deleted file mode 100644 index bfd4f992e5899..0000000000000 --- a/app/statistics/server/statisticsNamespace.js +++ /dev/null @@ -1 +0,0 @@ -export const statistics = {}; diff --git a/client/components/admin/info/InformationPage.js b/client/components/admin/info/InformationPage.js index d26f68cdaefc5..5831d6654734f 100644 --- a/client/components/admin/info/InformationPage.js +++ b/client/components/admin/info/InformationPage.js @@ -18,6 +18,7 @@ export function InformationPage({ statistics, instances, onClickRefreshButton, + onClickDownloadInfo, }) { const t = useTranslation(); @@ -30,11 +31,14 @@ export function InformationPage({ return
{canViewStatistics - && + && + - } + }
diff --git a/client/components/admin/info/InformationPage.stories.js b/client/components/admin/info/InformationPage.stories.js index c52f91f50595a..8064962cf98d0 100644 --- a/client/components/admin/info/InformationPage.stories.js +++ b/client/components/admin/info/InformationPage.stories.js @@ -115,12 +115,14 @@ export const _default = () => statistics={object('statistics', statistics)} instances={object('instances', exampleInstance)} onClickRefreshButton={action('clickRefreshButton')} + onClickDownloadInfo={action('clickDownloadInfo')} />; export const withoutCanViewStatisticsPermission = () => ; export const loading = () => @@ -129,6 +131,7 @@ export const loading = () => isLoading info={info} onClickRefreshButton={action('clickRefreshButton')} + onClickDownloadInfo={action('clickDownloadInfo')} />; export const withStatistics = () => @@ -137,6 +140,7 @@ export const withStatistics = () => info={info} statistics={statistics} onClickRefreshButton={action('clickRefreshButton')} + onClickDownloadInfo={action('clickDownloadInfo')} />; export const withOneInstance = () => @@ -146,6 +150,7 @@ export const withOneInstance = () => statistics={statistics} instances={[exampleInstance]} onClickRefreshButton={action('clickRefreshButton')} + onClickDownloadInfo={action('clickDownloadInfo')} />; export const withTwoInstances = () => @@ -155,6 +160,7 @@ export const withTwoInstances = () => statistics={statistics} instances={[exampleInstance, exampleInstance]} onClickRefreshButton={action('clickRefreshButton')} + onClickDownloadInfo={action('clickDownloadInfo')} />; export const withTwoInstancesAndDisabledOplog = () => @@ -164,4 +170,5 @@ export const withTwoInstancesAndDisabledOplog = () => statistics={{ ...statistics, instanceCount: 2, oplogEnabled: false }} instances={[exampleInstance, exampleInstance]} onClickRefreshButton={action('clickRefreshButton')} + onClickDownloadInfo={action('clickDownloadInfo')} />; diff --git a/client/components/admin/info/InformationRoute.js b/client/components/admin/info/InformationRoute.js index 513610a86c268..10c51e94ab0ab 100644 --- a/client/components/admin/info/InformationRoute.js +++ b/client/components/admin/info/InformationRoute.js @@ -1,9 +1,10 @@ import React, { useState, useEffect } from 'react'; import { usePermission } from '../../../contexts/AuthorizationContext'; -import { useMethod, useServerInformation } from '../../../contexts/ServerContext'; +import { useMethod, useServerInformation, useEndpoint } from '../../../contexts/ServerContext'; import { useAdminSideNav } from '../../../hooks/useAdminSideNav'; import { InformationPage } from './InformationPage'; +import { downloadJsonAsAFile } from '../../../helpers/download'; export function InformationRoute() { useAdminSideNav(); @@ -14,7 +15,7 @@ export function InformationRoute() { const [statistics, setStatistics] = useState({}); const [instances, setInstances] = useState([]); const [fetchStatistics, setFetchStatistics] = useState(() => () => ({})); - const getStatistics = useMethod('getStatistics'); + const getStatistics = useEndpoint('GET', 'statistics'); const getInstances = useMethod('instances/get'); useEffect(() => { @@ -31,14 +32,13 @@ export function InformationRoute() { try { const [statistics, instances] = await Promise.all([ - getStatistics(), + getStatistics({ refresh: true }), getInstances(), ]); if (didCancel) { return; } - setStatistics(statistics); setInstances(instances); } finally { @@ -65,6 +65,13 @@ export function InformationRoute() { fetchStatistics(); }; + const handleClickDownloadInfo = () => { + if (isLoading) { + return; + } + downloadJsonAsAFile(statistics, 'statistics'); + }; + return ; } diff --git a/client/contexts/ServerContext.js b/client/contexts/ServerContext.js index 26840573b6cd8..bc5a816243f51 100644 --- a/client/contexts/ServerContext.js +++ b/client/contexts/ServerContext.js @@ -4,6 +4,7 @@ export const ServerContext = createContext({ info: {}, absoluteUrl: (path) => path, callMethod: async () => {}, + callEndpoint: async () => {}, }); export const useServerInformation = () => useContext(ServerContext).info; @@ -14,3 +15,8 @@ export const useMethod = (methodName) => { const { callMethod } = useContext(ServerContext); return useCallback((...args) => callMethod(methodName, ...args), [callMethod, methodName]); }; + +export const useEndpoint = (httpMethod, endpoint) => { + const { callEndpoint } = useContext(ServerContext); + return useCallback((...args) => callEndpoint(httpMethod, endpoint, ...args), [callEndpoint, httpMethod, endpoint]); +}; diff --git a/client/helpers/download.js b/client/helpers/download.js new file mode 100644 index 0000000000000..089f19cecf171 --- /dev/null +++ b/client/helpers/download.js @@ -0,0 +1,15 @@ +export const downloadJsonAsAFile = (jsonData, name = 'jsonfile') => { + const filename = `${ name }.json`; + const contentType = 'application/json;charset=utf-8;'; + if (window.navigator && window.navigator.msSaveOrOpenBlob) { + const blob = new Blob([decodeURIComponent(encodeURI(JSON.stringify(jsonData)))], { type: contentType }); + return navigator.msSaveOrOpenBlob(blob, filename); + } + const aElement = document.createElement('a'); + aElement.download = filename; + aElement.href = `data:${ contentType },${ encodeURIComponent(JSON.stringify(jsonData)) }`; + aElement.target = '_blank'; + document.body.appendChild(aElement); + aElement.click(); + document.body.removeChild(aElement); +}; diff --git a/client/providers/ServerProvider.js b/client/providers/ServerProvider.js index ed43c5bd2e01e..397a6651bdbe9 100644 --- a/client/providers/ServerProvider.js +++ b/client/providers/ServerProvider.js @@ -3,6 +3,7 @@ import { Meteor } from 'meteor/meteor'; import { Info as info } from '../../app/utils'; import { ServerContext } from '../contexts/ServerContext'; +import { APIClient } from '../../app/utils/client'; const absoluteUrl = (path) => Meteor.absoluteUrl(path); @@ -17,10 +18,25 @@ const callMethod = (methodName, ...args) => new Promise((resolve, reject) => { }); }); +const callEndpoint = (httpMethod, endpoint, ...args) => { + const allowedHttpMethods = ['get', 'post', 'delete']; + if (!httpMethod || !allowedHttpMethods.includes(httpMethod.toLowerCase())) { + throw new Error('Invalid http method provided to "useEndpoint"'); + } + if (!endpoint) { + throw new Error('Invalid endpoint provided to "useEndpoint"'); + } + if (endpoint.startsWith('/')) { + endpoint = endpoint.replace('/', ''); + } + return APIClient.v1[httpMethod.toLowerCase()](endpoint, ...args); +}; + const contextValue = { info, absoluteUrl, callMethod, + callEndpoint, }; export function ServerProvider({ children }) { diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index a00e511e181ad..6dc2a171e9ac8 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -1159,6 +1159,7 @@ "Domains": "Domains", "Domains_allowed_to_embed_the_livechat_widget": "Comma-separated list of domains allowed to embed the livechat widget. Leave blank to allow all domains.", "Downloading_file_from_external_URL": "Downloading file from external URL", + "Download_Info": "Download Info", "Download_My_Data": "Download My Data (HTML)", "Download_Snippet": "Download", "Drop_to_upload_file": "Drop to upload file", diff --git a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index f46f8f0530dbd..ffc193511cbdc 100644 --- a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -1092,6 +1092,7 @@ "Domains": "Domínios", "Domains_allowed_to_embed_the_livechat_widget": "A lista de domínios separados por vírgulas permitiu incorporar o widget do Livechat. Deixe em branco para permitir todos os domínios.", "Downloading_file_from_external_URL": "Baixar arquivo de URL externa", + "Download_Info": "Baixar informações", "Download_My_Data": "Baixar meus dados (HTML)", "Download_Snippet": "Baixar", "Drop_to_upload_file": "Largue para enviar arquivos", diff --git a/tests/end-to-end/api/19-statistics.js b/tests/end-to-end/api/19-statistics.js new file mode 100644 index 0000000000000..09dbfe3846d4f --- /dev/null +++ b/tests/end-to-end/api/19-statistics.js @@ -0,0 +1,85 @@ +import { + getCredentials, + api, + request, + credentials, +} from '../../data/api-data.js'; +import { updatePermission } from '../../data/permissions.helper.js'; + +describe('[Statistics]', function() { + this.retries(0); + + before((done) => getCredentials(done)); + + describe('[/statistics]', () => { + let lastUptime; + it('should return an error when the user does not have the necessary permission', (done) => { + updatePermission('view-statistics', []).then(() => { + request.get(api('statistics')) + .set(credentials) + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal('error-not-allowed'); + }) + .end(done); + }); + }); + it('should return an object with the statistics', (done) => { + updatePermission('view-statistics', ['admin']).then(() => { + request.get(api('statistics')) + .set(credentials) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('process'); + expect(res.body.process).to.have.property('uptime'); + lastUptime = res.body.process.uptime; + }) + .end(done); + }); + }); + it('should update the statistics when is provided the "refresh:true" query parameter', (done) => { + request.get(api('statistics?refresh=true')) + .set(credentials) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('process'); + expect(res.body.process).to.have.property('uptime'); + expect(lastUptime).to.not.be.equal(res.body.process.uptime); + }) + .end(done); + }); + }); + + describe('[/statistics.list]', () => { + it('should return an error when the user does not have the necessary permission', (done) => { + updatePermission('view-statistics', []).then(() => { + request.get(api('statistics.list')) + .set(credentials) + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal('error-not-allowed'); + }) + .end(done); + }); + }); + it('should return an array with the statistics', (done) => { + updatePermission('view-statistics', ['admin']).then(() => { + request.get(api('statistics.list')) + .set(credentials) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('statistics').and.to.be.an('array'); + expect(res.body).to.have.property('offset'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('count'); + }) + .end(done); + }); + }); + }); +});