[NEW] Seats Cap (#23017)

* Base commit

* [NEW] licenses.maxActiveUsers endpoint (#23011)

* [IMPROVE] Banner Service (#22989)

* WIP

* Fix type import

Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat>

* [NEW] canAddNewUser function

* [NEW] Seats usage bar (#23018)

* usage bar component

* Seats limit in admin users page

* Remove dangling console.log

* Add some details on StatisticsEndpoint type

* Move to EE and use new endpoint

* Rename some components and hooks

* Refactor UsersPage

Co-authored-by: Gabriel Henriques <gabriel.henriques@rocket.chat>

* Fix edit and info page

* [NEW] Seats Card (#23077)

* Seats Card

* Fix review, make ts

* Fix review

* Add type guard for CardIcon props

Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat>

* [NEW] Seats Cap: Request seats link (#23151)

* create endpoint and consume it in the ui

* Fix review

* Remove unused param type

Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat>

* [NEW] Remove license downgrade if exceeding seats cap (#23220)

* [IMPROVE] Ensure Seats-cap design and UI are the same (#23222)

* Fix labels and buttons

* Reload seats cap data on user changes

* Use Fuselage on development version

Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat>

* [NEW] Prevent users from accidentally deactivating an enterprise license by adding more users than the license allows. (#23050)

Co-authored-by: Gabriel Henriques <gabriel.henriques@rocket.chat>
Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat>

* [NEW] stats on seats request  (#23225)

Co-authored-by: Gabriel Henriques <gabriel.henriques@rocket.chat>

* [NEW] Seats cap banners (#23211)

* [NEW] Prevent users from accidentally deactivating an enterprise license by adding more users than the license allows.

* Seats cap banners

* Deprecate preserveDismiss

* use request seats link

* Fix banner not closing and request seats link

Co-authored-by: Pierre Lehnen <pierre.lehnen@rocket.chat>

* [FIX] Banner not parsing markdown (#23036)

* Parse markdown

* Use markdownText

* Fix translations

* Move startup

* Always create seats limit banners

* Remove uneffective conditional

* [FIX] Seats Cap QA reports (#23272)

* Fix create banner and link

* Remove call from startup

* QA

* Improve readability

* Avoid using an outdated absolute URL

* Embedded counters into translation strings

Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat>

* Patch object-path so Snyk stop complaining

Co-authored-by: pierre-lehnen-rc <55164754+pierre-lehnen-rc@users.noreply.github.com>
Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com>
Co-authored-by: Pierre Lehnen <pierre.lehnen@rocket.chat>
Co-authored-by: Gabriel Henriques <gabriel.henriques@rocket.chat>
Co-authored-by: gabriellsh <40830821+gabriellsh@users.noreply.github.com>
Co-authored-by: Gabriel Thomé <38537062+g-thome@users.noreply.github.com>
pull/23037/head^2
Tasso Evangelista 4 years ago committed by GitHub
parent f993a6cb4b
commit 558bab0380
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 124
      app/api/server/v1/banners.ts
  2. 1
      app/authentication/server/startup/index.js
  3. 4
      app/crowd/server/crowd.js
  4. 2
      app/dolphin/lib/common.js
  5. 118
      app/lib/server/functions/saveUser.js
  6. 13
      app/lib/server/functions/setUserActiveStatus.js
  7. 22
      app/meteor-accounts-saml/server/loginHandler.ts
  8. 31
      app/models/server/raw/Banners.ts
  9. 3
      app/statistics/server/lib/statistics.js
  10. 35
      app/ui-login/client/login/form.js
  11. 11
      client/components/Card/CardIcon.tsx
  12. 8
      client/components/UserStatus/UserStatus.tsx
  13. 13
      client/contexts/ServerContext/ServerContext.ts
  14. 4
      client/contexts/ServerContext/endpoints.ts
  15. 13
      client/contexts/ServerContext/endpoints/v1/licenses.ts
  16. 7
      client/contexts/ServerContext/endpoints/v1/statistics.ts
  17. 11
      client/hooks/useLocalePercentage.ts
  18. 14
      client/lib/getLocalePercentage.ts
  19. 15
      client/startup/banners.ts
  20. 53
      client/views/admin/info/DeploymentCard.tsx
  21. 117
      client/views/admin/info/InformationPage.tsx
  22. 43
      client/views/admin/info/InformationRoute.tsx
  23. 43
      client/views/admin/info/InstancesCard.js
  24. 23
      client/views/admin/info/LicenseCard.js
  25. 2
      client/views/admin/info/NewInformationPage.js
  26. 40
      client/views/admin/info/PushCard.js
  27. 80
      client/views/admin/info/UsageCard.tsx
  28. 67
      client/views/admin/info/UsagePieGraph.js
  29. 100
      client/views/admin/info/UsagePieGraph.tsx
  30. 4
      client/views/admin/users/AddUser.js
  31. 4
      client/views/admin/users/EditUser.js
  32. 11
      client/views/admin/users/UserInfo.js
  33. 4
      client/views/admin/users/UserInfoActions.js
  34. 33
      client/views/admin/users/UserPageHeaderContent.tsx
  35. 62
      client/views/admin/users/UsersPage.js
  36. 18
      client/views/banners/UiKitBanner.tsx
  37. 10
      definition/IBanner.ts
  38. 21
      definition/IInstance.ts
  39. 22
      definition/IServerInfo.ts
  40. 78
      definition/IStats.ts
  41. 2
      definition/utils.ts
  42. 30
      ee/app/license/server/getSeatsRequestLink.ts
  43. 4
      ee/app/license/server/getStatistics.ts
  44. 32
      ee/app/license/server/license.ts
  45. 116
      ee/app/license/server/maxSeatsBanners.ts
  46. 67
      ee/client/views/admin/info/SeatsCard.tsx
  47. 51
      ee/client/views/admin/users/CloseToSeatsCapModal.tsx
  48. 52
      ee/client/views/admin/users/ReachedSeatsCapModal.tsx
  49. 12
      ee/client/views/admin/users/SeatsCapUsage/SeatsCapUsage.stories.tsx
  50. 44
      ee/client/views/admin/users/SeatsCapUsage/SeatsCapUsage.tsx
  51. 1
      ee/client/views/admin/users/SeatsCapUsage/index.ts
  52. 12
      ee/client/views/admin/users/SeatsCapUsage/useUsageLabel.ts
  53. 129
      ee/client/views/admin/users/UserPageHeaderContentWithSeatsCap.tsx
  54. 3
      ee/client/views/admin/users/useRequestSeatsLink.ts
  55. 21
      ee/client/views/admin/users/useSeatsCap.ts
  56. 13
      ee/server/api/licenses.ts
  57. 2
      ee/server/index.js
  58. 17
      ee/server/requestSeatsRoute.ts
  59. 1
      ee/server/startup/index.ts
  60. 109
      ee/server/startup/seatsCap.ts
  61. 601
      package-lock.json
  62. 24
      package.json
  63. 22
      packages/rocketchat-i18n/i18n/en.i18n.json
  64. 3
      server/methods/deleteUser.js
  65. 23
      server/methods/registerUser.js
  66. 4
      server/methods/setUserActiveStatus.js
  67. 9
      server/modules/listeners/listeners.module.ts
  68. 2
      server/sdk/index.ts
  69. 2
      server/sdk/lib/Events.ts
  70. 7
      server/sdk/types/IAnalyticsService.ts
  71. 9
      server/sdk/types/IBannerService.ts
  72. 29
      server/services/analytics/service.ts
  73. 62
      server/services/banner/service.ts
  74. 2
      server/services/startup.ts
  75. 2
      server/startup/migrations/v232.ts

@ -1,12 +1,13 @@
import { Promise } from 'meteor/promise';
import { Meteor } from 'meteor/meteor';
import { Match, check } from 'meteor/check';
import { TextObjectType, BlockType } from '@rocket.chat/apps-engine/definition/uikit';
import { API } from '../api';
import { Banner } from '../../../../server/sdk';
import { BannerPlatform } from '../../../../definition/IBanner';
import { BannerPlatform, IBanner } from '../../../../definition/IBanner';
API.v1.addRoute('banners.getNew', { authRequired: true }, {
API.v1.addRoute('banners.getNew', { authRequired: true }, { // deprecated
get() {
check(this.queryParams, Match.ObjectIncluding({
platform: String,
@ -22,12 +23,129 @@ API.v1.addRoute('banners.getNew', { authRequired: true }, {
throw new Meteor.Error('error-unknown-platform', 'Platform is unknown.');
}
const banners = Promise.await(Banner.getNewBannersForUser(this.userId, platform, bannerId));
const banners = Promise.await(Banner.getBannersForUser(this.userId, platform, bannerId));
return API.v1.success({ banners });
},
});
API.v1.addRoute('banners/:id', { authRequired: true }, {
get() {
check(this.urlParams, Match.ObjectIncluding({
id: String,
}));
const { platform } = this.queryParams;
if (!platform) {
throw new Meteor.Error('error-missing-param', 'The required "platform" param is missing.');
}
const { id } = this.urlParams;
if (!id) {
throw new Meteor.Error('error-missing-param', 'The required "id" param is missing.');
}
const banners = Promise.await(Banner.getBannersForUser(this.userId, platform, id));
return API.v1.success({ banners });
},
});
API.v1.addRoute('banners', { authRequired: true }, {
get() {
check(this.queryParams, Match.ObjectIncluding({
platform: String,
}));
const { platform } = this.queryParams;
if (!platform) {
throw new Meteor.Error('error-missing-param', 'The required "platform" param is missing.');
}
if (!Object.values(BannerPlatform).includes(platform)) {
throw new Meteor.Error('error-unknown-platform', 'Platform is unknown.');
}
const banners = Promise.await(Banner.getBannersForUser(this.userId, platform));
return API.v1.success({ banners });
},
...process.env.NODE_ENV !== 'production' && {
post(): {} {
check(this.bodyParams, Match.ObjectIncluding({
platform: Match.Maybe(String),
bid: String,
}));
const { platform = 'web', bid: bannerId } = this.bodyParams;
if (!platform) {
throw new Meteor.Error('error-missing-param', 'The required "platform" param is missing.');
}
if (!Object.values(BannerPlatform).includes(platform)) {
throw new Meteor.Error('error-unknown-platform', 'Platform is unknown.');
}
const b: IBanner = {
_id: bannerId,
platform: [platform],
expireAt: new Date(new Date().getTime() + (1000 * 60 * 60 * 24 * 7)),
startAt: new Date(),
roles: ['admin'],
createdBy: {
_id: this.userId,
username: this.userId,
},
createdAt: new Date(),
_updatedAt: new Date(),
view: {
viewId: '',
appId: '',
blocks: [{
type: BlockType.SECTION,
blockId: 'attention',
text: {
type: TextObjectType.PLAINTEXT,
text: 'Test',
emoji: false,
},
}],
},
};
const banners = Promise.await(Banner.create(b));
return API.v1.success({ banners });
},
delete(): {} {
check(this.bodyParams, Match.ObjectIncluding({
bid: String,
}));
const { bid } = this.bodyParams;
Promise.await(Banner.disable(bid));
return API.v1.success();
},
patch(): {} {
check(this.bodyParams, Match.ObjectIncluding({
bid: String,
}));
const { bid } = this.bodyParams;
Promise.await(Banner.enable(bid));
return API.v1.success();
},
},
});
API.v1.addRoute('banners.dismiss', { authRequired: true }, {
post() {
check(this.bodyParams, Match.ObjectIncluding({

@ -206,6 +206,7 @@ Accounts.onCreateUser(function(options, user = {}) {
Mailer.send(email);
}
callbacks.run('onCreateUser', options, user);
return user;
});

@ -10,6 +10,7 @@ import { Users } from '../../models';
import { settings } from '../../settings';
import { hasRole } from '../../authorization';
import { deleteUser } from '../../lib/server/functions';
import { setUserActiveStatus } from '../../lib/server/functions/setUserActiveStatus';
const logger = new Logger('CROWD');
@ -154,7 +155,6 @@ export class CROWD {
address: crowdUser.email,
verified: settings.get('Accounts_Verify_Email_For_External_Accounts'),
}],
active: crowdUser.active,
crowd: true,
};
@ -173,6 +173,8 @@ export class CROWD {
Meteor.users.update(id, {
$set: user,
});
setUserActiveStatus(id, crowdUser.active);
}
sync() {

@ -25,7 +25,7 @@ function DolphinOnCreateUser(options, user) {
if (user && user.services && user.services.dolphin && user.services.dolphin.NickName) {
user.username = user.services.dolphin.NickName;
}
return user;
return options;
}
if (Meteor.isServer) {

@ -9,10 +9,10 @@ import { getRoles, hasPermission } from '../../../authorization';
import { settings } from '../../../settings';
import { passwordPolicy } from '../lib/passwordPolicy';
import { validateEmailDomain } from '../lib';
import { validateUserRoles } from '../../../../ee/app/authorization/server/validateUserRoles';
import { getNewUserRoles } from '../../../../server/services/user/lib/getNewUserRoles';
import { saveUserIdentity } from './saveUserIdentity';
import { checkEmailAvailability, checkUsernameAvailability, setUserAvatar, setEmail, setStatusText } from '.';
import { callbacks } from '../../../callbacks/server';
let html = '';
let passwordChangedHtml = '';
@ -98,7 +98,7 @@ function validateUserData(userId, userData) {
}
if (userData.roles) {
validateUserRoles(userId, userData);
callbacks.run('validateUserRoles', userData);
}
let nameValidation;
@ -227,77 +227,85 @@ const handleNickname = (updateUser, nickname) => {
}
};
export const saveUser = function(userId, userData) {
validateUserData(userId, userData);
let sendPassword = false;
const saveNewUser = function(userData, sendPassword) {
validateEmailDomain(userData.email);
if (userData.hasOwnProperty('setRandomPassword')) {
if (userData.setRandomPassword) {
userData.password = passwordPolicy.generatePassword();
userData.requirePasswordChange = true;
sendPassword = true;
}
const roles = userData.roles || getNewUserRoles();
const isGuest = roles && roles.length === 1 && roles.includes('guest');
delete userData.setRandomPassword;
// insert user
const createUser = {
username: userData.username,
password: userData.password,
joinDefaultChannels: userData.joinDefaultChannels,
isGuest,
};
if (userData.email) {
createUser.email = userData.email;
}
if (!userData._id) {
validateEmailDomain(userData.email);
const _id = Accounts.createUser(createUser);
// insert user
const createUser = {
username: userData.username,
password: userData.password,
joinDefaultChannels: userData.joinDefaultChannels,
};
if (userData.email) {
createUser.email = userData.email;
}
const updateUser = {
$set: {
roles,
...typeof userData.name !== 'undefined' && { name: userData.name },
settings: userData.settings || {},
},
};
const _id = Accounts.createUser(createUser);
if (typeof userData.requirePasswordChange !== 'undefined') {
updateUser.$set.requirePasswordChange = userData.requirePasswordChange;
}
if (typeof userData.verified === 'boolean') {
updateUser.$set['emails.0.verified'] = userData.verified;
}
const updateUser = {
$set: {
roles: userData.roles || getNewUserRoles(),
...typeof userData.name !== 'undefined' && { name: userData.name },
settings: userData.settings || {},
},
};
handleBio(updateUser, userData.bio);
handleNickname(updateUser, userData.nickname);
if (typeof userData.requirePasswordChange !== 'undefined') {
updateUser.$set.requirePasswordChange = userData.requirePasswordChange;
}
Meteor.users.update({ _id }, updateUser);
if (typeof userData.verified === 'boolean') {
updateUser.$set['emails.0.verified'] = userData.verified;
}
if (userData.sendWelcomeEmail) {
_sendUserEmail(settings.get('Accounts_UserAddedEmail_Subject'), html, userData);
}
handleBio(updateUser, userData.bio);
handleNickname(updateUser, userData.nickname);
if (sendPassword) {
_sendUserEmail(settings.get('Password_Changed_Email_Subject'), passwordChangedHtml, userData);
}
Meteor.users.update({ _id }, updateUser);
userData._id = _id;
if (userData.sendWelcomeEmail) {
_sendUserEmail(settings.get('Accounts_UserAddedEmail_Subject'), html, userData);
}
if (settings.get('Accounts_SetDefaultAvatar') === true && userData.email) {
const gravatarUrl = Gravatar.imageUrl(userData.email, { default: '404', size: 200, secure: true });
if (sendPassword) {
_sendUserEmail(settings.get('Password_Changed_Email_Subject'), passwordChangedHtml, userData);
try {
setUserAvatar(userData, gravatarUrl, '', 'url');
} catch (e) {
// Ignore this error for now, as it not being successful isn't bad
}
}
userData._id = _id;
return _id;
};
if (settings.get('Accounts_SetDefaultAvatar') === true && userData.email) {
const gravatarUrl = Gravatar.imageUrl(userData.email, { default: '404', size: 200, secure: true });
export const saveUser = function(userId, userData) {
validateUserData(userId, userData);
let sendPassword = false;
try {
setUserAvatar(userData, gravatarUrl, '', 'url');
} catch (e) {
// Ignore this error for now, as it not being successful isn't bad
}
if (userData.hasOwnProperty('setRandomPassword')) {
if (userData.setRandomPassword) {
userData.password = passwordPolicy.generatePassword();
userData.requirePasswordChange = true;
sendPassword = true;
}
return _id;
delete userData.setRandomPassword;
}
if (!userData._id) {
return saveNewUser(userData, sendPassword);
}
validateUserEditing(userId, userData);
@ -356,6 +364,8 @@ export const saveUser = function(userId, userData) {
Meteor.users.update({ _id: userData._id }, updateUser);
callbacks.run('afterSaveUser', userData);
if (sendPassword) {
_sendUserEmail(settings.get('Password_Changed_Email_Subject'), passwordChangedHtml, userData);
}

@ -5,6 +5,7 @@ import { Accounts } from 'meteor/accounts-base';
import * as Mailer from '../../../mailer';
import { Users, Subscriptions, Rooms } from '../../../models';
import { settings } from '../../../settings';
import { callbacks } from '../../../callbacks/server';
import { relinquishRoomOwnerships } from './relinquishRoomOwnerships';
import { closeOmnichannelConversations } from './closeOmnichannelConversations';
import { shouldRemoveOrChangeOwner, getSubscribedRoomsForUserWithDetails } from './getRoomsWithSingleOwner';
@ -55,8 +56,20 @@ export function setUserActiveStatus(userId, active, confirmRelinquish = false) {
relinquishRoomOwnerships(user, chatSubscribedRooms, false);
}
if (active && !user.active) {
callbacks.run('beforeActivateUser', user);
}
Users.setUserActive(userId, active);
if (active && !user.active) {
callbacks.run('afterActivateUser', user);
}
if (!active && user.active) {
callbacks.run('afterDeactivateUser', user);
}
if (user.username) {
Subscriptions.setArchivedByUsername(user.username, !active);
}

@ -1,5 +1,6 @@
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { SAMLUtils } from './lib/Utils';
import { SAML } from './lib/SAML';
@ -31,8 +32,25 @@ Accounts.registerLoginHandler('saml', function(loginRequest) {
const userObject = SAMLUtils.mapProfileToUserObject(loginResult.profile);
return SAML.insertOrUpdateSAMLUser(userObject);
} catch (error) {
} catch (error: any) {
SystemLogger.error(error);
return makeError(error.toString());
let message = error.toString();
let errorCode = '';
if (error instanceof Meteor.Error) {
errorCode = (error.error || error.message) as string;
} else if (error instanceof Error) {
errorCode = error.message;
}
if (errorCode) {
const localizedMessage = TAPi18n.__(errorCode);
if (localizedMessage && localizedMessage !== errorCode) {
message = localizedMessage;
}
}
return makeError(message);
}
});

@ -1,4 +1,4 @@
import { Collection, Cursor, FindOneOptions, WithoutProjection } from 'mongodb';
import { Collection, Cursor, FindOneOptions, UpdateWriteOpResult, WithoutProjection, InsertOneWriteOpResult } from 'mongodb';
import { BannerPlatform, IBanner } from '../../../../definition/IBanner';
import { BaseRaw } from './BaseRaw';
@ -14,6 +14,30 @@ export class BannersRaw extends BaseRaw<T> {
this.col.createIndexes([
{ key: { platform: 1, startAt: 1, expireAt: 1 } },
]);
this.col.createIndexes([
{ key: { platform: 1, startAt: 1, expireAt: 1, active: 1 } },
]);
}
create(doc: IBanner): Promise<InsertOneWriteOpResult<IBanner>> {
const invalidPlatform = doc.platform?.some((platform) => !Object.values(BannerPlatform).includes(platform));
if (invalidPlatform) {
throw new Error('Invalid platform');
}
if (doc.startAt > doc.expireAt) {
throw new Error('Start date cannot be later than expire date');
}
if (doc.expireAt < new Date()) {
throw new Error('Cannot create banner already expired');
}
return this.insertOne({
active: true,
...doc,
});
}
findActiveByRoleOrId(roles: string[], platform: BannerPlatform, bannerId?: string, options?: WithoutProjection<FindOneOptions<T>>): Cursor<T> {
@ -24,6 +48,7 @@ export class BannersRaw extends BaseRaw<T> {
platform,
startAt: { $lte: today },
expireAt: { $gte: today },
active: { $ne: false },
$or: [
{ roles: { $in: roles } },
{ roles: { $exists: false } },
@ -32,4 +57,8 @@ export class BannersRaw extends BaseRaw<T> {
return this.col.find(query, options);
}
disable(bannerId: string): Promise<UpdateWriteOpResult> {
return this.col.updateOne({ _id: bannerId, active: { $ne: false } }, { $set: { active: false, inactivedAt: new Date() } });
}
}

@ -25,7 +25,7 @@ import { readSecondaryPreferred } from '../../../../server/database/readSecondar
import { getAppsStatistics } from './getAppsStatistics';
import { getServicesStatistics } from './getServicesStatistics';
import { getStatistics as getEnterpriseStatistics } from '../../../../ee/app/license/server';
import { Team } from '../../../../server/sdk';
import { Team, Analytics } from '../../../../server/sdk';
const wizardFields = [
'Organization_Type',
@ -211,6 +211,7 @@ export const statistics = {
statistics.pushQueue = Promise.await(NotificationQueue.col.estimatedDocumentCount());
statistics.enterprise = getEnterpriseStatistics();
Promise.await(Analytics.resetSeatRequestCount());
return statistics;
},

@ -141,21 +141,26 @@ Template.loginForm.events({
return Meteor[loginMethod](s.trim(formData.emailOrUsername), formData.pass, function(error) {
instance.loading.set(false);
if (error != null) {
if (error.error === 'error-user-is-not-activated') {
return toastr.error(t('Wait_activation_warning'));
} if (error.error === 'error-invalid-email') {
instance.typedEmail = formData.emailOrUsername;
return instance.state.set('email-verification');
} if (error.error === 'error-user-is-not-activated') {
toastr.error(t('Wait_activation_warning'));
} else if (error.error === 'error-app-user-is-not-allowed-to-login') {
toastr.error(t('App_user_not_allowed_to_login'));
} else if (error.error === 'error-login-blocked-for-ip') {
toastr.error(t('Error_login_blocked_for_ip'));
} else if (error.error === 'error-login-blocked-for-user') {
toastr.error(t('Error_login_blocked_for_user'));
} else {
return toastr.error(t('User_not_found_or_incorrect_password'));
switch (error.error) {
case 'error-user-is-not-activated':
return toastr.error(t('Wait_activation_warning'));
case 'error-invalid-email':
instance.typedEmail = formData.emailOrUsername;
return instance.state.set('email-verification');
case 'error-app-user-is-not-allowed-to-login':
toastr.error(t('App_user_not_allowed_to_login'));
break;
case 'error-login-blocked-for-ip':
toastr.error(t('Error_login_blocked_for_ip'));
break;
case 'error-login-blocked-for-user':
toastr.error(t('Error_login_blocked_for_user'));
break;
case 'error-license-user-limit-reached':
toastr.error(t('error-license-user-limit-reached'));
break;
default:
return toastr.error(t('User_not_found_or_incorrect_password'));
}
}
Session.set('forceLogin', false);

@ -1,7 +1,12 @@
import { Box, Icon } from '@rocket.chat/fuselage';
import React, { FC } from 'react';
import React, { ComponentProps, ReactElement, ReactNode } from 'react';
const CardIcon: FC<{ name: string }> = ({ name, children, ...props }) => (
type CardIconProps = { children: ReactNode } | ComponentProps<typeof Icon>;
const hasChildrenProp = (props: CardIconProps): props is { children: ReactNode } =>
'children' in props;
const CardIcon = (props: CardIconProps): ReactElement => (
<Box
minWidth='x16'
display='inline-flex'
@ -9,7 +14,7 @@ const CardIcon: FC<{ name: string }> = ({ name, children, ...props }) => (
alignItems='flex-end'
justifyContent='center'
>
{children || <Icon size='x16' name={name} {...props} />}
{hasChildrenProp(props) ? props.children : <Icon size='x16' {...props} />}
</Box>
);

@ -1,9 +1,13 @@
import { StatusBullet } from '@rocket.chat/fuselage';
import React, { memo } from 'react';
import React, { memo, ComponentProps, ReactElement } from 'react';
import { useTranslation } from '../../contexts/TranslationContext';
const UserStatus = ({ small, status, ...props }) => {
type UserStatusProps = {
small?: boolean;
} & ComponentProps<typeof StatusBullet>;
const UserStatus = ({ small, status, ...props }: UserStatusProps): ReactElement => {
const size = small ? 'small' : 'large';
const t = useTranslation();
switch (status) {

@ -1,5 +1,6 @@
import { createContext, useCallback, useContext, useMemo } from 'react';
import { IServerInfo } from '../../../definition/IServerInfo';
import type { Serialized } from '../../../definition/Serialized';
import type { PathFor, Params, Return, Method } from './endpoints';
import {
@ -11,7 +12,7 @@ import {
} from './methods';
type ServerContextValue = {
info: {};
info?: IServerInfo;
absoluteUrl: (path: string) => string;
callMethod?: <MethodName extends ServerMethodName>(
methodName: MethodName,
@ -30,7 +31,7 @@ type ServerContextValue = {
};
export const ServerContext = createContext<ServerContextValue>({
info: {},
info: undefined,
absoluteUrl: (path) => path,
callEndpoint: () => {
throw new Error('not implemented');
@ -39,7 +40,13 @@ export const ServerContext = createContext<ServerContextValue>({
getStream: () => () => (): void => undefined,
});
export const useServerInformation = (): {} => useContext(ServerContext).info;
export const useServerInformation = (): IServerInfo => {
const { info } = useContext(ServerContext);
if (!info) {
throw new Error('useServerInformation: no info available');
}
return info;
};
export const useAbsoluteUrl = (): ((path: string) => string) =>
useContext(ServerContext).absoluteUrl;

@ -11,9 +11,11 @@ import type { EmojiCustomEndpoints } from './endpoints/v1/emojiCustom';
import type { GroupsEndpoints } from './endpoints/v1/groups';
import type { ImEndpoints } from './endpoints/v1/im';
import type { LDAPEndpoints } from './endpoints/v1/ldap';
import type { LicensesEndpoints } from './endpoints/v1/licenses';
import type { MiscEndpoints } from './endpoints/v1/misc';
import type { OmnichannelEndpoints } from './endpoints/v1/omnichannel';
import type { RoomsEndpoints } from './endpoints/v1/rooms';
import type { StatisticsEndpoints } from './endpoints/v1/statistics';
import type { TeamsEndpoints } from './endpoints/v1/teams';
import type { UsersEndpoints } from './endpoints/v1/users';
@ -33,6 +35,8 @@ type Endpoints = ChatEndpoints &
EngagementDashboardEndpoints &
AppsEndpoints &
OmnichannelEndpoints &
StatisticsEndpoints &
LicensesEndpoints &
MiscEndpoints;
type Endpoint = UnionizeEndpoints<Endpoints>;

@ -0,0 +1,13 @@
import type { ILicense } from '../../../../../ee/app/license/server/license';
export type LicensesEndpoints = {
'licenses.get': {
GET: () => { licenses: Array<ILicense> };
};
'licenses.maxActiveUsers': {
GET: () => { maxActiveUsers: number | null; activeUsers: number };
};
'licenses.requestSeatsLink': {
GET: () => { url: string };
};
};

@ -0,0 +1,7 @@
import type { IStats } from '../../../../../definition/IStats';
export type StatisticsEndpoints = {
statistics: {
GET: (params: { refresh?: boolean }) => IStats;
};
};

@ -0,0 +1,11 @@
import { useLanguage } from '../contexts/TranslationContext';
import { getLocalePercentage } from '../lib/getLocalePercentage';
export const useLocalePercentage = (
total: number,
fraction: number,
decimalCount: number | undefined,
): string => {
const locale = useLanguage();
return getLocalePercentage(locale, total, fraction, decimalCount);
};

@ -0,0 +1,14 @@
export const getLocalePercentage = (
locale: string,
total: number,
fraction: number,
decimalCount = 2,
): string => {
const option = {
style: 'percent',
minimumFractionDigits: decimalCount,
maximumFractionDigits: decimalCount,
};
return new Intl.NumberFormat(locale, option).format(fraction / total);
};

@ -7,7 +7,7 @@ import { IBanner, BannerPlatform } from '../../definition/IBanner';
import * as banners from '../lib/banners';
const fetchInitialBanners = async (): Promise<void> => {
const response = (await APIClient.get('v1/banners.getNew', {
const response = (await APIClient.get('v1/banners', {
platform: BannerPlatform.Web,
})) as {
banners: IBanner[];
@ -21,14 +21,17 @@ const fetchInitialBanners = async (): Promise<void> => {
}
};
const handleNewBanner = async (event: { bannerId: string }): Promise<void> => {
const response = (await APIClient.get('v1/banners.getNew', {
const handleBanner = async (event: { bannerId: string }): Promise<void> => {
const response = (await APIClient.get(`v1/banners/${event.bannerId}`, {
platform: BannerPlatform.Web,
bid: event.bannerId,
})) as {
banners: IBanner[];
};
if (!response.banners.length) {
return banners.closeById(event.bannerId);
}
for (const banner of response.banners) {
banners.open({
...banner.view,
@ -40,10 +43,10 @@ const handleNewBanner = async (event: { bannerId: string }): Promise<void> => {
const watchBanners = (): (() => void) => {
fetchInitialBanners();
Notifications.onLogged('new-banner', handleNewBanner);
Notifications.onLogged('banner-changed', handleBanner);
return (): void => {
Notifications.unLogged(handleNewBanner);
Notifications.unLogged(handleBanner);
banners.clear();
};
};

@ -1,26 +1,33 @@
import { Skeleton, ButtonGroup, Button } from '@rocket.chat/fuselage';
import { ButtonGroup, Button } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import React, { memo } from 'react';
import React, { memo, ReactElement } from 'react';
import { IInstance } from '../../../../definition/IInstance';
import { IServerInfo } from '../../../../definition/IServerInfo';
import { IStats } from '../../../../definition/IStats';
import Card from '../../../components/Card';
import { useSetModal } from '../../../contexts/ModalContext';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useFormatDateAndTime } from '../../../hooks/useFormatDateAndTime';
import InstancesModal from './InstancesModal';
const DeploymentCard = memo(function DeploymentCard({ info, statistics, instances, isLoading }) {
type DeploymentCardProps = {
info: IServerInfo;
instances: Array<IInstance>;
statistics: IStats;
};
const DeploymentCard = ({ info, statistics, instances }: DeploymentCardProps): ReactElement => {
const t = useTranslation();
const formatDateAndTime = useFormatDateAndTime();
const setModal = useSetModal();
const { commit = {} } = info;
const s = (fn) => (isLoading ? <Skeleton width='50%' /> : fn());
const appsEngineVersion = info && info.marketplaceApiVersion;
const handleInstancesModal = useMutableCallback(() => {
setModal(<InstancesModal instances={instances} onClose={() => setModal()} />);
setModal(<InstancesModal instances={instances} onClose={(): void => setModal()} />);
});
return (
@ -30,11 +37,11 @@ const DeploymentCard = memo(function DeploymentCard({ info, statistics, instance
<Card.Col>
<Card.Col.Section>
<Card.Col.Title>{t('Version')}</Card.Col.Title>
{s(() => statistics.version)}
{statistics.version}
</Card.Col.Section>
<Card.Col.Section>
<Card.Col.Title>{t('Deployment_ID')}</Card.Col.Title>
{s(() => statistics.uniqueId)}
{statistics.uniqueId}
</Card.Col.Section>
{appsEngineVersion && (
<Card.Col.Section>
@ -44,34 +51,28 @@ const DeploymentCard = memo(function DeploymentCard({ info, statistics, instance
)}
<Card.Col.Section>
<Card.Col.Title>{t('Node_version')}</Card.Col.Title>
{s(() => statistics.process.nodeVersion)}
{statistics.process.nodeVersion}
</Card.Col.Section>
<Card.Col.Section>
<Card.Col.Title>{t('DB_Migration')}</Card.Col.Title>
{s(
() =>
`${statistics.migration.version} (${formatDateAndTime(
statistics.migration.lockedAt,
)})`,
)}
{`${statistics.migration.version} (${formatDateAndTime(
statistics.migration.lockedAt,
)})`}
</Card.Col.Section>
<Card.Col.Section>
<Card.Col.Title>{t('MongoDB')}</Card.Col.Title>
{s(
() =>
`${statistics.mongoVersion} / ${statistics.mongoStorageEngine} (oplog ${
statistics.oplogEnabled ? t('Enabled') : t('Disabled')
})`,
)}
{`${statistics.mongoVersion} / ${statistics.mongoStorageEngine} (oplog ${
statistics.oplogEnabled ? t('Enabled') : t('Disabled')
})`}
</Card.Col.Section>
<Card.Col.Section>
<Card.Col.Title>{t('Commit_details')}</Card.Col.Title>
{t('HEAD')}: ({s(() => (commit.hash ? commit.hash.slice(0, 9) : ''))}) <br />
{t('Branch')}: {s(() => commit.branch)}
{t('github_HEAD')}: ({commit.hash ? commit.hash.slice(0, 9) : ''}) <br />
{t('Branch')}: {commit.branch}
</Card.Col.Section>
<Card.Col.Section>
<Card.Col.Title>{t('PID')}</Card.Col.Title>
{s(() => statistics.process.pid)}
{statistics.process.pid}
</Card.Col.Section>
</Card.Col>
</Card.Body>
@ -87,6 +88,6 @@ const DeploymentCard = memo(function DeploymentCard({ info, statistics, instance
)}
</Card>
);
});
};
export default DeploymentCard;
export default memo(DeploymentCard);

@ -0,0 +1,117 @@
import { Box, Button, ButtonGroup, Callout, Icon, Margins } from '@rocket.chat/fuselage';
import { useResizeObserver } from '@rocket.chat/fuselage-hooks';
import React, { memo } from 'react';
import type { IInstance } from '../../../../definition/IInstance';
import type { IServerInfo } from '../../../../definition/IServerInfo';
import type { IStats } from '../../../../definition/IStats';
import SeatsCard from '../../../../ee/client/views/admin/info/SeatsCard';
import { DOUBLE_COLUMN_CARD_WIDTH } from '../../../components/Card';
import Page from '../../../components/Page';
import { useTranslation } from '../../../contexts/TranslationContext';
import DeploymentCard from './DeploymentCard';
import FederationCard from './FederationCard';
import LicenseCard from './LicenseCard';
import UsageCard from './UsageCard';
type InformationPageProps = {
canViewStatistics: boolean;
info: IServerInfo;
statistics: IStats;
instances: Array<IInstance>;
onClickRefreshButton: () => void;
onClickDownloadInfo: () => void;
};
const InformationPage = memo(function InformationPage({
canViewStatistics,
info,
statistics,
instances,
onClickRefreshButton,
onClickDownloadInfo,
}: InformationPageProps) {
const t = useTranslation();
const { ref, contentBoxSize: { inlineSize = DOUBLE_COLUMN_CARD_WIDTH } = {} } =
useResizeObserver();
const isSmall = inlineSize < DOUBLE_COLUMN_CARD_WIDTH;
if (!info) {
return null;
}
const alertOplogForMultipleInstances =
statistics && statistics.instanceCount > 1 && !statistics.oplogEnabled;
return (
<Page data-qa='admin-info'>
<Page.Header title={t('Info')}>
{canViewStatistics && (
<ButtonGroup>
<Button type='button' onClick={onClickDownloadInfo}>
<Icon name='download' /> {t('Download_Info')}
</Button>
<Button primary type='button' onClick={onClickRefreshButton}>
<Icon name='reload' /> {t('Refresh')}
</Button>
</ButtonGroup>
)}
</Page.Header>
<Page.ScrollableContentWithShadow>
<Box marginBlock='none' marginInline='auto' width='full'>
{alertOplogForMultipleInstances && (
<Callout
type='danger'
title={t(
'Error_RocketChat_requires_oplog_tailing_when_running_in_multiple_instances',
)}
marginBlockEnd='x16'
>
<Box withRichContent>
<p>
{t(
'Error_RocketChat_requires_oplog_tailing_when_running_in_multiple_instances_details',
)}
</p>
<p>
<a
rel='noopener noreferrer'
target='_blank'
href={
'https://rocket.chat/docs/installation/manual-installation/multiple-instances-to-improve-' +
'performance/#running-multiple-instances-per-host-to-improve-performance'
}
>
{t('Click_here_for_more_info')}
</a>
</p>
</Box>
</Callout>
)}
<Box
display='flex'
flexDirection='row'
w='full'
flexWrap='wrap'
justifyContent={isSmall ? 'center' : 'flex-start'}
ref={ref}
>
<Margins all='x8'>
<DeploymentCard info={info} statistics={statistics} instances={instances} />
<LicenseCard />
<UsageCard vertical={isSmall} statistics={statistics} />
<FederationCard />
<SeatsCard />
</Margins>
</Box>
</Box>
</Page.ScrollableContentWithShadow>
</Page>
);
});
export default InformationPage;

@ -1,30 +1,36 @@
import { Callout, ButtonGroup, Button, Icon } from '@rocket.chat/fuselage';
import React, { useState, useEffect, memo } from 'react';
import React, { useState, useEffect, memo, ReactElement } from 'react';
import { IStats } from '../../../../definition/IStats';
import NotAuthorizedPage from '../../../components/NotAuthorizedPage';
import Page from '../../../components/Page';
import PageSkeleton from '../../../components/PageSkeleton';
import { usePermission } from '../../../contexts/AuthorizationContext';
import { useMethod, useServerInformation, useEndpoint } from '../../../contexts/ServerContext';
import { useTranslation } from '../../../contexts/TranslationContext';
import { downloadJsonAs } from '../../../lib/download';
import NewInformationPage from './NewInformationPage';
import InformationPage from './InformationPage';
const InformationRoute = memo(function InformationRoute() {
type fetchStatisticsCallback = ((params: { refresh: boolean }) => void) | (() => void);
const InformationRoute = (): ReactElement => {
const t = useTranslation();
const canViewStatistics = usePermission('view-statistics');
const [isLoading, setLoading] = useState(true);
const [error, setError] = useState();
const [statistics, setStatistics] = useState({});
const [error, setError] = useState(false);
const [statistics, setStatistics] = useState<IStats>();
const [instances, setInstances] = useState([]);
const [fetchStatistics, setFetchStatistics] = useState(() => () => ({}));
const [fetchStatistics, setFetchStatistics] = useState<fetchStatisticsCallback>(
() => (): void => undefined,
);
const getStatistics = useEndpoint('GET', 'statistics');
const getInstances = useMethod('instances/get');
useEffect(() => {
let didCancel = false;
const fetchStatistics = async ({ refresh = false } = {}) => {
const fetchStatistics = async ({ refresh = false } = {}): Promise<void> => {
setLoading(true);
setError(false);
@ -50,14 +56,14 @@ const InformationRoute = memo(function InformationRoute() {
fetchStatistics();
return () => {
return (): void => {
didCancel = true;
};
}, [canViewStatistics, getInstances, getStatistics]);
const info = useServerInformation();
const handleClickRefreshButton = () => {
const handleClickRefreshButton = (): void => {
if (isLoading) {
return;
}
@ -65,14 +71,18 @@ const InformationRoute = memo(function InformationRoute() {
fetchStatistics({ refresh: true });
};
const handleClickDownloadInfo = () => {
const handleClickDownloadInfo = (): void => {
if (isLoading) {
return;
}
downloadJsonAs(statistics, 'statistics');
};
if (error) {
if (isLoading) {
return <PageSkeleton />;
}
if (error || !statistics) {
return (
<Page>
<Page.Header title={t('Info')}>
@ -83,9 +93,7 @@ const InformationRoute = memo(function InformationRoute() {
</ButtonGroup>
</Page.Header>
<Page.ScrollableContentWithShadow>
<Callout type='danger'>
{t('Error_loading_pages')} {/* : {error.message || error.stack}*/}
</Callout>
<Callout type='danger'>{t('Error_loading_pages')}</Callout>
</Page.ScrollableContentWithShadow>
</Page>
);
@ -93,9 +101,8 @@ const InformationRoute = memo(function InformationRoute() {
if (canViewStatistics) {
return (
<NewInformationPage
<InformationPage
canViewStatistics={canViewStatistics}
isLoading={isLoading}
info={info}
statistics={statistics}
instances={instances}
@ -106,6 +113,6 @@ const InformationRoute = memo(function InformationRoute() {
}
return <NotAuthorizedPage />;
});
};
export default InformationRoute;
export default memo(InformationRoute);

@ -1,43 +0,0 @@
import { Box, ButtonGroup, Button } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import React from 'react';
import Card from '../../../components/Card';
import { useSetModal } from '../../../contexts/ModalContext';
import { useTranslation } from '../../../contexts/TranslationContext';
import InstancesModal from './InstancesModal';
import UsagePieGraph from './UsagePieGraph';
const InstancesCard = ({ instances }) => {
const t = useTranslation();
const setModal = useSetModal();
const handleModal = useMutableCallback(() => {
setModal(<InstancesModal instances={instances} onClose={() => setModal()} />);
});
return (
<Card alignSelf='flex-start'>
<Card.Title>{t('Instances')}</Card.Title>
<Card.Body>
<Card.Col>
<Card.Col.Section>
<Box display='flex' flexDirection='row' justifyContent='center'>
<UsagePieGraph label={t('Instances_Health')} used={300} total={300} size={180} />
</Box>
</Card.Col.Section>
</Card.Col>
</Card.Body>
<Card.Footer>
<ButtonGroup align='end'>
<Button small onClick={handleModal}>
{t('Details')}
</Button>
</ButtonGroup>
</Card.Footer>
</Card>
);
};
export default InstancesCard;

@ -1,4 +1,4 @@
import { Box, ButtonGroup, Button, Skeleton, Margins } from '@rocket.chat/fuselage';
import { ButtonGroup, Button, Skeleton, Margins } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import React from 'react';
@ -11,9 +11,8 @@ import { AsyncStatePhase } from '../../../hooks/useAsyncState';
import { useEndpointData } from '../../../hooks/useEndpointData';
import Feature from './Feature';
import OfflineLicenseModal from './OfflineLicenseModal';
import UsagePieGraph from './UsagePieGraph';
const LicenseCard = ({ statistics, isLoading }) => {
const LicenseCard = () => {
const t = useTranslation();
const setModal = useSetModal();
@ -26,7 +25,7 @@ const LicenseCard = ({ statistics, isLoading }) => {
const { value, phase, error } = useEndpointData('licenses.get');
const endpointLoading = phase === AsyncStatePhase.LOADING;
const { maxActiveUsers = 0, modules = [] } =
const { modules = [] } =
endpointLoading || error || !value.licenses.length ? {} : value.licenses[0];
const hasEngagement = modules.includes('engagement-dashboard');
@ -74,22 +73,6 @@ const LicenseCard = ({ statistics, isLoading }) => {
)}
</Margins>
</Card.Col.Section>
<Card.Col.Section>
<Card.Col.Title>{t('Usage')}</Card.Col.Title>
<Box display='flex' flexDirection='row'>
{isLoading ? (
<Skeleton variant='rect' width='x112' height='x112' />
) : (
<UsagePieGraph
label={t('Active_users')}
used={statistics?.activeUsers}
total={maxActiveUsers}
size={112}
isLoading={isLoading}
/>
)}
</Box>
</Card.Col.Section>
</Card.Col>
</Card.Body>
<Card.Footer>

@ -2,6 +2,7 @@ import { Box, Button, ButtonGroup, Callout, Icon, Margins } from '@rocket.chat/f
import { useResizeObserver } from '@rocket.chat/fuselage-hooks';
import React, { memo } from 'react';
import SeatsCard from '../../../../ee/client/views/admin/info/SeatsCard';
import { DOUBLE_COLUMN_CARD_WIDTH } from '../../../components/Card';
import Page from '../../../components/Page';
import { useTranslation } from '../../../contexts/TranslationContext';
@ -100,6 +101,7 @@ const InformationPage = memo(function InformationPage({
<LicenseCard statistics={statistics} isLoading={isLoading} />
<UsageCard vertical={isSmall} statistics={statistics} isLoading={isLoading} />
<FederationCard />
<SeatsCard />
{/* {!!instances.length && <InstancesCard instances={instances}/>} */}
{/* <PushCard /> */}
</Margins>

@ -1,40 +0,0 @@
import { Box, ButtonGroup, Button } from '@rocket.chat/fuselage';
import React from 'react';
// import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import Card from '../../../components/Card';
import { useTranslation } from '../../../contexts/TranslationContext';
// import { useSetModal } from '../../contexts/ModalContext';
import UsagePieGraph from './UsagePieGraph';
// import PlanTag from '../../components/basic/PlanTag';
// import { useSetting } from '../../contexts/SettingsContext';
// import { useHasLicense } from '../../../ee/client/hooks/useHasLicense';
// import OfflineLicenseModal from './OfflineLicenseModal';
const PushCard = () => {
const t = useTranslation();
// const setModal = useSetModal();
return (
<Card alignSelf='flex-start'>
<Card.Title>{t('Push_Notifications')}</Card.Title>
<Card.Body>
<Card.Col>
<Card.Col.Section>
<Box display='flex' flexDirection='row' justifyContent='center'>
<UsagePieGraph label={t('Push_Notifications')} used={300} total={300} size={180} />
</Box>
</Card.Col.Section>
</Card.Col>
</Card.Body>
<Card.Footer>
<ButtonGroup align='end'>
<Button small>{t('Details')}</Button>
</ButtonGroup>
</Card.Footer>
</Card>
);
};
export default PushCard;

@ -1,7 +1,8 @@
import { Skeleton, ButtonGroup, Button } from '@rocket.chat/fuselage';
import { ButtonGroup, Button } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import React, { memo } from 'react';
import React, { memo, ReactElement } from 'react';
import { IStats } from '../../../../definition/IStats';
import { useHasLicense } from '../../../../ee/client/hooks/useHasLicense';
import Card from '../../../components/Card';
import { UserStatus } from '../../../components/UserStatus';
@ -10,8 +11,12 @@ import { useTranslation } from '../../../contexts/TranslationContext';
import { useFormatMemorySize } from '../../../hooks/useFormatMemorySize';
import TextSeparator from './TextSeparator';
const UsageCard = memo(function UsageCard({ statistics, isLoading, vertical }) {
const s = (fn) => (isLoading ? <Skeleton width='x40' /> : fn());
type UsageCardProps = {
statistics: IStats;
vertical: boolean;
};
const UsageCard = ({ statistics, vertical }: UsageCardProps): ReactElement => {
const t = useTranslation();
const formatMemorySize = useFormatMemorySize();
@ -36,7 +41,7 @@ const UsageCard = memo(function UsageCard({ statistics, isLoading, vertical }) {
<Card.Icon name='dialpad' /> {t('Total')}
</>
}
value={s(() => statistics.totalUsers)}
value={statistics.totalUsers}
/>
<TextSeparator
label={
@ -47,7 +52,7 @@ const UsageCard = memo(function UsageCard({ statistics, isLoading, vertical }) {
{t('Online')}
</>
}
value={s(() => statistics.onlineUsers)}
value={statistics.onlineUsers}
/>
<TextSeparator
label={
@ -58,7 +63,7 @@ const UsageCard = memo(function UsageCard({ statistics, isLoading, vertical }) {
{t('Busy')}
</>
}
value={s(() => statistics.busyUsers)}
value={statistics.busyUsers}
/>
<TextSeparator
label={
@ -69,7 +74,7 @@ const UsageCard = memo(function UsageCard({ statistics, isLoading, vertical }) {
{t('Away')}
</>
}
value={s(() => statistics.awayUsers)}
value={statistics.awayUsers}
/>
<TextSeparator
label={
@ -80,35 +85,23 @@ const UsageCard = memo(function UsageCard({ statistics, isLoading, vertical }) {
{t('Offline')}
</>
}
value={s(() => statistics.offlineUsers)}
value={statistics.offlineUsers}
/>
</Card.Col.Section>
<Card.Col.Section>
<Card.Col.Title>{t('Types_and_Distribution')}</Card.Col.Title>
<TextSeparator label={t('Connected')} value={s(() => statistics.totalConnectedUsers)} />
<TextSeparator
label={t('Stats_Active_Users')}
value={s(() => statistics.activeUsers)}
/>
<TextSeparator
label={t('Stats_Active_Guests')}
value={s(() => statistics.activeGuests)}
/>
<TextSeparator
label={t('Stats_Non_Active_Users')}
value={s(() => statistics.nonActiveUsers)}
/>
<TextSeparator label={t('Stats_App_Users')} value={s(() => statistics.appUsers)} />
<TextSeparator label={t('Connected')} value={statistics.totalConnectedUsers} />
<TextSeparator label={t('Stats_Active_Users')} value={statistics.activeUsers} />
<TextSeparator label={t('Stats_Active_Guests')} value={statistics.activeGuests} />
<TextSeparator label={t('Stats_Non_Active_Users')} value={statistics.nonActiveUsers} />
<TextSeparator label={t('Stats_App_Users')} value={statistics.appUsers} />
</Card.Col.Section>
<Card.Col.Section>
<Card.Col.Title>{t('Uploads')}</Card.Col.Title>
<TextSeparator
label={t('Stats_Total_Uploads')}
value={s(() => statistics.uploadsTotal)}
/>
<TextSeparator label={t('Stats_Total_Uploads')} value={statistics.uploadsTotal} />
<TextSeparator
label={t('Stats_Total_Uploads_Size')}
value={s(() => formatMemorySize(statistics.uploadsTotalSize))}
value={formatMemorySize(statistics.uploadsTotalSize)}
/>
</Card.Col.Section>
</Card.Col>
@ -122,7 +115,7 @@ const UsageCard = memo(function UsageCard({ statistics, isLoading, vertical }) {
<Card.Icon name='dialpad' size='x16' /> {t('Stats_Total_Rooms')}
</>
}
value={s(() => statistics.totalRooms)}
value={statistics.totalRooms}
/>
<TextSeparator
label={
@ -130,7 +123,7 @@ const UsageCard = memo(function UsageCard({ statistics, isLoading, vertical }) {
<Card.Icon name='hash' size='x16' /> {t('Stats_Total_Channels')}
</>
}
value={s(() => statistics.totalChannels)}
value={statistics.totalChannels}
/>
<TextSeparator
label={
@ -138,7 +131,7 @@ const UsageCard = memo(function UsageCard({ statistics, isLoading, vertical }) {
<Card.Icon name='lock' size='x16' /> {t('Stats_Total_Private_Groups')}
</>
}
value={s(() => statistics.totalPrivateGroups)}
value={statistics.totalPrivateGroups}
/>
<TextSeparator
label={
@ -146,7 +139,7 @@ const UsageCard = memo(function UsageCard({ statistics, isLoading, vertical }) {
<Card.Icon name='balloon' size='x16' /> {t('Stats_Total_Direct_Messages')}
</>
}
value={s(() => statistics.totalDirect)}
value={statistics.totalDirect}
/>
<TextSeparator
label={
@ -154,7 +147,7 @@ const UsageCard = memo(function UsageCard({ statistics, isLoading, vertical }) {
<Card.Icon name='discussion' size='x16' /> {t('Total_Discussions')}
</>
}
value={s(() => statistics.totalDiscussions)}
value={statistics.totalDiscussions}
/>
<TextSeparator
label={
@ -162,31 +155,28 @@ const UsageCard = memo(function UsageCard({ statistics, isLoading, vertical }) {
<Card.Icon name='headset' size='x16' /> {t('Stats_Total_Livechat_Rooms')}
</>
}
value={s(() => statistics.totalLivechat)}
value={statistics.totalLivechat}
/>
</Card.Col.Section>
<Card.Col.Section>
<Card.Col.Title>{t('Messages')}</Card.Col.Title>
<TextSeparator
label={t('Stats_Total_Messages')}
value={s(() => statistics.totalMessages)}
/>
<TextSeparator label={t('Total_Threads')} value={s(() => statistics.totalThreads)} />
<TextSeparator label={t('Stats_Total_Messages')} value={statistics.totalMessages} />
<TextSeparator label={t('Total_Threads')} value={statistics.totalThreads} />
<TextSeparator
label={t('Stats_Total_Messages_Channel')}
value={s(() => statistics.totalChannelMessages)}
value={statistics.totalChannelMessages}
/>
<TextSeparator
label={t('Stats_Total_Messages_PrivateGroup')}
value={s(() => statistics.totalPrivateGroupMessages)}
value={statistics.totalPrivateGroupMessages}
/>
<TextSeparator
label={t('Stats_Total_Messages_Direct')}
value={s(() => statistics.totalDirectMessages)}
value={statistics.totalDirectMessages}
/>
<TextSeparator
label={t('Stats_Total_Messages_Livechat')}
value={s(() => statistics.totalLivechatMessages)}
value={statistics.totalLivechatMessages}
/>
</Card.Col.Section>
</Card.Col>
@ -200,6 +190,6 @@ const UsageCard = memo(function UsageCard({ statistics, isLoading, vertical }) {
</Card.Footer>
</Card>
);
});
};
export default UsageCard;
export default memo(UsageCard);

@ -1,67 +0,0 @@
import { Pie } from '@nivo/pie';
import { Box } from '@rocket.chat/fuselage';
import colors from '@rocket.chat/fuselage-tokens/colors';
import React, { useMemo, useCallback } from 'react';
const graphColors = (color) => ({ used: color || colors.b500, free: colors.n300 });
const UsageGraph = ({ used = 0, total = 0, label, color, size }) => {
const parsedData = useMemo(
() => [
{
id: 'used',
label: 'used',
value: used,
},
{
id: 'free',
label: 'free',
value: total - used,
},
],
[total, used],
);
const getColor = useCallback((data) => graphColors(color)[data.id], [color]);
const unlimited = total === 0;
return (
<Box display='flex' flexDirection='column' alignItems='center'>
<Box size={`x${size}`}>
<Box position='relative'>
<Pie
data={parsedData}
margin={{ top: 10, right: 10, bottom: 10, left: 10 }}
innerRadius={0.8}
colors={getColor}
width={size}
height={size}
enableSlicesLabels={false}
enableRadialLabels={false}
/>
<Box
display='flex'
alignItems='center'
justifyContent='center'
position='absolute'
color={color}
fontScale='p2'
style={{ left: 0, right: 0, top: 0, bottom: 0 }}
>
{unlimited ? '∞' : `${Number((100 / total) * used).toFixed(2)}%`}
</Box>
</Box>
</Box>
<span>
<Box is='span' color='default'>
{used}
</Box>{' '}
/ {unlimited ? '' : total}
</span>
<span>{label}</span>
</Box>
);
};
export default UsageGraph;

@ -0,0 +1,100 @@
import { Pie, DatumId } from '@nivo/pie';
import { Box } from '@rocket.chat/fuselage';
import colors from '@rocket.chat/fuselage-tokens/colors';
import React, { useMemo, useCallback, ReactElement, CSSProperties, ReactNode } from 'react';
import { useLocalePercentage } from '../../../hooks/useLocalePercentage';
type GraphColorsReturn = { [key: string]: string };
const graphColors = (color: CSSProperties['color']): GraphColorsReturn => ({
used: color || colors.b500,
free: colors.n300,
});
type UsageGraphProps = {
used: number;
total: number;
label: ReactNode;
color?: string;
size: number;
};
type GraphData = Array<{
id: string;
label: string;
value: number;
}>;
const UsageGraph = ({ used = 0, total = 0, label, color, size }: UsageGraphProps): ReactElement => {
const parsedData = useMemo(
(): GraphData => [
{
id: 'used',
label: 'used',
value: used,
},
{
id: 'free',
label: 'free',
value: total - used,
},
],
[total, used],
);
const getColor = useCallback(
(datum: { id: DatumId } | undefined) => {
if (!datum || typeof datum.id !== 'string') {
return '';
}
return graphColors(color)[datum.id];
},
[color],
);
const unlimited = total === 0;
const localePercentage = useLocalePercentage(total, used, 0);
return (
<Box display='flex' flexDirection='column' alignItems='center'>
<Box size={size}>
<Box position='relative'>
<Pie
data={parsedData}
margin={{ top: 10, right: 10, bottom: 10, left: 10 }}
innerRadius={0.8}
colors={getColor}
width={size}
height={size}
enableArcLabels={false}
enableArcLinkLabels={false}
/>
<Box
display='flex'
alignItems='center'
justifyContent='center'
position='absolute'
color={color}
fontScale='p2'
style={{ left: 0, right: 0, top: 0, bottom: 0 }}
>
{unlimited ? '∞' : localePercentage}
</Box>
</Box>
</Box>
<span>
<Box is='span' color='default'>
{used}
</Box>{' '}
/ {unlimited ? '' : total}
</span>
<Box is='span' mbs='x4'>
{label}
</Box>
</Box>
);
};
export default UsageGraph;

@ -9,7 +9,7 @@ import { useEndpointData } from '../../../hooks/useEndpointData';
import { useForm } from '../../../hooks/useForm';
import UserForm from './UserForm';
export function AddUser({ roles, reloadTable, ...props }) {
export function AddUser({ roles, onReload, ...props }) {
const t = useTranslation();
const router = useRoute('admin-users');
@ -93,7 +93,7 @@ export function AddUser({ roles, reloadTable, ...props }) {
const result = await saveAction();
if (result.success) {
goToUser(result.user._id);
reloadTable();
onReload();
}
});

@ -26,7 +26,7 @@ const getInitialValue = (data) => ({
statusText: data.statusText ?? '',
});
function EditUser({ data, roles, reloadTable, ...props }) {
function EditUser({ data, roles, onReload, ...props }) {
const t = useTranslation();
const [avatarObj, setAvatarObj] = useState();
@ -143,7 +143,7 @@ function EditUser({ data, roles, reloadTable, ...props }) {
if (avatarObj) {
await updateAvatar();
}
reloadTable();
onReload();
goToUser(data._id);
}
}, [avatarObj, data._id, goToUser, saveAction, updateAvatar, values, errors, validationKeys]);

@ -14,7 +14,7 @@ import { getUserEmailVerified } from '../../../lib/utils/getUserEmailVerified';
import UserInfo from '../../room/contextualBar/UserInfo/UserInfo';
import { UserInfoActions } from './UserInfoActions';
export function UserInfoWithData({ uid, username, reloadTable, ...props }) {
export function UserInfoWithData({ uid, username, onReload, ...props }) {
const t = useTranslation();
const showRealNames = useSetting('UI_Use_Real_Name');
const approveManuallyUsers = useSetting('Accounts_ManuallyApproveNewUsers');
@ -23,7 +23,7 @@ export function UserInfoWithData({ uid, username, reloadTable, ...props }) {
value: data,
phase: state,
error,
reload,
reload: reloadUserInfo,
} = useEndpointData(
'users.info',
useMemo(
@ -32,8 +32,10 @@ export function UserInfoWithData({ uid, username, reloadTable, ...props }) {
),
);
const onChange = useMutableCallback(() => reload());
const onDelete = useMutableCallback(() => (reloadTable ? reloadTable() : null));
const onChange = useMutableCallback(() => {
onReload();
reloadUserInfo();
});
const user = useMemo(() => {
const { user } = data || { user: {} };
@ -100,7 +102,6 @@ export function UserInfoWithData({ uid, username, reloadTable, ...props }) {
_id={data.user._id}
username={data.user.username}
onChange={onChange}
onDelete={onDelete}
/>
)
}

@ -13,7 +13,7 @@ import { useTranslation } from '../../../contexts/TranslationContext';
import { useActionSpread } from '../../hooks/useActionSpread';
import UserInfo from '../../room/contextualBar/UserInfo';
export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange, onDelete }) => {
export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange }) => {
const t = useTranslation();
const setModal = useSetModal();
@ -39,7 +39,7 @@ export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange, on
const handleDeletedUser = () => {
setModal();
userRoute.push({});
onDelete();
onChange();
};
const confirmOwnerChanges =

@ -0,0 +1,33 @@
import { Button, ButtonGroup, Icon } from '@rocket.chat/fuselage';
import React, { ReactElement } from 'react';
import { useRoute } from '../../../contexts/RouterContext';
import { useTranslation } from '../../../contexts/TranslationContext';
const UserPageHeaderContent = (): ReactElement => {
const usersRoute = useRoute('admin-users');
const t = useTranslation();
const handleNewButtonClick = (): void => {
usersRoute.push({ context: 'new' });
};
const handleInviteButtonClick = (): void => {
usersRoute.push({ context: 'invite' });
};
return (
<>
<ButtonGroup>
<Button onClick={handleNewButtonClick}>
<Icon size='x20' name='user-plus' /> {t('New')}
</Button>
<Button onClick={handleInviteButtonClick}>
<Icon size='x20' name='mail' /> {t('Invite')}
</Button>
</ButtonGroup>
</>
);
};
export default UserPageHeaderContent;

@ -1,16 +1,18 @@
import { Button, ButtonGroup, Icon } from '@rocket.chat/fuselage';
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import React, { useMemo, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import UserPageHeaderContentWithSeatsCap from '../../../../ee/client/views/admin/users/UserPageHeaderContentWithSeatsCap';
import { useSeatsCap } from '../../../../ee/client/views/admin/users/useSeatsCap';
import Page from '../../../components/Page';
import VerticalBar from '../../../components/VerticalBar';
import { useRoute, useCurrentRoute } from '../../../contexts/RouterContext';
import { useRoute, useRouteParameter } from '../../../contexts/RouterContext';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useEndpointData } from '../../../hooks/useEndpointData';
import { AddUser } from './AddUser';
import EditUserWithData from './EditUserWithData';
import { InviteUsers } from './InviteUsers';
import { UserInfoWithData } from './UserInfo';
import UserPageHeaderContent from './UserPageHeaderContent';
import UsersTable from './UsersTable';
const sortDir = (sortDir) => (sortDir === 'asc' ? 1 : -1);
@ -47,22 +49,26 @@ const useQuery = ({ text, itemsPerPage, current }, sortFields) =>
);
function UsersPage() {
const t = useTranslation();
const context = useRouteParameter('context');
const id = useRouteParameter('id');
const seatsCap = useSeatsCap();
const usersRoute = useRoute('admin-users');
const handleVerticalBarCloseButtonClick = () => {
usersRoute.push({});
};
useEffect(() => {
if (!context || !seatsCap) {
return;
}
const handleNewButtonClick = () => {
usersRoute.push({ context: 'new' });
};
if (seatsCap.activeUsers >= seatsCap.maxActiveUsers && !['edit', 'info'].includes(context)) {
usersRoute.push({});
}
}, [context, seatsCap, usersRoute]);
const handleInviteButtonClick = () => {
usersRoute.push({ context: 'invite' });
};
const t = useTranslation();
const handleVerticalBarCloseButtonClick = () => {
usersRoute.push({});
};
const [params, setParams] = useState({ text: '', current: 0, itemsPerPage: 25 });
const [sort, setSort] = useState([
['name', 'asc'],
@ -72,21 +78,23 @@ function UsersPage() {
const debouncedParams = useDebouncedValue(params, 500);
const debouncedSort = useDebouncedValue(sort, 500);
const query = useQuery(debouncedParams, debouncedSort);
const { value: data = {}, reload } = useEndpointData('users.list', query);
const [, { context, id }] = useCurrentRoute();
const { value: data = {}, reload: reloadList } = useEndpointData('users.list', query);
const reload = () => {
seatsCap?.reload();
reloadList();
};
return (
<Page flexDirection='row'>
<Page>
<Page.Header title={t('Users')}>
<ButtonGroup>
<Button onClick={handleNewButtonClick}>
<Icon name='plus' /> {t('New')}
</Button>
<Button onClick={handleInviteButtonClick}>
<Icon name='send' /> {t('Invite')}
</Button>
</ButtonGroup>
{seatsCap &&
(seatsCap.maxActiveUsers < Number.POSITIVE_INFINITY ? (
<UserPageHeaderContentWithSeatsCap {...seatsCap} />
) : (
<UserPageHeaderContent />
))}
</Page.Header>
<Page.Content>
<UsersTable
@ -109,9 +117,9 @@ function UsersPage() {
<VerticalBar.Close onClick={handleVerticalBarCloseButtonClick} />
</VerticalBar.Header>
{context === 'info' && <UserInfoWithData uid={id} reloadTable={reload} />}
{context === 'edit' && <EditUserWithData uid={id} />}
{context === 'new' && <AddUser reloadTable={reload} />}
{context === 'info' && <UserInfoWithData uid={id} onReload={reload} />}
{context === 'edit' && <EditUserWithData uid={id} onReload={reload} />}
{context === 'new' && <AddUser onReload={reload} />}
{context === 'invite' && <InviteUsers />}
</VerticalBar>
)}

@ -1,15 +1,27 @@
/* eslint-disable new-cap */
import { Banner, Icon } from '@rocket.chat/fuselage';
import { kitContext, UiKitBanner as renderUiKitBannerBlocks } from '@rocket.chat/fuselage-ui-kit';
import React, { Context, FC, useMemo } from 'react';
import {
kitContext,
// @ts-ignore
bannerParser,
UiKitBanner as renderUiKitBannerBlocks,
} from '@rocket.chat/fuselage-ui-kit';
import React, { Context, FC, useMemo, ReactNode } from 'react';
// import { useEndpoint } from '../../contexts/ServerContext';
import { UiKitBannerProps, UiKitBannerPayload } from '../../../definition/UIKit';
import { useUIKitHandleAction } from '../../UIKit/hooks/useUIKitHandleAction';
import { useUIKitHandleClose } from '../../UIKit/hooks/useUIKitHandleClose';
import { useUIKitStateManager } from '../../UIKit/hooks/useUIKitStateManager';
import MarkdownText from '../../components/MarkdownText';
import * as banners from '../../lib/banners';
// TODO: move this to fuselage-ui-kit itself
const mrkdwn = ({ text }: { text: string } = { text: '' }): ReactNode => (
<MarkdownText variant='inline' content={text} />
);
bannerParser.mrkdwn = mrkdwn;
const UiKitBanner: FC<UiKitBannerProps> = ({ payload }) => {
const state = useUIKitStateManager<UiKitBannerPayload>(payload);

@ -14,8 +14,18 @@ export interface IBanner extends IRocketChatRecord {
createdBy: Pick<IUser, '_id' | 'username' >;
createdAt: Date;
view: UiKitBannerPayload;
active?: boolean;
inactivedAt?: Date;
snapshot?: string;
}
export type InactiveBanner = IBanner & {
active: false;
inactivedAt: Date;
};
export const isInactiveBanner = (banner: IBanner): banner is InactiveBanner => banner.active === false;
export interface IBannerDismiss extends IRocketChatRecord {
userId: IUser['_id']; // user receiving the banner dismissed
bannerId: IBanner['_id']; // banner dismissed

@ -0,0 +1,21 @@
export interface IInstance {
address: string;
currentStatus: {
connected: boolean;
retryCount: number;
retryTime: number;
status: string;
};
instanceRecord: {
name: string;
pid: number;
_createdAt: Date;
_id: string;
_updatedAt: Date;
extraInformation: {
host: string;
nodeVersion: string;
port: string;
};
};
}

@ -0,0 +1,22 @@
export interface IServerInfo {
build: {
arch: string;
cpus: number;
date: string;
freeMemory: number;
nodeVersion: string;
osRelease: string;
platform: string;
totalMemory: number;
};
commit: {
author?: string;
branch?: string;
date?: string;
hash?: string;
subject?: string;
tag?: string;
};
marketplaceApiVersion: string;
version: string;
}

@ -0,0 +1,78 @@
import type { CpuInfo } from 'os';
import type { ITeamStats } from './ITeam';
export interface IStats {
wizard: Record<string, unknown>;
uniqueId: string;
installedAt?: string;
version?: string;
tag?: string;
branch?: string;
totalUsers: number;
activeUsers: number;
activeGuests: number;
nonActiveUsers: number;
appUsers: number;
onlineUsers: number;
awayUsers: number;
busyUsers: number;
totalConnectedUsers: number;
offlineUsers: number;
userLanguages: Record<string, number>;
totalRooms: number;
totalChannels: number;
totalPrivateGroups: number;
totalDirect: number;
totalLivechat: number;
totalDiscussions: number;
totalThreads: number;
teams: ITeamStats;
totalLivechatVisitors: number;
totalLivechatAgents: number;
livechatEnabled: boolean;
totalChannelMessages: number;
totalPrivateGroupMessages: number;
totalDirectMessages: number;
totalLivechatMessages: number;
totalMessages: number;
federatedServers: number;
federatedUsers: number;
lastLogin: string;
lastMessageSentAt: string;
lastSeenSubscription: string;
os: {
type: string;
platform: NodeJS.Platform;
arch: string;
release: string;
uptime: number;
loadavg: number[];
totalmem: number;
freemem: number;
cpus: CpuInfo[];
};
process: {
nodeVersion: string;
pid: number;
uptime: number;
};
deploy: {
method: string;
platform: string;
};
enterpriseReady: boolean;
uploadsTotal: number;
uploadsTotalSize: number;
migration: {
_id: 'control';
locked: boolean;
version: number;
buildAt: string;
lockedAt: string;
};
instanceCount: number;
oplogEnabled: boolean;
mongoVersion: string;
mongoStorageEngine: string;
}

@ -1,3 +1,5 @@
export type Optional<T, K extends keyof T> = Omit<T, K> & Partial<T>;
export type ExtractKeys<T, K extends keyof T, U> = T[K] extends U ? K : never;
export type ValueOf<T> = T[keyof T];

@ -0,0 +1,30 @@
import { Settings, Users } from '../../../../app/models/server';
import { ISetting } from '../../../../definition/ISetting';
type WizardSettings = Array<ISetting>;
const url = 'https://rocket.chat/sales-contact';
export const getSeatsRequestLink = (): string => {
const workspaceId: ISetting | undefined = Settings.findOneById('Cloud_Workspace_Id');
const activeUsers = Users.getActiveLocalUserCount();
const wizardSettings: WizardSettings = Settings.findSetupWizardSettings().fetch();
const newUrl = new URL(url);
if (workspaceId?.value) {
newUrl.searchParams.append('workspaceId', String(workspaceId.value));
}
if (activeUsers) {
newUrl.searchParams.append('activeUsers', String(activeUsers));
}
wizardSettings
.filter(({ _id, value }) => ['Industry', 'Country', 'Size'].includes(_id) && value)
.forEach((setting) => {
newUrl.searchParams.append(setting._id.toLowerCase(), String(setting.value));
});
return newUrl.toString();
};

@ -1,16 +1,20 @@
import { getModules, getTags } from './license';
import { Analytics } from '../../../../server/sdk';
type ENTERPRISE_STATISTICS = {
modules: string[];
tags: string[];
seatRequests: number;
}
export function getStatistics(): ENTERPRISE_STATISTICS {
const modules = getModules();
const tags = getTags().map(({ name }) => name);
const seatRequests = Promise.await(Analytics.getSeatRequestCount());
return {
modules,
tags,
seatRequests,
};
}

@ -28,6 +28,7 @@ export interface IValidLicense {
}
let maxGuestUsers = 0;
let maxActiveUsers = 0;
class LicenseClass {
private url: string|null = null;
@ -80,10 +81,6 @@ class LicenseClass {
});
}
private _hasValidNumberOfActiveUsers(maxActiveUsers: number): boolean {
return Users.getActiveLocalUserCount() <= maxActiveUsers;
}
private _addTags(license: ILicense): void {
// if no tag present, it means it is an old license, so try check for bundles and use them as tags
if (typeof license.tag === 'undefined') {
@ -178,17 +175,14 @@ class LicenseClass {
return item;
}
if (license.maxActiveUsers && !this._hasValidNumberOfActiveUsers(license.maxActiveUsers)) {
item.valid = false;
console.error(`#### License error: over seats, max allowed ${ license.maxActiveUsers }, current active users ${ Users.getActiveLocalUserCount() }`);
this._invalidModules(license.modules);
return item;
}
if (license.maxGuestUsers > maxGuestUsers) {
maxGuestUsers = license.maxGuestUsers;
}
if (license.maxActiveUsers > maxActiveUsers) {
maxActiveUsers = license.maxActiveUsers;
}
this._validModules(license.modules);
this._addTags(license);
@ -203,6 +197,14 @@ class LicenseClass {
this.showLicenses();
}
canAddNewUser(): boolean {
if (!maxActiveUsers) {
return true;
}
return maxActiveUsers > Users.getActiveLocalUserCount();
}
showLicenses(): void {
if (!process.env.LICENSE_DEBUG || process.env.LICENSE_DEBUG === 'false') {
return;
@ -286,6 +288,10 @@ export function getMaxGuestUsers(): number {
return maxGuestUsers;
}
export function getMaxActiveUsers(): number {
return maxActiveUsers;
}
export function getLicenses(): IValidLicense[] {
return License.getLicenses();
}
@ -298,6 +304,10 @@ export function getTags(): ILicenseTag[] {
return License.getTags();
}
export function canAddNewUser(): boolean {
return License.canAddNewUser();
}
export function onLicense(feature: string, cb: (...args: any[]) => void): void {
if (hasLicense(feature)) {
return cb();

@ -0,0 +1,116 @@
import { Meteor } from 'meteor/meteor';
import { BlockType } from '@rocket.chat/apps-engine/definition/uikit/blocks/Blocks';
import { TextObjectType } from '@rocket.chat/apps-engine/definition/uikit/blocks/Objects';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { IBanner, BannerPlatform } from '../../../../definition/IBanner';
import { Banner } from '../../../../server/sdk';
const WARNING_BANNER_ID = 'closeToSeatsLimit';
const DANGER_BANNER_ID = 'reachedSeatsLimit';
const makeWarningBanner = (seats: number): IBanner => ({
_id: WARNING_BANNER_ID,
platform: [BannerPlatform.Web],
roles: ['admin'],
view: {
icon: 'warning',
variant: 'warning',
viewId: '',
appId: 'banner-core',
blocks: [
{
type: BlockType.SECTION,
blockId: 'attention',
text: {
type: TextObjectType.MARKDOWN,
text: TAPi18n.__('Close_to_seat_limit_banner_warning', {
seats,
url: Meteor.absoluteUrl('/requestSeats'),
}),
emoji: false,
},
},
],
},
createdBy: {
_id: 'rocket.cat',
username: 'rocket.cat',
},
expireAt: new Date(8640000000000000),
startAt: new Date(),
createdAt: new Date(),
_updatedAt: new Date(),
active: false,
});
const makeDangerBanner = (): IBanner => ({
_id: DANGER_BANNER_ID,
platform: [BannerPlatform.Web],
roles: ['admin'],
view: {
icon: 'ban',
variant: 'danger',
viewId: '',
appId: 'banner-core',
blocks: [
{
type: BlockType.SECTION,
blockId: 'attention',
text: {
type: TextObjectType.MARKDOWN,
text: TAPi18n.__('Reached_seat_limit_banner_warning', {
url: Meteor.absoluteUrl('/requestSeats'),
}),
emoji: false,
},
},
],
},
createdBy: {
_id: 'rocket.cat',
username: 'rocket.cat',
},
expireAt: new Date(8640000000000000),
startAt: new Date(),
createdAt: new Date(),
_updatedAt: new Date(),
active: false,
});
export const createSeatsLimitBanners = async (): Promise<void> => {
const [warning, danger] = await Promise.all([
Banner.getById(WARNING_BANNER_ID),
Banner.getById(DANGER_BANNER_ID),
]);
if (!warning) {
Banner.create(makeWarningBanner(0));
}
if (!danger) {
Banner.create(makeDangerBanner());
}
};
export const enableDangerBanner = (): void => {
Banner.enable(DANGER_BANNER_ID, makeDangerBanner());
};
export const disableDangerBannerDiscardingDismissal = async (): Promise<void> => {
const banner = await Banner.getById(DANGER_BANNER_ID);
if (banner && banner.active) {
Banner.disable(DANGER_BANNER_ID);
Banner.discardDismissal(DANGER_BANNER_ID);
}
};
export const enableWarningBanner = (seatsLeft: number): void => {
Banner.enable(WARNING_BANNER_ID, makeWarningBanner(seatsLeft));
};
export const disableWarningBannerDiscardingDismissal = async (): Promise<void> => {
const banner = await Banner.getById(WARNING_BANNER_ID);
if (banner && banner.active) {
Banner.disable(WARNING_BANNER_ID);
Banner.discardDismissal(WARNING_BANNER_ID);
}
};

@ -0,0 +1,67 @@
import { Box, Button, ButtonGroup, Skeleton } from '@rocket.chat/fuselage';
import colors from '@rocket.chat/fuselage-tokens/colors';
import React, { ReactElement } from 'react';
import Card from '../../../../../client/components/Card';
import ExternalLink from '../../../../../client/components/ExternalLink';
import { useTranslation } from '../../../../../client/contexts/TranslationContext';
import UsagePieGraph from '../../../../../client/views/admin/info/UsagePieGraph';
import { useRequestSeatsLink } from '../users/useRequestSeatsLink';
import { useSeatsCap } from '../users/useSeatsCap';
const SeatsCard = (): ReactElement | null => {
const t = useTranslation();
const seatsCap = useSeatsCap();
const requestSeatsLink = useRequestSeatsLink();
const seatsLeft = seatsCap && Math.max(seatsCap.maxActiveUsers - seatsCap.activeUsers, 0);
const isNearLimit = seatsCap && seatsCap.activeUsers / seatsCap.maxActiveUsers >= 0.8;
const color = isNearLimit ? colors.r500 : undefined;
if (seatsCap && seatsCap.maxActiveUsers === Infinity) {
return null;
}
return (
<Card>
<Card.Title>{t('Seats_usage')}</Card.Title>
<Card.Body>
<Card.Col>
<Card.Col.Section>
<Box
display='flex'
flexDirection='row'
justifyContent='center'
fontScale={isNearLimit ? 'p2' : 'p1'}
>
{!seatsCap ? (
<Skeleton variant='rect' width='x112' height='x112' />
) : (
<UsagePieGraph
label={<Box color={color}>{t('Seats_Available', { seatsLeft })}</Box>}
used={seatsCap.activeUsers}
total={seatsCap.maxActiveUsers}
size={140}
color={color}
/>
)}
</Box>
</Card.Col.Section>
</Card.Col>
</Card.Body>
<Card.Footer>
<ButtonGroup align='end'>
<ExternalLink to={requestSeatsLink}>
<Button small primary>
{t('Request_seats')}
</Button>
</ExternalLink>
</ButtonGroup>
</Card.Footer>
</Card>
);
};
export default SeatsCard;

@ -0,0 +1,51 @@
import { Modal, ButtonGroup, Button, Box } from '@rocket.chat/fuselage';
import React, { ReactElement } from 'react';
import ExternalLink from '../../../../../client/components/ExternalLink';
import { useTranslation } from '../../../../../client/contexts/TranslationContext';
import MemberCapUsage from './SeatsCapUsage';
type CloseToSeatsCapModalProps = {
members: number;
limit: number;
title: string;
requestSeatsLink: string;
onConfirm: () => void;
onClose: () => void;
};
const CloseToSeatsCapModal = ({
members,
limit,
title,
onConfirm,
onClose,
requestSeatsLink,
}: CloseToSeatsCapModalProps): ReactElement => {
const t = useTranslation();
return (
<Modal>
<Modal.Header>
<Modal.Title>{title}</Modal.Title>
<Modal.Close onClick={onClose} />
</Modal.Header>
<Modal.Content>
<Box is='p' mbe='x24'>
{t('Close_to_seat_limit_warning')}{' '}
<ExternalLink to={requestSeatsLink}>{t('Request_more_seats')}</ExternalLink>
</Box>
<MemberCapUsage members={members} limit={limit} />
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button onClick={onClose}>{t('Cancel')}</Button>
<Button onClick={onConfirm} primary>
{t('Continue')}
</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>
);
};
export default CloseToSeatsCapModal;

@ -0,0 +1,52 @@
import { Icon, Modal, ButtonGroup, Button, Box } from '@rocket.chat/fuselage';
import React, { ReactElement } from 'react';
import ExternalLink from '../../../../../client/components/ExternalLink';
import { useTranslation } from '../../../../../client/contexts/TranslationContext';
import SeatsCapUsage from './SeatsCapUsage';
type ReachedSeatsCapModalProps = {
members: number;
limit: number;
requestSeatsLink: string;
onClose: () => void;
};
const ReachedSeatsCapModal = ({
members,
limit,
onClose,
requestSeatsLink,
}: ReachedSeatsCapModalProps): ReactElement => {
const t = useTranslation();
return (
<Modal>
<Modal.Header>
<Modal.Title>{t('Request_more_seats_title')}</Modal.Title>
<Modal.Close onClick={onClose} />
</Modal.Header>
<Modal.Content>
<Box is='p' mbe='x16'>
{t('Request_more_seats_out_of_seats')}
</Box>
<Box is='p' mbe='x24'>
{t('Request_more_seats_sales_team')}
</Box>
<SeatsCapUsage members={members} limit={limit} />
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button onClick={onClose}>{t('Cancel')}</Button>
<ExternalLink to={requestSeatsLink}>
<Button onClick={onClose} primary>
<Icon name='new-window' size='x20' mie='x4' />
{t('Request')}
</Button>
</ExternalLink>
</ButtonGroup>
</Modal.Footer>
</Modal>
);
};
export default ReachedSeatsCapModal;

@ -0,0 +1,12 @@
import React, { ReactElement } from 'react';
import SeatsCapUsage from './SeatsCapUsage';
export default {
title: 'ee/admin/users/SeatsCapUsage',
component: SeatsCapUsage,
};
export const _default = (): ReactElement => <SeatsCapUsage members={150} limit={300} />;
export const CloseToLimit = (): ReactElement => <SeatsCapUsage members={270} limit={300} />;
export const ReachedLimit = (): ReactElement => <SeatsCapUsage members={300} limit={300} />;

@ -0,0 +1,44 @@
import { ProgressBar, Box } from '@rocket.chat/fuselage';
import React, { ReactElement } from 'react';
import { useTranslation } from '../../../../../../client/contexts/TranslationContext';
type SeatsCapUsageProps = {
limit: number;
members: number;
};
const SeatsCapUsage = ({ limit, members }: SeatsCapUsageProps): ReactElement => {
const t = useTranslation();
const percentage = Math.max(0, Math.min((100 / limit) * members, 100));
const closeToLimit = percentage >= 80;
const reachedLimit = percentage >= 100;
const color = closeToLimit ? 'danger-500' : 'success-500';
const seatsLeft = Math.max(0, limit - members);
return (
<Box display='flex' flexDirection='column' minWidth='x180'>
<Box
color={reachedLimit ? color : 'default'}
display='flex'
flexDirection='row'
justifyContent='space-between'
fontScale='c1'
mb='x8'
>
<div>{t('Seats_Available', { seatsLeft })}</div>
<Box color={reachedLimit ? color : 'neutral-700'}>{`${members}/${limit}`}</Box>
</Box>
<ProgressBar
borderRadius='x8'
overflow='hidden'
percentage={percentage}
barColor={color}
animated={false}
w='full'
/>
</Box>
);
};
export default SeatsCapUsage;

@ -0,0 +1 @@
export { default } from './SeatsCapUsage';

@ -0,0 +1,12 @@
import { useTranslation } from '../../../../../../client/contexts/TranslationContext';
export const useUsageLabel = (percentage: number): string => {
const fixedPercentage = percentage.toFixed(0);
const t = useTranslation();
if (percentage >= 100) {
return t('Out_of_seats');
}
return `${fixedPercentage}% ${t('Usage')}`;
};

@ -0,0 +1,129 @@
import { Button, ButtonGroup, Icon, Margins } from '@rocket.chat/fuselage';
import React, { ReactElement } from 'react';
import ExternalLink from '../../../../../client/components/ExternalLink';
import { useSetModal } from '../../../../../client/contexts/ModalContext';
import { useRoute } from '../../../../../client/contexts/RouterContext';
import { useTranslation } from '../../../../../client/contexts/TranslationContext';
import CloseToSeatsCapModal from './CloseToSeatsCapModal';
import ReachedSeatsCapModal from './ReachedSeatsCapModal';
import SeatsCapUsage from './SeatsCapUsage';
import { useRequestSeatsLink } from './useRequestSeatsLink';
type UserPageHeaderContentWithSeatsCapProps = {
activeUsers: number;
maxActiveUsers: number;
};
const UserPageHeaderContentWithSeatsCap = ({
activeUsers,
maxActiveUsers,
}: UserPageHeaderContentWithSeatsCapProps): ReactElement => {
const seatsLinkUrl = useRequestSeatsLink();
const t = useTranslation();
const usersRoute = useRoute('admin-users');
const setModal = useSetModal();
const closeModal = (): void => setModal(null);
const isCloseToLimit = (): boolean => {
const ratio = activeUsers / maxActiveUsers;
return ratio >= 0.8;
};
const hasReachedLimit = (): boolean => {
const ratio = activeUsers / maxActiveUsers;
return ratio >= 1;
};
const withPreventionOnReachedLimit = (fn: () => void) => (): void => {
if (typeof seatsLinkUrl !== 'string') {
return;
}
if (hasReachedLimit()) {
setModal(
<ReachedSeatsCapModal
members={activeUsers}
limit={maxActiveUsers}
requestSeatsLink={seatsLinkUrl}
onClose={closeModal}
/>,
);
return;
}
fn();
};
const handleNewButtonClick = withPreventionOnReachedLimit(() => {
if (typeof seatsLinkUrl !== 'string') {
return;
}
if (isCloseToLimit()) {
setModal(
<CloseToSeatsCapModal
members={activeUsers}
limit={maxActiveUsers}
title={t('Create_new_members')}
requestSeatsLink={seatsLinkUrl}
onConfirm={(): void => {
usersRoute.push({ context: 'new' });
closeModal();
}}
onClose={closeModal}
/>,
);
return;
}
usersRoute.push({ context: 'new' });
});
const handleInviteButtonClick = withPreventionOnReachedLimit(() => {
if (typeof seatsLinkUrl !== 'string') {
return;
}
if (isCloseToLimit()) {
setModal(
<CloseToSeatsCapModal
members={activeUsers}
limit={maxActiveUsers}
title={t('Invite_Users')}
requestSeatsLink={seatsLinkUrl}
onConfirm={(): void => {
usersRoute.push({ context: 'invite' });
closeModal();
}}
onClose={closeModal}
/>,
);
return;
}
usersRoute.push({ context: 'invite' });
});
return (
<>
<Margins inline='x16'>
<SeatsCapUsage members={activeUsers} limit={maxActiveUsers} />
</Margins>
<ButtonGroup>
<Button onClick={handleNewButtonClick}>
<Icon size='x20' name='user-plus' /> {t('New')}
</Button>
<Button onClick={handleInviteButtonClick}>
<Icon size='x20' name='mail' /> {t('Invite')}
</Button>
<ExternalLink to={seatsLinkUrl || ''}>
<Button>
<Icon size='x20' name='new-window' /> {t('Request_seats')}
</Button>
</ExternalLink>
</ButtonGroup>
</>
);
};
export default UserPageHeaderContentWithSeatsCap;

@ -0,0 +1,3 @@
import { useAbsoluteUrl } from '../../../../../client/contexts/ServerContext';
export const useRequestSeatsLink = (): string => useAbsoluteUrl()('/requestSeats');

@ -0,0 +1,21 @@
import { useEndpointData } from '../../../../../client/hooks/useEndpointData';
export const useSeatsCap = ():
| {
maxActiveUsers: number;
activeUsers: number;
reload: () => void;
}
| undefined => {
const { value, reload } = useEndpointData('licenses.maxActiveUsers');
if (!value) {
return undefined;
}
return {
activeUsers: value.activeUsers,
maxActiveUsers: value.maxActiveUsers ?? Number.POSITIVE_INFINITY,
reload,
};
};

@ -1,7 +1,7 @@
import { check } from 'meteor/check';
import { ILicense, getLicenses, validateFormat, flatModules } from '../../app/license/server/license';
import { Settings } from '../../../app/models/server';
import { ILicense, getLicenses, validateFormat, flatModules, getMaxActiveUsers } from '../../app/license/server/license';
import { Settings, Users } from '../../../app/models/server';
import { API } from '../../../app/api/server/api';
import { hasPermission } from '../../../app/authorization/server';
@ -46,3 +46,12 @@ API.v1.addRoute('licenses.add', { authRequired: true }, {
return API.v1.success();
},
});
API.v1.addRoute('licenses.maxActiveUsers', { authRequired: true }, {
get() {
const maxActiveUsers = getMaxActiveUsers() || null;
const activeUsers = Users.getActiveLocalUserCount();
return API.v1.success({ maxActiveUsers, activeUsers });
},
});

@ -1,4 +1,5 @@
import './broker';
import './startup';
import '../app/models';
import '../app/license/server/index';
@ -11,6 +12,7 @@ import '../app/livechat-enterprise/server/index';
import '../app/settings/server/index';
import '../app/teams-mention/server/index';
import './api';
import './requestSeatsRoute';
import './configuration/index';
import './local-services/ldap/service';
import './settings/index';

@ -0,0 +1,17 @@
import { IncomingMessage, ServerResponse } from 'http';
import { Meteor } from 'meteor/meteor';
import { WebApp } from 'meteor/webapp';
import { getSeatsRequestLink } from '../app/license/server/getSeatsRequestLink';
import { Analytics } from '../../server/sdk';
Meteor.startup(() => {
WebApp.connectHandlers.use('/requestSeats/', Meteor.bindEnvironment((_: IncomingMessage, res: ServerResponse) => {
const url = getSeatsRequestLink();
Analytics.saveSeatRequest();
res.writeHead(302, { Location: url });
res.end();
}));
});

@ -0,0 +1 @@
import './seatsCap';

@ -0,0 +1,109 @@
import { Meteor } from 'meteor/meteor';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { callbacks } from '../../../app/callbacks/server';
import { canAddNewUser, getMaxActiveUsers, isEnterprise, onValidateLicenses } from '../../app/license/server/license';
import { createSeatsLimitBanners, disableDangerBannerDiscardingDismissal, disableWarningBannerDiscardingDismissal, enableDangerBanner, enableWarningBanner } from '../../app/license/server/maxSeatsBanners';
import { validateUserRoles } from '../../app/authorization/server/validateUserRoles';
import { Users } from '../../../app/models/server';
import type { IUser } from '../../../definition/IUser';
callbacks.add('onCreateUser', ({ isGuest }: { isGuest: boolean }) => {
if (isGuest) {
return;
}
if (!canAddNewUser()) {
throw new Meteor.Error('error-license-user-limit-reached', TAPi18n.__('error-license-user-limit-reached'));
}
}, callbacks.priority.MEDIUM, 'check-max-user-seats');
callbacks.add('beforeActivateUser', (user: IUser) => {
if (user.roles.length === 1 && user.roles.includes('guest')) {
return;
}
if (user.type === 'app') {
return;
}
if (!canAddNewUser()) {
throw new Meteor.Error('error-license-user-limit-reached', TAPi18n.__('error-license-user-limit-reached'));
}
}, callbacks.priority.MEDIUM, 'check-max-user-seats');
callbacks.add('validateUserRoles', (userData: Record<string, any>) => {
const isGuest = userData.roles?.includes('guest');
if (isGuest) {
validateUserRoles(Meteor.userId(), userData);
return;
}
if (!userData._id) {
return;
}
const currentUserData = Users.findOneById(userData._id);
if (currentUserData.type === 'app') {
return;
}
const wasGuest = currentUserData?.roles?.length === 1 && currentUserData.roles.includes('guest');
if (!wasGuest) {
return;
}
if (!canAddNewUser()) {
throw new Meteor.Error('error-license-user-limit-reached', TAPi18n.__('error-license-user-limit-reached'));
}
}, callbacks.priority.MEDIUM, 'check-max-user-seats');
const handleMaxSeatsBanners = (): void => {
const maxActiveUsers = getMaxActiveUsers();
if (!maxActiveUsers) {
disableWarningBannerDiscardingDismissal();
disableDangerBannerDiscardingDismissal();
return;
}
const activeUsers = Users.getActiveLocalUserCount();
// callback runs before user is added, so we should add the user
// that is being created to the current value.
const ratio = activeUsers / maxActiveUsers;
const seatsLeft = maxActiveUsers - activeUsers;
if (ratio < 0.8 || ratio >= 1) {
disableWarningBannerDiscardingDismissal();
} else {
enableWarningBanner(seatsLeft);
}
if (ratio < 1) {
disableDangerBannerDiscardingDismissal();
} else {
enableDangerBanner();
}
};
callbacks.add('afterCreateUser', handleMaxSeatsBanners, callbacks.priority.MEDIUM, 'handle-max-seats-banners');
callbacks.add('afterSaveUser', handleMaxSeatsBanners, callbacks.priority.MEDIUM, 'handle-max-seats-banners');
callbacks.add('afterDeleteUser', handleMaxSeatsBanners, callbacks.priority.MEDIUM, 'handle-max-seats-banners');
callbacks.add('afterDeactivateUser', handleMaxSeatsBanners, callbacks.priority.MEDIUM, 'handle-max-seats-banners');
callbacks.add('afterActivateUser', handleMaxSeatsBanners, callbacks.priority.MEDIUM, 'handle-max-seats-banners');
Meteor.startup(() => {
createSeatsLimitBanners();
if (isEnterprise()) {
handleMaxSeatsBanners();
}
onValidateLicenses(handleMaxSeatsBanners);
});

601
package-lock.json generated

@ -746,12 +746,6 @@
"node-releases": "^1.1.71"
}
},
"caniuse-lite": {
"version": "1.0.30001237",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001237.tgz",
"integrity": "sha512-pDHgRndit6p1NR2GhzMbQ6CkRrp4VKuSsqbcLeOQppYPKOYkKT/6ZvZDvKJUqcmtyWIAHuZq3SVS2vc1egCZzw==",
"dev": true
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@ -3586,9 +3580,9 @@
}
},
"@discoveryjs/json-ext": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.5.tgz",
"integrity": "sha512-6nFkfkmSeV/rqSaS4oWHgmpnYw194f6hmWF5is6b0J1naJZoiD0NTc9AiUwPHvWsowkjuHErCZT1wa0jg+BLIA==",
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.3.tgz",
"integrity": "sha512-Fxt+AfXgjMoin2maPIYzFZnQjAXjAL0PHscM5pRTtatFqB+vZxAM9tLp2Optnuw3QOQC40jTNeGYFOMvyf7v9g==",
"dev": true
},
"@emotion/cache": {
@ -5301,19 +5295,37 @@
}
},
"@rocket.chat/css-in-js": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@rocket.chat/css-in-js/-/css-in-js-0.29.0.tgz",
"integrity": "sha512-4lZnRA8mkTp+x769b58hVyfpQSlSHVPlCp95Kk1DOd2b4T7oGlrenTFwUUtPhvDGYf5aI1n27aNkXMrFKfxdEQ==",
"version": "0.6.3-dev.322",
"resolved": "https://registry.npmjs.org/@rocket.chat/css-in-js/-/css-in-js-0.6.3-dev.322.tgz",
"integrity": "sha512-LzlPLlMfO/Brnl7oXe/dWezvsVqUAvu9N8KThp1e9ab0YbQzErOkn0KGzIxKgeFinjVPcdLjPRmawyFItK6yrg==",
"requires": {
"@emotion/hash": "^0.8.0",
"@rocket.chat/memo": "^0.29.0",
"@rocket.chat/css-supports": "^0.6.3-dev.322+b067cee6",
"@rocket.chat/memo": "^0.6.3-dev.322+b067cee6",
"@rocket.chat/stylis-logical-props-middleware": "^0.6.3-dev.322+b067cee6",
"stylis": "^4.0.10"
}
},
"@rocket.chat/css-supports": {
"version": "0.6.3-dev.322",
"resolved": "https://registry.npmjs.org/@rocket.chat/css-supports/-/css-supports-0.6.3-dev.322.tgz",
"integrity": "sha512-86hrV7ctnEu5ancYYkEWI4dS82MQEE5aI1Qln0Vnsc4NstDDvUsc9i5h6hy1RKqekRRpn+HhX0Q3Lh2/4K3wCA==",
"requires": {
"@rocket.chat/memo": "^0.6.3-dev.322+b067cee6",
"tslib": "^2.3.1"
},
"dependencies": {
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
}
}
},
"@rocket.chat/emitter": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@rocket.chat/emitter/-/emitter-0.29.0.tgz",
"integrity": "sha512-HBZnNFiIHQL0Lr5vIPew8WYQ8ztwZQVb+yKZqyBcmK71fWgYPW5SPMd9PaZNkue4D7Sp3SR57kKatmFtD7sKBQ=="
"version": "0.6.3-dev.322",
"resolved": "https://registry.npmjs.org/@rocket.chat/emitter/-/emitter-0.6.3-dev.322.tgz",
"integrity": "sha512-ytprj6pgVdHdxL3dpmeJq/IUd3cLpvaF/OV1uVJ3XtRYqvVnMtqfDXS41AoTlAVFYsD9swqrZBIigxdD4Q9t5g=="
},
"@rocket.chat/eslint-config": {
"version": "0.4.0",
@ -5325,26 +5337,27 @@
}
},
"@rocket.chat/fuselage": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@rocket.chat/fuselage/-/fuselage-0.29.0.tgz",
"integrity": "sha512-K9e501ovUvH0GRRdGfc113hYyUrBJBMLwUZrfIGbiA+gNc1irUNrkupLwwiJKjThIBzC9qWBmgVCAycUB5S7jw==",
"requires": {
"@rocket.chat/css-in-js": "^0.29.0",
"@rocket.chat/fuselage-tokens": "^0.29.0",
"@rocket.chat/memo": "^0.29.0",
"version": "0.6.3-dev.322",
"resolved": "https://registry.npmjs.org/@rocket.chat/fuselage/-/fuselage-0.6.3-dev.322.tgz",
"integrity": "sha512-+0zDTEfSuPDZuwq6T/oxC5klWyEO25U3FgfEDzWIDQpKk74LAMy2ULawQd1O37r7emvg8SPvxy7S9vyQNHnGsg==",
"requires": {
"@rocket.chat/css-in-js": "^0.6.3-dev.322+b067cee6",
"@rocket.chat/css-supports": "^0.6.3-dev.322+b067cee6",
"@rocket.chat/fuselage-tokens": "^0.6.3-dev.322+b067cee6",
"@rocket.chat/memo": "^0.6.3-dev.322+b067cee6",
"invariant": "^2.2.4",
"react-keyed-flatten-children": "^1.3.0"
}
},
"@rocket.chat/fuselage-hooks": {
"version": "0.6.3-dev.326",
"resolved": "https://registry.npmjs.org/@rocket.chat/fuselage-hooks/-/fuselage-hooks-0.6.3-dev.326.tgz",
"integrity": "sha512-pSeMIJ563OnnjP9tRep6HNi527bSjX2LLt6Z0y8DPL17sVabF46gplP2eoEVuZgcEVF5PrYWo0bqEzosL1cOvQ=="
"version": "0.6.3-dev.322",
"resolved": "https://registry.npmjs.org/@rocket.chat/fuselage-hooks/-/fuselage-hooks-0.6.3-dev.322.tgz",
"integrity": "sha512-J2rsvhFPo/by/y2/z6uBTmDuOqAiocXODsYuFUVp4AyNW4LmZ/KD7o9MfPwlwGjlkbU9889JOabhcvgZ3qLkxQ=="
},
"@rocket.chat/fuselage-polyfills": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@rocket.chat/fuselage-polyfills/-/fuselage-polyfills-0.29.0.tgz",
"integrity": "sha512-a1XrcupzEnMTAEj5aHcsu97OIJVoWItQOIZd4s5zxrWLUEY30d/fDvS12pdCB0Yx26jXrUNAgtjsSC/XS/vR7Q==",
"version": "0.6.3-dev.322",
"resolved": "https://registry.npmjs.org/@rocket.chat/fuselage-polyfills/-/fuselage-polyfills-0.6.3-dev.322.tgz",
"integrity": "sha512-8i1i5087FK1t1+ifwLu5lu4LNz370oDhXDKtHL13vYcowE8FVCME12P9rnP349dFmSPQ2ZhpNyDSu9kaUGw6nA==",
"requires": {
"@juggle/resize-observer": "^3.3.1",
"clipboard-polyfill": "^3.0.3",
@ -5355,19 +5368,19 @@
}
},
"@rocket.chat/fuselage-tokens": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@rocket.chat/fuselage-tokens/-/fuselage-tokens-0.29.0.tgz",
"integrity": "sha512-H2onK1pEF9SNd0xOc6zffZaSMnQMbQrfYB8LL0hpaX9fqlZ5xPEwoK7Qzty9ClSHq1AWPaxsOZaSxinoi+lz+w=="
"version": "0.6.3-dev.322",
"resolved": "https://registry.npmjs.org/@rocket.chat/fuselage-tokens/-/fuselage-tokens-0.6.3-dev.322.tgz",
"integrity": "sha512-FDbIEUeANKlZ/X6dVNwZwBHphVoTJRnHuYQUH1Uc65LX4FQe2BlD0dElp6f00z4SGmtSMDuyEWwwiEf2Z61eEA=="
},
"@rocket.chat/fuselage-ui-kit": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@rocket.chat/fuselage-ui-kit/-/fuselage-ui-kit-0.29.0.tgz",
"integrity": "sha512-rNmJvich3pPUiDxRXAuw2Q6hEEndWGHp7TpQiz+MeDMdHn1DbFCgAF/hFAQlFAniDOP1+lOlCGhu/snj9pSqhw=="
"version": "0.6.3-dev.322",
"resolved": "https://registry.npmjs.org/@rocket.chat/fuselage-ui-kit/-/fuselage-ui-kit-0.6.3-dev.322.tgz",
"integrity": "sha512-CVB0IRtpmc94fjQsMEIg6TkC2KX4WXBQ1ZZrOxgmuIcj4HQG/gBVah0B1O3ApQvORDijtJxogGE1oTXoLXrf0A=="
},
"@rocket.chat/icons": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@rocket.chat/icons/-/icons-0.29.0.tgz",
"integrity": "sha512-wDt1q1TbNCBoswKwBLv9/WIFHpgbiXgUk054Vx88sSni97rwHiSqod2fWmfNoaw9kK7NeMdUB08JklEJS4dGFQ=="
"version": "0.6.3-dev.322",
"resolved": "https://registry.npmjs.org/@rocket.chat/icons/-/icons-0.6.3-dev.322.tgz",
"integrity": "sha512-Ew5dIfoOovi/2cZxUboAD6jIr5dsPEwGDKBOxp9qVdP5VeJ+xzniLYK7XGh+4Lv98QIeT3Toi6en/RPcz9c9+g=="
},
"@rocket.chat/livechat": {
"version": "1.9.5",
@ -5422,9 +5435,9 @@
}
},
"@rocket.chat/memo": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@rocket.chat/memo/-/memo-0.29.0.tgz",
"integrity": "sha512-zgav/BgHdIyv0GeM5Zp+GluSww3SEzw0QALHv0uGmNr6LSjLT72xwc6lWMPfFeKTmsrI1WQM6xHwb8JCN8TKOQ==",
"version": "0.6.3-dev.322",
"resolved": "https://registry.npmjs.org/@rocket.chat/memo/-/memo-0.6.3-dev.322.tgz",
"integrity": "sha512-kcedVPwL0tvJaCtJFzDSTjibDw5MMEnvgwLxVykG8zcbjouVtGIL/sKqnee3Z0nqKBGWrvK4IchuPfC0ZYjjlw==",
"requires": {
"tslib": "^2.3.0"
},
@ -5437,9 +5450,9 @@
}
},
"@rocket.chat/message-parser": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@rocket.chat/message-parser/-/message-parser-0.29.0.tgz",
"integrity": "sha512-2qEPTNSHeWRCFfxCETqz9Asju6bf5dR98+AQUYwNus5911WW4tuehvrfvoqLYrlYqs4w5Qj6q9m2THCXqN27Gw=="
"version": "0.6.3-dev.322",
"resolved": "https://registry.npmjs.org/@rocket.chat/message-parser/-/message-parser-0.6.3-dev.322.tgz",
"integrity": "sha512-+0jSX9LIbvVkNuRHZruuW58m7yWyQ+GKiHW9k9maF0/pewDnD48YwxhB51u4eqmqhq6Yqmma36M2ucq5Twd3KA=="
},
"@rocket.chat/mp3-encoder": {
"version": "0.24.0",
@ -5492,9 +5505,9 @@
}
},
"@rocket.chat/string-helpers": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@rocket.chat/string-helpers/-/string-helpers-0.29.0.tgz",
"integrity": "sha512-3cpS4rabgMopg24f0heq9R+o21h6+wRWWgnIOFsO9yd2ZmAWZW6Vt7eKVmVDWjoNL1j0sDP12ogVPHlCnQarVA==",
"version": "0.6.3-dev.322",
"resolved": "https://registry.npmjs.org/@rocket.chat/string-helpers/-/string-helpers-0.6.3-dev.322.tgz",
"integrity": "sha512-Xk4Zzc/WgBwM9aDIQF2pWD71VrbzYfNGzvjx6Ws19eZQ5c4qQT19wu/lLvHvwop1pjwL0AwUO+Xg8hKTJG+WZA==",
"requires": {
"tslib": "^2.3.0"
},
@ -5506,10 +5519,26 @@
}
}
},
"@rocket.chat/stylis-logical-props-middleware": {
"version": "0.6.3-dev.322",
"resolved": "https://registry.npmjs.org/@rocket.chat/stylis-logical-props-middleware/-/stylis-logical-props-middleware-0.6.3-dev.322.tgz",
"integrity": "sha512-Lv4+nKJ75oWh4nj8vZkV+wZkBTjpsmiuR4YhQuW2PJG+LPoXrQkS3BbhB8B7EZKabsPHiyUWeqz5tvcKj5aqoA==",
"requires": {
"@rocket.chat/css-supports": "^0.6.3-dev.322+b067cee6",
"tslib": "^2.2.0"
},
"dependencies": {
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
}
}
},
"@rocket.chat/ui-kit": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/@rocket.chat/ui-kit/-/ui-kit-0.29.0.tgz",
"integrity": "sha512-a3NbyxPe4GlL3jSP1hhGwGPTeU6Fep57l2420SPnljvudgdb2BsBWVE1y5bp64fVYxW62a6b1YY1fVEtXF6EoA=="
"version": "0.6.3-dev.322",
"resolved": "https://registry.npmjs.org/@rocket.chat/ui-kit/-/ui-kit-0.6.3-dev.322.tgz",
"integrity": "sha512-O+1X2keDPnCR1LZztaRtWRPb9h8D8OSqav+pdnhJ3dEQ7i45x8rbwG+SntTl2rW9RIHFZgu2Z9pnoD9W1hy6Pg=="
},
"@samverschueren/stream-to-observable": {
"version": "0.3.1",
@ -8829,14 +8858,6 @@
"lodash.debounce": "^4.0.8",
"resolve": "^1.14.2",
"semver": "^6.1.2"
},
"dependencies": {
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
}
}
},
"@babel/highlight": {
@ -9023,45 +9044,6 @@
"webpack-filter-warnings-plugin": "^1.2.1",
"webpack-hot-middleware": "^2.25.0",
"webpack-virtual-modules": "^0.2.2"
},
"dependencies": {
"find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
"dev": true,
"requires": {
"locate-path": "^6.0.0",
"path-exists": "^4.0.0"
}
},
"locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
"dev": true,
"requires": {
"p-locate": "^5.0.0"
}
},
"p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"dev": true,
"requires": {
"yocto-queue": "^0.1.0"
}
},
"p-locate": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
"dev": true,
"requires": {
"p-limit": "^3.0.2"
}
}
}
},
"@storybook/channel-postmessage": {
@ -9277,16 +9259,6 @@
"resolve": "^1.19.0"
}
},
"find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
"dev": true,
"requires": {
"locate-path": "^6.0.0",
"path-exists": "^4.0.0"
}
},
"fork-ts-checker-webpack-plugin": {
"version": "6.3.3",
"resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.3.3.tgz",
@ -9323,33 +9295,6 @@
}
}
},
"locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
"dev": true,
"requires": {
"p-locate": "^5.0.0"
}
},
"p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"dev": true,
"requires": {
"yocto-queue": "^0.1.0"
}
},
"p-locate": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
"dev": true,
"requires": {
"p-limit": "^3.0.2"
}
},
"semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
@ -9477,45 +9422,6 @@
"webpack": "4",
"webpack-dev-middleware": "^3.7.3",
"webpack-virtual-modules": "^0.2.2"
},
"dependencies": {
"find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
"dev": true,
"requires": {
"locate-path": "^6.0.0",
"path-exists": "^4.0.0"
}
},
"locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
"dev": true,
"requires": {
"p-locate": "^5.0.0"
}
},
"p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"dev": true,
"requires": {
"yocto-queue": "^0.1.0"
}
},
"p-locate": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
"dev": true,
"requires": {
"p-limit": "^3.0.2"
}
}
}
},
"@storybook/node-logger": {
@ -9549,16 +9455,6 @@
"ts-dedent": "^2.0.0"
}
},
"@storybook/semver": {
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/@storybook/semver/-/semver-7.3.2.tgz",
"integrity": "sha512-SWeszlsiPsMI0Ps0jVNtH64cI5c0UF3f7KgjVKJoNP30crQ6wUSddY2hsdeczZXEKVJGEn50Q60flcGsQGIcrg==",
"dev": true,
"requires": {
"core-js": "^3.6.5",
"find-up": "^4.1.0"
}
},
"@storybook/theming": {
"version": "6.3.8",
"resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-6.3.8.tgz",
@ -9617,9 +9513,9 @@
}
},
"@types/node": {
"version": "14.17.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.18.tgz",
"integrity": "sha512-haYyibw4pbteEhkSg0xdDLAI3679L75EJ799ymVrPxOA922bPx3ML59SoDsQ//rHlvqpu+e36kcbR3XRQtFblA==",
"version": "14.17.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.15.tgz",
"integrity": "sha512-D1sdW0EcSCmNdLKBGMYb38YsHUS6JcM7yQ6sLQ9KuZ35ck7LYCKE7kYFHOO59ayFOY3zobWVZxf4KXhYHcHYFA==",
"dev": true
},
"ajv": {
@ -9635,9 +9531,9 @@
}
},
"ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true
},
"ansi-styles": {
@ -9758,33 +9654,6 @@
"ms": "2.1.2"
}
},
"detect-port": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.3.0.tgz",
"integrity": "sha512-E+B1gzkl2gqxt1IhUzwjrxBKRqx1UzC3WLONHinn8S3T6lwV/agVCyitiFOsGJ/eYuEUBvD71MZHy3Pv1G9doQ==",
"dev": true,
"requires": {
"address": "^1.0.1",
"debug": "^2.6.0"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"requires": {
"ms": "2.0.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true
}
}
},
"emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@ -9817,6 +9686,43 @@
"pkg-dir": "^4.1.0"
},
"dependencies": {
"find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"dev": true,
"requires": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
}
},
"locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"dev": true,
"requires": {
"p-locate": "^4.1.0"
}
},
"p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dev": true,
"requires": {
"p-try": "^2.0.0"
}
},
"p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"dev": true,
"requires": {
"p-limit": "^2.2.0"
}
},
"pkg-dir": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
@ -9829,12 +9735,12 @@
}
},
"find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
"dev": true,
"requires": {
"locate-path": "^5.0.0",
"locate-path": "^6.0.0",
"path-exists": "^4.0.0"
}
},
@ -9931,12 +9837,12 @@
}
},
"locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
"dev": true,
"requires": {
"p-locate": "^4.1.0"
"p-locate": "^5.0.0"
}
},
"lru-cache": {
@ -9955,14 +9861,6 @@
"dev": true,
"requires": {
"semver": "^6.0.0"
},
"dependencies": {
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
}
}
},
"markdown-to-jsx": {
@ -9992,9 +9890,9 @@
"dev": true
},
"minipass": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.5.tgz",
"integrity": "sha512-+8NzxD82XQoNKNrl1d/FSi+X8wAEWR+sbYAfIvub4Nz0d22plFG72CEVVaufV8PNf4qSslFTD8VMOxNVhHCjTw==",
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz",
"integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==",
"dev": true,
"requires": {
"yallist": "^4.0.0"
@ -10023,21 +9921,21 @@
"dev": true
},
"p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"dev": true,
"requires": {
"p-try": "^2.0.0"
"yocto-queue": "^0.1.0"
}
},
"p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
"dev": true,
"requires": {
"p-limit": "^2.2.0"
"p-limit": "^3.0.2"
}
},
"p-map": {
@ -10080,45 +9978,6 @@
"dev": true,
"requires": {
"find-up": "^5.0.0"
},
"dependencies": {
"find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
"dev": true,
"requires": {
"locate-path": "^6.0.0",
"path-exists": "^4.0.0"
}
},
"locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
"dev": true,
"requires": {
"p-locate": "^5.0.0"
}
},
"p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"dev": true,
"requires": {
"yocto-queue": "^0.1.0"
}
},
"p-locate": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
"dev": true,
"requires": {
"p-limit": "^3.0.2"
}
}
}
},
"postcss": {
@ -10248,6 +10107,45 @@
"find-up": "^4.1.0",
"read-pkg": "^5.2.0",
"type-fest": "^0.8.1"
},
"dependencies": {
"find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"dev": true,
"requires": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
}
},
"locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"dev": true,
"requires": {
"p-locate": "^4.1.0"
}
},
"p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dev": true,
"requires": {
"p-try": "^2.0.0"
}
},
"p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"dev": true,
"requires": {
"p-limit": "^2.2.0"
}
}
}
},
"regenerator-runtime": {
@ -10266,6 +10164,12 @@
"path-parse": "^1.0.6"
}
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
},
"serialize-javascript": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz",
@ -10350,14 +10254,14 @@
}
},
"terser": {
"version": "5.9.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.9.0.tgz",
"integrity": "sha512-h5hxa23sCdpzcye/7b8YqbE5OwKca/ni0RQz1uRX3tGh8haaGHqcuSqbGRybuAKNdntZ0mDgFNXPJ48xQ2RXKQ==",
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.7.2.tgz",
"integrity": "sha512-0Omye+RD4X7X69O0eql3lC4Heh/5iLj3ggxR/B5ketZLOtLiOqukUgjw3q4PDnNQbsrkKr3UMypqStQG3XKRvw==",
"dev": true,
"requires": {
"commander": "^2.20.0",
"source-map": "~0.7.2",
"source-map-support": "~0.5.20"
"source-map-support": "~0.5.19"
},
"dependencies": {
"commander": {
@ -10397,15 +10301,6 @@
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
"dev": true
},
"p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"dev": true,
"requires": {
"yocto-queue": "^0.1.0"
}
},
"schema-utils": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz",
@ -10642,6 +10537,61 @@
}
}
},
"@storybook/semver": {
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/@storybook/semver/-/semver-7.3.2.tgz",
"integrity": "sha512-SWeszlsiPsMI0Ps0jVNtH64cI5c0UF3f7KgjVKJoNP30crQ6wUSddY2hsdeczZXEKVJGEn50Q60flcGsQGIcrg==",
"dev": true,
"requires": {
"core-js": "^3.6.5",
"find-up": "^4.1.0"
},
"dependencies": {
"find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"dev": true,
"requires": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
}
},
"locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"dev": true,
"requires": {
"p-locate": "^4.1.0"
}
},
"p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dev": true,
"requires": {
"p-try": "^2.0.0"
}
},
"p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"dev": true,
"requires": {
"p-limit": "^2.2.0"
}
},
"path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true
}
}
},
"@storybook/source-loader": {
"version": "6.3.6",
"resolved": "https://registry.npmjs.org/@storybook/source-loader/-/source-loader-6.3.6.tgz",
@ -12366,9 +12316,9 @@
}
},
"@xmldom/xmldom": {
"version": "0.7.5",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.5.tgz",
"integrity": "sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A=="
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.4.tgz",
"integrity": "sha512-wdxC79cvO7PjSM34jATd/RYZuYWQ8y/R7MidZl1NYYlbpFn1+spfjkiR3ZsJfcaTs2IyslBN7VwBBJwrYKM+zw=="
},
"@xtuc/ieee754": {
"version": "1.2.0",
@ -15972,14 +15922,6 @@
"electron-to-chromium": "^1.3.723",
"escalade": "^3.1.1",
"node-releases": "^1.1.71"
},
"dependencies": {
"caniuse-lite": {
"version": "1.0.30001237",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001237.tgz",
"integrity": "sha512-pDHgRndit6p1NR2GhzMbQ6CkRrp4VKuSsqbcLeOQppYPKOYkKT/6ZvZDvKJUqcmtyWIAHuZq3SVS2vc1egCZzw==",
"dev": true
}
}
},
"bs58": {
@ -16010,9 +15952,9 @@
}
},
"bson": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/bson/-/bson-4.5.2.tgz",
"integrity": "sha512-8CEMJpwc7qlQtrn2rney38jQSEeMar847lz0LyitwRmVknAW8iHXrzW4fTjHfyWm0E3sukyD/zppdH+QU1QefA==",
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/bson/-/bson-4.5.1.tgz",
"integrity": "sha512-XqFP74pbTVLyLy5KFxVfTUyRrC1mgOlmu/iXHfXqfCKT59jyP9lwbotGfbN59cHBRbJSamZNkrSopjv+N0SqAA==",
"requires": {
"buffer": "^5.6.0"
}
@ -16322,9 +16264,9 @@
}
},
"caniuse-lite": {
"version": "1.0.30001207",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001207.tgz",
"integrity": "sha512-UPQZdmAsyp2qfCTiMU/zqGSWOYaY9F9LL61V8f+8MrubsaDGpaHD9HRV/EWZGULZn0Hxu48SKzI5DgFwTvHuYw=="
"version": "1.0.30001257",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001257.tgz",
"integrity": "sha512-JN49KplOgHSXpIsVSF+LUyhD8PUp6xPpAXeRrrcBh4KBeP7W864jHn6RvzJgDlrReyeVjMFJL3PLpPvKIxlIHA=="
},
"capital-case": {
"version": "1.0.4",
@ -16702,9 +16644,9 @@
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="
},
"nth-check": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz",
"integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.0.tgz",
"integrity": "sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==",
"requires": {
"boolbase": "^1.0.0"
}
@ -18632,6 +18574,16 @@
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups="
},
"detect-port": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.3.0.tgz",
"integrity": "sha512-E+B1gzkl2gqxt1IhUzwjrxBKRqx1UzC3WLONHinn8S3T6lwV/agVCyitiFOsGJ/eYuEUBvD71MZHy3Pv1G9doQ==",
"dev": true,
"requires": {
"address": "^1.0.1",
"debug": "^2.6.0"
}
},
"detect-port-alt": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz",
@ -20704,9 +20656,9 @@
"integrity": "sha512-YN+CYfCVRVMUZOUPeinHNKgytM1wPI/C/UCLEi56EsY2dwwvI00kIJHJoI7pMVqGoMew8SMZ2SSfHKHULHXDsg=="
},
"fast-safe-stringify": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz",
"integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==",
"dev": true
},
"fast-text-encoding": {
@ -25990,13 +25942,6 @@
"electron-to-chromium": "^1.3.723",
"escalade": "^3.1.1",
"node-releases": "^1.1.71"
},
"dependencies": {
"caniuse-lite": {
"version": "1.0.30001241",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001241.tgz",
"integrity": "sha512-1uoSZ1Pq1VpH0WerIMqwptXHNNGfdl7d1cJUFs80CwQ/lVzdhTvsFZCeNFslze7AjsQnb4C85tzclPa1VShbeQ=="
}
}
},
"chalk": {
@ -34716,9 +34661,9 @@
}
},
"sonic-boom": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.2.3.tgz",
"integrity": "sha512-dm32bzlBchhXoJZe0yLY/kdYsHtXhZphidIcCzJib1aEjfciZyvHJ3NjA1zh6jJCO/OBLfdjc5iw6jLS/Go2fg==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.3.0.tgz",
"integrity": "sha512-lEPaw654/4/rCJHz/TNzV4GIthqCq4inO+O3aFhbdOvR1bE+2//sVkcS+xlqPdb8gdjQCEE0hE9BuvnVixbnWQ==",
"requires": {
"atomic-sleep": "^1.0.0"
}

@ -160,19 +160,19 @@
"@nivo/line": "0.62.0",
"@nivo/pie": "0.73.0",
"@rocket.chat/apps-engine": "1.28.0-alpha.5379",
"@rocket.chat/css-in-js": "^0.29.0",
"@rocket.chat/emitter": "^0.29.0",
"@rocket.chat/fuselage": "^0.29.0",
"@rocket.chat/css-in-js": "^0.6.3-dev.322",
"@rocket.chat/emitter": "^0.6.3-dev.322",
"@rocket.chat/fuselage": "^0.6.3-dev.322",
"@rocket.chat/fuselage-hooks": "^0.6.3-dev.322",
"@rocket.chat/fuselage-polyfills": "^0.29.0",
"@rocket.chat/fuselage-tokens": "^0.29.0",
"@rocket.chat/fuselage-ui-kit": "^0.29.0",
"@rocket.chat/icons": "^0.29.0",
"@rocket.chat/memo": "^0.29.0",
"@rocket.chat/message-parser": "^0.29.0",
"@rocket.chat/fuselage-polyfills": "^0.6.3-dev.322",
"@rocket.chat/fuselage-tokens": "^0.6.3-dev.322",
"@rocket.chat/fuselage-ui-kit": "^0.6.3-dev.322",
"@rocket.chat/icons": "^0.6.3-dev.322",
"@rocket.chat/memo": "^0.6.3-dev.322",
"@rocket.chat/message-parser": "^0.6.3-dev.322",
"@rocket.chat/mp3-encoder": "^0.24.0",
"@rocket.chat/string-helpers": "^0.29.0",
"@rocket.chat/ui-kit": "^0.29.0",
"@rocket.chat/string-helpers": "^0.6.3-dev.322",
"@rocket.chat/ui-kit": "^0.6.3-dev.322",
"@slack/client": "^4.12.0",
"@types/lodash": "^4.14.171",
"adm-zip": "0.4.14",
@ -252,7 +252,7 @@
"node-gcm": "1.0.0",
"node-rsa": "^1.1.1",
"nodemailer": "^6.6.2",
"object-path": "^0.11.6",
"object-path": "^0.11.8",
"pdfjs-dist": "^2.8.335",
"photoswipe": "^4.1.3",
"pino": "^7.0.0-rc.6",

@ -868,6 +868,8 @@
"Close": "Close",
"Close_chat": "Close chat",
"Close_room_description": "You are about to close this chat. Are you sure you want to continue?",
"Close_to_seat_limit_banner_warning": "*You have [__seats__] seats left* \nThis workspace is nearing its seat limit. Once the limit is met no new members can be added. *[Request More Seats](__url__)*",
"Close_to_seat_limit_warning": "New members cannot be created once the seat limit is met.",
"close-livechat-room": "Close Omnichannel Room",
"close-livechat-room_description": "Permission to close the current Omnichannel room",
"close-others-livechat-room": "Close other Omnichannel Room",
@ -950,6 +952,7 @@
"Confirm_password": "Confirm your password",
"Confirmation": "Confirmation",
"Connect": "Connect",
"Connected": "Connected",
"Connect_SSL_TLS": "Connect with SSL/TLS",
"Connection_Closed": "Connection closed",
"Connection_Reset": "Connection reset",
@ -1238,6 +1241,7 @@
"Create_channel": "Create Channel",
"Create_A_New_Channel": "Create a New Channel",
"Create_new": "Create new",
"Create_new_members": "Create New Members",
"Create_unique_rules_for_this_channel": "Create unique rules for this channel",
"create-c": "Create Public Channels",
"create-c_description": "Permission to create public channels",
@ -1380,6 +1384,7 @@
"Department_removed": "Department removed",
"Departments": "Departments",
"Deployment_ID": "Deployment ID",
"Deployment": "Deployment",
"Description": "Description",
"Desktop": "Desktop",
"Desktop_Notification_Test": "Desktop Notification Test",
@ -1712,6 +1717,7 @@
"error-invalid-username": "Invalid username",
"error-invalid-value": "Invalid value",
"error-invalid-webhook-response": "The webhook URL responded with a status other than 200",
"error-license-user-limit-reached": "The maximum number of users has been reached.",
"error-logged-user-not-in-room": "You are not in the room `%s`",
"error-max-guests-number-reached": "You reached the maximum number of guest users allowed by your license. Contact sale@rocket.chat for a new license.",
"error-max-number-simultaneous-chats-reached": "The maximum number of simultaneous chats per agent has been reached.",
@ -2029,6 +2035,7 @@
"get-password-policy-mustContainAtLeastOneSpecialCharacter": "The password should contain at least one special character",
"get-password-policy-mustContainAtLeastOneUppercase": "The password should contain at least one uppercase letter",
"github_no_public_email": "You don't have any email as public email in your GitHub account",
"github_HEAD": "HEAD",
"Give_a_unique_name_for_the_custom_oauth": "Give a unique name for the custom oauth",
"Give_the_application_a_name_This_will_be_seen_by_your_users": "Give the application a name. This will be seen by your users.",
"Global": "Global",
@ -2279,6 +2286,7 @@
"Invitation_HTML_Default": "<h1>You have been invited to <strong>[Site_Name]</strong></h1><p>Go to [Site_URL] and try the best open source chat solution available today!</p>",
"Invitation_Subject": "Invitation Subject",
"Invitation_Subject_Default": "You have been invited to [Site_Name]",
"Invite": "Invite",
"Invite_Link": "Invite Link",
"Invite_user_to_join_channel": "Invite one user to join this channel",
"Invite_user_to_join_channel_all_from": "Invite all users from [#channel] to join this channel",
@ -2657,6 +2665,7 @@
"Load_Balancing": "Load Balancing",
"Load_more": "Load more",
"Load_Rotation": "Load Rotation",
"Loading": "Loading",
"Loading_more_from_history": "Loading more from history",
"Loading_suggestion": "Loading suggestions",
"Loading...": "Loading...",
@ -2947,6 +2956,7 @@
"Monday": "Monday",
"Mongo_storageEngine": "Mongo Storage Engine",
"Mongo_version": "Mongo Version",
"MongoDB": "MongoDB",
"MongoDB_Deprecated": "MongoDB Deprecated",
"MongoDB_version_s_is_deprecated_please_upgrade_your_installation": "MongoDB version %s is deprecated, please upgrade your installation.",
"Monitor_added": "Monitor Added",
@ -3194,6 +3204,7 @@
"Others": "Others",
"OTR": "OTR",
"OTR_is_only_available_when_both_users_are_online": "OTR is only available when both users are online",
"Out_of_seats": "Out of Seats",
"Outgoing_WebHook": "Outgoing WebHook",
"Outgoing_WebHook_Description": "Get data out of Rocket.Chat in real-time.",
"Output_format": "Output format",
@ -3229,6 +3240,7 @@
"Phone": "Phone",
"Phone_already_exists": "Phone already exists",
"Phone_number": "Phone number",
"PID": "PID",
"Pin": "Pin",
"Pin_Message": "Pin Message",
"pin-message": "Pin Message",
@ -3367,6 +3379,7 @@
"Random": "Random",
"RD Station": "RD Station",
"RDStation_Token": "RD Station Token",
"Reached_seat_limit_banner_warning": "*No more seats available* \nThis workspace has reached its seat limit so no more members can join. *[Request More Seats](__url__)*",
"React_when_read_only": "Allow Reacting",
"React_when_read_only_changed_successfully": "Allow reacting when read only changed successfully",
"Reacted_with": "Reacted with",
@ -3450,6 +3463,11 @@
"Report_this_message_question_mark": "Report this message?",
"Reporting": "Reporting",
"Request": "Request",
"Request_seats": "Request Seats",
"Request_more_seats": "Request more seats.",
"Request_more_seats_out_of_seats": "You can not add members because this Workspace is out of seats, please request more seats.",
"Request_more_seats_sales_team": "Once your request is submitted, our Sales Team will look into it and will reach out to you within the next couple of days.",
"Request_more_seats_title": "Request More Seats",
"Request_comment_when_closing_conversation": "Request comment when closing conversation",
"Request_comment_when_closing_conversation_description": "If enabled, the agent will need to set a comment before the conversation is closed.",
"Request_tag_before_closing_chat": "Request tag(s) before closing conversation",
@ -3680,6 +3698,8 @@
"Search_Provider": "Search Provider",
"Search_Rooms": "Search Rooms",
"Search_Users": "Search Users",
"Seats_Available": "__seatsLeft__ Seats Available",
"Seats_usage": "Seats Usage",
"seconds": "seconds",
"Secret_token": "Secret Token",
"Security": "Security",
@ -4288,6 +4308,7 @@
"Update_your_RocketChat": "Update your Rocket.Chat",
"Updated_at": "Updated at",
"Upload": "Upload",
"Uploads": "Uploads",
"Upload_app": "Upload App",
"Upload_file_description": "File description",
"Upload_file_name": "File name",
@ -4302,6 +4323,7 @@
"URL_room_hash_description": "Recommended to enable if the Jitsi instance doesn't use any authentication mechanism.",
"URL_room_prefix": "URL room prefix",
"URL_room_suffix": "URL room suffix",
"Usage": "Usage",
"Use": "Use",
"Use_account_preference": "Use account preference",
"Use_Emojis": "Use Emojis",

@ -3,6 +3,7 @@ import { check } from 'meteor/check';
import { Users } from '../../app/models';
import { hasPermission } from '../../app/authorization';
import { callbacks } from '../../app/callbacks/server';
import { deleteUser } from '../../app/lib/server';
Meteor.methods({
@ -47,6 +48,8 @@ Meteor.methods({
deleteUser(userId, confirmRelinquish);
callbacks.run('afterDeleteUser', user);
return true;
},
});

@ -62,14 +62,23 @@ Meteor.methods({
reason: formData.reason,
};
// Check if user has already been imported and never logged in. If so, set password and let it through
const importedUser = Users.findOneByEmailAddress(formData.email);
let userId;
if (importedUser && importedUser.importIds && importedUser.importIds.length && !importedUser.lastLogin) {
Accounts.setPassword(importedUser._id, userData.password);
userId = importedUser._id;
} else {
userId = Accounts.createUser(userData);
try {
// Check if user has already been imported and never logged in. If so, set password and let it through
const importedUser = Users.findOneByEmailAddress(formData.email);
if (importedUser && importedUser.importIds && importedUser.importIds.length && !importedUser.lastLogin) {
Accounts.setPassword(importedUser._id, userData.password);
userId = importedUser._id;
} else {
userId = Accounts.createUser(userData);
}
} catch (e) {
if (e instanceof Meteor.Error) {
throw e;
}
throw new Meteor.Error(e.message);
}
Users.setName(userId, s.trim(formData.name));

@ -5,7 +5,7 @@ import { hasPermission } from '../../app/authorization';
import { setUserActiveStatus } from '../../app/lib/server/functions/setUserActiveStatus';
Meteor.methods({
setUserActiveStatus(userId, active, confirmRelenquish) {
setUserActiveStatus(userId, active, confirmRelinquish) {
check(userId, String);
check(active, Boolean);
@ -21,7 +21,7 @@ Meteor.methods({
});
}
setUserActiveStatus(userId, active, confirmRelenquish);
setUserActiveStatus(userId, active, confirmRelinquish);
return true;
},

@ -257,7 +257,14 @@ export class ListenersModule {
});
service.onEvent('banner.new', (bannerId): void => {
notifications.notifyLoggedInThisInstance('new-banner', { bannerId });
notifications.notifyLoggedInThisInstance('new-banner', { bannerId }); // deprecated
notifications.notifyLoggedInThisInstance('banner-changed', { bannerId });
});
service.onEvent('banner.disabled', (bannerId): void => {
notifications.notifyLoggedInThisInstance('banner-changed', { bannerId });
});
service.onEvent('banner.enabled', (bannerId): void => {
notifications.notifyLoggedInThisInstance('banner-changed', { bannerId });
});
}
}

@ -14,6 +14,7 @@ import { INPSService } from './types/INPSService';
import { ITeamService } from './types/ITeamService';
import { IRoomService } from './types/IRoomService';
import { IMediaService } from './types/IMediaService';
import { IAnalyticsService } from './types/IAnalyticsService';
import { ILDAPService } from './types/ILDAPService';
// TODO think in a way to not have to pass the service name to proxify here as well
@ -28,6 +29,7 @@ export const NPS = proxifyWithWait<INPSService>('nps');
export const Team = proxifyWithWait<ITeamService>('team');
export const Room = proxifyWithWait<IRoomService>('room');
export const Media = proxifyWithWait<IMediaService>('media');
export const Analytics = proxifyWithWait<IAnalyticsService>('analytics');
export const LDAP = proxifyWithWait<ILDAPService>('ldap');
// Calls without wait. Means that the service is optional and the result may be an error

@ -20,6 +20,8 @@ type ClientAction = 'inserted' | 'updated' | 'removed' | 'changed';
export type EventSignatures = {
'banner.new'(bannerId: string): void;
'banner.enabled'(bannerId: string): void;
'banner.disabled'(bannerId: string): void;
'emoji.deleteCustom'(emoji: IEmoji): void;
'emoji.updateCustom'(emoji: IEmoji): void;
'license.module'(data: { module: string; valid: boolean }): void;

@ -0,0 +1,7 @@
import { IServiceClass } from './ServiceClass';
export interface IAnalyticsService extends IServiceClass {
saveSeatRequest(): Promise<void>;
getSeatRequestCount(): Promise<number>;
resetSeatRequestCount(): Promise<void>;
}

@ -1,7 +1,12 @@
import { BannerPlatform, IBanner } from '../../../definition/IBanner';
import { Optional } from '../../../definition/utils';
export interface IBannerService {
getNewBannersForUser(userId: string, platform: BannerPlatform, bannerId?: string): Promise<IBanner[]>;
create(banner: Omit<IBanner, '_id'>): Promise<IBanner>;
getBannersForUser(userId: string, platform: BannerPlatform, bannerId?: string): Promise<IBanner[]>;
create(banner: Optional<IBanner, '_id'>): Promise<IBanner>;
dismiss(userId: string, bannerId: string): Promise<boolean>;
discardDismissal(bannerId: string): Promise<boolean>;
getById(bannerId: string): Promise<null | IBanner>;
disable(bannerId: string): Promise<boolean>;
enable(bannerId: string, doc?: Partial<Omit<IBanner, '_id'>>): Promise<boolean>;
}

@ -0,0 +1,29 @@
import { Db } from 'mongodb';
import { ServiceClass } from '../../sdk/types/ServiceClass';
import { IAnalyticsService } from '../../sdk/types/IAnalyticsService';
import { AnalyticsRaw } from '../../../app/models/server/raw/Analytics';
export class AnalyticsService extends ServiceClass implements IAnalyticsService {
protected name = 'analytics';
private Analytics: AnalyticsRaw
constructor(db: Db) {
super();
this.Analytics = new AnalyticsRaw(db.collection('rocketchat_analytics'));
}
async saveSeatRequest(): Promise<void> {
this.Analytics.update({ type: 'seat-request' }, { $inc: { count: 1 } }, { upsert: true });
}
async getSeatRequestCount(): Promise<number> {
const result = await this.Analytics.findOne({ type: 'seat-request' });
return result ? result.count : 0;
}
async resetSeatRequestCount(): Promise<void> {
await this.Analytics.update({ type: 'seat-request' }, { $set: { count: 0 } }, { upsert: true });
}
}

@ -9,6 +9,7 @@ import { IBannerService } from '../../sdk/types/IBannerService';
import { BannerPlatform, IBanner, IBannerDismiss } from '../../../definition/IBanner';
import { api } from '../../sdk/api';
import { IUser } from '../../../definition/IUser';
import { Optional } from '../../../definition/utils';
export class BannerService extends ServiceClass implements IBannerService {
protected name = 'banner';
@ -27,26 +28,32 @@ export class BannerService extends ServiceClass implements IBannerService {
this.Users = new UsersRaw(db.collection('users'));
}
async create(doc: Omit<IBanner, '_id'>): Promise<IBanner> {
const bannerId = uuidv4();
async getById(bannerId: string): Promise<null | IBanner> {
return this.Banners.findOneById(bannerId);
}
doc.view.appId = 'banner-core';
doc.view.viewId = bannerId;
async discardDismissal(bannerId: string): Promise<boolean> {
const result = await this.Banners.findOneById(bannerId);
const invalidPlatform = doc.platform?.some((platform) => !Object.values(BannerPlatform).includes(platform));
if (invalidPlatform) {
throw new Error('Invalid platform');
if (!result) {
return false;
}
if (doc.startAt > doc.expireAt) {
throw new Error('Start date cannot be later than expire date');
}
const { _id, ...banner } = result;
if (doc.expireAt < new Date()) {
throw new Error('Cannot create banner already expired');
}
const snapshot = await this.create({ ...banner, snapshot: _id, active: false }); // create a snapshot
await this.BannersDismiss.updateMany({ bannerId }, { $set: { bannerId: snapshot._id } });
return true;
}
async create(doc: Optional<IBanner, '_id'>): Promise<IBanner> {
const bannerId = doc._id || uuidv4();
doc.view.appId = 'banner-core';
doc.view.viewId = bannerId;
await this.Banners.insertOne({
await this.Banners.create({
...doc,
_id: bannerId,
});
@ -61,7 +68,7 @@ export class BannerService extends ServiceClass implements IBannerService {
return banner;
}
async getNewBannersForUser(userId: string, platform: BannerPlatform, bannerId?: string): Promise<IBanner[]> {
async getBannersForUser(userId: string, platform: BannerPlatform, bannerId?: string): Promise<IBanner[]> {
const user = await this.Users.findOneById<Pick<IUser, 'roles'>>(userId, { projection: { roles: 1 } });
const { roles } = user || { roles: [] };
@ -111,4 +118,29 @@ export class BannerService extends ServiceClass implements IBannerService {
return true;
}
async disable(bannerId: string): Promise<boolean> {
const result = await this.Banners.disable(bannerId);
if (result) {
api.broadcast('banner.disabled', bannerId);
return true;
}
return false;
}
async enable(bannerId: string, doc: Partial<Omit<IBanner, '_id'>> = {}): Promise<boolean> {
const result = await this.Banners.findOneById(bannerId);
if (!result) {
return false;
}
const { _id, ...banner } = result;
this.Banners.update({ _id }, { ...banner, ...doc, active: true }); // reenable the banner
api.broadcast('banner.enabled', bannerId);
return true;
}
}

@ -9,6 +9,7 @@ import { RoomService } from './room/service';
import { TeamService } from './team/service';
import { UiKitCoreApp } from './uikit-core-app/service';
import { MediaService } from './image/service';
import { AnalyticsService } from './analytics/service';
import { LDAPService } from './ldap/service';
const { db } = MongoInternals.defaultRemoteCollectionDriver().mongo;
@ -21,4 +22,5 @@ api.registerService(new NPSService(db));
api.registerService(new RoomService(db));
api.registerService(new TeamService(db));
api.registerService(new MediaService());
api.registerService(new AnalyticsService(db));
api.registerService(new LDAPService());

@ -26,7 +26,7 @@ Migrations.add({
const admins = Promise.await(Users.find<IUser>({ roles: 'admin' }, {}).toArray());
const banners = admins.map((a) => Promise.await(Banner.getNewBannersForUser(a._id, BannerPlatform.Web))).flat();
const banners = admins.map((a) => Promise.await(Banner.getBannersForUser(a._id, BannerPlatform.Web))).flat();
const msg = 'Please notice that after the next release (4.0) advanced functionalities of LDAP, SAML, and Custom Oauth will be available only in Enterprise Edition and Gold plan. Check the official announcement for more info: https://go.rocket.chat/i/authentication-changes';
// @ts-ignore
const authBanner = banners.find((b) => b.view.blocks[0].text.text === msg);

Loading…
Cancel
Save