apps/meteor/client/views/admin/engagementDashboard/users/UsersByTimeOfTheDaySection.tsx
import { ResponsiveHeatMapCanvas } from '@nivo/heatmap';
import { Box, Flex, Skeleton, Tooltip } from '@rocket.chat/fuselage';
import colors from '@rocket.chat/fuselage-tokens/colors.json';
import { useTranslation } from '@rocket.chat/ui-contexts';
import moment from 'moment';
import type { ReactElement } from 'react';
import React, { useCallback, useMemo } from 'react';
import DownloadDataButton from '../../../../components/dashboards/DownloadDataButton';
import PeriodSelector from '../../../../components/dashboards/PeriodSelector';
import { usePeriodSelectorState } from '../../../../components/dashboards/usePeriodSelectorState';
import EngagementDashboardCardFilter from '../EngagementDashboardCardFilter';
import { useUsersByTimeOfTheDay } from './useUsersByTimeOfTheDay';
type UsersByTimeOfTheDaySectionProps = {
timezone: 'utc' | 'local';
};
const UsersByTimeOfTheDaySection = ({ timezone }: UsersByTimeOfTheDaySectionProps): ReactElement => {
const [period, periodSelectorProps] = usePeriodSelectorState('last 7 days', 'last 30 days', 'last 90 days');
const utc = timezone === 'utc';
const { data, isLoading } = useUsersByTimeOfTheDay({ period, utc });
const t = useTranslation();
const [dates, values] = useMemo(() => {
if (!data || isLoading) {
return [];
}
const length = utc ? moment(data.end).diff(data.start, 'days') + 1 : moment(data.end).diff(data.start, 'days') - 1;
const start = utc ? moment.utc(data.start).endOf('day') : moment(data.start).endOf('day').add(1, 'days');
const dates = new Array(length);
for (let i = 0; i < length; i++) {
dates[i] = start.clone().add(i, 'days').toISOString();
}
const values = new Array(24);
for (let hour = 0; hour < 24; hour++) {
values[hour] = {
id: hour.toString(),
data: dates.map((x) => ({ x, y: 0 })),
};
}
const timezoneOffset = moment().utcOffset() / 60;
for (const { users, hour, day, month, year } of data.week) {
const date = utc ? moment.utc([year, month - 1, day, hour]) : moment([year, month - 1, day, hour]).add(timezoneOffset, 'hours');
if (utc || (!date.isSame(data.end) && !date.clone().startOf('day').isSame(data.start))) {
const dataPoint = values[date.hour()].data.find((point: { x: string }) => point.x === date.endOf('day').toISOString());
if (dataPoint) {
dataPoint.y += users;
}
}
}
return [dates, values];
}, [data, isLoading, utc]);
const tooltip = useCallback(
({ cell }): ReactElement => {
return (
<Tooltip>
{moment(cell.data.x).format('ddd')}{' '}
{moment()
.set({ hour: parseInt(cell.serieId, 10), minute: 0, second: 0 })
.format('LT')}
<br />
{t('Value_users', { value: cell.data.y })}
</Tooltip>
);
},
[t],
);
return (
<>
<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>
{!isLoading && values && dates ? (
<Box display='flex' style={{ height: 696 }}>
<Flex.Item align='stretch' grow={1} shrink={0}>
<Box style={{ position: 'relative' }}>
<Box
style={{
position: 'absolute',
width: '100%',
height: '100%',
}}
>
<ResponsiveHeatMapCanvas
data={values}
xInnerPadding={0.1}
yInnerPadding={0.25}
margin={{
// TODO: Get it from theme
left: 60,
bottom: 20,
}}
colors={{
type: 'quantize',
colors: [
// TODO: Get it from theme
colors.b100,
colors.b200,
colors.b300,
colors.b400,
colors.b500,
colors.b600,
colors.b700,
],
}}
emptyColor='transparent'
enableLabels={false}
axisTop={null}
axisRight={null}
axisBottom={{
// TODO: Get it from theme
tickSize: 0,
tickPadding: 8,
tickRotation: 0,
format: (isoString): string => (dates?.length === 8 ? moment(isoString).format('ddd') : ''),
}}
axisLeft={{
// TODO: Get it from theme
tickSize: 0,
tickPadding: 8,
tickRotation: 0,
format: (hour): string =>
moment()
.set({ hour: parseInt(hour, 10), minute: 0, second: 0 })
.format('LT'),
}}
hoverTarget='cell'
animate={dates && dates.length <= 7}
motionConfig='stiff'
theme={{
// TODO: Get it from theme
axis: {
ticks: {
text: {
fill: colors.n600,
fontFamily:
'Inter, -apple-system, system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Meiryo UI", Arial, sans-serif',
fontSize: 10,
fontStyle: 'normal',
fontWeight: 600,
letterSpacing: '0.2px',
lineHeight: '12px',
},
},
},
tooltip: {
container: {
backgroundColor: colors.n900,
boxShadow: '0px 0px 12px rgba(47, 52, 61, 0.12), 0px 0px 2px rgba(47, 52, 61, 0.08)',
borderRadius: 2,
},
},
}}
tooltip={tooltip}
/>
</Box>
</Box>
</Flex.Item>
</Box>
) : (
<Skeleton variant='rect' height={696} />
)}
</>
);
};
export default UsersByTimeOfTheDaySection;