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';