[IMPROVE] Administration UI - React and Fuselage components (#15452)

pull/15770/head^2
Tasso Evangelista 7 years ago committed by Guilherme Gazzo
parent b918d40b82
commit eedc17b310
  1. 11
      .storybook/.babelrc
  2. 2
      .storybook/addons.js
  3. 48
      .storybook/config.js
  4. 1
      .storybook/empty.js
  5. 45
      .storybook/helpers.js
  6. 74
      .storybook/meteor.js
  7. 44
      .storybook/webpack.config.js
  8. 11
      app/apps/client/admin/views.js
  9. 11
      app/apps/client/index.js
  10. 2
      app/apps/client/routes.js
  11. 4
      app/authorization/client/index.js
  12. 9
      app/authorization/client/route.js
  13. 4
      app/authorization/client/views/index.js
  14. 2
      app/callbacks/lib/callbacks.js
  15. 2
      app/cloud/client/admin/index.js
  16. 9
      app/cloud/client/index.js
  17. 3
      app/custom-sounds/client/admin/route.js
  18. 8
      app/custom-sounds/client/admin/views.js
  19. 8
      app/custom-sounds/client/index.js
  20. 3
      app/emoji-custom/client/admin/route.js
  21. 9
      app/emoji-custom/client/admin/views.js
  22. 9
      app/emoji-custom/client/index.js
  23. 8
      app/importer/client/admin/views.js
  24. 44
      app/importer/client/index.js
  25. 11
      app/integrations/client/index.js
  26. 22
      app/integrations/client/route.js
  27. 11
      app/integrations/client/views/index.js
  28. 1
      app/logger/client/index.js
  29. 3
      app/logger/client/viewLogs.js
  30. 4
      app/mail-messages/client/index.js
  31. 6
      app/mail-messages/client/router.js
  32. 4
      app/mail-messages/client/views/index.js
  33. 6
      app/oauth2-server-config/client/admin/route.js
  34. 4
      app/oauth2-server-config/client/admin/views/index.js
  35. 4
      app/oauth2-server-config/client/index.js
  36. 16
      app/theme/client/imports/general/base_old.css
  37. 42
      app/theme/client/vendor/fontello/css/fontello.css
  38. 1
      app/ui-admin/client/SettingsCachedCollection.js
  39. 19
      app/ui-admin/client/index.js
  40. 6
      app/ui-admin/client/rooms/views.js
  41. 19
      app/ui-admin/client/routes.js
  42. 9
      app/ui-admin/client/users/adminUserChannels.html
  43. 29
      app/ui-admin/client/users/adminUserChannels.js
  44. 6
      app/ui-admin/client/users/views.js
  45. 2
      app/ui-cached-collection/client/models/CachedCollection.js
  46. 3
      app/user-status/client/admin/route.js
  47. 9
      app/user-status/client/admin/views.js
  48. 9
      app/user-status/client/index.js
  49. 14
      client/RocketChat.font.css
  50. 10
      client/components/admin/hooks.js
  51. 19
      client/components/admin/info/BuildEnvironmentSection.js
  52. 24
      client/components/admin/info/BuildEnvironmentSection.stories.js
  53. 21
      client/components/admin/info/CommitSection.js
  54. 24
      client/components/admin/info/CommitSection.stories.js
  55. 16
      client/components/admin/info/DescriptionList.js
  56. 20
      client/components/admin/info/DescriptionList.stories.js
  57. 7
      client/components/admin/info/InformationEntry.js
  58. 8
      client/components/admin/info/InformationList.js
  59. 114
      client/components/admin/info/InformationPage.js
  60. 151
      client/components/admin/info/InformationPage.stories.js
  61. 77
      client/components/admin/info/InformationRoute.js
  62. 27
      client/components/admin/info/InstancesSection.js
  63. 32
      client/components/admin/info/InstancesSection.stories.js
  64. 39
      client/components/admin/info/RocketChatSection.js
  65. 36
      client/components/admin/info/RocketChatSection.stories.js
  66. 41
      client/components/admin/info/RuntimeEnvironmentSection.js
  67. 34
      client/components/admin/info/RuntimeEnvironmentSection.stories.js
  68. 28
      client/components/admin/info/SkeletonText.css
  69. 9
      client/components/admin/info/SkeletonText.js
  70. 77
      client/components/admin/info/UsageSection.js
  71. 54
      client/components/admin/info/UsageSection.stories.js
  72. 92
      client/components/admin/settings/GroupPage.js
  73. 23
      client/components/admin/settings/GroupSelector.js
  74. 13
      client/components/admin/settings/NotAuthorizedPage.js
  75. 11
      client/components/admin/settings/NotAuthorizedPage.stories.js
  76. 27
      client/components/admin/settings/ResetSettingButton.js
  77. 51
      client/components/admin/settings/Section.js
  78. 131
      client/components/admin/settings/Setting.js
  79. 27
      client/components/admin/settings/SettingsRoute.js
  80. 384
      client/components/admin/settings/SettingsState.js
  81. 23
      client/components/admin/settings/groups/AssetsGroupPage.js
  82. 17
      client/components/admin/settings/groups/GenericGroupPage.js
  83. 40
      client/components/admin/settings/groups/OAuthGroupPage.js
  84. 44
      client/components/admin/settings/inputs/ActionSettingInput.js
  85. 72
      client/components/admin/settings/inputs/AssetSettingInput.js
  86. 40
      client/components/admin/settings/inputs/BooleanSettingInput.js
  87. 143
      client/components/admin/settings/inputs/CodeSettingInput.js
  88. 112
      client/components/admin/settings/inputs/ColorSettingInput.js
  89. 42
      client/components/admin/settings/inputs/FontSettingInput.js
  90. 42
      client/components/admin/settings/inputs/GenericSettingInput.js
  91. 43
      client/components/admin/settings/inputs/IntSettingInput.js
  92. 49
      client/components/admin/settings/inputs/LanguageSettingInput.js
  93. 42
      client/components/admin/settings/inputs/PasswordSettingInput.js
  94. 43
      client/components/admin/settings/inputs/RelativeUrlSettingInput.js
  95. 88
      client/components/admin/settings/inputs/RoomPickSettingInput.js
  96. 50
      client/components/admin/settings/inputs/SelectSettingInput.js
  97. 56
      client/components/admin/settings/inputs/StringSettingInput.js
  98. 4
      client/components/basic/Button.js
  99. 23
      client/components/basic/Button.stories.js
  100. 4
      client/components/basic/ErrorAlert.stories.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -5,9 +5,14 @@
{
"shippedProposals": true,
"useBuiltIns": "usage",
"corejs": "3"
"corejs": "3",
"modules": "commonjs",
}
],
"@babel/preset-react"
"@babel/preset-react",
"@babel/preset-flow"
],
"plugins": [
"@babel/plugin-proposal-class-properties"
]
}
}

@ -1,2 +1,4 @@
import '@storybook/addon-actions/register';
import '@storybook/addon-knobs/register';
import '@storybook/addon-links/register';
import '@storybook/addon-viewport/register';

@ -1,3 +1,49 @@
import { configure } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { withKnobs }from '@storybook/addon-knobs';
import { MINIMAL_VIEWPORTS, INITIAL_VIEWPORTS } from '@storybook/addon-viewport/dist/defaults';
import { addDecorator, addParameters, configure } from '@storybook/react';
import React from 'react';
import { ConnectionStatusProvider } from '../client/components/providers/ConnectionStatusProvider.mock';
import { TranslationProvider } from '../client/components/providers/TranslationProvider.mock';
addParameters({
viewport: {
viewports: {
...MINIMAL_VIEWPORTS,
...INITIAL_VIEWPORTS,
},
defaultViewport: 'responsive',
},
})
addDecorator(function RocketChatDecorator(fn) {
const linkElement = document.getElementById('theme-styles') || document.createElement('link');
if (linkElement.id !== 'theme-styles') {
require('../app/theme/client/main.css');
require('../app/theme/client/vendor/fontello/css/fontello.css');
require('../client/RocketChat.font.css');
linkElement.setAttribute('id', 'theme-styles');
linkElement.setAttribute('rel', 'stylesheet');
linkElement.setAttribute('href', 'https://open.rocket.chat/theme.css');
document.head.appendChild(linkElement);
}
return <ConnectionStatusProvider connected status='connected' reconnect={action('reconnect')}>
<TranslationProvider>
<style>{`
body {
background-color: white;
}
`}</style>
<div dangerouslySetInnerHTML={{ __html: require('!!raw-loader!../private/public/icons.svg').default }} />
<div className='global-font-family color-primary-font-color'>
{fn()}
</div>
</TranslationProvider>
</ConnectionStatusProvider>;
});
addDecorator(withKnobs);
configure(require.context('../client', true, /\.stories\.js$/), module);

@ -0,0 +1 @@
export default {};

@ -1,44 +1 @@
import { action } from '@storybook/addon-actions';
import '@rocket.chat/icons/dist/font/RocketChat.minimal.css';
import React from 'react';
import '../app/theme/client/main.css';
import { ConnectionStatusProvider } from '../client/components/providers/ConnectionStatusProvider.mock';
import { TranslationProvider } from '../client/components/providers/TranslationProvider.mock';
export const rocketChatWrapper = (fn) =>
<ConnectionStatusProvider connected status='connected' reconnect={action('reconnect')}>
<TranslationProvider>
<style>{`
body {
background-color: white;
}
.global-font-family {
font-family:
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Helvetica Neue',
'Apple Color Emoji',
'Segoe UI Emoji',
'Segoe UI Symbol',
'Meiryo UI',
Arial,
sans-serif;
}
.color-primary-font-color {
color: #444;
}
`}</style>
<div dangerouslySetInnerHTML={{__html: require('!!raw-loader!../private/public/icons.svg').default}} />
<div className='global-font-family color-primary-font-color'>
{fn()}
</div>
</TranslationProvider>
</ConnectionStatusProvider>;
export const dummyDate = new Date(2015, 4, 19);

@ -0,0 +1,74 @@
export const Meteor = {
isClient: true,
isServer: false,
_localStorage: window.localStorage,
absoluteUrl: () => {},
userId: () => {},
Streamer: () => {},
startup: () => {},
methods: () => {},
call: () => {},
};
Meteor.absoluteUrl.defaultOptions = {};
export const Tracker = {
autorun: () => ({
stop: () => {},
}),
nonreactive: (fn) => fn(),
Dependency: () => {},
};
export const Accounts = {};
export const Mongo = {
Collection: () => ({
find: () => ({
observe: () => {},
fetch: () => [],
})
}),
};
export const ReactiveVar = () => ({
get: () => {},
set: () => {},
});
export const ReactiveDict = () => ({
get: () => {},
set: () => {},
all: () => {},
});
export const Template = () => ({
onCreated: () => {},
onRendered: () => {},
onDestroyed: () => {},
helpers: () => {},
events: () => {},
});
Template.registerHelper = () => {};
Template.__checkName = () => {};
export const Blaze = {
Template,
registerHelper: () => {},
};
window.Blaze = Blaze;
export const check = () => {};
export const FlowRouter = {
route: () => {}
};
export const BlazeLayout = {};
export const Session = {
get: () => {},
set: () => {},
};

@ -1,14 +1,11 @@
'use strict';
module.exports = async ({ config, mode }) => {
const cssRule = config.module.rules.find(({ test }) => test.test('index.css'));
cssRule.use[1].options.url = (url, resourcePath) => {
if (/^(\.\/)?images\//.test(url)) {
return false;
}
const path = require('path');
const webpack = require('webpack');
return true;
};
module.exports = async ({ config }) => {
const cssRule = config.module.rules.find(({ test }) => test.test('index.css'));
cssRule.use[2].options.plugins = [
require('postcss-custom-properties')({ preserve: true }),
@ -16,7 +13,36 @@ module.exports = async ({ config, mode }) => {
require('postcss-selector-not')(),
require('postcss-nested')(),
require('autoprefixer')(),
require('postcss-url')({ url: ({ absolutePath, relativePath, url }) => {
const absoluteDir = absolutePath.slice(0, -relativePath.length);
const relativeDir = path.relative(absoluteDir, path.resolve(__dirname, '../public'));
const newPath = path.join(relativeDir, url);
return newPath;
} }),
];
return config;
config.module.rules.push({
test: /\.info$/,
type: 'json',
});
config.module.rules.push({
test: /\.html$/,
use: '@settlin/spacebars-loader',
});
config.plugins.push(new webpack.NormalModuleReplacementPlugin(
/^meteor/,
require.resolve('./meteor.js'),
));
config.plugins.push(new webpack.NormalModuleReplacementPlugin(
/\.\/server\/index.js/,
require.resolve('./empty.js'),
));
config.mode = 'development';
config.optimization.usedExports = true;
return config;
};

@ -0,0 +1,11 @@
import './modalTemplates/iframeModal.html';
import './modalTemplates/iframeModal';
import './marketplace';
import './apps';
import './appInstall.html';
import './appInstall';
import './appLogs.html';
import './appLogs';
import './appManage';
import './appWhatIsIt.html';
import './appWhatIsIt';

@ -1,14 +1,3 @@
import './admin/modalTemplates/iframeModal.html';
import './admin/modalTemplates/iframeModal';
import './admin/marketplace';
import './admin/apps';
import './admin/appInstall.html';
import './admin/appInstall';
import './admin/appLogs.html';
import './admin/appLogs';
import './admin/appManage';
import './admin/appWhatIsIt.html';
import './admin/appWhatIsIt';
import './routes';
export { Apps } from './orchestrator';

@ -7,6 +7,7 @@ FlowRouter.route('/admin/apps/what-is-it', {
name: 'apps-what-is-it',
action: async () => {
// TODO: render loading indicator
await import('./admin/views');
if (await Apps.isEnabled()) {
FlowRouter.go('apps');
} else {
@ -18,6 +19,7 @@ FlowRouter.route('/admin/apps/what-is-it', {
const createAppsRouteAction = (centerTemplate) => async () => {
// TODO: render loading indicator
if (await Apps.isEnabled()) {
await import('./admin/views');
BlazeLayout.render('main', { center: centerTemplate, old: true }); // TODO remove old
} else {
FlowRouter.go('apps-what-is-it');

@ -4,10 +4,6 @@ import './usersNameChanged';
import './requiresPermission.html';
import './route';
import './startup';
import './views/permissions.html';
import './views/permissions';
import './views/permissionsRole.html';
import './views/permissionsRole';
export {
hasAllPermission,

@ -5,7 +5,8 @@ import { t } from '../../utils/client';
FlowRouter.route('/admin/permissions', {
name: 'admin-permissions',
action(/* params*/) {
async action(/* params*/) {
await import('./views');
return BlazeLayout.render('main', {
center: 'permissions',
pageTitle: t('Permissions'),
@ -15,7 +16,8 @@ FlowRouter.route('/admin/permissions', {
FlowRouter.route('/admin/permissions/:name?/edit', {
name: 'admin-permissions-edit',
action(/* params*/) {
async action(/* params*/) {
await import('./views');
return BlazeLayout.render('main', {
center: 'pageContainer',
pageTitle: t('Role_Editing'),
@ -26,7 +28,8 @@ FlowRouter.route('/admin/permissions/:name?/edit', {
FlowRouter.route('/admin/permissions/new', {
name: 'admin-permissions-new',
action(/* params*/) {
async action(/* params*/) {
await import('./views');
return BlazeLayout.render('main', {
center: 'pageContainer',
pageTitle: t('Role_Editing'),

@ -0,0 +1,4 @@
import './permissions.html';
import './permissions';
import './permissionsRole.html';
import './permissionsRole';

@ -56,7 +56,7 @@ const createCallbackTimed = (hook, callbacks) =>
const create = (hook, cbs) =>
(timed ? createCallbackTimed(hook, cbs) : createCallback(hook, cbs));
const combinedCallbacks = new Map();
this.combinedCallbacks = combinedCallbacks;
/*
* Callback priorities
*/

@ -0,0 +1,2 @@
import './cloud';
import './callback';

@ -1,6 +1,3 @@
import './admin/callback';
import './admin/cloud';
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
import { FlowRouter } from 'meteor/kadira:flow-router';
@ -9,14 +6,16 @@ import { hasAtLeastOnePermission } from '../../authorization';
FlowRouter.route('/admin/cloud', {
name: 'cloud-config',
action() {
async action() {
await import('./admin');
BlazeLayout.render('main', { center: 'cloud', old: true });
},
});
FlowRouter.route('/admin/cloud/oauth-callback', {
name: 'cloud-oauth-callback',
action() {
async action() {
await import('./admin');
BlazeLayout.render('main', { center: 'cloudCallback', old: true });
},
});

@ -7,7 +7,8 @@ FlowRouter.route('/admin/custom-sounds', {
subscriptions(/* params, queryParams*/) {
this.register('customSounds', Meteor.subscribe('customSounds'));
},
action(/* params*/) {
async action(/* params*/) {
await import('./views');
BlazeLayout.render('main', { center: 'adminSounds' });
},
});

@ -0,0 +1,8 @@
import './adminSoundEdit.html';
import './adminSoundInfo.html';
import './adminSounds.html';
import './adminSounds';
import './soundEdit.html';
import './soundEdit';
import './soundInfo.html';
import './soundInfo';

@ -1,13 +1,5 @@
import './notifications/deleteCustomSound';
import './notifications/updateCustomSound';
import './admin/adminSoundEdit.html';
import './admin/adminSoundInfo.html';
import './admin/adminSounds.html';
import './admin/adminSounds';
import './admin/soundEdit.html';
import './admin/soundEdit';
import './admin/soundInfo.html';
import './admin/soundInfo';
import './admin/route';
import './admin/startup';

@ -3,7 +3,8 @@ import { BlazeLayout } from 'meteor/kadira:blaze-layout';
FlowRouter.route('/admin/emoji-custom', {
name: 'emoji-custom',
action(/* params*/) {
async action(/* params*/) {
await import('./views');
BlazeLayout.render('main', { center: 'adminEmoji' });
},
});

@ -0,0 +1,9 @@
import './adminEmoji.html';
import './adminEmoji';
import './adminEmojiEdit.html';
import './adminEmojiInfo.html';
import './emojiEdit.html';
import './emojiEdit';
import './emojiInfo.html';
import './emojiInfo';
import './emojiPreview.html';

@ -2,13 +2,4 @@ import './lib/emojiCustom';
import './notifications/deleteEmojiCustom';
import './notifications/updateEmojiCustom';
import './admin/startup';
import './admin/adminEmoji.html';
import './admin/adminEmoji';
import './admin/adminEmojiEdit.html';
import './admin/adminEmojiInfo.html';
import './admin/emojiEdit.html';
import './admin/emojiEdit';
import './admin/emojiInfo.html';
import './admin/emojiInfo';
import './admin/emojiPreview.html';
import './admin/route';

@ -0,0 +1,8 @@
import './adminImport.html';
import './adminImport';
import './adminImportHistory.html';
import './adminImportHistory';
import './adminImportPrepare.html';
import './adminImportPrepare';
import './adminImportProgress.html';
import './adminImportProgress';

@ -1,15 +1,43 @@
import { FlowRouter } from 'meteor/kadira:flow-router';
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
import { ImporterWebsocketReceiver } from './ImporterWebsocketReceiver';
import { Importers } from '../lib/Importers';
import { ImporterInfo } from '../lib/ImporterInfo';
import { ProgressStep } from '../lib/ImporterProgressStep';
import './admin/adminImport.html';
import './admin/adminImport';
import './admin/adminImportHistory.html';
import './admin/adminImportHistory';
import './admin/adminImportPrepare.html';
import './admin/adminImportPrepare';
import './admin/adminImportProgress.html';
import './admin/adminImportProgress';
FlowRouter.route('/admin/import', {
name: 'admin-import',
async action() {
await import('./admin/views');
BlazeLayout.render('main', { center: 'adminImport' });
},
});
FlowRouter.route('/admin/import/history', {
name: 'admin-import-history',
async action() {
await import('./admin/views');
BlazeLayout.render('main', { center: 'adminImportHistory' });
},
});
FlowRouter.route('/admin/import/prepare/:importer', {
name: 'admin-import-prepare',
async action() {
await import('./admin/views');
BlazeLayout.render('main', { center: 'adminImportPrepare' });
},
});
FlowRouter.route('/admin/import/progress/:importer', {
name: 'admin-import-progress',
async action() {
await import('./admin/views');
BlazeLayout.render('main', { center: 'adminImportProgress' });
},
});
export {
Importers,

@ -2,14 +2,3 @@ import '../lib/rocketchat';
import './collections';
import './startup';
import './route';
import './views/integrations.html';
import './views/integrations';
import './views/integrationsNew.html';
import './views/integrationsNew';
import './views/integrationsIncoming.html';
import './views/integrationsIncoming';
import './views/integrationsOutgoing.html';
import './views/integrationsOutgoing';
import './views/integrationsOutgoingHistory.html';
import './views/integrationsOutgoingHistory';
import './views/additional/zapier.html';

@ -4,12 +4,17 @@ import { BlazeLayout } from 'meteor/kadira:blaze-layout';
import { t } from '../../utils';
const dynamic = () => {
import('./views');
};
FlowRouter.route('/admin/integrations', {
name: 'admin-integrations',
subscriptions() {
this.register('integrations', Meteor.subscribe('integrations'));
},
action() {
async action() {
await dynamic();
return BlazeLayout.render('main', {
center: 'integrations',
pageTitle: t('Integrations'),
@ -22,7 +27,8 @@ FlowRouter.route('/admin/integrations/new', {
subscriptions() {
this.register('integrations', Meteor.subscribe('integrations'));
},
action() {
async action() {
await dynamic();
return BlazeLayout.render('main', {
center: 'integrationsNew',
pageTitle: t('Integration_New'),
@ -35,7 +41,8 @@ FlowRouter.route('/admin/integrations/incoming/:id?', {
subscriptions() {
this.register('integrations', Meteor.subscribe('integrations'));
},
action(params) {
async action(params) {
await dynamic();
return BlazeLayout.render('main', {
center: 'pageSettingsContainer',
pageTitle: t('Integration_Incoming_WebHook'),
@ -47,7 +54,8 @@ FlowRouter.route('/admin/integrations/incoming/:id?', {
FlowRouter.route('/admin/integrations/outgoing/:id?', {
name: 'admin-integrations-outgoing',
action(params) {
async action(params) {
await dynamic();
return BlazeLayout.render('main', {
center: 'integrationsOutgoing',
pageTitle: t('Integration_Outgoing_WebHook'),
@ -58,7 +66,8 @@ FlowRouter.route('/admin/integrations/outgoing/:id?', {
FlowRouter.route('/admin/integrations/outgoing/:id?/history', {
name: 'admin-integrations-outgoing-history',
action(params) {
async action(params) {
await dynamic();
return BlazeLayout.render('main', {
center: 'integrationsOutgoingHistory',
pageTitle: t('Integration_Outgoing_WebHook_History'),
@ -69,7 +78,8 @@ FlowRouter.route('/admin/integrations/outgoing/:id?/history', {
FlowRouter.route('/admin/integrations/additional/zapier', {
name: 'admin-integrations-additional-zapier',
action() {
async action() {
await dynamic();
BlazeLayout.render('main', {
center: 'integrationsAdditionalZapier',
});

@ -0,0 +1,11 @@
import './integrations.html';
import './integrations';
import './integrationsNew.html';
import './integrationsNew';
import './integrationsIncoming.html';
import './integrationsIncoming';
import './integrationsOutgoing.html';
import './integrationsOutgoing';
import './integrationsOutgoingHistory.html';
import './integrationsOutgoingHistory';
import './additional/zapier.html';

@ -1,3 +1,2 @@
import './logger';
import './viewLogs';
import './views/viewLogs';

@ -22,7 +22,8 @@ Meteor.startup(function() {
FlowRouter.route('/admin/view-logs', {
name: 'admin-view-logs',
action() {
async action() {
await import('./views/viewLogs');
return BlazeLayout.render('main', {
center: 'pageSettingsContainer',
pageTitle: t('View_Logs'),

@ -1,6 +1,2 @@
import './startup';
import './router';
import './views/mailer.html';
import './views/mailer';
import './views/mailerUnsubscribe.html';
import './views/mailerUnsubscribe';

@ -4,7 +4,8 @@ import { BlazeLayout } from 'meteor/kadira:blaze-layout';
FlowRouter.route('/admin/mailer', {
name: 'admin-mailer',
action() {
async action() {
await import('./views');
return BlazeLayout.render('main', {
center: 'mailer',
});
@ -13,7 +14,8 @@ FlowRouter.route('/admin/mailer', {
FlowRouter.route('/mailer/unsubscribe/:_id/:createdAt', {
name: 'mailer-unsubscribe',
action(params) {
async action(params) {
await import('./views');
Meteor.call('Mailer:unsubscribe', params._id, params.createdAt);
return BlazeLayout.render('mailerUnsubscribe');
},

@ -0,0 +1,4 @@
import './mailer.html';
import './mailer';
import './mailerUnsubscribe.html';
import './mailerUnsubscribe';

@ -5,7 +5,8 @@ import { t } from '../../../utils';
FlowRouter.route('/admin/oauth-apps', {
name: 'admin-oauth-apps',
action() {
async action() {
await import('./views');
return BlazeLayout.render('main', {
center: 'oauthApps',
pageTitle: t('OAuth_Applications'),
@ -15,7 +16,8 @@ FlowRouter.route('/admin/oauth-apps', {
FlowRouter.route('/admin/oauth-app/:id?', {
name: 'admin-oauth-app',
action(params) {
async action(params) {
await import('./views');
return BlazeLayout.render('main', {
center: 'pageSettingsContainer',
pageTitle: t('OAuth_Application'),

@ -0,0 +1,4 @@
import './oauthApp.html';
import './oauthApp';
import './oauthApps.html';
import './oauthApps';

@ -2,7 +2,3 @@ import './oauth/oauth2-client.html';
import './oauth/oauth2-client';
import './admin/startup';
import './admin/route';
import './admin/views/oauthApp.html';
import './admin/views/oauthApp';
import './admin/views/oauthApps.html';
import './admin/views/oauthApps';

@ -383,7 +383,7 @@
/* input & form styles */
.rc-old input:focus {
.rc-old :not(.rcx-input-control):focus {
outline: none;
box-shadow: 0 0 0;
}
@ -4196,20 +4196,6 @@ rc-old select,
}
}
& a.meteor {
position: fixed;
right: 30px;
bottom: 20px;
width: 100px;
height: 50px;
text-indent: -9999em;
background: url(images/meteor.png) no-repeat center center;
background-size: 100% auto;
}
& .share {
min-height: 40px;

@ -1,60 +1,50 @@
@font-face {
font-family: 'fontello';
src: url('../font/fontello.eot?9377982');
src: url('../font/fontello.eot?9377982#iefix') format('embedded-opentype'),
url('../font/fontello.woff2?9377982') format('woff2'),
url('../font/fontello.woff?9377982') format('woff'),
url('../font/fontello.ttf?9377982') format('truetype'),
url('../font/fontello.svg?9377982#fontello') format('svg');
src: url('/font/fontello.eot');
src: url('/font/fontello.eot#iefix') format('embedded-opentype'),
url('/font/fontello.woff2') format('woff2'),
url('/font/fontello.woff') format('woff'),
url('/font/fontello.ttf') format('truetype'),
url('/font/fontello.svg#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
/*
@media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face {
font-family: 'fontello';
src: url('../font/fontello.svg?9377982#fontello') format('svg');
}
}
*/
[class^="icon-"]:before, [class*=" icon-"]:before {
font-family: "fontello";
font-style: normal;
font-weight: normal;
speak: none;
display: inline-block;
text-decoration: inherit;
width: 1em;
margin-right: .2em;
text-align: center;
/* opacity: .8; */
/* For safety - reset parent styles, that can break glyph codes*/
font-variant: normal;
text-transform: none;
/* fix buttons height, for twitter bootstrap */
line-height: 1em;
/* Animation center compensation - margins should be symmetric */
/* remove if not needed */
margin-left: .2em;
/* you can be more comfortable with increased icons size */
/* font-size: 120%; */
/* Font smoothing. That was taken from TWBS */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* Uncomment for 3D effect */
/* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
}
.icon-parking:before { content: '\21'; } /* '!' */
.icon-bedroom:before { content: '\22'; } /* '&quot;' */
.icon-elevator:before { content: '\23'; } /* '#' */
@ -540,4 +530,4 @@
.icon-markdown:before { content: '\e98c'; } /* '' */
.icon-no-newline:before { content: '\e98d'; } /* '' */
.icon-tools:before { content: '\e98e'; } /* '' */
.icon-tape:before { content: '\e98f'; } /* '' */
.icon-tape:before { content: '\e98f'; } /* '' */

@ -6,7 +6,6 @@ export class PrivateSettingsCachedCollection extends CachedCollection {
super({
name: 'private-settings',
eventType: 'onLogged',
useCache: false,
});
}

@ -1,18 +1,9 @@
import './admin.html';
import './adminFlex.html';
import './rooms/adminRooms.html';
import './rooms/adminRoomInfo.html';
import './rooms/adminRoomInfo';
import './rooms/channelSettingsDefault.html';
import './rooms/channelSettingsDefault';
import './users/adminInviteUser.html';
import './users/adminUserChannels.html';
import './users/adminUserEdit.html';
import './users/adminUserInfo.html';
import './users/adminUsers.html';
import './admin';
import './adminFlex';
import './rooms/adminRooms';
import './users/adminInviteUser';
import './users/adminUserChannels';
import './users/adminUsers';
import './routes';
// import './users/adminUserChannels';
// import './users/adminUserChannels.html';

@ -0,0 +1,6 @@
import './adminRooms.html';
import './adminRoomInfo.html';
import './adminRoomInfo';
import './channelSettingsDefault.html';
import './channelSettingsDefault';
import './adminRooms';

@ -0,0 +1,19 @@
import { FlowRouter } from 'meteor/kadira:flow-router';
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
FlowRouter.route('/admin/users', {
name: 'admin-users',
async action() {
await import('./users/views');
BlazeLayout.render('main', { center: 'adminUsers' });
},
});
FlowRouter.route('/admin/rooms', {
name: 'admin-rooms',
async action() {
await import('./rooms/views');
BlazeLayout.render('main', { center: 'adminRooms' });
},
});

@ -1,9 +0,0 @@
<template name="adminUserChannels">
{{#unless hasPermission 'view-full-other-user-info'}}
<p>{{_ "You_are_not_authorized_to_view_this_page"}}</p>
{{else}}
<div class="user-info-channel">
<h3><a href="{{route}}"><i class="icon-{{type}}"></i> {{name}}</a></h3>
</div>
{{/unless}}
</template>

@ -1,29 +0,0 @@
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
Template.adminUserChannels.helpers({
type() {
if (this.t === 'd') {
return 'at';
} if (this.t === 'p') {
return 'lock';
}
return 'hash';
},
route() {
switch (this.t) {
case 'd':
return FlowRouter.path('direct', {
username: this.name,
});
case 'p':
return FlowRouter.path('group', {
name: this.name,
});
case 'c':
return FlowRouter.path('channel', {
name: this.name,
});
}
},
});

@ -0,0 +1,6 @@
import './adminInviteUser.html';
import './adminUserEdit.html';
import './adminUserInfo.html';
import './adminUsers.html';
import './adminInviteUser';
import './adminUsers';

@ -131,7 +131,6 @@ export class CachedCollection extends EventEmitter {
userRelated = true,
listenChangesForLoggedUsersOnly = false,
useSync = true,
useCache = true,
version = 8,
maxCacheTime = 60 * 60 * 24 * 30,
onSyncData = (/* action, record */) => {},
@ -146,7 +145,6 @@ export class CachedCollection extends EventEmitter {
this.eventName = eventName || `${ name }-changed`;
this.eventType = eventType;
this.useSync = useSync;
this.useCache = useCache;
this.listenChangesForLoggedUsersOnly = listenChangesForLoggedUsersOnly;
this.version = version;
this.userRelated = userRelated;

@ -3,7 +3,8 @@ import { BlazeLayout } from 'meteor/kadira:blaze-layout';
FlowRouter.route('/admin/user-status-custom', {
name: 'user-status-custom',
action(/* params */) {
async action(/* params */) {
await import('./views');
BlazeLayout.render('main', { center: 'adminUserStatus' });
},
});

@ -0,0 +1,9 @@
import './adminUserStatus.html';
import './adminUserStatus';
import './adminUserStatusEdit.html';
import './adminUserStatusInfo.html';
import './userStatusEdit.html';
import './userStatusEdit';
import './userStatusInfo.html';
import './userStatusInfo';
import './userStatusPreview.html';

@ -1,12 +1,3 @@
import './admin/adminUserStatus.html';
import './admin/adminUserStatus';
import './admin/adminUserStatusEdit.html';
import './admin/adminUserStatusInfo.html';
import './admin/userStatusEdit.html';
import './admin/userStatusEdit';
import './admin/userStatusInfo.html';
import './admin/userStatusInfo';
import './admin/userStatusPreview.html';
import './admin/route';
import './admin/startup';

@ -1,14 +0,0 @@
@font-face {
font-family: 'RocketChat';
font-weight: 400;
font-style: normal;
font-display: auto;
src: url('/fonts/RocketChat.eot');
src:
url('/fonts/RocketChat.eot?#iefix') format('embedded-opentype'),
url('/fonts/RocketChat.woff2') format('woff2'),
url('/fonts/RocketChat.woff') format('woff'),
url('/fonts/RocketChat.ttf') format('truetype'),
url('/fonts/RocketChat.svg#RocketChat') format('svg');
}

@ -0,0 +1,10 @@
import { useEffect } from 'react';
import { SideNav } from '../../../app/ui-utils/client';
export const useAdminSideNav = () => {
useEffect(() => {
SideNav.setFlex('adminFlex');
SideNav.openFlex();
}, []);
};

@ -1,8 +1,7 @@
import React from 'react';
import { useTranslation } from '../../contexts/TranslationContext';
import { InformationList } from './InformationList';
import { InformationEntry } from './InformationEntry';
import { useTranslation } from '../../providers/TranslationProvider';
import { DescriptionList } from './DescriptionList';
import { formatDate } from './formatters';
export function BuildEnvironmentSection({ info }) {
@ -11,12 +10,12 @@ export function BuildEnvironmentSection({ info }) {
return <>
<h3>{t('Build_Environment')}</h3>
<InformationList>
<InformationEntry label={t('OS_Platform')}>{build.platform}</InformationEntry>
<InformationEntry label={t('OS_Arch')}>{build.arch}</InformationEntry>
<InformationEntry label={t('OS_Release')}>{build.osRelease}</InformationEntry>
<InformationEntry label={t('Node_version')}>{build.nodeVersion}</InformationEntry>
<InformationEntry label={t('Date')}>{formatDate(build.date)}</InformationEntry>
</InformationList>
<DescriptionList>
<DescriptionList.Entry label={t('OS_Platform')}>{build.platform}</DescriptionList.Entry>
<DescriptionList.Entry label={t('OS_Arch')}>{build.arch}</DescriptionList.Entry>
<DescriptionList.Entry label={t('OS_Release')}>{build.osRelease}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Node_version')}>{build.nodeVersion}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Date')}>{formatDate(build.date)}</DescriptionList.Entry>
</DescriptionList>
</>;
}

@ -0,0 +1,24 @@
import React from 'react';
import { dummyDate } from '../../../../.storybook/helpers';
import { BuildEnvironmentSection } from './BuildEnvironmentSection';
export default {
title: 'admin/info/BuildEnvironmentSection',
component: BuildEnvironmentSection,
decorators: [
(fn) => <div className='rc-old'>{fn()}</div>,
],
};
const info = {
compile: {
platform: 'info.compile.platform',
arch: 'info.compile.arch',
osRelease: 'info.compile.osRelease',
nodeVersion: 'info.compile.nodeVersion',
date: dummyDate,
},
};
export const _default = () => <BuildEnvironmentSection info={info} />;

@ -1,8 +1,7 @@
import React from 'react';
import { useTranslation } from '../../contexts/TranslationContext';
import { InformationList } from './InformationList';
import { InformationEntry } from './InformationEntry';
import { useTranslation } from '../../providers/TranslationProvider';
import { DescriptionList } from './DescriptionList';
export function CommitSection({ info }) {
const t = useTranslation();
@ -10,13 +9,13 @@ export function CommitSection({ info }) {
return <>
<h3>{t('Commit')}</h3>
<InformationList>
<InformationEntry label={t('Hash')}>{commit.hash}</InformationEntry>
<InformationEntry label={t('Date')}>{commit.date}</InformationEntry>
<InformationEntry label={t('Branch')}>{commit.branch}</InformationEntry>
<InformationEntry label={t('Tag')}>{commit.tag}</InformationEntry>
<InformationEntry label={t('Author')}>{commit.author}</InformationEntry>
<InformationEntry label={t('Subject')}>{commit.subject}</InformationEntry>
</InformationList>
<DescriptionList>
<DescriptionList.Entry label={t('Hash')}>{commit.hash}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Date')}>{commit.date}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Branch')}>{commit.branch}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Tag')}>{commit.tag}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Author')}>{commit.author}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Subject')}>{commit.subject}</DescriptionList.Entry>
</DescriptionList>
</>;
}

@ -0,0 +1,24 @@
import React from 'react';
import { CommitSection } from './CommitSection';
export default {
title: 'admin/info/CommitSection',
component: CommitSection,
decorators: [
(fn) => <div className='rc-old'>{fn()}</div>,
],
};
const info = {
commit: {
hash: 'info.commit.hash',
date: 'info.commit.date',
branch: 'info.commit.branch',
tag: 'info.commit.tag',
author: 'info.commit.author',
subject: 'info.commit.subject',
},
};
export const _default = () => <CommitSection info={info} />;

@ -0,0 +1,16 @@
import React from 'react';
export const DescriptionList = ({ children }) =>
<table className='statistics-table secondary-background-color'>
<tbody>
{children}
</tbody>
</table>;
const Entry = ({ children, label }) =>
<tr className='admin-table-row'>
<th className='content-background-color border-component-color'>{label}</th>
<td className='border-component-color'>{children}</td>
</tr>;
DescriptionList.Entry = Entry;

@ -0,0 +1,20 @@
import React from 'react';
import { DescriptionList } from './DescriptionList';
export default {
title: 'admin/info/DescriptionList',
component: DescriptionList,
decorators: [
(fn) => <div className='rc-old'>{fn()}</div>,
(fn) => <section className='page-container page-list'>
<div className='content'>
{fn()}
</div>
</section>,
],
};
export const _default = () => <DescriptionList>
<DescriptionList.Entry label='Key'>Value</DescriptionList.Entry>
</DescriptionList>;

@ -1,7 +0,0 @@
import React from 'react';
export const InformationEntry = ({ children, label }) =>
<tr className='admin-table-row'>
<th className='content-background-color border-component-color'>{label}</th>
<td className='border-component-color'>{children}</td>
</tr>;

@ -1,8 +0,0 @@
import React from 'react';
export const InformationList = ({ children }) =>
<table className='statistics-table secondary-background-color'>
<tbody>
{children}
</tbody>
</table>;

@ -1,15 +1,10 @@
import { Button, Icon } from '@rocket.chat/fuselage';
import React, { useEffect, useState } from 'react';
import React from 'react';
import { call } from '../../../../app/ui-utils/client/lib/callMethod';
import { useViewStatisticsPermission } from '../../../hooks/usePermissions';
import { useReactiveValue } from '../../../hooks/useReactiveValue';
import { Info } from '../../../../app/utils';
import { SideNav } from '../../../../app/ui-utils/client/lib/SideNav';
import { Header } from '../../header/Header';
import { Link } from '../../basic/Link';
import { ErrorAlert } from '../../basic/ErrorAlert';
import { useTranslation } from '../../contexts/TranslationContext';
import { Header } from '../../header/Header';
import { useTranslation } from '../../providers/TranslationProvider';
import { RocketChatSection } from './RocketChatSection';
import { CommitSection } from './CommitSection';
import { RuntimeEnvironmentSection } from './RuntimeEnvironmentSection';
@ -17,95 +12,30 @@ import { BuildEnvironmentSection } from './BuildEnvironmentSection';
import { UsageSection } from './UsageSection';
import { InstancesSection } from './InstancesSection';
const useStatistics = (canViewStatistics) => {
const [isLoading, setLoading] = useState(true);
const [statistics, setStatistics] = useState({});
const [instances, setInstances] = useState([]);
const [fetchStatistics, setFetchStatistics] = useState(() => () => ({}));
useEffect(() => {
let didCancel = false;
const fetchStatistics = async () => {
if (!canViewStatistics) {
setStatistics(null);
setInstances(null);
return;
}
setLoading(true);
try {
const [statistics, instances] = await Promise.all([
call('getStatistics'),
call('instances/get'),
]);
if (didCancel) {
return;
}
setStatistics(statistics);
setInstances(instances);
} finally {
setLoading(false);
}
};
setFetchStatistics(() => fetchStatistics);
fetchStatistics();
return () => {
didCancel = true;
};
}, [canViewStatistics]);
return {
isLoading,
statistics,
instances,
fetchStatistics,
};
};
export function InformationPage() {
const canViewStatistics = useViewStatisticsPermission();
const {
isLoading,
statistics,
instances,
fetchStatistics,
} = useStatistics(canViewStatistics);
const info = useReactiveValue(() => Info, []);
export function InformationPage({
canViewStatistics,
isLoading,
info,
statistics,
instances,
onClickRefreshButton,
}) {
const t = useTranslation();
const handleRefreshClick = () => {
if (isLoading) {
return;
}
fetchStatistics();
};
useEffect(() => {
SideNav.setFlex('adminFlex');
SideNav.openFlex();
}, []);
if (!info) {
return null;
}
const alertOplogForMultipleInstances = statistics && statistics.instanceCount > 1 && !statistics.oplogEnabled;
return <section className='page-container page-list Admin__InformationPage'>
return <section className='page-container'>
<Header rawSectionName={t('Info')} hideHelp>
{canViewStatistics
&& <div className='rc-header__block rc-header__block-action'>
<Button primary type='button' onClick={handleRefreshClick}>
<Icon iconName='reload' /> {t('Refresh')}
&& <Header.ActionBlock>
<Button disabled={isLoading} primary type='button' onClick={onClickRefreshButton}>
<Icon name='reload' /> {t('Refresh')}
</Button>
</div>}
</Header.ActionBlock>}
</Header>
<div className='content'>
@ -121,11 +51,11 @@ export function InformationPage() {
</p>
</ErrorAlert>}
<RocketChatSection info={info} statistics={statistics} isLoading={isLoading} />
{canViewStatistics && <RocketChatSection info={info} statistics={statistics} isLoading={isLoading} />}
<CommitSection info={info} />
<RuntimeEnvironmentSection statistics={statistics} isLoading={isLoading} />
{canViewStatistics && <RuntimeEnvironmentSection statistics={statistics} isLoading={isLoading} />}
<BuildEnvironmentSection info={info} />
<UsageSection statistics={statistics} isLoading={isLoading} />
{canViewStatistics && <UsageSection statistics={statistics} isLoading={isLoading} />}
<InstancesSection instances={instances} />
</div>
</section>;

@ -0,0 +1,151 @@
import { action } from '@storybook/addon-actions';
import { boolean, object } from '@storybook/addon-knobs/react';
import React from 'react';
import { dummyDate } from '../../../../.storybook/helpers';
import { InformationPage } from './InformationPage';
export default {
title: 'admin/info/InformationPage',
component: InformationPage,
decorators: [
(fn) => <div className='rc-old'>{fn()}</div>,
],
};
const info = {
marketplaceApiVersion: 'info.marketplaceApiVersion',
commit: {
hash: 'info.commit.hash',
date: 'info.commit.date',
branch: 'info.commit.branch',
tag: 'info.commit.tag',
author: 'info.commit.author',
subject: 'info.commit.subject',
},
compile: {
platform: 'info.compile.platform',
arch: 'info.compile.arch',
osRelease: 'info.compile.osRelease',
nodeVersion: 'info.compile.nodeVersion',
date: dummyDate,
},
};
const statistics = {
version: 'statistics.version',
migration: {
version: 'statistics.migration.version',
lockedAt: dummyDate,
},
installedAt: dummyDate,
process: {
nodeVersion: 'statistics.process.nodeVersion',
uptime: 10 * 24 * 60 * 60,
pid: 'statistics.process.pid',
},
uniqueId: 'statistics.uniqueId',
instanceCount: 1,
oplogEnabled: true,
os: {
type: 'statistics.os.type',
platform: 'statistics.os.platform',
arch: 'statistics.os.arch',
release: 'statistics.os.release',
uptime: 10 * 24 * 60 * 60,
loadavg: [1.1, 1.5, 1.15],
totalmem: 1024,
freemem: 1024,
cpus: [{}],
},
mongoVersion: 'statistics.mongoVersion',
mongoStorageEngine: 'statistics.mongoStorageEngine',
totalUsers: 'statistics.totalUsers',
nonActiveUsers: 'nonActiveUsers',
activeUsers: 'statistics.activeUsers',
totalConnectedUsers: 'statistics.totalConnectedUsers',
onlineUsers: 'statistics.onlineUsers',
awayUsers: 'statistics.awayUsers',
offlineUsers: 'statistics.offlineUsers',
totalRooms: 'statistics.totalRooms',
totalChannels: 'statistics.totalChannels',
totalPrivateGroups: 'statistics.totalPrivateGroups',
totalDirect: 'statistics.totalDirect',
totalLivechat: 'statistics.totalLivechat',
totalDiscussions: 'statistics.totalDiscussions',
totalThreads: 'statistics.totalThreads',
totalMessages: 'statistics.totalMessages',
totalChannelMessages: 'statistics.totalChannelMessages',
totalPrivateGroupMessages: 'statistics.totalPrivateGroupMessages',
totalDirectMessages: 'statistics.totalDirectMessages',
totalLivechatMessages: 'statistics.totalLivechatMessages',
uploadsTotal: 'statistics.uploadsTotal',
uploadsTotalSize: 1024,
integrations: {
totalIntegrations: 'statistics.integrations.totalIntegrations',
totalIncoming: 'statistics.integrations.totalIncoming',
totalIncomingActive: 'statistics.integrations.totalIncomingActive',
totalOutgoing: 'statistics.integrations.totalOutgoing',
totalOutgoingActive: 'statistics.integrations.totalOutgoingActive',
totalWithScriptEnabled: 'statistics.integrations.totalWithScriptEnabled',
},
};
const instances = [
{
address: 'instances[].address',
broadcastAuth: 'instances[].broadcastAuth',
currentStatus: {
connected: 'instances[].currentStatus.connected',
retryCount: 'instances[].currentStatus.retryCount',
status: 'instances[].currentStatus.status',
},
instanceRecord: {
_id: 'instances[].instanceRecord._id',
pid: 'instances[].instanceRecord.pid',
_createdAt: dummyDate,
_updatedAt: dummyDate,
},
},
];
export const _default = () =>
<InformationPage
canViewStatistics={boolean('canViewStatistics', true)}
isLoading={boolean('isLoading', false)}
info={object('info', info)}
statistics={object('statistics', statistics)}
instances={object('instances', instances)}
onClickRefreshButton={action('clickRefreshButton')}
/>;
export const withoutCanViewStatisticsPermission = () =>
<InformationPage
info={info}
onClickRefreshButton={action('clickRefreshButton')}
/>;
export const loading = () =>
<InformationPage
canViewStatistics
isLoading
info={info}
onClickRefreshButton={action('clickRefreshButton')}
/>;
export const withStatistics = () =>
<InformationPage
canViewStatistics
info={info}
statistics={statistics}
onClickRefreshButton={action('clickRefreshButton')}
/>;
export const withOneInstance = () =>
<InformationPage
canViewStatistics
info={info}
statistics={statistics}
instances={instances}
onClickRefreshButton={action('clickRefreshButton')}
/>;

@ -0,0 +1,77 @@
import React, { useState, useEffect } from 'react';
import { useMethod } from '../../../hooks/useMethod';
import { useViewStatisticsPermission } from '../../../hooks/usePermissions';
import { useRocketChatInformation } from '../../../hooks/useRocketChatInformation';
import { useAdminSideNav } from '../hooks';
import { InformationPage } from './InformationPage';
export function InformationRoute() {
useAdminSideNav();
const canViewStatistics = useViewStatisticsPermission();
const [isLoading, setLoading] = useState(true);
const [statistics, setStatistics] = useState({});
const [instances, setInstances] = useState([]);
const [fetchStatistics, setFetchStatistics] = useState(() => () => ({}));
const getStatistics = useMethod('getStatistics');
const getInstances = useMethod('instances/get');
useEffect(() => {
let didCancel = false;
const fetchStatistics = async () => {
if (!canViewStatistics) {
setStatistics(null);
setInstances(null);
return;
}
setLoading(true);
try {
const [statistics, instances] = await Promise.all([
getStatistics(),
getInstances(),
]);
if (didCancel) {
return;
}
setStatistics(statistics);
setInstances(instances);
} finally {
setLoading(false);
}
};
setFetchStatistics(() => fetchStatistics);
fetchStatistics();
return () => {
didCancel = true;
};
}, [canViewStatistics]);
const info = useRocketChatInformation();
const handleClickRefreshButton = () => {
if (isLoading) {
return;
}
fetchStatistics();
};
return <InformationPage
canViewStatistics={canViewStatistics}
isLoading={isLoading}
info={info}
statistics={statistics}
instances={instances}
onClickRefreshButton={handleClickRefreshButton}
/>;
}

@ -1,9 +1,8 @@
import React from 'react';
import { useTranslation } from '../../providers/TranslationProvider';
import { DescriptionList } from './DescriptionList';
import { formatDate } from './formatters';
import { useTranslation } from '../../contexts/TranslationContext';
import { InformationList } from './InformationList';
import { InformationEntry } from './InformationEntry';
export function InstancesSection({ instances }) {
const t = useTranslation();
@ -15,17 +14,17 @@ export function InstancesSection({ instances }) {
return <>
<h3>{t('Broadcast_Connected_Instances')}</h3>
{instances.map(({ address, broadcastAuth, currentStatus, instanceRecord }, i) =>
<InformationList key={i}>
<InformationEntry label={t('Address')}>{address}</InformationEntry>
<InformationEntry label={t('Auth')}>{broadcastAuth ? 'true' : 'false'}</InformationEntry>
<InformationEntry label={<>{t('Current_Status')} > {t('Connected')}</>}>{currentStatus.connected ? 'true' : 'false'}</InformationEntry>
<InformationEntry label={<>{t('Current_Status')} > {t('Retry_Count')}</>}>{currentStatus.retryCount}</InformationEntry>
<InformationEntry label={<>{t('Current_Status')} > {t('Status')}</>}>{currentStatus.status}</InformationEntry>
<InformationEntry label={<>{t('Instance_Record')} > {t('ID')}</>}>{instanceRecord._id}</InformationEntry>
<InformationEntry label={<>{t('Instance_Record')} > {t('PID')}</>}>{instanceRecord.pid}</InformationEntry>
<InformationEntry label={<>{t('Instance_Record')} > {t('Created_at')}</>}>{formatDate(instanceRecord._createdAt)}</InformationEntry>
<InformationEntry label={<>{t('Instance_Record')} > {t('Updated_at')}</>}>{formatDate(instanceRecord._updatedAt)}</InformationEntry>
</InformationList>
<DescriptionList key={i}>
<DescriptionList.Entry label={t('Address')}>{address}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Auth')}>{broadcastAuth ? 'true' : 'false'}</DescriptionList.Entry>
<DescriptionList.Entry label={<>{t('Current_Status')} > {t('Connected')}</>}>{currentStatus.connected ? 'true' : 'false'}</DescriptionList.Entry>
<DescriptionList.Entry label={<>{t('Current_Status')} > {t('Retry_Count')}</>}>{currentStatus.retryCount}</DescriptionList.Entry>
<DescriptionList.Entry label={<>{t('Current_Status')} > {t('Status')}</>}>{currentStatus.status}</DescriptionList.Entry>
<DescriptionList.Entry label={<>{t('Instance_Record')} > {t('ID')}</>}>{instanceRecord._id}</DescriptionList.Entry>
<DescriptionList.Entry label={<>{t('Instance_Record')} > {t('PID')}</>}>{instanceRecord.pid}</DescriptionList.Entry>
<DescriptionList.Entry label={<>{t('Instance_Record')} > {t('Created_at')}</>}>{formatDate(instanceRecord._createdAt)}</DescriptionList.Entry>
<DescriptionList.Entry label={<>{t('Instance_Record')} > {t('Updated_at')}</>}>{formatDate(instanceRecord._updatedAt)}</DescriptionList.Entry>
</DescriptionList>
)}
</>;
}

@ -0,0 +1,32 @@
import React from 'react';
import { dummyDate } from '../../../../.storybook/helpers';
import { InstancesSection } from './InstancesSection';
export default {
title: 'admin/info/InstancesSection',
component: InstancesSection,
decorators: [
(fn) => <div className='rc-old'>{fn()}</div>,
],
};
const instances = [
{
address: 'instances[].address',
broadcastAuth: 'instances[].broadcastAuth',
currentStatus: {
connected: 'instances[].currentStatus.connected',
retryCount: 'instances[].currentStatus.retryCount',
status: 'instances[].currentStatus.status',
},
instanceRecord: {
_id: 'instances[].instanceRecord._id',
pid: 'instances[].instanceRecord.pid',
_createdAt: dummyDate,
_updatedAt: dummyDate,
},
},
];
export const _default = () => <InstancesSection instances={instances} />;

@ -1,34 +1,29 @@
import { Text } from '@rocket.chat/fuselage';
import React from 'react';
import { useTranslation } from '../../contexts/TranslationContext';
import { SkeletonText } from './SkeletonText';
import { useTranslation } from '../../providers/TranslationProvider';
import { formatDate, formatHumanReadableTime } from './formatters';
import { InformationList } from './InformationList';
import { InformationEntry } from './InformationEntry';
import { DescriptionList } from './DescriptionList';
export function RocketChatSection({ info, statistics, isLoading }) {
const s = (fn) => (isLoading ? <SkeletonText /> : fn());
const s = (fn) => (isLoading ? <Text.Skeleton animated width={'1/2'} /> : fn());
const t = useTranslation();
const appsEngineVersion = info.marketplaceApiVersion;
if (!statistics) {
return null;
}
const appsEngineVersion = info && info.marketplaceApiVersion;
return <>
<h3>{t('Rocket.Chat')}</h3>
<InformationList>
<InformationEntry label={t('Version')}>{s(() => statistics.version)}</InformationEntry>
{appsEngineVersion && <InformationEntry label={t('Apps_Engine_Version')}>{appsEngineVersion}</InformationEntry>}
<InformationEntry label={t('DB_Migration')}>{s(() => statistics.migration.version)}</InformationEntry>
<InformationEntry label={t('DB_Migration_Date')}>{s(() => formatDate(statistics.migration.lockedAt))}</InformationEntry>
<InformationEntry label={t('Installed_at')}>{s(() => formatDate(statistics.installedAt))}</InformationEntry>
<InformationEntry label={t('Uptime')}>{s(() => formatHumanReadableTime(statistics.process.uptime, t))}</InformationEntry>
<InformationEntry label={t('Deployment_ID')}>{s(() => statistics.uniqueId)}</InformationEntry>
<InformationEntry label={t('PID')}>{s(() => statistics.process.pid)}</InformationEntry>
<InformationEntry label={t('Running_Instances')}>{s(() => statistics.instanceCount)}</InformationEntry>
<InformationEntry label={t('OpLog')}>{s(() => (statistics.oplogEnabled ? t('Enabled') : t('Disabled')))}</InformationEntry>
</InformationList>
<DescriptionList>
<DescriptionList.Entry label={t('Version')}>{s(() => statistics.version)}</DescriptionList.Entry>
{appsEngineVersion && <DescriptionList.Entry label={t('Apps_Engine_Version')}>{appsEngineVersion}</DescriptionList.Entry>}
<DescriptionList.Entry label={t('DB_Migration')}>{s(() => statistics.migration.version)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('DB_Migration_Date')}>{s(() => formatDate(statistics.migration.lockedAt))}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Installed_at')}>{s(() => formatDate(statistics.installedAt))}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Uptime')}>{s(() => formatHumanReadableTime(statistics.process.uptime, t))}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Deployment_ID')}>{s(() => statistics.uniqueId)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('PID')}>{s(() => statistics.process.pid)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Running_Instances')}>{s(() => statistics.instanceCount)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('OpLog')}>{s(() => (statistics.oplogEnabled ? t('Enabled') : t('Disabled')))}</DescriptionList.Entry>
</DescriptionList>
</>;
}

@ -0,0 +1,36 @@
import React from 'react';
import { dummyDate } from '../../../../.storybook/helpers';
import { RocketChatSection } from './RocketChatSection';
export default {
title: 'admin/info/RocketChatSection',
component: RocketChatSection,
decorators: [
(fn) => <div className='rc-old'>{fn()}</div>,
],
};
const info = {
marketplaceApiVersion: 'info.marketplaceApiVersion',
};
const statistics = {
version: 'statistics.version',
migration: {
version: 'statistics.migration.version',
lockedAt: dummyDate,
},
installedAt: dummyDate,
process: {
uptime: 10 * 24 * 60 * 60,
pid: 'statistics.process.pid',
},
uniqueId: 'statistics.uniqueId',
instanceCount: 1,
oplogEnabled: true,
};
export const _default = () => <RocketChatSection info={info} statistics={statistics} />;
export const loading = () => <RocketChatSection info={{}} statistics={{}} isLoading />;

@ -1,34 +1,29 @@
import { Text } from '@rocket.chat/fuselage';
import React from 'react';
import { useTranslation } from '../../contexts/TranslationContext';
import { SkeletonText } from './SkeletonText';
import { useTranslation } from '../../providers/TranslationProvider';
import { DescriptionList } from './DescriptionList';
import { formatMemorySize, formatHumanReadableTime, formatCPULoad } from './formatters';
import { InformationList } from './InformationList';
import { InformationEntry } from './InformationEntry';
export function RuntimeEnvironmentSection({ statistics, isLoading }) {
const s = (fn) => (isLoading ? <SkeletonText /> : fn());
const s = (fn) => (isLoading ? <Text.Skeleton animated width={'1/2'} /> : fn());
const t = useTranslation();
if (!statistics) {
return null;
}
return <>
<h3>{t('Runtime_Environment')}</h3>
<InformationList>
<InformationEntry label={t('OS_Type')}>{s(() => statistics.os.type)}</InformationEntry>
<InformationEntry label={t('OS_Platform')}>{s(() => statistics.os.platform)}</InformationEntry>
<InformationEntry label={t('OS_Arch')}>{s(() => statistics.os.arch)}</InformationEntry>
<InformationEntry label={t('OS_Release')}>{s(() => statistics.os.release)}</InformationEntry>
<InformationEntry label={t('Node_version')}>{s(() => statistics.process.nodeVersion)}</InformationEntry>
<InformationEntry label={t('Mongo_version')}>{s(() => statistics.mongoVersion)}</InformationEntry>
<InformationEntry label={t('Mongo_storageEngine')}>{s(() => statistics.mongoStorageEngine)}</InformationEntry>
<InformationEntry label={t('OS_Uptime')}>{s(() => formatHumanReadableTime(statistics.os.uptime, t))}</InformationEntry>
<InformationEntry label={t('OS_Loadavg')}>{s(() => formatCPULoad(statistics.os.loadavg))}</InformationEntry>
<InformationEntry label={t('OS_Totalmem')}>{s(() => formatMemorySize(statistics.os.totalmem))}</InformationEntry>
<InformationEntry label={t('OS_Freemem')}>{s(() => formatMemorySize(statistics.os.freemem))}</InformationEntry>
<InformationEntry label={t('OS_Cpus')}>{s(() => statistics.os.cpus.length)}</InformationEntry>
</InformationList>
<DescriptionList>
<DescriptionList.Entry label={t('OS_Type')}>{s(() => statistics.os.type)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('OS_Platform')}>{s(() => statistics.os.platform)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('OS_Arch')}>{s(() => statistics.os.arch)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('OS_Release')}>{s(() => statistics.os.release)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Node_version')}>{s(() => statistics.process.nodeVersion)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Mongo_version')}>{s(() => statistics.mongoVersion)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Mongo_storageEngine')}>{s(() => statistics.mongoStorageEngine)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('OS_Uptime')}>{s(() => formatHumanReadableTime(statistics.os.uptime, t))}</DescriptionList.Entry>
<DescriptionList.Entry label={t('OS_Loadavg')}>{s(() => formatCPULoad(statistics.os.loadavg))}</DescriptionList.Entry>
<DescriptionList.Entry label={t('OS_Totalmem')}>{s(() => formatMemorySize(statistics.os.totalmem))}</DescriptionList.Entry>
<DescriptionList.Entry label={t('OS_Freemem')}>{s(() => formatMemorySize(statistics.os.freemem))}</DescriptionList.Entry>
<DescriptionList.Entry label={t('OS_Cpus')}>{s(() => statistics.os.cpus.length)}</DescriptionList.Entry>
</DescriptionList>
</>;
}

@ -0,0 +1,34 @@
import React from 'react';
import { RuntimeEnvironmentSection } from './RuntimeEnvironmentSection';
export default {
title: 'admin/info/RuntimeEnvironmentSection',
component: RuntimeEnvironmentSection,
decorators: [
(fn) => <div className='rc-old'>{fn()}</div>,
],
};
const statistics = {
os: {
type: 'statistics.os.type',
platform: 'statistics.os.platform',
arch: 'statistics.os.arch',
release: 'statistics.os.release',
uptime: 10 * 24 * 60 * 60,
loadavg: [1.1, 1.5, 1.15],
totalmem: 1024,
freemem: 1024,
cpus: [{}],
},
process: {
nodeVersion: 'statistics.process.nodeVersion',
},
mongoVersion: 'statistics.mongoVersion',
mongoStorageEngine: 'statistics.mongoStorageEngine',
};
export const _default = () => <RuntimeEnvironmentSection statistics={statistics} />;
export const loading = () => <RuntimeEnvironmentSection statistics={{}} isLoading />;

@ -1,28 +0,0 @@
.Admin__InformationPage__SkeletonText {
display: inline-flex;
min-width: 10em;
height: 1em;
animation: Admin__InformationPage__SkeletonText__animation 1s linear 1s infinite running;
opacity: 0.25;
background:
linear-gradient(
to right,
transparent,
currentColor 50%,
transparent 100%
);
background-size: 100vw 100vh;
}
@keyframes Admin__InformationPage__SkeletonText__animation {
0% {
background-position: 0 0;
}
100% {
background-position: 100vw 0;
}
}

@ -1,9 +0,0 @@
import React, { useMemo } from 'react';
import './SkeletonText.css';
export function SkeletonText() {
const width = useMemo(() => `${ Math.random() * 10 + 10 }em`, []);
return <span className='Admin__InformationPage__SkeletonText' style={{ width }} />;
}

@ -1,53 +1,48 @@
import { Text } from '@rocket.chat/fuselage';
import React from 'react';
import { useTranslation } from '../../contexts/TranslationContext';
import { SkeletonText } from './SkeletonText';
import { useTranslation } from '../../providers/TranslationProvider';
import { DescriptionList } from './DescriptionList';
import { formatMemorySize } from './formatters';
import { InformationList } from './InformationList';
import { InformationEntry } from './InformationEntry';
export function UsageSection({ statistics, isLoading }) {
const s = (fn) => (isLoading ? <SkeletonText /> : fn());
const s = (fn) => (isLoading ? <Text.Skeleton animated width={'1/2'} /> : fn());
const t = useTranslation();
if (!statistics) {
return null;
}
return <>
<h3>{t('Usage')}</h3>
<InformationList>
<InformationEntry label={t('Stats_Total_Users')}>{s(() => statistics.totalUsers)}</InformationEntry>
<InformationEntry label={t('Stats_Active_Users')}>{s(() => statistics.activeUsers)}</InformationEntry>
<InformationEntry label={t('Stats_Non_Active_Users')}>{s(() => statistics.nonActiveUsers)}</InformationEntry>
<InformationEntry label={t('Stats_Total_Connected_Users')}>{s(() => statistics.totalConnectedUsers)}</InformationEntry>
<InformationEntry label={t('Stats_Online_Users')}>{s(() => statistics.onlineUsers)}</InformationEntry>
<InformationEntry label={t('Stats_Away_Users')}>{s(() => statistics.awayUsers)}</InformationEntry>
<InformationEntry label={t('Stats_Offline_Users')}>{s(() => statistics.offlineUsers)}</InformationEntry>
<InformationEntry label={t('Stats_Total_Rooms')}>{s(() => statistics.totalRooms)}</InformationEntry>
<InformationEntry label={t('Stats_Total_Channels')}>{s(() => statistics.totalChannels)}</InformationEntry>
<InformationEntry label={t('Stats_Total_Private_Groups')}>{s(() => statistics.totalPrivateGroups)}</InformationEntry>
<InformationEntry label={t('Stats_Total_Direct_Messages')}>{s(() => statistics.totalDirect)}</InformationEntry>
<InformationEntry label={t('Stats_Total_Livechat_Rooms')}>{s(() => statistics.totalLivechat)}</InformationEntry>
<InformationEntry label={t('Total_Discussions')}>{s(() => statistics.totalDiscussions)}</InformationEntry>
<InformationEntry label={t('Total_Threads')}>{s(() => statistics.totalThreads)}</InformationEntry>
<InformationEntry label={t('Stats_Total_Messages')}>{s(() => statistics.totalMessages)}</InformationEntry>
<InformationEntry label={t('Stats_Total_Messages_Channel')}>{s(() => statistics.totalChannelMessages)}</InformationEntry>
<InformationEntry label={t('Stats_Total_Messages_PrivateGroup')}>{s(() => statistics.totalPrivateGroupMessages)}</InformationEntry>
<InformationEntry label={t('Stats_Total_Messages_Direct')}>{s(() => statistics.totalDirectMessages)}</InformationEntry>
<InformationEntry label={t('Stats_Total_Messages_Livechat')}>{s(() => statistics.totalLivechatMessages)}</InformationEntry>
<InformationEntry label={t('Stats_Total_Uploads')}>{s(() => statistics.uploadsTotal)}</InformationEntry>
<InformationEntry label={t('Stats_Total_Uploads_Size')}>{s(() => formatMemorySize(statistics.uploadsTotalSize))}</InformationEntry>
{statistics.apps && <>
<InformationEntry label={t('Stats_Total_Installed_Apps')}>{statistics.apps.totalInstalled}</InformationEntry>
<InformationEntry label={t('Stats_Total_Active_Apps')}>{statistics.apps.totalActive}</InformationEntry>
<DescriptionList>
<DescriptionList.Entry label={t('Stats_Total_Users')}>{s(() => statistics.totalUsers)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Active_Users')}>{s(() => statistics.activeUsers)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Non_Active_Users')}>{s(() => statistics.nonActiveUsers)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Connected_Users')}>{s(() => statistics.totalConnectedUsers)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Online_Users')}>{s(() => statistics.onlineUsers)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Away_Users')}>{s(() => statistics.awayUsers)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Offline_Users')}>{s(() => statistics.offlineUsers)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Rooms')}>{s(() => statistics.totalRooms)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Channels')}>{s(() => statistics.totalChannels)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Private_Groups')}>{s(() => statistics.totalPrivateGroups)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Direct_Messages')}>{s(() => statistics.totalDirect)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Livechat_Rooms')}>{s(() => statistics.totalLivechat)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Total_Discussions')}>{s(() => statistics.totalDiscussions)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Total_Threads')}>{s(() => statistics.totalThreads)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Messages')}>{s(() => statistics.totalMessages)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Messages_Channel')}>{s(() => statistics.totalChannelMessages)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Messages_PrivateGroup')}>{s(() => statistics.totalPrivateGroupMessages)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Messages_Direct')}>{s(() => statistics.totalDirectMessages)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Messages_Livechat')}>{s(() => statistics.totalLivechatMessages)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Uploads')}>{s(() => statistics.uploadsTotal)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Uploads_Size')}>{s(() => formatMemorySize(statistics.uploadsTotalSize))}</DescriptionList.Entry>
{statistics && statistics.apps && <>
<DescriptionList.Entry label={t('Stats_Total_Installed_Apps')}>{statistics.apps.totalInstalled}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Active_Apps')}>{statistics.apps.totalActive}</DescriptionList.Entry>
</>}
<InformationEntry label={t('Stats_Total_Integrations')}>{s(() => statistics.integrations.totalIntegrations)}</InformationEntry>
<InformationEntry label={t('Stats_Total_Incoming_Integrations')}>{s(() => statistics.integrations.totalIncoming)}</InformationEntry>
<InformationEntry label={t('Stats_Total_Active_Incoming_Integrations')}>{s(() => statistics.integrations.totalIncomingActive)}</InformationEntry>
<InformationEntry label={t('Stats_Total_Outgoing_Integrations')}>{s(() => statistics.integrations.totalOutgoing)}</InformationEntry>
<InformationEntry label={t('Stats_Total_Active_Outgoing_Integrations')}>{s(() => statistics.integrations.totalOutgoingActive)}</InformationEntry>
<InformationEntry label={t('Stats_Total_Integrations_With_Script_Enabled')}>{s(() => statistics.integrations.totalWithScriptEnabled)}</InformationEntry>
</InformationList>
<DescriptionList.Entry label={t('Stats_Total_Integrations')}>{s(() => statistics.integrations.totalIntegrations)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Incoming_Integrations')}>{s(() => statistics.integrations.totalIncoming)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Active_Incoming_Integrations')}>{s(() => statistics.integrations.totalIncomingActive)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Outgoing_Integrations')}>{s(() => statistics.integrations.totalOutgoing)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Active_Outgoing_Integrations')}>{s(() => statistics.integrations.totalOutgoingActive)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_Total_Integrations_With_Script_Enabled')}>{s(() => statistics.integrations.totalWithScriptEnabled)}</DescriptionList.Entry>
</DescriptionList>
</>;
}

@ -0,0 +1,54 @@
import React from 'react';
import { UsageSection } from './UsageSection';
export default {
title: 'admin/info/UsageSection',
component: UsageSection,
decorators: [
(fn) => <div className='rc-old'>{fn()}</div>,
],
};
const statistics = {
totalUsers: 'statistics.totalUsers',
nonActiveUsers: 'nonActiveUsers',
activeUsers: 'statistics.activeUsers',
totalConnectedUsers: 'statistics.totalConnectedUsers',
onlineUsers: 'statistics.onlineUsers',
awayUsers: 'statistics.awayUsers',
offlineUsers: 'statistics.offlineUsers',
totalRooms: 'statistics.totalRooms',
totalChannels: 'statistics.totalChannels',
totalPrivateGroups: 'statistics.totalPrivateGroups',
totalDirect: 'statistics.totalDirect',
totalLivechat: 'statistics.totalLivechat',
totalDiscussions: 'statistics.totalDiscussions',
totalThreads: 'statistics.totalThreads',
totalMessages: 'statistics.totalMessages',
totalChannelMessages: 'statistics.totalChannelMessages',
totalPrivateGroupMessages: 'statistics.totalPrivateGroupMessages',
totalDirectMessages: 'statistics.totalDirectMessages',
totalLivechatMessages: 'statistics.totalLivechatMessages',
uploadsTotal: 'statistics.uploadsTotal',
uploadsTotalSize: 1024,
integrations: {
totalIntegrations: 'statistics.integrations.totalIntegrations',
totalIncoming: 'statistics.integrations.totalIncoming',
totalIncomingActive: 'statistics.integrations.totalIncomingActive',
totalOutgoing: 'statistics.integrations.totalOutgoing',
totalOutgoingActive: 'statistics.integrations.totalOutgoingActive',
totalWithScriptEnabled: 'statistics.integrations.totalWithScriptEnabled',
},
};
const apps = {
totalInstalled: 'statistics.apps.totalInstalled',
totalActive: 'statistics.apps.totalActive',
};
export const _default = () => <UsageSection statistics={statistics} />;
export const withApps = () => <UsageSection statistics={{ ...statistics, apps }} />;
export const loading = () => <UsageSection statistics={{}} isLoading />;

@ -0,0 +1,92 @@
import { Accordion, Button, Paragraph, Text } from '@rocket.chat/fuselage';
import React from 'react';
import styled from 'styled-components';
import { Header } from '../../header/Header';
import { useTranslation } from '../../providers/TranslationProvider';
import { Section } from './Section';
const Wrapper = styled.div`
margin: 0 auto;
width: 100%;
max-width: 590px;
`;
export function GroupPage({ children, group, headerButtons }) {
const t = useTranslation();
const handleSubmit = (event) => {
event.preventDefault();
group.save();
};
const handleCancelClick = (event) => {
event.preventDefault();
group.cancel();
};
const handleSaveClick = (event) => {
event.preventDefault();
group.save();
};
if (!group) {
return <section className='page-container page-static page-settings'>
<Header />
<div className='content' />
</section>;
}
return <form action='#' className='page-container' method='post' onSubmit={handleSubmit}>
<Header rawSectionName={t(group.i18nLabel)}>
<Header.ButtonSection>
{group.changed && <Button danger primary type='reset' onClick={handleCancelClick}>{t('Cancel')}</Button>}
<Button
children={t('Save_changes')}
className='save'
disabled={!group.changed}
primary
type='submit'
onClick={handleSaveClick}
/>
{headerButtons}
</Header.ButtonSection>
</Header>
<div className='content'>
<Wrapper>
{t.has(group.i18nDescription) && <Paragraph hintColor>{t(group.i18nDescription)}</Paragraph>}
<Accordion className='page-settings'>
{children}
</Accordion>
</Wrapper>
</div>
</form>;
}
GroupPage.Skeleton = function Skeleton() {
const t = useTranslation();
return <div className='page-container'>
<Header rawSectionName={<div style={{ width: '20rem' }}><Text.Skeleton animated headline /></div>}>
<Header.ButtonSection>
<Button
children={t('Save_changes')}
disabled
primary
/>
</Header.ButtonSection>
</Header>
<div className='content'>
<Wrapper>
<Paragraph.Skeleton animated />
<Accordion className='page-settings'>
<Section.Skeleton />
</Accordion>
</Wrapper>
</div>
</div>;
};

@ -0,0 +1,23 @@
import React, { useMemo } from 'react';
import { AssetsGroupPage } from './groups/AssetsGroupPage';
import { OAuthGroupPage } from './groups/OAuthGroupPage';
import { GenericGroupPage } from './groups/GenericGroupPage';
import { GroupPage } from './GroupPage';
import { useGroup } from './SettingsState';
export function GroupSelector({ groupId }) {
const group = useGroup(groupId);
const children = useMemo(() => {
if (!group) {
return <GroupPage.Skeleton />;
}
return (group._id === 'Assets' && <AssetsGroupPage group={group} />)
|| (group._id === 'OAuth' && <OAuthGroupPage group={group} />)
|| <GenericGroupPage group={group} />;
}, [group]);
return children;
}

@ -0,0 +1,13 @@
import React from 'react';
import { useTranslation } from '../../providers/TranslationProvider';
export function NotAuthorizedPage() {
const t = useTranslation();
return <section className='page-container page-static page-settings'>
<div className='content'>
<p>{t('You_are_not_authorized_to_view_this_page')}</p>
</div>
</section>;
}

@ -0,0 +1,11 @@
import React from 'react';
import { NotAuthorizedPage } from './NotAuthorizedPage';
export default {
title: 'admin/settings/NotAuthorizedPage',
component: NotAuthorizedPage,
};
export const _default = () =>
<NotAuthorizedPage />;

@ -0,0 +1,27 @@
import { Button, Icon } from '@rocket.chat/fuselage';
import React from 'react';
import styled from 'styled-components';
import { useTranslation } from '../../providers/TranslationProvider';
// TODO: get rid of it
const StyledResetSettingButton = styled(Button)`
padding-block: 0 !important;
padding-top: 0 !important;
padding-bottom: 0 !important;
`;
export function ResetSettingButton(props) {
const t = useTranslation();
return <StyledResetSettingButton
aria-label={t('Reset')}
danger
ghost
small
title={t('Reset')}
{...props}
>
<Icon name='undo' />
</StyledResetSettingButton>;
}

@ -0,0 +1,51 @@
import { Accordion, Button, FieldGroup, Paragraph, Text } from '@rocket.chat/fuselage';
import React from 'react';
import { useTranslation } from '../../providers/TranslationProvider';
import { Setting } from './Setting';
import { useSection } from './SettingsState';
export function Section({ children, groupId, hasReset = true, help, sectionName, solo }) {
const section = useSection(groupId, sectionName);
const t = useTranslation();
const handleResetSectionClick = () => {
section.reset();
};
return <Accordion.Item
data-qa-section={sectionName}
noncollapsible={solo || !section.name}
title={section.name && t(section.name)}
>
{help && <Paragraph hintColor>{help}</Paragraph>}
<FieldGroup>
{section.settings.map((settingId) => <Setting key={settingId} settingId={settingId} />)}
{hasReset && section.canReset && <Button
children={t('Reset_section_settings')}
className='reset-group'
danger
data-section={section.name}
ghost
onClick={handleResetSectionClick}
/>}
{children}
</FieldGroup>
</Accordion.Item>;
}
Section.Skeleton = function Skeleton() {
return <Accordion.Item
noncollapsible
title={<Text.Skeleton animated subtitle />}
>
<Paragraph.Skeleton animated />
<FieldGroup>
{Array.from({ length: 10 }).map((_, i) => <Setting.Skeleton key={i} />)}
</FieldGroup>
</Accordion.Item>;
};

@ -0,0 +1,131 @@
import { Callout, Field, InputBox, Label, Text } from '@rocket.chat/fuselage';
import { useDebouncedCallback } from '@rocket.chat/fuselage-hooks';
import React, { useEffect, useMemo, useState } from 'react';
import { MarkdownText } from '../../basic/MarkdownText';
import { RawText } from '../../basic/RawText';
import { useTranslation } from '../../providers/TranslationProvider';
import { GenericSettingInput } from './inputs/GenericSettingInput';
import { BooleanSettingInput } from './inputs/BooleanSettingInput';
import { StringSettingInput } from './inputs/StringSettingInput';
import { RelativeUrlSettingInput } from './inputs/RelativeUrlSettingInput';
import { PasswordSettingInput } from './inputs/PasswordSettingInput';
import { IntSettingInput } from './inputs/IntSettingInput';
import { SelectSettingInput } from './inputs/SelectSettingInput';
import { LanguageSettingInput } from './inputs/LanguageSettingInput';
import { ColorSettingInput } from './inputs/ColorSettingInput';
import { FontSettingInput } from './inputs/FontSettingInput';
import { CodeSettingInput } from './inputs/CodeSettingInput';
import { ActionSettingInput } from './inputs/ActionSettingInput';
import { AssetSettingInput } from './inputs/AssetSettingInput';
import { RoomPickSettingInput } from './inputs/RoomPickSettingInput';
import { useSetting } from './SettingsState';
const getInputComponentByType = (type) => ({
boolean: BooleanSettingInput,
string: StringSettingInput,
relativeUrl: RelativeUrlSettingInput,
password: PasswordSettingInput,
int: IntSettingInput,
select: SelectSettingInput,
language: LanguageSettingInput,
color: ColorSettingInput,
font: FontSettingInput,
code: CodeSettingInput,
action: ActionSettingInput,
asset: AssetSettingInput,
roomPick: RoomPickSettingInput,
})[type] || GenericSettingInput;
const MemoizedSetting = React.memo(function MemoizedSetting({
type,
hint,
callout,
...inputProps
}) {
const InputComponent = getInputComponentByType(type);
return <Field>
<InputComponent {...inputProps} />
{hint && <Field.Hint>{hint}</Field.Hint>}
{callout && <Callout type='warning' title={callout} />}
</Field>;
});
export function Setting({ settingId }) {
const {
value: contextValue,
editor: contextEditor,
...setting
} = useSetting(settingId);
const t = useTranslation();
const [value, setValue] = useState(contextValue);
const setContextValue = useDebouncedCallback((value) => setting.update({ value }), 70, []);
useEffect(() => {
setValue(contextValue);
}, [contextValue]);
const [editor, setEditor] = useState(contextEditor);
const setContextEditor = useDebouncedCallback((editor) => setting.update({ editor }), 70, []);
useEffect(() => {
setEditor(contextEditor);
}, [contextEditor]);
const onChangeValue = (value) => {
setValue(value);
setContextValue(value);
};
const onChangeEditor = (editor) => {
setEditor(editor);
setContextEditor(editor);
};
const onResetButtonClick = () => {
setting.reset();
};
const {
_id,
disableReset,
readonly,
type,
packageValue,
blocked,
i18nLabel,
i18nDescription,
alert,
} = setting;
const label = (i18nLabel && t(i18nLabel)) || (_id || t(_id));
const hint = useMemo(() => t.has(i18nDescription) && <MarkdownText>{t(i18nDescription)}</MarkdownText>, [i18nDescription]);
const callout = useMemo(() => alert && <RawText>{t(alert)}</RawText>, [alert]);
const hasResetButton = !disableReset && !readonly && type !== 'asset' && value !== packageValue && !blocked;
return <MemoizedSetting
type={type}
label={label}
hint={hint}
callout={callout}
{...setting}
value={value}
editor={editor}
hasResetButton={hasResetButton}
onChangeValue={onChangeValue}
onChangeEditor={onChangeEditor}
onResetButtonClick={onResetButtonClick}
/>;
}
Setting.Skeleton = function Skeleton() {
return <Field>
<Label>
<Text.Skeleton animated width='1/4' />
</Label>
<InputBox.Skeleton animated />
</Field>;
};

@ -0,0 +1,27 @@
import React from 'react';
import { useAtLeastOnePermission } from '../../../hooks/usePermissions';
import { useAdminSideNav } from '../hooks';
import { GroupSelector } from './GroupSelector';
import { NotAuthorizedPage } from './NotAuthorizedPage';
import { SettingsState } from './SettingsState';
export function SettingsRoute({
group: groupId,
}) {
useAdminSideNav();
const hasPermission = useAtLeastOnePermission([
'view-privileged-setting',
'edit-privileged-setting',
'manage-selected-settings',
]);
if (!hasPermission) {
return <NotAuthorizedPage />;
}
return <SettingsState>
<GroupSelector groupId={groupId} />
</SettingsState>;
}

@ -0,0 +1,384 @@
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import React, { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState } from 'react';
import toastr from 'toastr';
import { PrivateSettingsCachedCollection } from '../../../../app/ui-admin/client/SettingsCachedCollection';
import { handleError } from '../../../../app/utils/client/lib/handleError';
import { useBatchSetSettings } from '../../../hooks/useBatchSetSettings';
import { useEventCallback } from '../../../hooks/useEventCallback';
import { useReactiveValue } from '../../../hooks/useReactiveValue';
const SettingsContext = createContext({});
let privateSettingsCachedCollection; // Remove this singleton (╯°□°)╯︵ ┻━┻
const getPrivateSettingsCachedCollection = () => {
if (privateSettingsCachedCollection) {
return [privateSettingsCachedCollection, Promise.resolve()];
}
privateSettingsCachedCollection = new PrivateSettingsCachedCollection();
return [privateSettingsCachedCollection, privateSettingsCachedCollection.init()];
};
const compareStrings = (a = '', b = '') => {
if (a === b || (!a && !b)) {
return 0;
}
return a > b ? 1 : -1;
};
const compareSettings = (a, b) =>
compareStrings(a.section, b.section)
|| compareStrings(a.sorter, b.sorter)
|| compareStrings(a.i18nLabel, b.i18nLabel);
const settingsReducer = (states, { type, payload }) => {
const {
settings,
persistedSettings,
} = states;
switch (type) {
case 'add': {
return {
settings: [...settings, ...payload].sort(compareSettings),
persistedSettings: [...persistedSettings, ...payload].sort(compareSettings),
};
}
case 'change': {
const mapping = (setting) => (setting._id !== payload._id ? setting : payload);
return {
settings: settings.map(mapping),
persistedSettings: settings.map(mapping),
};
}
case 'remove': {
const mapping = (setting) => setting._id !== payload;
return {
settings: settings.filter(mapping),
persistedSettings: persistedSettings.filter(mapping),
};
}
case 'hydrate': {
const map = {};
payload.forEach((setting) => {
map[setting._id] = setting;
});
const mapping = (setting) => (map[setting._id] ? { ...setting, ...map[setting._id] } : setting);
return {
settings: settings.map(mapping),
persistedSettings,
};
}
}
return states;
};
export function SettingsState({ children }) {
const [isLoading, setLoading] = useState(true);
const [subscribers] = useState(new Set());
const stateRef = useRef({ settings: [], persistedSettings: [] });
const enhancedReducer = useCallback((state, action) => {
const newState = settingsReducer(state, action);
stateRef.current = newState;
subscribers.forEach((subscriber) => {
subscriber(newState);
});
return newState;
}, [settingsReducer, subscribers]);
const [, dispatch] = useReducer(enhancedReducer, { settings: [], persistedSettings: [] });
const collectionsRef = useRef({});
useEffect(() => {
const [privateSettingsCachedCollection, loadingPromise] = getPrivateSettingsCachedCollection();
const stopLoading = () => {
setLoading(false);
};
loadingPromise.then(stopLoading, stopLoading);
const { collection: persistedSettingsCollection } = privateSettingsCachedCollection;
const settingsCollection = new Mongo.Collection(null);
collectionsRef.current = {
persistedSettingsCollection,
settingsCollection,
};
}, [collectionsRef]);
useEffect(() => {
if (isLoading) {
return;
}
const { current: { persistedSettingsCollection, settingsCollection } } = collectionsRef;
const query = persistedSettingsCollection.find();
const syncCollectionsHandle = query.observe({
added: (data) => settingsCollection.insert(data),
changed: (data) => settingsCollection.update(data._id, data),
removed: ({ _id }) => settingsCollection.remove(_id),
});
const addedQueue = [];
let addedActionTimer;
const syncStateHandle = query.observe({
added: (data) => {
addedQueue.push(data);
clearTimeout(addedActionTimer);
addedActionTimer = setTimeout(() => {
dispatch({ type: 'add', payload: addedQueue });
}, 70);
},
changed: (data) => {
dispatch({ type: 'change', payload: data });
},
removed: ({ _id }) => {
dispatch({ type: 'remove', payload: _id });
},
});
return () => {
syncCollectionsHandle.stop();
syncStateHandle.stop();
clearTimeout(addedActionTimer);
};
}, [isLoading, collectionsRef]);
const updateTimersRef = useRef({});
const updateAtCollection = useCallback(({ _id, ...data }) => {
const { current: { settingsCollection } } = collectionsRef;
const { current: updateTimers } = updateTimersRef;
clearTimeout(updateTimers[_id]);
updateTimers[_id] = setTimeout(() => {
settingsCollection.update(_id, { $set: data });
}, 70);
}, [collectionsRef, updateTimersRef]);
const hydrate = useCallback((changes) => {
changes.forEach(updateAtCollection);
dispatch({ type: 'hydrate', payload: changes });
}, [updateAtCollection, dispatch]);
const isDisabled = useCallback(({ blocked, enableQuery }) => {
if (blocked) {
return true;
}
if (!enableQuery) {
return false;
}
const { current: { settingsCollection } } = collectionsRef;
const queries = [].concat(typeof enableQuery === 'string' ? JSON.parse(enableQuery) : enableQuery);
return !queries.every((query) => !!settingsCollection.findOne(query));
}, [collectionsRef]);
const contextValue = useMemo(() => ({
subscribers,
stateRef,
hydrate,
isDisabled,
}), [
subscribers,
stateRef,
hydrate,
isDisabled,
]);
return <SettingsContext.Provider children={children} value={contextValue} />;
}
const useSelector = (selector, equalityFunction = (a, b) => a === b) => {
const { subscribers, stateRef } = useContext(SettingsContext);
const [value, setValue] = useState(() => selector(stateRef.current));
const handleUpdate = useEventCallback((selector, equalityFunction, value, state) => {
const newValue = selector(state);
if (!equalityFunction(newValue, value)) {
setValue(newValue);
}
}, selector, equalityFunction, value);
useEffect(() => {
subscribers.add(handleUpdate);
return () => {
subscribers.delete(handleUpdate);
};
}, [handleUpdate]);
useLayoutEffect(() => {
handleUpdate(stateRef.current);
});
return value;
};
export const useGroup = (groupId) => {
const group = useSelector((state) => state.settings.find(({ _id, type }) => _id === groupId && type === 'group'));
const filterSettings = (settings) => settings.filter(({ group }) => group === groupId);
const changed = useSelector((state) => filterSettings(state.settings).some(({ changed }) => changed));
const sections = useSelector((state) => Array.from(new Set(filterSettings(state.settings).map(({ section }) => section || ''))), (a, b) => a.length === b.length && a.join() === b.join());
const batchSetSettings = useBatchSetSettings();
const { stateRef, hydrate } = useContext(SettingsContext);
const save = useEventCallback(async (filterSettings, { current: state }, batchSetSettings) => {
const settings = filterSettings(state.settings);
const changes = settings.filter(({ changed }) => changed)
.map(({ _id, value, editor }) => ({ _id, value, editor }));
if (changes.length === 0) {
return;
}
try {
await batchSetSettings(changes);
if (changes.some(({ _id }) => _id === 'Language')) {
const lng = Meteor.user().language
|| changes.filter(({ _id }) => _id === 'Language').shift().value
|| 'en';
TAPi18n._loadLanguage(lng)
.then(() => toastr.success(TAPi18n.__('Settings_updated', { lng })))
.catch(handleError);
return;
}
toastr.success(TAPi18n.__('Settings_updated'));
} catch (error) {
handleError(error);
}
}, filterSettings, stateRef, batchSetSettings);
const cancel = useEventCallback((filterSettings, { current: state }, hydrate) => {
const settings = filterSettings(state.settings);
const persistedSettings = filterSettings(state.persistedSettings);
const changes = settings.filter(({ changed }) => changed)
.map((field) => {
const { _id, value, editor } = persistedSettings.find(({ _id }) => _id === field._id);
return { _id, value, editor, changed: false };
});
hydrate(changes);
}, filterSettings, stateRef, hydrate);
return group && { ...group, sections, changed, save, cancel };
};
export const useSection = (groupId, sectionName) => {
sectionName = sectionName || '';
const filterSettings = (settings) =>
settings.filter(({ group, section }) => group === groupId && ((!sectionName && !section) || (sectionName === section)));
const changed = useSelector((state) => filterSettings(state.settings).some(({ changed }) => changed));
const canReset = useSelector((state) => filterSettings(state.settings).some(({ value, packageValue }) => value !== packageValue));
const settingsIds = useSelector((state) => filterSettings(state.settings).map(({ _id }) => _id), (a, b) => a.length === b.length && a.join() === b.join());
const { stateRef, hydrate } = useContext(SettingsContext);
const reset = useEventCallback((filterSettings, { current: state }, hydrate) => {
const settings = filterSettings(state.settings);
const persistedSettings = filterSettings(state.persistedSettings);
const changes = settings.map((setting) => {
const { _id, value, packageValue, editor } = persistedSettings.find(({ _id }) => _id === setting._id);
return {
_id,
value: packageValue,
editor,
changed: packageValue !== value,
};
});
hydrate(changes);
}, filterSettings, stateRef, hydrate);
return {
name: sectionName,
changed,
canReset,
settings: settingsIds,
reset,
};
};
export const useSetting = (_id) => {
const { stateRef, hydrate, isDisabled } = useContext(SettingsContext);
const selectSetting = (settings) => settings.find((setting) => setting._id === _id);
const setting = useSelector((state) => selectSetting(state.settings));
const sectionChanged = useSelector((state) => state.settings.some(({ section, changed }) => section === setting.section && changed));
const disabled = useReactiveValue(() => isDisabled(setting), [setting.blocked, setting.enableQuery]);
const update = useEventCallback((selectSetting, { current: state }, hydrate, data) => {
const setting = { ...selectSetting(state.settings), ...data };
const persistedSetting = selectSetting(state.persistedSettings);
const changes = [{
_id: setting._id,
value: setting.value,
editor: setting.editor,
changed: (setting.value !== persistedSetting.value) || (setting.editor !== persistedSetting.editor),
}];
hydrate(changes);
}, selectSetting, stateRef, hydrate);
const reset = useEventCallback((selectSetting, { current: state }, hydrate) => {
const { _id, value, packageValue, editor } = selectSetting(state.persistedSettings);
const changes = [{
_id,
value: packageValue,
editor,
changed: packageValue !== value,
}];
hydrate(changes);
}, selectSetting, stateRef, hydrate);
return {
...setting,
sectionChanged,
disabled,
update,
reset,
};
};

@ -0,0 +1,23 @@
import { Button } from '@rocket.chat/fuselage';
import React from 'react';
import { useTranslation } from '../../../providers/TranslationProvider';
import { GroupPage } from '../GroupPage';
import { Section } from '../Section';
export function AssetsGroupPage({ group }) {
const solo = group.sections.length === 1;
const t = useTranslation();
return <GroupPage group={group} headerButtons={<>
<Button className='refresh-clients'>{t('Apply_and_refresh_all_clients')}</Button>
</>}>
{group.sections.map((sectionName) => <Section
key={sectionName}
groupId={group._id}
hasReset={false}
sectionName={sectionName}
solo={solo}
/>)}
</GroupPage>;
}

@ -0,0 +1,17 @@
import React from 'react';
import { GroupPage } from '../GroupPage';
import { Section } from '../Section';
export function GenericGroupPage({ group }) {
const solo = group.sections.length === 1;
return <GroupPage group={group}>
{group.sections.map((sectionName) => <Section
key={sectionName}
groupId={group._id}
sectionName={sectionName}
solo={solo}
/>)}
</GroupPage>;
}

@ -0,0 +1,40 @@
import { Button } from '@rocket.chat/fuselage';
import { Meteor } from 'meteor/meteor';
import React from 'react';
import s from 'underscore.string';
import { RawText } from '../../../basic/RawText';
import { useTranslation } from '../../../providers/TranslationProvider';
import { GroupPage } from '../GroupPage';
import { Section } from '../Section';
export function OAuthGroupPage({ group }) {
const solo = group.sections.length === 1;
const t = useTranslation();
const sectionIsCustomOAuth = (sectionName) => sectionName && /^Custom OAuth:\s.+/.test(sectionName);
const callbackURL = (sectionName) => {
const id = s.strRight(sectionName, 'Custom OAuth: ').toLowerCase();
return Meteor.absoluteUrl(`_oauth/${ id }`);
};
return <GroupPage group={group} headerButtons={<>
<Button className='refresh-oauth'>{t('Refresh_oauth_services')}</Button>
<Button className='add-custom-oauth'>{t('Add_custom_oauth')}</Button>
</>}>
{group.sections.map((sectionName) => (sectionIsCustomOAuth(sectionName)
? <Section
key={sectionName}
groupId={group._id}
help={<RawText>{t('Custom_oauth_helper', callbackURL(sectionName))}</RawText>}
sectionName={sectionName}
solo={solo}
>
<div className='submit'>
<Button cancel className='remove-custom-oauth'>{t('Remove_custom_oauth')}</Button>
</div>
</Section>
: <Section key={sectionName} groupId={group._id} sectionName={sectionName} solo={solo} />))}
</GroupPage>;
}

@ -0,0 +1,44 @@
import { Button, Field } from '@rocket.chat/fuselage';
import { Meteor } from 'meteor/meteor';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import React from 'react';
import toastr from 'toastr';
import { useTranslation } from '../../../providers/TranslationProvider';
import { handleError } from '../../../../../app/utils/client';
export function ActionSettingInput({
_id,
actionText,
value,
disabled,
sectionChanged,
}) {
const t = useTranslation();
const handleClick = async () => {
Meteor.call(value, (err, data) => {
if (err) {
err.details = Object.assign(err.details || {}, {
errorTitle: 'Error',
});
handleError(err);
return;
}
const args = [data.message].concat(data.params);
toastr.success(TAPi18n.__.apply(TAPi18n, args), TAPi18n.__('Success'));
});
};
return <>
<Button
data-qa-setting-id={_id}
children={t(actionText)}
disabled={disabled || sectionChanged}
primary
onClick={handleClick}
/>
{sectionChanged && <Field.Hint>{t('Save_to_enable_this_action')}</Field.Hint>}
</>;
}

@ -0,0 +1,72 @@
import { Button, Icon, Label } from '@rocket.chat/fuselage';
import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import React from 'react';
import toastr from 'toastr';
import { handleError } from '../../../../../app/utils/client';
import { useTranslation } from '../../../providers/TranslationProvider';
export function AssetSettingInput({
_id,
label,
value,
asset,
fileConstraints,
}) {
const t = useTranslation();
const handleUpload = (event) => {
event = event.originalEvent || event;
let { files } = event.target;
if (!files || files.length === 0) {
if (event.dataTransfer && event.dataTransfer.files) {
files = event.dataTransfer.files;
} else {
files = [];
}
}
Object.values(files).forEach((blob) => {
toastr.info(TAPi18n.__('Uploading_file'));
const reader = new FileReader();
reader.readAsBinaryString(blob);
reader.onloadend = () => Meteor.call('setAsset', reader.result, blob.type, asset, function(err) {
if (err != null) {
handleError(err);
console.log(err);
return;
}
return toastr.success(TAPi18n.__('File_uploaded'));
});
});
};
const handleDeleteButtonClick = () => {
Meteor.call('unsetAsset', asset);
};
return <>
<Label htmlFor={_id} text={label} title={_id} />
<div className='settings-file-preview'>
{value.url
? <div className='preview' style={{ backgroundImage: `url(${ value.url }?_dc=${ Random.id() })` }} />
: <div className='preview no-file background-transparent-light secondary-font-color'><Icon icon='icon-upload' /></div>}
<div className='action'>
{value.url
? <Button onClick={handleDeleteButtonClick}>
<Icon name='trash' />{t('Delete')}
</Button>
: <div className='rc-button rc-button--primary'>{t('Select_file')}
<input
type='file'
accept={fileConstraints.extensions && fileConstraints.extensions.length && `.${ fileConstraints.extensions.join(', .') }`}
onChange={handleUpload}
/>
</div>}
</div>
</div>
</>;
}

@ -0,0 +1,40 @@
import {
Field,
Label,
ToggleSwitch,
} from '@rocket.chat/fuselage';
import React from 'react';
import { ResetSettingButton } from '../ResetSettingButton';
export function BooleanSettingInput({
_id,
label,
disabled,
readonly,
autocomplete,
value,
hasResetButton,
onChangeValue,
onResetButtonClick,
}) {
const handleChange = (event) => {
const value = event.currentTarget.checked;
onChangeValue(value);
};
return <Field.Row>
<Label position='end' text={label} title={_id}>
<ToggleSwitch
data-qa-setting-id={_id}
value='true'
checked={value === true}
disabled={disabled}
readOnly={readonly}
autoComplete={autocomplete === false ? 'off' : undefined}
onChange={handleChange}
/>
</Label>
{hasResetButton && <ResetSettingButton data-qa-reset-setting-id={_id} onClick={onResetButtonClick} />}
</Field.Row>;
}

@ -0,0 +1,143 @@
import { Button, Field, Label } from '@rocket.chat/fuselage';
import { useToggle } from '@rocket.chat/fuselage-hooks';
import React, { useEffect, useRef, useState } from 'react';
import { useTranslation } from '../../../providers/TranslationProvider';
import { ResetSettingButton } from '../ResetSettingButton';
function CodeMirror({
lineNumbers = true,
lineWrapping = true,
mode = 'javascript',
gutters = ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
foldGutter = true,
matchBrackets = true,
autoCloseBrackets = true,
matchTags = true,
showTrailingSpace = true,
highlightSelectionMatches = true,
readOnly,
value: valueProp,
defaultValue,
onChange,
...props
}) {
const [editor, setEditor] = useState();
const [value, setValue] = useState(valueProp || defaultValue);
const ref = useRef();
useEffect(() => {
let editor;
const setupCodeMirror = async () => {
const CodeMirror = await import('codemirror/lib/codemirror.js');
await import('../../../../../app/ui/client/lib/codeMirror/codeMirror');
await import('codemirror/lib/codemirror.css');
const { current: textarea } = ref;
if (!textarea) {
return;
}
editor = CodeMirror.fromTextArea(textarea, {
lineNumbers,
lineWrapping,
mode,
gutters,
foldGutter,
matchBrackets,
autoCloseBrackets,
matchTags,
showTrailingSpace,
highlightSelectionMatches,
readOnly,
});
editor.on('change', (doc) => {
const value = doc.getValue();
setValue(value);
onChange(value);
});
setEditor(editor);
};
setupCodeMirror();
return () => {
if (!editor) {
return;
}
editor.toTextArea();
};
}, [ref]);
useEffect(() => {
setValue(valueProp);
}, [valueProp]);
useEffect(() => {
if (!editor) {
return;
}
if (value !== editor.getValue()) {
editor.setValue(value);
}
}, [editor, ref, value]);
return <textarea readOnly ref={ref} style={{ display: 'none' }} value={value} {...props}/>;
}
export function CodeSettingInput({
_id,
label,
value,
code,
placeholder,
readonly,
autocomplete,
disabled,
hasResetButton,
onChangeValue,
onResetButtonClick,
}) {
const t = useTranslation();
const [fullScreen, toggleFullScreen] = useToggle(false);
const handleChange = (value) => {
onChangeValue(value);
};
return <>
<Field.Row>
<Label htmlFor={_id} text={label} title={_id} />
{hasResetButton && <ResetSettingButton data-qa-reset-setting-id={_id} onClick={onResetButtonClick} />}
</Field.Row>
<div
className={[
'code-mirror-box',
fullScreen && 'code-mirror-box-fullscreen content-background-color',
].filter(Boolean).join(' ')}
>
<div className='title'>{label}</div>
<CodeMirror
data-qa-setting-id={_id}
id={_id}
mode={code}
value={value}
placeholder={placeholder}
disabled={disabled}
readOnly={readonly}
autoComplete={autocomplete === false ? 'off' : undefined}
onChange={handleChange}
/>
<div className='buttons'>
<Button primary onClick={() => toggleFullScreen()}>{fullScreen ? t('Exit_Full_Screen') : t('Full_Screen')}</Button>
</div>
</div>
</>;
}

@ -0,0 +1,112 @@
import {
Field,
InputBox,
Label,
SelectInput,
TextInput,
} from '@rocket.chat/fuselage';
import React from 'react';
import { useTranslation } from '../../../providers/TranslationProvider';
import { ResetSettingButton } from '../ResetSettingButton';
export function ColorSettingInput({
_id,
label,
value,
editor,
allowedTypes,
placeholder,
readonly,
autocomplete,
disabled,
hasResetButton,
onChangeValue,
onChangeEditor,
onResetButtonClick,
}) {
const t = useTranslation();
const handleChange = (event) => {
onChangeValue(event.currentTarget.value);
};
const handleEditorTypeChange = (event) => {
const editor = event.currentTarget.value.trim();
onChangeEditor(editor);
};
return <>
<div
style={{
display: 'flex',
flexFlow: 'row nowrap',
margin: '0 -0.5rem',
}}
>
<Field
style={{
flex: '2 2 0',
margin: '0 0.5rem',
}}
>
<Label htmlFor={_id} text={label} title={_id} />
{editor === 'color' && <InputBox
data-qa-setting-id={_id}
type='color'
id={_id}
value={value}
placeholder={placeholder}
disabled={disabled}
readOnly={readonly}
autoComplete={autocomplete === false ? 'off' : undefined}
onChange={handleChange}
style={{
width: '100%',
}}
/>}
{editor === 'expression' && <TextInput
data-qa-setting-id={_id}
id={_id}
value={value}
placeholder={placeholder}
disabled={disabled}
readOnly={readonly}
autoComplete={autocomplete === false ? 'off' : undefined}
onChange={handleChange}
style={{
width: '100%',
}}
/>}
</Field>
<Field
style={{
flex: '1 1 0',
margin: '0 0.5rem',
}}
>
<Field.Row>
<Label htmlFor={`${ _id }_editor`} text={t('Type')} title={_id} />
{hasResetButton && <ResetSettingButton data-qa-reset-setting-id={_id} onClick={onResetButtonClick} />}
</Field.Row>
<SelectInput
data-qa-setting-id={`${ _id }_editor`}
type='color'
id={`${ _id }_editor`}
value={editor}
disabled={disabled}
readOnly={readonly}
autoComplete={autocomplete === false ? 'off' : undefined}
onChange={handleEditorTypeChange}
>
{allowedTypes && allowedTypes.map((allowedType) =>
<SelectInput.Option key={allowedType} value={allowedType}>{t(allowedType)}</SelectInput.Option>
)}
</SelectInput>
</Field>
</div>
<Field.Hint>
Variable name: {_id.replace(/theme-color-/, '@')}
</Field.Hint>
</>;
}

@ -0,0 +1,42 @@
import {
Field,
Label,
TextInput,
} from '@rocket.chat/fuselage';
import React from 'react';
import { ResetSettingButton } from '../ResetSettingButton';
export function FontSettingInput({
_id,
label,
value,
placeholder,
readonly,
autocomplete,
disabled,
hasResetButton,
onChangeValue,
onResetButtonClick,
}) {
const handleChange = (event) => {
onChangeValue(event.currentTarget.value);
};
return <>
<Field.Row>
<Label htmlFor={_id} text={label} title={_id} />
{hasResetButton && <ResetSettingButton data-qa-reset-setting-id={_id} onClick={onResetButtonClick} />}
</Field.Row>
<TextInput
data-qa-setting-id={_id}
id={_id}
value={value}
placeholder={placeholder}
disabled={disabled}
readOnly={readonly}
autoComplete={autocomplete === false ? 'off' : undefined}
onChange={handleChange}
/>
</>;
}

@ -0,0 +1,42 @@
import {
Field,
Label,
TextInput,
} from '@rocket.chat/fuselage';
import React from 'react';
import { ResetSettingButton } from '../ResetSettingButton';
export function GenericSettingInput({
_id,
label,
value,
placeholder,
readonly,
autocomplete,
disabled,
hasResetButton,
onChangeValue,
onResetButtonClick,
}) {
const handleChange = (event) => {
onChangeValue(event.currentTarget.value);
};
return <>
<Field.Row>
<Label htmlFor={_id} text={label} title={_id} />
{hasResetButton && <ResetSettingButton data-qa-reset-setting-id={_id} onClick={onResetButtonClick} />}
</Field.Row>
<TextInput
data-qa-setting-id={_id}
id={_id}
value={value}
placeholder={placeholder}
disabled={disabled}
readOnly={readonly}
autoComplete={autocomplete === false ? 'off' : undefined}
onChange={handleChange}
/>
</>;
}

@ -0,0 +1,43 @@
import {
Field,
Label,
InputBox,
} from '@rocket.chat/fuselage';
import React from 'react';
import { ResetSettingButton } from '../ResetSettingButton';
export function IntSettingInput({
_id,
label,
value,
placeholder,
readonly,
autocomplete,
disabled,
onChangeValue,
hasResetButton,
onResetButtonClick,
}) {
const handleChange = (event) => {
onChangeValue(parseInt(event.currentTarget.value, 10));
};
return <>
<Field.Row>
<Label htmlFor={_id} text={label} title={_id} />
{hasResetButton && <ResetSettingButton data-qa-reset-setting-id={_id} onClick={onResetButtonClick} />}
</Field.Row>
<InputBox
data-qa-setting-id={_id}
id={_id}
type='number'
value={value}
placeholder={placeholder}
disabled={disabled}
readOnly={readonly}
autoComplete={autocomplete === false ? 'off' : undefined}
onChange={handleChange}
/>
</>;
}

@ -0,0 +1,49 @@
import {
Field,
Label,
SelectInput,
} from '@rocket.chat/fuselage';
import React from 'react';
import { useLanguages } from '../../../providers/TranslationProvider';
import { ResetSettingButton } from '../ResetSettingButton';
export function LanguageSettingInput({
_id,
label,
value,
placeholder,
readonly,
autocomplete,
disabled,
hasResetButton,
onChangeValue,
onResetButtonClick,
}) {
const languages = useLanguages();
const handleChange = (event) => {
onChangeValue(event.currentTarget.value);
};
return <>
<Field.Row>
<Label htmlFor={_id} text={label} title={_id} />
{hasResetButton && <ResetSettingButton data-qa-reset-setting-id={_id} onClick={onResetButtonClick} />}
</Field.Row>
<SelectInput
data-qa-setting-id={_id}
id={_id}
value={value}
placeholder={placeholder}
disabled={disabled}
readOnly={readonly}
autoComplete={autocomplete === false ? 'off' : undefined}
onChange={handleChange}
>
{languages.map(({ key, name }) =>
<SelectInput.Option key={key} value={key} dir='auto'>{name}</SelectInput.Option>
)}
</SelectInput>
</>;
}

@ -0,0 +1,42 @@
import {
Field,
Label,
PasswordInput,
} from '@rocket.chat/fuselage';
import React from 'react';
import { ResetSettingButton } from '../ResetSettingButton';
export function PasswordSettingInput({
_id,
label,
value,
placeholder,
readonly,
autocomplete,
disabled,
hasResetButton,
onChangeValue,
onResetButtonClick,
}) {
const handleChange = (event) => {
onChangeValue(event.currentTarget.value);
};
return <>
<Field.Row>
<Label htmlFor={_id} text={label} title={_id} />
{hasResetButton && <ResetSettingButton data-qa-reset-setting-id={_id} onClick={onResetButtonClick} />}
</Field.Row>
<PasswordInput
data-qa-setting-id={_id}
id={_id}
value={value}
placeholder={placeholder}
disabled={disabled}
readOnly={readonly}
autoComplete={autocomplete === false ? 'off' : undefined}
onChange={handleChange}
/>
</>;
}

@ -0,0 +1,43 @@
import {
Field,
Label,
UrlInput,
} from '@rocket.chat/fuselage';
import { Meteor } from 'meteor/meteor';
import React from 'react';
import { ResetSettingButton } from '../ResetSettingButton';
export function RelativeUrlSettingInput({
_id,
label,
value,
placeholder,
readonly,
autocomplete,
disabled,
hasResetButton,
onChangeValue,
onResetButtonClick,
}) {
const handleChange = (event) => {
onChangeValue(event.currentTarget.value);
};
return <>
<Field.Row>
<Label htmlFor={_id} text={label} title={_id} />
{hasResetButton && <ResetSettingButton data-qa-reset-setting-id={_id} onClick={onResetButtonClick} />}
</Field.Row>
<UrlInput
data-qa-setting-id={_id}
id={_id}
value={Meteor.absoluteUrl(value)}
placeholder={placeholder}
disabled={disabled}
readOnly={readonly}
autoComplete={autocomplete === false ? 'off' : undefined}
onChange={handleChange}
/>
</>;
}

@ -0,0 +1,88 @@
import { Field, Icon, Label } from '@rocket.chat/fuselage';
import { Blaze } from 'meteor/blaze';
import { Template } from 'meteor/templating';
import React, { useRef, useEffect, useLayoutEffect } from 'react';
import { ResetSettingButton } from '../ResetSettingButton';
export function RoomPickSettingInput({
_id,
label,
value,
placeholder,
readonly,
autocomplete,
disabled,
hasResetButton,
onChangeValue,
onResetButtonClick,
}) {
value = value || [];
const wrapperRef = useRef();
const valueRef = useRef(value);
const handleRemoveRoomButtonClick = (rid) => () => {
onChangeValue(value.filter(({ _id }) => _id !== rid));
};
useLayoutEffect(() => {
valueRef.current = value;
});
useEffect(() => {
const view = Blaze.renderWithData(Template.inputAutocomplete, {
id: _id,
name: _id,
class: 'search autocomplete rc-input__element',
autocomplete: autocomplete === false ? 'off' : undefined,
readOnly: readonly,
placeholder,
disabled,
settings: {
limit: 10,
// inputDelay: 300
rules: [
{
// @TODO maybe change this 'collection' and/or template
collection: 'CachedChannelList',
subscription: 'channelAndPrivateAutocomplete',
field: 'name',
template: Template.roomSearch,
noMatchTemplate: Template.roomSearchEmpty,
matchAll: true,
selector: (match) => ({ name: match }),
sort: 'name',
},
],
},
}, wrapperRef.current);
$('.autocomplete', wrapperRef.current).on('autocompleteselect', (event, doc) => {
const { current: value } = valueRef;
onChangeValue([...value.filter(({ _id }) => _id !== doc._id), doc]);
event.currentTarget.value = '';
event.currentTarget.focus();
});
return () => {
Blaze.remove(view);
};
}, [valueRef]);
return <>
<Field.Row>
<Label htmlFor={_id} text={label} title={_id} />
{hasResetButton && <ResetSettingButton data-qa-reset-setting-id={_id} onClick={onResetButtonClick} />}
</Field.Row>
<div style={{ position: 'relative' }} ref={wrapperRef} />
<ul className='selected-rooms'>
{value.map(({ _id, name }) =>
<li key={_id} className='remove-room' onClick={handleRemoveRoomButtonClick(_id)}>
{name} <Icon name='cross' />
</li>
)}
</ul>
</>;
}

@ -0,0 +1,50 @@
import {
Field,
Label,
SelectInput,
} from '@rocket.chat/fuselage';
import React from 'react';
import { useTranslation } from '../../../providers/TranslationProvider';
import { ResetSettingButton } from '../ResetSettingButton';
export function SelectSettingInput({
_id,
label,
value,
placeholder,
readonly,
autocomplete,
disabled,
values,
hasResetButton,
onChangeValue,
onResetButtonClick,
}) {
const t = useTranslation();
const handleChange = (event) => {
onChangeValue(event.currentTarget.value);
};
return <>
<Field.Row>
<Label htmlFor={_id} text={label} title={_id} />
{hasResetButton && <ResetSettingButton data-qa-reset-setting-id={_id} onClick={onResetButtonClick} />}
</Field.Row>
<SelectInput
data-qa-setting-id={_id}
id={_id}
value={value}
placeholder={placeholder}
disabled={disabled}
readOnly={readonly}
autoComplete={autocomplete === false ? 'off' : undefined}
onChange={handleChange}
>
{values.map(({ key, i18nLabel }) =>
<SelectInput.Option key={key} value={key}>{t(i18nLabel)}</SelectInput.Option>
)}
</SelectInput>
</>;
}

@ -0,0 +1,56 @@
import {
Field,
Label,
TextAreaInput,
TextInput,
} from '@rocket.chat/fuselage';
import React from 'react';
import { ResetSettingButton } from '../ResetSettingButton';
export function StringSettingInput({
_id,
label,
disabled,
multiline,
placeholder,
readonly,
autocomplete,
value,
hasResetButton,
onChangeValue,
onResetButtonClick,
}) {
const handleChange = (event) => {
onChangeValue(event.currentTarget.value);
};
return <>
<Field.Row>
<Label htmlFor={_id} text={label} title={_id} />
{hasResetButton && <ResetSettingButton data-qa-reset-setting-id={_id} onClick={onResetButtonClick} />}
</Field.Row>
{multiline
? <TextAreaInput
data-qa-setting-id={_id}
id={_id}
rows={4}
value={value}
placeholder={placeholder}
disabled={disabled}
readOnly={readonly}
autoComplete={autocomplete === false ? 'off' : undefined}
onChange={handleChange}
/>
: <TextInput
data-qa-setting-id={_id}
id={_id}
value={value}
placeholder={placeholder}
disabled={disabled}
readOnly={readonly}
autoComplete={autocomplete === false ? 'off' : undefined}
onChange={handleChange}
/> }
</>;
}

@ -6,6 +6,8 @@ export const Button = ({
invisible,
primary,
secondary,
cancel,
nude,
submit,
...props
}) => <button
@ -15,6 +17,8 @@ export const Button = ({
primary && 'rc-button--primary',
secondary && 'rc-button--secondary',
invisible && 'rc-button--invisible',
cancel && 'rc-button--cancel',
nude && 'rc-button--nude',
className,
].filter(Boolean).join(' ')}
{...props}

@ -1,20 +1,33 @@
import { action } from '@storybook/addon-actions';
import { boolean, text } from '@storybook/addon-knobs';
import React from 'react';
import { rocketChatWrapper } from '../../../.storybook/helpers';
import { Button } from './Button';
export default {
title: 'basic/Button',
component: Button,
decorators: [
rocketChatWrapper,
],
};
export const _default = () => <Button>Button</Button>;
export const _default = () => <Button
children={text('children', 'Button')}
invisible={boolean('invisible')}
primary={boolean('primary')}
secondary={boolean('secondary')}
cancel={boolean('cancel')}
nude={boolean('nude')}
submit={boolean('submit')}
onClick={action('click')}
/>;
export const invisible = () => <Button invisible>Button</Button>;
export const primary = () => <Button primary>Button</Button>;
export const secondary = () => <Button secondary>Button</Button>;
export const cancel = () => <Button cancel>Button</Button>;
export const nude = () => <Button nude>Button</Button>;
export const submit = () => <Button submit>Button</Button>;

@ -1,14 +1,10 @@
import React from 'react';
import { rocketChatWrapper } from '../../../.storybook/helpers';
import { ErrorAlert } from './ErrorAlert';
export default {
title: 'basic/ErrorAlert',
component: ErrorAlert,
decorators: [
rocketChatWrapper,
],
};
export const _default = () => <ErrorAlert>Content</ErrorAlert>;

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save