render slots with tz offset

This commit is contained in:
Maxim 2022-07-18 11:02:06 +01:00
parent b2f693b835
commit 0174c973e3
14 changed files with 125 additions and 84 deletions

View file

@ -8,6 +8,7 @@ import utc from 'dayjs/plugin/utc';
import RotationForm from 'components/RotationForm/RotationForm';
import TimelineMarks from 'components/TimelineMarks/TimelineMarks';
import Rotation from 'containers/Rotation/Rotation';
import { Timezone } from 'models/timezone/timezone.types';
import { getColor, getLabel, getRandomTimeslots, getRandomUser } from './Rotations.helpers';
@ -18,6 +19,7 @@ const cx = cn.bind(styles);
interface RotationsProps {
title: string;
startMoment: dayjs.Dayjs;
currentTimezone: Timezone;
}
type Layer = {
@ -34,7 +36,7 @@ class Rotations extends Component<RotationsProps, RotationsState> {
};
render() {
const { title, startMoment } = this.props;
const { title, startMoment, currentTimezone } = this.props;
const { layerIdToCreateRotation } = this.state;
const layers = [
@ -82,6 +84,8 @@ class Rotations extends Component<RotationsProps, RotationsState> {
id={`${layerIndex}-${rotationIndex}`}
layerIndex={layerIndex}
rotationIndex={rotationIndex}
startMoment={startMoment}
currentTimezone={currentTimezone}
/>
))}
</div>

View file

@ -17,6 +17,7 @@
display: flex;
flex-direction: column;
gap:1px;
transition: left 500ms ease;
}
.stack > .root {
@ -50,22 +51,8 @@
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
);
font-size: 10px;
font-weight: bold;
}
.details {

View file

@ -1,13 +1,15 @@
import React, { FC } from 'react';
import { HorizontalGroup, VerticalGroup, Icon, Tooltip, VerticalGroup } from '@grafana/ui';
import { HorizontalGroup, Icon, Tooltip, VerticalGroup } from '@grafana/ui';
import cn from 'classnames/bind';
import dayjs from 'dayjs';
import { observer } from 'mobx-react';
import Line from 'components/ScheduleUserDetails/img/line.svg';
import Text from 'components/Text/Text';
import WorkingHours from 'components/WorkingHours/WorkingHours';
import { Shift } from 'models/schedule/schedule.types';
import { Timezone } from 'models/timezone/timezone.types';
import { User } from 'models/user/user.types';
import { useStore } from 'state/useStore';
@ -20,29 +22,29 @@ interface ScheduleSlotProps {
layerIndex: number;
rotationIndex: number;
shift: Shift;
startMoment: dayjs.Dayjs;
currentTimezone: Timezone;
}
const cx = cn.bind(styles);
const ScheduleSlot: FC<ScheduleSlotProps> = observer((props) => {
const { index, layerIndex, rotationIndex, shift } = props;
const { index, layerIndex, rotationIndex, shift, startMoment, currentTimezone } = props;
const { duration, users } = shift;
const isGap = !users.length;
const store = useStore();
const width = duration / (60 * 60 * 24 * 7);
const base = 60 * 60 * 24 * 7;
const label = index === 0 && getLabel(layerIndex, rotationIndex);
const width = duration / base;
return (
<div className={cx('stack')} style={{ width: `${width * 100}%` }}>
<div className={cx('stack')} style={{ width: `${width * 100}%` /*left: `${x * 100}%`*/ }}>
{!isGap ? (
users.map((pk, userIndex) => {
const left = Math.random() * 50;
const right = 100 - (left + 20 + Math.random() * 30);
const label = index === 0 && userIndex == 0 && getLabel(layerIndex, rotationIndex);
const storeUser = store.userStore.items[pk];
const inactive = false;
@ -61,8 +63,9 @@ const ScheduleSlot: FC<ScheduleSlotProps> = observer((props) => {
{storeUser && (
<WorkingHours
className={cx('working-hours')}
timezone={storeUser.timezone}
workingHours={storeUser.working_hours}
// timezone={storeUser.timezone}
timezone={['America/Vancouver', 'Europe/London'][userIndex]}
//workingHours={storeUser.working_hours}
startMoment={shift.start}
duration={shift.duration}
/>

View file

@ -12,6 +12,7 @@
pointer-events: none;
}
.weekday {
width: calc(100% / 7);
display: flex;
@ -48,3 +49,14 @@
.weekday-time-title__hidden{
visibility: hidden;
}
/*
for debug purposes only
*/
.debug-scale {
position: absolute;
top: -6px;
width: 100%;
right: 0;
}

View file

@ -21,12 +21,10 @@ const TimelineMarks: FC<TimelineMarksProps> = (props) => {
const jLimit = 24 / hoursToSplit;
for (let i = 0; i < 7; i++) {
const d = dayjs(startMoment).utc().add(i, 'days');
const d = dayjs(startMoment).add(i, 'days');
const obj = { moment: d, moments: [] };
for (let j = 0; j < jLimit; j++) {
const m = dayjs(d)
.utc()
.add(j * hoursToSplit, 'hour');
const m = dayjs(d).add(j * hoursToSplit, 'hour');
obj.moments.push(m);
}
momentsToRender.push(obj);
@ -34,8 +32,26 @@ const TimelineMarks: FC<TimelineMarksProps> = (props) => {
return momentsToRender;
}, [startMoment]);
const cuts = [];
for (let i = 0; i < 24 * 7; i++) {
cuts.push({});
}
cuts.push({});
return (
<div className={cx('root')}>
<svg version="1.1" width="100%" height="6px" xmlns="http://www.w3.org/2000/svg" className={cx('debug-scale')}>
{cuts.map((cut, index) => (
<line
x1={`${(index * 100) / (24 * 7)}%`}
strokeWidth={1}
y1="0"
x2={`${(index * 100) / (24 * 7)}%`}
y2="6px"
stroke="rgba(204, 204, 220, 0.65)"
/>
))}
</svg>
{momentsToRender.map((m, i) => {
return (
<div key={i} className={cx('weekday')}>
@ -45,7 +61,7 @@ const TimelineMarks: FC<TimelineMarksProps> = (props) => {
<div key={j} className={cx('weekday-time')}>
<div
className={cx('weekday-time-title', {
'weekday-time-title__hidden': i == 0 && j == 0,
'weekday-time-title__hidden': i === 0 && j === 0,
})}
>
{mm.format('HH:mm')}

View file

@ -2,8 +2,10 @@ import React, { FC, useCallback, useMemo } from 'react';
import { Select } from '@grafana/ui';
import cn from 'classnames/bind';
import dayjs from 'dayjs';
import { get } from 'lodash-es';
import { getTzOffsetString } from 'models/timezone/timezone.helpers';
import { Timezone } from 'models/timezone/timezone.types';
import { User } from 'models/user/user.types';
@ -25,7 +27,12 @@ const UserTimezoneSelect: FC<UserTimezoneSelectProps> = (props) => {
let item = memo.find((item) => item.label === user.tz);
if (!item) {
item = { value: user.pk, label: user.tz, imgUrl: user.avatar, description: user.name };
item = {
value: user.pk,
label: `${user.tz} ${getTzOffsetString(dayjs().tz(user.tz))}`,
imgUrl: user.avatar,
description: user.name,
};
memo.push(item);
} else {
item.description += ', ' + user.name;

View file

@ -33,8 +33,6 @@ const WorkingHours: FC<WorkingHoursProps> = (props) => {
className,
} = props;
timezone = dayjs.tz.guess();
const endMoment = startMoment.add(duration, 'seconds');
const workingMoments = useMemo(
@ -42,9 +40,11 @@ const WorkingHours: FC<WorkingHoursProps> = (props) => {
[startMoment, endMoment, workingHours, timezone]
);
const nonWorkingMoments = getNonWorkingMoments(startMoment, endMoment, workingMoments);
/*console.log(
workingMoments.map(({ start, end }) => `${start.diff(startMoment, 'hours')} - ${end.diff(startMoment, 'hours')}`)
);*/
console.log(startMoment.tz(timezone).format('D MMM ddd HH:ss'));
const nonWorkingMoments = getNonWorkingMoments(startMoment, endMoment, workingMoments);
/*console.log(
workingMoments.map(

View file

@ -25,8 +25,13 @@
padding-bottom: 0;
}
.timeline {
/* overflow: hidden; */
}
.slots {
display: flex;
transition: transform 500ms ease;
}
.current-time {

View file

@ -2,12 +2,13 @@ import React, { FC, useMemo, useState, useEffect } from 'react';
import { LoadingPlaceholder } from '@grafana/ui';
import cn from 'classnames/bind';
import * as dayjs from 'dayjs';
import dayjs from 'dayjs';
import { observer } from 'mobx-react';
import ScheduleSlot from 'components/ScheduleSlot/ScheduleSlot';
import Text from 'components/Text/Text';
import { Rotation as RotationType } from 'models/schedule/schedule.types';
import { Timezone } from 'models/timezone/timezone.types';
import { useStore } from 'state/useStore';
import styles from './Rotation.module.css';
@ -21,10 +22,12 @@ interface RotationProps {
layerIndex: number;
rotationIndex: number;
label: string;
startMoment: dayjs.Dayjs;
currentTimezone: Timezone;
}
const Rotation: FC<RotationProps> = observer((props) => {
const { id, layerIndex, rotationIndex, label } = props;
const { id, layerIndex, rotationIndex, label, startMoment, currentTimezone } = props;
const store = useStore();
@ -38,13 +41,18 @@ const Rotation: FC<RotationProps> = observer((props) => {
return <LoadingPlaceholder text="Loading shifts..." />;
}
const base = 60 * 24 * 7; // in minutes
const utcOffset = dayjs().tz(currentTimezone).utcOffset();
const x = utcOffset / base;
const { shifts } = rotation;
return (
<div className={cx('root')}>
{/* <div className={cx('current-time')} />*/}
<div className={cx('timeline')}>
<div className={cx('slots')}>
<div className={cx('slots')} style={{ transform: `translate(${x * 100}%, 0)` }}>
{shifts.map((shift, index) => {
return (
<ScheduleSlot
@ -53,6 +61,8 @@ const Rotation: FC<RotationProps> = observer((props) => {
shift={shift}
layerIndex={layerIndex}
rotationIndex={rotationIndex}
startMoment={startMoment}
currentTimezone={currentTimezone}
/>
);
})}

View file

@ -126,23 +126,22 @@ export class ScheduleStore extends BaseStore {
*/
const users = [
'UQEAACAGQ5JHL',
'UEHYTCX4AMX75',
'U3U8343UTJ91U',
'UTNF7TCGBPADM',
'UWPPUTZHCC9U5',
'UDUG977U8V8AX',
'UNN22BHCXZ6TR',
'UTKBFZH8HM1TF',
'U1DJX6WMFTWY7',
'UPZ7AJPKVJL9K',
'U5WE86241LNEA',
'U9XM1G7KTE3KW',
'UYKS64M6C59XM',
'UFFIRDUFXA6W3',
'UPRMSTP9LCADE',
'UR6TVJWZYV19M',
'UHRMQQ7KETPCS',
];
if (rnd > 0.33) {
return [users[Math.floor(Math.random() * users.length)]];
}
/* if (rnd > 0.33) {
return [users[Math.floor(Math.random() * users.length)], users[Math.floor(Math.random() * users.length)]];
}*/
return [users[Math.floor(Math.random() * users.length)], users[Math.floor(Math.random() * users.length)]];
return ['UPRMSTP9LCADE', 'UHRMQQ7KETPCS'];
return [users[Math.floor(Math.random() * users.length)]];
}
setTimeout(() => {
@ -153,27 +152,17 @@ export class ScheduleStore extends BaseStore {
const startMoment = dayjs(`${from}.000Z`).utc();
const shifts = [];
for (let i = 0; i < 14; i++) {
for (let i = 0; i < 7; i++) {
shifts.push({
start: dayjs(startMoment).add(12 * i, 'hour'),
//duration: (Math.floor(Math.random() * 6) + 10) * 60 * 60,
duration: 12 * 60 * 60,
// start: dayjs(startMoment).add(12 * i, 'hour'),
// duration: (Math.floor(Math.random() * 6) + 10) * 60 * 60,
start: dayjs(startMoment).add(24 * i, 'hour'),
// duration: (Math.floor(Math.random() * 6) + 10) * 60 * 60,
duration: 24 * 60 * 60,
users: getUsers(),
});
}
const a = {
working_hours: {
monday: [{ start: '09:00:00', end: '18:00:00' }],
tuesday: [{ start: '09:00:00', end: '18:00:00' }],
wednesday: [{ start: '09:00:00', end: '18:00:00' }],
thursday: [{ start: '09:00:00', end: '18:00:00' }],
friday: [{ start: '09:00:00', end: '18:00:00' }],
saturday: [],
sunday: [],
},
};
resolve({ id: rotationId, shifts });
}, 500);
});

View file

@ -1,3 +1,5 @@
import dayjs from 'dayjs';
import { GrafanaTeam } from 'models/grafana_team/grafana_team.types';
import { SlackChannel } from 'models/slack_channel/slack_channel.types';
import { User } from 'models/user/user.types';
@ -45,7 +47,7 @@ export interface CreateScheduleExportTokenResponse {
}
export interface Shift {
start: string;
start: dayjs.Dayjs;
duration: number; // in seconds
users: Array<User['pk']>;
}

View file

@ -619,6 +619,7 @@ export const getRandomUsers = (count = 5) => {
//name: getRandomUser(),
pk: i,
name: [
'Hypothetical UTC user',
'Matias Bordese',
'Michael Derynck',
'Yulia Shanyrova',
@ -629,6 +630,7 @@ export const getRandomUsers = (count = 5) => {
][i],
//avatar: `https://i.pravatar.cc/32?rnd=${Math.random()}`,
avatar: [
'https://image.shutterstock.com/image-vector/male-avatar-icon-simple-man-600w-1504887869.jpg',
'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',
@ -638,11 +640,12 @@ export const getRandomUsers = (count = 5) => {
][i],
//tz: getRandomTimezone(),
tz: [
'UTC',
'America/Montevideo',
'America/Vancouver',
'Europe/Amsterdam',
'Europe/Moscow',
'Europe/Moscow',
'Europe/London',
'Asia/Yerevan',
/*'Asia/Tel_Aviv',*/
][i],

View file

@ -5,7 +5,8 @@
}
.header{
position: sticky;
position: sticky; /* TODO check */
width: 100%;
}
.desc{

View file

@ -3,7 +3,7 @@ import React, { useMemo } from 'react';
import { AppRootProps } from '@grafana/data';
import { Button, HorizontalGroup, VerticalGroup, RadioButtonGroup, IconButton, ToolbarButton } from '@grafana/ui';
import cn from 'classnames/bind';
import * as dayjs from 'dayjs';
import dayjs from 'dayjs';
import { observer } from 'mobx-react';
import Draggable from 'react-draggable';
@ -34,17 +34,19 @@ interface SchedulePageState {
schedulePeriodType: string;
renderType: string;
users: User[];
tz: Timezone;
currentTimezone: Timezone;
}
const INITIAL_TIMEZONE = 'UTC';
@observer
class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState> {
state: SchedulePageState = {
startMoment: dayjs().utc().startOf('week'),
startMoment: dayjs().tz(INITIAL_TIMEZONE).startOf('week'),
schedulePeriodType: 'week',
renderType: 'timeline',
users: getRandomUsers(),
tz: 'Europe/Moscow',
currentTimezone: INITIAL_TIMEZONE,
};
async componentDidMount() {
@ -56,7 +58,7 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
componentDidUpdate() {}
render() {
const { startMoment, schedulePeriodType, renderType, users, tz } = this.state;
const { startMoment, schedulePeriodType, renderType, users, currentTimezone } = this.state;
const { query } = this.props;
return (
@ -91,7 +93,7 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
/>
</HorizontalGroup>
<HorizontalGroup>
<UserTimezoneSelect value={tz} users={users} onChange={this.handleTimezoneChange} />
<UserTimezoneSelect value={currentTimezone} users={users} onChange={this.handleTimezoneChange} />
<ScheduleQuality quality={0.89} />
<ToolbarButton icon="copy" tooltip="Copy" />
<ToolbarButton icon="brackets-curly" tooltip="Code" />
@ -106,7 +108,7 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
Users from on-call schedule" step in escalation chains.
</Text>
<div className={cx('users-timezones')}>
<UsersTimezones users={users} tz={tz} onTzChange={this.handleTimezoneChange} />
<UsersTimezones users={users} tz={currentTimezone} onTzChange={this.handleTimezoneChange} />
</div>
<div className={cx('controls')}>
<HorizontalGroup justify="space-between">
@ -155,7 +157,7 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
{/* <div className={'current-time'} />*/}
<div className={cx('rotations')}>
{/*<Rotations startMoment={startMoment} title="Final schedule" />*/}
<Rotations startMoment={startMoment} title="Rotations" />
<Rotations currentTimezone={currentTimezone} startMoment={startMoment} title="Rotations" />
{/* <Rotations startMoment={startMoment} title="Overrides" />*/}
</div>
</VerticalGroup>
@ -164,7 +166,7 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
}
handleTimezoneChange = (value: Timezone) => {
this.setState({ tz: value });
this.setState({ currentTimezone: value, startMoment: dayjs().tz(value).startOf('week') });
};
handleShedulePeriodTypeChange = (value: string) => {
@ -176,9 +178,9 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
};
handleTodayClick = () => {
const { startMoment } = this.state;
const { startMoment, currentTimezone } = this.state;
this.setState({ startMoment: dayjs().utc().startOf('week') });
this.setState({ startMoment: dayjs().tz(currentTimezone).startOf('week') });
};
handleLeftClick = () => {