diff --git a/.storybook/.babelrc b/.storybook/.babelrc index 6e8b075e8c4..3f856fec45f 100644 --- a/.storybook/.babelrc +++ b/.storybook/.babelrc @@ -5,9 +5,14 @@ { "shippedProposals": true, "useBuiltIns": "usage", - "corejs": "3" + "corejs": "3", + "modules": "commonjs", } ], - "@babel/preset-react" + "@babel/preset-react", + "@babel/preset-flow" + ], + "plugins": [ + "@babel/plugin-proposal-class-properties" ] -} \ No newline at end of file +} diff --git a/.storybook/addons.js b/.storybook/addons.js index 6aed412d04a..9d64a3d0a8f 100644 --- a/.storybook/addons.js +++ b/.storybook/addons.js @@ -1,2 +1,4 @@ import '@storybook/addon-actions/register'; +import '@storybook/addon-knobs/register'; import '@storybook/addon-links/register'; +import '@storybook/addon-viewport/register'; diff --git a/.storybook/config.js b/.storybook/config.js index 1028ff1f13e..9ff5d797947 100644 --- a/.storybook/config.js +++ b/.storybook/config.js @@ -1,16 +1,27 @@ import { action } from '@storybook/addon-actions'; -import { addDecorator, configure } from '@storybook/react'; +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'; -addDecorator(function RocketChatDecorator(fn) { - require('@rocket.chat/icons/dist/font/RocketChat.minimal.css'); - require('../app/theme/client/main.css'); +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('@rocket.chat/icons/dist/font/RocketChat.minimal.css'); + require('../app/theme/client/main.css'); linkElement.setAttribute('id', 'theme-styles'); linkElement.setAttribute('rel', 'stylesheet'); linkElement.setAttribute('href', 'https://open.rocket.chat/theme.css'); @@ -25,7 +36,7 @@ addDecorator(function RocketChatDecorator(fn) { } `}
-
+
{fn()}
@@ -34,4 +45,6 @@ addDecorator(function RocketChatDecorator(fn) { ; }); +addDecorator(withKnobs); + configure(require.context('../client', true, /\.stories\.js$/), module); diff --git a/.storybook/empty.js b/.storybook/empty.js new file mode 100644 index 00000000000..ff8b4c56321 --- /dev/null +++ b/.storybook/empty.js @@ -0,0 +1 @@ +export default {}; diff --git a/.storybook/meteor.js b/.storybook/meteor.js new file mode 100644 index 00000000000..275568f7747 --- /dev/null +++ b/.storybook/meteor.js @@ -0,0 +1,68 @@ +export const Meteor = { + isClient: true, + isServer: false, + _localStorage: window.localStorage, + userId: () => {}, + Streamer: () => {}, + startup: () => {}, + methods: () => {}, + call: () => {}, +}; + +export const Tracker = { + autorun: () => {}, + 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: () => {}, +}; diff --git a/.storybook/webpack.config.js b/.storybook/webpack.config.js index b9ededccd61..b26a6a8600a 100644 --- a/.storybook/webpack.config.js +++ b/.storybook/webpack.config.js @@ -1,8 +1,10 @@ 'use strict'; -module.exports = async ({ config, mode }) => { +const webpack = require('webpack'); + +module.exports = async ({ config }) => { const cssRule = config.module.rules.find(({ test }) => test.test('index.css')); - cssRule.use[1].options.url = (url, resourcePath) => { + cssRule.use[1].options.url = (url) => { if (/^(\.\/)?images\//.test(url)) { return false; } @@ -18,5 +20,28 @@ module.exports = async ({ config, mode }) => { require('autoprefixer')(), ]; - return config; + config.module.rules.push({ + test: /\.info$/, + type: 'json', + }); + + config.module.rules.push({ + test: /\.html$/, + use: '@settlin/spacebars-loader', + }); + + config.plugins.push(new webpack.NormalModuleReplacementPlugin( + /^meteor/, + require.resolve('./meteor.js'), + )); + + config.plugins.push(new webpack.NormalModuleReplacementPlugin( + /\.\/server\/index.js/, + require.resolve('./empty.js'), + )); + + config.mode = 'development'; + config.optimization.usedExports = true; + + return config; }; diff --git a/client/components/admin/hooks.js b/client/components/admin/hooks.js new file mode 100644 index 00000000000..da2ac3b93ef --- /dev/null +++ b/client/components/admin/hooks.js @@ -0,0 +1,10 @@ +import { useEffect } from 'react'; + +import { SideNav } from '../../../app/ui-utils/client'; + +export const useAdminSideNav = () => { + useEffect(() => { + SideNav.setFlex('adminFlex'); + SideNav.openFlex(); + }, []); +}; diff --git a/client/components/admin/info/InformationPage.js b/client/components/admin/info/InformationPage.js index 752eb28d253..0cb9ded71be 100644 --- a/client/components/admin/info/InformationPage.js +++ b/client/components/admin/info/InformationPage.js @@ -1,15 +1,14 @@ import { Button, Icon } from '@rocket.chat/fuselage'; import React, { useEffect, useState } from 'react'; -import { call } from '../../../../app/ui-utils/client/lib/callMethod'; +import { useMethod } from '../../../hooks/useMethod'; import { useViewStatisticsPermission } from '../../../hooks/usePermissions'; -import { useReactiveValue } from '../../../hooks/useReactiveValue'; -import { Info } from '../../../../app/utils'; -import { SideNav } from '../../../../app/ui-utils/client/lib/SideNav'; +import { useRocketChatInformation } from '../../../hooks/useRocketChatInformation'; +import { useTranslation } from '../../../hooks/useTranslation'; import { Header } from '../../header/Header'; import { Link } from '../../basic/Link'; import { ErrorAlert } from '../../basic/ErrorAlert'; -import { useTranslation } from '../../../hooks/useTranslation'; +import { useAdminSideNav } from '../hooks'; import { RocketChatSection } from './RocketChatSection'; import { CommitSection } from './CommitSection'; import { RuntimeEnvironmentSection } from './RuntimeEnvironmentSection'; @@ -17,11 +16,17 @@ import { BuildEnvironmentSection } from './BuildEnvironmentSection'; import { UsageSection } from './UsageSection'; import { InstancesSection } from './InstancesSection'; -const useStatistics = (canViewStatistics) => { +export const useInformationPage = () => { + 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; @@ -37,8 +42,8 @@ const useStatistics = (canViewStatistics) => { try { const [statistics, instances] = await Promise.all([ - call('getStatistics'), - call('instances/get'), + getStatistics(), + getInstances(), ]); if (didCancel) { @@ -61,40 +66,39 @@ const useStatistics = (canViewStatistics) => { }; }, [canViewStatistics]); + const info = useRocketChatInformation(); + + const handleClickRefreshButton = () => { + if (isLoading) { + return; + } + + fetchStatistics(); + }; + return { + canViewStatistics, isLoading, + info, statistics, instances, - fetchStatistics, + onClickRefreshButton: handleClickRefreshButton, }; }; -export function InformationPage() { - const canViewStatistics = useViewStatisticsPermission(); - - const { - isLoading, - statistics, - instances, - fetchStatistics, - } = useStatistics(canViewStatistics); - - const info = useReactiveValue(() => Info, []); - +export function InformationPage({ + canViewStatistics, + isLoading, + info, + statistics, + instances, + onClickRefreshButton, +}) { const t = useTranslation(); - const handleRefreshClick = () => { - if (isLoading) { - return; - } - - fetchStatistics(); - }; - - useEffect(() => { - SideNav.setFlex('adminFlex'); - SideNav.openFlex(); - }, []); + if (!info) { + return null; + } const alertOplogForMultipleInstances = statistics && statistics.instanceCount > 1 && !statistics.oplogEnabled; @@ -102,7 +106,7 @@ export function InformationPage() {
{canViewStatistics &&
-
} @@ -121,11 +125,11 @@ export function InformationPage() {

} - + {canViewStatistics && } - + {canViewStatistics && } - + {canViewStatistics && }
; diff --git a/client/components/admin/info/InformationPage.stories.js b/client/components/admin/info/InformationPage.stories.js new file mode 100644 index 00000000000..6fd3bc3bddf --- /dev/null +++ b/client/components/admin/info/InformationPage.stories.js @@ -0,0 +1,148 @@ +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, +}; + +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 = () => + ; + +export const withoutCanViewStatisticsPermission = () => + ; + +export const loading = () => + ; + +export const withStatistics = () => + ; + +export const withOneInstance = () => + ; diff --git a/client/components/admin/info/RocketChatSection.js b/client/components/admin/info/RocketChatSection.js index 9b5baead612..40f9a9a1c20 100644 --- a/client/components/admin/info/RocketChatSection.js +++ b/client/components/admin/info/RocketChatSection.js @@ -10,11 +10,7 @@ export function RocketChatSection({ info, statistics, isLoading }) { const s = (fn) => (isLoading ? : fn()); const t = useTranslation(); - const appsEngineVersion = info.marketplaceApiVersion; - - if (!statistics) { - return null; - } + const appsEngineVersion = info && info.marketplaceApiVersion; return <>

{t('Rocket.Chat')}

diff --git a/client/components/admin/info/RuntimeEnvironmentSection.js b/client/components/admin/info/RuntimeEnvironmentSection.js index 0fe3eb0c08a..468c379e2b9 100644 --- a/client/components/admin/info/RuntimeEnvironmentSection.js +++ b/client/components/admin/info/RuntimeEnvironmentSection.js @@ -10,10 +10,6 @@ export function RuntimeEnvironmentSection({ statistics, isLoading }) { const s = (fn) => (isLoading ? : fn()); const t = useTranslation(); - if (!statistics) { - return null; - } - return <>

{t('Runtime_Environment')}

diff --git a/client/components/admin/info/UsageSection.js b/client/components/admin/info/UsageSection.js index 106d12646d7..0b2756056d6 100644 --- a/client/components/admin/info/UsageSection.js +++ b/client/components/admin/info/UsageSection.js @@ -10,10 +10,6 @@ export function UsageSection({ statistics, isLoading }) { const s = (fn) => (isLoading ? : fn()); const t = useTranslation(); - if (!statistics) { - return null; - } - return <>

{t('Usage')}

@@ -38,7 +34,7 @@ export function UsageSection({ statistics, isLoading }) { {s(() => statistics.totalLivechatMessages)} {s(() => statistics.uploadsTotal)} {s(() => formatMemorySize(statistics.uploadsTotalSize))} - {statistics.apps && <> + {statistics && statistics.apps && <> {statistics.apps.totalInstalled} {statistics.apps.totalActive} } diff --git a/client/components/header/BurgerMenuButton.js b/client/components/header/BurgerMenuButton.js index 42ade4c25f5..0cff17f9c64 100644 --- a/client/components/header/BurgerMenuButton.js +++ b/client/components/header/BurgerMenuButton.js @@ -1,83 +1,27 @@ -import React, { useMemo } from 'react'; - -import { ChatSubscription } from '../../../app/models/client/models/ChatSubscription'; -import { menu } from '../../../app/ui-utils/client/lib/menu'; -import { useEmbeddedLayout } from '../../hooks/useEmbeddedLayout'; -import { useReactiveValue } from '../../hooks/useReactiveValue'; -import { useSession } from '../../hooks/useSession'; -import { useUserPreference } from '../../hooks/useUserPreference'; +import React from 'react'; import './BurgerMenuButton.css'; -const useSidebarState = () => { - const isOpen = useSession('isMenuOpen'); - const toggle = () => menu.toggle(); - return [isOpen, toggle]; -}; - -const useUnreadMessagesBadge = () => { - const alertUnreadMessages = useUserPreference('unreadAlert') !== false; - const openedRoom = useSession('openedRoom'); - const [unreadCount, unreadAlert] = useReactiveValue(() => ChatSubscription - .find({ - open: true, - hideUnreadStatus: { $ne: true }, - rid: { $ne: openedRoom }, - }, { - fields: { - unread: 1, - alert: 1, - unreadAlert: 1, - }, - }) - .fetch() - .reduce(([unreadCount, unreadAlert], { alert, unread, unreadAlert: alertType }) => { - if (alert || unread > 0) { - unreadCount += unread; - if (alert === true && alertType !== 'nothing') { - if (alertType === 'all' || alertUnreadMessages !== false) { - unreadAlert = '•'; - } - } - } - - return [unreadCount, unreadAlert]; - }, [0, false]), [openedRoom, alertUnreadMessages]); - - return useMemo(() => { - if (unreadCount > 0) { - return unreadCount > 99 ? '99+' : unreadCount.toString(10); - } - - return unreadAlert || ''; - }, [unreadCount, unreadAlert]); -}; - -export function BurgerMenuButton() { - const [isSidebarOpen, toggleSidebarOpen] = useSidebarState(); - const isLayoutEmbedded = useEmbeddedLayout(); - const unreadMessagesBadge = useUnreadMessagesBadge(); - - const handleClick = () => { - toggleSidebarOpen(); - }; - - return