diff --git a/grafana-plugin/src/components/Rotations/Rotations.tsx b/grafana-plugin/src/components/Rotations/Rotations.tsx index c939298b..7593497b 100644 --- a/grafana-plugin/src/components/Rotations/Rotations.tsx +++ b/grafana-plugin/src/components/Rotations/Rotations.tsx @@ -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 { }; 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 { id={`${layerIndex}-${rotationIndex}`} layerIndex={layerIndex} rotationIndex={rotationIndex} + startMoment={startMoment} + currentTimezone={currentTimezone} /> ))} diff --git a/grafana-plugin/src/components/ScheduleSlot/ScheduleSlot.module.css b/grafana-plugin/src/components/ScheduleSlot/ScheduleSlot.module.css index 91f29443..30807864 100644 --- a/grafana-plugin/src/components/ScheduleSlot/ScheduleSlot.module.css +++ b/grafana-plugin/src/components/ScheduleSlot/ScheduleSlot.module.css @@ -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 { diff --git a/grafana-plugin/src/components/ScheduleSlot/ScheduleSlot.tsx b/grafana-plugin/src/components/ScheduleSlot/ScheduleSlot.tsx index 98d0715d..8fea9680 100644 --- a/grafana-plugin/src/components/ScheduleSlot/ScheduleSlot.tsx +++ b/grafana-plugin/src/components/ScheduleSlot/ScheduleSlot.tsx @@ -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 = 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 ( -
+
{!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 = observer((props) => { {storeUser && ( diff --git a/grafana-plugin/src/components/TimelineMarks/TimelineMarks.module.css b/grafana-plugin/src/components/TimelineMarks/TimelineMarks.module.css index b5742c32..0d434a32 100644 --- a/grafana-plugin/src/components/TimelineMarks/TimelineMarks.module.css +++ b/grafana-plugin/src/components/TimelineMarks/TimelineMarks.module.css @@ -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; +} diff --git a/grafana-plugin/src/components/TimelineMarks/TimelineMarks.tsx b/grafana-plugin/src/components/TimelineMarks/TimelineMarks.tsx index c89f8cb6..c237956e 100644 --- a/grafana-plugin/src/components/TimelineMarks/TimelineMarks.tsx +++ b/grafana-plugin/src/components/TimelineMarks/TimelineMarks.tsx @@ -21,12 +21,10 @@ const TimelineMarks: FC = (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 = (props) => { return momentsToRender; }, [startMoment]); + const cuts = []; + for (let i = 0; i < 24 * 7; i++) { + cuts.push({}); + } + cuts.push({}); + return (
+ + {cuts.map((cut, index) => ( + + ))} + {momentsToRender.map((m, i) => { return (
@@ -45,7 +61,7 @@ const TimelineMarks: FC = (props) => {
{mm.format('HH:mm')} diff --git a/grafana-plugin/src/components/UserTimezoneSelect/UserTimezoneSelect.tsx b/grafana-plugin/src/components/UserTimezoneSelect/UserTimezoneSelect.tsx index 4d5a8f5d..0af1d9c8 100644 --- a/grafana-plugin/src/components/UserTimezoneSelect/UserTimezoneSelect.tsx +++ b/grafana-plugin/src/components/UserTimezoneSelect/UserTimezoneSelect.tsx @@ -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 = (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; diff --git a/grafana-plugin/src/components/WorkingHours/WorkingHours.tsx b/grafana-plugin/src/components/WorkingHours/WorkingHours.tsx index 3f729216..3c62021c 100644 --- a/grafana-plugin/src/components/WorkingHours/WorkingHours.tsx +++ b/grafana-plugin/src/components/WorkingHours/WorkingHours.tsx @@ -33,8 +33,6 @@ const WorkingHours: FC = (props) => { className, } = props; - timezone = dayjs.tz.guess(); - const endMoment = startMoment.add(duration, 'seconds'); const workingMoments = useMemo( @@ -42,9 +40,11 @@ const WorkingHours: FC = (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( diff --git a/grafana-plugin/src/containers/Rotation/Rotation.module.css b/grafana-plugin/src/containers/Rotation/Rotation.module.css index 46f8256c..960ec28c 100644 --- a/grafana-plugin/src/containers/Rotation/Rotation.module.css +++ b/grafana-plugin/src/containers/Rotation/Rotation.module.css @@ -25,8 +25,13 @@ padding-bottom: 0; } +.timeline { + /* overflow: hidden; */ +} + .slots { display: flex; + transition: transform 500ms ease; } .current-time { diff --git a/grafana-plugin/src/containers/Rotation/Rotation.tsx b/grafana-plugin/src/containers/Rotation/Rotation.tsx index 1dd719ad..2a8fd222 100644 --- a/grafana-plugin/src/containers/Rotation/Rotation.tsx +++ b/grafana-plugin/src/containers/Rotation/Rotation.tsx @@ -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 = 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 = observer((props) => { return ; } + const base = 60 * 24 * 7; // in minutes + const utcOffset = dayjs().tz(currentTimezone).utcOffset(); + + const x = utcOffset / base; + const { shifts } = rotation; return (
{/*
*/}
-
+
{shifts.map((shift, index) => { return ( = observer((props) => { shift={shift} layerIndex={layerIndex} rotationIndex={rotationIndex} + startMoment={startMoment} + currentTimezone={currentTimezone} /> ); })} diff --git a/grafana-plugin/src/models/schedule/schedule.ts b/grafana-plugin/src/models/schedule/schedule.ts index 79a3b86a..a0fdebfd 100644 --- a/grafana-plugin/src/models/schedule/schedule.ts +++ b/grafana-plugin/src/models/schedule/schedule.ts @@ -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); }); diff --git a/grafana-plugin/src/models/schedule/schedule.types.ts b/grafana-plugin/src/models/schedule/schedule.types.ts index 0c9ffe09..ed252915 100644 --- a/grafana-plugin/src/models/schedule/schedule.types.ts +++ b/grafana-plugin/src/models/schedule/schedule.types.ts @@ -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; } diff --git a/grafana-plugin/src/pages/schedule/Schedule.helpers.ts b/grafana-plugin/src/pages/schedule/Schedule.helpers.ts index fe745de1..9d6e8589 100644 --- a/grafana-plugin/src/pages/schedule/Schedule.helpers.ts +++ b/grafana-plugin/src/pages/schedule/Schedule.helpers.ts @@ -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], diff --git a/grafana-plugin/src/pages/schedule/Schedule.module.css b/grafana-plugin/src/pages/schedule/Schedule.module.css index c4a276b4..229d0314 100644 --- a/grafana-plugin/src/pages/schedule/Schedule.module.css +++ b/grafana-plugin/src/pages/schedule/Schedule.module.css @@ -5,7 +5,8 @@ } .header{ - position: sticky; + position: sticky; /* TODO check */ + width: 100%; } .desc{ diff --git a/grafana-plugin/src/pages/schedule/Schedule.tsx b/grafana-plugin/src/pages/schedule/Schedule.tsx index 56486caf..f8aa5d02 100644 --- a/grafana-plugin/src/pages/schedule/Schedule.tsx +++ b/grafana-plugin/src/pages/schedule/Schedule.tsx @@ -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 { 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 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 /> - + @@ -106,7 +108,7 @@ class SchedulePage extends React.Component Users from on-call schedule" step in escalation chains.
- +
@@ -155,7 +157,7 @@ class SchedulePage extends React.Component {/*
*/}
{/**/} - + {/* */}
@@ -164,7 +166,7 @@ class SchedulePage extends React.Component } 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 }; 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 = () => {