diff --git a/grafana-plugin/src/components/ScheduleSlot/ScheduleSlot.tsx b/grafana-plugin/src/components/ScheduleSlot/ScheduleSlot.tsx index 3c596f61..787d7fa5 100644 --- a/grafana-plugin/src/components/ScheduleSlot/ScheduleSlot.tsx +++ b/grafana-plugin/src/components/ScheduleSlot/ScheduleSlot.tsx @@ -33,7 +33,7 @@ const ScheduleSlot: FC = observer((props) => { const { index, layerIndex, rotationIndex, event, startMoment, currentTimezone, color: propColor } = props; const { users } = event; - const trackMouse = true; + const trackMouse = false; const [mouseX, setMouseX] = useState(0); diff --git a/grafana-plugin/src/containers/Rotation/Rotation.tsx b/grafana-plugin/src/containers/Rotation/Rotation.tsx index b6105e1b..b1aa6e0c 100644 --- a/grafana-plugin/src/containers/Rotation/Rotation.tsx +++ b/grafana-plugin/src/containers/Rotation/Rotation.tsx @@ -3,15 +3,12 @@ import React, { FC, useMemo, useState, useEffect, useRef, useCallback } from 're import { HorizontalGroup, LoadingPlaceholder } from '@grafana/ui'; import cn from 'classnames/bind'; import dayjs from 'dayjs'; -import { observer } from 'mobx-react'; import { CSSTransitionGroup } from 'react-transition-group'; // ES6 import ScheduleSlot from 'components/ScheduleSlot/ScheduleSlot'; -import Text from 'components/Text/Text'; import { getFromString } from 'models/schedule/schedule.helpers'; import { Rotation as RotationType, Schedule, Event } from 'models/schedule/schedule.types'; import { Timezone } from 'models/timezone/timezone.types'; -import { useStore } from 'state/useStore'; import { usePrevious } from 'utils/hooks'; import styles from './Rotation.module.css'; @@ -30,15 +27,13 @@ interface RotationProps { onClick: () => void; } -const Rotation: FC = observer((props) => { +const Rotation: FC = (props) => { const { events, layerIndex, rotationIndex, startMoment, currentTimezone, color, onClick } = props; const [animate, setAnimate] = useState(true); const [width, setWidth] = useState(); const [transparent, setTransparent] = useState(false); - const store = useStore(); - const startMomentString = useMemo(() => getFromString(startMoment), [startMoment]); const prevStartMomentString = usePrevious(startMomentString); @@ -92,7 +87,6 @@ const Rotation: FC = observer((props) => {
{events.map((event, index) => { return ( @@ -110,7 +104,7 @@ const Rotation: FC = observer((props) => { })}
) : ( -
+ ) ) : ( @@ -120,6 +114,10 @@ const Rotation: FC = observer((props) => {
); -}); +}; + +const Empty = () => { + return
; +}; export default Rotation; diff --git a/grafana-plugin/src/containers/RotationForm/RotationForm.tsx b/grafana-plugin/src/containers/RotationForm/RotationForm.tsx index f6638fce..36a18cf8 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, useEffect, useState } from 'react'; +import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { dateTime, DateTime } from '@grafana/data'; import { @@ -22,21 +22,25 @@ import Text from 'components/Text/Text'; import UserGroups from 'components/UserGroups/UserGroups'; import WithConfirm from 'components/WithConfirm/WithConfirm'; import RemoteSelect from 'containers/RemoteSelect/RemoteSelect'; +import { getFromString } from 'models/schedule/schedule.helpers'; 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 { makeRequest } from 'network'; import { getDateTime, getUTCString } from 'pages/schedule/Schedule.helpers'; import { SelectOption } from 'state/types'; import { useStore } from 'state/useStore'; +import { useDebouncedCallback } from 'utils/hooks'; import { RotationCreateData } from './RotationForm.types'; import styles from './RotationForm.module.css'; interface RotationFormProps { - layerIndex: number; + layerPriority: number; onHide: () => void; + startMoment: dayjs.Dayjs; currentTimezone: Timezone; scheduleId: Schedule['id']; shiftId: Shift['id'] | 'new'; @@ -46,10 +50,10 @@ interface RotationFormProps { const cx = cn.bind(styles); -const startOfDay = dayjs().startOf('day'); +const startOfDay = dayjs().startOf('day').add(1, 'day'); const RotationForm: FC = observer((props) => { - const { onHide, onCreate, currentTimezone, scheduleId, onUpdate, layerIndex, shiftId } = props; + const { onHide, onCreate, startMoment, currentTimezone, scheduleId, onUpdate, layerPriority, shiftId } = props; const [repeatEveryValue, setRepeatEveryValue] = useState(1); const [repeatEveryPeriod, setRepeatEveryPeriod] = useState(0); @@ -88,9 +92,8 @@ const RotationForm: FC = observer((props) => { } }, [shiftId]); - const handleCreate = useCallback(() => { - const params = { - title: 'Rotation ' + Math.floor(Math.random() * 100), + const params = useMemo( + () => ({ rotation_start: getUTCString(rotationStart, currentTimezone), until: endLess ? null : getUTCString(rotationEnd, currentTimezone), shift_start: getUTCString(shiftStart, currentTimezone), @@ -99,9 +102,25 @@ const RotationForm: FC = observer((props) => { interval: repeatEveryValue, frequency: repeatEveryPeriod, by_day: repeatEveryPeriod === 1 ? selectedDays : null, - priority_level: shiftId === 'new' ? layerIndex + 1 : shift?.priority_level, - }; + priority_level: shiftId === 'new' ? layerPriority : shift?.priority_level, + }), + [ + rotationStart, + currentTimezone, + rotationEnd, + shiftStart, + shiftEnd, + userGroups, + repeatEveryValue, + repeatEveryPeriod, + selectedDays, + shiftId, + layerPriority, + shift, + ] + ); + const handleCreate = useCallback(() => { if (shiftId === 'new') { store.scheduleStore.createRotation(scheduleId, false, params).then(() => { onHide(); @@ -113,18 +132,13 @@ const RotationForm: FC = observer((props) => { onUpdate(); }); } - }, [ - repeatEveryValue, - repeatEveryPeriod, - selectedDays, - shiftStart, - shiftEnd, - rotationStart, - endLess, - rotationEnd, - userGroups, - layerIndex, - ]); + }, [shiftId, params]); + + const handleChange = useDebouncedCallback(() => { + store.scheduleStore.updateRotationPreview(scheduleId, shiftId, getFromString(startMoment), false, params); + }, 1000); + + useEffect(handleChange, [params]); useEffect(() => { if (shift) { @@ -173,7 +187,7 @@ const RotationForm: FC = observer((props) => { - [L{shiftId === 'new' ? layerIndex + 1 : shift?.priority_level}] + [L{shiftId === 'new' ? layerPriority : shift?.priority_level}] {shiftId === 'new' ? 'New Rotation' : shift?.title} diff --git a/grafana-plugin/src/containers/Rotations/Rotations.tsx b/grafana-plugin/src/containers/Rotations/Rotations.tsx index 595b81ab..02ea59ec 100644 --- a/grafana-plugin/src/containers/Rotations/Rotations.tsx +++ b/grafana-plugin/src/containers/Rotations/Rotations.tsx @@ -4,6 +4,7 @@ import { ValuePicker, IconButton, Icon, HorizontalGroup, Button, LoadingPlacehol import cn from 'classnames/bind'; import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; +import { toJS } from 'mobx'; import { observer } from 'mobx-react'; import TimelineMarks from 'components/TimelineMarks/TimelineMarks'; @@ -11,7 +12,7 @@ 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 { Event, Schedule, Shift } from 'models/schedule/schedule.types'; +import { Event, Layer, Schedule, Shift } from 'models/schedule/schedule.types'; import { Timezone } from 'models/timezone/timezone.types'; import { SelectOption, WithStoreProps } from 'state/types'; import { withMobXProviderContext } from 'state/withStore'; @@ -33,12 +34,7 @@ interface RotationsProps extends WithStoreProps { interface RotationsState { shiftIdToShowRotationForm?: Shift['id']; - layerIndexToShowRotationForm?: number; -} - -interface Layer { - priority: Shift['priority_level']; - shifts: Array<{ shiftId: Shift['id']; events: Event[] }>; + layerPriority?: Layer['priority']; } @observer @@ -49,7 +45,7 @@ class Rotations extends Component { render() { const { scheduleId, startMoment, currentTimezone, onCreate, onUpdate, store, onClick } = this.props; - const { shiftIdToShowRotationForm, layerIndexToShowRotationForm } = this.state; + const { shiftIdToShowRotationForm, layerPriority } = this.state; const base = 7 * 24 * 60; // in minutes const diff = dayjs().tz(currentTimezone).diff(startMoment, 'minutes'); @@ -58,32 +54,12 @@ class Rotations extends Component { const currentTimeHidden = currentTimeX < 0 || currentTimeX > 1; - const shifts = store.scheduleStore.events[scheduleId]?.['rotation']?.[getFromString(startMoment)]; + const storeLayers = store.scheduleStore.events[scheduleId]?.['rotation']?.[getFromString(startMoment)] as Layer[]; - 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; + let layers = storeLayers; + if (store.scheduleStore.rotationPreview) { + layers = [...layers, { priority: 2, shifts: [store.scheduleStore.rotationPreview] }]; + } const options = layers ? layers.map((layer) => ({ @@ -92,7 +68,9 @@ class Rotations extends Component { })) : []; - options.push({ label: 'New Layer', value: layers?.length || 0 }); + const nextPriority = layers && layers.length ? layers[layers.length - 1].priority + 1 : 1; + + options.push({ label: 'New Layer', value: nextPriority }); return ( <> @@ -158,7 +136,7 @@ class Rotations extends Component {
{ - this.handleAddLayer(layers ? layers.length : 0); + this.handleAddLayer(nextPriority); }} events={[]} layerIndex={0} @@ -175,7 +153,7 @@ class Rotations extends Component {
{ - this.handleAddLayer(layers ? layers.length : 0); + this.handleAddLayer(nextPriority); }} > Add rotations layer + @@ -186,11 +164,10 @@ class Rotations extends Component { { - this.setState({ shiftIdToShowRotationForm: undefined }); - }} + onHide={this.handleRotationFormHide} onUpdate={onUpdate} onCreate={onCreate} /> @@ -199,18 +176,23 @@ class Rotations extends Component { ); } + handleRotationFormHide = () => { + const { store } = this.props; + + store.scheduleStore.rotationPreview = undefined; + this.setState({ shiftIdToShowRotationForm: undefined, layerPriority: undefined }); + }; + onRotationClick = (shiftId: Shift['id']) => { this.setState({ shiftIdToShowRotationForm: shiftId }); }; - updateEvents = () => {}; - - handleAddLayer = (layerIndex: number) => { - this.setState({ shiftIdToShowRotationForm: 'new', layerIndexToShowRotationForm: layerIndex }); + handleAddLayer = (layerPriority: number) => { + this.setState({ shiftIdToShowRotationForm: 'new', layerPriority }); }; handleAddRotation = (option: SelectOption) => { - this.setState({ shiftIdToShowRotationForm: 'new', layerIndexToShowRotationForm: option.value }); + this.setState({ shiftIdToShowRotationForm: 'new', layerPriority: option.value }); }; } diff --git a/grafana-plugin/src/containers/Rotations/ScheduleFinal.tsx b/grafana-plugin/src/containers/Rotations/ScheduleFinal.tsx index 1bfcab67..752f4123 100644 --- a/grafana-plugin/src/containers/Rotations/ScheduleFinal.tsx +++ b/grafana-plugin/src/containers/Rotations/ScheduleFinal.tsx @@ -69,23 +69,10 @@ class ScheduleFinal extends Component {shifts && shifts.length ? ( shifts.map(({ shiftId, events }, index) => ( - + )) ) : ( - + )}
diff --git a/grafana-plugin/src/models/schedule/schedule.helpers.ts b/grafana-plugin/src/models/schedule/schedule.helpers.ts index 8e85beb3..963a4057 100644 --- a/grafana-plugin/src/models/schedule/schedule.helpers.ts +++ b/grafana-plugin/src/models/schedule/schedule.helpers.ts @@ -1,6 +1,6 @@ import dayjs from 'dayjs'; -import { Event } from './schedule.types'; +import { Event, Shift } from './schedule.types'; export const getFromString = (moment: dayjs.Dayjs) => { return moment.format('YYYY-MM-DD'); @@ -23,3 +23,24 @@ export const fillGaps = (events: Event[]) => { return newEvents; }; + +export const splitToShiftsAndFillGaps = (events: Event[]) => { + const shifts: Array<{ shiftId: Shift['id']; events: Event[] }> = []; + + for (const [i, event] of events.entries()) { + if (event.shift?.pk) { + let shift = shifts.find((shift) => shift.shiftId === event.shift?.pk); + if (!shift) { + shift = { shiftId: event.shift.pk, events: [] }; + shifts.push(shift); + } + shift.events.push(event); + } + } + + shifts.forEach((shift) => { + shift.events = fillGaps(shift.events); + }); + + return shifts; +}; diff --git a/grafana-plugin/src/models/schedule/schedule.ts b/grafana-plugin/src/models/schedule/schedule.ts index e4db3881..f268b4a7 100644 --- a/grafana-plugin/src/models/schedule/schedule.ts +++ b/grafana-plugin/src/models/schedule/schedule.ts @@ -11,8 +11,8 @@ import { makeRequest } from 'network'; import { RootStore } from 'state'; import { SelectOption } from 'state/types'; -import { fillGaps } from './schedule.helpers'; -import { Events, Rotation, RotationType, Schedule, ScheduleEvent, Shift, Event } from './schedule.types'; +import { fillGaps, splitToShiftsAndFillGaps } from './schedule.helpers'; +import { Events, Rotation, RotationType, Schedule, ScheduleEvent, Shift, Event, Layer } from './schedule.types'; const DEFAULT_FORMAT = 'YYYY-MM-DDTHH:mm:ss'; @@ -67,11 +67,17 @@ export class ScheduleStore extends BaseStore { events: { [scheduleId: string]: { [type: string]: { - [startMoment: string]: Array<{ shiftId: string; events: Event[] }>; + [startMoment: string]: Array<{ shiftId: string; events: Event[] }> | Layer[]; }; }; } = {}; + @observable.shallow + rotationPreview?: { shiftId: Shift['id']; events: Event[] }; + + @observable.shallow + finalPreview?: Array<{ shiftId: Shift['id']; events: Event[] }>; + @observable scheduleToScheduleEvents: { [id: string]: ScheduleEvent[]; @@ -187,6 +193,27 @@ export class ScheduleStore extends BaseStore { return response; } + async updateRotationPreview( + scheduleId: Schedule['id'], + shiftId: Shift['id'] | 'new', + fromString: string, + isOverride: boolean, + params: Partial + ) { + const type = isOverride ? 3 : 2; + + const typeString = isOverride ? 'override' : 'rotation'; + + const response = await makeRequest(`/oncall_shifts/preview/`, { + params: { date: fromString }, + data: { type, schedule: scheduleId, ...params }, + method: 'POST', + }).catch(this.onApiError); + + this.rotationPreview = { shiftId: shiftId, events: fillGaps(response.rotation.filter((event) => !event.is_gap)) }; + this.finalPreview = splitToShiftsAndFillGaps(response.final); + } + async updateRotation(shiftId: Shift['id'], params: Partial) { const response = await makeRequest(`/oncall_shifts/${shiftId}`, { data: { ...params }, @@ -297,21 +324,9 @@ export class ScheduleStore extends BaseStore { method: 'GET', }); - const events = type !== 'final' ? fillGaps(response.events) : response.events; - - const shifts: Array<{ shiftId: Shift['id']; events: Event[] }> = []; - - for (const [i, event] of response.events.entries()) { - if (event.shift?.pk) { - let shift = shifts.find((shift) => shift.shiftId === event.shift?.pk); - if (!shift) { - shift = { shiftId: event.shift.pk, events: [] }; - shifts.push(shift); - } - shift.events.push(event); - } - } + const shifts = splitToShiftsAndFillGaps(response.events); + // merge users on frontend side, we don't need it now /*shifts.forEach((shift) => { for (let i = 0; i < shift.events.length; i++) { const iEvent = shift.events[i]; @@ -327,11 +342,31 @@ export class ScheduleStore extends BaseStore { } });*/ - shifts.forEach((shift) => { - shift.events = fillGaps(shift.events); - }); + const layers: Layer[] | undefined = + type === 'rotation' + ? shifts + .reduce((memo, shift) => { + const storeShift = this.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); - //console.log(type, shifts); + return memo; + }, []) + .sort((a, b) => { + if (a.priority > b.priority) { + return 1; + } + if (a.priority < b.priority) { + return -1; + } + + return 0; + }) + : undefined; this.events = { ...this.events, @@ -339,7 +374,7 @@ export class ScheduleStore extends BaseStore { ...this.events[scheduleId], [type]: { ...this.events[scheduleId]?.[type], - [fromString]: shifts, + [fromString]: layers ? layers : shifts, }, }, }; diff --git a/grafana-plugin/src/models/schedule/schedule.types.ts b/grafana-plugin/src/models/schedule/schedule.types.ts index 6739ada9..6bb2af11 100644 --- a/grafana-plugin/src/models/schedule/schedule.types.ts +++ b/grafana-plugin/src/models/schedule/schedule.types.ts @@ -89,3 +89,8 @@ export interface Events { name: string; type: number; //? } + +export interface Layer { + priority: Shift['priority_level']; + shifts: Array<{ shiftId: Shift['id']; events: Event[] }>; +}