[NEW] Fallback Error component for Engagement Dashboard widgets (#26441)

pull/26195/head^2
Hugo Costa 3 years ago committed by GitHub
parent 22f209d584
commit 3039f2e82e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      apps/meteor/client/views/room/MessageList/MessageListErrorBoundary.tsx
  2. 6
      apps/meteor/client/views/room/Room/Room.tsx
  3. 27
      apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardCard.tsx
  4. 45
      apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardCardErrorBoundary.tsx
  5. 14
      apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardCardFilter.tsx
  6. 8
      apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx
  7. 26
      apps/meteor/ee/client/views/admin/engagementDashboard/Section.tsx
  8. 134
      apps/meteor/ee/client/views/admin/engagementDashboard/channels/ChannelsOverview.tsx
  9. 2
      apps/meteor/ee/client/views/admin/engagementDashboard/channels/ChannelsTab.stories.tsx
  10. 141
      apps/meteor/ee/client/views/admin/engagementDashboard/channels/ChannelsTab.tsx
  11. 1
      apps/meteor/ee/client/views/admin/engagementDashboard/channels/useChannelsList.ts
  12. 29
      apps/meteor/ee/client/views/admin/engagementDashboard/messages/MessagesPerChannelSection.tsx
  13. 29
      apps/meteor/ee/client/views/admin/engagementDashboard/messages/MessagesSentSection.tsx
  14. 24
      apps/meteor/ee/client/views/admin/engagementDashboard/messages/MessagesTab.tsx
  15. 1
      apps/meteor/ee/client/views/admin/engagementDashboard/messages/useMessageOrigins.ts
  16. 1
      apps/meteor/ee/client/views/admin/engagementDashboard/messages/useMessagesSent.ts
  17. 1
      apps/meteor/ee/client/views/admin/engagementDashboard/messages/useTopFivePopularChannels.ts
  18. 12
      apps/meteor/ee/client/views/admin/engagementDashboard/users/ActiveUsersSection.tsx
  19. 12
      apps/meteor/ee/client/views/admin/engagementDashboard/users/BusiestChatTimesSection.tsx
  20. 28
      apps/meteor/ee/client/views/admin/engagementDashboard/users/NewUsersSection.tsx
  21. 45
      apps/meteor/ee/client/views/admin/engagementDashboard/users/UsersByTimeOfTheDaySection.tsx
  22. 32
      apps/meteor/ee/client/views/admin/engagementDashboard/users/UsersTab.tsx
  23. 1
      apps/meteor/ee/client/views/admin/engagementDashboard/users/useActiveUsers.ts
  24. 1
      apps/meteor/ee/client/views/admin/engagementDashboard/users/useHourlyChatActivity.ts
  25. 3
      apps/meteor/ee/client/views/admin/engagementDashboard/users/useNewUsers.ts
  26. 1
      apps/meteor/ee/client/views/admin/engagementDashboard/users/useUsersByTimeOfTheDay.ts
  27. 1
      apps/meteor/ee/client/views/admin/engagementDashboard/users/useWeeklyChatActivity.ts
  28. 1
      apps/meteor/package.json
  29. 3
      packages/gazzodown/package.json
  30. 2
      packages/gazzodown/src/katex/KatexErrorBoundary.tsx
  31. 25
      packages/ui-client/src/components/ErrorBoundary.tsx
  32. 1
      packages/ui-client/src/components/index.ts
  33. 4
      yarn.lock

@ -1,7 +1,7 @@
import { States, StatesIcon, StatesTitle, StatesSubtitle, StatesActions, StatesAction, Icon } from '@rocket.chat/fuselage';
import { ErrorBoundary } from '@rocket.chat/ui-client';
import { useTranslation } from '@rocket.chat/ui-contexts';
import React, { ReactElement, ReactNode } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
const MessageListErrorBoundary = ({ children }: { children: ReactNode }): ReactElement => {
const t = useTranslation();

@ -1,7 +1,7 @@
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { ErrorBoundary } from '@rocket.chat/ui-client';
import { useUserPreference, useTranslation } from '@rocket.chat/ui-contexts';
import React, { useMemo, ReactElement } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { useEmbeddedLayout } from '../../../hooks/useEmbeddedLayout';
import Announcement from '../Announcement';
@ -43,7 +43,7 @@ export const Room = (): ReactElement => {
</RoomTemplate.Body>
{tab && (
<RoomTemplate.Aside data-qa-tabbar-name={tab.id}>
<ErrorBoundary>
<ErrorBoundary fallback={null}>
<SelectedMessagesProvider>
{typeof tab.template === 'string' && (
<VerticalBarOldActions {...tab} name={tab.template} tabBar={tabBar} rid={room._id} _id={room._id} />
@ -58,7 +58,7 @@ export const Room = (): ReactElement => {
{appsContextualBarContext && (
<RoomTemplate.Aside data-qa-tabbar-name={appsContextualBarContext.viewId}>
<SelectedMessagesProvider>
<ErrorBoundary>
<ErrorBoundary fallback={null}>
<LazyComponent
template={AppsContextualBar}
viewId={appsContextualBarContext.viewId}

@ -0,0 +1,27 @@
import { Box } from '@rocket.chat/fuselage';
import React, { ReactElement, ReactNode } from 'react';
import Card from '../../../../../client/components/Card';
import EngagementDashboardCardErrorBoundary from './EngagementDashboardCardErrorBoundary';
type EngagementDashboardCardProps = {
children?: ReactNode;
title?: string;
};
const EngagementDashboardCard = ({ children, title = undefined }: EngagementDashboardCardProps): ReactElement => (
<Box mb='x16'>
<Card variant='light'>
{title && <Card.Title>{title}</Card.Title>}
<Card.Body>
<Card.Col>
<EngagementDashboardCardErrorBoundary>
<Box>{children}</Box>
</EngagementDashboardCardErrorBoundary>
</Card.Col>
</Card.Body>
</Card>
</Box>
);
export default EngagementDashboardCard;

@ -0,0 +1,45 @@
import { States, StatesAction, StatesActions, StatesIcon, StatesSubtitle, StatesTitle } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import { QueryErrorResetBoundary } from '@tanstack/react-query';
import React, { ReactElement, ReactNode, useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
export type EngagementDashboardCardErrorBoundaryProps = {
children?: ReactNode;
};
const EngagementDashboardCardErrorBoundary = ({ children }: EngagementDashboardCardErrorBoundaryProps): ReactElement => {
const t = useTranslation();
const [error, setError] = useState<Error>();
const isError = (error: unknown): error is Error => error instanceof Error;
const errorHandler = (error: Error, info: { componentStack: string }): void => {
setError(error);
console.error('Uncaught Error:', error, info);
};
return (
<QueryErrorResetBoundary>
{({ reset }): ReactElement => (
<ErrorBoundary
children={children}
onError={errorHandler}
onReset={reset}
fallbackRender={({ resetErrorBoundary }): ReactElement => (
<States>
<StatesIcon name='circle-exclamation' />
<StatesTitle>{t('Something_Went_Wrong')}</StatesTitle>
<StatesSubtitle>{isError(error) && error?.message}</StatesSubtitle>
<StatesActions data-qa='EngagementDashboardCardErrorBoundary'>
<StatesAction onClick={(): void => resetErrorBoundary()}>{t('Retry')}</StatesAction>
</StatesActions>
</States>
)}
/>
)}
</QueryErrorResetBoundary>
);
};
export default EngagementDashboardCardErrorBoundary;

@ -0,0 +1,14 @@
import { Box, Flex, InputBox } from '@rocket.chat/fuselage';
import React, { ReactElement, ReactNode } from 'react';
type EngagementDashboardCardFilterProps = {
children?: ReactNode;
};
const EngagementDashboardCardFilter = ({ children = <InputBox.Skeleton /> }: EngagementDashboardCardFilterProps): ReactElement => (
<Box display='flex' justifyContent='flex-end' alignItems='center' wrap='no-wrap'>
{children && <Flex.Item grow={0}>{children}</Flex.Item>}
</Box>
);
export default EngagementDashboardCardFilter;

@ -32,18 +32,18 @@ const EngagementDashboardPage = ({ tab = 'users', onSelectTab }: EngagementDashb
);
return (
<Page>
<Page backgroundColor='neutral-100' data-qa='EngagementDashboardPage'>
<Page.Header title={t('Engagement_Dashboard')}>
<Select options={timezoneOptions} value={timezoneId} onChange={handleTimezoneChange} />
</Page.Header>
<Tabs>
<Tabs.Item selected={tab === 'users'} onClick={handleTabClick('users')}>
<Tabs.Item data-qa-id='EngagementDashboardPage-usersTab' selected={tab === 'users'} onClick={handleTabClick('users')}>
{t('Users')}
</Tabs.Item>
<Tabs.Item selected={tab === 'messages'} onClick={handleTabClick('messages')}>
<Tabs.Item data-qa-id='EngagementDashboardPage-messagesTab' selected={tab === 'messages'} onClick={handleTabClick('messages')}>
{t('Messages')}
</Tabs.Item>
<Tabs.Item selected={tab === 'channels'} onClick={handleTabClick('channels')}>
<Tabs.Item data-qa-id='EngagementDashboardPage-channelsTab' selected={tab === 'channels'} onClick={handleTabClick('channels')}>
{t('Channels')}
</Tabs.Item>
</Tabs>

@ -1,26 +0,0 @@
import { Box, Flex, InputBox, Margins } from '@rocket.chat/fuselage';
import React, { ReactElement, ReactNode } from 'react';
type SectionProps = {
children?: ReactNode;
title?: ReactNode;
filter?: ReactNode;
};
const Section = ({ children, title = undefined, filter = <InputBox.Skeleton /> }: SectionProps): ReactElement => (
<Box>
<Margins block='x24'>
<Box display='flex' justifyContent='flex-end' alignItems='center' wrap='no-wrap'>
{title && (
<Box flexGrow={1} fontScale='p2' color='default'>
{title}
</Box>
)}
{filter && <Flex.Item grow={0}>{filter}</Flex.Item>}
</Box>
{children}
</Margins>
</Box>
);
export default Section;

@ -0,0 +1,134 @@
import { Box, Icon, Margins, Pagination, Skeleton, Table, Tile } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import moment from 'moment';
import React, { ReactElement, useMemo, useState } from 'react';
import Growth from '../../../../../../client/components/dataView/Growth';
import EngagementDashboardCardFilter from '../EngagementDashboardCardFilter';
import DownloadDataButton from '../dataView/DownloadDataButton';
import PeriodSelector from '../dataView/PeriodSelector';
import { usePeriodSelectorState } from '../dataView/usePeriodSelectorState';
import { useChannelsList } from './useChannelsList';
const ChannelsOverview = (): ReactElement => {
const [period, periodSelectorProps] = usePeriodSelectorState('last 7 days', 'last 30 days', 'last 90 days');
const t = useTranslation();
const [current, setCurrent] = useState(0);
const [itemsPerPage, setItemsPerPage] = useState<25 | 50 | 100>(25);
const { data } = useChannelsList({
period,
offset: current,
count: itemsPerPage,
});
const channels = useMemo(() => {
if (!data) {
return;
}
return data?.channels?.map(({ room: { t, name, usernames, ts, _updatedAt }, messages, diffFromLastWeek }) => ({
t,
name: name || usernames?.join(' × '),
createdAt: ts,
updatedAt: _updatedAt,
messagesCount: messages,
messagesVariation: diffFromLastWeek,
}));
}, [data]);
return (
<>
<EngagementDashboardCardFilter>
<PeriodSelector {...periodSelectorProps} />
<DownloadDataButton
attachmentName={`Channels_start_${data?.start}_end_${data?.end}`}
headers={['Room type', 'Name', 'Messages', 'Last Update Date', 'Creation Date']}
dataAvailable={!!data}
dataExtractor={(): unknown[][] | undefined =>
data?.channels?.map(({ room: { t, name, usernames, ts, _updatedAt }, messages }) => [
t,
name || usernames?.join(' × '),
messages,
_updatedAt,
ts,
])
}
/>
</EngagementDashboardCardFilter>
<Box>
{channels && !channels.length && (
<Tile fontScale='p1' color='info' style={{ textAlign: 'center' }}>
{t('No_data_found')}
</Tile>
)}
{(!channels || channels.length) && (
<Table>
<Table.Head>
<Table.Row>
<Table.Cell>{'#'}</Table.Cell>
<Table.Cell>{t('Channel')}</Table.Cell>
<Table.Cell>{t('Created')}</Table.Cell>
<Table.Cell>{t('Last_active')}</Table.Cell>
<Table.Cell>{t('Messages_sent')}</Table.Cell>
</Table.Row>
</Table.Head>
<Table.Body>
{channels?.map(({ t, name, createdAt, updatedAt, messagesCount, messagesVariation }, i) => (
<Table.Row key={i}>
<Table.Cell>{i + 1}.</Table.Cell>
<Table.Cell>
<Margins inlineEnd='x4'>
{(t === 'd' && <Icon name='at' />) || (t === 'p' && <Icon name='lock' />) || (t === 'c' && <Icon name='hashtag' />)}
</Margins>
{name}
</Table.Cell>
<Table.Cell>{moment(createdAt).format('L')}</Table.Cell>
<Table.Cell>{moment(updatedAt).format('L')}</Table.Cell>
<Table.Cell>
{messagesCount} <Growth>{messagesVariation}</Growth>
</Table.Cell>
</Table.Row>
))}
{!channels &&
Array.from({ length: 5 }, (_, i) => (
<Table.Row key={i}>
<Table.Cell>
<Skeleton width='100%' />
</Table.Cell>
<Table.Cell>
<Skeleton width='100%' />
</Table.Cell>
<Table.Cell>
<Skeleton width='100%' />
</Table.Cell>
<Table.Cell>
<Skeleton width='100%' />
</Table.Cell>
<Table.Cell>
<Skeleton width='100%' />
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
)}
<Pagination
current={current}
itemsPerPage={itemsPerPage}
itemsPerPageLabel={(): string => t('Items_per_page:')}
showingResultsLabel={({ count, current, itemsPerPage }): string =>
t('Showing_results_of', current + 1, Math.min(current + itemsPerPage, count), count)
}
count={data?.total || 0}
onSetItemsPerPage={setItemsPerPage}
onSetCurrent={setCurrent}
/>
</Box>
</>
);
};
export default ChannelsOverview;

@ -2,7 +2,7 @@ import { Margins } from '@rocket.chat/fuselage';
import { Meta, Story } from '@storybook/react';
import React from 'react';
import ChannelsTab from './ChannelsTab';
import ChannelsTab from './ChannelsOverview';
export default {
title: 'Enterprise/Admin/Engagement Dashboard/ChannelsTab',

@ -1,137 +1,12 @@
import { Box, Icon, Margins, Pagination, Skeleton, Table, Tile } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import moment from 'moment';
import React, { ReactElement, useMemo, useState } from 'react';
import React, { ReactElement } from 'react';
import Growth from '../../../../../../client/components/dataView/Growth';
import Section from '../Section';
import DownloadDataButton from '../dataView/DownloadDataButton';
import PeriodSelector from '../dataView/PeriodSelector';
import { usePeriodSelectorState } from '../dataView/usePeriodSelectorState';
import { useChannelsList } from './useChannelsList';
import EngagementDashboardCard from '../EngagementDashboardCard';
import ChannelsOverview from './ChannelsOverview';
const ChannelsTab = (): ReactElement => {
const [period, periodSelectorProps] = usePeriodSelectorState('last 7 days', 'last 30 days', 'last 90 days');
const t = useTranslation();
const [current, setCurrent] = useState(0);
const [itemsPerPage, setItemsPerPage] = useState<25 | 50 | 100>(25);
const { data } = useChannelsList({
period,
offset: current,
count: itemsPerPage,
});
const channels = useMemo(() => {
if (!data) {
return;
}
return data?.channels?.map(({ room: { t, name, usernames, ts, _updatedAt }, messages, diffFromLastWeek }) => ({
t,
name: name || usernames?.join(' × '),
createdAt: ts,
updatedAt: _updatedAt,
messagesCount: messages,
messagesVariation: diffFromLastWeek,
}));
}, [data]);
return (
<Section
filter={
<>
<PeriodSelector {...periodSelectorProps} />
<DownloadDataButton
attachmentName={`Channels_start_${data?.start}_end_${data?.end}`}
headers={['Room type', 'Name', 'Messages', 'Last Update Date', 'Creation Date']}
dataAvailable={!!data}
dataExtractor={(): unknown[][] | undefined =>
data?.channels?.map(({ room: { t, name, usernames, ts, _updatedAt }, messages }) => [
t,
name || usernames?.join(' × '),
messages,
_updatedAt,
ts,
])
}
/>
</>
}
>
<Box>
{channels && !channels.length && (
<Tile fontScale='p1' color='info' style={{ textAlign: 'center' }}>
{t('No_data_found')}
</Tile>
)}
{(!channels || channels.length) && (
<Table>
<Table.Head>
<Table.Row>
<Table.Cell>{'#'}</Table.Cell>
<Table.Cell>{t('Channel')}</Table.Cell>
<Table.Cell>{t('Created')}</Table.Cell>
<Table.Cell>{t('Last_active')}</Table.Cell>
<Table.Cell>{t('Messages_sent')}</Table.Cell>
</Table.Row>
</Table.Head>
<Table.Body>
{channels?.map(({ t, name, createdAt, updatedAt, messagesCount, messagesVariation }, i) => (
<Table.Row key={i}>
<Table.Cell>{i + 1}.</Table.Cell>
<Table.Cell>
<Margins inlineEnd='x4'>
{(t === 'd' && <Icon name='at' />) || (t === 'p' && <Icon name='lock' />) || (t === 'c' && <Icon name='hashtag' />)}
</Margins>
{name}
</Table.Cell>
<Table.Cell>{moment(createdAt).format('L')}</Table.Cell>
<Table.Cell>{moment(updatedAt).format('L')}</Table.Cell>
<Table.Cell>
{messagesCount} <Growth>{messagesVariation}</Growth>
</Table.Cell>
</Table.Row>
))}
{!channels &&
Array.from({ length: 5 }, (_, i) => (
<Table.Row key={i}>
<Table.Cell>
<Skeleton width='100%' />
</Table.Cell>
<Table.Cell>
<Skeleton width='100%' />
</Table.Cell>
<Table.Cell>
<Skeleton width='100%' />
</Table.Cell>
<Table.Cell>
<Skeleton width='100%' />
</Table.Cell>
<Table.Cell>
<Skeleton width='100%' />
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
)}
<Pagination
current={current}
itemsPerPage={itemsPerPage}
itemsPerPageLabel={(): string => t('Items_per_page:')}
showingResultsLabel={({ count, current, itemsPerPage }): string =>
t('Showing_results_of', current + 1, Math.min(current + itemsPerPage, count), count)
}
count={data?.total || 0}
onSetItemsPerPage={setItemsPerPage}
onSetCurrent={setCurrent}
/>
</Box>
</Section>
);
};
const ChannelsTab = (): ReactElement => (
<EngagementDashboardCard>
<ChannelsOverview />
</EngagementDashboardCard>
);
export default ChannelsTab;

@ -36,6 +36,7 @@ export const useChannelsList = ({ period, offset, count }: UseChannelsListOption
{
keepPreviousData: true,
refetchInterval: 5 * 60 * 1000,
useErrorBoundary: true,
},
);
};

@ -4,7 +4,7 @@ import colors from '@rocket.chat/fuselage-tokens/colors';
import { useTranslation } from '@rocket.chat/ui-contexts';
import React, { ReactElement, useMemo } from 'react';
import Section from '../Section';
import EngagementDashboardCardFilter from '../EngagementDashboardCardFilter';
import DownloadDataButton from '../dataView/DownloadDataButton';
import LegendSymbol from '../dataView/LegendSymbol';
import PeriodSelector from '../dataView/PeriodSelector';
@ -39,20 +39,17 @@ const MessagesPerChannelSection = (): ReactElement => {
);
return (
<Section
title={t('Where_are_the_messages_being_sent?')}
filter={
<>
<PeriodSelector {...periodSelectorProps} />
<DownloadDataButton
attachmentName={`MessagesPerChannelSection_start_${messageOriginsData?.start}_end_${messageOriginsData?.end}`}
headers={['Room Type', 'Messages']}
dataAvailable={!!messageOriginsData}
dataExtractor={(): unknown[][] | undefined => messageOriginsData?.origins.map(({ t, messages }) => [t, messages])}
/>
</>
}
>
<>
<EngagementDashboardCardFilter>
<PeriodSelector {...periodSelectorProps} />
<DownloadDataButton
attachmentName={`MessagesPerChannelSection_start_${messageOriginsData?.start}_end_${messageOriginsData?.end}`}
headers={['Room Type', 'Messages']}
dataAvailable={!!messageOriginsData}
dataExtractor={(): unknown[][] | undefined => messageOriginsData?.origins.map(({ t, messages }) => [t, messages])}
/>
</EngagementDashboardCardFilter>
<Flex.Container>
<Margins inline='neg-x12'>
<Box>
@ -224,7 +221,7 @@ const MessagesPerChannelSection = (): ReactElement => {
</Box>
</Margins>
</Flex.Container>
</Section>
</>
);
};

@ -6,7 +6,7 @@ import moment from 'moment';
import React, { ReactElement, useMemo } from 'react';
import CounterSet from '../../../../../../client/components/dataView/CounterSet';
import Section from '../Section';
import EngagementDashboardCardFilter from '../EngagementDashboardCardFilter';
import DownloadDataButton from '../dataView/DownloadDataButton';
import PeriodSelector from '../dataView/PeriodSelector';
import { usePeriodLabel } from '../dataView/usePeriodLabel';
@ -42,20 +42,17 @@ const MessagesSentSection = (): ReactElement => {
}, [data]);
return (
<Section
title={t('Messages_sent')}
filter={
<>
<PeriodSelector {...periodSelectorProps} />
<DownloadDataButton
attachmentName={`MessagesSentSection_start_${data?.start}_end_${data?.end}`}
headers={['Date', 'Messages']}
dataAvailable={!!data}
dataExtractor={(): unknown[][] | undefined => values?.map(({ date, newMessages }) => [date, newMessages])}
/>
</>
}
>
<>
<EngagementDashboardCardFilter>
<PeriodSelector {...periodSelectorProps} />
<DownloadDataButton
attachmentName={`MessagesSentSection_start_${data?.start}_end_${data?.end}`}
headers={['Date', 'Messages']}
dataAvailable={!!data}
dataExtractor={(): unknown[][] | undefined => values?.map(({ date, newMessages }) => [date, newMessages])}
/>
</EngagementDashboardCardFilter>
<CounterSet
counters={[
{
@ -153,7 +150,7 @@ const MessagesSentSection = (): ReactElement => {
<Skeleton variant='rect' height={240} />
)}
</Flex.Container>
</Section>
</>
);
};

@ -1,15 +1,23 @@
import { Divider } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import React, { ReactElement } from 'react';
import EngagementDashboardCard from '../EngagementDashboardCard';
import MessagesPerChannelSection from './MessagesPerChannelSection';
import MessagesSentSection from './MessagesSentSection';
const MessagesTab = (): ReactElement => (
<>
<MessagesSentSection />
<Divider />
<MessagesPerChannelSection />
</>
);
const MessagesTab = (): ReactElement => {
const t = useTranslation();
return (
<>
<EngagementDashboardCard title={t('Messages_sent')}>
<MessagesSentSection />
</EngagementDashboardCard>
<EngagementDashboardCard title={t('Where_are_the_messages_being_sent?')}>
<MessagesPerChannelSection />
</EngagementDashboardCard>
</>
);
};
export default MessagesTab;

@ -29,6 +29,7 @@ export const useMessageOrigins = ({ period }: UseMessageOriginsOptions) => {
},
{
refetchInterval: 5 * 60 * 1000,
useErrorBoundary: true,
},
);
};

@ -29,6 +29,7 @@ export const useMessagesSent = ({ period }: UseMessagesSentOptions) => {
},
{
refetchInterval: 5 * 60 * 1000,
useErrorBoundary: true,
},
);
};

@ -29,6 +29,7 @@ export const useTopFivePopularChannels = ({ period }: UseTopFivePopularChannelsO
},
{
refetchInterval: 5 * 60 * 1000,
useErrorBoundary: true,
},
);
};

@ -7,7 +7,7 @@ import React, { ReactElement, useMemo } from 'react';
import CounterSet from '../../../../../../client/components/dataView/CounterSet';
import { useFormatDate } from '../../../../../../client/hooks/useFormatDate';
import Section from '../Section';
import EngagementDashboardCardFilter from '../EngagementDashboardCardFilter';
import DownloadDataButton from '../dataView/DownloadDataButton';
import LegendSymbol from '../dataView/LegendSymbol';
import { useActiveUsers } from './useActiveUsers';
@ -102,9 +102,8 @@ const ActiveUsersSection = ({ timezone }: ActiveUsersSectionProps): ReactElement
const t = useTranslation();
return (
<Section
title={t('Active_users')}
filter={
<>
<EngagementDashboardCardFilter>
<DownloadDataButton
attachmentName={`ActiveUsersSection_start_${data?.start}_end_${data?.end}`}
headers={['Date', 'DAU', 'WAU', 'MAU']}
@ -119,8 +118,7 @@ const ActiveUsersSection = ({ timezone }: ActiveUsersSectionProps): ReactElement
return values;
}}
/>
}
>
</EngagementDashboardCardFilter>
<CounterSet
counters={[
{
@ -272,7 +270,7 @@ const ActiveUsersSection = ({ timezone }: ActiveUsersSectionProps): ReactElement
<Skeleton variant='rect' height={240} />
)}
</Flex.Container>
</Section>
</>
);
};

@ -2,7 +2,7 @@ import { Select } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import React, { ReactElement, useMemo, useState } from 'react';
import Section from '../Section';
import EngagementDashboardCardFilter from '../EngagementDashboardCardFilter';
import ContentForDays from './ContentForDays';
import ContentForHours from './ContentForHours';
@ -42,17 +42,17 @@ const BusiestChatTimesSection = ({ timezone }: BusiestChatTimesSectionProps): Re
)[timeUnit];
return (
<Section
title={t('When_is_the_chat_busier?')}
filter={<Select options={timeUnitOptions} value={timeUnit} onChange={handleTimeUnitChange} />}
>
<>
<EngagementDashboardCardFilter>
<Select options={timeUnitOptions} value={timeUnit} onChange={handleTimeUnitChange} />
</EngagementDashboardCardFilter>
<Content
displacement={displacement}
onPreviousDateClick={handlePreviousDateClick}
onNextDateClick={handleNextDateClick}
timezone={timezone}
/>
</Section>
</>
);
};

@ -8,7 +8,7 @@ import React, { ReactElement, useMemo } from 'react';
import CounterSet from '../../../../../../client/components/dataView/CounterSet';
import { useFormatDate } from '../../../../../../client/hooks/useFormatDate';
import Section from '../Section';
import EngagementDashboardCardFilter from '../EngagementDashboardCardFilter';
import DownloadDataButton from '../dataView/DownloadDataButton';
import PeriodSelector from '../dataView/PeriodSelector';
import { usePeriodLabel } from '../dataView/usePeriodLabel';
@ -78,20 +78,16 @@ const NewUsersSection = ({ timezone }: NewUsersSectionProps): ReactElement => {
}, [data, utc]);
return (
<Section
title={t('New_users')}
filter={
<>
<PeriodSelector {...periodSelectorProps} />
<DownloadDataButton
attachmentName={`NewUsersSection_start_${data?.start}_end_${data?.end}`}
headers={['Date', 'New Users']}
dataAvailable={!!data}
dataExtractor={(): unknown[][] | undefined => values?.map(({ date, newUsers }) => [date, newUsers])}
/>
</>
}
>
<>
<EngagementDashboardCardFilter>
<PeriodSelector {...periodSelectorProps} />
<DownloadDataButton
attachmentName={`NewUsersSection_start_${data?.start}_end_${data?.end}`}
headers={['Date', 'New Users']}
dataAvailable={!!data}
dataExtractor={(): unknown[][] | undefined => values?.map(({ date, newUsers }) => [date, newUsers])}
/>
</EngagementDashboardCardFilter>
<CounterSet
counters={[
{
@ -195,7 +191,7 @@ const NewUsersSection = ({ timezone }: NewUsersSectionProps): ReactElement => {
</Box>
)}
</Flex.Container>
</Section>
</>
);
};

@ -5,7 +5,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts';
import moment from 'moment';
import React, { ReactElement, useMemo } from 'react';
import Section from '../Section';
import EngagementDashboardCardFilter from '../EngagementDashboardCardFilter';
import DownloadDataButton from '../dataView/DownloadDataButton';
import PeriodSelector from '../dataView/PeriodSelector';
import { usePeriodSelectorState } from '../dataView/usePeriodSelectorState';
@ -75,28 +75,25 @@ const UsersByTimeOfTheDaySection = ({ timezone }: UsersByTimeOfTheDaySectionProp
}, [data, utc]);
return (
<Section
title={t('Users_by_time_of_day')}
filter={
<>
<PeriodSelector {...periodSelectorProps} />
<DownloadDataButton
attachmentName={`UsersByTimeOfTheDaySection_start_${data?.start}_end_${data?.end}`}
headers={['Date', 'Users']}
dataAvailable={!!data}
dataExtractor={(): unknown[][] | undefined =>
data?.week
?.map(({ users, hour, day, month, year }) => ({
date: moment([year, month - 1, day, hour, 0, 0, 0]),
users,
}))
?.sort((a, b) => a.date.diff(b.date))
?.map(({ date, users }) => [date.toISOString(), users])
}
/>
</>
}
>
<>
<EngagementDashboardCardFilter>
<PeriodSelector {...periodSelectorProps} />
<DownloadDataButton
attachmentName={`UsersByTimeOfTheDaySection_start_${data?.start}_end_${data?.end}`}
headers={['Date', 'Users']}
dataAvailable={!!data}
dataExtractor={(): unknown[][] | undefined =>
data?.week
?.map(({ users, hour, day, month, year }) => ({
date: moment([year, month - 1, day, hour, 0, 0, 0]),
users,
}))
?.sort((a, b) => a.date.diff(b.date))
?.map(({ date, users }) => [date.toISOString(), users])
}
/>
</EngagementDashboardCardFilter>
{values ? (
<Box display='flex' style={{ height: 696 }}>
<Flex.Item align='stretch' grow={1} shrink={0}>
@ -187,7 +184,7 @@ const UsersByTimeOfTheDaySection = ({ timezone }: UsersByTimeOfTheDaySectionProp
) : (
<Skeleton variant='rect' height={696} />
)}
</Section>
</>
);
};

@ -1,7 +1,9 @@
import { Box, Divider, Flex, Margins } from '@rocket.chat/fuselage';
import { Box, Flex } from '@rocket.chat/fuselage';
import { useBreakpoints } from '@rocket.chat/fuselage-hooks';
import { useTranslation } from '@rocket.chat/ui-contexts';
import React, { ReactElement } from 'react';
import EngagementDashboardCard from '../EngagementDashboardCard';
import ActiveUsersSection from './ActiveUsersSection';
import BusiestChatTimesSection from './BusiestChatTimesSection';
import NewUsersSection from './NewUsersSection';
@ -12,23 +14,29 @@ type UsersTabProps = {
};
const UsersTab = ({ timezone }: UsersTabProps): ReactElement => {
const t = useTranslation();
const isXxlScreen = useBreakpoints().includes('xxl');
return (
<>
<NewUsersSection timezone={timezone} />
<Divider />
<ActiveUsersSection timezone={timezone} />
<Divider />
<Box display='flex' mi='x12' flexWrap='wrap'>
<Margins inline='x12'>
<Flex.Item grow={1} shrink={0} basis={isXxlScreen ? '0' : '100%'}>
<EngagementDashboardCard title={t('New_users')}>
<NewUsersSection timezone={timezone} />
</EngagementDashboardCard>
<EngagementDashboardCard title={t('Active_users')}>
<ActiveUsersSection timezone={timezone} />
</EngagementDashboardCard>
<Box display='flex' flexWrap='wrap' style={{ columnGap: '16px' }}>
<Flex.Item grow={1} shrink={0} basis={isXxlScreen ? '0' : '100%'}>
<EngagementDashboardCard title={t('Users_by_time_of_day')}>
<UsersByTimeOfTheDaySection timezone={timezone} />
</Flex.Item>
<Box flexGrow={1} flexShrink={0} flexBasis={isXxlScreen ? '0' : '100%'}>
</EngagementDashboardCard>
</Flex.Item>
<Box flexGrow={1} flexShrink={0} flexBasis={isXxlScreen ? '0' : '100%'}>
<EngagementDashboardCard title={t('When_is_the_chat_busier?')}>
<BusiestChatTimesSection timezone={timezone} />
</Box>
</Margins>
</EngagementDashboardCard>
</Box>
</Box>
</>
);

@ -30,6 +30,7 @@ export const useActiveUsers = ({ utc }: UseActiveUsersOptions) => {
},
{
refetchInterval: 5 * 60 * 1000,
useErrorBoundary: true,
},
);
};

@ -29,6 +29,7 @@ export const useHourlyChatActivity = ({ displacement, utc }: UseHourlyChatActivi
},
{
refetchInterval: 5 * 60 * 1000,
useErrorBoundary: true,
},
);
};

@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query';
import { getPeriodRange, Period } from '../dataView/periods';
type UseNewUsersOptions = { period: Period['key']; utc: boolean };
export type UseNewUsersOptions = { period: Period['key']; utc: boolean };
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export const useNewUsers = ({ period, utc }: UseNewUsersOptions) => {
@ -29,6 +29,7 @@ export const useNewUsers = ({ period, utc }: UseNewUsersOptions) => {
},
{
refetchInterval: 5 * 60 * 1000,
useErrorBoundary: true,
},
);
};

@ -29,6 +29,7 @@ export const useUsersByTimeOfTheDay = ({ period, utc }: UseUsersByTimeOfTheDayOp
},
{
refetchInterval: 5 * 60 * 1000,
useErrorBoundary: true,
},
);
};

@ -29,6 +29,7 @@ export const useWeeklyChatActivity = ({ displacement, utc }: UseWeeklyChatActivi
},
{
refetchInterval: 5 * 60 * 1000,
useErrorBoundary: true,
},
);
};

@ -342,6 +342,7 @@
"rc-scrollbars": "^1.1.5",
"react": "~17.0.2",
"react-dom": "~17.0.2",
"react-error-boundary": "^3.1.4",
"react-hook-form": "^7.30.0",
"react-i18next": "^11.16.7",
"react-keyed-flatten-children": "^1.3.0",

@ -73,6 +73,7 @@
"react": "~17.0.2"
},
"dependencies": {
"highlight.js": "^11.5.1"
"highlight.js": "^11.5.1",
"react-error-boundary": "^3.1.4"
}
}

@ -1,7 +1,7 @@
import colors from '@rocket.chat/fuselage-tokens/colors.json';
import styled from '@rocket.chat/styled';
import { ErrorBoundary } from '@rocket.chat/ui-client';
import { PropsWithChildren, ReactElement, useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
type KatexErrorBoundaryProps = PropsWithChildren<{ code: string }>;

@ -1,25 +0,0 @@
import { Component, ReactNode, ErrorInfo } from 'react';
export class ErrorBoundary extends Component<
{ fallback?: ReactNode; onError?: (error: Error, errorInfo: ErrorInfo) => void },
{ hasError: boolean }
> {
state = { hasError: false };
static getDerivedStateFromError(): { hasError: boolean } {
return { hasError: true };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
this.props.onError?.(error, errorInfo);
console.error('Uncaught Error:', error, errorInfo);
}
render(): ReactNode {
if (this.state.hasError) {
return this.props.fallback || null;
}
return this.props.children;
}
}

@ -1,4 +1,3 @@
export * from './ExternalLink';
export * from './ErrorBoundary';
export * from './DotLeader';
export * from './TooltipComponent';

@ -4076,6 +4076,7 @@ __metadata:
katex: ~0.16.0
outdent: ^0.8.0
react-dom: ~17.0.2
react-error-boundary: ^3.1.4
ts-jest: ^27.1.4
typescript: ~4.5.5
peerDependencies:
@ -4496,6 +4497,7 @@ __metadata:
rc-scrollbars: ^1.1.5
react: ~17.0.2
react-dom: ~17.0.2
react-error-boundary: ^3.1.4
react-hook-form: ^7.30.0
react-i18next: ^11.16.7
react-keyed-flatten-children: ^1.3.0
@ -26204,7 +26206,7 @@ __metadata:
languageName: node
linkType: hard
"react-error-boundary@npm:^3.1.0":
"react-error-boundary@npm:^3.1.4":
version: 3.1.4
resolution: "react-error-boundary@npm:3.1.4"
dependencies:

Loading…
Cancel
Save