# 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:
parent
8467199d94
commit
268151b0bf
5 changed files with 127 additions and 10 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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')}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue