From c3d5bcc2d88d9f018fca38ae76b415e61cc19c52 Mon Sep 17 00:00:00 2001 From: Maxim Date: Thu, 8 Sep 2022 15:42:12 +0300 Subject: [PATCH] use utc offset to group users --- .../UsersTimezones/UsersTimezones.module.css | 1 + .../UsersTimezones/UsersTimezones.tsx | 86 ++++++++++-------- .../src/models/schedule/schedule.ts | 87 +++---------------- .../src/models/user/user.helpers.tsx | 2 +- grafana-plugin/src/models/user/user.ts | 12 ++- .../src/pages/schedule/Schedule.tsx | 11 ++- .../src/pages/schedules_NEW/Schedules.tsx | 4 +- 7 files changed, 86 insertions(+), 117 deletions(-) rename grafana-plugin/src/{components => containers}/UsersTimezones/UsersTimezones.module.css (98%) rename grafana-plugin/src/{components => containers}/UsersTimezones/UsersTimezones.tsx (79%) diff --git a/grafana-plugin/src/components/UsersTimezones/UsersTimezones.module.css b/grafana-plugin/src/containers/UsersTimezones/UsersTimezones.module.css similarity index 98% rename from grafana-plugin/src/components/UsersTimezones/UsersTimezones.module.css rename to grafana-plugin/src/containers/UsersTimezones/UsersTimezones.module.css index 891b27e7..63d1081b 100644 --- a/grafana-plugin/src/components/UsersTimezones/UsersTimezones.module.css +++ b/grafana-plugin/src/containers/UsersTimezones/UsersTimezones.module.css @@ -73,6 +73,7 @@ border-radius: 8px; text-align: center; transition: opacity 200ms ease, left 200ms ease; + pointer-events: none; } .avatar-group_inactive { diff --git a/grafana-plugin/src/components/UsersTimezones/UsersTimezones.tsx b/grafana-plugin/src/containers/UsersTimezones/UsersTimezones.tsx similarity index 79% rename from grafana-plugin/src/components/UsersTimezones/UsersTimezones.tsx rename to grafana-plugin/src/containers/UsersTimezones/UsersTimezones.tsx index 6e1c37e9..22080b8d 100644 --- a/grafana-plugin/src/components/UsersTimezones/UsersTimezones.tsx +++ b/grafana-plugin/src/containers/UsersTimezones/UsersTimezones.tsx @@ -10,11 +10,12 @@ import Text from 'components/Text/Text'; import { IsOncallIcon } from 'icons'; import { Timezone } from 'models/timezone/timezone.types'; import { User } from 'models/user/user.types'; +import { useStore } from 'state/useStore'; import styles from './UsersTimezones.module.css'; interface UsersTimezonesProps { - users: User[]; + userIds: Array; tz: Timezone; onTzChange: (tz: Timezone) => void; onCallNow: Array>; @@ -27,11 +28,26 @@ const hoursToSplit = 3; const jLimit = 24 / hoursToSplit; const UsersTimezones: FC = (props) => { - const { users, tz, onTzChange, onCallNow } = props; + const { userIds, tz, onTzChange, onCallNow } = props; + + const store = useStore(); const [count, setCount] = useState(0); const [currentMoment, setCurrentMoment] = useState(dayjs().tz(tz)); + useEffect(() => { + userIds.forEach((userId) => { + if (!store.userStore.items[userId]) { + store.userStore.updateItem(userId); + } + }); + }, [userIds]); + + const users = useMemo( + () => userIds.map((userId) => store.userStore.items[userId]).filter(Boolean), + [userIds, store.userStore.items] + ); + useEffect(() => { setCurrentMoment(currentMoment.tz(tz).startOf('minute')); }, [tz]); @@ -121,9 +137,10 @@ const UserAvatars = (props: UserAvatarsProps) => { const userGroups = useMemo(() => { return users .reduce((memo, user) => { - let group = memo.find((group) => group.timezone === user.timezone); + const userUtcOffset = dayjs().tz(user.timezone).utcOffset(); + let group = memo.find((group) => group.utcOffset === userUtcOffset); if (!group) { - group = { timezone: user.timezone, users: [] }; + group = { utcOffset: userUtcOffset, users: [] }; memo.push(group); } group.users.push(user); @@ -131,13 +148,10 @@ const UserAvatars = (props: UserAvatarsProps) => { return memo; }, []) .sort((a, b) => { - const aOffset = dayjs().tz(a.timezone).utcOffset(); - const bOffset = dayjs().tz(b.timezone).utcOffset(); - - if (aOffset > bOffset) { + if (a.utcOffset > b.utcOffset) { return 1; } - if (aOffset < bOffset) { + if (a.utcOffset < b.utcOffset) { return -1; } @@ -145,28 +159,22 @@ const UserAvatars = (props: UserAvatarsProps) => { }); }, [users]); - const getAvatarClickHandler = useCallback((timezone: Timezone) => { - return () => { - onTzChange(timezone); - }; - }, []); - - const [activeTimezone, setActiveTimezone] = useState(undefined); + const [activeUtcOffset, setActiveUtcOffset] = useState(undefined); return (
{userGroups.map((group) => { - const userCurrentMoment = dayjs(currentMoment).tz(group.timezone); + const userCurrentMoment = dayjs(currentMoment).tz(group.users[0].timezone); // TODO try using group.utcOffset const diff = userCurrentMoment.diff(userCurrentMoment.startOf('day'), 'minutes'); const xPos = (diff / (60 * 24)) * 100; return ( void; - timezone: Timezone; - onSetActiveTimezone: (timezone: Timezone) => void; - activeTimezone: Timezone; + utcOffset: number; + onSetActiveUtcOffset: (utcOffset: number | undefined) => void; + activeUtcOffset: number; + onTzChange: (timezone: Timezone) => void; onCallNow: Array>; } @@ -198,14 +206,14 @@ const AvatarGroup = (props: AvatarGroupProps) => { users: propsUsers, currentMoment, xPos, - onClick, - timezone, - onSetActiveTimezone, - activeTimezone, + onTzChange, + utcOffset, + onSetActiveUtcOffset, + activeUtcOffset, onCallNow, } = props; - const active = activeTimezone && activeTimezone === timezone; + const active = !isNaN(activeUtcOffset) && activeUtcOffset === utcOffset; const translateLeft = -AVATAR_WIDTH / 2; @@ -225,15 +233,22 @@ const AvatarGroup = (props: AvatarGroupProps) => { }); }, [propsUsers]); + const getAvatarClickHandler = useCallback((timezone: Timezone) => { + return () => { + onTzChange(timezone); + }; + }, []); + const width = active ? users.length * AVATAR_WIDTH + (users.length - 1) * AVATAR_GAP : AVATAR_WIDTH; return (
onSetActiveTimezone(timezone)} - onMouseLeave={() => onSetActiveTimezone(undefined)} + className={cx('avatar-group', { + [`avatar-group_inactive`]: !isNaN(activeUtcOffset) && activeUtcOffset !== utcOffset, + })} + style={{ width: `${width}px`, left: `${xPos}%`, transform: `translate(${translateLeft}px, 0)` }} + onMouseEnter={() => onSetActiveUtcOffset(utcOffset)} + onMouseLeave={() => onSetActiveUtcOffset(undefined)} > {users.map((user, index, array) => { const isOncall = onCallNow.some((onCallUser) => user.pk === onCallUser.pk); @@ -254,6 +269,7 @@ const AvatarGroup = (props: AvatarGroupProps) => { zIndex: array.length - index - 1, /* opacity: userHour >= 9 && userHour < 18 ? 1 : 0.5,*/ }} + onClick={getAvatarClickHandler(user.timezone)} > {isOncall && } diff --git a/grafana-plugin/src/models/schedule/schedule.ts b/grafana-plugin/src/models/schedule/schedule.ts index 41dc1b23..9d127b91 100644 --- a/grafana-plugin/src/models/schedule/schedule.ts +++ b/grafana-plugin/src/models/schedule/schedule.ts @@ -8,6 +8,7 @@ import BaseStore from 'models/base_store'; import { EscalationChain } from 'models/escalation_chain/escalation_chain.types'; import { SlackChannel } from 'models/slack_channel/slack_channel.types'; import { Timezone } from 'models/timezone/timezone.types'; +import { User } from 'models/user/user.types'; import { makeRequest } from 'network'; import { RootStore } from 'state'; import { SelectOption } from 'state/types'; @@ -26,34 +27,6 @@ const DEFAULT_FORMAT = 'YYYY-MM-DDTHH:mm:ss'; let I = 0; -function getUsers() { - const rnd = Math.random(); - /* - - if (rnd > 0.66) { - return []; - } -*/ - - const users = [ - 'U5WE86241LNEA', - 'U9XM1G7KTE3KW', - 'UYKS64M6C59XM', - 'UFFIRDUFXA6W3', - 'UPRMSTP9LCADE', - 'UR6TVJWZYV19M', - 'UHRMQQ7KETPCS', - ]; - - /* if (rnd > 0.33) { - 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)]]; -} - export class ScheduleStore extends BaseStore { @observable searchResult: { [key: string]: Array } = {}; @@ -67,6 +40,9 @@ export class ScheduleStore extends BaseStore { @observable.shallow relatedEscalationChains: { [id: string]: EscalationChain[] } = {}; + @observable.shallow + relatedUsers: { [id: string]: { [key: string]: Event } } = {}; + @observable.shallow rotations: { [id: string]: { @@ -276,55 +252,18 @@ export class ScheduleStore extends BaseStore { return response; }; - async updateRotationMock(rotationId: Rotation['id'], fromString: string, currentTimezone: Timezone) { - if (this.rotations[rotationId]?.[fromString]) { - return; - } - - const response = await new Promise((resolve, reject) => { - setTimeout(() => { - if (!fromString) { - fromString = dayjs().startOf('week').format('YYYY-MM-DDTHH:mm:ss.000Z'); - } - - let startMoment = dayjs(fromString); - const utcOffset = dayjs().tz(currentTimezone).utcOffset(); - - startMoment = startMoment.add(utcOffset, 'minutes'); - //const startMoment = dayjs().utc().startOf('week'); - - const shifts = []; - for (let i = 0; i < 7; i++) { - const shiftDuration = (12 + Math.floor(Math.random() * 12)) * 60 * 60; - const gapDuration = 24 * 60 * 60 - shiftDuration; - - shifts.push({ - pk: I++, - start: startMoment.add(24 * i, 'hour'), - duration: shiftDuration, - users: getUsers(), - }); - - shifts.push({ - pk: I++, - start: startMoment.add(24 * i, 'hour').add(shiftDuration, 'seconds'), - duration: gapDuration, - is_gap: true, - }); - } - - resolve({ id: rotationId, shifts }); - }, 500); + updateRelatedUsers = async (id: Schedule['id']) => { + const { users } = await makeRequest(`/schedules/${id}/next_shifts_per_user`, { + method: 'GET', }); - this.rotations = { - ...this.rotations, - [rotationId]: { - ...this.rotations[rotationId], - [fromString]: response as Rotation, - }, + this.relatedUsers = { + ...this.relatedUsers, + [id]: users, }; - } + + return users; + }; async updateOncallShifts(scheduleId: Schedule['id']) { const { results } = await makeRequest(`/oncall_shifts/`, { diff --git a/grafana-plugin/src/models/user/user.helpers.tsx b/grafana-plugin/src/models/user/user.helpers.tsx index ec729ce0..bc633165 100644 --- a/grafana-plugin/src/models/user/user.helpers.tsx +++ b/grafana-plugin/src/models/user/user.helpers.tsx @@ -48,7 +48,7 @@ export const getTimezone = (user: User) => { 'Matvey Kukuy': 'Asia/Tel_Aviv', }; - return user.timezone || tzByName[user.username] || dayjs.tz.guess(); + return user.timezone || tzByName[user.username] || 'UTC'; }; export const getUserNotificationsSummary = (user: User) => { diff --git a/grafana-plugin/src/models/user/user.ts b/grafana-plugin/src/models/user/user.ts index 35293dc9..b837c62d 100644 --- a/grafana-plugin/src/models/user/user.ts +++ b/grafana-plugin/src/models/user/user.ts @@ -59,6 +59,16 @@ export class UserStore extends BaseStore { [user.pk]: { ...user, timezone: getTimezone(user) }, }; + // TODO comment + if (user.timezone) { + this.update(user.pk, { timezone: 'UTC' }); + } + + // TODO uncomment + /*if (!user.timezone) { + this.update(user.pk, { timezone: dayjs.tz.guess() }); + }*/ + this.currentUserPk = user.pk; } @@ -68,7 +78,7 @@ export class UserStore extends BaseStore { this.items = { ...this.items, - [user.pk]: user, + [user.pk]: { ...user, timezone: getTimezone(user) }, }; } diff --git a/grafana-plugin/src/pages/schedule/Schedule.tsx b/grafana-plugin/src/pages/schedule/Schedule.tsx index c82210d0..0cdbc4da 100644 --- a/grafana-plugin/src/pages/schedule/Schedule.tsx +++ b/grafana-plugin/src/pages/schedule/Schedule.tsx @@ -15,11 +15,11 @@ 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 WithConfirm from 'components/WithConfirm/WithConfirm'; import Rotations from 'containers/Rotations/Rotations'; import ScheduleFinal from 'containers/Rotations/ScheduleFinal'; import ScheduleOverrides from 'containers/Rotations/ScheduleOverrides'; +import UsersTimezones from 'containers/UsersTimezones/UsersTimezones'; import { Timezone } from 'models/timezone/timezone.types'; import { User } from 'models/user/user.types'; import { AppFeature } from 'state/features'; @@ -59,9 +59,9 @@ class SchedulePage extends React.Component const { store } = this.props; const { startMoment } = this.state; - if (!store.hasFeature(AppFeature.WebSchedules)) { + /*if (!store.hasFeature(AppFeature.WebSchedules)) { getLocationSrv().update({ query: { page: 'schedules' } }); - } + }*/ store.userStore.updateItems(); @@ -143,7 +143,9 @@ class SchedulePage extends React.Component
@@ -239,6 +241,7 @@ class SchedulePage extends React.Component const { startMoment } = this.state; store.scheduleStore.updateItem(scheduleId); // to refresh current oncall users + store.scheduleStore.updateRelatedUsers(scheduleId); // to refresh related users return Promise.all([ store.scheduleStore.updateEvents(scheduleId, startMoment, 'rotation'), diff --git a/grafana-plugin/src/pages/schedules_NEW/Schedules.tsx b/grafana-plugin/src/pages/schedules_NEW/Schedules.tsx index 2bceb135..f9a76c89 100644 --- a/grafana-plugin/src/pages/schedules_NEW/Schedules.tsx +++ b/grafana-plugin/src/pages/schedules_NEW/Schedules.tsx @@ -57,9 +57,9 @@ class SchedulesPage extends React.Component