[NEW] Server Info page (#19517)

pull/20181/head
gabriellsh 4 years ago committed by GitHub
parent b9f2f354b9
commit 14946314ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      app/statistics/server/lib/statistics.js
  2. 35
      client/components/Card/Card.js
  3. 84
      client/components/Card/Card.stories.js
  4. 15
      client/components/DotLeader.stories.js
  5. 20
      client/components/DotLeader.tsx
  6. 25
      client/views/admin/info/BuildEnvironmentSection.js
  7. 24
      client/views/admin/info/BuildEnvironmentSection.stories.js
  8. 24
      client/views/admin/info/CommitSection.js
  9. 24
      client/views/admin/info/CommitSection.stories.js
  10. 72
      client/views/admin/info/DeploymentCard.js
  11. 173
      client/views/admin/info/InformationPage.stories.js
  12. 5
      client/views/admin/info/InformationRoute.js
  13. 37
      client/views/admin/info/InstancesCard.js
  14. 47
      client/views/admin/info/InstancesModal.js
  15. 33
      client/views/admin/info/InstancesSection.js
  16. 32
      client/views/admin/info/InstancesSection.stories.js
  17. 98
      client/views/admin/info/LicenseCard.js
  18. 34
      client/views/admin/info/NewInformationPage.js
  19. 106
      client/views/admin/info/OfflineLicenseModal.js
  20. 10
      client/views/admin/info/OfflineLicenseModal.stories.js
  21. 38
      client/views/admin/info/PushCard.js
  22. 36
      client/views/admin/info/RocketChatSection.js
  23. 36
      client/views/admin/info/RocketChatSection.stories.js
  24. 45
      client/views/admin/info/RuntimeEnvironmentSection.js
  25. 34
      client/views/admin/info/RuntimeEnvironmentSection.stories.js
  26. 160
      client/views/admin/info/UsageCard.js
  27. 52
      client/views/admin/info/UsagePieGraph.js
  28. 54
      client/views/admin/info/UsageSection.js
  29. 54
      client/views/admin/info/UsageSection.stories.js
  30. 19
      packages/rocketchat-i18n/i18n/en.i18n.json

@ -71,8 +71,10 @@ export const statistics = {
statistics.appUsers = Users.find({ type: 'app' }).count();
statistics.onlineUsers = Meteor.users.find({ statusConnection: 'online' }).count();
statistics.awayUsers = Meteor.users.find({ statusConnection: 'away' }).count();
// TODO: Get statuses from the `status` property.
statistics.busyUsers = Meteor.users.find({ statusConnection: 'busy' }).count();
statistics.totalConnectedUsers = statistics.onlineUsers + statistics.awayUsers;
statistics.offlineUsers = statistics.totalUsers - statistics.onlineUsers - statistics.awayUsers;
statistics.offlineUsers = statistics.totalUsers - statistics.onlineUsers - statistics.awayUsers - statistics.busyUsers;
// Room statistics
statistics.totalRooms = Rooms.find().count();

@ -0,0 +1,35 @@
import React from 'react';
import { Box, Divider } from '@rocket.chat/fuselage';
export const DOUBLE_COLUMN_CARD_WIDTH = 552;
const Title = ({ children }) => <Box mb='x8' fontScale='p2'>{children}</Box>;
const Footer = ({ children }) => <Box mb='x8'>{children}</Box>;
const Body = ({ children, flexDirection = 'row' }) => <Box mb='x8' display='flex' flexDirection={flexDirection} flexGrow={1}>{children}</Box>;
const Col = ({ children }) => <Box display='flex' alignSelf='stretch' w='x228' flexDirection='column' fontScale='c1'>{children}</Box>;
const ColSection = ({ children }) => <Box mb='x8' color='info'>{children}</Box>;
const ColTitle = ({ children }) => <Box fontScale='c2' m='none'>{children}</Box>;
const CardDivider = () => <Divider width='x1' mi='x24' mb='none' alignSelf='stretch'/>;
const Card = ({ children, ...props }) => <Box display='flex' flexDirection='column' pi='x16' pb='x8' width='fit-content' bg='neutral-100' {...props}>{children}</Box>;
Object.assign(Col, {
Title: ColTitle,
Section: ColSection,
});
Object.assign(Card, {
Title,
Body,
Col,
Footer,
Divider: CardDivider,
});
export default Card;

@ -0,0 +1,84 @@
import React from 'react';
import { Box, Button, ButtonGroup } from '@rocket.chat/fuselage';
import Card from './Card';
export default {
title: 'components/basic/Card',
component: Card,
};
export const Single = () => <Box p='x40'>
<Card>
<Card.Title>A card</Card.Title>
<Card.Body>
<Card.Col>
<Box>
<Card.Col.Title>A Section</Card.Col.Title>
<div>A bunch of stuff</div>
<div>A bunch of stuff</div>
<div>A bunch of stuff</div>
<div>A bunch of stuff</div>
</Box>
<Box>
<Card.Col.Title>Another Section</Card.Col.Title>
<div>A bunch of stuff</div>
<div>A bunch of stuff</div>
<div>A bunch of stuff</div>
<div>A bunch of stuff</div>
</Box>
</Card.Col>
</Card.Body>
<Card.Footer>
<ButtonGroup align='end'>
<Button small>I'm a button in a footer</Button>
</ButtonGroup>
</Card.Footer>
</Card>
</Box>;
export const Double = () => <Box p='x40'>
<Card>
<Card.Title>A card</Card.Title>
<Card.Body>
<Card.Col>
<Box>
<Card.Col.Title>A Section</Card.Col.Title>
<div>A bunch of stuff</div>
<div>A bunch of stuff</div>
<div>A bunch of stuff</div>
<div>A bunch of stuff</div>
</Box>
<Box>
<Card.Col.Title>Another Section</Card.Col.Title>
<div>A bunch of stuff</div>
<div>A bunch of stuff</div>
<div>A bunch of stuff</div>
<div>A bunch of stuff</div>
</Box>
</Card.Col>
<Card.Divider />
<Card.Col>
<Box>
<Card.Col.Title>A Section</Card.Col.Title>
<div>A bunch of stuff</div>
<div>A bunch of stuff</div>
<div>A bunch of stuff</div>
<div>A bunch of stuff</div>
</Box>
<Box>
<Card.Col.Title>Another Section</Card.Col.Title>
<div>A bunch of stuff</div>
<div>A bunch of stuff</div>
<div>A bunch of stuff</div>
<div>A bunch of stuff</div>
</Box>
</Card.Col>
</Card.Body>
<Card.Footer>
<ButtonGroup align='end'>
<Button small>I'm a button in a footer</Button>
</ButtonGroup>
</Card.Footer>
</Card>
</Box>;

@ -0,0 +1,15 @@
import React from 'react';
import { Box } from '@rocket.chat/fuselage';
import DotLeader from './DotLeader';
export default {
title: 'components/basic/DotLeader',
component: DotLeader,
};
export const Default = () => <Box display='flex' flexDirection='row'>
Label
<DotLeader />
12345
</Box>;

@ -0,0 +1,20 @@
import React, { FC, CSSProperties } from 'react';
import { Box } from '@rocket.chat/fuselage';
type DotLeaderProps = {
color: CSSProperties['borderColor'];
dotSize: CSSProperties['borderBlockEndWidth'];
}
const DotLeader: FC<DotLeaderProps> = ({ color = 'neutral-300', dotSize = 'x2' }) => <Box
flexGrow={1}
h='full'
alignSelf='flex-end'
borderBlockEndStyle='dotted'
borderBlockEndWidth={dotSize}
m='x2'
borderColor={color}
/>;
export default DotLeader;

@ -1,25 +0,0 @@
import React from 'react';
import Subtitle from '../../../components/Subtitle';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useFormatDateAndTime } from '../../../hooks/useFormatDateAndTime';
import DescriptionList from './DescriptionList';
const BuildEnvironmentSection = React.memo(function BuildEnvironmentSection({ info }) {
const t = useTranslation();
const formatDateAndTime = useFormatDateAndTime();
const build = info && (info.compile || info.build);
return <DescriptionList
data-qa='build-env-list'
title={<Subtitle data-qa='build-env-title'>{t('Build_Environment')}</Subtitle>}
>
<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')}>{formatDateAndTime(build.date)}</DescriptionList.Entry>
</DescriptionList>;
});
export default BuildEnvironmentSection;

@ -1,24 +0,0 @@
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,24 +0,0 @@
import React from 'react';
import Subtitle from '../../../components/Subtitle';
import { useTranslation } from '../../../contexts/TranslationContext';
import DescriptionList from './DescriptionList';
const CommitSection = React.memo(function CommitSection({ info }) {
const t = useTranslation();
const { commit = {} } = info;
return <DescriptionList
data-qa='commit-list'
title={<Subtitle data-qa='commit-title'>{t('Commit')}</Subtitle>}
>
<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>;
});
export default CommitSection;

@ -1,24 +0,0 @@
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,72 @@
import React from 'react';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { Skeleton, ButtonGroup, Button } from '@rocket.chat/fuselage';
import Card from '../../../components/Card/Card';
import InstancesModal from './InstancesModal';
import { useSetModal } from '../../../contexts/ModalContext';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useFormatDateAndTime } from '../../../hooks/useFormatDateAndTime';
const DeploymentCard = React.memo(function DeploymentCard({ info, statistics, instances, isLoading }) {
const t = useTranslation();
const formatDateAndTime = useFormatDateAndTime();
const setModal = useSetModal();
const { commit = {} } = info;
const s = (fn) => (isLoading ? <Skeleton width='50%' /> : fn());
const appsEngineVersion = info && info.marketplaceApiVersion;
const handleInstancesModal = useMutableCallback(() => { setModal(<InstancesModal instances={instances} onClose={() => setModal()}/>); });
return <Card>
<Card.Title>{t('Deployment')}</Card.Title>
<Card.Body>
<Card.Col>
<Card.Col.Section>
<Card.Col.Title>{t('Version')}</Card.Col.Title>
{s(() => statistics.version)}
</Card.Col.Section>
<Card.Col.Section>
<Card.Col.Title>{t('Deployment_ID')}</Card.Col.Title>
{s(() => statistics.uniqueId)}
</Card.Col.Section>
{appsEngineVersion && <Card.Col.Section>
<Card.Col.Title>{t('Apps_Engine_Version')}</Card.Col.Title>
{appsEngineVersion}
</Card.Col.Section>}
<Card.Col.Section>
<Card.Col.Title>{t('Node_version')}</Card.Col.Title>
{s(() => statistics.process.nodeVersion)}
</Card.Col.Section>
<Card.Col.Section>
<Card.Col.Title>{t('DB_Migration')}</Card.Col.Title>
{s(() => `${ statistics.migration.version } (${ formatDateAndTime(statistics.migration.lockedAt) }`)}
</Card.Col.Section>
<Card.Col.Section>
<Card.Col.Title>{t('MongoDB')}</Card.Col.Title>
{s(() => `${ statistics.mongoVersion } / ${ statistics.mongoStorageEngine } (oplog ${ statistics.oplogEnabled ? t('Enabled') : t('Disabled') })`)}
</Card.Col.Section>
<Card.Col.Section>
<Card.Col.Title>{t('Commit_details')}</Card.Col.Title>
{t('HEAD')}: ({s(() => commit.hash.slice(0, 9))}) <br />
{t('Branch')}: {s(() => commit.branch)}
</Card.Col.Section>
<Card.Col.Section>
<Card.Col.Title>{t('PID')}</Card.Col.Title>
{s(() => statistics.process.pid)}
</Card.Col.Section>
</Card.Col>
</Card.Body>
{!!instances.length && <Card.Footer>
<ButtonGroup align='end'>
<Button small onClick={handleInstancesModal}>{t('Instances')}</Button>
</ButtonGroup>
</Card.Footer>}
</Card>;
});
export default DeploymentCard;

@ -1,173 +0,0 @@
import { action } from '@storybook/addon-actions';
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 exampleInstance = {
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={true}
isLoading={false}
info={info}
statistics={statistics}
instances={exampleInstance}
onClickRefreshButton={action('clickRefreshButton')}
onClickDownloadInfo={action('clickDownloadInfo')}
/>;
export const withoutCanViewStatisticsPermission = () =>
<InformationPage
info={info}
onClickRefreshButton={action('clickRefreshButton')}
onClickDownloadInfo={action('clickDownloadInfo')}
/>;
export const loading = () =>
<InformationPage
canViewStatistics
isLoading
info={info}
onClickRefreshButton={action('clickRefreshButton')}
onClickDownloadInfo={action('clickDownloadInfo')}
/>;
export const withStatistics = () =>
<InformationPage
canViewStatistics
info={info}
statistics={statistics}
onClickRefreshButton={action('clickRefreshButton')}
onClickDownloadInfo={action('clickDownloadInfo')}
/>;
export const withOneInstance = () =>
<InformationPage
canViewStatistics
info={info}
statistics={statistics}
instances={[exampleInstance]}
onClickRefreshButton={action('clickRefreshButton')}
onClickDownloadInfo={action('clickDownloadInfo')}
/>;
export const withTwoInstances = () =>
<InformationPage
canViewStatistics
info={info}
statistics={statistics}
instances={[exampleInstance, exampleInstance]}
onClickRefreshButton={action('clickRefreshButton')}
onClickDownloadInfo={action('clickDownloadInfo')}
/>;
export const withTwoInstancesAndDisabledOplog = () =>
<InformationPage
canViewStatistics
info={info}
statistics={{ ...statistics, instanceCount: 2, oplogEnabled: false }}
instances={[exampleInstance, exampleInstance]}
onClickRefreshButton={action('clickRefreshButton')}
onClickDownloadInfo={action('clickDownloadInfo')}
/>;

@ -4,7 +4,7 @@ import { usePermission } from '../../../contexts/AuthorizationContext';
import NotAuthorizedPage from '../../../components/NotAuthorizedPage';
import { useMethod, useServerInformation, useEndpoint } from '../../../contexts/ServerContext';
import { downloadJsonAs } from '../../../lib/download';
import InformationPage from './InformationPage';
import NewInformationPage from './NewInformationPage';
const InformationRoute = React.memo(function InformationRoute() {
const canViewStatistics = usePermission('view-statistics');
@ -49,6 +49,7 @@ const InformationRoute = React.memo(function InformationRoute() {
const info = useServerInformation();
const handleClickRefreshButton = () => {
if (isLoading) {
return;
@ -65,7 +66,7 @@ const InformationRoute = React.memo(function InformationRoute() {
};
if (canViewStatistics) {
return <InformationPage
return <NewInformationPage
canViewStatistics={canViewStatistics}
isLoading={isLoading}
info={info}

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

@ -0,0 +1,47 @@
import React from 'react';
import { Modal, ButtonGroup, Button, Accordion } from '@rocket.chat/fuselage';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useFormatDateAndTime } from '../../../hooks/useFormatDateAndTime';
import DescriptionList from './DescriptionList';
const InstancesModal = ({ instances = [], onClose }) => {
const t = useTranslation();
const formatDateAndTime = useFormatDateAndTime();
return <Modal width='x600'>
<Modal.Header>
<Modal.Title>{t('Instances')}</Modal.Title>
<Modal.Close onClick={onClose}/>
</Modal.Header>
<Modal.Content>
<Accordion>
{
instances.map(({ address, broadcastAuth, currentStatus, instanceRecord }) =>
<Accordion.Item title={address} key={address}>
<DescriptionList>
<DescriptionList.Entry label={t('Address')}>{address}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Auth')}>{broadcastAuth ? 'true' : 'false'}</DescriptionList.Entry>
<DescriptionList.Entry label={<>{t('Current_Status')} &gt; {t('Connected')}</>}>{currentStatus.connected ? 'true' : 'false'}</DescriptionList.Entry>
<DescriptionList.Entry label={<>{t('Current_Status')} &gt; {t('Retry_Count')}</>}>{currentStatus.retryCount}</DescriptionList.Entry>
<DescriptionList.Entry label={<>{t('Current_Status')} &gt; {t('Status')}</>}>{currentStatus.status}</DescriptionList.Entry>
<DescriptionList.Entry label={<>{t('Instance_Record')} &gt; {t('ID')}</>}>{instanceRecord._id}</DescriptionList.Entry>
<DescriptionList.Entry label={<>{t('Instance_Record')} &gt; {t('PID')}</>}>{instanceRecord.pid}</DescriptionList.Entry>
<DescriptionList.Entry label={<>{t('Instance_Record')} &gt; {t('Created_at')}</>}>{formatDateAndTime(instanceRecord._createdAt)}</DescriptionList.Entry>
<DescriptionList.Entry label={<>{t('Instance_Record')} &gt; {t('Updated_at')}</>}>{formatDateAndTime(instanceRecord._updatedAt)}</DescriptionList.Entry>
</DescriptionList>
</Accordion.Item>,
)
}
</Accordion>
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button primary onClick={onClose}>{t('Close')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};
export default InstancesModal;

@ -1,33 +0,0 @@
import React from 'react';
import Subtitle from '../../../components/Subtitle';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useFormatDateAndTime } from '../../../hooks/useFormatDateAndTime';
import DescriptionList from './DescriptionList';
function InstancesSection({ instances }) {
const t = useTranslation();
const formatDateAndTime = useFormatDateAndTime();
if (!instances || !instances.length) {
return null;
}
return <>
{instances.map(({ address, broadcastAuth, currentStatus, instanceRecord }, i) =>
<DescriptionList key={i} title={<Subtitle>{t('Broadcast_Connected_Instances')}</Subtitle>}>
<DescriptionList.Entry label={t('Address')}>{address}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Auth')}>{broadcastAuth ? 'true' : 'false'}</DescriptionList.Entry>
<DescriptionList.Entry label={<>{t('Current_Status')} &gt; {t('Connected')}</>}>{currentStatus.connected ? 'true' : 'false'}</DescriptionList.Entry>
<DescriptionList.Entry label={<>{t('Current_Status')} &gt; {t('Retry_Count')}</>}>{currentStatus.retryCount}</DescriptionList.Entry>
<DescriptionList.Entry label={<>{t('Current_Status')} &gt; {t('Status')}</>}>{currentStatus.status}</DescriptionList.Entry>
<DescriptionList.Entry label={<>{t('Instance_Record')} &gt; {t('ID')}</>}>{instanceRecord._id}</DescriptionList.Entry>
<DescriptionList.Entry label={<>{t('Instance_Record')} &gt; {t('PID')}</>}>{instanceRecord.pid}</DescriptionList.Entry>
<DescriptionList.Entry label={<>{t('Instance_Record')} &gt; {t('Created_at')}</>}>{formatDateAndTime(instanceRecord._createdAt)}</DescriptionList.Entry>
<DescriptionList.Entry label={<>{t('Instance_Record')} &gt; {t('Updated_at')}</>}>{formatDateAndTime(instanceRecord._updatedAt)}</DescriptionList.Entry>
</DescriptionList>,
)}
</>;
}
export default InstancesSection;

@ -1,32 +0,0 @@
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} />;

@ -0,0 +1,98 @@
import React from 'react';
import { Box, Icon, ButtonGroup, Button, Skeleton, Margins } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import PlanTag from '../../../components/PlanTag';
import Card from '../../../components/Card/Card';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useEndpointData } from '../../../hooks/useEndpointData';
import { AsyncStatePhase } from '../../../hooks/useAsyncState';
import { useSetting } from '../../../contexts/SettingsContext';
import { useSetModal } from '../../../contexts/ModalContext';
import UsagePieGraph from './UsagePieGraph';
import OfflineLicenseModal from './OfflineLicenseModal';
const Feature = ({ label, enabled }) => <Box display='flex' flexDirection='row'>
<Box color={enabled ? 'success' : 'danger'}><Icon name={enabled ? 'check' : 'cross'} size='x16' /></Box>
{label}
</Box>;
const LicenseCard = ({ statistics, isLoading }) => {
const t = useTranslation();
const setModal = useSetModal();
const currentLicense = useSetting('Enterprise_License');
const licenseStatus = useSetting('Enterprise_License_Status');
const isAirGapped = true;
const { value, phase, error } = useEndpointData('licenses.get');
const endpointLoading = phase === AsyncStatePhase.LOADING;
const { maxActiveUsers = 0, modules = [] } = endpointLoading || error ? {} : value.licenses[0];
const hasEngagement = modules.includes('engagement-dashboard');
const hasOmnichannel = modules.includes('livechat-enterprise');
const hasAuditing = modules.includes('auditing');
const hasCannedResponses = modules.includes('canned-responses');
const handleApplyLicense = useMutableCallback(() => setModal(<OfflineLicenseModal onClose={() => { setModal(); }} license={currentLicense} licenseStatus={licenseStatus}/>));
return <Card>
<Card.Title>{t('License')}</Card.Title>
<Card.Body>
<Card.Col>
<Card.Col.Section>
<PlanTag />
</Card.Col.Section>
<Card.Col.Section>
<Card.Col.Title>{t('Features')}</Card.Col.Title>
<Margins block='x4'>
{
endpointLoading
? <>
<Skeleton width='40x' />
<Skeleton width='40x' />
<Skeleton width='40x' />
<Skeleton width='40x' />
</>
: <>
<Feature label={t('Omnichannel')} enabled={hasOmnichannel}/>
<Feature label={t('Auditing')} enabled={hasAuditing}/>
<Feature label={t('Canned_responses')} enabled={hasCannedResponses}/>
<Feature label={t('Engagement_Dashboard')} enabled={hasEngagement}/>
</>
}
</Margins>
</Card.Col.Section>
<Card.Col.Section>
<Card.Col.Title>{t('Usage')}</Card.Col.Title>
<Box display='flex' flexDirection='row'>
{
isLoading
? <Skeleton variant='rect' width='x112' height='x112'/>
: <UsagePieGraph
label={t('Users')}
used={statistics?.totalUsers}
total={maxActiveUsers}
size={112}
isLoading={isLoading}
/>
}
</Box>
</Card.Col.Section>
</Card.Col>
</Card.Body>
<Card.Footer>
<ButtonGroup align='end'>
{isAirGapped
? <Button small onClick={handleApplyLicense}>{t(currentLicense ? 'Cloud_Change_Offline_License' : 'Cloud_Apply_Offline_License')}</Button>
: <Button small>{t('Cloud_connectivity')}</Button>
}
</ButtonGroup>
</Card.Footer>
</Card>;
};
export default LicenseCard;

@ -1,14 +1,15 @@
import { Box, Button, ButtonGroup, Callout, Icon } from '@rocket.chat/fuselage';
import { Box, Button, ButtonGroup, Callout, Icon, Margins } from '@rocket.chat/fuselage';
import { useResizeObserver } from '@rocket.chat/fuselage-hooks';
import React from 'react';
import Page from '../../../components/Page';
import DeploymentCard from './DeploymentCard';
import UsageCard from './UsageCard';
import LicenseCard from './LicenseCard';
// import InstancesCard from './InstancesCard';
// import PushCard from './PushCard';
import { DOUBLE_COLUMN_CARD_WIDTH } from '../../../components/Card/Card';
import { useTranslation } from '../../../contexts/TranslationContext';
import RocketChatSection from './RocketChatSection';
import CommitSection from './CommitSection';
import RuntimeEnvironmentSection from './RuntimeEnvironmentSection';
import BuildEnvironmentSection from './BuildEnvironmentSection';
import UsageSection from './UsageSection';
import InstancesSection from './InstancesSection';
const InformationPage = React.memo(function InformationPage({
canViewStatistics,
@ -21,6 +22,10 @@ const InformationPage = React.memo(function InformationPage({
}) {
const t = useTranslation();
const { ref, contentBoxSize: { inlineSize = DOUBLE_COLUMN_CARD_WIDTH } = {} } = useResizeObserver();
const isSmall = inlineSize < DOUBLE_COLUMN_CARD_WIDTH;
if (!info) {
return null;
}
@ -63,12 +68,15 @@ const InformationPage = React.memo(function InformationPage({
</Box>
</Callout>}
{canViewStatistics && <RocketChatSection info={info} statistics={statistics} isLoading={isLoading} />}
<CommitSection info={info} />
{canViewStatistics && <RuntimeEnvironmentSection statistics={statistics} isLoading={isLoading} />}
<BuildEnvironmentSection info={info} />
{canViewStatistics && <UsageSection statistics={statistics} isLoading={isLoading} />}
<InstancesSection instances={instances} />
<Box display='flex' flexDirection='row' w='full' flexWrap='wrap' justifyContent={isSmall ? 'center' : 'flex-start'} ref={ref}>
<Margins all='x8'>
<DeploymentCard info={info} statistics={statistics} instances={instances} isLoading={isLoading}/>
<LicenseCard statistics={statistics} isLoading={isLoading}/>
<UsageCard vertical={isSmall} statistics={statistics} isLoading={isLoading}/>
{/* {!!instances.length && <InstancesCard instances={instances}/>} */}
{/* <PushCard /> */}
</Margins>
</Box>
</Box>
</Page.ScrollableContentWithShadow>
</Page>;

@ -0,0 +1,106 @@
import { Modal, Box, ButtonGroup, Button, Scrollable, Callout, Margins, Icon } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import React, { useState } from 'react';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext';
import { useEndpointActionExperimental } from '../../../hooks/useEndpointAction';
const OfflineLicenseModal = ({ onClose, license, licenseStatus, ...props }) => {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const [newLicense, setNewLicense] = useState(license);
const [isUpdating, setIsUpdating] = useState(false);
const [status, setStatus] = useState(licenseStatus);
const [lastSetLicense, setLastSetLicense] = useState(license);
const handleNewLicense = (e) => {
setNewLicense(e.currentTarget.value);
};
const hasChanges = lastSetLicense !== newLicense;
const addLicense = useEndpointActionExperimental('POST', 'licenses.add', t('Cloud_License_applied_successfully'));
const handlePaste = useMutableCallback(async () => {
try {
const text = await navigator.clipboard.readText();
setNewLicense(text);
} catch (error) {
dispatchToastMessage({ type: 'error', message: `${ t('Paste_error') }: ${ error }` });
}
});
const handleApplyLicense = useMutableCallback(async () => {
setIsUpdating(true);
setLastSetLicense(newLicense);
const data = await addLicense({ license: newLicense });
if (data.success) {
onClose();
return;
}
setIsUpdating(false);
setStatus('invalid');
});
return <Modal {...props}>
<Modal.Header>
<Modal.Title>{t('Cloud_Apply_Offline_License')}</Modal.Title>
<Modal.Close onClick={onClose} />
</Modal.Header>
<Modal.Content>
<Box withRichContent>
<p>{t('Cloud_register_offline_finish_helper')}</p>
</Box>
<Box
display='flex'
flexDirection='column'
alignItems='stretch'
paddingInline='x16'
pb='x8'
flexGrow={1}
backgroundColor='neutral-800'
mb={status === 'invalid' && 'x8'}
>
<Margins block='x8'>
<Scrollable vertical>
<Box
is='textarea'
height='x108'
fontFamily='mono'
fontScale='p1'
color='alternative'
style={{ wordBreak: 'break-all', resize: 'none' }}
placeholder={t('Paste_here')}
disabled={isUpdating}
value={newLicense}
autoComplete='off'
autoCorrect='off'
autoCapitalize='off'
spellCheck='false'
onChange={handleNewLicense}
/>
</Scrollable>
<ButtonGroup align='start'>
<Button primary small disabled={isUpdating} onClick={handlePaste}>
<Icon name='clipboard' />
{t('Paste')}
</Button>
</ButtonGroup>
</Margins>
</Box>
{status === 'invalid' && <Callout type='danger'>{t('Cloud_Invalid_license')}</Callout>}
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button primary disabled={!hasChanges || isUpdating} onClick={handleApplyLicense}>
{t('Cloud_Apply_license')}
</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};
export default OfflineLicenseModal;

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

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

@ -1,36 +0,0 @@
import { Skeleton } from '@rocket.chat/fuselage';
import React from 'react';
import Subtitle from '../../../components/Subtitle';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useFormatDateAndTime } from '../../../hooks/useFormatDateAndTime';
import { useFormatDuration } from '../../../hooks/useFormatDuration';
import DescriptionList from './DescriptionList';
const RocketChatSection = React.memo(function RocketChatSection({ info, statistics, isLoading }) {
const t = useTranslation();
const formatDateAndTime = useFormatDateAndTime();
const formatDuration = useFormatDuration();
const s = (fn) => (isLoading ? <Skeleton width='50%' /> : fn());
const appsEngineVersion = info && info.marketplaceApiVersion;
return <DescriptionList
data-qa='rocket-chat-list'
title={<Subtitle data-qa='rocket-chat-title'>{t('Rocket.Chat')}</Subtitle>}
>
<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(() => formatDateAndTime(statistics.migration.lockedAt))}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Installed_at')}>{s(() => formatDateAndTime(statistics.installedAt))}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Uptime')}>{s(() => formatDuration(statistics.process.uptime))}</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>;
});
export default RocketChatSection;

@ -1,36 +0,0 @@
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,45 +0,0 @@
import { Skeleton } from '@rocket.chat/fuselage';
import React from 'react';
import s from 'underscore.string';
import Subtitle from '../../../components/Subtitle';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useFormatMemorySize } from '../../../hooks/useFormatMemorySize';
import { useFormatDuration } from '../../../hooks/useFormatDuration';
import DescriptionList from './DescriptionList';
const formatCPULoad = (load) => {
if (!load) {
return null;
}
const [oneMinute, fiveMinutes, fifteenMinutes] = load;
return `${ s.numberFormat(oneMinute, 2) }, ${ s.numberFormat(fiveMinutes, 2) }, ${ s.numberFormat(fifteenMinutes, 2) }`;
};
const RuntimeEnvironmentSection = React.memo(function RuntimeEnvironmentSection({ statistics, isLoading }) {
const s = (fn) => (isLoading ? <Skeleton width='50%' /> : fn());
const t = useTranslation();
const formatMemorySize = useFormatMemorySize();
const formatDuration = useFormatDuration();
return <DescriptionList
data-qa='runtime-env-list'
title={<Subtitle data-qa='runtime-env-title'>{t('Runtime_Environment')}</Subtitle>}
>
<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(() => formatDuration(statistics.os.uptime))}</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>;
});
export default RuntimeEnvironmentSection;

@ -1,34 +0,0 @@
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 />;

@ -0,0 +1,160 @@
import React from 'react';
import { Box, Skeleton, Icon, ButtonGroup, Button } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import DotLeader from '../../../components/DotLeader';
import Card from '../../../components/Card/Card';
import { UserStatus } from '../../../components/UserStatus';
import { useFormatMemorySize } from '../../../hooks/useFormatMemorySize';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useRoute } from '../../../contexts/RouterContext';
import { useHasLicense } from '../../../../ee/client/hooks/useHasLicense';
const TextSeparator = ({ label, value }) => <Box display='flex' flexDirection='row' mb='x4'>
<span>{label}</span>
<DotLeader />
<span>{value}</span>
</Box>;
const UsageCard = React.memo(function UsageCard({ statistics, isLoading, vertical }) {
const s = (fn) => (isLoading ? <Skeleton width='x40' /> : fn());
const t = useTranslation();
const formatMemorySize = useFormatMemorySize();
const router = useRoute('engagement-dashboard');
const handleEngagement = useMutableCallback(() => {
router.push();
});
const canViewEngagement = useHasLicense('engagement-dashboard');
return <Card>
<Card.Title>{t('Usage')}</Card.Title>
<Card.Body flexDirection={vertical ? 'column' : 'row' }>
<Card.Col>
<Card.Col.Section>
<Card.Col.Title>{t('Users')}</Card.Col.Title>
<TextSeparator
label={<span><Icon name='dialpad' size='x16'/> {t('Total')}</span>}
value={s(() => statistics.totalUsers)}
/>
<TextSeparator
label={<span><UserStatus status='online'/> {t('Online')}</span>}
value={s(() => statistics.onlineUsers)}
/>
<TextSeparator
label={<span><UserStatus status='busy'/> {t('Busy')}</span>}
value={s(() => statistics.busyUsers)}
/>
<TextSeparator
label={<span><UserStatus status='away'/> {t('Away')}</span>}
value={s(() => statistics.awayUsers)}
/>
<TextSeparator
label={<span><UserStatus status='offline'/> {t('Offline')}</span>}
value={s(() => statistics.offlineUsers)}
/>
</Card.Col.Section>
<Card.Col.Section>
<Card.Col.Title>{t('Types_and_Distribution')}</Card.Col.Title>
<TextSeparator
label={t('Connected')}
value={s(() => statistics.totalConnectedUsers)}
/>
<TextSeparator
label={t('Stats_Active_Users')}
value={s(() => statistics.activeUsers)}
/>
<TextSeparator
label={t('Stats_Active_Guests')}
value={s(() => statistics.activeGuests)}
/>
<TextSeparator
label={t('Stats_Non_Active_Users')}
value={s(() => statistics.nonActiveUsers)}
/>
<TextSeparator
label={t('Stats_App_Users')}
value={s(() => statistics.appUsers)}
/>
</Card.Col.Section>
<Card.Col.Section>
<Card.Col.Title>{t('Uploads')}</Card.Col.Title>
<TextSeparator
label={t('Stats_Total_Uploads')}
value={s(() => statistics.uploadsTotal)}
/>
<TextSeparator
label={t('Stats_Total_Uploads_Size')}
value={s(() => formatMemorySize(statistics.uploadsTotalSize))}
/>
</Card.Col.Section>
</Card.Col>
<Card.Divider />
<Card.Col>
<Card.Col.Section>
<Card.Col.Title>{t('Rooms')}</Card.Col.Title>
<TextSeparator
label={<span><Icon name='dialpad' size='x16'/> {t('Stats_Total_Rooms')}</span>}
value={s(() => statistics.totalRooms)}
/>
<TextSeparator
label={<span><Icon name='hash' size='x16'/> {t('Stats_Total_Channels')}</span>}
value={s(() => statistics.totalChannels)}
/>
<TextSeparator
label={<span><Icon name='lock' size='x16'/> {t('Stats_Total_Private_Groups')}</span>}
value={s(() => statistics.totalPrivateGroups)}
/>
<TextSeparator
label={<span><Icon name='team' size='x16'/> {t('Stats_Total_Direct_Messages')}</span>}
value={s(() => statistics.totalDirect)}
/>
<TextSeparator
label={<span><Icon name='discussion' size='x16'/> {t('Total_Discussions')}</span>}
value={s(() => statistics.totalDiscussions)}
/>
<TextSeparator
label={<span><Icon name='headset' size='x16'/> {t('Stats_Total_Livechat_Rooms')}</span>}
value={s(() => statistics.totalLivechat)}
/>
</Card.Col.Section>
<Card.Col.Section>
<Card.Col.Title>{t('Messages')}</Card.Col.Title>
<TextSeparator
label={<span>{t('Stats_Total_Messages')}</span>}
value={s(() => statistics.totalMessages)}
/>
<TextSeparator
label={<span>{t('Total_Threads')}</span>}
value={s(() => statistics.totalThreads)}
/>
<TextSeparator
label={<span>{t('Stats_Total_Messages_Channel')}</span>}
value={s(() => statistics.totalChannelMessages)}
/>
<TextSeparator
label={<span>{t('Stats_Total_Messages_PrivateGroup')}</span>}
value={s(() => statistics.totalPrivateGroupMessages)}
/>
<TextSeparator
label={<span>{t('Stats_Total_Messages_Direct')}</span>}
value={s(() => statistics.totalDirectMessages)}
/>
<TextSeparator
label={<span>{t('Stats_Total_Messages_Livechat')}</span>}
value={s(() => statistics.totalLivechatMessages)}
/>
</Card.Col.Section>
</Card.Col>
</Card.Body>
<Card.Footer>
<ButtonGroup align='end'>
<Button disabled={!canViewEngagement} small onClick={handleEngagement}>{t('See_on_Engagement_Dashboard')}</Button>
</ButtonGroup>
</Card.Footer>
</Card>;
});
export default UsageCard;

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

@ -1,54 +0,0 @@
import { Skeleton } from '@rocket.chat/fuselage';
import React from 'react';
import Subtitle from '../../../components/Subtitle';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useFormatMemorySize } from '../../../hooks/useFormatMemorySize';
import DescriptionList from './DescriptionList';
const UsageSection = React.memo(function UsageSection({ statistics, isLoading }) {
const s = (fn) => (isLoading ? <Skeleton width='50%' /> : fn());
const formatMemorySize = useFormatMemorySize();
const t = useTranslation();
return <DescriptionList
data-qa='usage-list'
title={<Subtitle data-qa='usage-title'>{t('Usage')}</Subtitle>}
>
<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_Active_Guests')}>{s(() => statistics.activeGuests)}</DescriptionList.Entry>
<DescriptionList.Entry label={t('Stats_App_Users')}>{s(() => statistics.appUsers)}</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>
</>}
<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>;
});
export default UsageSection;

@ -1,54 +0,0 @@
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 />;

@ -782,6 +782,12 @@
"Closing_chat": "Closing chat",
"Closing_chat_message": "Closing chat message",
"Cloud": "Cloud",
"Cloud_Apply_Offline_License": "Apply Offline License",
"Cloud_Change_Offline_License": "Change Offline License",
"Cloud_License_applied_successfully": "License applied successfully!",
"Cloud_Invalid_license": "Invalid license!",
"Cloud_Apply_license": "Apply license",
"Cloud_connectivity": "Cloud Connectivity",
"Cloud_address_to_send_registration_to": "The address to send your Cloud registration email to.",
"Cloud_click_here": "After copy the text, go to [cloud console (click here)](__cloudConsoleUrl__).",
"Cloud_console": "Cloud Console",
@ -828,9 +834,10 @@
"Common_Access": "Common Access",
"Community": "Community",
"Compact": "Compact",
"Condensed": "Condensed",
"Commit_details": "Commit Details",
"Completed": "Completed",
"Computer": "Computer",
"Condensed": "Condensed",
"Confirm_new_encryption_password": "Confirm new encryption password",
"Confirm_new_password": "Confirm New Password",
"Confirm_New_Password_Placeholder": "Please re-enter new password...",
@ -1423,6 +1430,7 @@
"Emoji_provided_by_JoyPixels": "Emoji provided by <strong>JoyPixels</strong>",
"EmojiCustomFilesystem": "Custom Emoji Filesystem",
"Empty_title": "Empty title",
"See_on_Engagement_Dashboard": "See on Engagement Dashboard",
"Enable": "Enable",
"Enable_Auto_Away": "Enable Auto Away",
"Enable_Desktop_Notifications": "Enable Desktop Notifications",
@ -1648,6 +1656,7 @@
"Favorite_Rooms": "Enable Favorite Rooms",
"Favorites": "Favorites",
"Feature_Depends_on_Livechat_Visitor_navigation_as_a_message_to_be_enabled": "This feature depends on \"Send Visitor Navigation History as a Message\" to be enabled.",
"Features": "Features",
"Features_Enabled": "Features Enabled",
"Federation_Dashboard": "Federation Dashboard",
"FEDERATION_Discovery_Method": "Discovery Method",
@ -1993,6 +2002,8 @@
"Installed": "Installed",
"Installed_at": "Installed at",
"Instance": "Instance",
"Instances": "Instances",
"Instances_health": "Instances Health",
"Instance_Record": "Instance Record",
"Instructions": "Instructions",
"Instructions_to_your_visitor_fill_the_form_to_send_a_message": "Instructions to your visitor fill the form to send a message",
@ -2319,6 +2330,7 @@
"List_of_departments_for_forward_description": "Allow to set a restricted list of departments that can receive chats from this department",
"List_of_departments_to_apply_this_business_hour": "List of departments to apply this business hour",
"List_of_Direct_Messages": "List of Direct Messages",
"Omnichannel": "Omnichannel",
"Livechat": "Livechat",
"Livechat_abandoned_rooms_closed_custom_message": "Custom message when room is automatically closed by visitor inactivity",
"Livechat_agents": "Omnichannel agents",
@ -2899,6 +2911,8 @@
"Passwords_do_not_match": "Passwords do not match",
"Past_Chats": "Past Chats",
"Paste_here": "Paste here...",
"Paste": "Paste",
"Paste_error": "Error reading from clipboard",
"Payload": "Payload",
"Peer_Password": "Peer Password",
"People": "People",
@ -3013,6 +3027,7 @@
"Purchase_for_price": "Purchase for $%s",
"Purchased": "Purchased",
"Push": "Push",
"Push_Notifications": "Push Notifications",
"Push_apn_cert": "APN Cert",
"Push_apn_dev_cert": "APN Dev Cert",
"Push_apn_dev_key": "APN Dev Key",
@ -3409,6 +3424,7 @@
"Setup_Wizard": "Setup Wizard",
"Setup_Wizard_Info": "We'll guide you through setting up your first admin user, configuring your organisation and registering your server to receive free push notifications and more.",
"Share_Location_Title": "Share Location?",
"Canned_responses": "Canned responses",
"Shared_Location": "Shared Location",
"Shared_Secret": "Shared Secret",
"Shortcut": "Shortcut",
@ -3787,6 +3803,7 @@
"Two-factor_authentication_is_currently_disabled": "Two-factor authentication via TOTP is currently disabled",
"Two-factor_authentication_native_mobile_app_warning": "WARNING: Once you enable this, you will not be able to login on the native mobile apps (Rocket.Chat+) using your password until they implement the 2FA.",
"Type": "Type",
"Types_and_Distribution": "Types and Distribution",
"Type_your_email": "Type your email",
"Type_your_job_title": "Type your job title",
"Type_your_message": "Type your message",

Loading…
Cancel
Save