add user timezone select

This commit is contained in:
Maxim 2022-06-17 12:19:34 +03:00
parent b83fcb092c
commit 9afb637aed
40 changed files with 4581 additions and 233 deletions

View file

@ -9,7 +9,7 @@ module.exports = {
'^assets|^components|^containers|^declare|^icons|^img|^interceptors|^models|^network|^pages|^services|^state|^utils',
},
rules: {
'no-unused-vars': ['error', { vars: 'all', args: 'after-used', ignoreRestSiblings: false }],
'no-unused-vars': ['warn', { vars: 'all', args: 'after-used', ignoreRestSiblings: false }],
'react/prop-types': 'warn',
'react/display-name': 'warn',
'react/jsx-key': 'warn',

View file

@ -42,7 +42,7 @@
"@grafana/data": "7.5.7",
"@grafana/runtime": "7.5.7",
"@grafana/toolkit": "7.5.7",
"@grafana/ui": "8.2.1",
"@grafana/ui": "9.0.0-beta3",
"@types/dompurify": "^2.0.2",
"@types/lodash-es": "^4.17.3",
"@types/moment-timezone": "^0.5.12",
@ -61,6 +61,7 @@
},
"dependencies": {
"@types/query-string": "^6.3.0",
"array-move": "^4.0.0",
"change-case": "^4.1.1",
"circular-dependency-plugin": "^5.2.2",
"dompurify": "^2.0.12",
@ -71,7 +72,9 @@
"moment-timezone": "^0.5.34",
"rc-table": "^7.17.1",
"react-copy-to-clipboard": "^5.0.2",
"react-draggable": "^4.4.5",
"react-emoji-render": "^1.2.4",
"react-modal": "^3.15.1",
"react-responsive": "^8.1.0",
"react-router-dom": "^5.2.0",
"react-sortable-hoc": "^1.11.0",

View file

@ -2,6 +2,12 @@ import React, { useEffect, useMemo } from 'react';
import { AppRootProps } from '@grafana/data';
import { Button, HorizontalGroup, LinkButton, VerticalGroup } from '@grafana/ui';
import dayjs from 'dayjs';
dayjs.extend(utc);
dayjs.extend(timezone);
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';
import { observer, Provider } from 'mobx-react';
import 'interceptors';

View file

@ -13,13 +13,13 @@ interface AvatarProps {
const cx = cn.bind(styles);
const Avatar: FC<AvatarProps> = (props) => {
const { src, size, className } = props;
const { src, size, className, ...rest } = props;
if (!src) {
return null;
}
return <img src={src} className={cx('root', `avatarSize-${size}`, className)} />;
return <img src={src} className={cx('root', `avatarSize-${size}`, className)} {...rest} />;
};
export default Avatar;

View file

@ -0,0 +1,32 @@
.root {
position: fixed;
width: 750px;
max-width: 100%;
left: 0px;
right: 0px;
margin-left: auto;
margin-right: auto;
top: 10%;
max-height: 80%;
display: flex;
flex-direction: column;
border-image: initial;
outline: none;
padding: 15px;
background: #181B1F;
border: 1px solid #2D2E35;
box-shadow: 0px 2px 4px 2px rgba(10, 10, 16, 0.1), 0px 8px 16px rgba(10, 10, 16, 0.2), 0px 12px 24px rgba(3, 3, 8, 0.3), 0px 16px 32px rgba(3, 3, 8, 0.8);
border-radius: 2px;
}
.overlay {
position: fixed;
inset: 0px;
z-index: 10;
/*background-color: rgba(0, 0, 0, 0.45);
backdrop-filter: blur(1px);*/
}
.body-open {
overflow: hidden;
}

View file

@ -0,0 +1,45 @@
import React, { FC, PropsWithChildren } from 'react';
import ReactModal from 'react-modal';
import cn from 'classnames/bind';
import styles from './Modal.module.css';
export interface ModalProps {
title: string | JSX.Element;
className?: string;
contentClassName?: string;
closeOnEscape?: boolean;
closeOnBackdropClick?: boolean;
onDismiss?: () => void;
width: string;
contentElement?: (props, children: React.ReactNode) => React.ReactNode;
}
const cx = cn.bind(styles);
const Modal: FC<PropsWithChildren<ModalProps>> = (props) => {
const { title, children, onDismiss, width = '600px', contentElement } = props;
return (
<ReactModal
style={{
overlay: {},
content: {
width,
},
}}
isOpen
onAfterOpen={() => {}}
onRequestClose={onDismiss}
contentLabel={title}
className={cx('root')}
overlayClassName={cx('overlay')}
bodyOpenClassName={cx('body-open')}
contentElement={contentElement}
>
{children}
</ReactModal>
);
};
export default Modal;

View file

@ -0,0 +1,28 @@
.root {
background: var(--primary-background);
transition: background-color 300ms;
}
.root:last-child{
padding-bottom: 26px;
}
.root:hover {
background: var(--secondary-background);
}
.timeline {
display: flex;
flex-direction: column;
gap: 5px;
padding-bottom: 8px;
}
.root:first-child .timeline {
padding-top: 26px;
}
.root:last-child .timeline {
padding-bottom: 0;
}

View file

@ -0,0 +1,31 @@
import React, { FC } from 'react';
import cn from 'classnames/bind';
import ScheduleTimeline from 'components/ScheduleTimeline/ScheduleTimeline';
import styles from './Rotation.module.css';
interface RotationProps {}
const cx = cn.bind(styles);
const Rotation: FC<RotationProps> = (props) => {
const { layerIndex, rotationIndex, slots, color, label } = props;
return (
<div className={cx('root')}>
<div className={cx('timeline')}>
<ScheduleTimeline
layerIndex={layerIndex}
rotationIndex={rotationIndex}
slots={slots}
color={color}
label={label}
/>
</div>
</div>
);
};
export default Rotation;

View file

@ -0,0 +1,9 @@
.root {
}
.header{
width: 100%;
display: flex;
justify-content: space-between;
}

View file

@ -0,0 +1,51 @@
import React, { FC } from 'react';
import { IconButton, VerticalGroup, HorizontalGroup } from '@grafana/ui';
import cn from 'classnames/bind';
import Draggable from 'react-draggable';
import Modal from 'components/Modal/Modal';
import Text from 'components/Text/Text';
import UserGroups from 'components/UserGroups/UserGroups';
import styles from './RotationForm.module.css';
interface RotationFormProps {
layerId: string;
onHide: () => void;
id: number | 'new';
}
const cx = cn.bind(styles);
const RotationForm: FC<RotationFormProps> = (props) => {
const { onHide } = props;
return (
<Modal
width="400px"
title="New Rotation"
onDismiss={onHide}
contentElement={(props, children) => (
<Draggable handle=".drag-handler" positionOffset={{ x: 0, y: 0 }}>
<div {...props}>{children}</div>
</Draggable>
)}
>
<VerticalGroup>
<div className={cx('header')}>
<Text size="medium">Rotation 1</Text>
<div className={cx('header-buttons')}>
<IconButton className={cx('handle', 'drag-handler')} name="draggabledots" />
</div>
</div>
<UserGroups />
{/*<HorizontalGroup justify="end">
<Button variant="primary">Create</Button>
</HorizontalGroup>*/}
</VerticalGroup>
</Modal>
);
};
export default RotationForm;

View file

@ -0,0 +1,83 @@
import dayjs from 'dayjs';
export const getRandomTimeslots = (count: number = 6) => {
const slots = [];
for (let i = 0; i < count; i++) {
const start = dayjs()
.startOf('day')
.add(i * 4, 'hour');
const end = dayjs()
.startOf('day')
.add(i * 4 + 2, 'hour');
//const inactive = end.isBefore(dayjs());
const inactive = false;
slots.push({
start,
end,
inactive,
users: [getRandomUser(), getRandomUser()],
});
}
return slots;
};
const L1_COLORS = [
'#3D71D9',
'#1A6BE8',
'#6D609C',
'#50639C',
'#8214A0',
'#44449F',
'#4D3B72',
'#273C6C',
];
const L2_COLORS = [
'#3CB979',
'#A49E7C',
'#188343',
'#746D46',
'#84362A',
'#464121',
'#521913',
'#414130',
];
const L3_COLORS = [
'#377277',
'#797B83',
'#638282',
'#626779',
'#364E4E',
'#47494F',
'#423220',
'#44321D',
];
const OVERRIDE_COLORS = ['#C69B06', '#797B83', '#638282', '#626779'];
const COLORS = [L1_COLORS, L2_COLORS, L3_COLORS, OVERRIDE_COLORS];
export const getColor = (layerIndex: number, rotationIndex) => {
return COLORS[layerIndex][rotationIndex];
};
const USERS = [
'Innokentii Konstantinov',
'Ildar Iskhakov',
'Matias Bordese',
'Michael Derynck',
'Vadim Stepanov',
'Matvey Kukuy',
'Yulya Artyukhina',
'Raphael Batyrbaev',
];
export const getRandomUser = () => {
return USERS[Math.floor(Math.random() * USERS.length)];
};
export const getLabel = (layerIndex: number, rotationIndex) => {
return `L ${layerIndex + 1}-${rotationIndex + 1}`;
};

View file

@ -0,0 +1,87 @@
.root {
border: var(--border-medium);
border-radius: 2px;
background: var(--primary-background);
}
.current-time {
position: absolute;
left: 650px;
width: 1px;
background: #fff;
top: 0;
bottom: 0;
z-index: 1;
}
.header{
padding: 0 10px;
}
.title {
font-weight: 500;
font-size: 19px;
line-height: 24px;
color: rgba(204, 204, 220, 0.65);
margin: 16px 0;
}
.rotations-plus-title{
display: flex;
flex-direction: column;
}
.layer {
}
.layer-title {
text-align: center;
font-weight: 500;
font-size: 11px;
line-height: 16px;
letter-spacing: 0.1em;
color: rgba(204, 204, 220, 0.65);
text-transform: uppercase;
padding: 8px;
background: var(--secondary-background);
}
.layer-title:hover{
background: rgba(204,204,220, 0.12);
}
.header-plus-content {
position: relative;
}
.layer-header {
padding: 12px;
display: flex;
justify-content: space-between;
}
.layer-header-title {
font-weight: 400;
font-size: 14px;
line-height: 20px;
color: rgba(204, 204, 220, 0.65);
}
.layer-content {
position: relative;
}
.rotation {
background: var(--primary-background);
transition: background-color 300ms;
}
.add-rotations-layer {
font-weight: 400;
font-size: 12px;
line-height: 16px;
text-align: center;
padding: 12px;
color: rgba(204, 204, 220, 0.65);
}

View file

@ -0,0 +1,115 @@
import React, { Component, useMemo, useState } from 'react';
import { ValuePicker, IconButton, Icon, HorizontalGroup, Button } from '@grafana/ui';
import cn from 'classnames/bind';
import * as dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import Rotation from 'components/Rotation/Rotation';
import RotationForm from 'components/RotationForm/RotationForm';
import ScheduleTimeline from 'components/ScheduleTimeline/ScheduleTimeline';
import TimelineMarks from 'components/TimelineMarks/TimelineMarks';
import { getColor, getLabel, getRandomTimeslots, getRandomUser } from './Rotations.helpers';
import styles from './Rotations.module.css';
const cx = cn.bind(styles);
interface RotationsProps {
title: string;
startMoment: dayjs.Dayjs;
}
type Layer = {
id: string;
};
interface RotationsState {
layerIdToCreateRotation?: Layer['id'];
}
class Rotations extends Component<RotationsProps, RotationsState> {
state: RotationsState = {
//layerIdToCreateRotation: '12',
};
render() {
const { title, startMoment } = this.props;
const { layerIdToCreateRotation } = this.state;
const layers = [
{ id: 0, title: 'Layer 1' },
{ id: 1, title: 'Layer 2' },
{ id: 2, title: 'Layer 3' },
{ id: 3, title: 'Layer 4' },
];
const rotations = [{ slots: getRandomTimeslots() }, { slots: getRandomTimeslots() }];
return (
<>
<div className={cx('root')}>
<div className={cx('header')}>
<HorizontalGroup justify="space-between">
<div className={cx('title')}>{title}</div>
<ValuePicker
label="Add rotation"
options={layers.map(({ title, id }) => ({
label: title,
value: id,
}))}
onChange={this.handleAddRotation}
variant="secondary"
size="md"
/>
</HorizontalGroup>
</div>
<div className={cx('rotations-plus-title')}>
{layers.map((layer, layerIndex) => (
<div key={layerIndex}>
<div className={cx('layer')}>
<div className={cx('layer-title')}>
<HorizontalGroup spacing="sm" justify="center">
Layer {layerIndex + 1} <Icon name="info-circle" />
</HorizontalGroup>
</div>
<div className={cx('header-plus-content')}>
<div className={cx('current-time')} />
<TimelineMarks startMoment={startMoment} hideTimeMarks />
<div className={cx('rotations')}>
{rotations.map((rotation, rotationIndex) => (
<Rotation
slots={rotation.slots}
layerIndex={layerIndex}
rotationIndex={rotationIndex}
color={getColor(layerIndex, rotationIndex)}
label={getLabel(layerIndex, rotationIndex)}
/>
))}
</div>
</div>
</div>
</div>
))}
<div className={cx('add-rotations-layer')}>Add rotations layer +</div>
</div>
</div>
{layerIdToCreateRotation && (
<RotationForm
layerId={layerIdToCreateRotation}
onHide={() => {
this.setState({ layerIdToCreateRotation: undefined });
}}
/>
)}
</>
);
}
handleAddRotation = (option) => {
this.setState({ layerIdToCreateRotation: option.value });
};
}
export default Rotations;

View file

@ -0,0 +1,43 @@
.root {
}
.root__type_link {
padding: 2px 4px;
background: rgba(27, 133, 94, 0.15);
border: 1px solid var(--success-text-color);
border-radius: 2px;
}
.root__type_warning {
padding: 2px 4px;
background: rgba(245, 183, 61, 0.18);
border: 1px solid var(--warning-text-color);
border-radius: 2px;
}
.icon__type_link {
color: var(--success-text-color);
}
.icon__type_warning {
color: var(--warning-text-color);
}
.tooltip {
min-width: 276px;
}
/*
.tooltip__type_link {
border: 1px solid #6CCF8E;
background: #132322;
}
.tooltip__type_warning {
border: 1px solid #F8D06B;
background: #3A301E;
}
*/

View file

@ -0,0 +1,67 @@
import React, { FC, useCallback } from 'react';
import { HorizontalGroup, VerticalGroup, Icon, IconButton, Tooltip } from '@grafana/ui';
import cn from 'classnames/bind';
import Text from 'components/Text/Text';
import styles from './ScheduleCounter.module.css';
interface ScheduleCounterProps {
type: 'link' | 'warning';
count: number;
tooltipTitle: string;
tooltipContent: string;
}
const typeToIcon = {
link: 'link',
warning: 'exclamation-triangle',
};
const typeToColor = {
link: 'success',
warning: 'warning',
};
const typeToBorderColor = {
link: '#6CCF8E',
warning: '#F8D06B',
};
const typeToBackgroundColor = {
link: '#132322',
warning: '#3A301E',
};
const cx = cn.bind(styles);
const ScheduleCounter: FC<ScheduleCounterProps> = (props) => {
const { type, count, tooltipTitle, tooltipContent } = props;
return (
<Tooltip
placement="bottom-start"
interactive
content={
<div className={cx('tooltip', { [`tooltip__type_${type}`]: true })}>
<VerticalGroup>
<Text type={typeToColor[type]}>{tooltipTitle}</Text>
<Text type="secondary">
<div dangerouslySetInnerHTML={{ __html: tooltipContent }} />
</Text>
</VerticalGroup>
</div>
}
>
<div className={cx('root', { [`root__type_${type}`]: true })}>
<HorizontalGroup spacing="sm">
<Icon className={cx('icon', { [`icon__type_${type}`]: true })} name={typeToIcon[type]} />
<Text type={typeToColor[type]}>{count}</Text>
</HorizontalGroup>
</div>
</Tooltip>
);
};
export default ScheduleCounter;

View file

@ -0,0 +1,45 @@
.root {
padding: 6px 10px;
gap: 10px;
background: var(--primary-background);
border: var(--border-medium);
border-radius: 2px;
}
.details{
width: 276px;
}
.progress{
width:100%;
height: 16px;
background-color: var( --secondary-background-shade);
position: relative;
}
.progress-filler{
height: 100%;
position: absolute;
}
.progress-filler__type_success{
background-color: var(--success-text-color);
}
.progress-filler__type_warning{
background-color: var(--warning-text-color);
}
.quality-text{
float: right;
line-height: 16px;
margin-right: 3px;
}
.quality-text__type_success{
color: var(--primary-text-color);
}
.quality-text__type_warning{
color: #111217
}

View file

@ -0,0 +1,97 @@
import React, { FC, useCallback, useState } from 'react';
import { HorizontalGroup, VerticalGroup, Icon, IconButton, Tooltip } from '@grafana/ui';
import cn from 'classnames/bind';
import Text from 'components/Text/Text';
import styles from './ScheduleQuality.module.css';
interface ScheduleQualityProps {
quality: number;
}
const cx = cn.bind(styles);
const ScheduleQuality: FC<ScheduleQualityProps> = (props) => {
const { quality } = props;
return (
<Tooltip placement="bottom-start" interactive content={<SheduleQualityDetails quality={quality} />}>
<div className={cx('root')}>
<HorizontalGroup spacing="sm">
<Icon name="like" />
<Text type="secondary">Quality:</Text>
<Text type="primary">{Math.floor(quality * 100)}%</Text>
</HorizontalGroup>
</div>
</Tooltip>
);
};
interface ScheduleQualityDetailsProps {
quality: number;
}
const SheduleQualityDetails = (props: ScheduleQualityDetailsProps) => {
const { quality } = props;
const [expanded, setExpanded] = useState<boolean>(true);
const type = quality > 0.8 ? 'success' : 'warning';
const qualityPercent = quality * 100;
const handleExpandClick = useCallback(() => {
setExpanded((expanded) => !expanded);
}, []);
return (
<div className={cx('details')}>
<VerticalGroup>
<Text type="secondary">Schedule quality</Text>
<div className={cx('progress')}>
<div
style={{ width: `${qualityPercent}%` }}
className={cx('progress-filler', {
[`progress-filler__type_${type}`]: true,
})}
>
<div
className={cx('quality-text', {
[`quality-text__type_${type}`]: true,
})}
>
{qualityPercent}%
</div>{' '}
</div>
</div>
{type === 'success' && (
<Text type="primary">
You are doing a great job! <br />
Schedule is well balanced for all members.
</Text>
)}
{type === 'warning' && <Text type="primary">Your schedule has balance problems.</Text>}
<hr style={{ width: '100%' }} />
<VerticalGroup>
<HorizontalGroup justify="space-between">
<HorizontalGroup spacing="sm">
<Icon name="info-circle" />
<Text type="secondary">Calculation methodology</Text>
</HorizontalGroup>
<IconButton name="angle-down" onClick={handleExpandClick} />
</HorizontalGroup>
{expanded && (
<Text type="secondary">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer elementum purus egestas porta ultricies.
Sed quis maximus sem. Phasellus semper pulvinar sapien ac euismod.
</Text>
)}
</VerticalGroup>
</VerticalGroup>
</div>
);
};
export default ScheduleQuality;

View file

@ -0,0 +1,60 @@
.root {
height: 28px;
background: #3274D9;
border-radius: 2px;
position: relative;
display: flex;
gap: 4px;
}
.root__inactive {
opacity: 0.5;
}
.title {
padding: 5px;
z-index: 1;
color: #ffffff;
font-size: 12px;
font-weight: 500;
}
.label {
background: rgba(255, 255, 255, 0.7);
border-radius: 2px;
display: inline-block;
padding: 2px 4px;
margin: 4px;
line-height: 16px;
z-index: 1;
}
.striped {
--color: rgba(17, 18, 23, 0.3);
position: absolute;
top: 0;
bottom: 0;
opacity: 0.4;
height: 100%;
background: repeating-linear-gradient(
-45deg,
var(--color),
var(--color) 4px,
transparent 4px,
transparent 8px
);
}
.details{
width: 300px;
}
.details-user-status{
width: 10px;
height: 10px;
border-radius: 50%;
}
.details-user-status__type_success{
background-color: var(--success-text-color);
}

View file

@ -0,0 +1,104 @@
import React, { FC } from 'react';
import { HorizontalGroup, VerticalGroup, Icon, Tooltip, VerticalGroup } from '@grafana/ui';
import cn from 'classnames/bind';
import Line from 'components/ScheduleUserDetails/img/line.svg';
import Text from 'components/Text/Text';
import styles from './ScheduleSlot.module.css';
interface ScheduleSlotProps {
color: string;
user: string;
label: string;
}
const cx = cn.bind(styles);
const ScheduleSlot: FC<ScheduleSlotProps> = (props) => {
const { color, user, inactive, label } = props;
const left = Math.random() * 50;
const right = 100 - (left + 20 + Math.random() * 30);
const width = Math.random() * 150 + 100;
let title = user;
if (width < 150) {
title = title
.split(' ')
.map((word) => word.charAt(0).toUpperCase())
.join('');
}
return (
<Tooltip content={<ScheduleSlotDetails user={user} />}>
<div
className={cx('root', { root__inactive: inactive })}
style={{
backgroundColor: color,
width: `${width}px`,
}}
>
<div style={{ left: `${left}%`, right: `${right}%` }} className={cx('striped')} />
{label && (
<div className={cx('label')} style={{ color }}>
{label}
</div>
)}
<div className={cx('title')}>{title}</div>
</div>
</Tooltip>
);
};
export default ScheduleSlot;
interface ScheduleSlotDetailsProps {}
const ScheduleSlotDetails = (props) => {
const { user, currentUser } = props;
const userStatus = 'success';
return (
<div className={cx('details')}>
<HorizontalGroup>
<VerticalGroup spacing="sm">
<HorizontalGroup spacing="md">
<div
className={cx('details-user-status', {
[`details-user-status__type_${userStatus}`]: true,
})}
/>
<Text type="secondary">{user}</Text>
</HorizontalGroup>
<HorizontalGroup>
<VerticalGroup spacing="none">
<HorizontalGroup spacing="md">
<Icon name="clock-nine" />
<Text type="secondary">30 apr, 7:54 </Text>
</HorizontalGroup>
<HorizontalGroup>
<img src={Line} />
<VerticalGroup spacing="none">
<Text type="secondary">30 apr, 00:00</Text>
<Text type="secondary">30 apr, 23:59</Text>
</VerticalGroup>
</HorizontalGroup>
</VerticalGroup>
</HorizontalGroup>
</VerticalGroup>
<VerticalGroup spacing="sm">
<Text type="primary">Maxim Mordasov</Text>
<VerticalGroup spacing="none">
<Text type="primary">30 apr, 12:54 </Text>
<Text type="primary">29 apr, 20:00 </Text>
<Text type="primary">30 apr, 20:00 </Text>
</VerticalGroup>
</VerticalGroup>
</HorizontalGroup>
</div>
);
};

View file

@ -0,0 +1,24 @@
.root {
}
.slots {
display: flex;
gap: 4px;
padding: 0 2px;
}
.users{
display: flex;
flex-direction: column;
gap:1px;
}
.current-time {
position: absolute;
left: 450px;
width: 1px;
background: #fff;
top: -10px;
bottom: -10px;
z-index: 1;
}

View file

@ -0,0 +1,47 @@
import React, { FC, useMemo, useState } from 'react';
import cn from 'classnames/bind';
import * as dayjs from 'dayjs';
import ScheduleSlot from 'components/ScheduleSlot/ScheduleSlot';
import Text from 'components/Text/Text';
import styles from './ScheduleTimeline.module.css';
const cx = cn.bind(styles);
interface ScheduleSlotState {}
interface ScheduleTimelineProps {
layerIndex: number;
rotationIndex: number;
}
const ScheduleTimeline: FC<ScheduleTimelineProps> = (props) => {
const { layerIndex, rotationIndex, color, slots, label } = props;
return (
<div className={cx('root')}>
{/* <div className={cx('current-time')} />*/}
<div className={cx('slots')}>
{slots.map(({ users, inactive }, slotIndex) => {
return (
<div className={cx('users')}>
{users.map((user, userIndex) => (
<ScheduleSlot
key={userIndex}
color={color}
label={slotIndex === 0 && userIndex === 0 && label}
user={user}
inactive={inactive}
/>
))}
</div>
);
})}
</div>
</div>
);
};
export default ScheduleTimeline;

View file

@ -0,0 +1,39 @@
.root {
width: 220px;
padding: 10px;
}
.oncall-badge {
line-height: 16px;
color: var(--primary-background);
padding: 2px 7px;
border-radius: 4px;
margin-bottom: 10px;
}
.oncall-badge__type_now{
background: #6CCF8E;
}
.oncall-badge__type_inside{
background: #CCCCDC;
}
.oncall-badge__type_outside{
background: rgba(204, 204, 220, 0.4);
}
.hr{
width: 100%;
margin:0 -11px;
}
.times{
display: flex;
flex-direction: column;
}
.icon{
color: #CCCCDC;
}

View file

@ -0,0 +1,127 @@
import React, { FC } from 'react';
import { Icon, Button, HorizontalGroup, VerticalGroup } 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 Line from './img/line.svg';
import styles from './ScheduleUserDetails.module.css';
interface ScheduleUser {
name: string;
avatar: string;
tz: string;
}
interface ScheduleUserDetailsProps {
currentMoment: dayjs.Dayjs;
user: ScheduleUser;
}
const cx = cn.bind(styles);
enum UserOncallStatus {
Now = 'now',
Outside = 'outside',
Inside = 'inside',
}
const userOncallStatusToText = {
[UserOncallStatus.Now]: 'Oncall now',
[UserOncallStatus.Inside]: 'Inside working hours',
[UserOncallStatus.Outside]: 'Outside working hours',
};
const ScheduleUserDetails: FC<ScheduleUserDetailsProps> = (props) => {
const { user, currentMoment } = props;
const userStatus =
Math.random() > 0.66
? UserOncallStatus.Now
: Math.random() > 0.33
? UserOncallStatus.Inside
: UserOncallStatus.Outside;
const userMoment = currentMoment.tz(user.tz);
const userOffset = userMoment.utcOffset();
const userOffsetHours = userOffset / 60;
const userOffsetHoursStr =
userOffsetHours > 0 ? `( +${userOffsetHours} GMT)` : userOffset < 0 ? `( ${userOffsetHours} GMT)` : `(GMT)`;
return (
<div className={cx('root')}>
<VerticalGroup spacing="sm">
<HorizontalGroup justify="space-between">
<Avatar src={user.avatar} size="large" />
<Button variant="secondary">
<HorizontalGroup spacing="sm">
<Icon name="bell" />
Push
</HorizontalGroup>
</Button>
</HorizontalGroup>
<VerticalGroup spacing="sm">
<Text type="primary">{user.name}</Text>
<Text type="secondary">
{`${userMoment.format('DD MMM, HH:mm')}`} {userOffsetHoursStr}
</Text>
<div
className={cx('oncall-badge', {
[`oncall-badge__type_${userStatus}`]: true,
})}
>
{userOncallStatusToText[userStatus]}
</div>
<HorizontalGroup>
<VerticalGroup spacing="sm">
<Text type="primary">Next shift</Text>
<div className={cx('times')}>
<HorizontalGroup>
<img src={Line} />
<VerticalGroup spacing="none">
<Text type="secondary">30 apr, 00:00</Text>
<Text type="secondary">30 apr, 23:59</Text>
</VerticalGroup>
</HorizontalGroup>
</div>
</VerticalGroup>
<VerticalGroup spacing="sm">
<Text type="primary">Last shift</Text>
<div className={cx('times')}>
<HorizontalGroup>
<img src={Line} />
<VerticalGroup spacing="none">
<Text type="secondary">30 apr, 00:00</Text>
<Text type="secondary">30 apr, 23:59</Text>
</VerticalGroup>
</HorizontalGroup>
</div>
</VerticalGroup>
</HorizontalGroup>
</VerticalGroup>
<hr style={{ width: '100%' }} />
<VerticalGroup spacing="sm">
<Text type="primary">Contacts</Text>
<HorizontalGroup spacing="sm">
<Icon className={cx('icon')} name="message" />
<Text type="link">mail@grafana.com</Text>
</HorizontalGroup>
<HorizontalGroup spacing="sm">
<Icon className={cx('icon')} name="slack" />
<Text type="link">@slackid</Text>
</HorizontalGroup>
<HorizontalGroup spacing="sm">
<Icon className={cx('icon')} name="phone" />
<Text type="secondary">+39 555 449 00 00</Text>
</HorizontalGroup>
</VerticalGroup>
</VerticalGroup>
</div>
);
};
export default ScheduleUserDetails;

View file

@ -0,0 +1,5 @@
<svg width="14" height="34" viewBox="0 0 14 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.2207 13.436C6.93327 13.436 7.60465 13.2982 8.23486 13.0225C8.86507 12.7503 9.42008 12.3743 9.8999 11.8945C10.3797 11.4147 10.7557 10.8615 11.0278 10.2349C11.3 9.60465 11.436 8.93148 11.436 8.21533C11.436 7.50277 11.2982 6.83138 11.0225 6.20117C10.7503 5.57096 10.3743 5.01595 9.89453 4.53613C9.41471 4.05632 8.8597 3.68034 8.22949 3.4082C7.59928 3.13607 6.9279 3 6.21533 3C5.50277 3 4.83138 3.13607 4.20117 3.4082C3.57454 3.68034 3.01953 4.05632 2.53613 4.53613C2.05632 5.01595 1.68034 5.57096 1.4082 6.20117C1.13607 6.83138 1 7.50277 1 8.21533C1 8.93148 1.13607 9.60465 1.4082 10.2349C1.68392 10.8615 2.06169 11.4147 2.5415 11.8945C3.02132 12.3743 3.57454 12.7503 4.20117 13.0225C4.83138 13.2982 5.50456 13.436 6.2207 13.436ZM6.2207 12.48C5.62988 12.48 5.07666 12.369 4.56104 12.147C4.04541 11.9285 3.59245 11.6242 3.20215 11.2339C2.81185 10.8436 2.50749 10.3906 2.28906 9.875C2.07064 9.35938 1.96143 8.80615 1.96143 8.21533C1.96143 7.62451 2.07064 7.07129 2.28906 6.55566C2.50749 6.04004 2.81185 5.58708 3.20215 5.19678C3.59245 4.80648 4.04362 4.50212 4.55566 4.28369C5.07129 4.06527 5.62451 3.95605 6.21533 3.95605C6.80615 3.95605 7.35938 4.06527 7.875 4.28369C8.39062 4.50212 8.84359 4.80648 9.23389 5.19678C9.62419 5.58708 9.92855 6.04004 10.147 6.55566C10.369 7.07129 10.48 7.62451 10.48 8.21533C10.4836 8.80615 10.3743 9.35938 10.1523 9.875C9.93392 10.3906 9.62956 10.8436 9.23926 11.2339C8.85254 11.6242 8.39958 11.9285 7.88037 12.147C7.36475 12.369 6.81152 12.48 6.2207 12.48ZM4.98535 9.39697C5.28255 9.69417 5.59229 9.95736 5.91455 10.1865C6.2404 10.4121 6.56266 10.584 6.88135 10.7021C7.20361 10.8167 7.50798 10.8579 7.79443 10.8257C8.08089 10.7935 8.33512 10.6663 8.55713 10.4443C8.57503 10.43 8.59115 10.4139 8.60547 10.396C8.61979 10.3781 8.63411 10.3602 8.64844 10.3423C8.75944 10.2134 8.8221 10.0719 8.83643 9.91797C8.85433 9.764 8.78988 9.63151 8.64307 9.52051C8.56071 9.46322 8.47656 9.40413 8.39062 9.34326C8.30827 9.28239 8.21338 9.21615 8.10596 9.14453C8.00212 9.06934 7.88216 8.98519 7.74609 8.89209C7.5957 8.78825 7.4668 8.74528 7.35938 8.76318C7.25195 8.78109 7.14095 8.84554 7.02637 8.95654L6.82764 9.1499C6.79541 9.18213 6.75781 9.19824 6.71484 9.19824C6.67188 9.19466 6.63607 9.18213 6.60742 9.16064C6.5179 9.10693 6.40332 9.02637 6.26367 8.91895C6.1276 8.80794 5.98796 8.68083 5.84473 8.5376C5.70866 8.39795 5.58333 8.2583 5.46875 8.11865C5.35417 7.979 5.27181 7.86621 5.22168 7.78027C5.20378 7.75163 5.19124 7.71761 5.18408 7.67822C5.1805 7.63883 5.19661 7.60124 5.23242 7.56543L5.43115 7.35596C5.54215 7.23421 5.60661 7.12142 5.62451 7.01758C5.646 6.91374 5.60124 6.78841 5.49023 6.6416L4.86719 5.76074C4.79915 5.66048 4.72038 5.59782 4.63086 5.57275C4.54134 5.54411 4.44287 5.54948 4.33545 5.58887C4.23161 5.62467 4.12419 5.68376 4.01318 5.76611C3.99886 5.77327 3.98633 5.78223 3.97559 5.79297C3.96484 5.80371 3.9541 5.81445 3.94336 5.8252C3.72135 6.0472 3.59424 6.30322 3.56201 6.59326C3.52979 6.87972 3.57096 7.18408 3.68555 7.50635C3.80371 7.82503 3.97559 8.1473 4.20117 8.47314C4.42676 8.79541 4.68815 9.10335 4.98535 9.39697Z" fill="#CCCCDC"/>
<path d="M6.0332 13.0295L6.0332 24.543" stroke="#CCCCDC"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6 31.0005C7.65685 31.0005 9 29.6573 9 28.0005C9 26.3436 7.65685 25.0005 6 25.0005C4.34315 25.0005 3 26.3436 3 28.0005C3 29.6573 4.34315 31.0005 6 31.0005ZM6 32.0005C8.20914 32.0005 10 30.2096 10 28.0005C10 25.7913 8.20914 24.0005 6 24.0005C3.79086 24.0005 2 25.7913 2 28.0005C2 30.2096 3.79086 32.0005 6 32.0005Z" fill="#CCCCDC"/>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

@ -0,0 +1,50 @@
.root {
position: absolute;
display: flex;
z-index: 1;
width: 100%;
top: 0;
bottom: 0;
font-weight: 400;
font-size: 12px;
line-height: 16px;
color: rgba(204, 204, 220, 0.65);
pointer-events: none;
}
.weekday {
width: calc(100% / 7);
display: flex;
flex-direction: column;
justify-content: space-between;
}
.weekday-title {
width: 100%;
text-align: center;
padding-top: 4px;
flex-grow: 1;
}
.weekday:not(:last-child) .weekday-title{
border-right: var(--border-medium)
}
.weekday-times {
width: 100%;
display: flex;
height: 16px;
}
.weekday-time {
width: 50%;
}
.weekday-time-title {
display: inline-block;
transform: translate(-50%, 0);
}
.weekday-time-title__hidden{
visibility: hidden;
}

View file

@ -0,0 +1,65 @@
import React, { FC, useMemo } from 'react';
import cn from 'classnames/bind';
import styles from './TimelineMarks.module.css';
import * as dayjs from 'dayjs';
interface TimelineMarksProps {
hideTimeMarks: boolean;
startMoment: dayjs.Dayjs;
}
const cx = cn.bind(styles);
const TimelineMarks: FC<TimelineMarksProps> = (props) => {
const { hideTimeMarks, startMoment } = props;
const momentsToRender = useMemo(() => {
const hoursToSplit = 12;
const momentsToRender = [];
const jLimit = 24 / hoursToSplit;
for (let i = 0; i < 7; i++) {
const d = dayjs(startMoment).utc().add(i, 'days');
const obj = { moment: d, moments: [] };
for (let j = 0; j < jLimit; j++) {
const m = dayjs(d)
.utc()
.add(j * hoursToSplit, 'hour');
obj.moments.push(m);
}
momentsToRender.push(obj);
}
return momentsToRender;
}, [startMoment]);
return (
<div className={cx('root')}>
{momentsToRender.map((m, i) => {
return (
<div key={i} className={cx('weekday')}>
<div className={cx('weekday-title')}>
{m.moment.format('DD MMM')}
</div>
<div className={cx('weekday-times')}>
{m.moments.map((mm, j) => (
<div key={j} className={cx('weekday-time')}>
<div
className={cx('weekday-time-title', {
'weekday-time-title__hidden': i == 0 && j == 0,
})}
>
{mm.format('HH:mm')}
</div>
</div>
))}
</div>
</div>
);
})}
</div>
);
};
export default TimelineMarks;

View file

@ -0,0 +1,74 @@
import { getRandomTimezone } from '../UsersTimezones/UsersTimezones.helpers';
export const getRandomGroups = () => {
return [
[
{ id: 13, name: 'Maxim Mordasov', tz: 'Europe/Moscow' },
{ id: 2, name: 'Raphael Batyrbaev', tz: 'Europe/Rome' },
],
[
{ id: 5, name: 'Michael Derynck', tz: 'America/Vancouver' },
{ id: 10, name: 'Ildar Iskhakov', tz: 'Asia/Yerevan' },
{ id: 7, name: 'Innokentii Konstantinov', tz: 'Asia/Yerevan' },
],
[
{ id: 5, name: 'Michael Derynck', tz: 'America/Vancouver' },
{ id: 10, name: 'Vadim Stepanov', tz: 'Asia/Yekaterinburg' },
{ id: 7, name: 'Innokentii Konstantinov', tz: 'Asia/Yerevan' },
],
];
};
export const toPlainArray = (groups) => {
let i = 0;
const items = [];
groups.forEach((group, groupIndex) => {
items.push({
index: i++,
key: `group-${groupIndex}`,
type: 'group',
data: { name: `Group ${groupIndex + 1}` },
});
groups[groupIndex].forEach((item, itemIndex) => {
items.push({
index: i++,
key: `item-${groupIndex}-${itemIndex}`,
type: 'item',
data: item,
});
});
});
return items;
};
export const fromPlainArray = (
items,
createNewGroup = false,
deleteEmptyGroups = true,
) => {
const groups = [];
return items
.reduce((memo, item, currentIndex) => {
if (item.type === 'item') {
let lastGroup = memo[memo.length - 1];
if (
!lastGroup ||
(createNewGroup && currentIndex === items.length - 1)
) {
lastGroup = [];
memo.push(lastGroup);
}
lastGroup.push(item.data);
} else {
memo.push([]);
}
return memo;
}, [])
.filter((group) => !deleteEmptyGroups || group.length);
};

View file

@ -0,0 +1,84 @@
.root {
width: 100%;
}
.sortable-helper {
z-index: 1062;
box-shadow: var(--focused-box-shadow);
background: var(--hover-selected-hardcoded) !important;
}
.separator {
font-weight: 400;
font-size: 12px;
line-height: 16px;
text-align: center;
color: rgba(204, 204, 220, 0.4);
margin: 4px 0;
display: flex;
align-items: center;
}
.separator__clickable{
cursor: pointer;
}
.separator::before {
display: block;
content: "";
flex-grow: 1;
border-bottom: 1px solid rgba(204, 204, 220, 0.15);
height: 0;
margin-right: 5px;
}
.separator::after {
display: block;
content: "";
flex-grow: 1;
border-bottom: 1px solid rgba(204, 204, 220, 0.15);
height: 0;
margin-left: 5px;
}
.groups {
width: 100%;
padding: 0;
margin: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 1px;
}
.user {
background: #22252B;
border-radius: 2px;
padding: 6px 10px;
display: flex;
justify-content: space-between;
}
.user:hover{
background: var(--hover-selected-hardcoded);
}
.delete-icon{
display: none;
}
.user:hover .delete-icon{
display: block;
}
.add-user-group {
width: 100%;
text-align: center;
font-weight: 400;
font-size: 12px;
line-height: 16px;
color: var(--secondary-text-color);
cursor: pointer;
}

View file

@ -0,0 +1,94 @@
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { VerticalGroup, HorizontalGroup, IconButton } from '@grafana/ui';
import { arrayMoveImmutable } from 'array-move';
import cn from 'classnames/bind';
import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc';
import Text from 'components/Text/Text';
import { fromPlainArray, getRandomGroups, toPlainArray } from './UserGroups.helpers';
import styles from './UserGroups.module.css';
interface UserGroupsProps {}
const cx = cn.bind(styles);
const DragHandle = () => <IconButton name="draggable" />;
const SortableHandleHoc = SortableHandle(DragHandle);
const SortableItem = SortableElement(({ children }) => children);
const SortableList = SortableContainer(({ items, onAddUserGroup }) => {
return (
<ul className={cx('groups')}>
{items.map((item) =>
item.type === 'item' ? (
<SortableItem key={item.key} index={item.index}>
<li className={cx('user')}>
<div className={cx('user-title')}>
<Text type="primary"> {item.data.name}</Text> <Text type="secondary">({item.data.tz})</Text>
</div>
<div className={cx('user-buttons')}>
<HorizontalGroup>
<IconButton className={cx('delete-icon')} name="trash-alt" />
</HorizontalGroup>
</div>
</li>
</SortableItem>
) : (
<SortableItem key={item.key} index={item.index}>
<li className={cx('separator')}>{item.data.name}</li>
</SortableItem>
)
)}
<SortableItem disabled key="New Group" index={items.length + 1}>
<li onClick={onAddUserGroup} className={cx('separator', { separator__clickable: true })}>
Add user group +
</li>
</SortableItem>
</ul>
);
});
const UserGroups = () => {
const [groups, setGroups] = useState(getRandomGroups());
const handleAddUserGroup = useCallback(() => {
setGroups((oldGroups) => [...oldGroups, []]);
}, [groups]);
const items = useMemo(() => toPlainArray(groups), [groups]);
const onSortEnd = useCallback(
({ oldIndex, newIndex, collection, isKeySorting }) => {
const newPlainArray = arrayMoveImmutable(items, oldIndex, newIndex);
setGroups(fromPlainArray(newPlainArray, newIndex > items.length));
},
[items]
);
return (
<div className={cx('root')}>
<VerticalGroup>
<SortableList
axis="y"
lockAxis="y"
helperClass={cx('sortable-helper')}
items={items}
onSortEnd={onSortEnd}
onAddUserGroup={handleAddUserGroup}
//useDragHandle
/>
{/* <div className={cx('add-user-group')} onClick={handleAddUserGroup}>
Add user group +
</div>*/}
</VerticalGroup>
</div>
);
};
export default UserGroups;

View file

@ -0,0 +1,3 @@
.root {
width: 300px;
}

View file

@ -0,0 +1,60 @@
import React, { FC, useCallback, useMemo } from 'react';
import { Select } from '@grafana/ui';
import cn from 'classnames/bind';
import { get } from 'lodash-es';
import { Timezone } from 'models/timezone/timezone.types';
import { User } from 'models/user/user.types';
import styles from './UserTimezoneSelect.module.css';
interface UserTimezoneSelectProps {
users: User[];
value: Timezone;
onChange: (value: Timezone) => void;
}
const cx = cn.bind(styles);
const UserTimezoneSelect: FC<UserTimezoneSelectProps> = (props) => {
const { users, value, onChange } = props;
const options = useMemo(() => {
return users.reduce((memo, user) => {
let item = memo.find((item) => item.label === user.tz);
if (!item) {
item = { value: user.pk, label: user.tz, imgUrl: user.avatar, description: user.name };
memo.push(item);
} else {
item.description += ', ' + user.name;
// item.imgUrl = undefined;
}
return memo;
}, []);
}, [users]);
const selectValue = useMemo(() => {
const user = users.find((user) => user.tz === value);
return user.pk;
}, [value, users]);
const handleChange = useCallback(
({ value }) => {
const user = users.find((user) => user.pk === value);
onChange(user.tz);
},
[users]
);
return (
<div className={cx('root')}>
<Select value={selectValue} onChange={handleChange} width={100} placeholder="Timezone" options={options} />
</div>
);
};
export default UserTimezoneSelect;

View file

@ -0,0 +1,110 @@
.root {
border: var(--border-medium);
display: flex;
flex-direction: column;
border-radius: 2px;
background: var(--primary-background);
}
.header {
padding: 0 10px;
}
.title {
font-weight: 500;
font-size: 19px;
line-height: 24px;
color: rgba(204, 204, 220, 0.65);
margin: 16px 0;
}
.current-time {
position: absolute;
left: 0;
width: 1px;
background: #fff;
top: 0;
bottom: 0;
z-index: 0;
transition: left 1s linear;
}
@-webkit-keyframes run {
0% {
left: 0;
}
100% {
left: 100%;
}
}
.users {
position: relative;
height: 76px;
}
.user {
position: absolute;
top: 10px;
border: 2px solid #C65210;
transition: left 1s linear;
transform: translate(-50%,0);
z-index: 0;
border-radius: 50%;
}
.time-stripe {
position: relative;
height: 4px;
--color: rgba(61, 113, 217, 0.2);
background: repeating-linear-gradient(
-45deg,
var(--color),
var(--color) 4px,
transparent 4px,
transparent 8px
);
}
.current-user-stripe {
position: absolute;
top: 0;
bottom: 0;
height: 4px;
background: #3D71D9;
border-radius: 2px;
left: calc((3 / 8) * 100%);
right: calc((2 / 8) * 100%);
}
.time-marks {
position: absolute;
top: -24px;
display: flex;
font-weight: 400;
font-size: 14px;
line-height: 20px;
color: rgba(204, 204, 220, 0.65);
width: 100%;
}
.time-mark {
}
.time-mark-text {
display: inline-block;
padding: 0 5px;
}
.time-mark-text__translated {
transform: translate(-50%, 0);
padding: 0;
}
.time-mark:last-child{
position: absolute;
right: 0;
}

View file

@ -0,0 +1,135 @@
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { HorizontalGroup, Tooltip } from '@grafana/ui';
import cn from 'classnames/bind';
import dayjs from 'dayjs';
import Avatar from 'components/Avatar/Avatar';
import ScheduleUserDetails from 'components/ScheduleUserDetails/ScheduleUserDetails';
import Text from 'components/Text/Text';
import { Timezone } from 'models/timezone/timezone.types';
import { User } from 'models/user/user.types';
import styles from './UsersTimezones.module.css';
interface UsersTimezonesProps {
users: User[];
tz: Timezone;
onTzChange: (tz: Timezone) => void;
}
const cx = cn.bind(styles);
const hoursToSplit = 3;
const jLimit = 24 / hoursToSplit;
const UsersTimezones: FC<UsersTimezonesProps> = (props) => {
const { users, tz, onTzChange } = props;
const [count, setCount] = useState<number>(0);
const [currentMoment, setCurrentMoment] = useState<dayjs.Dayjs>(dayjs().tz(tz).startOf('minute'));
const getAvatarClickHandler = useCallback((user) => {
return () => {
onTzChange(user.tz);
};
}, []);
useEffect(() => {
setCurrentMoment(currentMoment.tz(tz).startOf('minute'));
}, [tz]);
/*useInterval(
() => {
setCurrentMoment(currentMoment.add(10, 'minute'));
//setCount(count + 1);
},
// Delay in milliseconds or null to stop it
1000,
);*/
const currentTimeX = useMemo(() => {
const midnight = dayjs().tz(tz).startOf('day');
const diff = currentMoment.diff(midnight, 'minutes');
return (diff / 1440) * 100;
}, [currentMoment, tz]);
const momentsToRender = useMemo(() => {
const momentsToRender = [];
const d = dayjs().utc().startOf('day');
for (let j = 0; j < jLimit; j++) {
const m = dayjs(d).add(j * hoursToSplit, 'hour');
momentsToRender.push(m);
}
return momentsToRender;
}, []);
return (
<div className={cx('root')}>
<div className={cx('header')}>
<HorizontalGroup justify="space-between">
<div className={cx('title')}>Daily team timezones</div>
<div className={cx('timezone-select')}>
<Text type="secondary">
Current timezone: {tz}, local time: {currentMoment.format('HH:mm')}
</Text>
</div>
</HorizontalGroup>
</div>
<div className={cx('users')}>
<div className={cx('current-time')} style={{ left: `${currentTimeX}%` }} />
{users.map((user, index) => {
const userCurrentMoment = dayjs(currentMoment).tz(user.tz);
const diff = userCurrentMoment.diff(userCurrentMoment.startOf('day'), 'minutes');
const userHour = userCurrentMoment.hour();
const x = (diff / 1440) * 100;
return (
<Tooltip
interactive
key={index}
content={<ScheduleUserDetails currentMoment={currentMoment} user={user} />}
>
<div
className={cx('user')}
onClick={getAvatarClickHandler(user)}
style={{
left: `${x}%`,
opacity: userHour >= 9 && userHour < 18 ? 1 : 0.5,
}}
>
<Avatar src={user.avatar} size="large" />
</div>
</Tooltip>
);
})}
</div>
<div className={cx('time-stripe')}>
<div className={cx('current-user-stripe')} />
<div className={cx('time-marks')}>
{momentsToRender.map((mm, index) => (
<div key={index} className={cx('time-mark')} style={{ width: `${100 / jLimit}%` }}>
<span
className={cx('time-mark-text', {
'time-mark-text__translated': index > 0,
})}
>
{mm.format('HH:mm')}
</span>
</div>
))}
<div key={jLimit} className={cx('time-mark')}>
<span className={cx('time-mark-text')}>24:00</span>
</div>
</div>
</div>
</div>
);
};
export default UsersTimezones;

View file

@ -0,0 +1,598 @@
import { concat } from 'lodash-es';
const tzs = [
'Africa/Abidjan',
'Africa/Accra',
'Africa/Addis_Ababa',
'Africa/Algiers',
'Africa/Asmara',
'Africa/Asmera',
'Africa/Bamako',
'Africa/Bangui',
'Africa/Banjul',
'Africa/Bissau',
'Africa/Blantyre',
'Africa/Brazzaville',
'Africa/Bujumbura',
'Africa/Cairo',
'Africa/Casablanca',
'Africa/Ceuta',
'Africa/Conakry',
'Africa/Dakar',
'Africa/Dar_es_Salaam',
'Africa/Djibouti',
'Africa/Douala',
'Africa/El_Aaiun',
'Africa/Freetown',
'Africa/Gaborone',
'Africa/Harare',
'Africa/Johannesburg',
'Africa/Juba',
'Africa/Kampala',
'Africa/Khartoum',
'Africa/Kigali',
'Africa/Kinshasa',
'Africa/Lagos',
'Africa/Libreville',
'Africa/Lome',
'Africa/Luanda',
'Africa/Lubumbashi',
'Africa/Lusaka',
'Africa/Malabo',
'Africa/Maputo',
'Africa/Maseru',
'Africa/Mbabane',
'Africa/Mogadishu',
'Africa/Monrovia',
'Africa/Nairobi',
'Africa/Ndjamena',
'Africa/Niamey',
'Africa/Nouakchott',
'Africa/Ouagadougou',
'Africa/Porto-Novo',
'Africa/Sao_Tome',
'Africa/Timbuktu',
'Africa/Tripoli',
'Africa/Tunis',
'Africa/Windhoek',
'America/Adak',
'America/Anchorage',
'America/Anguilla',
'America/Antigua',
'America/Araguaina',
'America/Argentina/Buenos_Aires',
'America/Argentina/Catamarca',
'America/Argentina/ComodRivadavia',
'America/Argentina/Cordoba',
'America/Argentina/Jujuy',
'America/Argentina/La_Rioja',
'America/Argentina/Mendoza',
'America/Argentina/Rio_Gallegos',
'America/Argentina/Salta',
'America/Argentina/San_Juan',
'America/Argentina/San_Luis',
'America/Argentina/Tucuman',
'America/Argentina/Ushuaia',
'America/Aruba',
'America/Asuncion',
'America/Atikokan',
'America/Atka',
'America/Bahia',
'America/Bahia_Banderas',
'America/Barbados',
'America/Belem',
'America/Belize',
'America/Blanc-Sablon',
'America/Boa_Vista',
'America/Bogota',
'America/Boise',
'America/Buenos_Aires',
'America/Cambridge_Bay',
'America/Campo_Grande',
'America/Cancun',
'America/Caracas',
'America/Catamarca',
'America/Cayenne',
'America/Cayman',
'America/Chicago',
'America/Chihuahua',
'America/Coral_Harbour',
'America/Cordoba',
'America/Costa_Rica',
'America/Creston',
'America/Cuiaba',
'America/Curacao',
'America/Danmarkshavn',
'America/Dawson',
'America/Dawson_Creek',
'America/Denver',
'America/Detroit',
'America/Dominica',
'America/Edmonton',
'America/Eirunepe',
'America/El_Salvador',
'America/Ensenada',
'America/Fort_Nelson',
'America/Fort_Wayne',
'America/Fortaleza',
'America/Glace_Bay',
'America/Godthab',
'America/Goose_Bay',
'America/Grand_Turk',
'America/Grenada',
'America/Guadeloupe',
'America/Guatemala',
'America/Guayaquil',
'America/Guyana',
'America/Halifax',
'America/Havana',
'America/Hermosillo',
'America/Indiana/Indianapolis',
'America/Indiana/Knox',
'America/Indiana/Marengo',
'America/Indiana/Petersburg',
'America/Indiana/Tell_City',
'America/Indiana/Vevay',
'America/Indiana/Vincennes',
'America/Indiana/Winamac',
'America/Indianapolis',
'America/Inuvik',
'America/Iqaluit',
'America/Jamaica',
'America/Jujuy',
'America/Juneau',
'America/Kentucky/Louisville',
'America/Kentucky/Monticello',
'America/Knox_IN',
'America/Kralendijk',
'America/La_Paz',
'America/Lima',
'America/Los_Angeles',
'America/Louisville',
'America/Lower_Princes',
'America/Maceio',
'America/Managua',
'America/Manaus',
'America/Marigot',
'America/Martinique',
'America/Matamoros',
'America/Mazatlan',
'America/Mendoza',
'America/Menominee',
'America/Merida',
'America/Metlakatla',
'America/Mexico_City',
'America/Miquelon',
'America/Moncton',
'America/Monterrey',
'America/Montevideo',
'America/Montreal',
'America/Montserrat',
'America/Nassau',
'America/New_York',
'America/Nipigon',
'America/Nome',
'America/Noronha',
'America/North_Dakota/Beulah',
'America/North_Dakota/Center',
'America/North_Dakota/New_Salem',
'America/Ojinaga',
'America/Panama',
'America/Pangnirtung',
'America/Paramaribo',
'America/Phoenix',
'America/Port-au-Prince',
'America/Port_of_Spain',
'America/Porto_Acre',
'America/Porto_Velho',
'America/Puerto_Rico',
'America/Punta_Arenas',
'America/Rainy_River',
'America/Rankin_Inlet',
'America/Recife',
'America/Regina',
'America/Resolute',
'America/Rio_Branco',
'America/Rosario',
'America/Santa_Isabel',
'America/Santarem',
'America/Santiago',
'America/Santo_Domingo',
'America/Sao_Paulo',
'America/Scoresbysund',
'America/Shiprock',
'America/Sitka',
'America/St_Barthelemy',
'America/St_Johns',
'America/St_Kitts',
'America/St_Lucia',
'America/St_Thomas',
'America/St_Vincent',
'America/Swift_Current',
'America/Tegucigalpa',
'America/Thule',
'America/Thunder_Bay',
'America/Tijuana',
'America/Toronto',
'America/Tortola',
'America/Vancouver',
'America/Virgin',
'America/Whitehorse',
'America/Winnipeg',
'America/Yakutat',
'America/Yellowknife',
'Antarctica/Casey',
'Antarctica/Davis',
'Antarctica/DumontDUrville',
'Antarctica/Macquarie',
'Antarctica/Mawson',
'Antarctica/McMurdo',
'Antarctica/Palmer',
'Antarctica/Rothera',
'Antarctica/South_Pole',
'Antarctica/Syowa',
'Antarctica/Troll',
'Antarctica/Vostok',
'Arctic/Longyearbyen',
'Asia/Aden',
'Asia/Almaty',
'Asia/Amman',
'Asia/Anadyr',
'Asia/Aqtau',
'Asia/Aqtobe',
'Asia/Ashgabat',
'Asia/Ashkhabad',
'Asia/Atyrau',
'Asia/Baghdad',
'Asia/Bahrain',
'Asia/Baku',
'Asia/Bangkok',
'Asia/Barnaul',
'Asia/Beirut',
'Asia/Bishkek',
'Asia/Brunei',
'Asia/Calcutta',
'Asia/Chita',
'Asia/Choibalsan',
'Asia/Chongqing',
'Asia/Chungking',
'Asia/Colombo',
'Asia/Dacca',
'Asia/Damascus',
'Asia/Dhaka',
'Asia/Dili',
'Asia/Dubai',
'Asia/Dushanbe',
'Asia/Famagusta',
'Asia/Gaza',
'Asia/Harbin',
'Asia/Hebron',
'Asia/Ho_Chi_Minh',
'Asia/Hong_Kong',
'Asia/Hovd',
'Asia/Irkutsk',
'Asia/Istanbul',
'Asia/Jakarta',
'Asia/Jayapura',
'Asia/Jerusalem',
'Asia/Kabul',
'Asia/Kamchatka',
'Asia/Karachi',
'Asia/Kashgar',
'Asia/Kathmandu',
'Asia/Katmandu',
'Asia/Khandyga',
'Asia/Kolkata',
'Asia/Krasnoyarsk',
'Asia/Kuala_Lumpur',
'Asia/Kuching',
'Asia/Kuwait',
'Asia/Macao',
'Asia/Macau',
'Asia/Magadan',
'Asia/Makassar',
'Asia/Manila',
'Asia/Muscat',
'Asia/Nicosia',
'Asia/Novokuznetsk',
'Asia/Novosibirsk',
'Asia/Omsk',
'Asia/Oral',
'Asia/Phnom_Penh',
'Asia/Pontianak',
'Asia/Pyongyang',
'Asia/Qatar',
'Asia/Qyzylorda',
'Asia/Rangoon',
'Asia/Riyadh',
'Asia/Saigon',
'Asia/Sakhalin',
'Asia/Samarkand',
'Asia/Seoul',
'Asia/Shanghai',
'Asia/Singapore',
'Asia/Srednekolymsk',
'Asia/Taipei',
'Asia/Tashkent',
'Asia/Tbilisi',
'Asia/Tehran',
'Asia/Tel_Aviv',
'Asia/Thimbu',
'Asia/Thimphu',
'Asia/Tokyo',
'Asia/Tomsk',
'Asia/Ujung_Pandang',
'Asia/Ulaanbaatar',
'Asia/Ulan_Bator',
'Asia/Urumqi',
'Asia/Ust-Nera',
'Asia/Vientiane',
'Asia/Vladivostok',
'Asia/Yakutsk',
'Asia/Yangon',
'Asia/Yekaterinburg',
'Asia/Yerevan',
'Atlantic/Azores',
'Atlantic/Bermuda',
'Atlantic/Canary',
'Atlantic/Cape_Verde',
'Atlantic/Faeroe',
'Atlantic/Faroe',
'Atlantic/Jan_Mayen',
'Atlantic/Madeira',
'Atlantic/Reykjavik',
'Atlantic/South_Georgia',
'Atlantic/St_Helena',
'Atlantic/Stanley',
'Australia/ACT',
'Australia/Adelaide',
'Australia/Brisbane',
'Australia/Broken_Hill',
'Australia/Canberra',
'Australia/Currie',
'Australia/Darwin',
'Australia/Eucla',
'Australia/Hobart',
'Australia/LHI',
'Australia/Lindeman',
'Australia/Lord_Howe',
'Australia/Melbourne',
'Australia/NSW',
'Australia/North',
'Australia/Perth',
'Australia/Queensland',
'Australia/South',
'Australia/Sydney',
'Australia/Tasmania',
'Australia/Victoria',
'Australia/West',
'Australia/Yancowinna',
'Brazil/Acre',
'Brazil/DeNoronha',
'Brazil/East',
'Brazil/West',
'CET',
'CST6CDT',
'Canada/Atlantic',
'Canada/Central',
'Canada/Eastern',
'Canada/Mountain',
'Canada/Newfoundland',
'Canada/Pacific',
'Canada/Saskatchewan',
'Canada/Yukon',
'Chile/Continental',
'Chile/EasterIsland',
'Cuba',
'EET',
'EST',
'EST5EDT',
'Egypt',
'Eire',
'Etc/GMT',
'Etc/GMT+0',
'Etc/GMT+1',
'Etc/GMT+10',
'Etc/GMT+11',
'Etc/GMT+12',
'Etc/GMT+2',
'Etc/GMT+3',
'Etc/GMT+4',
'Etc/GMT+5',
'Etc/GMT+6',
'Etc/GMT+7',
'Etc/GMT+8',
'Etc/GMT+9',
'Etc/GMT-0',
'Etc/GMT-1',
'Etc/GMT-10',
'Etc/GMT-11',
'Etc/GMT-12',
'Etc/GMT-13',
'Etc/GMT-14',
'Etc/GMT-2',
'Etc/GMT-3',
'Etc/GMT-4',
'Etc/GMT-5',
'Etc/GMT-6',
'Etc/GMT-7',
'Etc/GMT-8',
'Etc/GMT-9',
'Etc/GMT0',
'Etc/Greenwich',
'Etc/UCT',
'Etc/UTC',
'Etc/Universal',
'Etc/Zulu',
'Europe/Amsterdam',
'Europe/Andorra',
'Europe/Astrakhan',
'Europe/Athens',
'Europe/Belfast',
'Europe/Belgrade',
'Europe/Berlin',
'Europe/Bratislava',
'Europe/Brussels',
'Europe/Bucharest',
'Europe/Budapest',
'Europe/Busingen',
'Europe/Chisinau',
'Europe/Copenhagen',
'Europe/Dublin',
'Europe/Gibraltar',
'Europe/Guernsey',
'Europe/Helsinki',
'Europe/Isle_of_Man',
'Europe/Istanbul',
'Europe/Jersey',
'Europe/Kaliningrad',
'Europe/Kiev',
'Europe/Kirov',
'Europe/Lisbon',
'Europe/Ljubljana',
'Europe/London',
'Europe/Luxembourg',
'Europe/Madrid',
'Europe/Malta',
'Europe/Mariehamn',
'Europe/Minsk',
'Europe/Monaco',
'Europe/Moscow',
'Europe/Nicosia',
'Europe/Oslo',
'Europe/Paris',
'Europe/Podgorica',
'Europe/Prague',
'Europe/Riga',
'Europe/Rome',
'Europe/Samara',
'Europe/San_Marino',
'Europe/Sarajevo',
'Europe/Saratov',
'Europe/Simferopol',
'Europe/Skopje',
'Europe/Sofia',
'Europe/Stockholm',
'Europe/Tallinn',
'Europe/Tirane',
'Europe/Tiraspol',
'Europe/Ulyanovsk',
'Europe/Uzhgorod',
'Europe/Vaduz',
'Europe/Vatican',
'Europe/Vienna',
'Europe/Vilnius',
'Europe/Volgograd',
'Europe/Warsaw',
'Europe/Zagreb',
'Europe/Zaporozhye',
'Europe/Zurich',
'GB',
'GB-Eire',
'GMT',
'GMT+0',
'GMT-0',
'GMT0',
'Greenwich',
'HST',
'Hongkong',
'Iceland',
'Indian/Antananarivo',
'Indian/Chagos',
'Indian/Christmas',
'Indian/Cocos',
'Indian/Comoro',
'Indian/Kerguelen',
'Indian/Mahe',
'Indian/Maldives',
'Indian/Mauritius',
'Indian/Mayotte',
'Indian/Reunion',
'Iran',
'Israel',
'Jamaica',
'Japan',
'Kwajalein',
'Libya',
'MET',
'MST',
'MST7MDT',
'Mexico/BajaNorte',
'Mexico/BajaSur',
'Mexico/General',
'NZ',
'NZ-CHAT',
'Navajo',
'PRC',
'PST8PDT',
'Pacific/Apia',
'Pacific/Auckland',
'Pacific/Bougainville',
'Pacific/Chatham',
'Pacific/Chuuk',
'Pacific/Easter',
'Pacific/Efate',
'Pacific/Enderbury',
'Pacific/Fakaofo',
'Pacific/Fiji',
'Pacific/Funafuti',
'Pacific/Galapagos',
'Pacific/Gambier',
'Pacific/Guadalcanal',
'Pacific/Guam',
'Pacific/Honolulu',
'Pacific/Johnston',
'Pacific/Kiritimati',
'Pacific/Kosrae',
'Pacific/Kwajalein',
'Pacific/Majuro',
'Pacific/Marquesas',
'Pacific/Midway',
'Pacific/Nauru',
'Pacific/Niue',
'Pacific/Norfolk',
'Pacific/Noumea',
'Pacific/Pago_Pago',
'Pacific/Palau',
'Pacific/Pitcairn',
'Pacific/Pohnpei',
'Pacific/Ponape',
'Pacific/Port_Moresby',
'Pacific/Rarotonga',
'Pacific/Saipan',
'Pacific/Samoa',
'Pacific/Tahiti',
'Pacific/Tarawa',
'Pacific/Tongatapu',
'Pacific/Truk',
'Pacific/Wake',
'Pacific/Wallis',
'Pacific/Yap',
'Poland',
'Portugal',
'ROC',
'ROK',
'Singapore',
'Turkey',
'UCT',
'US/Alaska',
'US/Aleutian',
'US/Arizona',
'US/Central',
'US/East-Indiana',
'US/Eastern',
'US/Hawaii',
'US/Indiana-Starke',
'US/Michigan',
'US/Mountain',
'US/Pacific',
'US/Pacific-New',
'US/Samoa',
'UTC',
'Universal',
'W-SU',
'WET',
'Zulu',
] as const;
export type Timezone = typeof tzs[number];

View file

@ -1,4 +1,5 @@
import { Team } from 'models/team/team.types';
import { Timezone } from 'models/timezone/timezone.types';
import { UserAction } from 'state/userAction';
export enum UserRole {
@ -53,4 +54,5 @@ export interface User {
status?: number;
link?: string;
cloud_connection_status?: number;
tz: Timezone;
}

View file

@ -0,0 +1,653 @@
const tzs = [
'Africa/Abidjan',
'Africa/Accra',
'Africa/Addis_Ababa',
'Africa/Algiers',
'Africa/Asmara',
'Africa/Asmera',
'Africa/Bamako',
'Africa/Bangui',
'Africa/Banjul',
'Africa/Bissau',
'Africa/Blantyre',
'Africa/Brazzaville',
'Africa/Bujumbura',
'Africa/Cairo',
'Africa/Casablanca',
'Africa/Ceuta',
'Africa/Conakry',
'Africa/Dakar',
'Africa/Dar_es_Salaam',
'Africa/Djibouti',
'Africa/Douala',
'Africa/El_Aaiun',
'Africa/Freetown',
'Africa/Gaborone',
'Africa/Harare',
'Africa/Johannesburg',
'Africa/Juba',
'Africa/Kampala',
'Africa/Khartoum',
'Africa/Kigali',
'Africa/Kinshasa',
'Africa/Lagos',
'Africa/Libreville',
'Africa/Lome',
'Africa/Luanda',
'Africa/Lubumbashi',
'Africa/Lusaka',
'Africa/Malabo',
'Africa/Maputo',
'Africa/Maseru',
'Africa/Mbabane',
'Africa/Mogadishu',
'Africa/Monrovia',
'Africa/Nairobi',
'Africa/Ndjamena',
'Africa/Niamey',
'Africa/Nouakchott',
'Africa/Ouagadougou',
'Africa/Porto-Novo',
'Africa/Sao_Tome',
'Africa/Timbuktu',
'Africa/Tripoli',
'Africa/Tunis',
'Africa/Windhoek',
'America/Adak',
'America/Anchorage',
'America/Anguilla',
'America/Antigua',
'America/Araguaina',
'America/Argentina/Buenos_Aires',
'America/Argentina/Catamarca',
'America/Argentina/ComodRivadavia',
'America/Argentina/Cordoba',
'America/Argentina/Jujuy',
'America/Argentina/La_Rioja',
'America/Argentina/Mendoza',
'America/Argentina/Rio_Gallegos',
'America/Argentina/Salta',
'America/Argentina/San_Juan',
'America/Argentina/San_Luis',
'America/Argentina/Tucuman',
'America/Argentina/Ushuaia',
'America/Aruba',
'America/Asuncion',
'America/Atikokan',
'America/Atka',
'America/Bahia',
'America/Bahia_Banderas',
'America/Barbados',
'America/Belem',
'America/Belize',
'America/Blanc-Sablon',
'America/Boa_Vista',
'America/Bogota',
'America/Boise',
'America/Buenos_Aires',
'America/Cambridge_Bay',
'America/Campo_Grande',
'America/Cancun',
'America/Caracas',
'America/Catamarca',
'America/Cayenne',
'America/Cayman',
'America/Chicago',
'America/Chihuahua',
'America/Coral_Harbour',
'America/Cordoba',
'America/Costa_Rica',
'America/Creston',
'America/Cuiaba',
'America/Curacao',
'America/Danmarkshavn',
'America/Dawson',
'America/Dawson_Creek',
'America/Denver',
'America/Detroit',
'America/Dominica',
'America/Edmonton',
'America/Eirunepe',
'America/El_Salvador',
'America/Ensenada',
'America/Fort_Nelson',
'America/Fort_Wayne',
'America/Fortaleza',
'America/Glace_Bay',
'America/Godthab',
'America/Goose_Bay',
'America/Grand_Turk',
'America/Grenada',
'America/Guadeloupe',
'America/Guatemala',
'America/Guayaquil',
'America/Guyana',
'America/Halifax',
'America/Havana',
'America/Hermosillo',
'America/Indiana/Indianapolis',
'America/Indiana/Knox',
'America/Indiana/Marengo',
'America/Indiana/Petersburg',
'America/Indiana/Tell_City',
'America/Indiana/Vevay',
'America/Indiana/Vincennes',
'America/Indiana/Winamac',
'America/Indianapolis',
'America/Inuvik',
'America/Iqaluit',
'America/Jamaica',
'America/Jujuy',
'America/Juneau',
'America/Kentucky/Louisville',
'America/Kentucky/Monticello',
'America/Knox_IN',
'America/Kralendijk',
'America/La_Paz',
'America/Lima',
'America/Los_Angeles',
'America/Louisville',
'America/Lower_Princes',
'America/Maceio',
'America/Managua',
'America/Manaus',
'America/Marigot',
'America/Martinique',
'America/Matamoros',
'America/Mazatlan',
'America/Mendoza',
'America/Menominee',
'America/Merida',
'America/Metlakatla',
'America/Mexico_City',
'America/Miquelon',
'America/Moncton',
'America/Monterrey',
'America/Montevideo',
'America/Montreal',
'America/Montserrat',
'America/Nassau',
'America/New_York',
'America/Nipigon',
'America/Nome',
'America/Noronha',
'America/North_Dakota/Beulah',
'America/North_Dakota/Center',
'America/North_Dakota/New_Salem',
'America/Ojinaga',
'America/Panama',
'America/Pangnirtung',
'America/Paramaribo',
'America/Phoenix',
'America/Port-au-Prince',
'America/Port_of_Spain',
'America/Porto_Acre',
'America/Porto_Velho',
'America/Puerto_Rico',
'America/Punta_Arenas',
'America/Rainy_River',
'America/Rankin_Inlet',
'America/Recife',
'America/Regina',
'America/Resolute',
'America/Rio_Branco',
'America/Rosario',
'America/Santa_Isabel',
'America/Santarem',
'America/Santiago',
'America/Santo_Domingo',
'America/Sao_Paulo',
'America/Scoresbysund',
'America/Shiprock',
'America/Sitka',
'America/St_Barthelemy',
'America/St_Johns',
'America/St_Kitts',
'America/St_Lucia',
'America/St_Thomas',
'America/St_Vincent',
'America/Swift_Current',
'America/Tegucigalpa',
'America/Thule',
'America/Thunder_Bay',
'America/Tijuana',
'America/Toronto',
'America/Tortola',
'America/Vancouver',
'America/Virgin',
'America/Whitehorse',
'America/Winnipeg',
'America/Yakutat',
'America/Yellowknife',
'Antarctica/Casey',
'Antarctica/Davis',
'Antarctica/DumontDUrville',
'Antarctica/Macquarie',
'Antarctica/Mawson',
'Antarctica/McMurdo',
'Antarctica/Palmer',
'Antarctica/Rothera',
'Antarctica/South_Pole',
'Antarctica/Syowa',
'Antarctica/Troll',
'Antarctica/Vostok',
'Arctic/Longyearbyen',
'Asia/Aden',
'Asia/Almaty',
'Asia/Amman',
'Asia/Anadyr',
'Asia/Aqtau',
'Asia/Aqtobe',
'Asia/Ashgabat',
'Asia/Ashkhabad',
'Asia/Atyrau',
'Asia/Baghdad',
'Asia/Bahrain',
'Asia/Baku',
'Asia/Bangkok',
'Asia/Barnaul',
'Asia/Beirut',
'Asia/Bishkek',
'Asia/Brunei',
'Asia/Calcutta',
'Asia/Chita',
'Asia/Choibalsan',
'Asia/Chongqing',
'Asia/Chungking',
'Asia/Colombo',
'Asia/Dacca',
'Asia/Damascus',
'Asia/Dhaka',
'Asia/Dili',
'Asia/Dubai',
'Asia/Dushanbe',
'Asia/Famagusta',
'Asia/Gaza',
'Asia/Harbin',
'Asia/Hebron',
'Asia/Ho_Chi_Minh',
'Asia/Hong_Kong',
'Asia/Hovd',
'Asia/Irkutsk',
'Asia/Istanbul',
'Asia/Jakarta',
'Asia/Jayapura',
'Asia/Jerusalem',
'Asia/Kabul',
'Asia/Kamchatka',
'Asia/Karachi',
'Asia/Kashgar',
'Asia/Kathmandu',
'Asia/Katmandu',
'Asia/Khandyga',
'Asia/Kolkata',
'Asia/Krasnoyarsk',
'Asia/Kuala_Lumpur',
'Asia/Kuching',
'Asia/Kuwait',
'Asia/Macao',
'Asia/Macau',
'Asia/Magadan',
'Asia/Makassar',
'Asia/Manila',
'Asia/Muscat',
'Asia/Nicosia',
'Asia/Novokuznetsk',
'Asia/Novosibirsk',
'Asia/Omsk',
'Asia/Oral',
'Asia/Phnom_Penh',
'Asia/Pontianak',
'Asia/Pyongyang',
'Asia/Qatar',
'Asia/Qyzylorda',
'Asia/Rangoon',
'Asia/Riyadh',
'Asia/Saigon',
'Asia/Sakhalin',
'Asia/Samarkand',
'Asia/Seoul',
'Asia/Shanghai',
'Asia/Singapore',
'Asia/Srednekolymsk',
'Asia/Taipei',
'Asia/Tashkent',
'Asia/Tbilisi',
'Asia/Tehran',
'Asia/Tel_Aviv',
'Asia/Thimbu',
'Asia/Thimphu',
'Asia/Tokyo',
'Asia/Tomsk',
'Asia/Ujung_Pandang',
'Asia/Ulaanbaatar',
'Asia/Ulan_Bator',
'Asia/Urumqi',
'Asia/Ust-Nera',
'Asia/Vientiane',
'Asia/Vladivostok',
'Asia/Yakutsk',
'Asia/Yangon',
'Asia/Yekaterinburg',
'Asia/Yerevan',
'Atlantic/Azores',
'Atlantic/Bermuda',
'Atlantic/Canary',
'Atlantic/Cape_Verde',
'Atlantic/Faeroe',
'Atlantic/Faroe',
'Atlantic/Jan_Mayen',
'Atlantic/Madeira',
'Atlantic/Reykjavik',
'Atlantic/South_Georgia',
'Atlantic/St_Helena',
'Atlantic/Stanley',
'Australia/ACT',
'Australia/Adelaide',
'Australia/Brisbane',
'Australia/Broken_Hill',
'Australia/Canberra',
'Australia/Currie',
'Australia/Darwin',
'Australia/Eucla',
'Australia/Hobart',
'Australia/LHI',
'Australia/Lindeman',
'Australia/Lord_Howe',
'Australia/Melbourne',
'Australia/NSW',
'Australia/North',
'Australia/Perth',
'Australia/Queensland',
'Australia/South',
'Australia/Sydney',
'Australia/Tasmania',
'Australia/Victoria',
'Australia/West',
'Australia/Yancowinna',
'Brazil/Acre',
'Brazil/DeNoronha',
'Brazil/East',
'Brazil/West',
'CET',
'CST6CDT',
'Canada/Atlantic',
'Canada/Central',
'Canada/Eastern',
'Canada/Mountain',
'Canada/Newfoundland',
'Canada/Pacific',
'Canada/Saskatchewan',
'Canada/Yukon',
'Chile/Continental',
'Chile/EasterIsland',
'Cuba',
'EET',
'EST',
'EST5EDT',
'Egypt',
'Eire',
'Etc/GMT',
'Etc/GMT+0',
'Etc/GMT+1',
'Etc/GMT+10',
'Etc/GMT+11',
'Etc/GMT+12',
'Etc/GMT+2',
'Etc/GMT+3',
'Etc/GMT+4',
'Etc/GMT+5',
'Etc/GMT+6',
'Etc/GMT+7',
'Etc/GMT+8',
'Etc/GMT+9',
'Etc/GMT-0',
'Etc/GMT-1',
'Etc/GMT-10',
'Etc/GMT-11',
'Etc/GMT-12',
'Etc/GMT-13',
'Etc/GMT-14',
'Etc/GMT-2',
'Etc/GMT-3',
'Etc/GMT-4',
'Etc/GMT-5',
'Etc/GMT-6',
'Etc/GMT-7',
'Etc/GMT-8',
'Etc/GMT-9',
'Etc/GMT0',
'Etc/Greenwich',
'Etc/UCT',
'Etc/UTC',
'Etc/Universal',
'Etc/Zulu',
'Europe/Amsterdam',
'Europe/Andorra',
'Europe/Astrakhan',
'Europe/Athens',
'Europe/Belfast',
'Europe/Belgrade',
'Europe/Berlin',
'Europe/Bratislava',
'Europe/Brussels',
'Europe/Bucharest',
'Europe/Budapest',
'Europe/Busingen',
'Europe/Chisinau',
'Europe/Copenhagen',
'Europe/Dublin',
'Europe/Gibraltar',
'Europe/Guernsey',
'Europe/Helsinki',
'Europe/Isle_of_Man',
'Europe/Istanbul',
'Europe/Jersey',
'Europe/Kaliningrad',
'Europe/Kiev',
'Europe/Kirov',
'Europe/Lisbon',
'Europe/Ljubljana',
'Europe/London',
'Europe/Luxembourg',
'Europe/Madrid',
'Europe/Malta',
'Europe/Mariehamn',
'Europe/Minsk',
'Europe/Monaco',
'Europe/Moscow',
'Europe/Nicosia',
'Europe/Oslo',
'Europe/Paris',
'Europe/Podgorica',
'Europe/Prague',
'Europe/Riga',
'Europe/Rome',
'Europe/Samara',
'Europe/San_Marino',
'Europe/Sarajevo',
'Europe/Saratov',
'Europe/Simferopol',
'Europe/Skopje',
'Europe/Sofia',
'Europe/Stockholm',
'Europe/Tallinn',
'Europe/Tirane',
'Europe/Tiraspol',
'Europe/Ulyanovsk',
'Europe/Uzhgorod',
'Europe/Vaduz',
'Europe/Vatican',
'Europe/Vienna',
'Europe/Vilnius',
'Europe/Volgograd',
'Europe/Warsaw',
'Europe/Zagreb',
'Europe/Zaporozhye',
'Europe/Zurich',
'GB',
'GB-Eire',
'GMT',
'GMT+0',
'GMT-0',
'GMT0',
'Greenwich',
'HST',
'Hongkong',
'Iceland',
'Indian/Antananarivo',
'Indian/Chagos',
'Indian/Christmas',
'Indian/Cocos',
'Indian/Comoro',
'Indian/Kerguelen',
'Indian/Mahe',
'Indian/Maldives',
'Indian/Mauritius',
'Indian/Mayotte',
'Indian/Reunion',
'Iran',
'Israel',
'Jamaica',
'Japan',
'Kwajalein',
'Libya',
'MET',
'MST',
'MST7MDT',
'Mexico/BajaNorte',
'Mexico/BajaSur',
'Mexico/General',
'NZ',
'NZ-CHAT',
'Navajo',
'PRC',
'PST8PDT',
'Pacific/Apia',
'Pacific/Auckland',
'Pacific/Bougainville',
'Pacific/Chatham',
'Pacific/Chuuk',
'Pacific/Easter',
'Pacific/Efate',
'Pacific/Enderbury',
'Pacific/Fakaofo',
'Pacific/Fiji',
'Pacific/Funafuti',
'Pacific/Galapagos',
'Pacific/Gambier',
'Pacific/Guadalcanal',
'Pacific/Guam',
'Pacific/Honolulu',
'Pacific/Johnston',
'Pacific/Kiritimati',
'Pacific/Kosrae',
'Pacific/Kwajalein',
'Pacific/Majuro',
'Pacific/Marquesas',
'Pacific/Midway',
'Pacific/Nauru',
'Pacific/Niue',
'Pacific/Norfolk',
'Pacific/Noumea',
'Pacific/Pago_Pago',
'Pacific/Palau',
'Pacific/Pitcairn',
'Pacific/Pohnpei',
'Pacific/Ponape',
'Pacific/Port_Moresby',
'Pacific/Rarotonga',
'Pacific/Saipan',
'Pacific/Samoa',
'Pacific/Tahiti',
'Pacific/Tarawa',
'Pacific/Tongatapu',
'Pacific/Truk',
'Pacific/Wake',
'Pacific/Wallis',
'Pacific/Yap',
'Poland',
'Portugal',
'ROC',
'ROK',
'Singapore',
'Turkey',
'UCT',
'US/Alaska',
'US/Aleutian',
'US/Arizona',
'US/Central',
'US/East-Indiana',
'US/Eastern',
'US/Hawaii',
'US/Indiana-Starke',
'US/Michigan',
'US/Mountain',
'US/Pacific',
'US/Pacific-New',
'US/Samoa',
'UTC',
'Universal',
'W-SU',
'WET',
'Zulu',
];
const USERS = [
'Innokentii Konstantinov',
'Ildar Iskhakov',
'Matias Bordese',
'Michael Derynck',
'Vadim Stepanov',
'Matvey Kukuy',
'Yulya Artyukhina',
'Raphael Batyrbaev',
];
function getRandomUser() {
return USERS[Math.floor(Math.random() * USERS.length)];
}
export const getRandomTimezone = () => {
return tzs[Math.floor(Math.random() * tzs.length)];
};
export const getRandomUsers = (count = 5) => {
const users = [];
for (let i = 0; i < count; i++) {
users.push({
//name: getRandomUser(),
pk: i,
name: [
'Matias Bordese',
'Michael Derynck',
'Yulia Shanyrova',
'Maxim Mordasov',
'Vadim Stepanov',
'Ildar Iskhakov',
/* 'Matvey Kukuy',*/
][i],
//avatar: `https://i.pravatar.cc/32?rnd=${Math.random()}`,
avatar: [
'https://avatars.githubusercontent.com/u/260710?v=4',
'https://avatars.githubusercontent.com/u/28077050?s=60&v=4',
'https://avatars.githubusercontent.com/u/20494436?v=4',
'https://avatars.githubusercontent.com/u/3278022?v=4',
'https://avatars.githubusercontent.com/u/20116910?s=60&v=4',
'https://avatars.githubusercontent.com/u/2262529?v=4',
][i],
//tz: getRandomTimezone(),
tz: [
'America/Montevideo',
'America/Vancouver',
'Europe/Amsterdam',
'Europe/Moscow',
'Europe/Moscow',
'Asia/Yerevan',
/*'Asia/Tel_Aviv',*/
][i],
});
}
return users;
};

View file

@ -1,4 +1,27 @@
.root {
margin-top: 24px;
max-width: 1600px;
margin: 0 auto;
margin-top: 24px;
}
.desc{
width: 736px;
}
.users-timezones{
width: 100%;
margin-bottom: 16px;
}
.controls{
width: 100%;
}
.rotations {
display: flex;
flex-direction: column;
gap: 20px;
position: relative;
width: 100%;
}

View file

@ -1,30 +1,173 @@
import React from 'react';
import React, { useMemo } from 'react';
import { Button, HorizontalGroup, VerticalGroup, RadioButtonGroup } from '@grafana/ui';
import cn from 'classnames/bind';
import * as dayjs from 'dayjs';
import { observer } from 'mobx-react';
import Draggable from 'react-draggable';
import PluginLink from 'components/PluginLink/PluginLink';
import GSelect from 'containers/GSelect/GSelect';
import { PRIVATE_CHANNEL_NAME } from 'models/slack_channel/slack_channel.config';
// import Rotations from 'components/Rotations/Rotations';
import Rotations from 'components/Rotations/Rotations';
import ScheduleCounter from 'components/ScheduleCounter/ScheduleCounter';
import ScheduleQuality from 'components/ScheduleQuality/ScheduleQuality';
import Text from 'components/Text/Text';
// import UsersTimezones from 'components/UsersTimezones/UsersTimezones';
import UserTimezoneSelect from 'components/UserTimezoneSelect/UserTimezoneSelect';
import UsersTimezones from 'components/UsersTimezones/UsersTimezones';
import { Timezone } from 'models/timezone/timezone.types';
import { User } from 'models/user/user.types';
import { withMobXProviderContext } from 'state/withStore';
import { getRandomUsers } from './Schedule.helpers';
import styles from './Schedule.module.css';
const cx = cn.bind(styles);
interface SchedulePageProps {}
interface SchedulePageState {}
interface SchedulePageState {
startMoment: dayjs.Dayjs;
schedulePeriodType: string;
renderType: string;
users: User[];
tz: Timezone;
}
@observer
class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState> {
state: SchedulePageState = {
startMoment: dayjs().utc().startOf('week'),
schedulePeriodType: 'week',
renderType: 'timeline',
users: getRandomUsers(),
tz: 'Europe/Moscow',
};
async componentDidMount() {}
componentDidUpdate() {}
render() {
return <div className={cx('root')}>Hello!</div>;
const { startMoment, schedulePeriodType, renderType, users, tz } = this.state;
return (
<div className={cx('root')}>
<VerticalGroup spacing="lg">
<HorizontalGroup justify="space-between">
<HorizontalGroup>
<Text.Title level={3}>Schedule Team 1</Text.Title>
<ScheduleCounter
type="link"
count={5}
tooltipTitle="Used in escalations"
tooltipContent={['machine learning dev', 'query squad critical', 'integrations-synthetics-dev'].join(
'<br/>'
)}
/>
<ScheduleCounter
type="warning"
count={2}
tooltipTitle="Warnings"
tooltipContent="Schedule has unassigned time periods during next 7 days"
/>
</HorizontalGroup>
<HorizontalGroup>
<UserTimezoneSelect value={tz} users={users} onChange={this.handleTimezoneChange} />
<ScheduleQuality quality={0.89} />
</HorizontalGroup>
</HorizontalGroup>
<Text className={cx('desc')} size="small" type="secondary">
On-call Schedules. Use this to distribute notifications among team members you specified in the "Notify
Users from on-call schedule" step in escalation chains.
</Text>
<div className={cx('users-timezones')}>
<UsersTimezones users={users} tz={tz} onTzChange={this.handleTimezoneChange} />
</div>
<div className={cx('controls')}>
<HorizontalGroup justify="space-between">
<HorizontalGroup>
<Button variant="secondary" onClick={this.handleTodayClick}>
Today
</Button>
<Button variant="secondary" onClick={this.handleLeftClick}>
&larr;
</Button>
<Button variant="secondary" onClick={this.handleRightClick}>
&rarr;
</Button>
<div>
{startMoment.format('DD MMM')} - {startMoment.add(6, 'day').format('DD MMM')}
</div>
</HorizontalGroup>
<HorizontalGroup width="auto">
<RadioButtonGroup
options={[
{ label: 'Day', value: 'day' },
{
label: 'Week',
value: 'week',
},
{ label: 'Month', value: 'month' },
{ label: 'Custom', value: 'custom' },
]}
value={schedulePeriodType}
onChange={this.handleShedulePeriodTypeChange}
/>
<RadioButtonGroup
options={[
{ label: 'Timeline', value: 'timeline' },
{
label: 'Grid',
value: 'grid',
},
]}
value={renderType}
onChange={this.handleRenderTypeChange}
/>
</HorizontalGroup>
</HorizontalGroup>
</div>
{/* <div className={'current-time'} />*/}
<div className={cx('rotations')}>
{/*<Rotations startMoment={startMoment} title="Final schedule" />*/}
<Rotations startMoment={startMoment} title="Rotations" />
{/* <Rotations startMoment={startMoment} title="Overrides" />*/}
</div>
</VerticalGroup>
</div>
);
}
handleTimezoneChange = (value: Timezone) => {
this.setState({ tz: value });
};
handleShedulePeriodTypeChange = (value: string) => {
this.setState({ schedulePeriodType: value });
};
handleRenderTypeChange = (value: string) => {
this.setState({ renderType: value });
};
handleTodayClick = () => {
const { startMoment } = this.state;
this.setState({ startMoment: dayjs().utc().startOf('week') });
};
handleLeftClick = () => {
const { startMoment } = this.state;
this.setState({ startMoment: startMoment.add(-7, 'day') });
};
handleRightClick = () => {
const { startMoment } = this.state;
this.setState({ startMoment: startMoment.add(7, 'day') });
};
}
export default withMobXProviderContext(SchedulePage);

View file

@ -45,4 +45,13 @@
--primary-text-link: #6e9fff;
--timeline-icon-background: rgba(70, 76, 84, 1);
--timeline-icon-background-resolution-note: rgba(50, 116, 217, 1);
--focused-box-shadow: rgb(17 18 23) 0px 0px 0px 2px, rgb(61 113 217) 0px 0px 0px 4px;
--border-medium: 1px solid rgba(204, 204, 220, 0.15);
--hover-selected: rgba(204,204,220,0.12);
--hover-selected-hardcoded: #34363d;
--secondary-background-shade: rgba(204, 204, 220, 0.2);;
}

File diff suppressed because it is too large Load diff