Enrich user tooltip on Schedule page #1456 (#1136)

# What this PR does

Information has been added to User Tooltip:

- Inside/Outside working hours badge
- Is Oncall now badge
- User's local time/time in selected timezone
- Contacts (with ability to click and go directly to email, slack
account or telegram messaging)

## Checklist

- [ ] Tests updated
- [ ] Documentation added
- [ ] `CHANGELOG.md` updated
This commit is contained in:
Yulia Shanyrova 2023-01-16 13:06:43 +01:00 committed by GitHub
parent 8467199d94
commit 268151b0bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 127 additions and 10 deletions

View file

@ -23,9 +23,9 @@
background: rgba(204, 204, 220, 0.4);
}
.hr {
width: 100%;
margin: 0 -11px;
.line-break {
width: 100vw;
margin: -4px -18px -4px -18px;
}
.times {
@ -36,3 +36,22 @@
.icon {
color: #ccccdc;
}
.timezone-wrapper {
display: flex;
}
.timezone-icon {
width: 10%;
}
.timezone-info {
width: 50%;
overflow-wrap: anywhere;
margin-left: 4px;
}
.contact-details a {
text-decoration-line: none;
word-break: break-all;
}

View file

@ -1,39 +1,120 @@
import React, { FC } from 'react';
import { HorizontalGroup, VerticalGroup } from '@grafana/ui';
import { HorizontalGroup, VerticalGroup, Icon, Badge } from '@grafana/ui';
import cn from 'classnames/bind';
import dayjs from 'dayjs';
import Avatar from 'components/Avatar/Avatar';
import Text from 'components/Text/Text';
import { isInWorkingHours } from 'components/WorkingHours/WorkingHours.helpers';
import { getTzOffsetString } from 'models/timezone/timezone.helpers';
import { User } from 'models/user/user.types';
import { useStore } from 'state/useStore';
import styles from './ScheduleUserDetails.module.css';
interface ScheduleUserDetailsProps {
currentMoment: dayjs.Dayjs;
user: User;
isOncall: boolean;
}
const cx = cn.bind(styles);
const ScheduleUserDetails: FC<ScheduleUserDetailsProps> = (props) => {
const { user, currentMoment } = props;
const { user, currentMoment, isOncall } = props;
const userMoment = currentMoment.tz(user.timezone);
const userOffsetHoursStr = getTzOffsetString(userMoment);
const isInWH = isInWorkingHours(currentMoment, user.working_hours, user.timezone);
const store = useStore();
const { teamStore } = store;
let slackWorkspaceNameOrigin = teamStore.currentTeam.slack_team_identity.cached_name;
const slackWorkspaceName = slackWorkspaceNameOrigin.replace(/[^0-9a-z]/gi, '');
return (
<div className={cx('root')}>
<VerticalGroup spacing="sm">
<VerticalGroup spacing="md">
<HorizontalGroup justify="space-between">
<Avatar src={user.avatar} size="large" />
</HorizontalGroup>
<VerticalGroup spacing="sm">
<Text type="primary">{user.username}</Text>
<Text type="secondary">
{`${userMoment.tz(user.timezone).format('DD MMM, HH:mm')}`} {userOffsetHoursStr}
</Text>
{isOncall && <Badge text="OnCall now" color="green" />}
{isInWH ? (
<Badge text="Inside working hours" color="blue" />
) : (
<Badge text="Outside working hours" color="orange" />
)}
<HorizontalGroup align="flex-start">
<div className={cx('timezone-icon')}>
<Text type="secondary">
<Icon name="clock-nine" />
</Text>
</div>
<div className={cx('timezone-wrapper')}>
<div className={cx('timezone-info')}>
<VerticalGroup>
<Text type="secondary">Local time</Text>
<Text type="secondary">{currentMoment.tz().format('DD MMM, HH:mm')}</Text>
<Text type="secondary">({getTzOffsetString(currentMoment)})</Text>
</VerticalGroup>
</div>
<div className={cx('timezone-info')}>
<VerticalGroup className={cx('timezone-info')}>
<Text>{user.username}'s time</Text>
<Text>{`${userMoment.tz(user.timezone).format('DD MMM, HH:mm')}`}</Text>
<Text>({userOffsetHoursStr})</Text>
</VerticalGroup>
</div>
</div>
</HorizontalGroup>
</VerticalGroup>
<hr className={cx('line-break')} />
<VerticalGroup spacing="sm">
<Text>Contacts</Text>
<div className={cx('contact-details')}>
<Text type="secondary">
<Icon name="envelope" />{' '}
<a href={`mailto:${user.email}`} target="_blank" rel="noreferrer">
<Text type="link">{user.email}</Text>
</a>{' '}
</Text>
</div>
{user.slack_user_identity && (
<div className={cx('contact-details')}>
<Text type="secondary">
<Icon name="slack" />{' '}
<a
href={`https://${slackWorkspaceName}.slack.com/team/${user.slack_user_identity.slack_id}`}
target="_blank"
rel="noreferrer"
>
<Text type="link">{user.slack_user_identity.slack_login}</Text>
</a>{' '}
</Text>
</div>
)}
{user.telegram_configuration && (
<div className={cx('contact-details')}>
<Text type="secondary">
<Icon name="message" />{' '}
<a
href={`https://t.me/${user.telegram_configuration.telegram_nick_name}`}
target="_blank"
rel="noreferrer"
>
<Text type="link">{user.telegram_configuration.telegram_nick_name}</Text>
</a>{' '}
</Text>
</div>
)}
{!user.hide_phone_number && user.verified_phone_number && (
<Text type="secondary">Phone: {user.verified_phone_number}</Text>
)}
</VerticalGroup>
</VerticalGroup>
</div>

View file

@ -82,3 +82,16 @@ export const getNonWorkingMoments = (startMoment, endMoment, workingHours) => {
return nonWorkingMoments;
};
export const isInWorkingHours = (currentMoment: dayjs.Dayjs, workingHours, timezone) => {
const timeFormat = 'HH:mm:ss';
const currentDayOfTheWeek = currentMoment.format('dddd').toLowerCase();
const workingHourStart = workingHours[currentDayOfTheWeek][0].start;
const workingHourEnd = workingHours[currentDayOfTheWeek][0].end;
const startTime = dayjs(workingHourStart, timeFormat).tz(timezone).format(timeFormat);
const endTime = dayjs(workingHourEnd, timeFormat).tz(timezone).format(timeFormat);
const currentTime = dayjs(currentMoment, timeFormat).format(timeFormat);
return currentTime < endTime && currentTime >= startTime;
};

View file

@ -279,7 +279,7 @@ const AvatarGroup = (props: AvatarGroupProps) => {
placement="top"
interactive
key={index}
content={<ScheduleUserDetails currentMoment={currentMoment} user={user} />}
content={<ScheduleUserDetails currentMoment={currentMoment} user={user} isOncall={isOncall} />}
>
<div
className={cx('avatar')}

View file

@ -3,6 +3,8 @@ import React, { useEffect, useMemo, useState } from 'react';
import { locationService } from '@grafana/runtime';
import classnames from 'classnames';
import dayjs from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import isBetween from 'dayjs/plugin/isBetween';
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
import isoWeek from 'dayjs/plugin/isoWeek';
@ -33,6 +35,8 @@ dayjs.extend(localeData);
dayjs.extend(isSameOrBefore);
dayjs.extend(isSameOrAfter);
dayjs.extend(isoWeek);
dayjs.extend(isBetween);
dayjs.extend(customParseFormat);
import 'style/vars.css';
import 'style/global.css';