diff --git a/engine/apps/api/serializers/on_call_shifts.py b/engine/apps/api/serializers/on_call_shifts.py index f2178e28..58f129f9 100644 --- a/engine/apps/api/serializers/on_call_shifts.py +++ b/engine/apps/api/serializers/on_call_shifts.py @@ -56,6 +56,7 @@ class OnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer): "source", "rolling_users", "updated_shift", + "start_rotation_from_user_index", ] extra_kwargs = { "interval": {"required": False, "allow_null": True}, diff --git a/engine/apps/api/tests/test_oncall_shift.py b/engine/apps/api/tests/test_oncall_shift.py index e8a3a6f6..99d3c9ea 100644 --- a/engine/apps/api/tests/test_oncall_shift.py +++ b/engine/apps/api/tests/test_oncall_shift.py @@ -53,7 +53,11 @@ def test_create_on_call_shift_rotation(on_call_shift_internal_api_setup, make_us with patch("apps.schedules.models.CustomOnCallShift.refresh_schedule") as mock_refresh_schedule: response = client.post(url, data, format="json", **make_user_auth_headers(user1, token)) - expected_payload = data | {"id": response.data["id"], "updated_shift": None} + expected_payload = data | { + "id": response.data["id"], + "updated_shift": None, + "start_rotation_from_user_index": None, + } assert response.status_code == status.HTTP_201_CREATED assert response.json() == expected_payload assert mock_refresh_schedule.called @@ -162,6 +166,7 @@ def test_create_on_call_shift_override(on_call_shift_internal_api_setup, make_us "updated_shift": None, "rolling_users": returned_rolling_users, "week_start": CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.MONDAY], + "start_rotation_from_user_index": None, } assert response.status_code == status.HTTP_201_CREATED @@ -209,6 +214,7 @@ def test_get_on_call_shift( "week_start": CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.SUNDAY], "rolling_users": [[user1.public_primary_key], [user2.public_primary_key]], "updated_shift": None, + "start_rotation_from_user_index": None, } assert response.status_code == status.HTTP_200_OK @@ -258,6 +264,7 @@ def test_get_calendar_on_call_shift( "week_start": CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.SUNDAY], "rolling_users": [[user1.public_primary_key], [user2.public_primary_key]], "updated_shift": None, + "start_rotation_from_user_index": None, } assert response.status_code == status.HTTP_200_OK @@ -309,6 +316,7 @@ def test_list_on_call_shift( "week_start": CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.SUNDAY], "rolling_users": [[user1.public_primary_key], [user2.public_primary_key]], "updated_shift": None, + "start_rotation_from_user_index": None, } ], "current_page_number": 1, @@ -370,6 +378,7 @@ def test_list_on_call_shift_filter_schedule_id( "week_start": CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.SUNDAY], "rolling_users": [[user1.public_primary_key], [user2.public_primary_key]], "updated_shift": None, + "start_rotation_from_user_index": None, } ], "current_page_number": 1, @@ -494,6 +503,7 @@ def test_update_future_on_call_shift( "week_start": CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.MONDAY], "rolling_users": [[user1.public_primary_key]], "updated_shift": None, + "start_rotation_from_user_index": None, } assert response.status_code == status.HTTP_200_OK @@ -605,6 +615,7 @@ def test_update_started_on_call_shift( "week_start": CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.MONDAY], "rolling_users": [[user1.public_primary_key]], "updated_shift": None, + "start_rotation_from_user_index": 0, } assert response.status_code == status.HTTP_200_OK @@ -746,6 +757,7 @@ def test_update_old_on_call_shift_with_future_version( "schedule": schedule.public_primary_key, "updated_shift": None, "week_start": CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.MONDAY], + "start_rotation_from_user_index": 0, } assert response.status_code == status.HTTP_200_OK @@ -816,6 +828,7 @@ def test_update_started_on_call_shift_name( "schedule": schedule.public_primary_key, "updated_shift": None, "week_start": CustomOnCallShift.ICAL_WEEKDAY_MAP[CustomOnCallShift.MONDAY], + "start_rotation_from_user_index": None, } assert response.status_code == status.HTTP_200_OK diff --git a/grafana-plugin/src/containers/RotationForm/RotationForm.tsx b/grafana-plugin/src/containers/RotationForm/RotationForm.tsx index 27fdd8ba..8ddab461 100644 --- a/grafana-plugin/src/containers/RotationForm/RotationForm.tsx +++ b/grafana-plugin/src/containers/RotationForm/RotationForm.tsx @@ -113,6 +113,8 @@ export const RotationForm = observer((props: RotationFormProps) => { const shift = store.scheduleStore.shifts[shiftId]; + const [startRotationFromUserIndex, setStartRotationFromUserIndex] = useState(0); + const [errors, setErrors] = useState<{ [key: string]: string[] }>({}); const [bounds, setDraggableBounds] = useState<{ left: number; right: number; top: number; bottom: number }>( undefined @@ -246,6 +248,7 @@ export const RotationForm = observer((props: RotationFormProps) => { ), priority_level: shiftId === 'new' ? layerPriority : shift?.priority_level, name: rotationName, + start_rotation_from_user_index: startRotationFromUserIndex, }), [ rotationStart, @@ -261,6 +264,7 @@ export const RotationForm = observer((props: RotationFormProps) => { shift, endLess, rotationName, + startRotationFromUserIndex, store.timezoneStore.selectedTimezoneOffset, ] ); @@ -490,6 +494,7 @@ export const RotationForm = observer((props: RotationFormProps) => { } setUserGroups(shift.rolling_users); + setStartRotationFromUserIndex(shift.start_rotation_from_user_index); } }, [shift]); diff --git a/grafana-plugin/src/models/schedule/schedule.types.ts b/grafana-plugin/src/models/schedule/schedule.types.ts index bf77f32d..07d6c651 100644 --- a/grafana-plugin/src/models/schedule/schedule.types.ts +++ b/grafana-plugin/src/models/schedule/schedule.types.ts @@ -69,6 +69,7 @@ export interface Shift { type: number; // 2 - rotations, 3 - overrides until: string | null; updated_shift: null; + start_rotation_from_user_index?: number; } export interface Rotation {