Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make rotation start and rotation end take timezone into consideration #4481

Merged
merged 28 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b4c6fba
schedules time shift fix
teodosii Jun 6, 2024
e42bc86
lint
teodosii Jun 6, 2024
3575572
fix issue when in dst
teodosii Jun 10, 2024
2d2b081
removed unused
teodosii Jun 10, 2024
c150553
Merge branch 'dev' into rares/fix-schedule-shifting-time-in-rotation
teodosii Jun 10, 2024
821b3f5
force datepicker use current offset
teodosii Jun 10, 2024
a2c9726
added comment
teodosii Jun 10, 2024
9d2ff75
moved to util
teodosii Jun 10, 2024
bca5c44
schedule fix (wip)
teodosii Jun 11, 2024
12ae8be
force keep date in selected offset
teodosii Jun 11, 2024
ca32396
fix time shifting
teodosii Jun 12, 2024
add8633
time conversion
teodosii Jun 12, 2024
c6642fc
lint
teodosii Jun 12, 2024
6f464e6
Merge branch 'dev' into rares/fix-schedule-shifting-time-in-rotation
teodosii Jun 12, 2024
80831ec
unused
teodosii Jun 12, 2024
aba0f88
improvements
teodosii Jun 12, 2024
05599b8
more tweaks to date offsetting
teodosii Jun 13, 2024
199aa5c
cleanup
teodosii Jun 14, 2024
d3f4647
Merge branch 'dev' into rares/fix-schedule-shifting-time-in-rotation
teodosii Jun 14, 2024
1131300
cleanup
teodosii Jun 14, 2024
ef0b934
lint
teodosii Jun 14, 2024
b55c5cc
cleanup
teodosii Jun 14, 2024
2c484a9
revert
teodosii Jun 14, 2024
66524e7
simplify logic for rotation change on new rotations
teodosii Jun 14, 2024
6f151c8
ci
teodosii Jun 14, 2024
ad05205
fixed calendar period shift when changing offset
teodosii Jun 14, 2024
66b4af6
Merge branch 'dev' into rares/fix-schedule-shifting-time-in-rotation
teodosii Jun 17, 2024
893f1b9
tweak
teodosii Jun 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions grafana-plugin/src/containers/Rotation/Rotation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ interface RotationProps {

export const Rotation: FC<RotationProps> = observer((props) => {
const {
timezoneStore: { calendarStartDate, getDateInSelectedTimezone },
timezoneStore: { calendarStartDate, getDateInSelectedTimezone, selectedTimezoneOffset },
scheduleStore: { scheduleView: storeScheduleView },
} = useStore();
const {
Expand Down Expand Up @@ -144,7 +144,7 @@ export const Rotation: FC<RotationProps> = observer((props) => {
const base = 60 * 60 * 24 * days;

return firstShiftOffset / base;
}, [events, startDate]);
}, [events, startDate, selectedTimezoneOffset]);

return (
<div className={cx('root')} onClick={onClick && handleRotationClick}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,12 @@ import dayjs, { Dayjs } from 'dayjs';
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';

import { dayJSAddWithDSTFixed, getDateForDatePicker } from './RotationForm.helpers';
import { dayJSAddWithDSTFixed } from './RotationForm.helpers';

dayjs.extend(timezone);
dayjs.extend(utc);

describe('RotationForm helpers', () => {
describe('getDateForDatePicker()', () => {
it(`should return the same regular JS Date as input dayJsDate
even if selected day of month doesn't exist in current month
(in this case there is no 30th Feb and it should still work ok)`, () => {
jest.useFakeTimers().setSystemTime(new Date('2024-02-01'));

const inputDate = dayjs()
.utcOffset(360)
.set('year', 2024)
.set('month', 3) // 0-indexed so April
.set('date', 30)
.set('hour', 12)
.set('minute', 20);
const result = getDateForDatePicker(inputDate);

expect(result.toString()).toContain('Tue Apr 30 2024');
});
});

describe('dayJSAddWithDSTFixed() @london-tz', () => {
it(`corrects resulting hour to be the same as in input if start date is before London DST (GMT + 0)
and resulting date is within London DST (GMT + 1)`, () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import dayjs, { Dayjs, ManipulateType } from 'dayjs';

import { Timezone } from 'models/timezone/timezone.types';
import { Dayjs, ManipulateType } from 'dayjs';

import { RepeatEveryPeriod } from './RotationForm.types';

Expand All @@ -11,19 +9,6 @@ export const getRepeatShiftsEveryOptions = (repeatEveryPeriod: number) => {
.map((i) => ({ label: String(i), value: i }));
};

export const toDate = (moment: dayjs.Dayjs, timezone: Timezone) => {
const localMoment = moment.tz(timezone);

return new Date(
localMoment.get('year'),
localMoment.get('month'),
localMoment.get('date'),
localMoment.get('hour'),
localMoment.get('minute'),
localMoment.get('second')
);
};

export interface TimeUnit {
unit: RepeatEveryPeriod;
value: number;
Expand Down Expand Up @@ -171,23 +156,6 @@ export const repeatEveryInSeconds = (repeatEveryPeriod: RepeatEveryPeriod, repea
return repeatEveryPeriodMultiplier[repeatEveryPeriod] * repeatEveryValue;
};

export const getDateForDatePicker = (dayJsDate: Dayjs) => {
const date = new Date();
// Day of the month needs to be set to 1st day at first to prevent incorrect month increment
// when selected day of month doesn't exist in current month
// E.g. selected date is 30th March and current month is Feb, so in this case date.setMonth(2) results in April

date.setDate(1); // temporary selection to prevent incorrect month increment

date.setFullYear(dayJsDate.year());
date.setMonth(dayJsDate.month());
date.setDate(dayJsDate.date());
date.setHours(dayJsDate.hour());
date.setMinutes(dayJsDate.minute());
date.setSeconds(dayJsDate.second());
return date;
};

export const dayJSAddWithDSTFixed = ({
baseDate,
addParams,
Expand Down
103 changes: 70 additions & 33 deletions grafana-plugin/src/containers/RotationForm/RotationForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';

import {
Alert,
Button,
Field,
HorizontalGroup,
Expand Down Expand Up @@ -52,11 +53,11 @@ import { ApiSchemas } from 'network/oncall-api/api.types';
import {
getDateTime,
getSelectedDays,
getStartOfWeekBasedOnCurrentDate,
getUTCByDay,
getUTCString,
getUTCWeekStart,
getWeekStartString,
toDateWithTimezoneOffset,
} from 'pages/schedule/Schedule.helpers';
import { isTopNavbar } from 'plugin/GrafanaPluginRootPage.helpers';
import { useStore } from 'state/useStore';
Expand All @@ -82,8 +83,25 @@ interface RotationFormProps {
onShowRotationForm: (shiftId: Shift['id']) => void;
}

const getStartShift = (start: dayjs.Dayjs, timezoneOffset: number, isNewRotation = false) => {
if (isNewRotation) {
// all new rotations default to midnight in selected timezone offset
return toDateWithTimezoneOffset(start, timezoneOffset)
.set('date', 1)
.set('year', start.year())
.set('month', start.month())
.set('date', start.date())
.set('hour', 0)
.set('minute', 0)
.set('second', 0);
}

return toDateWithTimezoneOffset(start, timezoneOffset);
};

export const RotationForm = observer((props: RotationFormProps) => {
const store = useStore();

const {
onHide,
onCreate,
Expand All @@ -92,7 +110,7 @@ export const RotationForm = observer((props: RotationFormProps) => {
onDelete,
layerPriority,
shiftId,
shiftStart: propsShiftStart = getStartOfWeekBasedOnCurrentDate(store.timezoneStore.currentDateInSelectedTimezone),
shiftStart: propsShiftStart = store.timezoneStore.calendarStartDate,
shiftEnd: propsShiftEnd,
shiftColor = '#3D71D9',
onShowRotationForm,
Expand All @@ -101,7 +119,6 @@ export const RotationForm = observer((props: RotationFormProps) => {
const shift = store.scheduleStore.shifts[shiftId];

const [errors, setErrors] = useState<{ [key: string]: string[] }>({});

const [bounds, setDraggableBounds] = useState<{ left: number; right: number; top: number; bottom: number }>(
undefined
);
Expand All @@ -111,13 +128,19 @@ export const RotationForm = observer((props: RotationFormProps) => {
const [offsetTop, setOffsetTop] = useState<number>(GRAFANA_HEADER_HEIGHT + 10);
const [draggablePosition, setDraggablePosition] = useState<{ x: number; y: number }>(undefined);

const [shiftStart, setShiftStart] = useState<dayjs.Dayjs>(propsShiftStart);
const [shiftEnd, setShiftEnd] = useState<dayjs.Dayjs>(propsShiftEnd || shiftStart.add(1, 'day'));
const [shiftStart, setShiftStart] = useState<dayjs.Dayjs>(
getStartShift(propsShiftStart, store.timezoneStore.selectedTimezoneOffset, shiftId === 'new')
);

const [shiftEnd, setShiftEnd] = useState<dayjs.Dayjs>(
propsShiftEnd?.utcOffset(store.timezoneStore.selectedTimezoneOffset) || shiftStart.add(1, 'day')
);

const [activePeriod, setActivePeriod] = useState<number | undefined>(undefined);
const [shiftPeriodDefaultValue, setShiftPeriodDefaultValue] = useState<number | undefined>(undefined);

const [rotationStart, setRotationStart] = useState<dayjs.Dayjs>(shiftStart);
const [endLess, setEndless] = useState<boolean>(true);
const [endLess, setEndless] = useState<boolean>(shift?.until === undefined ? true : !Boolean(shift.until));
const [rotationEnd, setRotationEnd] = useState<dayjs.Dayjs>(shiftStart.add(1, 'month'));

const [repeatEveryValue, setRepeatEveryValue] = useState<number>(1);
Expand Down Expand Up @@ -248,10 +271,11 @@ export const RotationForm = observer((props: RotationFormProps) => {
shift,
endLess,
rotationName,
store.timezoneStore.selectedTimezoneOffset,
]
);

useEffect(handleChange, [params, store.timezoneStore.calendarStartDate]);
useEffect(handleChange, [params, store.timezoneStore.calendarStartDate, store.timezoneStore.selectedTimezoneOffset]);

const create = useCallback(async () => {
try {
Expand Down Expand Up @@ -330,28 +354,22 @@ export const RotationForm = observer((props: RotationFormProps) => {
}
};

const handleRotationStartChange = useCallback(
(value) => {
setRotationStart(value);
setShiftStart(value);
if (showActiveOnSelectedPartOfDay) {
setShiftEnd(
dayJSAddWithDSTFixed({
const handleRotationStartChange = (value: dayjs.Dayjs) => {
setRotationStart(value);
setShiftStart(value);

setShiftEnd(
showActiveOnSelectedPartOfDay
? dayJSAddWithDSTFixed({
baseDate: value,
addParams: [activePeriod, 'seconds'],
})
);
} else {
setShiftEnd(
dayJSAddWithDSTFixed({
: dayJSAddWithDSTFixed({
baseDate: value,
addParams: [repeatEveryValue, repeatEveryPeriodToUnitName[repeatEveryPeriod]],
})
);
}
},
[showActiveOnSelectedPartOfDay, activePeriod, repeatEveryPeriod, repeatEveryValue]
);
);
};

const handleActivePeriodChange = useCallback(
(value) => {
Expand Down Expand Up @@ -435,15 +453,23 @@ export const RotationForm = observer((props: RotationFormProps) => {
useEffect(() => {
if (shift) {
setRotationName(getShiftName(shift));
const shiftStart = getDateTime(shift.shift_start);

// use shiftStart as rotationStart for existing shifts
// (original rotationStart defaulted to the shift creation timestamp)
const shiftStart = toDateWithTimezoneOffset(dayjs(shift.shift_start), store.timezoneStore.selectedTimezoneOffset);

setRotationStart(shiftStart);
setRotationEnd(shift.until ? getDateTime(shift.until) : getDateTime(shift.shift_start).add(1, 'month'));
setRotationEnd(
toDateWithTimezoneOffset(
// always keep the date offseted
shift.until ? getDateTime(shift.until) : getDateTime(shift.shift_start).add(1, 'month'),
store.timezoneStore.selectedTimezoneOffset
)
);
setShiftStart(shiftStart);
const shiftEnd = getDateTime(shift.shift_end);

const shiftEnd = toDateWithTimezoneOffset(dayjs(shift.shift_end), store.timezoneStore.selectedTimezoneOffset);
setShiftEnd(shiftEnd);
setEndless(!shift.until);

setRepeatEveryValue(shift.interval);
setRepeatEveryPeriod(shift.frequency);
Expand Down Expand Up @@ -475,13 +501,25 @@ export const RotationForm = observer((props: RotationFormProps) => {

useEffect(() => {
if (shift) {
// for existing rotations
handleRotationStartChange(toDateWithTimezoneOffset(rotationStart, store.timezoneStore.selectedTimezoneOffset));
setRotationEnd(toDateWithTimezoneOffset(rotationEnd, store.timezoneStore.selectedTimezoneOffset));

setSelectedDays(
getSelectedDays({
dayOptions: store.scheduleStore.byDayOptions,
by_day: shift.by_day,
moment: store.timezoneStore.getDateInSelectedTimezone(shiftStart),
})
);
} else {
// for new rotations
handleRotationStartChange(toDateWithTimezoneOffset(rotationStart, store.timezoneStore.selectedTimezoneOffset));

setShiftEnd(toDateWithTimezoneOffset(shiftEnd, store.timezoneStore.selectedTimezoneOffset));

// not behind an "if" such that it will reflect correct value after toggle gets switched
setRotationEnd(toDateWithTimezoneOffset(rotationEnd, store.timezoneStore.selectedTimezoneOffset));
}
}, [store.timezoneStore.selectedTimezoneOffset]);

Expand Down Expand Up @@ -561,14 +599,11 @@ export const RotationForm = observer((props: RotationFormProps) => {
</Block>
)}
{!hasUpdatedShift && ended && (
<Block bordered className={cx('updated-shift-info')}>
<div className={cx('updated-shift-info')}>
<VerticalGroup>
<HorizontalGroup>
<Icon name="info-circle" size="md"></Icon>
<Text>This rotation is over</Text>
</HorizontalGroup>
<Alert severity="info" title={(<Text>This rotation is over</Text>) as unknown as string} />
</VerticalGroup>
</Block>
</div>
)}
<div className={cx('two-fields')}>
<Field
Expand All @@ -580,6 +615,7 @@ export const RotationForm = observer((props: RotationFormProps) => {
>
<DateTimePicker
value={rotationStart}
utcOffset={store.timezoneStore.selectedTimezoneOffset}
onChange={handleRotationStartChange}
error={errors.rotation_start}
disabled={disabled}
Expand Down Expand Up @@ -608,6 +644,7 @@ export const RotationForm = observer((props: RotationFormProps) => {
) : (
<DateTimePicker
value={rotationEnd}
utcOffset={store.timezoneStore.selectedTimezoneOffset}
onChange={setRotationEnd}
error={errors.until}
disabled={disabled}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export const ScheduleOverrideForm: FC<RotationFormProps> = (props) => {

const handleChange = useDebouncedCallback(updatePreview, 200);

useEffect(handleChange, [params, store.timezoneStore.calendarStartDate]);
useEffect(handleChange, [params, store.timezoneStore.calendarStartDate, store.timezoneStore.selectedTimezoneOffset]);

const isFormValid = useMemo(() => !Object.keys(errors).length, [errors]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export const ShiftSwapForm = (props: ShiftSwapFormProps) => {
...shiftSwap,
});
}
}, [shiftSwap, store.timezoneStore.calendarStartDate]);
}, [shiftSwap, store.timezoneStore.calendarStartDate, store.timezoneStore.selectedTimezoneOffset]);

const handleDescriptionChange = useCallback(
(event) => {
Expand Down
Loading
Loading