diff --git a/.storybook/config.js b/.storybook/config.js index c6380996898..420f7df4a6d 100644 --- a/.storybook/config.js +++ b/.storybook/config.js @@ -20,4 +20,5 @@ addDecorator(withKnobs); configure([ require.context('../app', true, /\.stories\.js$/), require.context('../client', true, /\.stories\.js$/), + require.context('../ee/app', true, /\.stories\.js$/), ], module); diff --git a/app/models/server/models/Analytics.js b/app/models/server/models/Analytics.js new file mode 100644 index 00000000000..c521fda8923 --- /dev/null +++ b/app/models/server/models/Analytics.js @@ -0,0 +1,11 @@ +import { Base } from './_Base'; + +export class Analytics extends Base { + constructor() { + super('analytics'); + this.tryEnsureIndex({ date: 1 }); + this.tryEnsureIndex({ 'room._id': 1, date: 1 }, { unique: true }); + } +} + +export default new Analytics(); diff --git a/app/models/server/raw/Analytics.js b/app/models/server/raw/Analytics.js new file mode 100644 index 00000000000..7544d897fbd --- /dev/null +++ b/app/models/server/raw/Analytics.js @@ -0,0 +1,145 @@ +import { Random } from 'meteor/random'; + +import { BaseRaw } from './BaseRaw'; +import Analytics from '../models/Analytics'; + +export class AnalyticsRaw extends BaseRaw { + saveMessageSent({ room, date }) { + return this.update({ date, 'room._id': room._id, type: 'messages' }, { + $set: { + room: { _id: room._id, name: room.fname || room.name, t: room.t, usernames: room.usernames || [] }, + }, + $setOnInsert: { + _id: Random.id(), + date, + type: 'messages', + }, + $inc: { messages: 1 }, + }, { upsert: true }); + } + + saveUserData({ date }) { + return this.update({ date, type: 'users' }, { + $setOnInsert: { + _id: Random.id(), + date, + type: 'users', + }, + $inc: { users: 1 }, + }, { upsert: true }); + } + + saveMessageDeleted({ room, date }) { + return this.update({ date, 'room._id': room._id }, { + $inc: { messages: -1 }, + }); + } + + getMessagesSentTotalByDate({ start, end, options = {} }) { + const params = [ + { + $match: { + type: 'messages', + date: { $gte: start, $lte: end }, + }, + }, + { + $group: { + _id: '$date', + messages: { $sum: '$messages' }, + }, + }, + ]; + if (options.sort) { + params.push({ $sort: options.sort }); + } + if (options.count) { + params.push({ $limit: options.count }); + } + return this.col.aggregate(params).toArray(); + } + + getMessagesOrigin({ start, end }) { + const params = [ + { + $match: { + type: 'messages', + date: { $gte: start, $lte: end }, + }, + }, + { + $group: { + _id: { t: '$room.t' }, + messages: { $sum: '$messages' }, + }, + }, + { + $project: { + _id: 0, + t: '$_id.t', + messages: 1, + }, + }, + ]; + return this.col.aggregate(params).toArray(); + } + + getMostPopularChannelsByMessagesSentQuantity({ start, end, options = {} }) { + const params = [ + { + $match: { + type: 'messages', + date: { $gte: start, $lte: end }, + }, + }, + { + $group: { + _id: { t: '$room.t', name: '$room.name', usernames: '$room.usernames' }, + messages: { $sum: '$messages' }, + }, + }, + { + $project: { + _id: 0, + t: '$_id.t', + name: '$_id.name', + usernames: '$_id.usernames', + messages: 1, + }, + }, + ]; + if (options.sort) { + params.push({ $sort: options.sort }); + } + if (options.count) { + params.push({ $limit: options.count }); + } + return this.col.aggregate(params).toArray(); + } + + getTotalOfRegisteredUsersByDate({ start, end, options = {} }) { + const params = [ + { + $match: { + type: 'users', + date: { $gte: start, $lte: end }, + }, + }, + { + $group: { + _id: '$date', + users: { $sum: '$users' }, + }, + }, + ]; + if (options.sort) { + params.push({ $sort: options.sort }); + } + if (options.count) { + params.push({ $limit: options.count }); + } + return this.col.aggregate(params).toArray(); + } +} + +export default new AnalyticsRaw(Analytics.model.rawCollection()); diff --git a/app/models/server/raw/Rooms.js b/app/models/server/raw/Rooms.js index dc9ab5445ad..fe88c305a09 100644 --- a/app/models/server/raw/Rooms.js +++ b/app/models/server/raw/Rooms.js @@ -89,4 +89,107 @@ export class RoomsRaw extends BaseRaw { return this.find(query, options); } + + findChannelsWithNumberOfMessagesBetweenDate({ start, end, startOfLastWeek, endOfLastWeek, onlyCount = false, options = {} }) { + const lookup = { + $lookup: { + from: 'rocketchat_analytics', + localField: '_id', + foreignField: 'room._id', + as: 'messages', + }, + }; + const messagesProject = { + $project: { + room: '$$ROOT', + messages: { + $filter: { + input: '$messages', + as: 'message', + cond: { + $and: [ + { $gte: ['$$message.date', start] }, + { $lte: ['$$message.date', end] }, + ], + }, + }, + }, + lastWeekMessages: { + $filter: { + input: '$messages', + as: 'message', + cond: { + $and: [ + { $gte: ['$$message.date', startOfLastWeek] }, + { $lte: ['$$message.date', endOfLastWeek] }, + ], + }, + }, + }, + }, + }; + const messagesUnwind = { + $unwind: { + path: '$messages', + preserveNullAndEmptyArrays: true, + }, + }; + const messagesGroup = { + $group: { + _id: { + _id: '$room._id', + }, + room: { $first: '$room' }, + messages: { $sum: '$messages.messages' }, + lastWeekMessages: { $first: '$lastWeekMessages' }, + }, + }; + const lastWeekMessagesUnwind = { + $unwind: { + path: '$lastWeekMessages', + preserveNullAndEmptyArrays: true, + }, + }; + const lastWeekMessagesGroup = { + $group: { + _id: { + _id: '$room._id', + }, + room: { $first: '$room' }, + messages: { $first: '$messages' }, + lastWeekMessages: { $sum: '$lastWeekMessages.messages' }, + }, + }; + const presentationProject = { + $project: { + _id: 0, + room: { + _id: '$_id._id', + name: { $ifNull: ['$room.name', '$room.fname'] }, + ts: '$room.ts', + t: '$room.t', + _updatedAt: '$room._updatedAt', + usernames: '$room.usernames', + }, + messages: '$messages', + lastWeekMessages: '$lastWeekMessages', + diffFromLastWeek: { $subtract: ['$messages', '$lastWeekMessages'] }, + }, + }; + const firstParams = [lookup, messagesProject, messagesUnwind, messagesGroup, lastWeekMessagesUnwind, lastWeekMessagesGroup, presentationProject]; + const sort = { $sort: options.sort || { messages: -1 } }; + const params = [...firstParams, sort]; + if (onlyCount) { + params.push({ $count: 'total' }); + return this.col.aggregate(params); + } + if (options.offset) { + params.push({ $skip: options.offset }); + } + if (options.count) { + params.push({ $limit: options.count }); + } + + return this.col.aggregate(params).toArray(); + } } diff --git a/app/models/server/raw/Sessions.js b/app/models/server/raw/Sessions.js new file mode 100644 index 00000000000..4b259c10d46 --- /dev/null +++ b/app/models/server/raw/Sessions.js @@ -0,0 +1,251 @@ +import { BaseRaw } from './BaseRaw'; +import Sessions from '../models/Sessions'; + +const matchBasedOnDate = (start, end) => { + if (start.year === end.year && start.month === end.month) { + return { + year: start.year, + month: start.month, + day: { $gte: start.day, $lte: end.day }, + }; + } + + if (start.year === end.year) { + return { + year: start.year, + $and: [{ + $or: [{ + month: { $gt: start.month }, + }, { + month: start.month, + day: { $gte: start.day }, + }], + }, { + $or: [{ + month: { $lt: end.month }, + }, { + month: end.month, + day: { $lte: end.day }, + }], + }], + }; + } + + return { + $and: [{ + $or: [{ + year: { $gt: start.year }, + }, { + year: start.year, + month: { $gt: start.month }, + }, { + year: start.year, + month: start.month, + day: { $gte: start.day }, + }], + }, { + $or: [{ + year: { $lt: end.year }, + }, { + year: end.year, + month: { $lt: end.month }, + }, { + year: end.year, + month: end.month, + day: { $lte: end.day }, + }], + }], + }; +}; + +const getGroupSessionsByHour = (_id) => { + const isOpenSession = { $not: ['$session.closedAt'] }; + const isAfterLoginAt = { $gte: ['$range', { $hour: '$session.loginAt' }] }; + const isBeforeClosedAt = { $lte: ['$range', { $hour: '$session.closedAt' }] }; + return { + $group: { + _id, + users: { + $sum: { + $cond: [ + { + $or: [ + { $and: [isOpenSession, isAfterLoginAt] }, + { $and: [isAfterLoginAt, isBeforeClosedAt] }, + ], + }, + 1, + 0, + ], + }, + }, + }, + }; +}; + +const getSortByFullDate = () => ({ + year: -1, + month: -1, + day: -1, +}); + +const getProjectionByFullDate = () => ({ + day: '$_id.day', + month: '$_id.month', + year: '$_id.year', +}); + +export class SessionsRaw extends BaseRaw { + getActiveUsersBetweenDates({ start, end }) { + return this.col.aggregate([ + { + $match: { + ...matchBasedOnDate(start, end), + type: 'user_daily', + }, + }, + { + $group: { + _id: '$userId', + }, + }, + ]).toArray(); + } + + getActiveUsersOfPeriodByDayBetweenDates({ start, end }) { + return this.col.aggregate([ + { + $match: { + ...matchBasedOnDate(start, end), + type: 'user_daily', + }, + }, + { + $group: { + _id: { + day: '$day', + month: '$month', + year: '$year', + }, + users: { $sum: 1 }, + }, + }, + { + $project: { + _id: 0, + ...getProjectionByFullDate(), + users: 1, + }, + }, + { + $sort: { + ...getSortByFullDate(), + }, + }, + ]).toArray(); + } + + getBusiestTimeWithinHoursPeriod({ start, end }) { + const match = { + $match: { + type: 'computed-session', + loginAt: { $gte: start, $lte: end }, + }, + }; + const rangeProject = { + $project: { + range: { + $range: [0, 24], + }, + session: '$$ROOT', + }, + }; + const unwind = { + $unwind: '$range', + }; + const group = getGroupSessionsByHour('$range'); + const presentationProject = { + $project: { + _id: 0, + hour: '$_id', + users: 1, + }, + }; + const sort = { + $sort: { + hour: -1, + }, + }; + return this.col.aggregate([match, rangeProject, unwind, group, presentationProject, sort]).toArray(); + } + + getTotalOfSessionsByDayBetweenDates({ start, end }) { + return this.col.aggregate([ + { + $match: { + ...matchBasedOnDate(start, end), + type: 'user_daily', + }, + }, + { + $group: { + _id: { year: '$year', month: '$month', day: '$day' }, + users: { $sum: '$sessions' }, + }, + }, + { + $project: { + _id: 0, + ...getProjectionByFullDate(), + users: 1, + }, + }, + { + $sort: { + ...getSortByFullDate(), + }, + }, + ]).toArray(); + } + + getTotalOfSessionByHourAndDayBetweenDates({ start, end }) { + const match = { + $match: { + type: 'computed-session', + loginAt: { $gte: start, $lte: end }, + }, + }; + const rangeProject = { + $project: { + range: { + $range: [ + { $hour: '$loginAt' }, + { $sum: [{ $ifNull: [{ $hour: '$closedAt' }, 23] }, 1] }], + }, + session: '$$ROOT', + }, + + }; + const unwind = { + $unwind: '$range', + }; + const group = getGroupSessionsByHour({ range: '$range', day: '$session.day', month: '$session.month', year: '$session.year' }); + const presentationProject = { + $project: { + _id: 0, + hour: '$_id.range', + ...getProjectionByFullDate(), + users: 1, + }, + }; + const sort = { + $sort: { + ...getSortByFullDate(), + hour: -1, + }, + }; + return this.col.aggregate([match, rangeProject, unwind, group, presentationProject, sort]).toArray(); + } +} + +export default new SessionsRaw(Sessions.model.rawCollection()); diff --git a/client/main.js b/client/main.js index 605b6cd6617..a1013d38d72 100644 --- a/client/main.js +++ b/client/main.js @@ -6,6 +6,7 @@ import '../imports/startup/client'; import '../lib/RegExp'; +import '../ee/client'; import './lib/toastr'; import './templateHelpers'; import './methods/deleteMessage'; @@ -27,5 +28,3 @@ import './startup/startup'; import './startup/unread'; import './startup/userSetUtcOffset'; import './startup/usersObserve'; - -import '../ee/client'; diff --git a/ee/app/engagement-dashboard/client/components/ChannelsTab/TableSection.js b/ee/app/engagement-dashboard/client/components/ChannelsTab/TableSection.js new file mode 100644 index 00000000000..673b2079bb6 --- /dev/null +++ b/ee/app/engagement-dashboard/client/components/ChannelsTab/TableSection.js @@ -0,0 +1,146 @@ +import { Box, Icon, Margins, Pagination, Select, Skeleton, Table, Tile } from '@rocket.chat/fuselage'; +import moment from 'moment'; +import React, { useMemo, useState } from 'react'; + +import { useTranslation } from '../../../../../../client/contexts/TranslationContext'; +import { Growth } from '../data/Growth'; +import { Section } from '../Section'; +import { useEndpointData } from '../../hooks/useEndpointData'; + +export function TableSection() { + const t = useTranslation(); + + const periodOptions = useMemo(() => [ + ['last 7 days', t('Last_7_days')], + ['last 30 days', t('Last_30_days')], + ['last 90 days', t('Last_90_days')], + ], [t]); + + const [periodId, setPeriodId] = useState('last 7 days'); + + const period = useMemo(() => { + switch (periodId) { + case 'last 7 days': + return { + start: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(7, 'days'), + end: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(1), + }; + + case 'last 30 days': + return { + start: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(30, 'days'), + end: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(1), + }; + + case 'last 90 days': + return { + start: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(90, 'days'), + end: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(1), + }; + } + }, [periodId]); + + const handlePeriodChange = (periodId) => setPeriodId(periodId); + + const [current, setCurrent] = useState(0); + const [itemsPerPage, setItemsPerPage] = useState(25); + + const params = useMemo(() => ({ + start: period.start.toISOString(), + end: period.end.toISOString(), + offset: current, + count: itemsPerPage, + }), [period, current, itemsPerPage]); + + const data = useEndpointData('GET', 'engagement-dashboard/channels/list', params); + + const channels = useMemo(() => { + if (!data) { + return; + } + + return data.channels.map(({ + room: { t, name, usernames, ts, _updatedAt }, + messages, + diffFromLastWeek, + }) => ({ + t, + name: name || usernames.join(' × '), + createdAt: ts, + updatedAt: _updatedAt, + messagesCount: messages, + messagesVariation: diffFromLastWeek, + })); + }, [data]); + + return
}> + + {channels && !channels.length && + {t('No_data_found')} + } + {(!channels || channels.length) + && + + + {'#'} + {t('Channel')} + {t('Created')} + {t('Last_active')} + {t('Messages_sent')} + + + + {channels && channels.map(({ t, name, createdAt, updatedAt, messagesCount, messagesVariation }, i) => + + {i + 1}. + + + {(t === 'd' && ) + || (t === 'c' && ) + || (t === 'p' && )} + + {name} + + + {moment(createdAt).format('L')} + + + {moment(updatedAt).format('L')} + + + {messagesCount} {messagesVariation} + + )} + {!channels && Array.from({ length: 5 }, (_, i) => + + + + + + + + + + + + + + + + + )} + +
} + t('Items_per_page:')} + showingResultsLabel={({ count, current, itemsPerPage }) => + t('Showing results %s - %s of %s', current + 1, Math.min(current + itemsPerPage, count), count)} + count={(data && data.total) || 0} + onSetItemsPerPage={setItemsPerPage} + onSetCurrent={setCurrent} + /> +
+
; +} diff --git a/ee/app/engagement-dashboard/client/components/ChannelsTab/index.js b/ee/app/engagement-dashboard/client/components/ChannelsTab/index.js new file mode 100644 index 00000000000..c59914097a6 --- /dev/null +++ b/ee/app/engagement-dashboard/client/components/ChannelsTab/index.js @@ -0,0 +1,9 @@ +import React from 'react'; + +import { TableSection } from './TableSection'; + +export function ChannelsTab() { + return <> + + ; +} diff --git a/ee/app/engagement-dashboard/client/components/ChannelsTab/index.stories.js b/ee/app/engagement-dashboard/client/components/ChannelsTab/index.stories.js new file mode 100644 index 00000000000..c9878245fdf --- /dev/null +++ b/ee/app/engagement-dashboard/client/components/ChannelsTab/index.stories.js @@ -0,0 +1,14 @@ +import { Margins } from '@rocket.chat/fuselage'; +import React from 'react'; + +import { ChannelsTab } from '.'; + +export default { + title: 'admin/engagement/ChannelsTab', + component: ChannelsTab, + decorators: [ + (fn) => , + ], +}; + +export const _default = () => ; diff --git a/ee/app/engagement-dashboard/client/components/EngagementDashboardPage.js b/ee/app/engagement-dashboard/client/components/EngagementDashboardPage.js new file mode 100644 index 00000000000..f9843adf08c --- /dev/null +++ b/ee/app/engagement-dashboard/client/components/EngagementDashboardPage.js @@ -0,0 +1,35 @@ +import { Box, Margins, Tabs } from '@rocket.chat/fuselage'; +import React from 'react'; + +import { useTranslation } from '../../../../../client/contexts/TranslationContext'; +import { Page } from '../../../../../client/components/basic/Page'; +import { UsersTab } from './UsersTab'; +import { MessagesTab } from './MessagesTab'; +import { ChannelsTab } from './ChannelsTab'; + +export function EngagementDashboardPage({ + tab = 'users', + onSelectTab, +}) { + const t = useTranslation(); + + const handleTabClick = onSelectTab ? (tab) => () => onSelectTab(tab) : () => undefined; + + return + + + {t('Users')} + {t('Messages')} + {t('Channels')} + + + + + {(tab === 'users' && ) + || (tab === 'messages' && ) + || (tab === 'channels' && )} + + + + ; +} diff --git a/ee/app/engagement-dashboard/client/components/EngagementDashboardPage.stories.js b/ee/app/engagement-dashboard/client/components/EngagementDashboardPage.stories.js new file mode 100644 index 00000000000..0ac38d641bf --- /dev/null +++ b/ee/app/engagement-dashboard/client/components/EngagementDashboardPage.stories.js @@ -0,0 +1,11 @@ +import React from 'react'; + +import { EngagementDashboardPage } from './EngagementDashboardPage'; + +export default { + title: 'admin/engagement/EngagementDashboardPage', + component: EngagementDashboardPage, + decorators: [(fn) =>
], +}; + +export const _default = () => ; diff --git a/ee/app/engagement-dashboard/client/components/EngagementDashboardRoute.js b/ee/app/engagement-dashboard/client/components/EngagementDashboardRoute.js new file mode 100644 index 00000000000..4d232810240 --- /dev/null +++ b/ee/app/engagement-dashboard/client/components/EngagementDashboardRoute.js @@ -0,0 +1,24 @@ +import React, { useEffect } from 'react'; + +import { useRoute, useRouteParameter } from '../../../../../client/contexts/RouterContext'; +import { useAdminSideNav } from '../../../../../client/hooks/useAdminSideNav'; +import { EngagementDashboardPage } from './EngagementDashboardPage'; + +export function EngagementDashboardRoute() { + useAdminSideNav(); + + const goToEngagementDashboard = useRoute('engagement-dashboard'); + + const tab = useRouteParameter('tab'); + + useEffect(() => { + if (!tab) { + goToEngagementDashboard.replacingState({ tab: 'users' }); + } + }, [tab]); + + return goToEngagementDashboard({ tab })} + />; +} diff --git a/ee/app/engagement-dashboard/client/components/MessagesTab/MessagesPerChannelSection.js b/ee/app/engagement-dashboard/client/components/MessagesTab/MessagesPerChannelSection.js new file mode 100644 index 00000000000..bfa2527d893 --- /dev/null +++ b/ee/app/engagement-dashboard/client/components/MessagesTab/MessagesPerChannelSection.js @@ -0,0 +1,217 @@ +import { ResponsivePie } from '@nivo/pie'; +import { Box, Flex, Icon, Margins, Select, Skeleton, Table, Tile } from '@rocket.chat/fuselage'; +import moment from 'moment'; +import React, { useMemo, useState } from 'react'; + +import { useTranslation } from '../../../../../../client/contexts/TranslationContext'; +import { LegendSymbol } from '../data/LegendSymbol'; +import { useEndpointData } from '../../hooks/useEndpointData'; +import { Section } from '../Section'; + +export function MessagesPerChannelSection() { + const t = useTranslation(); + + const periodOptions = useMemo(() => [ + ['last 7 days', t('Last_7_days')], + ['last 30 days', t('Last_30_days')], + ['last 90 days', t('Last_90_days')], + ], [t]); + + const [periodId, setPeriodId] = useState('last 7 days'); + + const period = useMemo(() => { + switch (periodId) { + case 'last 7 days': + return { + start: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(7, 'days'), + end: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(1), + }; + + case 'last 30 days': + return { + start: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(30, 'days'), + end: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(1), + }; + + case 'last 90 days': + return { + start: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(90, 'days'), + end: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(1), + }; + } + }, [periodId]); + + const handlePeriodChange = (periodId) => setPeriodId(periodId); + + const params = useMemo(() => ({ + start: period.start.toISOString(), + end: period.end.toISOString(), + }), [period]); + + const pieData = useEndpointData('GET', 'engagement-dashboard/messages/origin', params); + const tableData = useEndpointData('GET', 'engagement-dashboard/messages/top-five-popular-channels', params); + + const [pie, table] = useMemo(() => { + if (!pieData || !tableData) { + return []; + } + + const pie = pieData.origins.reduce((obj, { messages, t }) => ({ ...obj, [t]: messages }), {}); + + const table = tableData.channels.reduce((entries, { t, messages, name, usernames }, i) => + [...entries, { i, t, name: name || usernames.join(' × '), messages }], []); + + return [pie, table]; + }, [period, pieData, tableData]); + + return
} + > + + + + + + + + {pie + ? + + + + + + {t('Value_messages', { value })} + } + /> + + + + + + + + + + + {t('Private_Chats')} + + + + {t('Private_Channels')} + + + + {t('Public_Channels')} + + + + + + + : } + + + + + + + {table ? {t('Most_popular_channels_top_5')} : } + + {table && !table.length && + {t('Not_enough_data')} + } + {(!table || !!table.length) && + + + {'#'} + {t('Channel')} + {t('Number_of_messages')} + + + + {table && table.map(({ i, t, name, messages }) => + {i + 1}. + + + {(t === 'd' && ) + || (t === 'c' && ) + || (t === 'p' && )} + + {name} + + {messages} + )} + {!table && Array.from({ length: 5 }, (_, i) => + + + + + + + + + + )} + +
} +
+
+
+
+
+
+
; +} diff --git a/ee/app/engagement-dashboard/client/components/MessagesTab/MessagesSentSection.js b/ee/app/engagement-dashboard/client/components/MessagesTab/MessagesSentSection.js new file mode 100644 index 00000000000..aefd34d7488 --- /dev/null +++ b/ee/app/engagement-dashboard/client/components/MessagesTab/MessagesSentSection.js @@ -0,0 +1,169 @@ +import { ResponsiveBar } from '@nivo/bar'; +import { Box, Flex, Select, Skeleton } from '@rocket.chat/fuselage'; +import moment from 'moment'; +import React, { useMemo, useState } from 'react'; + +import { useTranslation } from '../../../../../../client/contexts/TranslationContext'; +import { CounterSet } from '../data/CounterSet'; +import { Section } from '../Section'; +import { useEndpointData } from '../../hooks/useEndpointData'; + +export function MessagesSentSection() { + const t = useTranslation(); + + const periodOptions = useMemo(() => [ + ['last 7 days', t('Last_7_days')], + ['last 30 days', t('Last_30_days')], + ['last 90 days', t('Last_90_days')], + ], [t]); + + const [periodId, setPeriodId] = useState('last 7 days'); + + const period = useMemo(() => { + switch (periodId) { + case 'last 7 days': + return { + start: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(7, 'days'), + end: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(1), + }; + + case 'last 30 days': + return { + start: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(30, 'days'), + end: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(1), + }; + + case 'last 90 days': + return { + start: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(90, 'days'), + end: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(1), + }; + } + }, [periodId]); + + const handlePeriodChange = (periodId) => setPeriodId(periodId); + + const params = useMemo(() => ({ + start: period.start.toISOString(), + end: period.end.toISOString(), + }), [period]); + + const data = useEndpointData('GET', 'engagement-dashboard/messages/messages-sent', params); + + const [ + countFromPeriod, + variatonFromPeriod, + countFromYesterday, + variationFromYesterday, + values, + ] = useMemo(() => { + if (!data) { + return []; + } + + const values = Array.from({ length: moment(period.end).diff(period.start, 'days') + 1 }, (_, i) => ({ + date: moment(period.start).add(i, 'days').toISOString(), + newUsers: 0, + })); + for (const { day, users } of data.days) { + const i = moment(day).diff(period.start, 'days'); + values[i].newUsers += users; + } + + return [ + data.period.count, + data.period.variation, + data.yesterday.count, + data.yesterday.variation, + values, + ]; + }, [data, period]); + + return
} + > + , + variation: data ? variatonFromPeriod : 0, + description: periodOptions.find(([id]) => id === periodId)[1], + }, + { + count: data ? countFromYesterday : , + variation: data ? variationFromYesterday : 0, + description: t('Yesterday'), + }, + ]} + /> + + {data + ? + + + + moment(date).format('dddd'), + }) || null } + axisLeft={null} + animate={true} + motionStiffness={90} + motionDamping={15} + theme={{ + // TODO: Get it from theme + axis: { + ticks: { + text: { + fill: '#9EA2A8', + fontFamily: 'Inter, -apple-system, system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Meiryo UI", Arial, sans-serif', + fontSize: '10px', + fontStyle: 'normal', + fontWeight: '600', + letterSpacing: '0.2px', + lineHeight: '12px', + }, + }, + }, + tooltip: { + container: { + backgroundColor: '#1F2329', + boxShadow: '0px 0px 12px rgba(47, 52, 61, 0.12), 0px 0px 2px rgba(47, 52, 61, 0.08)', + borderRadius: 2, + }, + }, + }} + tooltip={({ value }) => + {t('Value_users', { value })} + } + /> + + + + + : } + +
; +} diff --git a/ee/app/engagement-dashboard/client/components/MessagesTab/index.js b/ee/app/engagement-dashboard/client/components/MessagesTab/index.js new file mode 100644 index 00000000000..04daa9970db --- /dev/null +++ b/ee/app/engagement-dashboard/client/components/MessagesTab/index.js @@ -0,0 +1,13 @@ +import { Divider } from '@rocket.chat/fuselage'; +import React from 'react'; + +import { MessagesSentSection } from './MessagesSentSection'; +import { MessagesPerChannelSection } from './MessagesPerChannelSection'; + +export function MessagesTab() { + return <> + + + + ; +} diff --git a/ee/app/engagement-dashboard/client/components/MessagesTab/index.stories.js b/ee/app/engagement-dashboard/client/components/MessagesTab/index.stories.js new file mode 100644 index 00000000000..196bf07ea4b --- /dev/null +++ b/ee/app/engagement-dashboard/client/components/MessagesTab/index.stories.js @@ -0,0 +1,14 @@ +import { Margins } from '@rocket.chat/fuselage'; +import React from 'react'; + +import { MessagesTab } from '.'; + +export default { + title: 'admin/engagement/MessagesTab', + component: MessagesTab, + decorators: [ + (fn) => , + ], +}; + +export const _default = () => ; diff --git a/ee/app/engagement-dashboard/client/components/Section.js b/ee/app/engagement-dashboard/client/components/Section.js new file mode 100644 index 00000000000..94c9e22cec0 --- /dev/null +++ b/ee/app/engagement-dashboard/client/components/Section.js @@ -0,0 +1,24 @@ +import { Box, Flex, InputBox, Margins } from '@rocket.chat/fuselage'; +import React from 'react'; + +export function Section({ + children, + title, + filter = , +}) { + return + + + + + {title} + + {filter && + {filter} + } + + + {children} + + ; +} diff --git a/ee/app/engagement-dashboard/client/components/UsersTab/ActiveUsersSection.js b/ee/app/engagement-dashboard/client/components/UsersTab/ActiveUsersSection.js new file mode 100644 index 00000000000..d90e5c64947 --- /dev/null +++ b/ee/app/engagement-dashboard/client/components/UsersTab/ActiveUsersSection.js @@ -0,0 +1,223 @@ +import { ResponsiveLine } from '@nivo/line'; +import { Box, Flex, Skeleton, Tile } from '@rocket.chat/fuselage'; +import moment from 'moment'; +import React, { useMemo } from 'react'; + +import { useTranslation } from '../../../../../../client/contexts/TranslationContext'; +import { CounterSet } from '../data/CounterSet'; +import { LegendSymbol } from '../data/LegendSymbol'; +import { useEndpointData } from '../../hooks/useEndpointData'; +import { Section } from '../Section'; + +export function ActiveUsersSection() { + const t = useTranslation(); + + const period = useMemo(() => ({ + start: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(30, 'days'), + end: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(1), + }), []); + + const params = useMemo(() => ({ + start: period.start.clone().subtract(30, 'days').toISOString(), + end: period.end.toISOString(), + }), [period]); + + const data = useEndpointData('GET', 'engagement-dashboard/users/active-users', params); + + const [ + countDailyActiveUsers, + diffDailyActiveUsers, + countWeeklyActiveUsers, + diffWeeklyActiveUsers, + countMonthlyActiveUsers, + diffMonthlyActiveUsers, + dauValues, + wauValues, + mauValues, + ] = useMemo(() => { + if (!data) { + return []; + } + + const createPoint = (i) => ({ + x: moment(period.start).add(i, 'days').toDate(), + y: 0, + }); + + const createPoints = () => Array.from({ length: moment(period.end).diff(period.start, 'days') + 1 }, (_, i) => createPoint(i)); + + const distributeValueOverPoints = (value, i, T, array, prev) => { + for (let j = 0; j < T; ++j) { + const k = i + j; + + if (k >= array.length) { + continue; + } + + if (k >= 0) { + array[k].y += value; + } + + if (k === -1) { + prev.y += value; + } + } + }; + + const dauValues = createPoints(); + const prevDauValue = createPoint(-1); + const wauValues = createPoints(); + const prevWauValue = createPoint(-1); + const mauValues = createPoints(); + const prevMauValue = createPoint(-1); + + for (const { users, day, month, year } of data.month) { + const i = moment.utc([year, month - 1, day, 0, 0, 0, 0]).diff(period.start, 'days'); + distributeValueOverPoints(users, i, 1, dauValues, prevDauValue); + distributeValueOverPoints(users, i, 7, wauValues, prevWauValue); + distributeValueOverPoints(users, i, 30, mauValues, prevMauValue); + } + + return [ + dauValues[dauValues.length - 1].y, + dauValues[dauValues.length - 1].y - prevDauValue.y, + wauValues[wauValues.length - 1].y, + wauValues[wauValues.length - 1].y - prevWauValue.y, + mauValues[mauValues.length - 1].y, + mauValues[mauValues.length - 1].y - prevMauValue.y, + dauValues, + wauValues, + mauValues, + ]; + }, [period, data]); + + return
+ , + variation: data ? diffDailyActiveUsers : 0, + description: <> {t('Daily_Active_Users')}, + }, + { + count: data ? countWeeklyActiveUsers : , + variation: data ? diffWeeklyActiveUsers : 0, + description: <> {t('Weekly_Active_Users')}, + }, + { + count: data ? countMonthlyActiveUsers : , + variation: data ? diffMonthlyActiveUsers : 0, + description: <> {t('Monthly_Active_Users')}, + }, + ]} + /> + + {data + ? + + + + moment(date).format(dauValues.length === 7 ? 'dddd' : 'L'), + }} + animate={true} + motionStiffness={90} + motionDamping={15} + theme={{ + // TODO: Get it from theme + axis: { + ticks: { + text: { + fill: '#9EA2A8', + fontFamily: 'Inter, -apple-system, system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Meiryo UI", Arial, sans-serif', + fontSize: '10px', + fontStyle: 'normal', + fontWeight: '600', + letterSpacing: '0.2px', + lineHeight: '12px', + }, + }, + }, + tooltip: { + container: { + backgroundColor: '#1F2329', + boxShadow: '0px 0px 12px rgba(47, 52, 61, 0.12), 0px 0px 2px rgba(47, 52, 61, 0.08)', + borderRadius: 2, + }, + }, + }} + enableSlices='x' + sliceTooltip={({ slice: { points } }) => + {points.map(({ serieId, data: { y: activeUsers } }) => + + {(serieId === 'dau' && t('DAU_value', { value: activeUsers })) + || (serieId === 'wau' && t('WAU_value', { value: activeUsers })) + || (serieId === 'mau' && t('MAU_value', { value: activeUsers }))} + )} + } + /> + + + + + : } + +
; +} diff --git a/ee/app/engagement-dashboard/client/components/UsersTab/BusiestChatTimesSection.js b/ee/app/engagement-dashboard/client/components/UsersTab/BusiestChatTimesSection.js new file mode 100644 index 00000000000..86091bd7409 --- /dev/null +++ b/ee/app/engagement-dashboard/client/components/UsersTab/BusiestChatTimesSection.js @@ -0,0 +1,250 @@ +import { ResponsiveBar } from '@nivo/bar'; +import { Box, Button, Chevron, Flex, Margins, Select, Skeleton } from '@rocket.chat/fuselage'; +import moment from 'moment'; +import React, { useMemo, useState } from 'react'; + +import { useTranslation } from '../../../../../../client/contexts/TranslationContext'; +import { Section } from '../Section'; +import { useEndpointData } from '../../hooks/useEndpointData'; + +function ContentForHours({ displacement, onPreviousDateClick, onNextDateClick }) { + const t = useTranslation(); + + const currentDate = useMemo(() => + moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }) + .subtract(1).subtract(displacement, 'days'), [displacement]); + const params = useMemo(() => ({ start: currentDate.toISOString() }), [currentDate]); + const data = useEndpointData('GET', 'engagement-dashboard/users/chat-busier/hourly-data', params); + const values = useMemo(() => { + if (!data) { + return []; + } + + const divider = 2; + const values = Array.from({ length: 24 / divider }, (_, i) => ({ + hour: divider * i, + users: 0, + })); + for (const { hour, users } of data.hours) { + const i = Math.floor(hour / divider); + values[i] = values[i] || { hour: divider * i, users: 0 }; + values[i].users += users; + } + + return values; + }, [data]); + + return <> + + + + + + + {currentDate.format(displacement < 7 ? 'dddd' : 'L')} + + + + + + + + {data + ? + + + + moment().set({ hour, minute: 0, second: 0 }).format('LT'), + }} + axisLeft={null} + animate={true} + motionStiffness={90} + motionDamping={15} + theme={{ + // TODO: Get it from theme + axis: { + ticks: { + text: { + fill: '#9EA2A8', + fontFamily: 'Inter, -apple-system, system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Meiryo UI", Arial, sans-serif', + fontSize: '10px', + fontStyle: 'normal', + fontWeight: '600', + letterSpacing: '0.2px', + lineHeight: '12px', + }, + }, + }, + tooltip: { + container: { + backgroundColor: '#1F2329', + boxShadow: '0px 0px 12px rgba(47, 52, 61, 0.12), 0px 0px 2px rgba(47, 52, 61, 0.08)', + borderRadius: 2, + }, + }, + }} + tooltip={({ value }) => + {t('Value_users', { value })} + } + /> + + + + + : } + + ; +} + +function ContentForDays({ displacement, onPreviousDateClick, onNextDateClick }) { + const currentDate = useMemo(() => moment.utc().subtract(displacement, 'weeks'), [displacement]); + const formattedCurrentDate = useMemo(() => { + const startOfWeekDate = currentDate.clone().subtract(6, 'days'); + return `${ startOfWeekDate.format('L') } - ${ currentDate.format('L') }`; + }, [currentDate]); + const params = useMemo(() => ({ start: currentDate.toISOString() }), [currentDate]); + const data = useEndpointData('GET', 'engagement-dashboard/users/chat-busier/weekly-data', params); + const values = useMemo(() => (data ? data.month.map(({ users, day, month, year }) => ({ + users, + day: moment.utc([year, month - 1, day, 0, 0, 0]).valueOf(), + })).sort(({ day: a }, { day: b }) => a - b) : []), [data]); + + return <> + + + + + + + {formattedCurrentDate} + + + + + + + + {data + ? + + + + moment(timestamp).format('L'), + }} + axisLeft={null} + animate={true} + motionStiffness={90} + motionDamping={15} + theme={{ + // TODO: Get it from theme + axis: { + ticks: { + text: { + fill: '#9EA2A8', + fontFamily: 'Inter, -apple-system, system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Meiryo UI", Arial, sans-serif', + fontSize: '10px', + fontStyle: 'normal', + fontWeight: '600', + letterSpacing: '0.2px', + lineHeight: '12px', + }, + }, + }, + }} + /> + + + + + : } + + ; +} + +export function BusiestChatTimesSection() { + const t = useTranslation(); + + const [timeUnit, setTimeUnit] = useState('hours'); + const timeUnitOptions = useMemo(() => [ + ['hours', t('Hours')], + ['days', t('Days')], + ], [t]); + + const handleTimeUnitChange = (timeUnit) => { + setTimeUnit(timeUnit); + }; + + const [displacement, setDisplacement] = useState(0); + + const handlePreviousDateClick = () => setDisplacement((displacement) => displacement + 1); + const handleNextDateClick = () => setDisplacement((displacement) => displacement - 1); + + const Content = (timeUnit === 'hours' && ContentForHours) || (timeUnit === 'days' && ContentForDays); + + return
} + > + +
; +} diff --git a/ee/app/engagement-dashboard/client/components/UsersTab/NewUsersSection.js b/ee/app/engagement-dashboard/client/components/UsersTab/NewUsersSection.js new file mode 100644 index 00000000000..3ffb763f91c --- /dev/null +++ b/ee/app/engagement-dashboard/client/components/UsersTab/NewUsersSection.js @@ -0,0 +1,169 @@ +import { ResponsiveBar } from '@nivo/bar'; +import { Box, Flex, Select, Skeleton } from '@rocket.chat/fuselage'; +import moment from 'moment'; +import React, { useMemo, useState } from 'react'; + +import { useTranslation } from '../../../../../../client/contexts/TranslationContext'; +import { useEndpointData } from '../../hooks/useEndpointData'; +import { CounterSet } from '../data/CounterSet'; +import { Section } from '../Section'; + +export function NewUsersSection() { + const t = useTranslation(); + + const periodOptions = useMemo(() => [ + ['last 7 days', t('Last_7_days')], + ['last 30 days', t('Last_30_days')], + ['last 90 days', t('Last_90_days')], + ], [t]); + + const [periodId, setPeriodId] = useState('last 7 days'); + + const period = useMemo(() => { + switch (periodId) { + case 'last 7 days': + return { + start: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(7, 'days'), + end: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(1), + }; + + case 'last 30 days': + return { + start: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(30, 'days'), + end: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(1), + }; + + case 'last 90 days': + return { + start: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(90, 'days'), + end: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(1), + }; + } + }, [periodId]); + + const handlePeriodChange = (periodId) => setPeriodId(periodId); + + const params = useMemo(() => ({ + start: period.start.toISOString(), + end: period.end.toISOString(), + }), [period]); + + const data = useEndpointData('GET', 'engagement-dashboard/users/new-users', params); + + const [ + countFromPeriod, + variatonFromPeriod, + countFromYesterday, + variationFromYesterday, + values, + ] = useMemo(() => { + if (!data) { + return []; + } + + const values = Array.from({ length: moment(period.end).diff(period.start, 'days') + 1 }, (_, i) => ({ + date: moment(period.start).add(i, 'days').toISOString(), + newUsers: 0, + })); + for (const { day, users } of data.days) { + const i = moment(day).diff(period.start, 'days'); + values[i].newUsers += users; + } + + return [ + data.period.count, + data.period.variation, + data.yesterday.count, + data.yesterday.variation, + values, + ]; + }, [data, period]); + + return
} + > + , + variation: data ? variatonFromPeriod : 0, + description: periodOptions.find(([id]) => id === periodId)[1], + }, + { + count: data ? countFromYesterday : , + variation: data ? variationFromYesterday : 0, + description: t('Yesterday'), + }, + ]} + /> + + {data + ? + + + + moment(date).format('dddd'), + }) || null } + axisLeft={null} + animate={true} + motionStiffness={90} + motionDamping={15} + theme={{ + // TODO: Get it from theme + axis: { + ticks: { + text: { + fill: '#9EA2A8', + fontFamily: 'Inter, -apple-system, system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Meiryo UI", Arial, sans-serif', + fontSize: '10px', + fontStyle: 'normal', + fontWeight: '600', + letterSpacing: '0.2px', + lineHeight: '12px', + }, + }, + }, + tooltip: { + container: { + backgroundColor: '#1F2329', + boxShadow: '0px 0px 12px rgba(47, 52, 61, 0.12), 0px 0px 2px rgba(47, 52, 61, 0.08)', + borderRadius: 2, + }, + }, + }} + tooltip={({ value }) => + {t('Value_users', { value })} + } + /> + + + + + : } + +
; +} diff --git a/ee/app/engagement-dashboard/client/components/UsersTab/UsersByTimeOfTheDaySection.js b/ee/app/engagement-dashboard/client/components/UsersTab/UsersByTimeOfTheDaySection.js new file mode 100644 index 00000000000..08b17733f26 --- /dev/null +++ b/ee/app/engagement-dashboard/client/components/UsersTab/UsersByTimeOfTheDaySection.js @@ -0,0 +1,166 @@ +import { ResponsiveHeatMap } from '@nivo/heatmap'; +import { Box, Flex, Select, Skeleton } from '@rocket.chat/fuselage'; +import moment from 'moment'; +import React, { useMemo, useState } from 'react'; + +import { useTranslation } from '../../../../../../client/contexts/TranslationContext'; +import { Section } from '../Section'; +import { useEndpointData } from '../../hooks/useEndpointData'; + +export function UsersByTimeOfTheDaySection() { + const t = useTranslation(); + + const periodOptions = useMemo(() => [ + ['last 7 days', t('Last_7_days')], + ['last 30 days', t('Last_30_days')], + ['last 90 days', t('Last_90_days')], + ], [t]); + + const [periodId, setPeriodId] = useState('last 7 days'); + + const period = useMemo(() => { + switch (periodId) { + case 'last 7 days': + return { + start: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(7, 'days'), + end: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(1), + }; + + case 'last 30 days': + return { + start: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(30, 'days'), + end: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(1), + }; + + case 'last 90 days': + return { + start: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(90, 'days'), + end: moment().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }).subtract(1), + }; + } + }, [periodId]); + + const handlePeriodChange = (periodId) => setPeriodId(periodId); + + const params = useMemo(() => ({ + start: period.start.toISOString(), + end: period.end.toISOString(), + }), [period]); + + const data = useEndpointData('GET', 'engagement-dashboard/users/users-by-time-of-the-day-in-a-week', params); + + const [ + dates, + values, + ] = useMemo(() => { + if (!data) { + return []; + } + + const dates = Array.from({ length: moment(period.end).diff(period.start, 'days') + 1 }, + (_, i) => moment(period.start).add(i, 'days')); + + const values = Array.from({ length: 24 }, (_, hour) => ({ + hour: String(hour), + ...dates.map((date) => ({ [date.toISOString()]: 0 })) + .reduce((obj, elem) => ({ ...obj, ...elem }), {}), + })); + + for (const { users, hour, day, month, year } of data.week) { + const date = moment([year, month - 1, day, 0, 0, 0, 0]).toISOString(); + values[hour][date] += users; + } + + return [ + dates.map((date) => date.toISOString()), + values, + ]; + }, [data]); + + return
} + > + + {data + ? + + + + (dates.length === 7 ? moment(isoString).format('dddd') : ''), + }} + axisLeft={{ + // TODO: Get it from theme + tickSize: 0, + tickPadding: 4, + tickRotation: 0, + format: (hour) => moment().set({ hour: parseInt(hour, 10), minute: 0, second: 0 }).format('LT'), + }} + hoverTarget='cell' + animate={dates.length <= 7} + motionStiffness={90} + motionDamping={15} + theme={{ + // TODO: Get it from theme + axis: { + ticks: { + text: { + fill: '#9EA2A8', + fontFamily: 'Inter, -apple-system, system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Meiryo UI", Arial, sans-serif', + fontSize: 10, + fontStyle: 'normal', + fontWeight: '600', + letterSpacing: '0.2px', + lineHeight: '12px', + }, + }, + }, + tooltip: { + container: { + backgroundColor: '#1F2329', + boxShadow: '0px 0px 12px rgba(47, 52, 61, 0.12), 0px 0px 2px rgba(47, 52, 61, 0.08)', + borderRadius: 2, + }, + }, + }} + tooltip={({ value }) => + {t('Value_users', { value })} + } + /> + + + + + : } + +
; +} diff --git a/ee/app/engagement-dashboard/client/components/UsersTab/index.js b/ee/app/engagement-dashboard/client/components/UsersTab/index.js new file mode 100644 index 00000000000..4bc28760fc4 --- /dev/null +++ b/ee/app/engagement-dashboard/client/components/UsersTab/index.js @@ -0,0 +1,32 @@ +import { Box, Divider, Flex, Margins } from '@rocket.chat/fuselage'; +import React from 'react'; + +import { NewUsersSection } from './NewUsersSection'; +import { ActiveUsersSection } from './ActiveUsersSection'; +import { UsersByTimeOfTheDaySection } from './UsersByTimeOfTheDaySection'; +import { BusiestChatTimesSection } from './BusiestChatTimesSection'; + +export function UsersTab() { + return <> + + + + + + + + + + + + + + + + + + + + + ; +} diff --git a/ee/app/engagement-dashboard/client/components/UsersTab/index.stories.js b/ee/app/engagement-dashboard/client/components/UsersTab/index.stories.js new file mode 100644 index 00000000000..43684456328 --- /dev/null +++ b/ee/app/engagement-dashboard/client/components/UsersTab/index.stories.js @@ -0,0 +1,14 @@ +import { Margins } from '@rocket.chat/fuselage'; +import React from 'react'; + +import { UsersTab } from '.'; + +export default { + title: 'admin/engagement/UsersTab', + component: UsersTab, + decorators: [ + (fn) => , + ], +}; + +export const _default = () => ; diff --git a/ee/app/engagement-dashboard/client/components/data/Counter.js b/ee/app/engagement-dashboard/client/components/data/Counter.js new file mode 100644 index 00000000000..9093b5e863a --- /dev/null +++ b/ee/app/engagement-dashboard/client/components/data/Counter.js @@ -0,0 +1,24 @@ +import { Box, Flex, Margins } from '@rocket.chat/fuselage'; +import React from 'react'; + +import { Growth } from './Growth'; + +export function Counter({ count, variation = 0, description }) { + return <> + + + + {count} + + {variation} + + + + + + {description} + + + + ; +} diff --git a/ee/app/engagement-dashboard/client/components/data/Counter.stories.js b/ee/app/engagement-dashboard/client/components/data/Counter.stories.js new file mode 100644 index 00000000000..b61686bf630 --- /dev/null +++ b/ee/app/engagement-dashboard/client/components/data/Counter.stories.js @@ -0,0 +1,16 @@ +import React from 'react'; + +import { Counter } from './Counter'; + +export default { + title: 'admin/engagement/data/Counter', + component: Counter, +}; + +export const _default = () => ; + +export const withPositiveVariation = () => ; + +export const withNegativeVariation = () => ; + +export const withDescription = () => ; diff --git a/ee/app/engagement-dashboard/client/components/data/CounterSet.js b/ee/app/engagement-dashboard/client/components/data/CounterSet.js new file mode 100644 index 00000000000..26cbc5863f5 --- /dev/null +++ b/ee/app/engagement-dashboard/client/components/data/CounterSet.js @@ -0,0 +1,16 @@ +import { Grid } from '@rocket.chat/fuselage'; +import React from 'react'; + +import { Counter } from './Counter'; + +export function CounterSet({ counters = [] }) { + return + {counters.map(({ count, variation, description }, i) => + + )} + ; +} diff --git a/ee/app/engagement-dashboard/client/components/data/CounterSet.stories.js b/ee/app/engagement-dashboard/client/components/data/CounterSet.stories.js new file mode 100644 index 00000000000..ba8e26d67f5 --- /dev/null +++ b/ee/app/engagement-dashboard/client/components/data/CounterSet.stories.js @@ -0,0 +1,16 @@ +import React from 'react'; + +import { CounterSet } from './CounterSet'; + +export default { + title: 'admin/engagement/data/CounterSet', + component: CounterSet, +}; + +export const _default = () => ; diff --git a/ee/app/engagement-dashboard/client/components/data/Growth.js b/ee/app/engagement-dashboard/client/components/data/Growth.js new file mode 100644 index 00000000000..46548abed93 --- /dev/null +++ b/ee/app/engagement-dashboard/client/components/data/Growth.js @@ -0,0 +1,16 @@ +import { Box } from '@rocket.chat/fuselage'; +import React from 'react'; + +import { NegativeGrowthSymbol } from './NegativeGrowthSymbol'; +import { PositiveGrowthSymbol } from './PositiveGrowthSymbol'; + +export function Growth({ children, ...props }) { + if (children === 0) { + return null; + } + + return + {children < 0 ? : } + {String(Math.abs(children))} + ; +} diff --git a/ee/app/engagement-dashboard/client/components/data/Growth.stories.js b/ee/app/engagement-dashboard/client/components/data/Growth.stories.js new file mode 100644 index 00000000000..d90790f004d --- /dev/null +++ b/ee/app/engagement-dashboard/client/components/data/Growth.stories.js @@ -0,0 +1,23 @@ +import { Box, Margins } from '@rocket.chat/fuselage'; +import React from 'react'; + +import { Growth } from './Growth'; + +export default { + title: 'admin/engagement/data/Growth', + component: Growth, + decorators: [(fn) => ], +}; + +export const positive = () => {3}; + +export const zero = () => {0}; + +export const negative = () => {-3}; + +export const withTextStyle = () => + ['h1', 's1', 'c1', 'micro'] + .map((textStyle) => + {3} + {-3} + ); diff --git a/ee/app/engagement-dashboard/client/components/data/Histogram.js b/ee/app/engagement-dashboard/client/components/data/Histogram.js new file mode 100644 index 00000000000..30653cf05a1 --- /dev/null +++ b/ee/app/engagement-dashboard/client/components/data/Histogram.js @@ -0,0 +1,85 @@ +import { ResponsiveBar } from '@nivo/bar'; +import { Box, Flex } from '@rocket.chat/fuselage'; +import React from 'react'; + +import { polychromaticColors } from './colors'; + +export function Histogram() { + return + + + `${ users }%`, + }} + axisLeft={{ + tickSize: 0, + tickPadding: 5, + tickRotation: 0, + format: (utc) => `UTF ${ utc }`, + }} + animate={true} + motionStiffness={90} + motionDamping={15} + theme={{ + font: 'inherit', + fontStyle: 'normal', + fontWeight: 600, + fontSize: 10, + lineHeight: 12, + letterSpacing: 0.2, + color: '#9EA2A8', + grid: { + line: { + stroke: '#CBCED1', + strokeWidth: 1, + strokeDasharray: '4 1.5', + }, + }, + }} + /> + + + ; +} diff --git a/ee/app/engagement-dashboard/client/components/data/Histogram.stories.js b/ee/app/engagement-dashboard/client/components/data/Histogram.stories.js new file mode 100644 index 00000000000..48e3f015ff5 --- /dev/null +++ b/ee/app/engagement-dashboard/client/components/data/Histogram.stories.js @@ -0,0 +1,16 @@ +import { Box, Flex, Margins } from '@rocket.chat/fuselage'; +import React from 'react'; + +import { Histogram } from './Histogram'; + +export default { + title: 'admin/engagement/data/Histogram', + component: Histogram, + decorators: [(fn) => + + + + ], +}; + +export const _default = () => ; diff --git a/ee/app/engagement-dashboard/client/components/data/LegendSymbol.js b/ee/app/engagement-dashboard/client/components/data/LegendSymbol.js new file mode 100644 index 00000000000..947631e9799 --- /dev/null +++ b/ee/app/engagement-dashboard/client/components/data/LegendSymbol.js @@ -0,0 +1,19 @@ +import { Box, Margins } from '@rocket.chat/fuselage'; +import React from 'react'; + +export function LegendSymbol({ color = 'currentColor' }) { + return + ; +} diff --git a/ee/app/engagement-dashboard/client/components/data/LegendSymbol.stories.js b/ee/app/engagement-dashboard/client/components/data/LegendSymbol.stories.js new file mode 100644 index 00000000000..457456da286 --- /dev/null +++ b/ee/app/engagement-dashboard/client/components/data/LegendSymbol.stories.js @@ -0,0 +1,25 @@ +import { Box, Margins } from '@rocket.chat/fuselage'; +import React from 'react'; + +import { LegendSymbol } from './LegendSymbol'; +import { monochromaticColors, polychromaticColors } from './colors'; + +export default { + title: 'admin/engagement/data/LegendSymbol', + component: LegendSymbol, + decorators: [(fn) => ], +}; + +export const _default = () => + + Legend text +; + +export const withColor = () => <> + {monochromaticColors.map((color) => + {color} + )} + {polychromaticColors.map((color) => + {color} + )} +; diff --git a/ee/app/engagement-dashboard/client/components/data/NegativeGrowthSymbol.js b/ee/app/engagement-dashboard/client/components/data/NegativeGrowthSymbol.js new file mode 100644 index 00000000000..bb7a0944a68 --- /dev/null +++ b/ee/app/engagement-dashboard/client/components/data/NegativeGrowthSymbol.js @@ -0,0 +1,26 @@ +import React from 'react'; + +const style = { width: '1.5em', height: '1.5em', verticalAlign: '-0.5em' }; + +export const NegativeGrowthSymbol = (props) => + + + ; diff --git a/ee/app/engagement-dashboard/client/components/data/NegativeGrowthSymbol.stories.js b/ee/app/engagement-dashboard/client/components/data/NegativeGrowthSymbol.stories.js new file mode 100644 index 00000000000..6daa6bb7641 --- /dev/null +++ b/ee/app/engagement-dashboard/client/components/data/NegativeGrowthSymbol.stories.js @@ -0,0 +1,16 @@ +import { Box, Margins } from '@rocket.chat/fuselage'; +import React from 'react'; + +import { NegativeGrowthSymbol } from './NegativeGrowthSymbol'; + +export default { + title: 'admin/engagement/data/NegativeGrowthSymbol', + component: NegativeGrowthSymbol, + decorators: [(fn) => ], +}; + +export const _default = () => ; + +export const withColor = () => + +; diff --git a/ee/app/engagement-dashboard/client/components/data/PositiveGrowthSymbol.js b/ee/app/engagement-dashboard/client/components/data/PositiveGrowthSymbol.js new file mode 100644 index 00000000000..15d485dadaf --- /dev/null +++ b/ee/app/engagement-dashboard/client/components/data/PositiveGrowthSymbol.js @@ -0,0 +1,26 @@ +import React from 'react'; + +const style = { width: '1.5em', height: '1.5em', verticalAlign: '-0.5em' }; + +export const PositiveGrowthSymbol = (props) => + + + ; diff --git a/ee/app/engagement-dashboard/client/components/data/PositiveGrowthSymbol.stories.js b/ee/app/engagement-dashboard/client/components/data/PositiveGrowthSymbol.stories.js new file mode 100644 index 00000000000..d5edb0c0434 --- /dev/null +++ b/ee/app/engagement-dashboard/client/components/data/PositiveGrowthSymbol.stories.js @@ -0,0 +1,16 @@ +import { Box, Margins } from '@rocket.chat/fuselage'; +import React from 'react'; + +import { PositiveGrowthSymbol } from './PositiveGrowthSymbol'; + +export default { + title: 'admin/engagement/data/PositiveGrowthSymbol', + component: PositiveGrowthSymbol, + decorators: [(fn) => ], +}; + +export const _default = () => ; + +export const withColor = () => + +; diff --git a/ee/app/engagement-dashboard/client/components/data/colors.js b/ee/app/engagement-dashboard/client/components/data/colors.js new file mode 100644 index 00000000000..9373ef2f3c4 --- /dev/null +++ b/ee/app/engagement-dashboard/client/components/data/colors.js @@ -0,0 +1,2 @@ +export const monochromaticColors = ['#E8F2FF', '#D1EBFE', '#A4D3FE', '#76B7FC', '#549DF9', '#1D74F5', '#10529E']; +export const polychromaticColors = ['#FFD031', '#2DE0A5', '#1D74F5']; diff --git a/ee/app/engagement-dashboard/client/hooks/useEndpointData.js b/ee/app/engagement-dashboard/client/hooks/useEndpointData.js new file mode 100644 index 00000000000..d35a956ab5f --- /dev/null +++ b/ee/app/engagement-dashboard/client/hooks/useEndpointData.js @@ -0,0 +1,52 @@ +import { useEffect, useState } from 'react'; + +import { useEndpoint } from '../../../../../client/contexts/ServerContext'; +import { useToastMessageDispatch } from '../../../../../client/contexts/ToastMessagesContext'; + +export const useEndpointData = (httpMethod, endpoint, params = {}) => { + const [data, setData] = useState(null); + + const getData = useEndpoint(httpMethod, endpoint); + const dispatchToastMessage = useToastMessageDispatch(); + + useEffect(() => { + let mounted = true; + + const fetchData = async () => { + try { + const timer = setTimeout(() => { + if (!mounted) { + return; + } + + setData(null); + }, 3000); + + const data = await getData(params); + + clearTimeout(timer); + + if (!data.success) { + throw new Error(data.status); + } + + if (!mounted) { + return; + } + + setData(data); + } catch (error) { + console.error(error); + dispatchToastMessage({ type: 'error', message: error }); + } + }; + + fetchData(); + + return () => { + mounted = false; + }; + }, [getData, params]); + + return data; +}; diff --git a/ee/app/engagement-dashboard/client/index.js b/ee/app/engagement-dashboard/client/index.js new file mode 100644 index 00000000000..86c73e4462c --- /dev/null +++ b/ee/app/engagement-dashboard/client/index.js @@ -0,0 +1 @@ +import './routes'; diff --git a/ee/app/engagement-dashboard/client/routes.js b/ee/app/engagement-dashboard/client/routes.js new file mode 100644 index 00000000000..d4d966f5fd9 --- /dev/null +++ b/ee/app/engagement-dashboard/client/routes.js @@ -0,0 +1,71 @@ +import { Blaze } from 'meteor/blaze'; +import { HTML } from 'meteor/htmljs'; +import { FlowRouter } from 'meteor/kadira:flow-router'; +import { BlazeLayout } from 'meteor/kadira:blaze-layout'; +import { Template } from 'meteor/templating'; + +import { hasAllPermission } from '../../../../app/authorization'; +import { AdminBox } from '../../../../app/ui-utils'; +import { hasLicense } from '../../license/client'; + +Template.EngagementDashboardRoute = new Blaze.Template('EngagementDashboardRoute', + () => HTML.DIV.call(null, { style: 'overflow: hidden; flex: 1 1 auto; height: 1%;' })); + +Template.EngagementDashboardRoute.onRendered(async function() { + const [ + { createElement }, + { render, unmountComponentAtNode }, + { MeteorProvider }, + { EngagementDashboardRoute }, + ] = await Promise.all([ + import('react'), + import('react-dom'), + import('../../../../client/providers/MeteorProvider'), + import('./components/EngagementDashboardRoute'), + ]); + + const container = this.firstNode; + + if (!container) { + return; + } + + this.autorun(() => { + const routeName = FlowRouter.getRouteName(); + if (routeName !== 'engagement-dashboard') { + unmountComponentAtNode(container); + } + }); + + render(createElement(MeteorProvider, { children: createElement(EngagementDashboardRoute) }), container); +}); + +let licensed = false; + +FlowRouter.route('/admin/engagement-dashboard/:tab?', { + name: 'engagement-dashboard', + action: () => { + if (!licensed) { + return; + } + + BlazeLayout.render('main', { center: 'EngagementDashboardRoute' }); + }, +}); + +hasLicense('engagement-dashboard').then((enabled) => { + if (!enabled) { + return; + } + + licensed = true; + + AdminBox.addOption({ + href: 'engagement-dashboard', + i18nLabel: 'Engagement Dashboard', + icon: 'file-keynote', + permissionGranted: () => hasAllPermission('view-statistics'), + }); +}).catch((error) => { + console.error('Error checking license.', error); +}); diff --git a/ee/app/engagement-dashboard/server/api/channels.js b/ee/app/engagement-dashboard/server/api/channels.js new file mode 100644 index 00000000000..405161d3e0c --- /dev/null +++ b/ee/app/engagement-dashboard/server/api/channels.js @@ -0,0 +1,26 @@ +import { check } from 'meteor/check'; + +import { API } from '../../../../../app/api'; +import { findAllChannelsWithNumberOfMessages } from '../lib/channels'; +import { transformDatesForAPI } from './helpers/date'; + +API.v1.addRoute('engagement-dashboard/channels/list', { authRequired: true }, { + get() { + const { start, end } = this.requestParams(); + const { offset, count } = this.getPaginationItems(); + + check(start, String); + check(end, String); + + const { channels, total } = Promise.await(findAllChannelsWithNumberOfMessages({ + ...transformDatesForAPI(start, end), + options: { offset, count }, + })); + return API.v1.success({ + channels, + count: channels.length, + offset, + total, + }); + }, +}); diff --git a/ee/app/engagement-dashboard/server/api/helpers/date.js b/ee/app/engagement-dashboard/server/api/helpers/date.js new file mode 100644 index 00000000000..0aae7b2b013 --- /dev/null +++ b/ee/app/engagement-dashboard/server/api/helpers/date.js @@ -0,0 +1,14 @@ +export const transformDatesForAPI = (start, end) => { + if (isNaN(Date.parse(start))) { + throw new Error('The "start" query parameter must be a valid date.'); + } + if (end && isNaN(Date.parse(end))) { + throw new Error('The "end" query parameter must be a valid date.'); + } + start = new Date(start); + end = new Date(end); + return { + start, + end, + }; +}; diff --git a/ee/app/engagement-dashboard/server/api/index.js b/ee/app/engagement-dashboard/server/api/index.js new file mode 100644 index 00000000000..007fb85a9f7 --- /dev/null +++ b/ee/app/engagement-dashboard/server/api/index.js @@ -0,0 +1,3 @@ +import './messages'; +import './channels'; +import './users'; diff --git a/ee/app/engagement-dashboard/server/api/messages.js b/ee/app/engagement-dashboard/server/api/messages.js new file mode 100644 index 00000000000..b51f393e009 --- /dev/null +++ b/ee/app/engagement-dashboard/server/api/messages.js @@ -0,0 +1,41 @@ +import { check } from 'meteor/check'; + +import { API } from '../../../../../app/api'; +import { findWeeklyMessagesSentData, findMessagesSentOrigin, findTopFivePopularChannelsByMessageSentQuantity } from '../lib/messages'; +import { transformDatesForAPI } from './helpers/date'; + +API.v1.addRoute('engagement-dashboard/messages/messages-sent', { authRequired: true }, { + get() { + const { start, end } = this.requestParams(); + + check(start, String); + check(end, String); + + const data = Promise.await(findWeeklyMessagesSentData(transformDatesForAPI(start, end))); + return API.v1.success(data); + }, +}); + +API.v1.addRoute('engagement-dashboard/messages/origin', { authRequired: true }, { + get() { + const { start, end } = this.requestParams(); + + check(start, String); + check(end, String); + + const data = Promise.await(findMessagesSentOrigin(transformDatesForAPI(start, end))); + return API.v1.success(data); + }, +}); + +API.v1.addRoute('engagement-dashboard/messages/top-five-popular-channels', { authRequired: true }, { + get() { + const { start, end } = this.requestParams(); + + check(start, String); + check(end, String); + + const data = Promise.await(findTopFivePopularChannelsByMessageSentQuantity(transformDatesForAPI(start, end))); + return API.v1.success(data); + }, +}); diff --git a/ee/app/engagement-dashboard/server/api/users.js b/ee/app/engagement-dashboard/server/api/users.js new file mode 100644 index 00000000000..aaf33b4c0f2 --- /dev/null +++ b/ee/app/engagement-dashboard/server/api/users.js @@ -0,0 +1,67 @@ +import { check } from 'meteor/check'; + +import { API } from '../../../../../app/api'; +import { + findWeeklyUsersRegisteredData, + findActiveUsersMonthlyData, + findBusiestsChatsInADayByHours, + findBusiestsChatsWithinAWeek, + findUserSessionsByHourWithinAWeek, +} from '../lib/users'; +import { transformDatesForAPI } from './helpers/date'; + +API.v1.addRoute('engagement-dashboard/users/new-users', { authRequired: true }, { + get() { + const { start, end } = this.requestParams(); + + check(start, String); + check(end, String); + + const data = Promise.await(findWeeklyUsersRegisteredData(transformDatesForAPI(start, end))); + return API.v1.success(data); + }, +}); + +API.v1.addRoute('engagement-dashboard/users/active-users', { authRequired: true }, { + get() { + const { start, end } = this.requestParams(); + + check(start, String); + check(end, String); + + const data = Promise.await(findActiveUsersMonthlyData(transformDatesForAPI(start, end))); + return API.v1.success(data); + }, +}); + +API.v1.addRoute('engagement-dashboard/users/chat-busier/hourly-data', { authRequired: true }, { + get() { + const { start } = this.requestParams(); + + const data = Promise.await(findBusiestsChatsInADayByHours(transformDatesForAPI(start))); + return API.v1.success(data); + }, +}); + +API.v1.addRoute('engagement-dashboard/users/chat-busier/weekly-data', { authRequired: true }, { + get() { + const { start } = this.requestParams(); + + check(start, String); + + const data = Promise.await(findBusiestsChatsWithinAWeek(transformDatesForAPI(start))); + return API.v1.success(data); + }, +}); + +API.v1.addRoute('engagement-dashboard/users/users-by-time-of-the-day-in-a-week', { authRequired: true }, { + get() { + const { start, end } = this.requestParams(); + + check(start, String); + check(end, String); + + const data = Promise.await(findUserSessionsByHourWithinAWeek(transformDatesForAPI(start, end))); + return API.v1.success(data); + }, +}); diff --git a/ee/app/engagement-dashboard/server/index.js b/ee/app/engagement-dashboard/server/index.js new file mode 100644 index 00000000000..0083bfabbf5 --- /dev/null +++ b/ee/app/engagement-dashboard/server/index.js @@ -0,0 +1,6 @@ +import { onLicense } from '../../license/server'; + +onLicense('engagement-dashboard', async () => { + await import('./listeners'); + await import('./api'); +}); diff --git a/ee/app/engagement-dashboard/server/lib/channels.js b/ee/app/engagement-dashboard/server/lib/channels.js new file mode 100644 index 00000000000..923365e3108 --- /dev/null +++ b/ee/app/engagement-dashboard/server/lib/channels.js @@ -0,0 +1,27 @@ +import moment from 'moment'; + +import { Rooms } from '../../../../../app/models/server/raw'; +import { convertDateToInt, diffBetweenDaysInclusive } from './date'; + +export const findAllChannelsWithNumberOfMessages = async ({ start, end, options = {} }) => { + const daysBetweenDates = diffBetweenDaysInclusive(end, start); + const endOfLastWeek = moment(start).clone().subtract(1, 'days').toDate(); + const startOfLastWeek = moment(endOfLastWeek).clone().subtract(daysBetweenDates, 'days').toDate(); + const total = await Rooms.findChannelsWithNumberOfMessagesBetweenDate({ + start: convertDateToInt(start), + end: convertDateToInt(end), + startOfLastWeek: convertDateToInt(startOfLastWeek), + endOfLastWeek: convertDateToInt(endOfLastWeek), + onlyCount: true, + }).toArray(); + return { + channels: await Rooms.findChannelsWithNumberOfMessagesBetweenDate({ + start: convertDateToInt(start), + end: convertDateToInt(end), + startOfLastWeek: convertDateToInt(startOfLastWeek), + endOfLastWeek: convertDateToInt(endOfLastWeek), + options, + }), + total: total.length ? total[0].total : 0, + }; +}; diff --git a/ee/app/engagement-dashboard/server/lib/date.js b/ee/app/engagement-dashboard/server/lib/date.js new file mode 100644 index 00000000000..189c5bd0ff3 --- /dev/null +++ b/ee/app/engagement-dashboard/server/lib/date.js @@ -0,0 +1,11 @@ +import moment from 'moment'; + +export const convertDateToInt = (date) => parseInt(moment(date).clone().format('YYYYMMDD')); +export const convertIntToDate = (intValue) => moment(intValue, 'YYYYMMDD').clone().toDate(); +export const diffBetweenDays = (start, end) => moment(new Date(start)).clone().diff(new Date(end), 'days'); +export const diffBetweenDaysInclusive = (start, end) => diffBetweenDays(start, end) + 1; + +export const getTotalOfWeekItems = (weekItems, property) => weekItems.reduce((acc, item) => { + acc += item[property]; + return acc; +}, 0); diff --git a/ee/app/engagement-dashboard/server/lib/messages.js b/ee/app/engagement-dashboard/server/lib/messages.js new file mode 100644 index 00000000000..e6f0d621dcc --- /dev/null +++ b/ee/app/engagement-dashboard/server/lib/messages.js @@ -0,0 +1,85 @@ +import moment from 'moment'; + +import Analytics from '../../../../../app/models/server/raw/Analytics'; +import { roomTypes } from '../../../../../app/utils'; +import { convertDateToInt, diffBetweenDaysInclusive, convertIntToDate, getTotalOfWeekItems } from './date'; + +export const handleMessagesSent = (message, room) => { + const roomTypesToShow = roomTypes.getTypesToShowOnDashboard(); + if (!roomTypesToShow.includes(room.t)) { + return; + } + Promise.await(Analytics.saveMessageSent({ + date: convertDateToInt(message.ts), + room, + })); + return message; +}; + +export const handleMessagesDeleted = (message, room) => { + const roomTypesToShow = roomTypes.getTypesToShowOnDashboard(); + if (!roomTypesToShow.includes(room.t)) { + return; + } + Promise.await(Analytics.saveMessageDeleted({ + date: convertDateToInt(message.ts), + room, + })); + return message; +}; + +export const findWeeklyMessagesSentData = async ({ start, end }) => { + const daysBetweenDates = diffBetweenDaysInclusive(end, start); + const endOfLastWeek = moment(start).clone().subtract(1, 'days').toDate(); + const startOfLastWeek = moment(endOfLastWeek).clone().subtract(daysBetweenDates, 'days').toDate(); + const today = convertDateToInt(end); + const yesterday = convertDateToInt(moment(end).clone().subtract(1, 'days').toDate()); + const currentPeriodMessages = await Analytics.getMessagesSentTotalByDate({ + start: convertDateToInt(start), + end: convertDateToInt(end), + options: { count: daysBetweenDates, sort: { _id: -1 } }, + }); + const lastPeriodMessages = await Analytics.getMessagesSentTotalByDate({ + start: convertDateToInt(startOfLastWeek), + end: convertDateToInt(endOfLastWeek), + options: { count: daysBetweenDates, sort: { _id: -1 } }, + }); + const yesterdayMessages = (currentPeriodMessages.find((item) => item._id === yesterday) || {}).messages || 0; + const todayMessages = (currentPeriodMessages.find((item) => item._id === today) || {}).messages || 0; + const currentPeriodTotalOfMessages = getTotalOfWeekItems(currentPeriodMessages, 'messages'); + const lastPeriodTotalOfMessages = getTotalOfWeekItems(lastPeriodMessages, 'messages'); + return { + days: currentPeriodMessages.map((day) => ({ day: convertIntToDate(day._id), messages: day.messages })), + period: { + count: currentPeriodTotalOfMessages, + variation: currentPeriodTotalOfMessages - lastPeriodTotalOfMessages, + }, + yesterday: { + count: yesterdayMessages, + variation: todayMessages - yesterdayMessages, + }, + }; +}; + +export const findMessagesSentOrigin = async ({ start, end }) => { + const origins = await Analytics.getMessagesOrigin({ + start: convertDateToInt(start), + end: convertDateToInt(end), + }); + const roomTypesToShow = roomTypes.getTypesToShowOnDashboard(); + const responseTypes = origins.map((origin) => origin.t); + const missingTypes = roomTypesToShow.filter((type) => !responseTypes.includes(type)); + if (missingTypes.length) { + missingTypes.forEach((type) => origins.push({ messages: 0, t: type })); + } + return { origins }; +}; + +export const findTopFivePopularChannelsByMessageSentQuantity = async ({ start, end }) => { + const channels = await Analytics.getMostPopularChannelsByMessagesSentQuantity({ + start: convertDateToInt(start), + end: convertDateToInt(end), + options: { count: 5, sort: { messages: -1 } }, + }); + return { channels }; +}; diff --git a/ee/app/engagement-dashboard/server/lib/users.js b/ee/app/engagement-dashboard/server/lib/users.js new file mode 100644 index 00000000000..5ea0a8b5ee5 --- /dev/null +++ b/ee/app/engagement-dashboard/server/lib/users.js @@ -0,0 +1,109 @@ +import moment from 'moment'; + +import Analytics from '../../../../../app/models/server/raw/Analytics'; +import Sessions from '../../../../../app/models/server/raw/Sessions'; +import { convertDateToInt, diffBetweenDaysInclusive, getTotalOfWeekItems, convertIntToDate } from './date'; + +export const handleUserCreated = (user) => { + Promise.await(Analytics.saveUserData({ + date: convertDateToInt(user.ts), + user, + })); + return user; +}; + +export const findWeeklyUsersRegisteredData = async ({ start, end }) => { + const daysBetweenDates = diffBetweenDaysInclusive(end, start); + const endOfLastWeek = moment(start).clone().subtract(1, 'days').toDate(); + const startOfLastWeek = moment(endOfLastWeek).clone().subtract(daysBetweenDates, 'days').toDate(); + const today = convertDateToInt(end); + const yesterday = convertDateToInt(moment(end).clone().subtract(1, 'days').toDate()); + const currentPeriodUsers = await Analytics.getTotalOfRegisteredUsersByDate({ + start: convertDateToInt(start), + end: convertDateToInt(end), + options: { count: daysBetweenDates, sort: { _id: -1 } }, + }); + const lastPeriodUsers = await Analytics.getTotalOfRegisteredUsersByDate({ + start: convertDateToInt(startOfLastWeek), + end: convertDateToInt(endOfLastWeek), + options: { count: daysBetweenDates, sort: { _id: -1 } }, + }); + const yesterdayUsers = (currentPeriodUsers.find((item) => item._id === yesterday) || {}).users || 0; + const todayUsers = (currentPeriodUsers.find((item) => item._id === today) || {}).users || 0; + const currentPeriodTotalUsers = getTotalOfWeekItems(currentPeriodUsers, 'users'); + const lastPeriodTotalUsers = getTotalOfWeekItems(lastPeriodUsers, 'users'); + return { + days: currentPeriodUsers.map((day) => ({ day: convertIntToDate(day._id), users: day.users })), + period: { + count: currentPeriodTotalUsers, + variation: currentPeriodTotalUsers - lastPeriodTotalUsers, + }, + yesterday: { + count: yesterdayUsers, + variation: todayUsers - yesterdayUsers, + }, + }; +}; + +export const findActiveUsersMonthlyData = async ({ start, end }) => { + const startOfPeriod = moment(start); + const endOfPeriod = moment(end); + + return { + month: await Sessions.getActiveUsersOfPeriodByDayBetweenDates({ + start: { + year: startOfPeriod.year(), + month: startOfPeriod.month() + 1, + day: startOfPeriod.date(), + }, + end: { + year: endOfPeriod.year(), + month: endOfPeriod.month() + 1, + day: endOfPeriod.date(), + }, + }), + }; +}; + +export const findBusiestsChatsInADayByHours = async ({ start }) => { + const now = moment(start); + const yesterday = moment(now).clone().subtract(24, 'hours'); + return { + hours: await Sessions.getBusiestTimeWithinHoursPeriod({ + start: yesterday.toDate(), + end: now.toDate(), + }), + }; +}; + +export const findBusiestsChatsWithinAWeek = async ({ start }) => { + const today = moment(start); + const startOfCurrentWeek = moment(today).clone().subtract(7, 'days'); + + return { + month: await Sessions.getTotalOfSessionsByDayBetweenDates({ + start: { + year: startOfCurrentWeek.year(), + month: startOfCurrentWeek.month() + 1, + day: startOfCurrentWeek.date(), + }, + end: { + year: today.year(), + month: today.month() + 1, + day: today.date(), + }, + }), + }; +}; + +export const findUserSessionsByHourWithinAWeek = async ({ start, end }) => { + const startOfPeriod = moment(start); + const endOfPeriod = moment(end); + + return { + week: await Sessions.getTotalOfSessionByHourAndDayBetweenDates({ + start: startOfPeriod.toDate(), + end: endOfPeriod.toDate(), + }), + }; +}; diff --git a/ee/app/engagement-dashboard/server/listeners/index.js b/ee/app/engagement-dashboard/server/listeners/index.js new file mode 100644 index 00000000000..0ca660eabe6 --- /dev/null +++ b/ee/app/engagement-dashboard/server/listeners/index.js @@ -0,0 +1,2 @@ +import './messages'; +import './users'; diff --git a/ee/app/engagement-dashboard/server/listeners/messages.js b/ee/app/engagement-dashboard/server/listeners/messages.js new file mode 100644 index 00000000000..56af5840abb --- /dev/null +++ b/ee/app/engagement-dashboard/server/listeners/messages.js @@ -0,0 +1,5 @@ +import { callbacks } from '../../../../../app/callbacks/server'; +import { handleMessagesSent, handleMessagesDeleted } from '../lib/messages'; + +callbacks.add('afterSaveMessage', handleMessagesSent); +callbacks.add('afterDeleteMessage', handleMessagesDeleted); diff --git a/ee/app/engagement-dashboard/server/listeners/users.js b/ee/app/engagement-dashboard/server/listeners/users.js new file mode 100644 index 00000000000..b6b730d2d96 --- /dev/null +++ b/ee/app/engagement-dashboard/server/listeners/users.js @@ -0,0 +1,4 @@ +import { callbacks } from '../../../../../app/callbacks/server'; +import { handleUserCreated } from '../lib/users'; + +callbacks.add('afterCreateUser', handleUserCreated); diff --git a/ee/app/license/server/bundles.js b/ee/app/license/server/bundles.js index 361aede9b17..f4cc45c2d34 100644 --- a/ee/app/license/server/bundles.js +++ b/ee/app/license/server/bundles.js @@ -4,6 +4,7 @@ const bundles = { 'canned-responses', 'ldap-enterprise', 'livechat-enterprise', + 'engagement-dashboard', ], pro: [ ], diff --git a/ee/client/index.js b/ee/client/index.js index 8e58d438ebd..1d7f2241758 100644 --- a/ee/client/index.js +++ b/ee/client/index.js @@ -1,4 +1,5 @@ import '../app/auditing/client/index'; import '../app/canned-responses/client/index'; +import '../app/engagement-dashboard/client/index'; import '../app/license/client/index'; import '../app/livechat-enterprise/client/index'; diff --git a/ee/server/index.js b/ee/server/index.js index 81102e91054..00359f17b20 100644 --- a/ee/server/index.js +++ b/ee/server/index.js @@ -2,5 +2,6 @@ import '../app/models'; import '../app/api-enterprise/server/index'; import '../app/auditing/server/index'; import '../app/canned-responses/server/index'; +import '../app/engagement-dashboard/server/index'; import '../app/ldap-enterprise/server/index'; import '../app/livechat-enterprise/server/index'; diff --git a/package-lock.json b/package-lock.json index 2131987508a..b7d03fdf0c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2372,8 +2372,7 @@ "@emotion/stylis": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", - "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==", - "dev": true + "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" }, "@emotion/unitless": { "version": "0.7.4", @@ -2558,6 +2557,170 @@ "glob-to-regexp": "^0.3.0" } }, + "@nivo/annotations": { + "version": "0.61.0", + "resolved": "https://registry.npmjs.org/@nivo/annotations/-/annotations-0.61.0.tgz", + "integrity": "sha512-i2JKYeMEPC+zwgN/p+hVRWUJ4aee+kE8BfMzCLt1/a+rsfT+v2v5kj12zx072teoaQt53lOi1GdV2lEYA6HJpg==", + "requires": { + "@nivo/colors": "0.61.0", + "@nivo/core": "0.61.0", + "lodash": "^4.17.11", + "react-motion": "^0.5.2" + } + }, + "@nivo/axes": { + "version": "0.61.0", + "resolved": "https://registry.npmjs.org/@nivo/axes/-/axes-0.61.0.tgz", + "integrity": "sha512-U0rHIYNnwt03dFFBz0aosfd5nFIRVD1Wff5DwVeM7PouBZM3AsLVWeLlUWLWOmg+BHftqhbOGTOoZN2SjU7bwA==", + "requires": { + "@nivo/core": "0.61.0", + "d3-format": "^1.3.2", + "d3-time": "^1.0.11", + "d3-time-format": "^2.1.3", + "lodash": "^4.17.11", + "react-motion": "^0.5.2" + } + }, + "@nivo/bar": { + "version": "0.61.1", + "resolved": "https://registry.npmjs.org/@nivo/bar/-/bar-0.61.1.tgz", + "integrity": "sha512-MYdjeFt6HqEyIBFwbVHKPjpw8+DPirAZwJDZi7dPXT0F7WcwJOnRITlRHhmQEGq5u71h26UN+M0z/ZQkB54mvw==", + "requires": { + "@nivo/annotations": "0.61.0", + "@nivo/axes": "0.61.0", + "@nivo/colors": "0.61.0", + "@nivo/core": "0.61.0", + "@nivo/legends": "0.61.1", + "@nivo/tooltip": "0.61.0", + "d3-scale": "^3.0.0", + "d3-shape": "^1.2.2", + "lodash": "^4.17.11", + "react-motion": "^0.5.2", + "recompose": "^0.30.0" + } + }, + "@nivo/colors": { + "version": "0.61.0", + "resolved": "https://registry.npmjs.org/@nivo/colors/-/colors-0.61.0.tgz", + "integrity": "sha512-yeb5YsQDoN7D5DbBIhHTnVn0bX+4ObNVGyJAepSn64zNPiskO3/o1FnQw70aIkN4O7BDXb/vVPrftq6wSwQtvQ==", + "requires": { + "d3-color": "^1.2.3", + "d3-scale": "^3.0.0", + "d3-scale-chromatic": "^1.3.3", + "lodash.get": "^4.4.2", + "lodash.isplainobject": "^4.0.6", + "react-motion": "^0.5.2" + } + }, + "@nivo/core": { + "version": "0.61.0", + "resolved": "https://registry.npmjs.org/@nivo/core/-/core-0.61.0.tgz", + "integrity": "sha512-7DGsTW12vfUvMIr9jl28KZaJMJqMMhEJi1lW1R2TPMTg+qSG01v6tqMtcEwUp4bdAdr3n57ytLWSgqKWXkwjvw==", + "requires": { + "@nivo/tooltip": "0.61.0", + "d3-color": "^1.2.3", + "d3-format": "^1.3.2", + "d3-hierarchy": "^1.1.8", + "d3-interpolate": "^1.3.2", + "d3-scale": "^3.0.0", + "d3-scale-chromatic": "^1.3.3", + "d3-shape": "^1.3.5", + "d3-time-format": "^2.1.3", + "lodash": "^4.17.11", + "react-measure": "^2.2.4", + "react-motion": "^0.5.2", + "recompose": "^0.30.0" + } + }, + "@nivo/heatmap": { + "version": "0.61.0", + "resolved": "https://registry.npmjs.org/@nivo/heatmap/-/heatmap-0.61.0.tgz", + "integrity": "sha512-ROJSP0mvCYuavdkTZdRj1+9Nd+2s2oN86eEyC6iiDqFGYjBSFy3iNnGLFq8jGHZ/GDYBrLC3L3IxnWhM184T+Q==", + "requires": { + "@nivo/axes": "0.61.0", + "@nivo/colors": "0.61.0", + "@nivo/core": "0.61.0", + "@nivo/tooltip": "0.61.0", + "d3-scale": "^3.0.0", + "lodash": "^4.17.11", + "react-motion": "^0.5.2", + "recompose": "^0.30.0" + } + }, + "@nivo/legends": { + "version": "0.61.1", + "resolved": "https://registry.npmjs.org/@nivo/legends/-/legends-0.61.1.tgz", + "integrity": "sha512-bKVXffFwTKGySZRUf6sdVzWUb5jjGffuvRczs0giQCu8OUgeJIi0IOOyYhHtww+rTVGIKAi0xPGQTQnF4kpufA==", + "requires": { + "@nivo/core": "0.61.0", + "lodash": "^4.17.11", + "recompose": "^0.30.0" + } + }, + "@nivo/line": { + "version": "0.61.1", + "resolved": "https://registry.npmjs.org/@nivo/line/-/line-0.61.1.tgz", + "integrity": "sha512-PZFGgcj+IlDtZG6kTdBrGJ5cJvs1w5kaAI86IaH5AXJ0MQqVIZYWgbXdf5Vg6Hv2ouLmwNwONA/ORACKVkG+YA==", + "requires": { + "@nivo/annotations": "0.61.0", + "@nivo/axes": "0.61.0", + "@nivo/colors": "0.61.0", + "@nivo/core": "0.61.0", + "@nivo/legends": "0.61.1", + "@nivo/scales": "0.61.0", + "@nivo/tooltip": "0.61.0", + "@nivo/voronoi": "0.61.0", + "d3-shape": "^1.3.5", + "lodash": "^4.17.11", + "react-motion": "^0.5.2" + } + }, + "@nivo/pie": { + "version": "0.61.1", + "resolved": "https://registry.npmjs.org/@nivo/pie/-/pie-0.61.1.tgz", + "integrity": "sha512-3xmYrB/rccJ6f5AtckhIm51Bj7IVYomeCJsM1vK07wzOS+ZpvJRODokzXSNvp7NkMl3jrqCcGriLhSg3mf9+yA==", + "requires": { + "@nivo/colors": "0.61.0", + "@nivo/core": "0.61.0", + "@nivo/legends": "0.61.1", + "@nivo/tooltip": "0.61.0", + "d3-shape": "^1.3.5", + "lodash": "^4.17.11", + "react-motion": "^0.5.2", + "recompose": "^0.30.0" + } + }, + "@nivo/scales": { + "version": "0.61.0", + "resolved": "https://registry.npmjs.org/@nivo/scales/-/scales-0.61.0.tgz", + "integrity": "sha512-7MoxxecMDvpK9L0Py/drEQxG/4YAzo9KBvLzo3/KjInc1VEscpDkpVSSN5tmg1qbQE3WCrziec4JuH9q1V/Q7g==", + "requires": { + "d3-scale": "^3.0.0", + "d3-time-format": "^2.1.3", + "lodash": "^4.17.11" + } + }, + "@nivo/tooltip": { + "version": "0.61.0", + "resolved": "https://registry.npmjs.org/@nivo/tooltip/-/tooltip-0.61.0.tgz", + "integrity": "sha512-CqEJ4v1jSikZ3fmuSJVb1UYF8fuCo/c7JFB+LsNH9X01IERSufO3tSNBTzJ3JugCminQpbo6/R7oBhNwZFqSxw==", + "requires": { + "@nivo/core": "0.61.0", + "react-measure": "^2.2.4", + "react-motion": "^0.5.2" + } + }, + "@nivo/voronoi": { + "version": "0.61.0", + "resolved": "https://registry.npmjs.org/@nivo/voronoi/-/voronoi-0.61.0.tgz", + "integrity": "sha512-VVB7BW8GX8Gq9kTf/L52HrCD//4PAT6RTeDwb4N8BpSNfyfmBXacU9U9RMK7HAJjxICzEuxam75/oTCjX6iVBg==", + "requires": { + "@nivo/core": "0.61.0", + "d3-delaunay": "^5.1.1", + "d3-scale": "^3.0.0", + "recompose": "^0.30.0" + } + }, "@nodelib/fs.stat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", @@ -2706,6 +2869,22 @@ } } }, + "@rocket.chat/css-in-js": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@rocket.chat/css-in-js/-/css-in-js-0.6.0.tgz", + "integrity": "sha512-XbkUl2kJ/j24qGOAhBY/FP8yxeiHN0WnGw46WppDrZufDv/yBqCKUkWOSfsYPZq3lqUsu7sPv//pVTMfn8ao4Q==", + "requires": { + "@emotion/hash": "^0.8.0", + "@emotion/stylis": "^0.8.5" + }, + "dependencies": { + "@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" + } + } + }, "@rocket.chat/eslint-config": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@rocket.chat/eslint-config/-/eslint-config-0.3.0.tgz", @@ -2716,36 +2895,44 @@ } }, "@rocket.chat/fuselage": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@rocket.chat/fuselage/-/fuselage-0.3.0.tgz", - "integrity": "sha512-nZdtJtEjZO2ZKQafo0ijANRAlr+wSQ9PzsjpRcan4XVaOdnS7Yri7ZlqtA2jdmHyr5/bbQzReM0WudujS5JMyg==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@rocket.chat/fuselage/-/fuselage-0.6.0.tgz", + "integrity": "sha512-+jZmEO89xo7wW2yMEC/ZrGQUD4TxQJQH1hNL0hV1t39d7YzGdJO3eMNGYEIeADwPpCm1TMgAmavivaDkSUH58A==", "requires": { - "@rocket.chat/fuselage-tokens": "^0.3.0", - "@rocket.chat/icons": "^0.3.0" + "@rocket.chat/css-in-js": "^0.6.0", + "@rocket.chat/fuselage-tokens": "^0.6.0", + "@rocket.chat/icons": "^0.6.0" + }, + "dependencies": { + "@rocket.chat/icons": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@rocket.chat/icons/-/icons-0.6.0.tgz", + "integrity": "sha512-Kl2C5m9glngFq6J2JxvijMMmrq6f+TAcxPUUEYeglHiORvIJtfYgHT7dwzLhlUiAwMUJSBVsYUzvsAvgiOxdbQ==" + } } }, "@rocket.chat/fuselage-hooks": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@rocket.chat/fuselage-hooks/-/fuselage-hooks-0.3.0.tgz", - "integrity": "sha512-VpRyEhntgJ1HLYtaRmOzdHTpfGxkyyD9ElxBwEENMcu4/Ke6brIttznevndjaZWznfF/VV+15vcFovimRt/giQ==" + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@rocket.chat/fuselage-hooks/-/fuselage-hooks-0.6.0.tgz", + "integrity": "sha512-RacebD01VEL1UWa+0qt0X5KwyAzx9bgfk0RC2T9WfwcYWntI0MX3RlMnM6Ld8KwTqOpWs5iIpOasQYCCKM7Ljw==" }, "@rocket.chat/fuselage-tokens": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@rocket.chat/fuselage-tokens/-/fuselage-tokens-0.3.0.tgz", - "integrity": "sha512-PEFww6Q4gRmBo97nA6JHICSYHqQuCDHS4KFS070ofdaRParEaBhrQ8CBF21RpIXZpofk9d4nR96JdI0uf6rWOw==" + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@rocket.chat/fuselage-tokens/-/fuselage-tokens-0.6.0.tgz", + "integrity": "sha512-QKObG0aTnWTzOQGOkqE4yYZRmKg7aZnV97uPnx42iKjyNEE8fM++L9hBaWEnJDfqUrLA1UNCjOn5snO5/bGPNw==" }, "@rocket.chat/fuselage-ui-kit": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@rocket.chat/fuselage-ui-kit/-/fuselage-ui-kit-0.3.0.tgz", - "integrity": "sha512-V5HhWREpiTCkZ8ww5ow2Iafa9sB/uWGXEa5s0sOJEpGmgXZIRsUQlgcbD87Dm2fx57AXJR19Os4HxVNodF55Ow==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@rocket.chat/fuselage-ui-kit/-/fuselage-ui-kit-0.6.0.tgz", + "integrity": "sha512-PGCw+IvNsW4A/msAahUXpkv6A6PDcxTGhMEGmA/DiOc7Y72u+7giQl43vLTuW/u5pafJ0gG83TKNxwCBcHPw8g==", "requires": { - "@rocket.chat/ui-kit": "^0.3.0" + "@rocket.chat/ui-kit": "^0.6.0" }, "dependencies": { "@rocket.chat/ui-kit": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@rocket.chat/ui-kit/-/ui-kit-0.3.0.tgz", - "integrity": "sha512-YAV5l6iVIWuileg5DOU/0NQ/23/AULEr8UblIUSF3OShPN8nWanGMdG0E6xFLyXT+KHvmvzhiQe0/ju3Tk7x8A==" + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@rocket.chat/ui-kit/-/ui-kit-0.6.0.tgz", + "integrity": "sha512-Mh82FDw2AOp88LI0IqqT20N6g+cNHfmnXsUU5BZUFh9rI5dMZDrOiYEOZpiWyC5idnWjlmPnxiANemyb8e3Gow==" } } }, @@ -10062,6 +10249,11 @@ "supports-color": "^2.0.0" } }, + "change-emitter": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/change-emitter/-/change-emitter-0.1.6.tgz", + "integrity": "sha1-6LL+PX8at9aaMhma/5HqaTFAlRU=" + }, "character-entities": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.2.tgz", @@ -12186,6 +12378,89 @@ } } }, + "d3-array": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.4.0.tgz", + "integrity": "sha512-KQ41bAF2BMakf/HdKT865ALd4cgND6VcIztVQZUTt0+BH3RWy6ZYnHghVXf6NFjt2ritLr8H1T8LreAAlfiNcw==" + }, + "d3-color": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.0.tgz", + "integrity": "sha512-TzNPeJy2+iEepfiL92LAAB7fvnp/dV2YwANPVHdDWmYMm23qIJBYww3qT8I8C1wXrmrg4UWs7BKc2tKIgyjzHg==" + }, + "d3-delaunay": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-5.2.1.tgz", + "integrity": "sha512-ZZdeJl6cKRyqYVFYK+/meXvWIrAvZsZTD7WSxl4OPXCmuXNgDyACAClAJHD63zL25TA+IJGURUNO7rFseNFCYw==", + "requires": { + "delaunator": "4" + } + }, + "d3-format": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.3.tgz", + "integrity": "sha512-mm/nE2Y9HgGyjP+rKIekeITVgBtX97o1nrvHCWX8F/yBYyevUTvu9vb5pUnKwrcSw7o7GuwMOWjS9gFDs4O+uQ==" + }, + "d3-hierarchy": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", + "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==" + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, + "d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" + }, + "d3-scale": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.1.tgz", + "integrity": "sha512-huz5byJO/6MPpz6Q8d4lg7GgSpTjIZW/l+1MQkzKfu2u8P6hjaXaStOpmyrD6ymKoW87d2QVFCKvSjLwjzx/rA==", + "requires": { + "d3-array": "1.2.0 - 2", + "d3-format": "1", + "d3-interpolate": "^1.2.0", + "d3-time": "1", + "d3-time-format": "2" + } + }, + "d3-scale-chromatic": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz", + "integrity": "sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==", + "requires": { + "d3-color": "1", + "d3-interpolate": "1" + } + }, + "d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "requires": { + "d3-path": "1" + } + }, + "d3-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", + "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==" + }, + "d3-time-format": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.2.3.tgz", + "integrity": "sha512-RAHNnD8+XvC4Zc4d2A56Uw0yJoM7bsvOlJR33bclxq399Rak/b9bhvu/InjxdWhPtkgU53JJcleJTGkNRnN6IA==", + "requires": { + "d3-time": "1" + } + }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -12536,6 +12811,11 @@ } } }, + "delaunator": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-4.0.1.tgz", + "integrity": "sha512-WNPWi1IRKZfCt/qIDMfERkDp93+iZEmOxN2yy4Jg+Xhv8SLk2UTqqbe1sfiipn0and9QrE914/ihdx82Y/Giag==" + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -14460,6 +14740,27 @@ "format": "^0.2.0" } }, + "fbjs": { + "version": "0.8.17", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz", + "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=", + "requires": { + "core-js": "^1.0.0", + "isomorphic-fetch": "^2.1.1", + "loose-envify": "^1.0.0", + "object-assign": "^4.1.0", + "promise": "^7.1.1", + "setimmediate": "^1.0.5", + "ua-parser-js": "^0.7.18" + }, + "dependencies": { + "core-js": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", + "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" + } + } + }, "fd-slicer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", @@ -15061,28 +15362,28 @@ "dependencies": { "abbrev": { "version": "1.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true, "optional": true }, "ansi-regex": { "version": "2.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "dev": true, "optional": true }, "aproba": { "version": "1.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "dev": true, "optional": true }, "are-we-there-yet": { "version": "1.1.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "dev": true, "optional": true, @@ -15093,14 +15394,14 @@ }, "balanced-match": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true, "optional": true }, "brace-expansion": { "version": "1.1.11", - "resolved": false, + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "optional": true, @@ -15118,28 +15419,28 @@ }, "code-point-at": { "version": "1.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "dev": true, "optional": true }, "concat-map": { "version": "0.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true, "optional": true }, "console-control-strings": { "version": "1.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "dev": true, "optional": true }, "core-util-is": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true, "optional": true @@ -15156,21 +15457,21 @@ }, "deep-extend": { "version": "0.6.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true, "optional": true }, "delegates": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "dev": true, "optional": true }, "detect-libc": { "version": "1.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "dev": true, "optional": true @@ -15187,14 +15488,14 @@ }, "fs.realpath": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true, "optional": true }, "gauge": { "version": "2.7.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "dev": true, "optional": true, @@ -15226,14 +15527,14 @@ }, "has-unicode": { "version": "2.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "dev": true, "optional": true }, "iconv-lite": { "version": "0.4.24", - "resolved": false, + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, "optional": true, @@ -15253,7 +15554,7 @@ }, "inflight": { "version": "1.0.6", - "resolved": false, + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, "optional": true, @@ -15271,14 +15572,14 @@ }, "ini": { "version": "1.3.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "dev": true, "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, "optional": true, @@ -15288,14 +15589,14 @@ }, "isarray": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "dev": true, "optional": true }, "minimatch": { "version": "3.0.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, "optional": true, @@ -15417,7 +15718,7 @@ }, "npmlog": { "version": "4.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "dev": true, "optional": true, @@ -15430,21 +15731,21 @@ }, "number-is-nan": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "dev": true, "optional": true }, "object-assign": { "version": "4.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "dev": true, "optional": true }, "once": { "version": "1.4.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, "optional": true, @@ -15454,21 +15755,21 @@ }, "os-homedir": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "dev": true, "optional": true }, "os-tmpdir": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true, "optional": true }, "osenv": { "version": "0.1.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "dev": true, "optional": true, @@ -15479,7 +15780,7 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true, "optional": true @@ -15493,7 +15794,7 @@ }, "rc": { "version": "1.2.8", - "resolved": false, + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, "optional": true, @@ -15541,21 +15842,21 @@ }, "safe-buffer": { "version": "5.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, "optional": true }, "safer-buffer": { "version": "2.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, "optional": true }, "sax": { "version": "1.2.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "dev": true, "optional": true @@ -15569,21 +15870,21 @@ }, "set-blocking": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true, "optional": true }, "signal-exit": { "version": "3.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true, "optional": true }, "string-width": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, "optional": true, @@ -15595,7 +15896,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "optional": true, @@ -15605,7 +15906,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "optional": true, @@ -15615,7 +15916,7 @@ }, "strip-json-comments": { "version": "2.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true, "optional": true @@ -15638,14 +15939,14 @@ }, "util-deprecate": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true, "optional": true }, "wide-align": { "version": "1.1.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "dev": true, "optional": true, @@ -15655,7 +15956,7 @@ }, "wrappy": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true, "optional": true @@ -15864,6 +16165,11 @@ "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", "dev": true }, + "get-node-dimensions": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-node-dimensions/-/get-node-dimensions-1.2.1.tgz", + "integrity": "sha512-2MSPMu7S1iOTL+BOa6K1S62hB2zUAYNF/lV0gSVlOaacd087lc6nR1H1r0e3B1CerTo+RceOmi1iJW+vp21xcQ==" + }, "get-stdin": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", @@ -18049,6 +18355,26 @@ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "dev": true }, + "isomorphic-fetch": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", + "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", + "requires": { + "node-fetch": "^1.0.1", + "whatwg-fetch": ">=0.10.0" + }, + "dependencies": { + "node-fetch": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", + "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", + "requires": { + "encoding": "^0.1.11", + "is-stream": "^1.0.1" + } + } + } + }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -19212,8 +19538,7 @@ "lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", - "dev": true + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" }, "lodash.has": { "version": "4.5.2", @@ -20093,7 +20418,7 @@ "dependencies": { "asn1.js": { "version": "4.10.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", "requires": { "bn.js": "^4.0.0", @@ -20103,7 +20428,7 @@ }, "assert": { "version": "1.4.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", "requires": { "util": "0.10.3" @@ -20111,7 +20436,7 @@ "dependencies": { "util": { "version": "0.10.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", "requires": { "inherits": "2.0.1" @@ -20121,22 +20446,22 @@ }, "base64-js": { "version": "1.3.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" }, "bn.js": { "version": "4.11.8", - "resolved": false, + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" }, "brorand": { "version": "1.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" }, "browserify-aes": { "version": "1.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", "requires": { "buffer-xor": "^1.0.3", @@ -20149,7 +20474,7 @@ }, "browserify-cipher": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", "requires": { "browserify-aes": "^1.0.4", @@ -20159,7 +20484,7 @@ }, "browserify-des": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", "requires": { "cipher-base": "^1.0.1", @@ -20170,7 +20495,7 @@ }, "browserify-rsa": { "version": "4.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", "requires": { "bn.js": "^4.1.0", @@ -20179,7 +20504,7 @@ }, "browserify-sign": { "version": "4.0.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", "requires": { "bn.js": "^4.1.1", @@ -20193,7 +20518,7 @@ }, "browserify-zlib": { "version": "0.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", "requires": { "pako": "~1.0.5" @@ -20201,7 +20526,7 @@ }, "buffer": { "version": "5.2.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", "requires": { "base64-js": "^1.0.2", @@ -20210,17 +20535,17 @@ }, "buffer-xor": { "version": "1.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" }, "builtin-status-codes": { "version": "3.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=" }, "cipher-base": { "version": "1.0.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", "requires": { "inherits": "^2.0.1", @@ -20229,7 +20554,7 @@ }, "console-browserify": { "version": "1.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", "requires": { "date-now": "^0.1.4" @@ -20237,17 +20562,17 @@ }, "constants-browserify": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=" }, "core-util-is": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "create-ecdh": { "version": "4.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", "requires": { "bn.js": "^4.1.0", @@ -20256,7 +20581,7 @@ }, "create-hash": { "version": "1.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "requires": { "cipher-base": "^1.0.1", @@ -20268,7 +20593,7 @@ }, "create-hmac": { "version": "1.1.7", - "resolved": false, + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "requires": { "cipher-base": "^1.0.3", @@ -20281,7 +20606,7 @@ }, "crypto-browserify": { "version": "3.12.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", "requires": { "browserify-cipher": "^1.0.0", @@ -20299,12 +20624,12 @@ }, "date-now": { "version": "0.1.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=" }, "des.js": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", "requires": { "inherits": "^2.0.1", @@ -20313,7 +20638,7 @@ }, "diffie-hellman": { "version": "5.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", "requires": { "bn.js": "^4.1.0", @@ -20323,12 +20648,12 @@ }, "domain-browser": { "version": "1.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==" }, "elliptic": { "version": "6.4.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz", "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==", "requires": { "bn.js": "^4.4.0", @@ -20342,12 +20667,12 @@ }, "events": { "version": "3.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz", "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==" }, "evp_bytestokey": { "version": "1.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", "requires": { "md5.js": "^1.3.4", @@ -20356,7 +20681,7 @@ }, "hash-base": { "version": "3.0.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", "requires": { "inherits": "^2.0.1", @@ -20365,7 +20690,7 @@ }, "hash.js": { "version": "1.1.7", - "resolved": false, + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", "requires": { "inherits": "^2.0.3", @@ -20374,14 +20699,14 @@ "dependencies": { "inherits": { "version": "2.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" } } }, "hmac-drbg": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", "requires": { "hash.js": "^1.0.3", @@ -20391,27 +20716,27 @@ }, "https-browserify": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" }, "ieee754": { "version": "1.1.13", - "resolved": false, + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" }, "inherits": { "version": "2.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" }, "isarray": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "md5.js": { "version": "1.3.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", "requires": { "hash-base": "^3.0.0", @@ -20421,7 +20746,7 @@ }, "miller-rabin": { "version": "4.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", "requires": { "bn.js": "^4.0.0", @@ -20430,27 +20755,27 @@ }, "minimalistic-assert": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, "minimalistic-crypto-utils": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" }, "os-browserify": { "version": "0.3.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=" }, "pako": { "version": "1.0.10", - "resolved": false, + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==" }, "parse-asn1": { "version": "5.1.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.4.tgz", "integrity": "sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw==", "requires": { "asn1.js": "^4.0.0", @@ -20463,12 +20788,12 @@ }, "path-browserify": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.0.tgz", "integrity": "sha512-Hkavx/nY4/plImrZPHRk2CL9vpOymZLgEbMNX1U0bjcBL7QN9wODxyx0yaMZURSQaUtSEvDrfAvxa9oPb0at9g==" }, "pbkdf2": { "version": "3.0.17", - "resolved": false, + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", "requires": { "create-hash": "^1.1.2", @@ -20480,17 +20805,17 @@ }, "process": { "version": "0.11.10", - "resolved": false, + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" }, "process-nextick-args": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" }, "public-encrypt": { "version": "4.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", "requires": { "bn.js": "^4.1.0", @@ -20503,22 +20828,22 @@ }, "punycode": { "version": "2.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "querystring": { "version": "0.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" }, "querystring-es3": { "version": "0.2.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" }, "randombytes": { "version": "2.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "requires": { "safe-buffer": "^5.1.0" @@ -20526,7 +20851,7 @@ }, "randomfill": { "version": "1.0.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", "requires": { "randombytes": "^2.0.5", @@ -20535,7 +20860,7 @@ }, "readable-stream": { "version": "3.3.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.3.0.tgz", "integrity": "sha512-EsI+s3k3XsW+fU8fQACLN59ky34AZ14LoeVZpYwmZvldCFo0r0gnelwF2TcMjLor/BTL5aDJVBMkss0dthToPw==", "requires": { "inherits": "^2.0.3", @@ -20545,14 +20870,14 @@ "dependencies": { "inherits": { "version": "2.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" } } }, "ripemd160": { "version": "2.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", "requires": { "hash-base": "^3.0.0", @@ -20561,17 +20886,17 @@ }, "safe-buffer": { "version": "5.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "setimmediate": { "version": "1.0.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" }, "sha.js": { "version": "2.4.11", - "resolved": false, + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "requires": { "inherits": "^2.0.1", @@ -20580,7 +20905,7 @@ }, "stream-browserify": { "version": "2.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", "requires": { "inherits": "~2.0.1", @@ -20589,7 +20914,7 @@ "dependencies": { "readable-stream": { "version": "2.3.6", - "resolved": false, + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -20603,14 +20928,14 @@ "dependencies": { "inherits": { "version": "2.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" } } }, "string_decoder": { "version": "1.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" @@ -20620,7 +20945,7 @@ }, "stream-http": { "version": "3.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.0.0.tgz", "integrity": "sha512-JELJfd+btL9GHtxU3+XXhg9NLYrKFnhybfvRuDghtyVkOFydz3PKNT1df07AMr88qW03WHF+FSV0PySpXignCA==", "requires": { "builtin-status-codes": "^3.0.0", @@ -20631,7 +20956,7 @@ }, "string_decoder": { "version": "1.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", "requires": { "safe-buffer": "~5.1.0" @@ -20639,7 +20964,7 @@ }, "timers-browserify": { "version": "2.0.10", - "resolved": false, + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.10.tgz", "integrity": "sha512-YvC1SV1XdOUaL6gx5CoGroT3Gu49pK9+TZ38ErPldOWW4j49GI1HKs9DV+KGq/w6y+LZ72W1c8cKz2vzY+qpzg==", "requires": { "setimmediate": "^1.0.4" @@ -20647,12 +20972,12 @@ }, "tty-browserify": { "version": "0.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==" }, "url": { "version": "0.11.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", "requires": { "punycode": "1.3.2", @@ -20661,14 +20986,14 @@ "dependencies": { "punycode": { "version": "1.3.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" } } }, "util": { "version": "0.11.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", "requires": { "inherits": "2.0.3" @@ -20676,24 +21001,24 @@ "dependencies": { "inherits": { "version": "2.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" } } }, "util-deprecate": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "vm-browserify": { "version": "1.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.0.tgz", "integrity": "sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==" }, "xtend": { "version": "4.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" } } @@ -24146,6 +24471,14 @@ "integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=", "dev": true }, + "raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "requires": { + "performance-now": "^2.1.0" + } + }, "ramda": { "version": "0.21.0", "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.21.0.tgz", @@ -25038,8 +25371,35 @@ "react-lifecycles-compat": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", - "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", - "dev": true + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "react-measure": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/react-measure/-/react-measure-2.3.0.tgz", + "integrity": "sha512-dwAvmiOeblj5Dvpnk8Jm7Q8B4THF/f1l1HtKVi0XDecsG6LXwGvzV5R1H32kq3TW6RW64OAf5aoQxpIgLa4z8A==", + "requires": { + "@babel/runtime": "^7.2.0", + "get-node-dimensions": "^1.2.1", + "prop-types": "^15.6.2", + "resize-observer-polyfill": "^1.5.0" + } + }, + "react-motion": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/react-motion/-/react-motion-0.5.2.tgz", + "integrity": "sha512-9q3YAvHoUiWlP3cK0v+w1N5Z23HXMj4IF4YuvjvWegWqNPfLXsOBE/V7UvQGpXxHFKRQQcNcVQE31g9SB/6qgQ==", + "requires": { + "performance-now": "^0.2.0", + "prop-types": "^15.5.8", + "raf": "^3.1.0" + }, + "dependencies": { + "performance-now": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=" + } + } }, "react-popper": { "version": "1.3.7", @@ -25303,6 +25663,26 @@ "resolve": "^1.1.6" } }, + "recompose": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/recompose/-/recompose-0.30.0.tgz", + "integrity": "sha512-ZTrzzUDa9AqUIhRk4KmVFihH0rapdCSMFXjhHbNrjAWxBuUD/guYlyysMnuHjlZC/KRiOKRtB4jf96yYSkKE8w==", + "requires": { + "@babel/runtime": "^7.0.0", + "change-emitter": "^0.1.2", + "fbjs": "^0.8.1", + "hoist-non-react-statics": "^2.3.1", + "react-lifecycles-compat": "^3.0.2", + "symbol-observable": "^1.0.4" + }, + "dependencies": { + "hoist-non-react-statics": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz", + "integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==" + } + } + }, "recursive-readdir": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz", @@ -25740,8 +26120,7 @@ "resize-observer-polyfill": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", - "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", - "dev": true + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" }, "resolve": { "version": "1.8.1", @@ -26137,8 +26516,7 @@ "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", - "dev": true + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" }, "setprototypeof": { "version": "1.1.0", @@ -27937,8 +28315,7 @@ "symbol-observable": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", - "dev": true + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" }, "symbol-tree": { "version": "3.2.2", diff --git a/package.json b/package.json index 823a7669080..86296c06c16 100644 --- a/package.json +++ b/package.json @@ -124,10 +124,14 @@ "@google-cloud/language": "^3.7.0", "@google-cloud/storage": "^2.3.1", "@google-cloud/vision": "^1.8.0", + "@nivo/bar": "^0.61.1", + "@nivo/heatmap": "^0.61.0", + "@nivo/line": "^0.61.1", + "@nivo/pie": "^0.61.1", "@rocket.chat/apps-engine": "^1.13.0-beta.2857", - "@rocket.chat/fuselage": "^0.3.0", - "@rocket.chat/fuselage-hooks": "^0.3.0", - "@rocket.chat/fuselage-ui-kit": "^0.3.0", + "@rocket.chat/fuselage": "^0.6.0", + "@rocket.chat/fuselage-hooks": "^0.6.0", + "@rocket.chat/fuselage-ui-kit": "^0.6.0", "@rocket.chat/icons": "^0.3.0", "@rocket.chat/ui-kit": "^0.3.0", "@slack/client": "^4.8.0", diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 0fe71603ed5..379d32dd969 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -223,6 +223,13 @@ "Accounts_UserAddedEmailSubject_Default": "You have been added to [Site_Name]", "Accounts_UserAddedEmail_Description": "You may use the following placeholders:
  • [name], [fname], [lname] for the user's full name, first name or last name, respectively.
  • [email] for the user's email.
  • [password] for the user's password.
  • [Site_Name] and [Site_URL] for the Application Name and URL respectively.
", "Activate": "Activate", + "Active_users": "Active users", + "Daily_Active_Users": "Daily Active Users", + "Weekly_Active_Users": "Weekly Active Users", + "Monthly_Active_Users": "Monthly Active Users", + "DAU_value": "DAU __value__", + "WAU_value": "WAU __value__", + "MAU_value": "MAU __value__", "Activity": "Activity", "Add": "Add", "add-oauth-service": "Add Oauth Service", @@ -1011,6 +1018,7 @@ "Create_A_New_Channel": "Create a New Channel", "Create_new": "Create new", "Create_unique_rules_for_this_channel": "Create unique rules for this channel", + "Created": "Created", "Created_at": "Created at", "Created_at_s_by_s": "Created at %s by %s", "Created_at_s_by_s_triggered_by_s": "Created at %s by %s triggered by %s", @@ -1074,6 +1082,7 @@ "Date_From": "From", "Date_to": "to", "days": "days", + "Days": "Days", "DB_Migration": "Database Migration", "DB_Migration_Date": "Database Migration Date", "DDP_Rate_Limit_IP_Enabled": "Limit by IP: enabled", @@ -1271,6 +1280,7 @@ "Emoji_provided_by_JoyPixels": "Emoji provided by JoyPixels", "EmojiCustomFilesystem": "Custom Emoji Filesystem", "Empty_title": "Empty title", + "Engagement_Dashboard": "Engagement Dashboard", "Enable": "Enable", "Enable_Auto_Away": "Enable Auto Away", "Enable_Desktop_Notifications": "Enable Desktop Notifications", @@ -1890,6 +1900,7 @@ "IssueLinks_Incompatible": "Warning: do not enable this and the 'Hex Color Preview' at the same time.", "IssueLinks_LinkTemplate": "Template for issue links", "IssueLinks_LinkTemplate_Description": "Template for issue links; %s will be replaced by the issue number.", + "Items_per_page:": "Items per page:", "It_works": "It works", "italic": "Italic", "italics": "italics", @@ -1903,6 +1914,7 @@ "Mobex_sms_gateway_restful_address": "Mobex SMS REST API Address", "Mobex_sms_gateway_restful_address_desc": "IP or Host of your Mobex REST API. E.g. `http://192.168.1.1:8080` or `https://www.example.com:8080`", "Mobex_sms_gateway_username": "Username", + "Most_popular_channels_top_5": "Most popular channels (Top 5)", "Jitsi_Chrome_Extension": "Chrome Extension Id", "Jitsi_Enabled_TokenAuth": "Enable JWT auth", "Jitsi_Application_ID": "Application ID (iss)", @@ -1961,6 +1973,7 @@ "Language_Russian": "Russian", "Language_Spanish": "Spanish", "Language_Version": "English Version", + "Last_active": "Last active", "Last_login": "Last login", "Last_Message_At": "Last Message At", "Last_seen": "Last seen", @@ -1968,6 +1981,9 @@ "Last_Message": "Last Message", "Last_Status": "Last Status", "Last_Updated": "Last Updated", + "Last_7_days": "Last 7 Days", + "Last_30_days": "Last 30 Days", + "Last_90_days": "Last 90 Days", "Launched_successfully": "Launched successfully", "Layout": "Layout", "Layout_Home_Body": "Home Body", @@ -2339,6 +2355,7 @@ "Message": "Message", "messages": "messages", "Messages": "Messages", + "Messages_sent": "Messages sent", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "Messages that are sent to the Incoming WebHook will be posted here.", "Meta": "Meta", "Meta_custom": "Custom Meta Tags", @@ -2398,6 +2415,7 @@ "Name_optional": "Name (optional)", "Name_Placeholder": "Please enter your name...", "Navigation_History": "Navigation History", + "New_users": "New users", "New_Application": "New Application", "New_chat_transfer": "New Chat Transfer: __transfer__", "New_Custom_Field": "New Custom Field", @@ -2447,11 +2465,13 @@ "No_discussions_yet": "No discussions yet", "No_Threads": "No threads found", "No_user_with_username_%s_was_found": "No user with username \"%s\" was found!", + "No_data_found": "No data found", "Nobody_available": "Nobody available", "Node_version": "Node Version", "None": "None", "Nonprofit": "Nonprofit", "Normal": "Normal", + "Not_enough_data": "Not enough data", "Not_authorized": "Not authorized", "Not_Available": "Not Available", "Not_found_or_not_allowed": "Not Found or Not Allowed", @@ -2634,6 +2654,8 @@ "Privacy_Policy": "Privacy Policy", "Private": "Private", "Private_Channel": "Private Channel", + "Private_Channels": "Private Channels", + "Private_Chats": "Private Chats", "Private_Group": "Private Group", "Private_Groups": "Private Groups", "Private_Groups_list": "List of Private Groups", @@ -2661,6 +2683,7 @@ "files_pruned": "files pruned", "Public": "Public", "Public_Channel": "Public Channel", + "Public_Channels": "Public Channels", "Public_Community": "Public Community", "Public_Relations": "Public Relations", "Public_URL": "Public URL", @@ -3493,6 +3516,7 @@ "Username_wants_to_start_otr_Do_you_want_to_accept": "__username__ wants to start OTR. Do you want to accept?", "Users": "Users", "Users_added": "The users have been added", + "Users_by_time_of_day": "Users by time of day", "Users_in_role": "Users in role", "Users must use Two Factor Authentication": "Users must use Two Factor Authentication", "Leave_the_description_field_blank_if_you_dont_want_to_show_the_role": "Leave the description field blank if you don't want to show the role", @@ -3501,6 +3525,8 @@ "UTF8_Names_Slugify": "UTF8 Names Slugify", "UTF8_Names_Validation": "UTF8 Names Validation", "UTF8_Names_Validation_Description": "RegExp that will be used to validate usernames and channel names", + "Value_messages": "__value__ messages", + "Value_users": "__value__ users", "Validate_email_address": "Validate Email Address", "Verification_email_body": "You have succesfully created an account on [Site_Name]. Please, click on the button below to confirm your email address and finish registration.", "Verification": "Verification", @@ -3603,6 +3629,8 @@ "Welcome": "Welcome %s.", "Welcome_to": "Welcome to __Site_Name__", "Welcome_to_the": "Welcome to the", + "Where_are_the_messages_being_sent?": "Where are the messages being sent?", + "When_is_the_chat_busier?": "When is the chat busier?", "Why_do_you_want_to_report_question_mark": "Why do you want to report?", "will_be_able_to": "will be able to", "Worldwide": "Worldwide", diff --git a/server/main.js b/server/main.js index 72ffa6273ff..b096109e3e9 100644 --- a/server/main.js +++ b/server/main.js @@ -3,6 +3,7 @@ import '../imports/startup/server'; import '../lib/RegExp'; +import '../ee/server'; import './lib/accounts'; import './lib/cordova'; import './lib/roomFiles'; @@ -74,5 +75,3 @@ import './routes/avatar'; import './stream/messages'; import './stream/rooms'; import './stream/streamBroadcast'; - -import '../ee/server';