[NEW] Server Info page (#19517)
parent
b9f2f354b9
commit
14946314ef
@ -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')} |
||||
/>; |
@ -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')} > {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')}</>}>{formatDateAndTime(instanceRecord._createdAt)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={<>{t('Instance_Record')} > {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')} > {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')}</>}>{formatDateAndTime(instanceRecord._createdAt)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={<>{t('Instance_Record')} > {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; |
@ -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 />; |
Loading…
Reference in new issue