diff --git a/grafana-plugin/src/containers/RotationForm/RotationForm.tsx b/grafana-plugin/src/containers/RotationForm/RotationForm.tsx index daa8d552..15aa59ea 100644 --- a/grafana-plugin/src/containers/RotationForm/RotationForm.tsx +++ b/grafana-plugin/src/containers/RotationForm/RotationForm.tsx @@ -46,6 +46,7 @@ interface RotationFormProps { shiftId: Shift['id'] | 'new'; onCreate: () => void; onUpdate: () => void; + onDelete: () => void; } const cx = cn.bind(styles); @@ -53,7 +54,8 @@ const cx = cn.bind(styles); const startOfDay = dayjs().startOf('day').add(1, 'day'); const RotationForm: FC = observer((props) => { - const { onHide, onCreate, startMoment, currentTimezone, scheduleId, onUpdate, layerPriority, shiftId } = props; + const { onHide, onCreate, startMoment, currentTimezone, scheduleId, onUpdate, onDelete, layerPriority, shiftId } = + props; const [isOpen, setIsOpen] = useState(true); @@ -79,8 +81,7 @@ const RotationForm: FC = observer((props) => { const handleDeleteClick = useCallback(() => { store.scheduleStore.deleteOncallShift(shiftId).then(() => { - onHide(); - onUpdate(); + onDelete(); }); }, []); @@ -100,7 +101,7 @@ const RotationForm: FC = observer((props) => { until: endLess ? null : getUTCString(rotationEnd, currentTimezone), shift_start: getUTCString(shiftStart, currentTimezone), shift_end: getUTCString(shiftEnd, currentTimezone), - rolling_users: userGroups.filter((group) => group.length), + rolling_users: userGroups, interval: repeatEveryValue, frequency: repeatEveryPeriod, by_day: repeatEveryPeriod === 1 ? selectedDays : null, @@ -125,23 +126,17 @@ const RotationForm: FC = observer((props) => { const handleCreate = useCallback(() => { if (shiftId === 'new') { store.scheduleStore.createRotation(scheduleId, false, params).then(() => { - onHide(); onCreate(); }); } else { store.scheduleStore.updateRotation(shiftId, params).then(() => { - onHide(); onUpdate(); }); } - }, [shiftId, params]); + }, [scheduleId, shiftId, params]); const handleChange = useDebouncedCallback(() => { - store.scheduleStore - .updateRotationPreview(scheduleId, shiftId, getFromString(startMoment), false, params) - .finally(() => { - setIsOpen(true); - }); + store.scheduleStore.updateRotationPreview(scheduleId, shiftId, getFromString(startMoment), false, params); }, 1000); useEffect(handleChange, [params]); @@ -173,10 +168,6 @@ const RotationForm: FC = observer((props) => { setRepeatEveryValue(option.value); }, []); - const handleRepeatEveryPeriodChange = useCallback((option) => { - setRepeatEveryPeriod(option.value); - }, []); - const moment = dayjs(); return ( diff --git a/grafana-plugin/src/containers/RotationForm/ScheduleOverrideForm.tsx b/grafana-plugin/src/containers/RotationForm/ScheduleOverrideForm.tsx index f0803672..62204844 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, useEffect, useState } from 'react'; +import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { dateTime, DateTime } from '@grafana/data'; import { @@ -20,12 +20,14 @@ import Modal from 'components/Modal/Modal'; import Text from 'components/Text/Text'; import UserGroups from 'components/UserGroups/UserGroups'; import WithConfirm from 'components/WithConfirm/WithConfirm'; +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 { getDateTime, getUTCString } from 'pages/schedule/Schedule.helpers'; import { useStore } from 'state/useStore'; +import { useDebouncedCallback } from 'utils/hooks'; import { RotationCreateData } from './RotationForm.types'; @@ -34,18 +36,20 @@ import styles from './RotationForm.module.css'; interface RotationFormProps { onHide: () => void; shiftId: Shift['id'] | 'new'; + startMoment: dayjs.Dayjs; currentTimezone: Timezone; scheduleId: Schedule['id']; onCreate: () => void; onUpdate: () => void; + onDelete: () => void; } const cx = cn.bind(styles); -const startOfDay = dayjs().startOf('day'); +const startOfDay = dayjs().startOf('day').add(1, 'day'); const ScheduleOverrideForm: FC = (props) => { - const { onHide, onCreate, currentTimezone, scheduleId, onUpdate, shiftId } = props; + const { onHide, onCreate, currentTimezone, scheduleId, onUpdate, onDelete, shiftId, startMoment } = props; const store = useStore(); @@ -71,6 +75,17 @@ const ScheduleOverrideForm: FC = (props) => { } }, [shiftId]); + const params = useMemo( + () => ({ + rotation_start: getUTCString(shiftStart, currentTimezone), + shift_start: getUTCString(shiftStart, currentTimezone), + shift_end: getUTCString(shiftEnd, currentTimezone), + rolling_users: userGroups, + frequency: null, + }), + [currentTimezone, shiftStart, shiftEnd, userGroups] + ); + useEffect(() => { if (shift) { setShiftStart(getDateTime(shift.shift_start)); @@ -83,32 +98,28 @@ const ScheduleOverrideForm: FC = (props) => { const handleDeleteClick = useCallback(() => { store.scheduleStore.deleteOncallShift(shiftId).then(() => { onHide(); - onUpdate(); + + onDelete(); }); }, []); const handleCreate = useCallback(() => { - 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]); + }, [scheduleId, shiftId, params]); + + const handleChange = useDebouncedCallback(() => { + store.scheduleStore.updateRotationPreview(scheduleId, shiftId, getFromString(startMoment), true, params); + }, 1000); + + useEffect(handleChange, [params]); return ( = (props) => { > - {shiftId === 'new' ? 'New Override' : shift?.title} + {shiftId === 'new' ? 'New Override' : shift?.id} diff --git a/grafana-plugin/src/containers/Rotations/Rotations.tsx b/grafana-plugin/src/containers/Rotations/Rotations.tsx index 6fafbf52..d0cb61c5 100644 --- a/grafana-plugin/src/containers/Rotations/Rotations.tsx +++ b/grafana-plugin/src/containers/Rotations/Rotations.tsx @@ -28,6 +28,7 @@ interface RotationsProps extends WithStoreProps { onClick: (id: Shift['id'] | 'new') => void; onCreate: () => void; onUpdate: () => void; + onDelete: () => void; } interface RotationsState { @@ -42,7 +43,7 @@ class Rotations extends Component { }; render() { - const { scheduleId, startMoment, currentTimezone, onCreate, onUpdate, store, onClick } = this.props; + const { scheduleId, startMoment, currentTimezone, onCreate, onUpdate, onDelete, store, onClick } = this.props; const { shiftIdToShowRotationForm, layerPriority } = this.state; const base = 7 * 24 * 60; // in minutes @@ -161,21 +162,35 @@ class Rotations extends Component { layerPriority={layerPriority} startMoment={startMoment} currentTimezone={currentTimezone} - onHide={this.handleRotationFormHide} - onUpdate={onUpdate} - onCreate={onCreate} + onHide={() => { + this.hideRotationForm(); + + store.scheduleStore.clearPreview(); + }} + onUpdate={() => { + this.hideRotationForm(); + + onUpdate(); + }} + onCreate={() => { + this.hideRotationForm(); + + onCreate(); + }} + onDelete={() => { + this.hideRotationForm(); + + onDelete(); + }} /> )} ); } - handleRotationFormHide = () => { + hideRotationForm = () => { const { store } = this.props; - store.scheduleStore.rotationPreview = undefined; - store.scheduleStore.finalPreview = undefined; - this.setState({ shiftIdToShowRotationForm: undefined, layerPriority: undefined }); }; diff --git a/grafana-plugin/src/containers/Rotations/ScheduleFinal.tsx b/grafana-plugin/src/containers/Rotations/ScheduleFinal.tsx index 3b4373b4..5a07e1b8 100644 --- a/grafana-plugin/src/containers/Rotations/ScheduleFinal.tsx +++ b/grafana-plugin/src/containers/Rotations/ScheduleFinal.tsx @@ -8,7 +8,7 @@ import { observer } from 'mobx-react'; import TimelineMarks from 'components/TimelineMarks/TimelineMarks'; import Rotation from 'containers/Rotation/Rotation'; -import { getColor, getFromString } from 'models/schedule/schedule.helpers'; +import { getColor, getFromString, getOverrideColor } from 'models/schedule/schedule.helpers'; import { Layer, Schedule } from 'models/schedule/schedule.types'; import { Timezone } from 'models/timezone/timezone.types'; import { WithStoreProps } from 'state/types'; @@ -52,6 +52,9 @@ class ScheduleFinal extends Component 1; /* console.log('shifts', toJS(shifts)); @@ -79,6 +82,8 @@ class ScheduleFinal extends Component {shifts && shifts.length ? ( shifts.map(({ shiftId, events }, index) => { + let color = undefined; + const layerIndex = layers ? layers.findIndex((layer) => layer.shifts.some((shift) => shift.shiftId === shiftId)) : -1; @@ -86,7 +91,15 @@ class ScheduleFinal extends Component -1 ? layers[layerIndex].shifts.findIndex((shift) => shift.shiftId === shiftId) : -1; - console.log(layerIndex, rotationIndex); + if (layerIndex > -1 && rotationIndex > -1) { + color = getColor(layerIndex, rotationIndex); + } else { + const overrideIndex = overrides ? overrides.findIndex((shift) => shift.shiftId === shiftId) : -1; + + if (overrideIndex > -1) { + color = getOverrideColor(overrideIndex); + } + } return ( ); }) diff --git a/grafana-plugin/src/containers/Rotations/ScheduleOverrides.tsx b/grafana-plugin/src/containers/Rotations/ScheduleOverrides.tsx index 16456048..4eb8aa33 100644 --- a/grafana-plugin/src/containers/Rotations/ScheduleOverrides.tsx +++ b/grafana-plugin/src/containers/Rotations/ScheduleOverrides.tsx @@ -25,6 +25,7 @@ interface ScheduleOverridesProps extends WithStoreProps { scheduleId: Schedule['id']; onCreate: () => void; onUpdate: () => void; + onDelete: () => void; } interface ScheduleOverridesState { @@ -38,10 +39,12 @@ class ScheduleOverrides extends Component { - this.setState({ shiftIdToShowOverrideForm: undefined }); + this.handleHide(); + + store.scheduleStore.clearPreview(); + }} + onUpdate={() => { + this.handleHide(); + + onUpdate(); + }} + onCreate={() => { + this.handleHide(); + + onCreate(); + }} + onDelete={() => { + this.handleHide(); + + onDelete(); }} - onUpdate={onUpdate} - onCreate={onCreate} /> )} @@ -117,6 +136,12 @@ class ScheduleOverrides extends Component { this.setState({ shiftIdToShowOverrideForm: 'new' }); }; + + handleHide = () => { + const { store } = this.props; + + this.setState({ shiftIdToShowOverrideForm: undefined }); + }; } export default withMobXProviderContext(ScheduleOverrides); diff --git a/grafana-plugin/src/models/schedule/schedule.helpers.ts b/grafana-plugin/src/models/schedule/schedule.helpers.ts index fda67137..9d2ec980 100644 --- a/grafana-plugin/src/models/schedule/schedule.helpers.ts +++ b/grafana-plugin/src/models/schedule/schedule.helpers.ts @@ -89,10 +89,12 @@ export const enrichLayers = ( shiftId: Shift['id'] | 'new', priority: Shift['priority_level'] ) => { - /*const event = newEvents.find((event) => !event.is_gap); - if (event) { - shiftId = event.shift.pk; - }*/ + if (shiftId === 'new') { + const event = newEvents.find((event) => !event.is_gap); + if (event) { + shiftId = event.shift.pk; + } + } const updatingLayer = { priority, @@ -135,6 +137,31 @@ export const enrichLayers = ( return layers; }; +export const enrichOverrides = ( + overrides: Array<{ shiftId: Shift['id']; events: Event[] }>, + newEvents: Event[], + shiftId: Shift['id'] +) => { + if (shiftId === 'new') { + const event = newEvents.find((event) => !event.is_gap); + if (event) { + shiftId = event.shift.pk; + } + } + + const newShift = { shiftId, events: fillGaps(newEvents) }; + + const index = overrides.findIndex((shift) => shift.shiftId === shiftId); + + if (index > -1) { + overrides[index] = newShift; + } else { + overrides.push(newShift); + } + + return overrides; +}; + const L1_COLORS = ['#3D71D9', '#6D609C', '#4D3B72', '#8214A0']; const L2_COLORS = ['#3CB979', '#188343', '#84362A', '#521913']; diff --git a/grafana-plugin/src/models/schedule/schedule.ts b/grafana-plugin/src/models/schedule/schedule.ts index 87e6fcb4..0b032e28 100644 --- a/grafana-plugin/src/models/schedule/schedule.ts +++ b/grafana-plugin/src/models/schedule/schedule.ts @@ -11,7 +11,14 @@ import { makeRequest } from 'network'; import { RootStore } from 'state'; import { SelectOption } from 'state/types'; -import { enrichLayers, fillGaps, getFromString, splitToLayers, splitToShiftsAndFillGaps } from './schedule.helpers'; +import { + enrichLayers, + enrichOverrides, + fillGaps, + getFromString, + splitToLayers, + splitToShiftsAndFillGaps, +} from './schedule.helpers'; import { Events, Rotation, RotationType, Schedule, ScheduleEvent, Shift, Event, Layer } from './schedule.types'; const DEFAULT_FORMAT = 'YYYY-MM-DDTHH:mm:ss'; @@ -72,11 +79,14 @@ export class ScheduleStore extends BaseStore { }; } = {}; + @observable + finalPreview?: Array<{ shiftId: Shift['id']; events: Event[] }>; + @observable rotationPreview?: Layer[]; @observable - finalPreview?: Array<{ shiftId: Shift['id']; events: Event[] }>; + overridePreview?: Array<{ shiftId: Shift['id']; events: Event[] }>; @observable scheduleToScheduleEvents: { @@ -209,6 +219,11 @@ export class ScheduleStore extends BaseStore { }).catch(this.onApiError); if (isOverride) { + this.overridePreview = enrichOverrides( + [...this.events[scheduleId]?.['override']?.[fromString]], + response.rotation, + shiftId + ); } else { const layers = enrichLayers( [...(this.events[scheduleId]?.['rotation']?.[fromString] as Layer[])], @@ -220,7 +235,14 @@ export class ScheduleStore extends BaseStore { this.rotationPreview = layers; } - this.finalPreview = splitToShiftsAndFillGaps(response.final).filter((shift) => shift.shiftId !== shiftId); + this.finalPreview = splitToShiftsAndFillGaps(response.final); /*.filter((shift) => shift.shiftId !== shiftId);*/ + } + + @action + clearPreview() { + this.finalPreview = undefined; + this.rotationPreview = undefined; + this.overridePreview = undefined; } async updateRotation(shiftId: Shift['id'], params: Partial) { diff --git a/grafana-plugin/src/pages/schedule/Schedule.tsx b/grafana-plugin/src/pages/schedule/Schedule.tsx index e9d017b3..5541feff 100644 --- a/grafana-plugin/src/pages/schedule/Schedule.tsx +++ b/grafana-plugin/src/pages/schedule/Schedule.tsx @@ -190,14 +190,16 @@ class SchedulePage extends React.Component currentTimezone={currentTimezone} startMoment={startMoment} onCreate={this.handleCreateRotation} - onUpdate={this.updateEvents} + onUpdate={this.handleUpdateRotation} + onDelete={this.handleDeleteRotation} /> @@ -213,9 +215,11 @@ class SchedulePage extends React.Component const { startMoment } = this.state; - store.scheduleStore.updateEvents(scheduleId, getFromString(startMoment), 'rotation'); - store.scheduleStore.updateEvents(scheduleId, getFromString(startMoment), 'override'); - store.scheduleStore.updateEvents(scheduleId, getFromString(startMoment), 'final'); + return Promise.all([ + store.scheduleStore.updateEvents(scheduleId, getFromString(startMoment), 'rotation'), + store.scheduleStore.updateEvents(scheduleId, getFromString(startMoment), 'override'), + store.scheduleStore.updateEvents(scheduleId, getFromString(startMoment), 'final'), + ]); }; handleCreateRotation = () => { @@ -224,13 +228,49 @@ class SchedulePage extends React.Component query: { id: scheduleId }, } = this.props; - this.updateEvents(); + this.updateEvents().then(() => { + store.scheduleStore.clearPreview(); + }); }; handleCreateOverride = () => { const { store } = this.props; - this.updateEvents(); + this.updateEvents().then(() => { + store.scheduleStore.clearPreview(); + }); + }; + + handleUpdateRotation = () => { + const { store } = this.props; + + this.updateEvents().then(() => { + store.scheduleStore.clearPreview(); + }); + }; + + handleDeleteRotation = () => { + const { store } = this.props; + + this.updateEvents().then(() => { + store.scheduleStore.clearPreview(); + }); + }; + + handleDeleteOverride = () => { + const { store } = this.props; + + this.updateEvents().then(() => { + store.scheduleStore.clearPreview(); + }); + }; + + handleUpdateOverride = () => { + const { store } = this.props; + + this.updateEvents().then(() => { + store.scheduleStore.clearPreview(); + }); }; handleTimezoneChange = (value: Timezone) => {