Commit 9be7d887 authored by David O'Regan's avatar David O'Regan Committed by Simon Knox

Fix(oncallschedule): Update render performance

Increase render performance
for the grid by filtering
rotation shifts to render
parent 81b2ae7f
......@@ -8,7 +8,6 @@ import {
GlTooltipDirective,
} from '@gitlab/ui';
import { capitalize } from 'lodash';
import { fetchPolicies } from '~/lib/graphql';
import {
formatDate,
nWeeksBefore,
......@@ -68,7 +67,6 @@ export default {
},
apollo: {
rotations: {
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
query: getShiftsForRotations,
variables() {
const startsAt = this.timeframeStartDate;
......@@ -123,7 +121,7 @@ export default {
return '';
}
},
isLoading() {
loading() {
return this.$apollo.queries.rotations.loading;
},
},
......@@ -159,6 +157,9 @@ export default {
break;
}
},
fetchRotationShifts() {
this.$apollo.queries.rotations.refetch();
},
},
};
</script>
......@@ -215,13 +216,13 @@ export default {
<gl-button
data-testid="previous-timeframe-btn"
icon="chevron-left"
:disabled="isLoading"
:disabled="loading"
@click="updateToViewPreviousTimeframe"
/>
<gl-button
data-testid="next-timeframe-btn"
icon="chevron-right"
:disabled="isLoading"
:disabled="loading"
@click="updateToViewNextTimeframe"
/>
</gl-button-group>
......@@ -248,6 +249,7 @@ export default {
:rotations="rotations"
:timeframe="timeframe"
:schedule-iid="schedule.iid"
:loading="loading"
/>
</div>
</gl-card>
......@@ -258,7 +260,11 @@ export default {
:modal-id="$options.editScheduleModalId"
is-edit-mode
/>
<add-edit-rotation-modal :schedule="schedule" :modal-id="$options.addRotationModalId" />
<add-edit-rotation-modal
:schedule="schedule"
:modal-id="$options.addRotationModalId"
@fetchRotationShifts="fetchRotationShifts"
/>
<add-edit-rotation-modal
:schedule="schedule"
:modal-id="$options.editRotationModalId"
......
......@@ -5,10 +5,7 @@ import { LENGTH_ENUM } from 'ee/oncall_schedules/constants';
import createOncallScheduleRotationMutation from 'ee/oncall_schedules/graphql/mutations/create_oncall_schedule_rotation.mutation.graphql';
import updateOncallScheduleRotationMutation from 'ee/oncall_schedules/graphql/mutations/update_oncall_schedule_rotation.mutation.graphql';
import getOncallSchedulesWithRotationsQuery from 'ee/oncall_schedules/graphql/queries/get_oncall_schedules.query.graphql';
import {
updateStoreAfterRotationAdd,
updateStoreAfterRotationEdit,
} from 'ee/oncall_schedules/utils/cache_updates';
import { updateStoreAfterRotationEdit } from 'ee/oncall_schedules/utils/cache_updates';
import { isNameFieldValid, getParticipantsForSave } from 'ee/oncall_schedules/utils/common_utils';
import createFlash, { FLASH_TYPES } from '~/flash';
import usersSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
......@@ -149,22 +146,11 @@ export default {
methods: {
createRotation() {
this.loading = true;
const { projectPath, schedule } = this;
this.$apollo
.mutate({
mutation: createOncallScheduleRotationMutation,
variables: { input: this.rotationVariables },
update(store, { data }) {
updateStoreAfterRotationAdd(
store,
getOncallSchedulesWithRotationsQuery,
{ ...data, scheduleIid: schedule.iid },
{
projectPath,
},
);
},
})
.then(
({
......@@ -179,6 +165,7 @@ export default {
}
this.$refs.addEditScheduleRotationModal.hide();
this.$emit('fetchRotationShifts');
return createFlash({
message: this.$options.i18n.rotationCreated,
type: FLASH_TYPES.SUCCESS,
......
<script>
import { GlButtonGroup, GlButton, GlTooltipDirective, GlModalDirective } from '@gitlab/ui';
import {
GlButtonGroup,
GlButton,
GlLoadingIcon,
GlTooltipDirective,
GlModalDirective,
} from '@gitlab/ui';
import DeleteRotationModal from 'ee/oncall_schedules/components/rotations/components/delete_rotation_modal.vue';
import ScheduleShiftWrapper from 'ee/oncall_schedules/components/schedule/components/shifts/components/schedule_shift_wrapper.vue';
import {
......@@ -14,6 +20,7 @@ import CurrentDayIndicator from './current_day_indicator.vue';
export const i18n = {
editRotationLabel: s__('OnCallSchedules|Edit rotation'),
deleteRotationLabel: s__('OnCallSchedules|Delete rotation'),
addRotationLabel: s__('OnCallSchedules|Currently no rotation.'),
};
export default {
......@@ -23,6 +30,7 @@ export default {
components: {
GlButton,
GlButtonGroup,
GlLoadingIcon,
CurrentDayIndicator,
DeleteRotationModal,
ScheduleShiftWrapper,
......@@ -48,6 +56,11 @@ export default {
type: String,
required: true,
},
loading: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
......@@ -80,60 +93,78 @@ export default {
cellShouldHideOverflow(index) {
return index + 1 === this.timeframe.length || this.presetIsDay;
},
timeframeItemUniqueKey(timeframeItem) {
return timeframeItem.valueOf();
},
},
};
</script>
<template>
<div class="list-section">
<div
v-for="rotation in rotations"
:key="rotation.id"
class="list-item list-item-empty clearfix"
>
<gl-loading-icon v-if="loading" />
<div v-else-if="rotations.length === 0 && !loading" class="gl-clearfix">
<span
class="details-cell gl-display-flex gl-justify-content-space-between gl-align-items-center gl-pl-3"
>
<span class="gl-str-truncated">{{ rotation.name }}</span>
<gl-button-group class="gl-px-2">
<gl-button
v-gl-modal="$options.editRotationModalId"
v-gl-tooltip
category="tertiary"
:title="$options.i18n.editRotationLabel"
icon="pencil"
:aria-label="$options.i18n.editRotationLabel"
:disabled="true"
/>
<gl-button
v-gl-modal="$options.deleteRotationModalId"
v-gl-tooltip
category="tertiary"
:title="$options.i18n.deleteRotationLabel"
icon="remove"
:aria-label="$options.i18n.deleteRotationLabel"
@click="setRotationToUpdate(rotation)"
/>
</gl-button-group>
<span class="gl-text-truncated">{{ $options.i18n.addRotationLabel }}</span>
</span>
<span
v-for="(timeframeItem, index) in timeframeToDraw"
:key="index"
class="timeline-cell gl-border-b-solid gl-border-b-gray-100 gl-border-b-1"
:class="{ 'gl-overflow-hidden': cellShouldHideOverflow(index) }"
:style="timelineStyles"
data-testid="timelineCell"
data-testid="empty-timeline-cell"
>
<current-day-indicator :preset-type="presetType" :timeframe-item="timeframeItem" />
<schedule-shift-wrapper
v-if="rotation.shifts"
:preset-type="presetType"
:timeframe-item="timeframeItem"
:timeframe="timeframe"
:rotation="rotation"
/>
</span>
</div>
<div v-else>
<div v-for="rotation in rotations" :key="rotation.id" class="gl-clearfix">
<span
class="details-cell gl-display-flex gl-justify-content-space-between gl-align-items-center gl-pl-3"
>
<span class="gl-text-truncated">{{ rotation.name }}</span>
<gl-button-group class="gl-px-2">
<gl-button
v-gl-modal="$options.editRotationModalId"
v-gl-tooltip
category="tertiary"
:title="$options.i18n.editRotationLabel"
icon="pencil"
:aria-label="$options.i18n.editRotationLabel"
:disabled="true"
/>
<gl-button
v-gl-modal="$options.deleteRotationModalId"
v-gl-tooltip
category="tertiary"
:title="$options.i18n.deleteRotationLabel"
icon="remove"
:aria-label="$options.i18n.deleteRotationLabel"
@click="setRotationToUpdate(rotation)"
/>
</gl-button-group>
</span>
<span
v-for="(timeframeItem, index) in timeframeToDraw"
:key="timeframeItemUniqueKey(timeframeItem)"
class="timeline-cell gl-border-b-solid gl-border-b-gray-100 gl-border-b-1"
:class="{ 'gl-overflow-hidden': cellShouldHideOverflow(index) }"
:style="timelineStyles"
data-testid="timeline-cell"
>
<current-day-indicator :preset-type="presetType" :timeframe-item="timeframeItem" />
<schedule-shift-wrapper
v-if="rotation.shifts"
:preset-type="presetType"
:timeframe-item="timeframeItem"
:timeframe="timeframe"
:rotation="rotation"
/>
</span>
</div>
</div>
<delete-rotation-modal
:rotation="rotationToUpdate"
:schedule-iid="scheduleIid"
......
<script>
import { PRESET_TYPES } from 'ee/oncall_schedules/constants';
import { PRESET_TYPES, DAYS_IN_DATE_WEEK } from 'ee/oncall_schedules/constants';
import getShiftTimeUnitWidthQuery from 'ee/oncall_schedules/graphql/queries/get_shift_time_unit_width.query.graphql';
import { getOverlapDateInPeriods, nDaysAfter } from '~/lib/utils/datetime_utility';
import DaysScheduleShift from './days_schedule_shift.vue';
import WeeksScheduleShift from './weeks_schedule_shift.vue';
......@@ -41,6 +42,31 @@ export default {
query: getShiftTimeUnitWidthQuery,
},
},
computed: {
currentTimeframeEndsAt() {
return new Date(
nDaysAfter(
this.timeframeItem,
this.presetType === PRESET_TYPES.DAYS ? 1 : DAYS_IN_DATE_WEEK,
),
);
},
shiftsToRender() {
const validShifts = this.rotation.shifts.nodes.filter(
({ startsAt, endsAt }) => this.shiftRangeOverlap(startsAt, endsAt).hoursOverlap > 0,
);
// TODO: If week view and on same day, dont show more than 1 assignee or use CSS to limit their size to be readable
return Object.freeze(validShifts);
},
},
methods: {
shiftRangeOverlap(shiftStartsAt, shiftEndsAt) {
return getOverlapDateInPeriods(
{ start: this.timeframeItem, end: this.currentTimeframeEndsAt },
{ start: shiftStartsAt, end: shiftEndsAt },
);
},
},
};
</script>
......@@ -48,7 +74,7 @@ export default {
<div>
<component
:is="componentByPreset[presetType]"
v-for="(shift, shiftIndex) in rotation.shifts.nodes"
v-for="(shift, shiftIndex) in shiftsToRender"
:key="shift.startAt"
:shift="shift"
:shift-index="shiftIndex"
......
......@@ -38,6 +38,10 @@ export default {
return nDaysAfter(this.timeframeItem, DAYS_IN_DATE_WEEK);
},
daysUntilEndOfTimeFrame() {
if (this.currentTimeframeEndsAt.getMonth() !== this.timeframeItem.getMonth()) {
// TODO: Handle Edge case where timeframe spans two different months
}
return (
this.currentTimeframeEndsAt.getDate() -
new Date(this.shiftRangeOverlap.overlapStartDate).getDate() +
......@@ -73,6 +77,8 @@ export default {
},
shiftShouldRender() {
if (this.timeFrameIndex !== 0) {
// TDOD: Handle edge case where this.shiftRangeOverlap.overlapStartDate is the same as this.timeframeItem
return (
new Date(this.shiftRangeOverlap.overlapStartDate) > this.timeframeItem &&
new Date(this.shiftRangeOverlap.overlapStartDate) < this.currentTimeframeEndsAt
......@@ -96,9 +102,9 @@ export default {
const baseWidth =
this.timeFrameIndex === 0
? this.totalShiftRangeOverlap.daysOverlap
: this.shiftRangeOverlap.daysOverlap;
: this.shiftRangeOverlap.daysOverlap + offset;
return baseWidth + offset;
return baseWidth;
},
timeFrameIndex() {
return this.timeframe.indexOf(this.timeframeItem);
......
......@@ -87,32 +87,6 @@ const updateScheduleFromStore = (store, query, { oncallScheduleUpdate }, variabl
});
};
const addRotationToStore = (store, query, { oncallRotationCreate, scheduleIid }, variables) => {
const rotation = oncallRotationCreate?.oncallRotation;
if (!rotation) {
return;
}
const sourceData = store.readQuery({
query,
variables,
});
const data = produce(sourceData, (draftData) => {
const scheduleToUpdate = draftData.project.incidentManagementOncallSchedules.nodes.find(
({ iid }) => iid === scheduleIid,
);
scheduleToUpdate.rotations.nodes = [...scheduleToUpdate.rotations.nodes, rotation];
});
store.writeQuery({
query,
variables,
data,
});
};
const updateRotationFromStore = (
store,
query,
......@@ -212,14 +186,6 @@ export const updateStoreAfterScheduleEdit = (store, query, data, variables) => {
}
};
export const updateStoreAfterRotationAdd = (store, query, data, variables) => {
if (hasErrors(data)) {
onError(data, UPDATE_SCHEDULE_ERROR);
} else {
addRotationToStore(store, query, data, variables);
}
};
export const updateStoreAfterRotationEdit = (store, query, data, variables) => {
if (hasErrors(data)) {
onError(data, UPDATE_ROTATION_ERROR);
......
......@@ -66,7 +66,7 @@ describe('On-call schedule', () => {
beforeEach(() => {
jest.spyOn(utils, 'getTimeframeForWeeksView').mockReturnValue(mockWeeksTimeFrame);
jest.spyOn(commonUtils, 'getFormattedTimezone').mockReturnValue(formattedTimezone);
createComponent({ schedule: mockSchedule });
createComponent({ schedule: mockSchedule, loading: false });
});
afterEach(() => {
......@@ -119,6 +119,7 @@ describe('On-call schedule', () => {
timeframe: mockWeeksTimeFrame,
rotations: expect.any(Array),
scheduleIid: mockSchedule.iid,
loading: wrapper.vm.$apollo.queries.rotations.loading,
});
});
......
......@@ -142,7 +142,6 @@ describe('AddEditRotationModal', () => {
findModal().vm.$emit('primary', { preventDefault: jest.fn() });
expect(mutate).toHaveBeenCalledWith({
mutation: expect.any(Object),
update: expect.anything(),
variables: { input: expect.objectContaining({ projectPath }) },
});
});
......@@ -174,6 +173,7 @@ describe('AddEditRotationModal', () => {
it('calls a mutation with correct parameters and creates a rotation', async () => {
createComponentWithApollo();
expect(wrapper.emitted('fetchRotationShifts')).toBeUndefined();
await createRotation(wrapper);
await awaitApolloDomMock();
......@@ -184,6 +184,7 @@ describe('AddEditRotationModal', () => {
message: i18n.rotationCreated,
type: FLASH_TYPES.SUCCESS,
});
expect(wrapper.emitted('fetchRotationShifts')).toHaveLength(1);
});
it('displays alert if mutation had a recoverable error', async () => {
......
......@@ -35,7 +35,7 @@ describe('RotationsListSectionComponent', () => {
});
}
const findTimelineCells = () => wrapper.findAll('[data-testid="timelineCell"]');
const findTimelineCells = () => wrapper.findAll('[data-testid="timeline-cell"]');
const findRotationAssignees = () => wrapper.findAllComponents(RotationsAssignee);
const findCurrentDayIndicatorContent = () =>
wrapper.find('[data-testid="current-day-indicator"]');
......
......@@ -56,7 +56,7 @@ describe('ee/oncall_schedules/components/schedule/components/shifts/components/s
describe('when the preset type is DAYS', () => {
it('should render a selection of day grid shifts inside the rotation', () => {
createComponent({ props: { presetType: PRESET_TYPES.DAYS } });
expect(findDaysScheduleShifts()).toHaveLength(2);
expect(findDaysScheduleShifts()).toHaveLength(1);
});
});
});
......@@ -75,7 +75,7 @@ describe('ee/oncall_schedules/components/schedule/components/shifts/components/w
});
expect(findRotationAssignee().props('rotationAssigneeStyle')).toEqual({
left: '52px',
width: '100px',
width: '50px',
});
});
});
......
......@@ -20742,6 +20742,9 @@ msgstr ""
msgid "OnCallSchedules|Create on-call schedules in GitLab"
msgstr ""
msgid "OnCallSchedules|Currently no rotation."
msgstr ""
msgid "OnCallSchedules|Delete rotation"
msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment