[IMPROVE] Administration UI - React and Fuselage components (#15452)
parent
b918d40b82
commit
eedc17b310
@ -1,2 +1,4 @@ |
||||
import '@storybook/addon-actions/register'; |
||||
import '@storybook/addon-knobs/register'; |
||||
import '@storybook/addon-links/register'; |
||||
import '@storybook/addon-viewport/register'; |
||||
|
||||
@ -1,3 +1,49 @@ |
||||
import { configure } from '@storybook/react'; |
||||
import { action } from '@storybook/addon-actions'; |
||||
import { withKnobs }from '@storybook/addon-knobs'; |
||||
import { MINIMAL_VIEWPORTS, INITIAL_VIEWPORTS } from '@storybook/addon-viewport/dist/defaults'; |
||||
import { addDecorator, addParameters, configure } from '@storybook/react'; |
||||
import React from 'react'; |
||||
|
||||
import { ConnectionStatusProvider } from '../client/components/providers/ConnectionStatusProvider.mock'; |
||||
import { TranslationProvider } from '../client/components/providers/TranslationProvider.mock'; |
||||
|
||||
addParameters({ |
||||
viewport: { |
||||
viewports: { |
||||
...MINIMAL_VIEWPORTS, |
||||
...INITIAL_VIEWPORTS, |
||||
}, |
||||
defaultViewport: 'responsive', |
||||
}, |
||||
}) |
||||
|
||||
addDecorator(function RocketChatDecorator(fn) { |
||||
const linkElement = document.getElementById('theme-styles') || document.createElement('link'); |
||||
if (linkElement.id !== 'theme-styles') { |
||||
require('../app/theme/client/main.css'); |
||||
require('../app/theme/client/vendor/fontello/css/fontello.css'); |
||||
require('../client/RocketChat.font.css'); |
||||
linkElement.setAttribute('id', 'theme-styles'); |
||||
linkElement.setAttribute('rel', 'stylesheet'); |
||||
linkElement.setAttribute('href', 'https://open.rocket.chat/theme.css'); |
||||
document.head.appendChild(linkElement); |
||||
} |
||||
|
||||
return <ConnectionStatusProvider connected status='connected' reconnect={action('reconnect')}> |
||||
<TranslationProvider> |
||||
<style>{` |
||||
body { |
||||
background-color: white; |
||||
} |
||||
`}</style>
|
||||
<div dangerouslySetInnerHTML={{ __html: require('!!raw-loader!../private/public/icons.svg').default }} /> |
||||
<div className='global-font-family color-primary-font-color'> |
||||
{fn()} |
||||
</div> |
||||
</TranslationProvider> |
||||
</ConnectionStatusProvider>; |
||||
}); |
||||
|
||||
addDecorator(withKnobs); |
||||
|
||||
configure(require.context('../client', true, /\.stories\.js$/), module); |
||||
|
||||
@ -0,0 +1 @@ |
||||
export default {}; |
||||
@ -1,44 +1 @@ |
||||
import { action } from '@storybook/addon-actions'; |
||||
import '@rocket.chat/icons/dist/font/RocketChat.minimal.css'; |
||||
import React from 'react'; |
||||
|
||||
import '../app/theme/client/main.css'; |
||||
import { ConnectionStatusProvider } from '../client/components/providers/ConnectionStatusProvider.mock'; |
||||
import { TranslationProvider } from '../client/components/providers/TranslationProvider.mock'; |
||||
|
||||
export const rocketChatWrapper = (fn) => |
||||
<ConnectionStatusProvider connected status='connected' reconnect={action('reconnect')}> |
||||
<TranslationProvider> |
||||
<style>{` |
||||
body { |
||||
background-color: white; |
||||
} |
||||
|
||||
.global-font-family { |
||||
font-family: |
||||
-apple-system, |
||||
BlinkMacSystemFont, |
||||
'Segoe UI', |
||||
Roboto, |
||||
Oxygen, |
||||
Ubuntu, |
||||
Cantarell, |
||||
'Helvetica Neue', |
||||
'Apple Color Emoji', |
||||
'Segoe UI Emoji', |
||||
'Segoe UI Symbol', |
||||
'Meiryo UI', |
||||
Arial, |
||||
sans-serif; |
||||
} |
||||
|
||||
.color-primary-font-color { |
||||
color: #444; |
||||
} |
||||
`}</style>
|
||||
<div dangerouslySetInnerHTML={{__html: require('!!raw-loader!../private/public/icons.svg').default}} /> |
||||
<div className='global-font-family color-primary-font-color'> |
||||
{fn()} |
||||
</div> |
||||
</TranslationProvider> |
||||
</ConnectionStatusProvider>; |
||||
export const dummyDate = new Date(2015, 4, 19); |
||||
|
||||
@ -0,0 +1,74 @@ |
||||
export const Meteor = { |
||||
isClient: true, |
||||
isServer: false, |
||||
_localStorage: window.localStorage, |
||||
absoluteUrl: () => {}, |
||||
userId: () => {}, |
||||
Streamer: () => {}, |
||||
startup: () => {}, |
||||
methods: () => {}, |
||||
call: () => {}, |
||||
}; |
||||
|
||||
Meteor.absoluteUrl.defaultOptions = {}; |
||||
|
||||
export const Tracker = { |
||||
autorun: () => ({ |
||||
stop: () => {}, |
||||
}), |
||||
nonreactive: (fn) => fn(), |
||||
Dependency: () => {}, |
||||
}; |
||||
|
||||
export const Accounts = {}; |
||||
|
||||
export const Mongo = { |
||||
Collection: () => ({ |
||||
find: () => ({ |
||||
observe: () => {}, |
||||
fetch: () => [], |
||||
}) |
||||
}), |
||||
}; |
||||
|
||||
export const ReactiveVar = () => ({ |
||||
get: () => {}, |
||||
set: () => {}, |
||||
}); |
||||
|
||||
export const ReactiveDict = () => ({ |
||||
get: () => {}, |
||||
set: () => {}, |
||||
all: () => {}, |
||||
}); |
||||
|
||||
export const Template = () => ({ |
||||
onCreated: () => {}, |
||||
onRendered: () => {}, |
||||
onDestroyed: () => {}, |
||||
helpers: () => {}, |
||||
events: () => {}, |
||||
}); |
||||
|
||||
Template.registerHelper = () => {}; |
||||
Template.__checkName = () => {}; |
||||
|
||||
export const Blaze = { |
||||
Template, |
||||
registerHelper: () => {}, |
||||
}; |
||||
|
||||
window.Blaze = Blaze; |
||||
|
||||
export const check = () => {}; |
||||
|
||||
export const FlowRouter = { |
||||
route: () => {} |
||||
}; |
||||
|
||||
export const BlazeLayout = {}; |
||||
|
||||
export const Session = { |
||||
get: () => {}, |
||||
set: () => {}, |
||||
}; |
||||
@ -0,0 +1,11 @@ |
||||
import './modalTemplates/iframeModal.html'; |
||||
import './modalTemplates/iframeModal'; |
||||
import './marketplace'; |
||||
import './apps'; |
||||
import './appInstall.html'; |
||||
import './appInstall'; |
||||
import './appLogs.html'; |
||||
import './appLogs'; |
||||
import './appManage'; |
||||
import './appWhatIsIt.html'; |
||||
import './appWhatIsIt'; |
||||
@ -1,14 +1,3 @@ |
||||
import './admin/modalTemplates/iframeModal.html'; |
||||
import './admin/modalTemplates/iframeModal'; |
||||
import './admin/marketplace'; |
||||
import './admin/apps'; |
||||
import './admin/appInstall.html'; |
||||
import './admin/appInstall'; |
||||
import './admin/appLogs.html'; |
||||
import './admin/appLogs'; |
||||
import './admin/appManage'; |
||||
import './admin/appWhatIsIt.html'; |
||||
import './admin/appWhatIsIt'; |
||||
import './routes'; |
||||
|
||||
export { Apps } from './orchestrator'; |
||||
|
||||
@ -0,0 +1,4 @@ |
||||
import './permissions.html'; |
||||
import './permissions'; |
||||
import './permissionsRole.html'; |
||||
import './permissionsRole'; |
||||
@ -0,0 +1,2 @@ |
||||
import './cloud'; |
||||
import './callback'; |
||||
@ -0,0 +1,8 @@ |
||||
import './adminSoundEdit.html'; |
||||
import './adminSoundInfo.html'; |
||||
import './adminSounds.html'; |
||||
import './adminSounds'; |
||||
import './soundEdit.html'; |
||||
import './soundEdit'; |
||||
import './soundInfo.html'; |
||||
import './soundInfo'; |
||||
@ -0,0 +1,9 @@ |
||||
import './adminEmoji.html'; |
||||
import './adminEmoji'; |
||||
import './adminEmojiEdit.html'; |
||||
import './adminEmojiInfo.html'; |
||||
import './emojiEdit.html'; |
||||
import './emojiEdit'; |
||||
import './emojiInfo.html'; |
||||
import './emojiInfo'; |
||||
import './emojiPreview.html'; |
||||
@ -0,0 +1,8 @@ |
||||
import './adminImport.html'; |
||||
import './adminImport'; |
||||
import './adminImportHistory.html'; |
||||
import './adminImportHistory'; |
||||
import './adminImportPrepare.html'; |
||||
import './adminImportPrepare'; |
||||
import './adminImportProgress.html'; |
||||
import './adminImportProgress'; |
||||
@ -0,0 +1,11 @@ |
||||
import './integrations.html'; |
||||
import './integrations'; |
||||
import './integrationsNew.html'; |
||||
import './integrationsNew'; |
||||
import './integrationsIncoming.html'; |
||||
import './integrationsIncoming'; |
||||
import './integrationsOutgoing.html'; |
||||
import './integrationsOutgoing'; |
||||
import './integrationsOutgoingHistory.html'; |
||||
import './integrationsOutgoingHistory'; |
||||
import './additional/zapier.html'; |
||||
@ -0,0 +1,4 @@ |
||||
import './mailer.html'; |
||||
import './mailer'; |
||||
import './mailerUnsubscribe.html'; |
||||
import './mailerUnsubscribe'; |
||||
@ -0,0 +1,4 @@ |
||||
import './oauthApp.html'; |
||||
import './oauthApp'; |
||||
import './oauthApps.html'; |
||||
import './oauthApps'; |
||||
@ -1,18 +1,9 @@ |
||||
import './admin.html'; |
||||
import './adminFlex.html'; |
||||
import './rooms/adminRooms.html'; |
||||
import './rooms/adminRoomInfo.html'; |
||||
import './rooms/adminRoomInfo'; |
||||
import './rooms/channelSettingsDefault.html'; |
||||
import './rooms/channelSettingsDefault'; |
||||
import './users/adminInviteUser.html'; |
||||
import './users/adminUserChannels.html'; |
||||
import './users/adminUserEdit.html'; |
||||
import './users/adminUserInfo.html'; |
||||
import './users/adminUsers.html'; |
||||
import './admin'; |
||||
import './adminFlex'; |
||||
import './rooms/adminRooms'; |
||||
import './users/adminInviteUser'; |
||||
import './users/adminUserChannels'; |
||||
import './users/adminUsers'; |
||||
import './routes'; |
||||
|
||||
|
||||
// import './users/adminUserChannels';
|
||||
// import './users/adminUserChannels.html';
|
||||
|
||||
@ -0,0 +1,6 @@ |
||||
import './adminRooms.html'; |
||||
import './adminRoomInfo.html'; |
||||
import './adminRoomInfo'; |
||||
import './channelSettingsDefault.html'; |
||||
import './channelSettingsDefault'; |
||||
import './adminRooms'; |
||||
@ -0,0 +1,19 @@ |
||||
import { FlowRouter } from 'meteor/kadira:flow-router'; |
||||
import { BlazeLayout } from 'meteor/kadira:blaze-layout'; |
||||
|
||||
FlowRouter.route('/admin/users', { |
||||
name: 'admin-users', |
||||
async action() { |
||||
await import('./users/views'); |
||||
BlazeLayout.render('main', { center: 'adminUsers' }); |
||||
}, |
||||
}); |
||||
|
||||
|
||||
FlowRouter.route('/admin/rooms', { |
||||
name: 'admin-rooms', |
||||
async action() { |
||||
await import('./rooms/views'); |
||||
BlazeLayout.render('main', { center: 'adminRooms' }); |
||||
}, |
||||
}); |
||||
@ -1,9 +0,0 @@ |
||||
<template name="adminUserChannels"> |
||||
{{#unless hasPermission 'view-full-other-user-info'}} |
||||
<p>{{_ "You_are_not_authorized_to_view_this_page"}}</p> |
||||
{{else}} |
||||
<div class="user-info-channel"> |
||||
<h3><a href="{{route}}"><i class="icon-{{type}}"></i> {{name}}</a></h3> |
||||
</div> |
||||
{{/unless}} |
||||
</template> |
||||
@ -1,29 +0,0 @@ |
||||
import { FlowRouter } from 'meteor/kadira:flow-router'; |
||||
import { Template } from 'meteor/templating'; |
||||
|
||||
Template.adminUserChannels.helpers({ |
||||
type() { |
||||
if (this.t === 'd') { |
||||
return 'at'; |
||||
} if (this.t === 'p') { |
||||
return 'lock'; |
||||
} |
||||
return 'hash'; |
||||
}, |
||||
route() { |
||||
switch (this.t) { |
||||
case 'd': |
||||
return FlowRouter.path('direct', { |
||||
username: this.name, |
||||
}); |
||||
case 'p': |
||||
return FlowRouter.path('group', { |
||||
name: this.name, |
||||
}); |
||||
case 'c': |
||||
return FlowRouter.path('channel', { |
||||
name: this.name, |
||||
}); |
||||
} |
||||
}, |
||||
}); |
||||
@ -0,0 +1,6 @@ |
||||
import './adminInviteUser.html'; |
||||
import './adminUserEdit.html'; |
||||
import './adminUserInfo.html'; |
||||
import './adminUsers.html'; |
||||
import './adminInviteUser'; |
||||
import './adminUsers'; |
||||
@ -0,0 +1,9 @@ |
||||
import './adminUserStatus.html'; |
||||
import './adminUserStatus'; |
||||
import './adminUserStatusEdit.html'; |
||||
import './adminUserStatusInfo.html'; |
||||
import './userStatusEdit.html'; |
||||
import './userStatusEdit'; |
||||
import './userStatusInfo.html'; |
||||
import './userStatusInfo'; |
||||
import './userStatusPreview.html'; |
||||
@ -1,14 +0,0 @@ |
||||
@font-face { |
||||
font-family: 'RocketChat'; |
||||
font-weight: 400; |
||||
font-style: normal; |
||||
font-display: auto; |
||||
|
||||
src: url('/fonts/RocketChat.eot'); |
||||
src: |
||||
url('/fonts/RocketChat.eot?#iefix') format('embedded-opentype'), |
||||
url('/fonts/RocketChat.woff2') format('woff2'), |
||||
url('/fonts/RocketChat.woff') format('woff'), |
||||
url('/fonts/RocketChat.ttf') format('truetype'), |
||||
url('/fonts/RocketChat.svg#RocketChat') format('svg'); |
||||
} |
||||
@ -0,0 +1,10 @@ |
||||
import { useEffect } from 'react'; |
||||
|
||||
import { SideNav } from '../../../app/ui-utils/client'; |
||||
|
||||
export const useAdminSideNav = () => { |
||||
useEffect(() => { |
||||
SideNav.setFlex('adminFlex'); |
||||
SideNav.openFlex(); |
||||
}, []); |
||||
}; |
||||
@ -0,0 +1,24 @@ |
||||
import React from 'react'; |
||||
|
||||
import { dummyDate } from '../../../../.storybook/helpers'; |
||||
import { BuildEnvironmentSection } from './BuildEnvironmentSection'; |
||||
|
||||
export default { |
||||
title: 'admin/info/BuildEnvironmentSection', |
||||
component: BuildEnvironmentSection, |
||||
decorators: [ |
||||
(fn) => <div className='rc-old'>{fn()}</div>, |
||||
], |
||||
}; |
||||
|
||||
const info = { |
||||
compile: { |
||||
platform: 'info.compile.platform', |
||||
arch: 'info.compile.arch', |
||||
osRelease: 'info.compile.osRelease', |
||||
nodeVersion: 'info.compile.nodeVersion', |
||||
date: dummyDate, |
||||
}, |
||||
}; |
||||
|
||||
export const _default = () => <BuildEnvironmentSection info={info} />; |
||||
@ -0,0 +1,24 @@ |
||||
import React from 'react'; |
||||
|
||||
import { CommitSection } from './CommitSection'; |
||||
|
||||
export default { |
||||
title: 'admin/info/CommitSection', |
||||
component: CommitSection, |
||||
decorators: [ |
||||
(fn) => <div className='rc-old'>{fn()}</div>, |
||||
], |
||||
}; |
||||
|
||||
const info = { |
||||
commit: { |
||||
hash: 'info.commit.hash', |
||||
date: 'info.commit.date', |
||||
branch: 'info.commit.branch', |
||||
tag: 'info.commit.tag', |
||||
author: 'info.commit.author', |
||||
subject: 'info.commit.subject', |
||||
}, |
||||
}; |
||||
|
||||
export const _default = () => <CommitSection info={info} />; |
||||
@ -0,0 +1,16 @@ |
||||
import React from 'react'; |
||||
|
||||
export const DescriptionList = ({ children }) => |
||||
<table className='statistics-table secondary-background-color'> |
||||
<tbody> |
||||
{children} |
||||
</tbody> |
||||
</table>; |
||||
|
||||
const Entry = ({ children, label }) => |
||||
<tr className='admin-table-row'> |
||||
<th className='content-background-color border-component-color'>{label}</th> |
||||
<td className='border-component-color'>{children}</td> |
||||
</tr>; |
||||
|
||||
DescriptionList.Entry = Entry; |
||||
@ -0,0 +1,20 @@ |
||||
import React from 'react'; |
||||
|
||||
import { DescriptionList } from './DescriptionList'; |
||||
|
||||
export default { |
||||
title: 'admin/info/DescriptionList', |
||||
component: DescriptionList, |
||||
decorators: [ |
||||
(fn) => <div className='rc-old'>{fn()}</div>, |
||||
(fn) => <section className='page-container page-list'> |
||||
<div className='content'> |
||||
{fn()} |
||||
</div> |
||||
</section>, |
||||
], |
||||
}; |
||||
|
||||
export const _default = () => <DescriptionList> |
||||
<DescriptionList.Entry label='Key'>Value</DescriptionList.Entry> |
||||
</DescriptionList>; |
||||
@ -1,7 +0,0 @@ |
||||
import React from 'react'; |
||||
|
||||
export const InformationEntry = ({ children, label }) => |
||||
<tr className='admin-table-row'> |
||||
<th className='content-background-color border-component-color'>{label}</th> |
||||
<td className='border-component-color'>{children}</td> |
||||
</tr>; |
||||
@ -1,8 +0,0 @@ |
||||
import React from 'react'; |
||||
|
||||
export const InformationList = ({ children }) => |
||||
<table className='statistics-table secondary-background-color'> |
||||
<tbody> |
||||
{children} |
||||
</tbody> |
||||
</table>; |
||||
@ -0,0 +1,151 @@ |
||||
import { action } from '@storybook/addon-actions'; |
||||
import { boolean, object } from '@storybook/addon-knobs/react'; |
||||
import React from 'react'; |
||||
|
||||
import { dummyDate } from '../../../../.storybook/helpers'; |
||||
import { InformationPage } from './InformationPage'; |
||||
|
||||
export default { |
||||
title: 'admin/info/InformationPage', |
||||
component: InformationPage, |
||||
decorators: [ |
||||
(fn) => <div className='rc-old'>{fn()}</div>, |
||||
], |
||||
}; |
||||
|
||||
const info = { |
||||
marketplaceApiVersion: 'info.marketplaceApiVersion', |
||||
commit: { |
||||
hash: 'info.commit.hash', |
||||
date: 'info.commit.date', |
||||
branch: 'info.commit.branch', |
||||
tag: 'info.commit.tag', |
||||
author: 'info.commit.author', |
||||
subject: 'info.commit.subject', |
||||
}, |
||||
compile: { |
||||
platform: 'info.compile.platform', |
||||
arch: 'info.compile.arch', |
||||
osRelease: 'info.compile.osRelease', |
||||
nodeVersion: 'info.compile.nodeVersion', |
||||
date: dummyDate, |
||||
}, |
||||
}; |
||||
|
||||
const statistics = { |
||||
version: 'statistics.version', |
||||
migration: { |
||||
version: 'statistics.migration.version', |
||||
lockedAt: dummyDate, |
||||
}, |
||||
installedAt: dummyDate, |
||||
process: { |
||||
nodeVersion: 'statistics.process.nodeVersion', |
||||
uptime: 10 * 24 * 60 * 60, |
||||
pid: 'statistics.process.pid', |
||||
}, |
||||
uniqueId: 'statistics.uniqueId', |
||||
instanceCount: 1, |
||||
oplogEnabled: true, |
||||
os: { |
||||
type: 'statistics.os.type', |
||||
platform: 'statistics.os.platform', |
||||
arch: 'statistics.os.arch', |
||||
release: 'statistics.os.release', |
||||
uptime: 10 * 24 * 60 * 60, |
||||
loadavg: [1.1, 1.5, 1.15], |
||||
totalmem: 1024, |
||||
freemem: 1024, |
||||
cpus: [{}], |
||||
}, |
||||
mongoVersion: 'statistics.mongoVersion', |
||||
mongoStorageEngine: 'statistics.mongoStorageEngine', |
||||
totalUsers: 'statistics.totalUsers', |
||||
nonActiveUsers: 'nonActiveUsers', |
||||
activeUsers: 'statistics.activeUsers', |
||||
totalConnectedUsers: 'statistics.totalConnectedUsers', |
||||
onlineUsers: 'statistics.onlineUsers', |
||||
awayUsers: 'statistics.awayUsers', |
||||
offlineUsers: 'statistics.offlineUsers', |
||||
totalRooms: 'statistics.totalRooms', |
||||
totalChannels: 'statistics.totalChannels', |
||||
totalPrivateGroups: 'statistics.totalPrivateGroups', |
||||
totalDirect: 'statistics.totalDirect', |
||||
totalLivechat: 'statistics.totalLivechat', |
||||
totalDiscussions: 'statistics.totalDiscussions', |
||||
totalThreads: 'statistics.totalThreads', |
||||
totalMessages: 'statistics.totalMessages', |
||||
totalChannelMessages: 'statistics.totalChannelMessages', |
||||
totalPrivateGroupMessages: 'statistics.totalPrivateGroupMessages', |
||||
totalDirectMessages: 'statistics.totalDirectMessages', |
||||
totalLivechatMessages: 'statistics.totalLivechatMessages', |
||||
uploadsTotal: 'statistics.uploadsTotal', |
||||
uploadsTotalSize: 1024, |
||||
integrations: { |
||||
totalIntegrations: 'statistics.integrations.totalIntegrations', |
||||
totalIncoming: 'statistics.integrations.totalIncoming', |
||||
totalIncomingActive: 'statistics.integrations.totalIncomingActive', |
||||
totalOutgoing: 'statistics.integrations.totalOutgoing', |
||||
totalOutgoingActive: 'statistics.integrations.totalOutgoingActive', |
||||
totalWithScriptEnabled: 'statistics.integrations.totalWithScriptEnabled', |
||||
}, |
||||
}; |
||||
|
||||
const instances = [ |
||||
{ |
||||
address: 'instances[].address', |
||||
broadcastAuth: 'instances[].broadcastAuth', |
||||
currentStatus: { |
||||
connected: 'instances[].currentStatus.connected', |
||||
retryCount: 'instances[].currentStatus.retryCount', |
||||
status: 'instances[].currentStatus.status', |
||||
}, |
||||
instanceRecord: { |
||||
_id: 'instances[].instanceRecord._id', |
||||
pid: 'instances[].instanceRecord.pid', |
||||
_createdAt: dummyDate, |
||||
_updatedAt: dummyDate, |
||||
}, |
||||
}, |
||||
]; |
||||
|
||||
export const _default = () => |
||||
<InformationPage |
||||
canViewStatistics={boolean('canViewStatistics', true)} |
||||
isLoading={boolean('isLoading', false)} |
||||
info={object('info', info)} |
||||
statistics={object('statistics', statistics)} |
||||
instances={object('instances', instances)} |
||||
onClickRefreshButton={action('clickRefreshButton')} |
||||
/>; |
||||
|
||||
export const withoutCanViewStatisticsPermission = () => |
||||
<InformationPage |
||||
info={info} |
||||
onClickRefreshButton={action('clickRefreshButton')} |
||||
/>; |
||||
|
||||
export const loading = () => |
||||
<InformationPage |
||||
canViewStatistics |
||||
isLoading |
||||
info={info} |
||||
onClickRefreshButton={action('clickRefreshButton')} |
||||
/>; |
||||
|
||||
export const withStatistics = () => |
||||
<InformationPage |
||||
canViewStatistics |
||||
info={info} |
||||
statistics={statistics} |
||||
onClickRefreshButton={action('clickRefreshButton')} |
||||
/>; |
||||
|
||||
export const withOneInstance = () => |
||||
<InformationPage |
||||
canViewStatistics |
||||
info={info} |
||||
statistics={statistics} |
||||
instances={instances} |
||||
onClickRefreshButton={action('clickRefreshButton')} |
||||
/>; |
||||
@ -0,0 +1,77 @@ |
||||
import React, { useState, useEffect } from 'react'; |
||||
|
||||
import { useMethod } from '../../../hooks/useMethod'; |
||||
import { useViewStatisticsPermission } from '../../../hooks/usePermissions'; |
||||
import { useRocketChatInformation } from '../../../hooks/useRocketChatInformation'; |
||||
import { useAdminSideNav } from '../hooks'; |
||||
import { InformationPage } from './InformationPage'; |
||||
|
||||
export function InformationRoute() { |
||||
useAdminSideNav(); |
||||
|
||||
const canViewStatistics = useViewStatisticsPermission(); |
||||
|
||||
const [isLoading, setLoading] = useState(true); |
||||
const [statistics, setStatistics] = useState({}); |
||||
const [instances, setInstances] = useState([]); |
||||
const [fetchStatistics, setFetchStatistics] = useState(() => () => ({})); |
||||
const getStatistics = useMethod('getStatistics'); |
||||
const getInstances = useMethod('instances/get'); |
||||
|
||||
useEffect(() => { |
||||
let didCancel = false; |
||||
|
||||
const fetchStatistics = async () => { |
||||
if (!canViewStatistics) { |
||||
setStatistics(null); |
||||
setInstances(null); |
||||
return; |
||||
} |
||||
|
||||
setLoading(true); |
||||
|
||||
try { |
||||
const [statistics, instances] = await Promise.all([ |
||||
getStatistics(), |
||||
getInstances(), |
||||
]); |
||||
|
||||
if (didCancel) { |
||||
return; |
||||
} |
||||
|
||||
setStatistics(statistics); |
||||
setInstances(instances); |
||||
} finally { |
||||
setLoading(false); |
||||
} |
||||
}; |
||||
|
||||
setFetchStatistics(() => fetchStatistics); |
||||
|
||||
fetchStatistics(); |
||||
|
||||
return () => { |
||||
didCancel = true; |
||||
}; |
||||
}, [canViewStatistics]); |
||||
|
||||
const info = useRocketChatInformation(); |
||||
|
||||
const handleClickRefreshButton = () => { |
||||
if (isLoading) { |
||||
return; |
||||
} |
||||
|
||||
fetchStatistics(); |
||||
}; |
||||
|
||||
return <InformationPage |
||||
canViewStatistics={canViewStatistics} |
||||
isLoading={isLoading} |
||||
info={info} |
||||
statistics={statistics} |
||||
instances={instances} |
||||
onClickRefreshButton={handleClickRefreshButton} |
||||
/>; |
||||
} |
||||
@ -0,0 +1,32 @@ |
||||
import React from 'react'; |
||||
|
||||
import { dummyDate } from '../../../../.storybook/helpers'; |
||||
import { InstancesSection } from './InstancesSection'; |
||||
|
||||
export default { |
||||
title: 'admin/info/InstancesSection', |
||||
component: InstancesSection, |
||||
decorators: [ |
||||
(fn) => <div className='rc-old'>{fn()}</div>, |
||||
], |
||||
}; |
||||
|
||||
const instances = [ |
||||
{ |
||||
address: 'instances[].address', |
||||
broadcastAuth: 'instances[].broadcastAuth', |
||||
currentStatus: { |
||||
connected: 'instances[].currentStatus.connected', |
||||
retryCount: 'instances[].currentStatus.retryCount', |
||||
status: 'instances[].currentStatus.status', |
||||
}, |
||||
instanceRecord: { |
||||
_id: 'instances[].instanceRecord._id', |
||||
pid: 'instances[].instanceRecord.pid', |
||||
_createdAt: dummyDate, |
||||
_updatedAt: dummyDate, |
||||
}, |
||||
}, |
||||
]; |
||||
|
||||
export const _default = () => <InstancesSection instances={instances} />; |
||||
@ -1,34 +1,29 @@ |
||||
import { Text } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { SkeletonText } from './SkeletonText'; |
||||
import { useTranslation } from '../../providers/TranslationProvider'; |
||||
import { formatDate, formatHumanReadableTime } from './formatters'; |
||||
import { InformationList } from './InformationList'; |
||||
import { InformationEntry } from './InformationEntry'; |
||||
import { DescriptionList } from './DescriptionList'; |
||||
|
||||
export function RocketChatSection({ info, statistics, isLoading }) { |
||||
const s = (fn) => (isLoading ? <SkeletonText /> : fn()); |
||||
const s = (fn) => (isLoading ? <Text.Skeleton animated width={'1/2'} /> : fn()); |
||||
const t = useTranslation(); |
||||
|
||||
const appsEngineVersion = info.marketplaceApiVersion; |
||||
|
||||
if (!statistics) { |
||||
return null; |
||||
} |
||||
const appsEngineVersion = info && info.marketplaceApiVersion; |
||||
|
||||
return <> |
||||
<h3>{t('Rocket.Chat')}</h3> |
||||
<InformationList> |
||||
<InformationEntry label={t('Version')}>{s(() => statistics.version)}</InformationEntry> |
||||
{appsEngineVersion && <InformationEntry label={t('Apps_Engine_Version')}>{appsEngineVersion}</InformationEntry>} |
||||
<InformationEntry label={t('DB_Migration')}>{s(() => statistics.migration.version)}</InformationEntry> |
||||
<InformationEntry label={t('DB_Migration_Date')}>{s(() => formatDate(statistics.migration.lockedAt))}</InformationEntry> |
||||
<InformationEntry label={t('Installed_at')}>{s(() => formatDate(statistics.installedAt))}</InformationEntry> |
||||
<InformationEntry label={t('Uptime')}>{s(() => formatHumanReadableTime(statistics.process.uptime, t))}</InformationEntry> |
||||
<InformationEntry label={t('Deployment_ID')}>{s(() => statistics.uniqueId)}</InformationEntry> |
||||
<InformationEntry label={t('PID')}>{s(() => statistics.process.pid)}</InformationEntry> |
||||
<InformationEntry label={t('Running_Instances')}>{s(() => statistics.instanceCount)}</InformationEntry> |
||||
<InformationEntry label={t('OpLog')}>{s(() => (statistics.oplogEnabled ? t('Enabled') : t('Disabled')))}</InformationEntry> |
||||
</InformationList> |
||||
<DescriptionList> |
||||
<DescriptionList.Entry label={t('Version')}>{s(() => statistics.version)}</DescriptionList.Entry> |
||||
{appsEngineVersion && <DescriptionList.Entry label={t('Apps_Engine_Version')}>{appsEngineVersion}</DescriptionList.Entry>} |
||||
<DescriptionList.Entry label={t('DB_Migration')}>{s(() => statistics.migration.version)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('DB_Migration_Date')}>{s(() => formatDate(statistics.migration.lockedAt))}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Installed_at')}>{s(() => formatDate(statistics.installedAt))}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Uptime')}>{s(() => formatHumanReadableTime(statistics.process.uptime, t))}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Deployment_ID')}>{s(() => statistics.uniqueId)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('PID')}>{s(() => statistics.process.pid)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Running_Instances')}>{s(() => statistics.instanceCount)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('OpLog')}>{s(() => (statistics.oplogEnabled ? t('Enabled') : t('Disabled')))}</DescriptionList.Entry> |
||||
</DescriptionList> |
||||
</>; |
||||
} |
||||
|
||||
@ -0,0 +1,36 @@ |
||||
import React from 'react'; |
||||
|
||||
import { dummyDate } from '../../../../.storybook/helpers'; |
||||
import { RocketChatSection } from './RocketChatSection'; |
||||
|
||||
export default { |
||||
title: 'admin/info/RocketChatSection', |
||||
component: RocketChatSection, |
||||
decorators: [ |
||||
(fn) => <div className='rc-old'>{fn()}</div>, |
||||
], |
||||
}; |
||||
|
||||
const info = { |
||||
marketplaceApiVersion: 'info.marketplaceApiVersion', |
||||
}; |
||||
|
||||
const statistics = { |
||||
version: 'statistics.version', |
||||
migration: { |
||||
version: 'statistics.migration.version', |
||||
lockedAt: dummyDate, |
||||
}, |
||||
installedAt: dummyDate, |
||||
process: { |
||||
uptime: 10 * 24 * 60 * 60, |
||||
pid: 'statistics.process.pid', |
||||
}, |
||||
uniqueId: 'statistics.uniqueId', |
||||
instanceCount: 1, |
||||
oplogEnabled: true, |
||||
}; |
||||
|
||||
export const _default = () => <RocketChatSection info={info} statistics={statistics} />; |
||||
|
||||
export const loading = () => <RocketChatSection info={{}} statistics={{}} isLoading />; |
||||
@ -1,34 +1,29 @@ |
||||
import { Text } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { SkeletonText } from './SkeletonText'; |
||||
import { useTranslation } from '../../providers/TranslationProvider'; |
||||
import { DescriptionList } from './DescriptionList'; |
||||
import { formatMemorySize, formatHumanReadableTime, formatCPULoad } from './formatters'; |
||||
import { InformationList } from './InformationList'; |
||||
import { InformationEntry } from './InformationEntry'; |
||||
|
||||
export function RuntimeEnvironmentSection({ statistics, isLoading }) { |
||||
const s = (fn) => (isLoading ? <SkeletonText /> : fn()); |
||||
const s = (fn) => (isLoading ? <Text.Skeleton animated width={'1/2'} /> : fn()); |
||||
const t = useTranslation(); |
||||
|
||||
if (!statistics) { |
||||
return null; |
||||
} |
||||
|
||||
return <> |
||||
<h3>{t('Runtime_Environment')}</h3> |
||||
<InformationList> |
||||
<InformationEntry label={t('OS_Type')}>{s(() => statistics.os.type)}</InformationEntry> |
||||
<InformationEntry label={t('OS_Platform')}>{s(() => statistics.os.platform)}</InformationEntry> |
||||
<InformationEntry label={t('OS_Arch')}>{s(() => statistics.os.arch)}</InformationEntry> |
||||
<InformationEntry label={t('OS_Release')}>{s(() => statistics.os.release)}</InformationEntry> |
||||
<InformationEntry label={t('Node_version')}>{s(() => statistics.process.nodeVersion)}</InformationEntry> |
||||
<InformationEntry label={t('Mongo_version')}>{s(() => statistics.mongoVersion)}</InformationEntry> |
||||
<InformationEntry label={t('Mongo_storageEngine')}>{s(() => statistics.mongoStorageEngine)}</InformationEntry> |
||||
<InformationEntry label={t('OS_Uptime')}>{s(() => formatHumanReadableTime(statistics.os.uptime, t))}</InformationEntry> |
||||
<InformationEntry label={t('OS_Loadavg')}>{s(() => formatCPULoad(statistics.os.loadavg))}</InformationEntry> |
||||
<InformationEntry label={t('OS_Totalmem')}>{s(() => formatMemorySize(statistics.os.totalmem))}</InformationEntry> |
||||
<InformationEntry label={t('OS_Freemem')}>{s(() => formatMemorySize(statistics.os.freemem))}</InformationEntry> |
||||
<InformationEntry label={t('OS_Cpus')}>{s(() => statistics.os.cpus.length)}</InformationEntry> |
||||
</InformationList> |
||||
<DescriptionList> |
||||
<DescriptionList.Entry label={t('OS_Type')}>{s(() => statistics.os.type)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('OS_Platform')}>{s(() => statistics.os.platform)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('OS_Arch')}>{s(() => statistics.os.arch)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('OS_Release')}>{s(() => statistics.os.release)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Node_version')}>{s(() => statistics.process.nodeVersion)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Mongo_version')}>{s(() => statistics.mongoVersion)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Mongo_storageEngine')}>{s(() => statistics.mongoStorageEngine)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('OS_Uptime')}>{s(() => formatHumanReadableTime(statistics.os.uptime, t))}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('OS_Loadavg')}>{s(() => formatCPULoad(statistics.os.loadavg))}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('OS_Totalmem')}>{s(() => formatMemorySize(statistics.os.totalmem))}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('OS_Freemem')}>{s(() => formatMemorySize(statistics.os.freemem))}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('OS_Cpus')}>{s(() => statistics.os.cpus.length)}</DescriptionList.Entry> |
||||
</DescriptionList> |
||||
</>; |
||||
} |
||||
|
||||
@ -0,0 +1,34 @@ |
||||
import React from 'react'; |
||||
|
||||
import { RuntimeEnvironmentSection } from './RuntimeEnvironmentSection'; |
||||
|
||||
export default { |
||||
title: 'admin/info/RuntimeEnvironmentSection', |
||||
component: RuntimeEnvironmentSection, |
||||
decorators: [ |
||||
(fn) => <div className='rc-old'>{fn()}</div>, |
||||
], |
||||
}; |
||||
|
||||
const statistics = { |
||||
os: { |
||||
type: 'statistics.os.type', |
||||
platform: 'statistics.os.platform', |
||||
arch: 'statistics.os.arch', |
||||
release: 'statistics.os.release', |
||||
uptime: 10 * 24 * 60 * 60, |
||||
loadavg: [1.1, 1.5, 1.15], |
||||
totalmem: 1024, |
||||
freemem: 1024, |
||||
cpus: [{}], |
||||
}, |
||||
process: { |
||||
nodeVersion: 'statistics.process.nodeVersion', |
||||
}, |
||||
mongoVersion: 'statistics.mongoVersion', |
||||
mongoStorageEngine: 'statistics.mongoStorageEngine', |
||||
}; |
||||
|
||||
export const _default = () => <RuntimeEnvironmentSection statistics={statistics} />; |
||||
|
||||
export const loading = () => <RuntimeEnvironmentSection statistics={{}} isLoading />; |
||||
@ -1,28 +0,0 @@ |
||||
.Admin__InformationPage__SkeletonText { |
||||
display: inline-flex; |
||||
|
||||
min-width: 10em; |
||||
height: 1em; |
||||
|
||||
animation: Admin__InformationPage__SkeletonText__animation 1s linear 1s infinite running; |
||||
|
||||
opacity: 0.25; |
||||
background: |
||||
linear-gradient( |
||||
to right, |
||||
transparent, |
||||
currentColor 50%, |
||||
transparent 100% |
||||
); |
||||
background-size: 100vw 100vh; |
||||
} |
||||
|
||||
@keyframes Admin__InformationPage__SkeletonText__animation { |
||||
0% { |
||||
background-position: 0 0; |
||||
} |
||||
|
||||
100% { |
||||
background-position: 100vw 0; |
||||
} |
||||
} |
||||
@ -1,9 +0,0 @@ |
||||
import React, { useMemo } from 'react'; |
||||
import './SkeletonText.css'; |
||||
|
||||
|
||||
export function SkeletonText() { |
||||
const width = useMemo(() => `${ Math.random() * 10 + 10 }em`, []); |
||||
|
||||
return <span className='Admin__InformationPage__SkeletonText' style={{ width }} />; |
||||
} |
||||
@ -1,53 +1,48 @@ |
||||
import { Text } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import { useTranslation } from '../../contexts/TranslationContext'; |
||||
import { SkeletonText } from './SkeletonText'; |
||||
import { useTranslation } from '../../providers/TranslationProvider'; |
||||
import { DescriptionList } from './DescriptionList'; |
||||
import { formatMemorySize } from './formatters'; |
||||
import { InformationList } from './InformationList'; |
||||
import { InformationEntry } from './InformationEntry'; |
||||
|
||||
export function UsageSection({ statistics, isLoading }) { |
||||
const s = (fn) => (isLoading ? <SkeletonText /> : fn()); |
||||
const s = (fn) => (isLoading ? <Text.Skeleton animated width={'1/2'} /> : fn()); |
||||
const t = useTranslation(); |
||||
|
||||
if (!statistics) { |
||||
return null; |
||||
} |
||||
|
||||
return <> |
||||
<h3>{t('Usage')}</h3> |
||||
<InformationList> |
||||
<InformationEntry label={t('Stats_Total_Users')}>{s(() => statistics.totalUsers)}</InformationEntry> |
||||
<InformationEntry label={t('Stats_Active_Users')}>{s(() => statistics.activeUsers)}</InformationEntry> |
||||
<InformationEntry label={t('Stats_Non_Active_Users')}>{s(() => statistics.nonActiveUsers)}</InformationEntry> |
||||
<InformationEntry label={t('Stats_Total_Connected_Users')}>{s(() => statistics.totalConnectedUsers)}</InformationEntry> |
||||
<InformationEntry label={t('Stats_Online_Users')}>{s(() => statistics.onlineUsers)}</InformationEntry> |
||||
<InformationEntry label={t('Stats_Away_Users')}>{s(() => statistics.awayUsers)}</InformationEntry> |
||||
<InformationEntry label={t('Stats_Offline_Users')}>{s(() => statistics.offlineUsers)}</InformationEntry> |
||||
<InformationEntry label={t('Stats_Total_Rooms')}>{s(() => statistics.totalRooms)}</InformationEntry> |
||||
<InformationEntry label={t('Stats_Total_Channels')}>{s(() => statistics.totalChannels)}</InformationEntry> |
||||
<InformationEntry label={t('Stats_Total_Private_Groups')}>{s(() => statistics.totalPrivateGroups)}</InformationEntry> |
||||
<InformationEntry label={t('Stats_Total_Direct_Messages')}>{s(() => statistics.totalDirect)}</InformationEntry> |
||||
<InformationEntry label={t('Stats_Total_Livechat_Rooms')}>{s(() => statistics.totalLivechat)}</InformationEntry> |
||||
<InformationEntry label={t('Total_Discussions')}>{s(() => statistics.totalDiscussions)}</InformationEntry> |
||||
<InformationEntry label={t('Total_Threads')}>{s(() => statistics.totalThreads)}</InformationEntry> |
||||
<InformationEntry label={t('Stats_Total_Messages')}>{s(() => statistics.totalMessages)}</InformationEntry> |
||||
<InformationEntry label={t('Stats_Total_Messages_Channel')}>{s(() => statistics.totalChannelMessages)}</InformationEntry> |
||||
<InformationEntry label={t('Stats_Total_Messages_PrivateGroup')}>{s(() => statistics.totalPrivateGroupMessages)}</InformationEntry> |
||||
<InformationEntry label={t('Stats_Total_Messages_Direct')}>{s(() => statistics.totalDirectMessages)}</InformationEntry> |
||||
<InformationEntry label={t('Stats_Total_Messages_Livechat')}>{s(() => statistics.totalLivechatMessages)}</InformationEntry> |
||||
<InformationEntry label={t('Stats_Total_Uploads')}>{s(() => statistics.uploadsTotal)}</InformationEntry> |
||||
<InformationEntry label={t('Stats_Total_Uploads_Size')}>{s(() => formatMemorySize(statistics.uploadsTotalSize))}</InformationEntry> |
||||
{statistics.apps && <> |
||||
<InformationEntry label={t('Stats_Total_Installed_Apps')}>{statistics.apps.totalInstalled}</InformationEntry> |
||||
<InformationEntry label={t('Stats_Total_Active_Apps')}>{statistics.apps.totalActive}</InformationEntry> |
||||
<DescriptionList> |
||||
<DescriptionList.Entry label={t('Stats_Total_Users')}>{s(() => statistics.totalUsers)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Active_Users')}>{s(() => statistics.activeUsers)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Non_Active_Users')}>{s(() => statistics.nonActiveUsers)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Connected_Users')}>{s(() => statistics.totalConnectedUsers)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Online_Users')}>{s(() => statistics.onlineUsers)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Away_Users')}>{s(() => statistics.awayUsers)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Offline_Users')}>{s(() => statistics.offlineUsers)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Rooms')}>{s(() => statistics.totalRooms)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Channels')}>{s(() => statistics.totalChannels)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Private_Groups')}>{s(() => statistics.totalPrivateGroups)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Direct_Messages')}>{s(() => statistics.totalDirect)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Livechat_Rooms')}>{s(() => statistics.totalLivechat)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Total_Discussions')}>{s(() => statistics.totalDiscussions)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Total_Threads')}>{s(() => statistics.totalThreads)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Messages')}>{s(() => statistics.totalMessages)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Messages_Channel')}>{s(() => statistics.totalChannelMessages)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Messages_PrivateGroup')}>{s(() => statistics.totalPrivateGroupMessages)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Messages_Direct')}>{s(() => statistics.totalDirectMessages)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Messages_Livechat')}>{s(() => statistics.totalLivechatMessages)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Uploads')}>{s(() => statistics.uploadsTotal)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Uploads_Size')}>{s(() => formatMemorySize(statistics.uploadsTotalSize))}</DescriptionList.Entry> |
||||
{statistics && statistics.apps && <> |
||||
<DescriptionList.Entry label={t('Stats_Total_Installed_Apps')}>{statistics.apps.totalInstalled}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Active_Apps')}>{statistics.apps.totalActive}</DescriptionList.Entry> |
||||
</>} |
||||
<InformationEntry label={t('Stats_Total_Integrations')}>{s(() => statistics.integrations.totalIntegrations)}</InformationEntry> |
||||
<InformationEntry label={t('Stats_Total_Incoming_Integrations')}>{s(() => statistics.integrations.totalIncoming)}</InformationEntry> |
||||
<InformationEntry label={t('Stats_Total_Active_Incoming_Integrations')}>{s(() => statistics.integrations.totalIncomingActive)}</InformationEntry> |
||||
<InformationEntry label={t('Stats_Total_Outgoing_Integrations')}>{s(() => statistics.integrations.totalOutgoing)}</InformationEntry> |
||||
<InformationEntry label={t('Stats_Total_Active_Outgoing_Integrations')}>{s(() => statistics.integrations.totalOutgoingActive)}</InformationEntry> |
||||
<InformationEntry label={t('Stats_Total_Integrations_With_Script_Enabled')}>{s(() => statistics.integrations.totalWithScriptEnabled)}</InformationEntry> |
||||
</InformationList> |
||||
<DescriptionList.Entry label={t('Stats_Total_Integrations')}>{s(() => statistics.integrations.totalIntegrations)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Incoming_Integrations')}>{s(() => statistics.integrations.totalIncoming)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Active_Incoming_Integrations')}>{s(() => statistics.integrations.totalIncomingActive)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Outgoing_Integrations')}>{s(() => statistics.integrations.totalOutgoing)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Active_Outgoing_Integrations')}>{s(() => statistics.integrations.totalOutgoingActive)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Integrations_With_Script_Enabled')}>{s(() => statistics.integrations.totalWithScriptEnabled)}</DescriptionList.Entry> |
||||
</DescriptionList> |
||||
</>; |
||||
} |
||||
|
||||
@ -0,0 +1,54 @@ |
||||
import React from 'react'; |
||||
|
||||
import { UsageSection } from './UsageSection'; |
||||
|
||||
export default { |
||||
title: 'admin/info/UsageSection', |
||||
component: UsageSection, |
||||
decorators: [ |
||||
(fn) => <div className='rc-old'>{fn()}</div>, |
||||
], |
||||
}; |
||||
|
||||
const statistics = { |
||||
totalUsers: 'statistics.totalUsers', |
||||
nonActiveUsers: 'nonActiveUsers', |
||||
activeUsers: 'statistics.activeUsers', |
||||
totalConnectedUsers: 'statistics.totalConnectedUsers', |
||||
onlineUsers: 'statistics.onlineUsers', |
||||
awayUsers: 'statistics.awayUsers', |
||||
offlineUsers: 'statistics.offlineUsers', |
||||
totalRooms: 'statistics.totalRooms', |
||||
totalChannels: 'statistics.totalChannels', |
||||
totalPrivateGroups: 'statistics.totalPrivateGroups', |
||||
totalDirect: 'statistics.totalDirect', |
||||
totalLivechat: 'statistics.totalLivechat', |
||||
totalDiscussions: 'statistics.totalDiscussions', |
||||
totalThreads: 'statistics.totalThreads', |
||||
totalMessages: 'statistics.totalMessages', |
||||
totalChannelMessages: 'statistics.totalChannelMessages', |
||||
totalPrivateGroupMessages: 'statistics.totalPrivateGroupMessages', |
||||
totalDirectMessages: 'statistics.totalDirectMessages', |
||||
totalLivechatMessages: 'statistics.totalLivechatMessages', |
||||
uploadsTotal: 'statistics.uploadsTotal', |
||||
uploadsTotalSize: 1024, |
||||
integrations: { |
||||
totalIntegrations: 'statistics.integrations.totalIntegrations', |
||||
totalIncoming: 'statistics.integrations.totalIncoming', |
||||
totalIncomingActive: 'statistics.integrations.totalIncomingActive', |
||||
totalOutgoing: 'statistics.integrations.totalOutgoing', |
||||
totalOutgoingActive: 'statistics.integrations.totalOutgoingActive', |
||||
totalWithScriptEnabled: 'statistics.integrations.totalWithScriptEnabled', |
||||
}, |
||||
}; |
||||
|
||||
const apps = { |
||||
totalInstalled: 'statistics.apps.totalInstalled', |
||||
totalActive: 'statistics.apps.totalActive', |
||||
}; |
||||
|
||||
export const _default = () => <UsageSection statistics={statistics} />; |
||||
|
||||
export const withApps = () => <UsageSection statistics={{ ...statistics, apps }} />; |
||||
|
||||
export const loading = () => <UsageSection statistics={{}} isLoading />; |
||||
@ -0,0 +1,92 @@ |
||||
import { Accordion, Button, Paragraph, Text } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
import styled from 'styled-components'; |
||||
|
||||
import { Header } from '../../header/Header'; |
||||
import { useTranslation } from '../../providers/TranslationProvider'; |
||||
import { Section } from './Section'; |
||||
|
||||
const Wrapper = styled.div` |
||||
margin: 0 auto; |
||||
width: 100%; |
||||
max-width: 590px; |
||||
`;
|
||||
|
||||
export function GroupPage({ children, group, headerButtons }) { |
||||
const t = useTranslation(); |
||||
|
||||
const handleSubmit = (event) => { |
||||
event.preventDefault(); |
||||
group.save(); |
||||
}; |
||||
|
||||
const handleCancelClick = (event) => { |
||||
event.preventDefault(); |
||||
group.cancel(); |
||||
}; |
||||
|
||||
const handleSaveClick = (event) => { |
||||
event.preventDefault(); |
||||
group.save(); |
||||
}; |
||||
|
||||
if (!group) { |
||||
return <section className='page-container page-static page-settings'> |
||||
<Header /> |
||||
<div className='content' /> |
||||
</section>; |
||||
} |
||||
|
||||
return <form action='#' className='page-container' method='post' onSubmit={handleSubmit}> |
||||
<Header rawSectionName={t(group.i18nLabel)}> |
||||
<Header.ButtonSection> |
||||
{group.changed && <Button danger primary type='reset' onClick={handleCancelClick}>{t('Cancel')}</Button>} |
||||
<Button |
||||
children={t('Save_changes')} |
||||
className='save' |
||||
disabled={!group.changed} |
||||
primary |
||||
type='submit' |
||||
onClick={handleSaveClick} |
||||
/> |
||||
{headerButtons} |
||||
</Header.ButtonSection> |
||||
</Header> |
||||
|
||||
<div className='content'> |
||||
<Wrapper> |
||||
{t.has(group.i18nDescription) && <Paragraph hintColor>{t(group.i18nDescription)}</Paragraph>} |
||||
|
||||
<Accordion className='page-settings'> |
||||
{children} |
||||
</Accordion> |
||||
</Wrapper> |
||||
</div> |
||||
</form>; |
||||
} |
||||
|
||||
GroupPage.Skeleton = function Skeleton() { |
||||
const t = useTranslation(); |
||||
|
||||
return <div className='page-container'> |
||||
<Header rawSectionName={<div style={{ width: '20rem' }}><Text.Skeleton animated headline /></div>}> |
||||
<Header.ButtonSection> |
||||
<Button |
||||
children={t('Save_changes')} |
||||
disabled |
||||
primary |
||||
/> |
||||
</Header.ButtonSection> |
||||
</Header> |
||||
|
||||
<div className='content'> |
||||
<Wrapper> |
||||
<Paragraph.Skeleton animated /> |
||||
|
||||
<Accordion className='page-settings'> |
||||
<Section.Skeleton /> |
||||
</Accordion> |
||||
</Wrapper> |
||||
</div> |
||||
</div>; |
||||
}; |
||||
@ -0,0 +1,23 @@ |
||||
import React, { useMemo } from 'react'; |
||||
|
||||
import { AssetsGroupPage } from './groups/AssetsGroupPage'; |
||||
import { OAuthGroupPage } from './groups/OAuthGroupPage'; |
||||
import { GenericGroupPage } from './groups/GenericGroupPage'; |
||||
import { GroupPage } from './GroupPage'; |
||||
import { useGroup } from './SettingsState'; |
||||
|
||||
export function GroupSelector({ groupId }) { |
||||
const group = useGroup(groupId); |
||||
|
||||
const children = useMemo(() => { |
||||
if (!group) { |
||||
return <GroupPage.Skeleton />; |
||||
} |
||||
|
||||
return (group._id === 'Assets' && <AssetsGroupPage group={group} />) |
||||
|| (group._id === 'OAuth' && <OAuthGroupPage group={group} />) |
||||
|| <GenericGroupPage group={group} />; |
||||
}, [group]); |
||||
|
||||
return children; |
||||
} |
||||
@ -0,0 +1,13 @@ |
||||
import React from 'react'; |
||||
|
||||
import { useTranslation } from '../../providers/TranslationProvider'; |
||||
|
||||
export function NotAuthorizedPage() { |
||||
const t = useTranslation(); |
||||
|
||||
return <section className='page-container page-static page-settings'> |
||||
<div className='content'> |
||||
<p>{t('You_are_not_authorized_to_view_this_page')}</p> |
||||
</div> |
||||
</section>; |
||||
} |
||||
@ -0,0 +1,11 @@ |
||||
import React from 'react'; |
||||
|
||||
import { NotAuthorizedPage } from './NotAuthorizedPage'; |
||||
|
||||
export default { |
||||
title: 'admin/settings/NotAuthorizedPage', |
||||
component: NotAuthorizedPage, |
||||
}; |
||||
|
||||
export const _default = () => |
||||
<NotAuthorizedPage />; |
||||
@ -0,0 +1,27 @@ |
||||
import { Button, Icon } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
import styled from 'styled-components'; |
||||
|
||||
import { useTranslation } from '../../providers/TranslationProvider'; |
||||
|
||||
// TODO: get rid of it
|
||||
const StyledResetSettingButton = styled(Button)` |
||||
padding-block: 0 !important; |
||||
padding-top: 0 !important; |
||||
padding-bottom: 0 !important; |
||||
`;
|
||||
|
||||
export function ResetSettingButton(props) { |
||||
const t = useTranslation(); |
||||
|
||||
return <StyledResetSettingButton |
||||
aria-label={t('Reset')} |
||||
danger |
||||
ghost |
||||
small |
||||
title={t('Reset')} |
||||
{...props} |
||||
> |
||||
<Icon name='undo' /> |
||||
</StyledResetSettingButton>; |
||||
} |
||||
@ -0,0 +1,51 @@ |
||||
import { Accordion, Button, FieldGroup, Paragraph, Text } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import { useTranslation } from '../../providers/TranslationProvider'; |
||||
import { Setting } from './Setting'; |
||||
import { useSection } from './SettingsState'; |
||||
|
||||
export function Section({ children, groupId, hasReset = true, help, sectionName, solo }) { |
||||
const section = useSection(groupId, sectionName); |
||||
const t = useTranslation(); |
||||
|
||||
const handleResetSectionClick = () => { |
||||
section.reset(); |
||||
}; |
||||
|
||||
return <Accordion.Item |
||||
data-qa-section={sectionName} |
||||
noncollapsible={solo || !section.name} |
||||
title={section.name && t(section.name)} |
||||
> |
||||
{help && <Paragraph hintColor>{help}</Paragraph>} |
||||
|
||||
<FieldGroup> |
||||
{section.settings.map((settingId) => <Setting key={settingId} settingId={settingId} />)} |
||||
|
||||
{hasReset && section.canReset && <Button |
||||
children={t('Reset_section_settings')} |
||||
className='reset-group' |
||||
danger |
||||
data-section={section.name} |
||||
ghost |
||||
onClick={handleResetSectionClick} |
||||
/>} |
||||
|
||||
{children} |
||||
</FieldGroup> |
||||
</Accordion.Item>; |
||||
} |
||||
|
||||
Section.Skeleton = function Skeleton() { |
||||
return <Accordion.Item |
||||
noncollapsible |
||||
title={<Text.Skeleton animated subtitle />} |
||||
> |
||||
<Paragraph.Skeleton animated /> |
||||
|
||||
<FieldGroup> |
||||
{Array.from({ length: 10 }).map((_, i) => <Setting.Skeleton key={i} />)} |
||||
</FieldGroup> |
||||
</Accordion.Item>; |
||||
}; |
||||
@ -0,0 +1,131 @@ |
||||
import { Callout, Field, InputBox, Label, Text } from '@rocket.chat/fuselage'; |
||||
import { useDebouncedCallback } from '@rocket.chat/fuselage-hooks'; |
||||
import React, { useEffect, useMemo, useState } from 'react'; |
||||
|
||||
import { MarkdownText } from '../../basic/MarkdownText'; |
||||
import { RawText } from '../../basic/RawText'; |
||||
import { useTranslation } from '../../providers/TranslationProvider'; |
||||
import { GenericSettingInput } from './inputs/GenericSettingInput'; |
||||
import { BooleanSettingInput } from './inputs/BooleanSettingInput'; |
||||
import { StringSettingInput } from './inputs/StringSettingInput'; |
||||
import { RelativeUrlSettingInput } from './inputs/RelativeUrlSettingInput'; |
||||
import { PasswordSettingInput } from './inputs/PasswordSettingInput'; |
||||
import { IntSettingInput } from './inputs/IntSettingInput'; |
||||
import { SelectSettingInput } from './inputs/SelectSettingInput'; |
||||
import { LanguageSettingInput } from './inputs/LanguageSettingInput'; |
||||
import { ColorSettingInput } from './inputs/ColorSettingInput'; |
||||
import { FontSettingInput } from './inputs/FontSettingInput'; |
||||
import { CodeSettingInput } from './inputs/CodeSettingInput'; |
||||
import { ActionSettingInput } from './inputs/ActionSettingInput'; |
||||
import { AssetSettingInput } from './inputs/AssetSettingInput'; |
||||
import { RoomPickSettingInput } from './inputs/RoomPickSettingInput'; |
||||
import { useSetting } from './SettingsState'; |
||||
|
||||
const getInputComponentByType = (type) => ({ |
||||
boolean: BooleanSettingInput, |
||||
string: StringSettingInput, |
||||
relativeUrl: RelativeUrlSettingInput, |
||||
password: PasswordSettingInput, |
||||
int: IntSettingInput, |
||||
select: SelectSettingInput, |
||||
language: LanguageSettingInput, |
||||
color: ColorSettingInput, |
||||
font: FontSettingInput, |
||||
code: CodeSettingInput, |
||||
action: ActionSettingInput, |
||||
asset: AssetSettingInput, |
||||
roomPick: RoomPickSettingInput, |
||||
})[type] || GenericSettingInput; |
||||
|
||||
const MemoizedSetting = React.memo(function MemoizedSetting({ |
||||
type, |
||||
hint, |
||||
callout, |
||||
...inputProps |
||||
}) { |
||||
const InputComponent = getInputComponentByType(type); |
||||
|
||||
return <Field> |
||||
<InputComponent {...inputProps} /> |
||||
{hint && <Field.Hint>{hint}</Field.Hint>} |
||||
{callout && <Callout type='warning' title={callout} />} |
||||
</Field>; |
||||
}); |
||||
|
||||
export function Setting({ settingId }) { |
||||
const { |
||||
value: contextValue, |
||||
editor: contextEditor, |
||||
...setting |
||||
} = useSetting(settingId); |
||||
|
||||
const t = useTranslation(); |
||||
|
||||
const [value, setValue] = useState(contextValue); |
||||
const setContextValue = useDebouncedCallback((value) => setting.update({ value }), 70, []); |
||||
|
||||
useEffect(() => { |
||||
setValue(contextValue); |
||||
}, [contextValue]); |
||||
|
||||
const [editor, setEditor] = useState(contextEditor); |
||||
const setContextEditor = useDebouncedCallback((editor) => setting.update({ editor }), 70, []); |
||||
|
||||
useEffect(() => { |
||||
setEditor(contextEditor); |
||||
}, [contextEditor]); |
||||
|
||||
const onChangeValue = (value) => { |
||||
setValue(value); |
||||
setContextValue(value); |
||||
}; |
||||
|
||||
const onChangeEditor = (editor) => { |
||||
setEditor(editor); |
||||
setContextEditor(editor); |
||||
}; |
||||
|
||||
const onResetButtonClick = () => { |
||||
setting.reset(); |
||||
}; |
||||
|
||||
const { |
||||
_id, |
||||
disableReset, |
||||
readonly, |
||||
type, |
||||
packageValue, |
||||
blocked, |
||||
i18nLabel, |
||||
i18nDescription, |
||||
alert, |
||||
} = setting; |
||||
|
||||
const label = (i18nLabel && t(i18nLabel)) || (_id || t(_id)); |
||||
const hint = useMemo(() => t.has(i18nDescription) && <MarkdownText>{t(i18nDescription)}</MarkdownText>, [i18nDescription]); |
||||
const callout = useMemo(() => alert && <RawText>{t(alert)}</RawText>, [alert]); |
||||
const hasResetButton = !disableReset && !readonly && type !== 'asset' && value !== packageValue && !blocked; |
||||
|
||||
return <MemoizedSetting |
||||
type={type} |
||||
label={label} |
||||
hint={hint} |
||||
callout={callout} |
||||
{...setting} |
||||
value={value} |
||||
editor={editor} |
||||
hasResetButton={hasResetButton} |
||||
onChangeValue={onChangeValue} |
||||
onChangeEditor={onChangeEditor} |
||||
onResetButtonClick={onResetButtonClick} |
||||
/>; |
||||
} |
||||
|
||||
Setting.Skeleton = function Skeleton() { |
||||
return <Field> |
||||
<Label> |
||||
<Text.Skeleton animated width='1/4' /> |
||||
</Label> |
||||
<InputBox.Skeleton animated /> |
||||
</Field>; |
||||
}; |
||||
@ -0,0 +1,27 @@ |
||||
import React from 'react'; |
||||
|
||||
import { useAtLeastOnePermission } from '../../../hooks/usePermissions'; |
||||
import { useAdminSideNav } from '../hooks'; |
||||
import { GroupSelector } from './GroupSelector'; |
||||
import { NotAuthorizedPage } from './NotAuthorizedPage'; |
||||
import { SettingsState } from './SettingsState'; |
||||
|
||||
export function SettingsRoute({ |
||||
group: groupId, |
||||
}) { |
||||
useAdminSideNav(); |
||||
|
||||
const hasPermission = useAtLeastOnePermission([ |
||||
'view-privileged-setting', |
||||
'edit-privileged-setting', |
||||
'manage-selected-settings', |
||||
]); |
||||
|
||||
if (!hasPermission) { |
||||
return <NotAuthorizedPage />; |
||||
} |
||||
|
||||
return <SettingsState> |
||||
<GroupSelector groupId={groupId} /> |
||||
</SettingsState>; |
||||
} |
||||
@ -0,0 +1,384 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { Mongo } from 'meteor/mongo'; |
||||
import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; |
||||
import React, { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState } from 'react'; |
||||
import toastr from 'toastr'; |
||||
|
||||
import { PrivateSettingsCachedCollection } from '../../../../app/ui-admin/client/SettingsCachedCollection'; |
||||
import { handleError } from '../../../../app/utils/client/lib/handleError'; |
||||
import { useBatchSetSettings } from '../../../hooks/useBatchSetSettings'; |
||||
import { useEventCallback } from '../../../hooks/useEventCallback'; |
||||
import { useReactiveValue } from '../../../hooks/useReactiveValue'; |
||||
|
||||
const SettingsContext = createContext({}); |
||||
|
||||
let privateSettingsCachedCollection; // Remove this singleton (╯°□°)╯︵ ┻━┻
|
||||
|
||||
const getPrivateSettingsCachedCollection = () => { |
||||
if (privateSettingsCachedCollection) { |
||||
return [privateSettingsCachedCollection, Promise.resolve()]; |
||||
} |
||||
|
||||
privateSettingsCachedCollection = new PrivateSettingsCachedCollection(); |
||||
|
||||
return [privateSettingsCachedCollection, privateSettingsCachedCollection.init()]; |
||||
}; |
||||
|
||||
const compareStrings = (a = '', b = '') => { |
||||
if (a === b || (!a && !b)) { |
||||
return 0; |
||||
} |
||||
|
||||
return a > b ? 1 : -1; |
||||
}; |
||||
|
||||
const compareSettings = (a, b) => |
||||
compareStrings(a.section, b.section) |
||||
|| compareStrings(a.sorter, b.sorter) |
||||
|| compareStrings(a.i18nLabel, b.i18nLabel); |
||||
|
||||
const settingsReducer = (states, { type, payload }) => { |
||||
const { |
||||
settings, |
||||
persistedSettings, |
||||
} = states; |
||||
|
||||
switch (type) { |
||||
case 'add': { |
||||
return { |
||||
settings: [...settings, ...payload].sort(compareSettings), |
||||
persistedSettings: [...persistedSettings, ...payload].sort(compareSettings), |
||||
}; |
||||
} |
||||
|
||||
case 'change': { |
||||
const mapping = (setting) => (setting._id !== payload._id ? setting : payload); |
||||
|
||||
return { |
||||
settings: settings.map(mapping), |
||||
persistedSettings: settings.map(mapping), |
||||
}; |
||||
} |
||||
|
||||
case 'remove': { |
||||
const mapping = (setting) => setting._id !== payload; |
||||
|
||||
return { |
||||
settings: settings.filter(mapping), |
||||
persistedSettings: persistedSettings.filter(mapping), |
||||
}; |
||||
} |
||||
|
||||
case 'hydrate': { |
||||
const map = {}; |
||||
payload.forEach((setting) => { |
||||
map[setting._id] = setting; |
||||
}); |
||||
|
||||
const mapping = (setting) => (map[setting._id] ? { ...setting, ...map[setting._id] } : setting); |
||||
|
||||
return { |
||||
settings: settings.map(mapping), |
||||
persistedSettings, |
||||
}; |
||||
} |
||||
} |
||||
|
||||
return states; |
||||
}; |
||||
|
||||
export function SettingsState({ children }) { |
||||
const [isLoading, setLoading] = useState(true); |
||||
|
||||
const [subscribers] = useState(new Set()); |
||||
|
||||
const stateRef = useRef({ settings: [], persistedSettings: [] }); |
||||
|
||||
const enhancedReducer = useCallback((state, action) => { |
||||
const newState = settingsReducer(state, action); |
||||
|
||||
stateRef.current = newState; |
||||
|
||||
subscribers.forEach((subscriber) => { |
||||
subscriber(newState); |
||||
}); |
||||
|
||||
return newState; |
||||
}, [settingsReducer, subscribers]); |
||||
|
||||
const [, dispatch] = useReducer(enhancedReducer, { settings: [], persistedSettings: [] }); |
||||
|
||||
const collectionsRef = useRef({}); |
||||
|
||||
useEffect(() => { |
||||
const [privateSettingsCachedCollection, loadingPromise] = getPrivateSettingsCachedCollection(); |
||||
|
||||
const stopLoading = () => { |
||||
setLoading(false); |
||||
}; |
||||
|
||||
loadingPromise.then(stopLoading, stopLoading); |
||||
|
||||
const { collection: persistedSettingsCollection } = privateSettingsCachedCollection; |
||||
const settingsCollection = new Mongo.Collection(null); |
||||
|
||||
collectionsRef.current = { |
||||
persistedSettingsCollection, |
||||
settingsCollection, |
||||
}; |
||||
}, [collectionsRef]); |
||||
|
||||
useEffect(() => { |
||||
if (isLoading) { |
||||
return; |
||||
} |
||||
|
||||
const { current: { persistedSettingsCollection, settingsCollection } } = collectionsRef; |
||||
|
||||
const query = persistedSettingsCollection.find(); |
||||
|
||||
const syncCollectionsHandle = query.observe({ |
||||
added: (data) => settingsCollection.insert(data), |
||||
changed: (data) => settingsCollection.update(data._id, data), |
||||
removed: ({ _id }) => settingsCollection.remove(_id), |
||||
}); |
||||
|
||||
const addedQueue = []; |
||||
let addedActionTimer; |
||||
|
||||
const syncStateHandle = query.observe({ |
||||
added: (data) => { |
||||
addedQueue.push(data); |
||||
clearTimeout(addedActionTimer); |
||||
addedActionTimer = setTimeout(() => { |
||||
dispatch({ type: 'add', payload: addedQueue }); |
||||
}, 70); |
||||
}, |
||||
changed: (data) => { |
||||
dispatch({ type: 'change', payload: data }); |
||||
}, |
||||
removed: ({ _id }) => { |
||||
dispatch({ type: 'remove', payload: _id }); |
||||
}, |
||||
}); |
||||
|
||||
return () => { |
||||
syncCollectionsHandle.stop(); |
||||
syncStateHandle.stop(); |
||||
clearTimeout(addedActionTimer); |
||||
}; |
||||
}, [isLoading, collectionsRef]); |
||||
|
||||
const updateTimersRef = useRef({}); |
||||
|
||||
const updateAtCollection = useCallback(({ _id, ...data }) => { |
||||
const { current: { settingsCollection } } = collectionsRef; |
||||
const { current: updateTimers } = updateTimersRef; |
||||
clearTimeout(updateTimers[_id]); |
||||
updateTimers[_id] = setTimeout(() => { |
||||
settingsCollection.update(_id, { $set: data }); |
||||
}, 70); |
||||
}, [collectionsRef, updateTimersRef]); |
||||
|
||||
const hydrate = useCallback((changes) => { |
||||
changes.forEach(updateAtCollection); |
||||
dispatch({ type: 'hydrate', payload: changes }); |
||||
}, [updateAtCollection, dispatch]); |
||||
|
||||
const isDisabled = useCallback(({ blocked, enableQuery }) => { |
||||
if (blocked) { |
||||
return true; |
||||
} |
||||
|
||||
if (!enableQuery) { |
||||
return false; |
||||
} |
||||
|
||||
const { current: { settingsCollection } } = collectionsRef; |
||||
|
||||
const queries = [].concat(typeof enableQuery === 'string' ? JSON.parse(enableQuery) : enableQuery); |
||||
return !queries.every((query) => !!settingsCollection.findOne(query)); |
||||
}, [collectionsRef]); |
||||
|
||||
const contextValue = useMemo(() => ({ |
||||
subscribers, |
||||
stateRef, |
||||
hydrate, |
||||
isDisabled, |
||||
}), [ |
||||
subscribers, |
||||
stateRef, |
||||
hydrate, |
||||
isDisabled, |
||||
]); |
||||
|
||||
return <SettingsContext.Provider children={children} value={contextValue} />; |
||||
} |
||||
|
||||
const useSelector = (selector, equalityFunction = (a, b) => a === b) => { |
||||
const { subscribers, stateRef } = useContext(SettingsContext); |
||||
const [value, setValue] = useState(() => selector(stateRef.current)); |
||||
|
||||
const handleUpdate = useEventCallback((selector, equalityFunction, value, state) => { |
||||
const newValue = selector(state); |
||||
|
||||
if (!equalityFunction(newValue, value)) { |
||||
setValue(newValue); |
||||
} |
||||
}, selector, equalityFunction, value); |
||||
|
||||
useEffect(() => { |
||||
subscribers.add(handleUpdate); |
||||
|
||||
return () => { |
||||
subscribers.delete(handleUpdate); |
||||
}; |
||||
}, [handleUpdate]); |
||||
|
||||
useLayoutEffect(() => { |
||||
handleUpdate(stateRef.current); |
||||
}); |
||||
|
||||
return value; |
||||
}; |
||||
|
||||
export const useGroup = (groupId) => { |
||||
const group = useSelector((state) => state.settings.find(({ _id, type }) => _id === groupId && type === 'group')); |
||||
|
||||
const filterSettings = (settings) => settings.filter(({ group }) => group === groupId); |
||||
|
||||
const changed = useSelector((state) => filterSettings(state.settings).some(({ changed }) => changed)); |
||||
const sections = useSelector((state) => Array.from(new Set(filterSettings(state.settings).map(({ section }) => section || ''))), (a, b) => a.length === b.length && a.join() === b.join()); |
||||
|
||||
const batchSetSettings = useBatchSetSettings(); |
||||
const { stateRef, hydrate } = useContext(SettingsContext); |
||||
|
||||
const save = useEventCallback(async (filterSettings, { current: state }, batchSetSettings) => { |
||||
const settings = filterSettings(state.settings); |
||||
|
||||
const changes = settings.filter(({ changed }) => changed) |
||||
.map(({ _id, value, editor }) => ({ _id, value, editor })); |
||||
|
||||
if (changes.length === 0) { |
||||
return; |
||||
} |
||||
|
||||
try { |
||||
await batchSetSettings(changes); |
||||
|
||||
if (changes.some(({ _id }) => _id === 'Language')) { |
||||
const lng = Meteor.user().language |
||||
|| changes.filter(({ _id }) => _id === 'Language').shift().value |
||||
|| 'en'; |
||||
|
||||
TAPi18n._loadLanguage(lng) |
||||
.then(() => toastr.success(TAPi18n.__('Settings_updated', { lng }))) |
||||
.catch(handleError); |
||||
|
||||
return; |
||||
} |
||||
|
||||
toastr.success(TAPi18n.__('Settings_updated')); |
||||
} catch (error) { |
||||
handleError(error); |
||||
} |
||||
}, filterSettings, stateRef, batchSetSettings); |
||||
|
||||
const cancel = useEventCallback((filterSettings, { current: state }, hydrate) => { |
||||
const settings = filterSettings(state.settings); |
||||
const persistedSettings = filterSettings(state.persistedSettings); |
||||
|
||||
const changes = settings.filter(({ changed }) => changed) |
||||
.map((field) => { |
||||
const { _id, value, editor } = persistedSettings.find(({ _id }) => _id === field._id); |
||||
return { _id, value, editor, changed: false }; |
||||
}); |
||||
|
||||
hydrate(changes); |
||||
}, filterSettings, stateRef, hydrate); |
||||
|
||||
return group && { ...group, sections, changed, save, cancel }; |
||||
}; |
||||
|
||||
export const useSection = (groupId, sectionName) => { |
||||
sectionName = sectionName || ''; |
||||
|
||||
const filterSettings = (settings) => |
||||
settings.filter(({ group, section }) => group === groupId && ((!sectionName && !section) || (sectionName === section))); |
||||
|
||||
const changed = useSelector((state) => filterSettings(state.settings).some(({ changed }) => changed)); |
||||
const canReset = useSelector((state) => filterSettings(state.settings).some(({ value, packageValue }) => value !== packageValue)); |
||||
const settingsIds = useSelector((state) => filterSettings(state.settings).map(({ _id }) => _id), (a, b) => a.length === b.length && a.join() === b.join()); |
||||
|
||||
const { stateRef, hydrate } = useContext(SettingsContext); |
||||
|
||||
const reset = useEventCallback((filterSettings, { current: state }, hydrate) => { |
||||
const settings = filterSettings(state.settings); |
||||
const persistedSettings = filterSettings(state.persistedSettings); |
||||
|
||||
const changes = settings.map((setting) => { |
||||
const { _id, value, packageValue, editor } = persistedSettings.find(({ _id }) => _id === setting._id); |
||||
return { |
||||
_id, |
||||
value: packageValue, |
||||
editor, |
||||
changed: packageValue !== value, |
||||
}; |
||||
}); |
||||
|
||||
hydrate(changes); |
||||
}, filterSettings, stateRef, hydrate); |
||||
|
||||
return { |
||||
name: sectionName, |
||||
changed, |
||||
canReset, |
||||
settings: settingsIds, |
||||
reset, |
||||
}; |
||||
}; |
||||
|
||||
export const useSetting = (_id) => { |
||||
const { stateRef, hydrate, isDisabled } = useContext(SettingsContext); |
||||
|
||||
const selectSetting = (settings) => settings.find((setting) => setting._id === _id); |
||||
|
||||
const setting = useSelector((state) => selectSetting(state.settings)); |
||||
const sectionChanged = useSelector((state) => state.settings.some(({ section, changed }) => section === setting.section && changed)); |
||||
const disabled = useReactiveValue(() => isDisabled(setting), [setting.blocked, setting.enableQuery]); |
||||
|
||||
const update = useEventCallback((selectSetting, { current: state }, hydrate, data) => { |
||||
const setting = { ...selectSetting(state.settings), ...data }; |
||||
const persistedSetting = selectSetting(state.persistedSettings); |
||||
|
||||
const changes = [{ |
||||
_id: setting._id, |
||||
value: setting.value, |
||||
editor: setting.editor, |
||||
changed: (setting.value !== persistedSetting.value) || (setting.editor !== persistedSetting.editor), |
||||
}]; |
||||
|
||||
hydrate(changes); |
||||
}, selectSetting, stateRef, hydrate); |
||||
|
||||
const reset = useEventCallback((selectSetting, { current: state }, hydrate) => { |
||||
const { _id, value, packageValue, editor } = selectSetting(state.persistedSettings); |
||||
|
||||
const changes = [{ |
||||
_id, |
||||
value: packageValue, |
||||
editor, |
||||
changed: packageValue !== value, |
||||
}]; |
||||
|
||||
hydrate(changes); |
||||
}, selectSetting, stateRef, hydrate); |
||||
|
||||
return { |
||||
...setting, |
||||
sectionChanged, |
||||
disabled, |
||||
update, |
||||
reset, |
||||
}; |
||||
}; |
||||
@ -0,0 +1,23 @@ |
||||
import { Button } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import { useTranslation } from '../../../providers/TranslationProvider'; |
||||
import { GroupPage } from '../GroupPage'; |
||||
import { Section } from '../Section'; |
||||
|
||||
export function AssetsGroupPage({ group }) { |
||||
const solo = group.sections.length === 1; |
||||
const t = useTranslation(); |
||||
|
||||
return <GroupPage group={group} headerButtons={<> |
||||
<Button className='refresh-clients'>{t('Apply_and_refresh_all_clients')}</Button> |
||||
</>}> |
||||
{group.sections.map((sectionName) => <Section |
||||
key={sectionName} |
||||
groupId={group._id} |
||||
hasReset={false} |
||||
sectionName={sectionName} |
||||
solo={solo} |
||||
/>)} |
||||
</GroupPage>; |
||||
} |
||||
@ -0,0 +1,17 @@ |
||||
import React from 'react'; |
||||
|
||||
import { GroupPage } from '../GroupPage'; |
||||
import { Section } from '../Section'; |
||||
|
||||
export function GenericGroupPage({ group }) { |
||||
const solo = group.sections.length === 1; |
||||
|
||||
return <GroupPage group={group}> |
||||
{group.sections.map((sectionName) => <Section |
||||
key={sectionName} |
||||
groupId={group._id} |
||||
sectionName={sectionName} |
||||
solo={solo} |
||||
/>)} |
||||
</GroupPage>; |
||||
} |
||||
@ -0,0 +1,40 @@ |
||||
import { Button } from '@rocket.chat/fuselage'; |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import React from 'react'; |
||||
import s from 'underscore.string'; |
||||
|
||||
import { RawText } from '../../../basic/RawText'; |
||||
import { useTranslation } from '../../../providers/TranslationProvider'; |
||||
import { GroupPage } from '../GroupPage'; |
||||
import { Section } from '../Section'; |
||||
|
||||
export function OAuthGroupPage({ group }) { |
||||
const solo = group.sections.length === 1; |
||||
const t = useTranslation(); |
||||
|
||||
const sectionIsCustomOAuth = (sectionName) => sectionName && /^Custom OAuth:\s.+/.test(sectionName); |
||||
|
||||
const callbackURL = (sectionName) => { |
||||
const id = s.strRight(sectionName, 'Custom OAuth: ').toLowerCase(); |
||||
return Meteor.absoluteUrl(`_oauth/${ id }`); |
||||
}; |
||||
|
||||
return <GroupPage group={group} headerButtons={<> |
||||
<Button className='refresh-oauth'>{t('Refresh_oauth_services')}</Button> |
||||
<Button className='add-custom-oauth'>{t('Add_custom_oauth')}</Button> |
||||
</>}> |
||||
{group.sections.map((sectionName) => (sectionIsCustomOAuth(sectionName) |
||||
? <Section |
||||
key={sectionName} |
||||
groupId={group._id} |
||||
help={<RawText>{t('Custom_oauth_helper', callbackURL(sectionName))}</RawText>} |
||||
sectionName={sectionName} |
||||
solo={solo} |
||||
> |
||||
<div className='submit'> |
||||
<Button cancel className='remove-custom-oauth'>{t('Remove_custom_oauth')}</Button> |
||||
</div> |
||||
</Section> |
||||
: <Section key={sectionName} groupId={group._id} sectionName={sectionName} solo={solo} />))} |
||||
</GroupPage>; |
||||
} |
||||
@ -0,0 +1,44 @@ |
||||
import { Button, Field } from '@rocket.chat/fuselage'; |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; |
||||
import React from 'react'; |
||||
import toastr from 'toastr'; |
||||
|
||||
import { useTranslation } from '../../../providers/TranslationProvider'; |
||||
import { handleError } from '../../../../../app/utils/client'; |
||||
|
||||
export function ActionSettingInput({ |
||||
_id, |
||||
actionText, |
||||
value, |
||||
disabled, |
||||
sectionChanged, |
||||
}) { |
||||
const t = useTranslation(); |
||||
|
||||
const handleClick = async () => { |
||||
Meteor.call(value, (err, data) => { |
||||
if (err) { |
||||
err.details = Object.assign(err.details || {}, { |
||||
errorTitle: 'Error', |
||||
}); |
||||
handleError(err); |
||||
return; |
||||
} |
||||
|
||||
const args = [data.message].concat(data.params); |
||||
toastr.success(TAPi18n.__.apply(TAPi18n, args), TAPi18n.__('Success')); |
||||
}); |
||||
}; |
||||
|
||||
return <> |
||||
<Button |
||||
data-qa-setting-id={_id} |
||||
children={t(actionText)} |
||||
disabled={disabled || sectionChanged} |
||||
primary |
||||
onClick={handleClick} |
||||
/> |
||||
{sectionChanged && <Field.Hint>{t('Save_to_enable_this_action')}</Field.Hint>} |
||||
</>; |
||||
} |
||||
@ -0,0 +1,72 @@ |
||||
import { Button, Icon, Label } from '@rocket.chat/fuselage'; |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { Random } from 'meteor/random'; |
||||
import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; |
||||
import React from 'react'; |
||||
import toastr from 'toastr'; |
||||
|
||||
import { handleError } from '../../../../../app/utils/client'; |
||||
import { useTranslation } from '../../../providers/TranslationProvider'; |
||||
|
||||
export function AssetSettingInput({ |
||||
_id, |
||||
label, |
||||
value, |
||||
asset, |
||||
fileConstraints, |
||||
}) { |
||||
const t = useTranslation(); |
||||
|
||||
const handleUpload = (event) => { |
||||
event = event.originalEvent || event; |
||||
|
||||
let { files } = event.target; |
||||
if (!files || files.length === 0) { |
||||
if (event.dataTransfer && event.dataTransfer.files) { |
||||
files = event.dataTransfer.files; |
||||
} else { |
||||
files = []; |
||||
} |
||||
} |
||||
|
||||
Object.values(files).forEach((blob) => { |
||||
toastr.info(TAPi18n.__('Uploading_file')); |
||||
const reader = new FileReader(); |
||||
reader.readAsBinaryString(blob); |
||||
reader.onloadend = () => Meteor.call('setAsset', reader.result, blob.type, asset, function(err) { |
||||
if (err != null) { |
||||
handleError(err); |
||||
console.log(err); |
||||
return; |
||||
} |
||||
return toastr.success(TAPi18n.__('File_uploaded')); |
||||
}); |
||||
}); |
||||
}; |
||||
|
||||
const handleDeleteButtonClick = () => { |
||||
Meteor.call('unsetAsset', asset); |
||||
}; |
||||
|
||||
return <> |
||||
<Label htmlFor={_id} text={label} title={_id} /> |
||||
<div className='settings-file-preview'> |
||||
{value.url |
||||
? <div className='preview' style={{ backgroundImage: `url(${ value.url }?_dc=${ Random.id() })` }} /> |
||||
: <div className='preview no-file background-transparent-light secondary-font-color'><Icon icon='icon-upload' /></div>} |
||||
<div className='action'> |
||||
{value.url |
||||
? <Button onClick={handleDeleteButtonClick}> |
||||
<Icon name='trash' />{t('Delete')} |
||||
</Button> |
||||
: <div className='rc-button rc-button--primary'>{t('Select_file')} |
||||
<input |
||||
type='file' |
||||
accept={fileConstraints.extensions && fileConstraints.extensions.length && `.${ fileConstraints.extensions.join(', .') }`} |
||||
onChange={handleUpload} |
||||
/> |
||||
</div>} |
||||
</div> |
||||
</div> |
||||
</>; |
||||
} |
||||
@ -0,0 +1,40 @@ |
||||
import { |
||||
Field, |
||||
Label, |
||||
ToggleSwitch, |
||||
} from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import { ResetSettingButton } from '../ResetSettingButton'; |
||||
|
||||
export function BooleanSettingInput({ |
||||
_id, |
||||
label, |
||||
disabled, |
||||
readonly, |
||||
autocomplete, |
||||
value, |
||||
hasResetButton, |
||||
onChangeValue, |
||||
onResetButtonClick, |
||||
}) { |
||||
const handleChange = (event) => { |
||||
const value = event.currentTarget.checked; |
||||
onChangeValue(value); |
||||
}; |
||||
|
||||
return <Field.Row> |
||||
<Label position='end' text={label} title={_id}> |
||||
<ToggleSwitch |
||||
data-qa-setting-id={_id} |
||||
value='true' |
||||
checked={value === true} |
||||
disabled={disabled} |
||||
readOnly={readonly} |
||||
autoComplete={autocomplete === false ? 'off' : undefined} |
||||
onChange={handleChange} |
||||
/> |
||||
</Label> |
||||
{hasResetButton && <ResetSettingButton data-qa-reset-setting-id={_id} onClick={onResetButtonClick} />} |
||||
</Field.Row>; |
||||
} |
||||
@ -0,0 +1,143 @@ |
||||
import { Button, Field, Label } from '@rocket.chat/fuselage'; |
||||
import { useToggle } from '@rocket.chat/fuselage-hooks'; |
||||
import React, { useEffect, useRef, useState } from 'react'; |
||||
|
||||
import { useTranslation } from '../../../providers/TranslationProvider'; |
||||
import { ResetSettingButton } from '../ResetSettingButton'; |
||||
|
||||
function CodeMirror({ |
||||
lineNumbers = true, |
||||
lineWrapping = true, |
||||
mode = 'javascript', |
||||
gutters = ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], |
||||
foldGutter = true, |
||||
matchBrackets = true, |
||||
autoCloseBrackets = true, |
||||
matchTags = true, |
||||
showTrailingSpace = true, |
||||
highlightSelectionMatches = true, |
||||
readOnly, |
||||
value: valueProp, |
||||
defaultValue, |
||||
onChange, |
||||
...props |
||||
}) { |
||||
const [editor, setEditor] = useState(); |
||||
const [value, setValue] = useState(valueProp || defaultValue); |
||||
const ref = useRef(); |
||||
|
||||
useEffect(() => { |
||||
let editor; |
||||
|
||||
const setupCodeMirror = async () => { |
||||
const CodeMirror = await import('codemirror/lib/codemirror.js'); |
||||
await import('../../../../../app/ui/client/lib/codeMirror/codeMirror'); |
||||
await import('codemirror/lib/codemirror.css'); |
||||
|
||||
const { current: textarea } = ref; |
||||
|
||||
if (!textarea) { |
||||
return; |
||||
} |
||||
|
||||
editor = CodeMirror.fromTextArea(textarea, { |
||||
lineNumbers, |
||||
lineWrapping, |
||||
mode, |
||||
gutters, |
||||
foldGutter, |
||||
matchBrackets, |
||||
autoCloseBrackets, |
||||
matchTags, |
||||
showTrailingSpace, |
||||
highlightSelectionMatches, |
||||
readOnly, |
||||
}); |
||||
|
||||
editor.on('change', (doc) => { |
||||
const value = doc.getValue(); |
||||
setValue(value); |
||||
onChange(value); |
||||
}); |
||||
|
||||
setEditor(editor); |
||||
}; |
||||
|
||||
setupCodeMirror(); |
||||
|
||||
return () => { |
||||
if (!editor) { |
||||
return; |
||||
} |
||||
|
||||
editor.toTextArea(); |
||||
}; |
||||
}, [ref]); |
||||
|
||||
useEffect(() => { |
||||
setValue(valueProp); |
||||
}, [valueProp]); |
||||
|
||||
useEffect(() => { |
||||
if (!editor) { |
||||
return; |
||||
} |
||||
|
||||
if (value !== editor.getValue()) { |
||||
editor.setValue(value); |
||||
} |
||||
}, [editor, ref, value]); |
||||
|
||||
return <textarea readOnly ref={ref} style={{ display: 'none' }} value={value} {...props}/>; |
||||
} |
||||
|
||||
export function CodeSettingInput({ |
||||
_id, |
||||
label, |
||||
value, |
||||
code, |
||||
placeholder, |
||||
readonly, |
||||
autocomplete, |
||||
disabled, |
||||
hasResetButton, |
||||
onChangeValue, |
||||
onResetButtonClick, |
||||
}) { |
||||
const t = useTranslation(); |
||||
|
||||
const [fullScreen, toggleFullScreen] = useToggle(false); |
||||
|
||||
const handleChange = (value) => { |
||||
onChangeValue(value); |
||||
}; |
||||
|
||||
return <> |
||||
<Field.Row> |
||||
<Label htmlFor={_id} text={label} title={_id} /> |
||||
{hasResetButton && <ResetSettingButton data-qa-reset-setting-id={_id} onClick={onResetButtonClick} />} |
||||
</Field.Row> |
||||
<div |
||||
className={[ |
||||
'code-mirror-box', |
||||
fullScreen && 'code-mirror-box-fullscreen content-background-color', |
||||
].filter(Boolean).join(' ')} |
||||
> |
||||
<div className='title'>{label}</div> |
||||
<CodeMirror |
||||
data-qa-setting-id={_id} |
||||
id={_id} |
||||
mode={code} |
||||
value={value} |
||||
placeholder={placeholder} |
||||
disabled={disabled} |
||||
readOnly={readonly} |
||||
autoComplete={autocomplete === false ? 'off' : undefined} |
||||
onChange={handleChange} |
||||
/> |
||||
<div className='buttons'> |
||||
<Button primary onClick={() => toggleFullScreen()}>{fullScreen ? t('Exit_Full_Screen') : t('Full_Screen')}</Button> |
||||
</div> |
||||
</div> |
||||
</>; |
||||
} |
||||
@ -0,0 +1,112 @@ |
||||
import { |
||||
Field, |
||||
InputBox, |
||||
Label, |
||||
SelectInput, |
||||
TextInput, |
||||
} from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import { useTranslation } from '../../../providers/TranslationProvider'; |
||||
import { ResetSettingButton } from '../ResetSettingButton'; |
||||
|
||||
export function ColorSettingInput({ |
||||
_id, |
||||
label, |
||||
value, |
||||
editor, |
||||
allowedTypes, |
||||
placeholder, |
||||
readonly, |
||||
autocomplete, |
||||
disabled, |
||||
hasResetButton, |
||||
onChangeValue, |
||||
onChangeEditor, |
||||
onResetButtonClick, |
||||
}) { |
||||
const t = useTranslation(); |
||||
|
||||
const handleChange = (event) => { |
||||
onChangeValue(event.currentTarget.value); |
||||
}; |
||||
|
||||
const handleEditorTypeChange = (event) => { |
||||
const editor = event.currentTarget.value.trim(); |
||||
onChangeEditor(editor); |
||||
}; |
||||
|
||||
return <> |
||||
<div |
||||
style={{ |
||||
display: 'flex', |
||||
flexFlow: 'row nowrap', |
||||
margin: '0 -0.5rem', |
||||
}} |
||||
> |
||||
<Field |
||||
style={{ |
||||
flex: '2 2 0', |
||||
margin: '0 0.5rem', |
||||
}} |
||||
> |
||||
<Label htmlFor={_id} text={label} title={_id} /> |
||||
{editor === 'color' && <InputBox |
||||
data-qa-setting-id={_id} |
||||
type='color' |
||||
id={_id} |
||||
value={value} |
||||
placeholder={placeholder} |
||||
disabled={disabled} |
||||
readOnly={readonly} |
||||
autoComplete={autocomplete === false ? 'off' : undefined} |
||||
onChange={handleChange} |
||||
style={{ |
||||
width: '100%', |
||||
}} |
||||
/>} |
||||
{editor === 'expression' && <TextInput |
||||
data-qa-setting-id={_id} |
||||
id={_id} |
||||
value={value} |
||||
placeholder={placeholder} |
||||
disabled={disabled} |
||||
readOnly={readonly} |
||||
autoComplete={autocomplete === false ? 'off' : undefined} |
||||
onChange={handleChange} |
||||
style={{ |
||||
width: '100%', |
||||
}} |
||||
/>} |
||||
</Field> |
||||
<Field |
||||
style={{ |
||||
flex: '1 1 0', |
||||
margin: '0 0.5rem', |
||||
}} |
||||
> |
||||
<Field.Row> |
||||
<Label htmlFor={`${ _id }_editor`} text={t('Type')} title={_id} /> |
||||
{hasResetButton && <ResetSettingButton data-qa-reset-setting-id={_id} onClick={onResetButtonClick} />} |
||||
</Field.Row> |
||||
<SelectInput |
||||
data-qa-setting-id={`${ _id }_editor`} |
||||
type='color' |
||||
id={`${ _id }_editor`} |
||||
value={editor} |
||||
disabled={disabled} |
||||
readOnly={readonly} |
||||
autoComplete={autocomplete === false ? 'off' : undefined} |
||||
onChange={handleEditorTypeChange} |
||||
> |
||||
{allowedTypes && allowedTypes.map((allowedType) => |
||||
<SelectInput.Option key={allowedType} value={allowedType}>{t(allowedType)}</SelectInput.Option> |
||||
)} |
||||
</SelectInput> |
||||
</Field> |
||||
</div> |
||||
<Field.Hint> |
||||
Variable name: {_id.replace(/theme-color-/, '@')} |
||||
</Field.Hint> |
||||
</>; |
||||
} |
||||
@ -0,0 +1,42 @@ |
||||
import { |
||||
Field, |
||||
Label, |
||||
TextInput, |
||||
} from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import { ResetSettingButton } from '../ResetSettingButton'; |
||||
|
||||
export function FontSettingInput({ |
||||
_id, |
||||
label, |
||||
value, |
||||
placeholder, |
||||
readonly, |
||||
autocomplete, |
||||
disabled, |
||||
hasResetButton, |
||||
onChangeValue, |
||||
onResetButtonClick, |
||||
}) { |
||||
const handleChange = (event) => { |
||||
onChangeValue(event.currentTarget.value); |
||||
}; |
||||
|
||||
return <> |
||||
<Field.Row> |
||||
<Label htmlFor={_id} text={label} title={_id} /> |
||||
{hasResetButton && <ResetSettingButton data-qa-reset-setting-id={_id} onClick={onResetButtonClick} />} |
||||
</Field.Row> |
||||
<TextInput |
||||
data-qa-setting-id={_id} |
||||
id={_id} |
||||
value={value} |
||||
placeholder={placeholder} |
||||
disabled={disabled} |
||||
readOnly={readonly} |
||||
autoComplete={autocomplete === false ? 'off' : undefined} |
||||
onChange={handleChange} |
||||
/> |
||||
</>; |
||||
} |
||||
@ -0,0 +1,42 @@ |
||||
import { |
||||
Field, |
||||
Label, |
||||
TextInput, |
||||
} from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import { ResetSettingButton } from '../ResetSettingButton'; |
||||
|
||||
export function GenericSettingInput({ |
||||
_id, |
||||
label, |
||||
value, |
||||
placeholder, |
||||
readonly, |
||||
autocomplete, |
||||
disabled, |
||||
hasResetButton, |
||||
onChangeValue, |
||||
onResetButtonClick, |
||||
}) { |
||||
const handleChange = (event) => { |
||||
onChangeValue(event.currentTarget.value); |
||||
}; |
||||
|
||||
return <> |
||||
<Field.Row> |
||||
<Label htmlFor={_id} text={label} title={_id} /> |
||||
{hasResetButton && <ResetSettingButton data-qa-reset-setting-id={_id} onClick={onResetButtonClick} />} |
||||
</Field.Row> |
||||
<TextInput |
||||
data-qa-setting-id={_id} |
||||
id={_id} |
||||
value={value} |
||||
placeholder={placeholder} |
||||
disabled={disabled} |
||||
readOnly={readonly} |
||||
autoComplete={autocomplete === false ? 'off' : undefined} |
||||
onChange={handleChange} |
||||
/> |
||||
</>; |
||||
} |
||||
@ -0,0 +1,43 @@ |
||||
import { |
||||
Field, |
||||
Label, |
||||
InputBox, |
||||
} from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import { ResetSettingButton } from '../ResetSettingButton'; |
||||
|
||||
export function IntSettingInput({ |
||||
_id, |
||||
label, |
||||
value, |
||||
placeholder, |
||||
readonly, |
||||
autocomplete, |
||||
disabled, |
||||
onChangeValue, |
||||
hasResetButton, |
||||
onResetButtonClick, |
||||
}) { |
||||
const handleChange = (event) => { |
||||
onChangeValue(parseInt(event.currentTarget.value, 10)); |
||||
}; |
||||
|
||||
return <> |
||||
<Field.Row> |
||||
<Label htmlFor={_id} text={label} title={_id} /> |
||||
{hasResetButton && <ResetSettingButton data-qa-reset-setting-id={_id} onClick={onResetButtonClick} />} |
||||
</Field.Row> |
||||
<InputBox |
||||
data-qa-setting-id={_id} |
||||
id={_id} |
||||
type='number' |
||||
value={value} |
||||
placeholder={placeholder} |
||||
disabled={disabled} |
||||
readOnly={readonly} |
||||
autoComplete={autocomplete === false ? 'off' : undefined} |
||||
onChange={handleChange} |
||||
/> |
||||
</>; |
||||
} |
||||
@ -0,0 +1,49 @@ |
||||
import { |
||||
Field, |
||||
Label, |
||||
SelectInput, |
||||
} from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import { useLanguages } from '../../../providers/TranslationProvider'; |
||||
import { ResetSettingButton } from '../ResetSettingButton'; |
||||
|
||||
export function LanguageSettingInput({ |
||||
_id, |
||||
label, |
||||
value, |
||||
placeholder, |
||||
readonly, |
||||
autocomplete, |
||||
disabled, |
||||
hasResetButton, |
||||
onChangeValue, |
||||
onResetButtonClick, |
||||
}) { |
||||
const languages = useLanguages(); |
||||
|
||||
const handleChange = (event) => { |
||||
onChangeValue(event.currentTarget.value); |
||||
}; |
||||
|
||||
return <> |
||||
<Field.Row> |
||||
<Label htmlFor={_id} text={label} title={_id} /> |
||||
{hasResetButton && <ResetSettingButton data-qa-reset-setting-id={_id} onClick={onResetButtonClick} />} |
||||
</Field.Row> |
||||
<SelectInput |
||||
data-qa-setting-id={_id} |
||||
id={_id} |
||||
value={value} |
||||
placeholder={placeholder} |
||||
disabled={disabled} |
||||
readOnly={readonly} |
||||
autoComplete={autocomplete === false ? 'off' : undefined} |
||||
onChange={handleChange} |
||||
> |
||||
{languages.map(({ key, name }) => |
||||
<SelectInput.Option key={key} value={key} dir='auto'>{name}</SelectInput.Option> |
||||
)} |
||||
</SelectInput> |
||||
</>; |
||||
} |
||||
@ -0,0 +1,42 @@ |
||||
import { |
||||
Field, |
||||
Label, |
||||
PasswordInput, |
||||
} from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import { ResetSettingButton } from '../ResetSettingButton'; |
||||
|
||||
export function PasswordSettingInput({ |
||||
_id, |
||||
label, |
||||
value, |
||||
placeholder, |
||||
readonly, |
||||
autocomplete, |
||||
disabled, |
||||
hasResetButton, |
||||
onChangeValue, |
||||
onResetButtonClick, |
||||
}) { |
||||
const handleChange = (event) => { |
||||
onChangeValue(event.currentTarget.value); |
||||
}; |
||||
|
||||
return <> |
||||
<Field.Row> |
||||
<Label htmlFor={_id} text={label} title={_id} /> |
||||
{hasResetButton && <ResetSettingButton data-qa-reset-setting-id={_id} onClick={onResetButtonClick} />} |
||||
</Field.Row> |
||||
<PasswordInput |
||||
data-qa-setting-id={_id} |
||||
id={_id} |
||||
value={value} |
||||
placeholder={placeholder} |
||||
disabled={disabled} |
||||
readOnly={readonly} |
||||
autoComplete={autocomplete === false ? 'off' : undefined} |
||||
onChange={handleChange} |
||||
/> |
||||
</>; |
||||
} |
||||
@ -0,0 +1,43 @@ |
||||
import { |
||||
Field, |
||||
Label, |
||||
UrlInput, |
||||
} from '@rocket.chat/fuselage'; |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import React from 'react'; |
||||
|
||||
import { ResetSettingButton } from '../ResetSettingButton'; |
||||
|
||||
export function RelativeUrlSettingInput({ |
||||
_id, |
||||
label, |
||||
value, |
||||
placeholder, |
||||
readonly, |
||||
autocomplete, |
||||
disabled, |
||||
hasResetButton, |
||||
onChangeValue, |
||||
onResetButtonClick, |
||||
}) { |
||||
const handleChange = (event) => { |
||||
onChangeValue(event.currentTarget.value); |
||||
}; |
||||
|
||||
return <> |
||||
<Field.Row> |
||||
<Label htmlFor={_id} text={label} title={_id} /> |
||||
{hasResetButton && <ResetSettingButton data-qa-reset-setting-id={_id} onClick={onResetButtonClick} />} |
||||
</Field.Row> |
||||
<UrlInput |
||||
data-qa-setting-id={_id} |
||||
id={_id} |
||||
value={Meteor.absoluteUrl(value)} |
||||
placeholder={placeholder} |
||||
disabled={disabled} |
||||
readOnly={readonly} |
||||
autoComplete={autocomplete === false ? 'off' : undefined} |
||||
onChange={handleChange} |
||||
/> |
||||
</>; |
||||
} |
||||
@ -0,0 +1,88 @@ |
||||
import { Field, Icon, Label } from '@rocket.chat/fuselage'; |
||||
import { Blaze } from 'meteor/blaze'; |
||||
import { Template } from 'meteor/templating'; |
||||
import React, { useRef, useEffect, useLayoutEffect } from 'react'; |
||||
|
||||
import { ResetSettingButton } from '../ResetSettingButton'; |
||||
|
||||
export function RoomPickSettingInput({ |
||||
_id, |
||||
label, |
||||
value, |
||||
placeholder, |
||||
readonly, |
||||
autocomplete, |
||||
disabled, |
||||
hasResetButton, |
||||
onChangeValue, |
||||
onResetButtonClick, |
||||
}) { |
||||
value = value || []; |
||||
|
||||
const wrapperRef = useRef(); |
||||
const valueRef = useRef(value); |
||||
|
||||
const handleRemoveRoomButtonClick = (rid) => () => { |
||||
onChangeValue(value.filter(({ _id }) => _id !== rid)); |
||||
}; |
||||
|
||||
useLayoutEffect(() => { |
||||
valueRef.current = value; |
||||
}); |
||||
|
||||
useEffect(() => { |
||||
const view = Blaze.renderWithData(Template.inputAutocomplete, { |
||||
id: _id, |
||||
name: _id, |
||||
class: 'search autocomplete rc-input__element', |
||||
autocomplete: autocomplete === false ? 'off' : undefined, |
||||
readOnly: readonly, |
||||
placeholder, |
||||
disabled, |
||||
settings: { |
||||
limit: 10, |
||||
// inputDelay: 300
|
||||
rules: [ |
||||
{ |
||||
// @TODO maybe change this 'collection' and/or template
|
||||
collection: 'CachedChannelList', |
||||
subscription: 'channelAndPrivateAutocomplete', |
||||
field: 'name', |
||||
template: Template.roomSearch, |
||||
noMatchTemplate: Template.roomSearchEmpty, |
||||
matchAll: true, |
||||
selector: (match) => ({ name: match }), |
||||
sort: 'name', |
||||
}, |
||||
], |
||||
}, |
||||
|
||||
}, wrapperRef.current); |
||||
|
||||
$('.autocomplete', wrapperRef.current).on('autocompleteselect', (event, doc) => { |
||||
const { current: value } = valueRef; |
||||
onChangeValue([...value.filter(({ _id }) => _id !== doc._id), doc]); |
||||
event.currentTarget.value = ''; |
||||
event.currentTarget.focus(); |
||||
}); |
||||
|
||||
return () => { |
||||
Blaze.remove(view); |
||||
}; |
||||
}, [valueRef]); |
||||
|
||||
return <> |
||||
<Field.Row> |
||||
<Label htmlFor={_id} text={label} title={_id} /> |
||||
{hasResetButton && <ResetSettingButton data-qa-reset-setting-id={_id} onClick={onResetButtonClick} />} |
||||
</Field.Row> |
||||
<div style={{ position: 'relative' }} ref={wrapperRef} /> |
||||
<ul className='selected-rooms'> |
||||
{value.map(({ _id, name }) => |
||||
<li key={_id} className='remove-room' onClick={handleRemoveRoomButtonClick(_id)}> |
||||
{name} <Icon name='cross' /> |
||||
</li> |
||||
)} |
||||
</ul> |
||||
</>; |
||||
} |
||||
@ -0,0 +1,50 @@ |
||||
import { |
||||
Field, |
||||
Label, |
||||
SelectInput, |
||||
} from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import { useTranslation } from '../../../providers/TranslationProvider'; |
||||
import { ResetSettingButton } from '../ResetSettingButton'; |
||||
|
||||
export function SelectSettingInput({ |
||||
_id, |
||||
label, |
||||
value, |
||||
placeholder, |
||||
readonly, |
||||
autocomplete, |
||||
disabled, |
||||
values, |
||||
hasResetButton, |
||||
onChangeValue, |
||||
onResetButtonClick, |
||||
}) { |
||||
const t = useTranslation(); |
||||
|
||||
const handleChange = (event) => { |
||||
onChangeValue(event.currentTarget.value); |
||||
}; |
||||
|
||||
return <> |
||||
<Field.Row> |
||||
<Label htmlFor={_id} text={label} title={_id} /> |
||||
{hasResetButton && <ResetSettingButton data-qa-reset-setting-id={_id} onClick={onResetButtonClick} />} |
||||
</Field.Row> |
||||
<SelectInput |
||||
data-qa-setting-id={_id} |
||||
id={_id} |
||||
value={value} |
||||
placeholder={placeholder} |
||||
disabled={disabled} |
||||
readOnly={readonly} |
||||
autoComplete={autocomplete === false ? 'off' : undefined} |
||||
onChange={handleChange} |
||||
> |
||||
{values.map(({ key, i18nLabel }) => |
||||
<SelectInput.Option key={key} value={key}>{t(i18nLabel)}</SelectInput.Option> |
||||
)} |
||||
</SelectInput> |
||||
</>; |
||||
} |
||||
@ -0,0 +1,56 @@ |
||||
import { |
||||
Field, |
||||
Label, |
||||
TextAreaInput, |
||||
TextInput, |
||||
} from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import { ResetSettingButton } from '../ResetSettingButton'; |
||||
|
||||
export function StringSettingInput({ |
||||
_id, |
||||
label, |
||||
disabled, |
||||
multiline, |
||||
placeholder, |
||||
readonly, |
||||
autocomplete, |
||||
value, |
||||
hasResetButton, |
||||
onChangeValue, |
||||
onResetButtonClick, |
||||
}) { |
||||
const handleChange = (event) => { |
||||
onChangeValue(event.currentTarget.value); |
||||
}; |
||||
|
||||
return <> |
||||
<Field.Row> |
||||
<Label htmlFor={_id} text={label} title={_id} /> |
||||
{hasResetButton && <ResetSettingButton data-qa-reset-setting-id={_id} onClick={onResetButtonClick} />} |
||||
</Field.Row> |
||||
{multiline |
||||
? <TextAreaInput |
||||
data-qa-setting-id={_id} |
||||
id={_id} |
||||
rows={4} |
||||
value={value} |
||||
placeholder={placeholder} |
||||
disabled={disabled} |
||||
readOnly={readonly} |
||||
autoComplete={autocomplete === false ? 'off' : undefined} |
||||
onChange={handleChange} |
||||
/> |
||||
: <TextInput |
||||
data-qa-setting-id={_id} |
||||
id={_id} |
||||
value={value} |
||||
placeholder={placeholder} |
||||
disabled={disabled} |
||||
readOnly={readonly} |
||||
autoComplete={autocomplete === false ? 'off' : undefined} |
||||
onChange={handleChange} |
||||
/> } |
||||
</>; |
||||
} |
||||
@ -1,20 +1,33 @@ |
||||
import { action } from '@storybook/addon-actions'; |
||||
import { boolean, text } from '@storybook/addon-knobs'; |
||||
import React from 'react'; |
||||
|
||||
import { rocketChatWrapper } from '../../../.storybook/helpers'; |
||||
import { Button } from './Button'; |
||||
|
||||
export default { |
||||
title: 'basic/Button', |
||||
component: Button, |
||||
decorators: [ |
||||
rocketChatWrapper, |
||||
], |
||||
}; |
||||
|
||||
export const _default = () => <Button>Button</Button>; |
||||
export const _default = () => <Button |
||||
children={text('children', 'Button')} |
||||
invisible={boolean('invisible')} |
||||
primary={boolean('primary')} |
||||
secondary={boolean('secondary')} |
||||
cancel={boolean('cancel')} |
||||
nude={boolean('nude')} |
||||
submit={boolean('submit')} |
||||
onClick={action('click')} |
||||
/>; |
||||
|
||||
export const invisible = () => <Button invisible>Button</Button>; |
||||
|
||||
export const primary = () => <Button primary>Button</Button>; |
||||
|
||||
export const secondary = () => <Button secondary>Button</Button>; |
||||
|
||||
export const cancel = () => <Button cancel>Button</Button>; |
||||
|
||||
export const nude = () => <Button nude>Button</Button>; |
||||
|
||||
export const submit = () => <Button submit>Button</Button>; |
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue