diff --git a/grafana-plugin/src/components/UserTimezoneSelect/UserTimezoneSelect.tsx b/grafana-plugin/src/components/UserTimezoneSelect/UserTimezoneSelect.tsx index 70c05f3e..00bce0cd 100644 --- a/grafana-plugin/src/components/UserTimezoneSelect/UserTimezoneSelect.tsx +++ b/grafana-plugin/src/components/UserTimezoneSelect/UserTimezoneSelect.tsx @@ -45,14 +45,14 @@ const UserTimezoneSelect: FC = (props) => { const selectValue = useMemo(() => { const user = users.find((user) => user.timezone === value); - return user.pk; + return user?.pk; }, [value, users]); const handleChange = useCallback( ({ value }) => { const user = users.find((user) => user.pk === value); - onChange(user.timezone); + onChange(user?.timezone); }, [users] ); diff --git a/grafana-plugin/src/components/UsersTimezones/UsersTimezones.tsx b/grafana-plugin/src/components/UsersTimezones/UsersTimezones.tsx index fd6957fd..1cf500fd 100644 --- a/grafana-plugin/src/components/UsersTimezones/UsersTimezones.tsx +++ b/grafana-plugin/src/components/UsersTimezones/UsersTimezones.tsx @@ -28,7 +28,7 @@ const UsersTimezones: FC = (props) => { const { users, tz, onTzChange } = props; const [count, setCount] = useState(0); - const [currentMoment, setCurrentMoment] = useState(dayjs().tz(tz).startOf('minute')); + const [currentMoment, setCurrentMoment] = useState(dayjs().tz(tz)); const getAvatarClickHandler = useCallback((user) => { return () => { @@ -74,10 +74,10 @@ const UsersTimezones: FC = (props) => {
Team timezones
- + {/* Current schedule users only - + */}
diff --git a/grafana-plugin/src/containers/Rotation/Rotation.module.css b/grafana-plugin/src/containers/Rotation/Rotation.module.css index 33af60ba..a8edd3bf 100644 --- a/grafana-plugin/src/containers/Rotation/Rotation.module.css +++ b/grafana-plugin/src/containers/Rotation/Rotation.module.css @@ -24,8 +24,7 @@ flex-direction: column; gap: 5px; padding-bottom: 8px; - - /* overflow: hidden; */ + overflow: hidden; } .root:first-child .timeline { diff --git a/grafana-plugin/src/containers/RotationForm/RotationForm.tsx b/grafana-plugin/src/containers/RotationForm/RotationForm.tsx index fd8fff12..f6638fce 100644 --- a/grafana-plugin/src/containers/RotationForm/RotationForm.tsx +++ b/grafana-plugin/src/containers/RotationForm/RotationForm.tsx @@ -1,4 +1,4 @@ -import React, { FC, useCallback, useState } from 'react'; +import React, { FC, useCallback, useEffect, useState } from 'react'; import { dateTime, DateTime } from '@grafana/data'; import { @@ -14,6 +14,7 @@ import { } from '@grafana/ui'; import cn from 'classnames/bind'; import dayjs from 'dayjs'; +import { observer } from 'mobx-react'; import Draggable from 'react-draggable'; import Modal from 'components/Modal/Modal'; @@ -25,7 +26,7 @@ import { Rotation, Schedule, Shift } from 'models/schedule/schedule.types'; import { getTzOffsetString } from 'models/timezone/timezone.helpers'; import { Timezone } from 'models/timezone/timezone.types'; import { User } from 'models/user/user.types'; -import { getUTCString } from 'pages/schedule/Schedule.helpers'; +import { getDateTime, getUTCString } from 'pages/schedule/Schedule.helpers'; import { SelectOption } from 'state/types'; import { useStore } from 'state/useStore'; @@ -45,19 +46,23 @@ interface RotationFormProps { const cx = cn.bind(styles); -const RotationForm: FC = (props) => { +const startOfDay = dayjs().startOf('day'); + +const RotationForm: FC = observer((props) => { const { onHide, onCreate, currentTimezone, scheduleId, onUpdate, layerIndex, shiftId } = props; const [repeatEveryValue, setRepeatEveryValue] = useState(1); const [repeatEveryPeriod, setRepeatEveryPeriod] = useState(0); const [selectedDays, setSelectedDays] = useState([]); - const [shiftStart, setShiftStart] = useState(dateTime('2022-07-26 12:00:00')); - const [shiftEnd, setShiftEnd] = useState(dateTime('2022-07-26 19:00:00')); - const [rotationStart, setRotationStart] = useState(dateTime('2022-07-26 12:00:00')); + const [shiftStart, setShiftStart] = useState(dateTime(startOfDay.format('YYYY-MM-DD HH:mm:ss'))); + const [shiftEnd, setShiftEnd] = useState(dateTime(startOfDay.add(1, 'day').format('YYYY-MM-DD HH:mm:ss'))); + const [rotationStart, setRotationStart] = useState(dateTime(startOfDay.format('YYYY-MM-DD HH:mm:ss'))); const [endLess, setEndless] = useState(true); - const [rotationEnd, setRotationEnd] = useState(dateTime('2022-08-26 12:00:00')); + const [rotationEnd, setRotationEnd] = useState( + dateTime(startOfDay.add(1, 'month').format('YYYY-MM-DD HH:mm:ss')) + ); - const [userGroups, setUserGroups] = useState([['U9XM1G7KTE3KW'], ['UYKS64M6C59XM']]); + const [userGroups, setUserGroups] = useState([[]]); const getUser = (pk: User['pk']) => { return { @@ -77,19 +82,13 @@ const RotationForm: FC = (props) => { const shift = store.scheduleStore.shifts[shiftId]; - const handleCreate = useCallback(() => { - /* console.log( - repeatEveryValue, - repeatEveryPeriod, - selectedDays, - shiftStart, - shiftEnd, - rotationStart, - endLess, - rotationEnd - ); - */ + useEffect(() => { + if (shiftId !== 'new') { + store.scheduleStore.updateOncallShift(shiftId); + } + }, [shiftId]); + const handleCreate = useCallback(() => { const params = { title: 'Rotation ' + Math.floor(Math.random() * 100), rotation_start: getUTCString(rotationStart, currentTimezone), @@ -97,17 +96,23 @@ const RotationForm: FC = (props) => { shift_start: getUTCString(shiftStart, currentTimezone), shift_end: getUTCString(shiftEnd, currentTimezone), rolling_users: userGroups.filter((group) => group.length), + interval: repeatEveryValue, frequency: repeatEveryPeriod, by_day: repeatEveryPeriod === 1 ? selectedDays : null, - priority_level: layerIndex + 1, + priority_level: shiftId === 'new' ? layerIndex + 1 : shift?.priority_level, }; - // console.log('params', params); - - store.scheduleStore.createRotation(scheduleId, false, params).then(() => { - onHide(); - onCreate(); - }); + if (shiftId === 'new') { + store.scheduleStore.createRotation(scheduleId, false, params).then(() => { + onHide(); + onCreate(); + }); + } else { + store.scheduleStore.updateRotation(shiftId, params).then(() => { + onHide(); + onUpdate(); + }); + } }, [ repeatEveryValue, repeatEveryPeriod, @@ -121,6 +126,22 @@ const RotationForm: FC = (props) => { layerIndex, ]); + useEffect(() => { + if (shift) { + setRotationStart(getDateTime(shift.rotation_start)); + setRotationEnd(getDateTime(shift.until)); + setShiftStart(getDateTime(shift.shift_start)); + setShiftEnd(getDateTime(shift.shift_end)); + setEndless(!shift.until); + + setRepeatEveryValue(shift.interval); + setRepeatEveryPeriod(shift.frequency); + setSelectedDays(shift.by_day); + + setUserGroups(shift.rolling_users); + } + }, [shift]); + const handleChangeEndless = useCallback( (event: React.ChangeEvent) => { setEndless(!event.currentTarget.checked); @@ -150,7 +171,12 @@ const RotationForm: FC = (props) => { > - {shiftId === 'new' ? 'New Rotation' : shift?.title} + + + [L{shiftId === 'new' ? layerIndex + 1 : shift?.priority_level}] + {shiftId === 'new' ? 'New Rotation' : shift?.title} + + @@ -176,6 +202,7 @@ const RotationForm: FC = (props) => { { label: '4', value: 4 }, { label: '5', value: 5 }, { label: '6', value: 6 }, + { label: '7', value: 7 }, ]} onChange={handleRepeatEveryValueChange} /> @@ -265,14 +292,14 @@ const RotationForm: FC = (props) => { ); -}; +}); interface DaysSelectorProps { value: string[]; diff --git a/grafana-plugin/src/containers/RotationForm/ScheduleOverrideForm.tsx b/grafana-plugin/src/containers/RotationForm/ScheduleOverrideForm.tsx index c1a94914..f0803672 100644 --- a/grafana-plugin/src/containers/RotationForm/ScheduleOverrideForm.tsx +++ b/grafana-plugin/src/containers/RotationForm/ScheduleOverrideForm.tsx @@ -1,4 +1,4 @@ -import React, { FC, useCallback, useState } from 'react'; +import React, { FC, useCallback, useEffect, useState } from 'react'; import { dateTime, DateTime } from '@grafana/data'; import { @@ -24,7 +24,7 @@ import { Rotation, Schedule, Shift } from 'models/schedule/schedule.types'; import { getTzOffsetString } from 'models/timezone/timezone.helpers'; import { Timezone } from 'models/timezone/timezone.types'; import { User } from 'models/user/user.types'; -import { getUTCString } from 'pages/schedule/Schedule.helpers'; +import { getDateTime, getUTCString } from 'pages/schedule/Schedule.helpers'; import { useStore } from 'state/useStore'; import { RotationCreateData } from './RotationForm.types'; @@ -42,13 +42,13 @@ interface RotationFormProps { const cx = cn.bind(styles); +const startOfDay = dayjs().startOf('day'); + const ScheduleOverrideForm: FC = (props) => { const { onHide, onCreate, currentTimezone, scheduleId, onUpdate, shiftId } = props; const store = useStore(); - const startOfDay = dayjs().startOf('day'); - const [shiftStart, setShiftStart] = useState(dateTime(startOfDay.format('YYYY-MM-DD HH:mm:ss'))); const [shiftEnd, setShiftEnd] = useState( dateTime(startOfDay.add(12, 'hours').format('YYYY-MM-DD HH:mm:ss')) @@ -65,6 +65,21 @@ const ScheduleOverrideForm: FC = (props) => { const shift = store.scheduleStore.shifts[shiftId]; + useEffect(() => { + if (shiftId !== 'new') { + store.scheduleStore.updateOncallShift(shiftId); + } + }, [shiftId]); + + useEffect(() => { + if (shift) { + setShiftStart(getDateTime(shift.shift_start)); + setShiftEnd(getDateTime(shift.shift_end)); + + setUserGroups(shift.rolling_users); + } + }, [shift]); + const handleDeleteClick = useCallback(() => { store.scheduleStore.deleteOncallShift(shiftId).then(() => { onHide(); @@ -73,19 +88,26 @@ const ScheduleOverrideForm: FC = (props) => { }, []); const handleCreate = useCallback(() => { - store.scheduleStore - .createRotation(scheduleId, true, { - title: 'Override ' + Math.floor(Math.random() * 100), - rotation_start: getUTCString(shiftStart, currentTimezone), - shift_start: getUTCString(shiftStart, currentTimezone), - shift_end: getUTCString(shiftEnd, currentTimezone), - rolling_users: userGroups, - frequency: null, - }) - .then(() => { + const params = { + title: 'Override ' + Math.floor(Math.random() * 100), + rotation_start: getUTCString(shiftStart, currentTimezone), + shift_start: getUTCString(shiftStart, currentTimezone), + shift_end: getUTCString(shiftEnd, currentTimezone), + rolling_users: userGroups, + frequency: null, + }; + + if (shiftId === 'new') { + store.scheduleStore.createRotation(scheduleId, true, params).then(() => { onHide(); onCreate(); }); + } else { + store.scheduleStore.updateRotation(shiftId, params).then(() => { + onHide(); + onUpdate(); + }); + } }, [shiftStart, shiftEnd, userGroups]); return ( @@ -142,7 +164,7 @@ const ScheduleOverrideForm: FC = (props) => { Timezone: {getTzOffsetString(dayjs().tz(currentTimezone))} diff --git a/grafana-plugin/src/containers/Rotations/Rotations.module.css b/grafana-plugin/src/containers/Rotations/Rotations.module.css index 6d054716..e9dd92b6 100644 --- a/grafana-plugin/src/containers/Rotations/Rotations.module.css +++ b/grafana-plugin/src/containers/Rotations/Rotations.module.css @@ -11,8 +11,7 @@ top: 0; bottom: 0; z-index: 1; - - /* transition: left 500ms ease; */ + transition: left 500ms ease; } .header { @@ -36,6 +35,10 @@ display: block; } +.rotations { + position: relative; +} + .layer-title { text-align: center; font-weight: 500; @@ -80,4 +83,9 @@ text-align: center; padding: 12px; color: rgba(204, 204, 220, 0.65); + cursor: pointer; +} + +.add-rotations-layer:hover { + background: var(--secondary-background); } diff --git a/grafana-plugin/src/containers/Rotations/Rotations.tsx b/grafana-plugin/src/containers/Rotations/Rotations.tsx index b8c18344..8f67e9f3 100644 --- a/grafana-plugin/src/containers/Rotations/Rotations.tsx +++ b/grafana-plugin/src/containers/Rotations/Rotations.tsx @@ -11,9 +11,9 @@ import Rotation from 'containers/Rotation/Rotation'; import RotationForm from 'containers/RotationForm/RotationForm'; import { RotationCreateData } from 'containers/RotationForm/RotationForm.types'; import { getFromString } from 'models/schedule/schedule.helpers'; -import { Schedule, Shift } from 'models/schedule/schedule.types'; +import { Event, Schedule, Shift } from 'models/schedule/schedule.types'; import { Timezone } from 'models/timezone/timezone.types'; -import { WithStoreProps } from 'state/types'; +import { SelectOption, WithStoreProps } from 'state/types'; import { withMobXProviderContext } from 'state/withStore'; import { getColor, getLabel, getRandomTimeslots, getRandomUser } from './Rotations.helpers'; @@ -31,12 +31,14 @@ interface RotationsProps extends WithStoreProps { onUpdate: () => void; } -type Layer = { - id: string; -}; - interface RotationsState { shiftIdToShowRotationForm?: Shift['id']; + layerIndexToShowRotationForm?: number; +} + +interface Layer { + priority: Shift['priority_level']; + shifts: Array<{ shiftId: Shift['id']; events: Event[] }>; } @observer @@ -47,24 +49,51 @@ class Rotations extends Component { render() { const { scheduleId, startMoment, currentTimezone, onCreate, onUpdate, store, onClick } = this.props; - const { shiftIdToShowRotationForm } = this.state; - - const layers = [ - { id: 1, title: 'Layer 1' }, - /*{ id: 1, title: 'Layer 2' }, - { id: 2, title: 'Layer 3' }, - { id: 3, title: 'Layer 4' }*/ - ]; + const { shiftIdToShowRotationForm, layerIndexToShowRotationForm } = this.state; const base = 7 * 24 * 60; // in minutes const diff = dayjs().tz(currentTimezone).diff(startMoment, 'minutes'); const currentTimeX = diff / base; - const rotations = [{} /* {}*/]; + const currentTimeHidden = currentTimeX < 0 || currentTimeX > 1; const shifts = store.scheduleStore.events[scheduleId]?.['rotation']?.[getFromString(startMoment)]; + const layers: Layer[] | undefined = shifts + ? shifts + .reduce((memo, shift) => { + const storeShift = store.scheduleStore.shifts[shift.shiftId]; + let layer = memo.find((level) => level.priority === storeShift.priority_level); + if (!layer) { + layer = { priority: storeShift.priority_level, shifts: [] }; + memo.push(layer); + } + layer.shifts.push(shift); + + return memo; + }, []) + .sort((a, b) => { + if (a.priority > b.priority) { + return 1; + } + if (a.priority < b.priority) { + return -1; + } + + return 0; + }) + : undefined; + + const options = layers + ? layers.map((layer) => ({ + label: `Layer ${layer.priority}`, + value: layer.priority - 1, + })) + : []; + + options.push({ label: 'New Layer', value: layers?.length || 0 }); + return ( <>
@@ -73,10 +102,7 @@ class Rotations extends Component {
Rotations
({ - label: title, - value: id, - }))} + options={options} onChange={this.handleAddRotation} variant="secondary" size="md" @@ -84,30 +110,36 @@ class Rotations extends Component {
- {shifts && shifts.length ? ( - shifts.map(({ shiftId, events }, layerIndex) => ( -
+ {layers && layers.length ? ( + layers.map((layer) => ( +
- Layer {layerIndex + 1} + Layer {layer.priority}
-
-
- -
- { - this.onRotationClick(shiftId); - }} - events={events} - layerIndex={layerIndex} - rotationIndex={0} - startMoment={startMoment} - currentTimezone={currentTimezone} - /> -
+
+ + {layer.shifts.map(({ shiftId, events }, rotationIndex) => ( +
+ {!currentTimeHidden && ( +
+ )} +
+ { + this.onRotationClick(shiftId); + }} + events={events} + layerIndex={layer.priority - 1} + rotationIndex={rotationIndex} + startMoment={startMoment} + currentTimezone={currentTimezone} + /> +
+
+ ))}
@@ -140,7 +172,12 @@ class Rotations extends Component {
)} -
+
{ + this.handleAddLayer(layers ? layers.length : 0); + }} + > Add rotations layer +
@@ -149,7 +186,7 @@ class Rotations extends Component { { this.setState({ shiftIdToShowRotationForm: undefined }); @@ -168,10 +205,12 @@ class Rotations extends Component { updateEvents = () => {}; - handleAddLayer = () => {}; + handleAddLayer = (layerIndex: number) => { + this.setState({ shiftIdToShowRotationForm: 'new', layerIndexToShowRotationForm: layerIndex }); + }; - handleAddRotation = (option) => { - this.setState({ shiftIdToShowRotationForm: option.value }); + handleAddRotation = (option: SelectOption) => { + this.setState({ shiftIdToShowRotationForm: 'new', layerIndexToShowRotationForm: option.value }); }; } diff --git a/grafana-plugin/src/containers/Rotations/ScheduleFinal.tsx b/grafana-plugin/src/containers/Rotations/ScheduleFinal.tsx index 99f4cb90..1bfcab67 100644 --- a/grafana-plugin/src/containers/Rotations/ScheduleFinal.tsx +++ b/grafana-plugin/src/containers/Rotations/ScheduleFinal.tsx @@ -45,6 +45,8 @@ class ScheduleFinal extends Component 1; + return ( <>
@@ -52,17 +54,17 @@ class ScheduleFinal extends Component
Final schedule
- } placeholder="Search..." value={searchTerm} onChange={this.onSearchTermChangeCallback} - /> + />*/}
)}
-
+ {!currentTimeHidden &&
}
{shifts && shifts.length ? ( diff --git a/grafana-plugin/src/containers/Rotations/ScheduleOverrides.tsx b/grafana-plugin/src/containers/Rotations/ScheduleOverrides.tsx index 7b68e126..37ce474c 100644 --- a/grafana-plugin/src/containers/Rotations/ScheduleOverrides.tsx +++ b/grafana-plugin/src/containers/Rotations/ScheduleOverrides.tsx @@ -48,6 +48,8 @@ class ScheduleOverrides extends Component 1; + return ( <>
@@ -60,7 +62,7 @@ class ScheduleOverrides extends Component
-
+ {!currentTimeHidden &&
}
{shifts && shifts.length ? ( @@ -89,7 +91,9 @@ class ScheduleOverrides extends Component
-
Add override +
+
+ Add override + +
{shiftIdToShowOverrideForm && ( ) { const type = isOverride ? 3 : 2; - return await makeRequest(`/oncall_shifts/`, { + const response = await makeRequest(`/oncall_shifts/`, { data: { type, schedule: scheduleId, ...params }, method: 'POST', - }); + }).catch(this.onApiError); + + this.shifts = { + ...this.shifts, + [response.id]: response, + }; + + return response; } - async updateRotation(rotationId: Rotation['id']) { - return await makeRequest(`/oncall_shifts/`, { - params: { shift_id: rotationId }, - method: 'GET', - }); + async updateRotation(shiftId: Shift['id'], params: Partial) { + const response = await makeRequest(`/oncall_shifts/${shiftId}`, { + data: { ...params }, + method: 'PUT', + }).catch(this.onApiError); + + this.shifts = { + ...this.shifts, + [response.id]: response, + }; + + return response; } async updateRotationMock(rotationId: Rotation['id'], fromString: string, currentTimezone: Timezone) { @@ -256,6 +271,16 @@ export class ScheduleStore extends BaseStore { }; } + @action + async updateOncallShift(shiftId: Shift['id']) { + const response = await makeRequest(`/oncall_shifts/${shiftId}`, {}); + + this.shifts = { + ...this.shifts, + [shiftId]: response, + }; + } + async deleteOncallShift(shiftId: Shift['id']) { return await makeRequest(`/oncall_shifts/${shiftId}`, { method: 'DELETE', @@ -287,6 +312,21 @@ export class ScheduleStore extends BaseStore { } } + shifts.forEach((shift) => { + for (let i = 0; i < shift.events.length; i++) { + const iEvent = shift.events[i]; + + for (let j = i + 1; j < shift.events.length; j++) { + const jEvent = shift.events[j]; + if (iEvent.start === jEvent.start && iEvent.end === jEvent.end) { + iEvent.users.push(...jEvent.users); + jEvent.merged = true; + } + } + shift.events = shift.events.filter((event) => !event.merged); + } + }); + shifts.forEach((shift) => { shift.events = fillGaps(shift.events); }); @@ -305,16 +345,6 @@ export class ScheduleStore extends BaseStore { }; console.log(toJS(this.events)); - - /*this.rotations = { - ...this.rotations, - [rotationId]: { - ...this.rotations[rotationId], - [level]: { - [fromString]: response as Rotation, - }, - }, - };*/ } async updateFrequencyOptions() { diff --git a/grafana-plugin/src/models/schedule/schedule.types.ts b/grafana-plugin/src/models/schedule/schedule.types.ts index 336b934f..6739ada9 100644 --- a/grafana-plugin/src/models/schedule/schedule.types.ts +++ b/grafana-plugin/src/models/schedule/schedule.types.ts @@ -46,10 +46,10 @@ export interface CreateScheduleExportTokenResponse { } export interface Shift { - by_day: null; - frequency: number; + by_day: string[]; + frequency: number | null; id: string; - interval: null; + interval: number; priority_level: number; rolling_users: Array>; rotation_start: string; diff --git a/grafana-plugin/src/models/user/user.helpers.tsx b/grafana-plugin/src/models/user/user.helpers.tsx index bbee6eac..b1b0122f 100644 --- a/grafana-plugin/src/models/user/user.helpers.tsx +++ b/grafana-plugin/src/models/user/user.helpers.tsx @@ -1,8 +1,11 @@ import React from 'react'; import { Tooltip } from '@grafana/ui'; +import dayjs from 'dayjs'; import { pick } from 'lodash-es'; +import { Timezone } from 'models/timezone/timezone.types'; + import { User, UserRole } from './user.types'; export const getIconType = (role: UserRole) => { @@ -31,6 +34,23 @@ export const getRole = (role: UserRole) => { } }; +export const getTimezone = (user: User) => { + const tzByName = { + 'Hello Oncall': 'UTC', + 'Matías Bordese': 'America/Montevideo', + 'Michael Derynck': 'America/Vancouver', + 'Yulia Shanyrova': 'Europe/Amsterdam', + 'Maxim Mordasov': 'Europe/Moscow', + 'Vadim Stepanov': 'Europe/London', + 'Ildar Iskhakov': 'Asia/Yerevan', + 'Raphael Batyrbaev': 'Europe/Rome', + 'Innokentii Konstantinov': 'Asia/Singapore', + 'Matvey Kukuy': 'Asia/Tel_Aviv', + }; + + return user.timezone || tzByName[user.username] || dayjs.tz.guess(); +}; + export const getUserNotificationsSummary = (user: User) => { if (!user) { return null; diff --git a/grafana-plugin/src/models/user/user.ts b/grafana-plugin/src/models/user/user.ts index a86f3068..5c000c8d 100644 --- a/grafana-plugin/src/models/user/user.ts +++ b/grafana-plugin/src/models/user/user.ts @@ -1,5 +1,7 @@ +import dayjs from 'dayjs'; import { get } from 'lodash-es'; import { action, computed, observable } from 'mobx'; +import moment from 'moment-timezone'; import BaseStore from 'models/base_store'; import { NotificationPolicyType } from 'models/notification_policy'; @@ -9,7 +11,7 @@ import { Mixpanel } from 'services/mixpanel'; import { RootStore } from 'state'; import { move } from 'state/helpers'; -import { prepareForUpdate } from './user.helpers'; +import { getTimezone, prepareForUpdate } from './user.helpers'; import { User } from './user.types'; export class UserStore extends BaseStore { @@ -54,7 +56,7 @@ export class UserStore extends BaseStore { this.items = { ...this.items, - [user.pk]: { ...user, timezone: this.rootStore.currentTimezone }, + [user.pk]: { ...user, timezone: getTimezone(user) }, }; this.currentUserPk = user.pk; @@ -100,27 +102,7 @@ export class UserStore extends BaseStore { ...acc, [item.pk]: { ...item, - timezone: { - 'Hello Oncall': 'UTC', - 'Matías Bordese': 'America/Montevideo', - 'Michael Derynck': 'America/Vancouver', - 'Yulia Shanyrova': 'Europe/Amsterdam', - 'Maxim Mordasov': 'Europe/Moscow', - 'Vadim Stepanov': 'Europe/London', - 'Ildar Iskhakov': 'Asia/Yerevan', - 'Raphael Batyrbaev': 'Europe/Rome', - 'Innokentii Konstantinov': 'Asia/Singapore', - /* 'Matvey Kukuy',*/ - }[item.username], - 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: [], - }, + timezone: getTimezone(item), }, }), {} diff --git a/grafana-plugin/src/pages/schedule/Schedule.helpers.ts b/grafana-plugin/src/pages/schedule/Schedule.helpers.ts index 258f8cae..3d0f9fa6 100644 --- a/grafana-plugin/src/pages/schedule/Schedule.helpers.ts +++ b/grafana-plugin/src/pages/schedule/Schedule.helpers.ts @@ -1,4 +1,4 @@ -import { DateTime } from '@grafana/data'; +import { dateTime, DateTime } from '@grafana/data'; import dayjs from 'dayjs'; import { subtract } from 'lodash-es'; @@ -678,3 +678,11 @@ export const getUTCString = (moment: dayjs.Dayjs | DateTime, timezone: Timezone) .subtract(timezoneOffset, 'minutes') .format('YYYY-MM-DDTHH:mm:ss.000Z'); }; + +export const getDateTime = (date: string) => { + const browserTimezone = dayjs.tz.guess(); + + const browserTimezoneOffset = dayjs().tz(browserTimezone).utcOffset(); + + return dateTime(dayjs(date).subtract(browserTimezoneOffset, 'minutes').format('YYYY-MM-DDTHH:mm:ss.000Z')); +}; diff --git a/grafana-plugin/src/pages/schedule/Schedule.tsx b/grafana-plugin/src/pages/schedule/Schedule.tsx index 38ed4218..e9d017b3 100644 --- a/grafana-plugin/src/pages/schedule/Schedule.tsx +++ b/grafana-plugin/src/pages/schedule/Schedule.tsx @@ -1,6 +1,7 @@ import React, { useMemo } from 'react'; import { AppRootProps } from '@grafana/data'; +import { getLocationSrv } from '@grafana/runtime'; import { Button, HorizontalGroup, VerticalGroup, RadioButtonGroup, IconButton, ToolbarButton } from '@grafana/ui'; import cn from 'classnames/bind'; import dayjs from 'dayjs'; @@ -15,6 +16,7 @@ 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'; @@ -66,7 +68,7 @@ class SchedulePage extends React.Component store.scheduleStore.updateItem(id); store.scheduleStore.updateFrequencyOptions(); store.scheduleStore.updateDaysOptions(); - store.scheduleStore.updateOncallShifts(id); + await store.scheduleStore.updateOncallShifts(id); // TODO we should know shifts to render Rotations this.updateEvents(); } @@ -93,7 +95,7 @@ class SchedulePage extends React.Component {schedule?.name} - count={2} tooltipTitle="Warnings" tooltipContent="Schedule has unassigned time periods during next 7 days" - /> + />*/} {users && ( )} - + {/* - - + */} + + +
@@ -150,7 +154,7 @@ class SchedulePage extends React.Component {startMoment.format('DD MMM')} - {startMoment.add(6, 'day').format('DD MMM')}
- + {/* value={renderType} onChange={this.handleRenderTypeChange} /> - + */}
{/*
*/} @@ -257,6 +261,17 @@ class SchedulePage extends React.Component this.setState({ startMoment: startMoment.add(-7, 'day') }, this.updateEvents); }; + handleDelete = () => { + const { + store, + query: { id: scheduleId }, + } = this.props; + + store.scheduleStore.delete(scheduleId).then(() => { + getLocationSrv().update({ query: { page: 'schedules' } }); + }); + }; + handleRightClick = () => { const { startMoment } = this.state; diff --git a/grafana-plugin/src/pages/schedules_NEW/Schedules.module.css b/grafana-plugin/src/pages/schedules_NEW/Schedules.module.css index 2eff6caa..19fd4532 100644 --- a/grafana-plugin/src/pages/schedules_NEW/Schedules.module.css +++ b/grafana-plugin/src/pages/schedules_NEW/Schedules.module.css @@ -11,6 +11,8 @@ margin: 20px 0; } +/* .root .expanded-row { background: var(--secondary-background); } +*/ diff --git a/grafana-plugin/src/pages/schedules_NEW/Schedules.tsx b/grafana-plugin/src/pages/schedules_NEW/Schedules.tsx index 088d5c35..f502a664 100644 --- a/grafana-plugin/src/pages/schedules_NEW/Schedules.tsx +++ b/grafana-plugin/src/pages/schedules_NEW/Schedules.tsx @@ -6,6 +6,7 @@ import cn from 'classnames/bind'; import dayjs from 'dayjs'; import { observer } from 'mobx-react'; +import Avatar from 'components/Avatar/Avatar'; import NewScheduleSelector from 'components/NewScheduleSelector/NewScheduleSelector'; import PluginLink from 'components/PluginLink/PluginLink'; import ScheduleCounter from 'components/ScheduleCounter/ScheduleCounter'; @@ -69,25 +70,25 @@ class SchedulesPage extends React.Component{item.name}; }; - renderUsers = (item: Schedule) => { - return ( - - {/*{item.users.map((user) => ( - - {user.name} - - ))}*/} - - ); + renderOncallNow = (item: Schedule, index: number) => { + if (item.on_call_now?.length > 0) { + return item.on_call_now.map((user, index) => { + return ( + +
+ + {user.username} +
+
+ ); + }); + } + return null; }; renderChatOps = (item: Schedule) => { @@ -259,9 +265,9 @@ class SchedulesPage extends React.Component { return ( - + {/* - + */}