Commit 5894ca6c authored by Brandon Labuschagne's avatar Brandon Labuschagne

Merge branch '333038-fix-alerts-on-oncalls-schedules-page' into 'master'

Fix alert positioning and content for oncall schedules page

See merge request gitlab-org/gitlab!66858
parents 718b30c2 4b228dce
......@@ -222,7 +222,11 @@ export default {
break;
}
},
fetchRotationShifts() {
onRotationUpdate(message) {
this.$apollo.queries.rotations.refetch();
this.$emit('rotation-updated', message);
},
onRotationDelete() {
this.$apollo.queries.rotations.refetch();
},
setRotationToUpdate(rotation) {
......@@ -343,20 +347,20 @@ export default {
<add-edit-rotation-modal
:schedule="schedule"
:modal-id="addRotationModalId"
@fetch-rotation-shifts="fetchRotationShifts"
@rotation-updated="onRotationUpdate"
/>
<add-edit-rotation-modal
:schedule="schedule"
:modal-id="editRotationModalId"
:rotation="rotationToUpdate"
is-edit-mode
@fetch-rotation-shifts="fetchRotationShifts"
@rotation-updated="onRotationUpdate"
/>
<delete-rotation-modal
:rotation="rotationToUpdate"
:schedule="schedule"
:modal-id="deleteRotationModalId"
@fetch-rotation-shifts="fetchRotationShifts"
@rotation-deleted="onRotationDelete"
/>
</div>
</template>
......@@ -11,7 +11,6 @@ import {
} from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { s__ } from '~/locale';
import { escalationPolicyUrl } from '../constants';
import getOncallSchedulesWithRotationsQuery from '../graphql/queries/get_oncall_schedules.query.graphql';
import AddScheduleModal from './add_edit_schedule_modal.vue';
import OncallSchedule from './oncall_schedule.vue';
......@@ -31,7 +30,7 @@ export const i18n = {
successNotification: {
title: s__('OnCallSchedules|Try adding a rotation'),
description: s__(
'OnCallSchedules|Your schedule has been successfully created. To add individual users to this schedule, use the add a rotation button. To create an escalation policy that defines which schedule is used when, visit the %{linkStart}escalation policy%{linkEnd} page.',
'OnCallSchedules|Your schedule has been successfully created. To add individual users to this schedule, use the Add a rotation button. To enable notifications for this schedule, you must also create an %{linkStart}escalation policy%{linkEnd}.',
),
},
};
......@@ -39,7 +38,6 @@ export const i18n = {
export default {
i18n,
addScheduleModalId,
escalationPolicyUrl,
components: {
GlAlert,
GlButton,
......@@ -54,11 +52,13 @@ export default {
GlModal: GlModalDirective,
GlTooltip: GlTooltipDirective,
},
inject: ['emptyOncallSchedulesSvgPath', 'projectPath'],
inject: ['emptyOncallSchedulesSvgPath', 'projectPath', 'escalationPoliciesPath'],
data() {
return {
schedules: [],
showSuccessNotification: false,
showScheduleCreatedNotification: false,
showRotationUpdatedNotification: false,
rotationUpdateMsg: null,
};
},
apollo: {
......@@ -85,6 +85,13 @@ export default {
return this.schedules.length;
},
},
methods: {
onRotationUpdate(message) {
this.showScheduleCreatedNotification = false;
this.showRotationUpdatedNotification = true;
this.rotationUpdateMsg = message;
},
},
};
</script>
......@@ -108,26 +115,38 @@ export default {
{{ $options.i18n.add.button }}
</gl-button>
</div>
<gl-alert
v-if="showSuccessNotification"
v-if="showScheduleCreatedNotification"
data-testid="tip-alert"
variant="tip"
:title="$options.i18n.successNotification.title"
class="gl-my-3"
@dismiss="showSuccessNotification = false"
@dismiss="showScheduleCreatedNotification = false"
>
<gl-sprintf :message="$options.i18n.successNotification.description">
<template #link="{ content }">
<gl-link :href="$options.escalationPolicyUrl" target="_blank">
{{ content }}
</gl-link>
<gl-link :href="escalationPoliciesPath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</gl-alert>
<gl-alert
v-if="showRotationUpdatedNotification"
data-testid="success-alert"
variant="success"
class="gl-my-3"
@dismiss="showRotationUpdatedNotification = false"
>
{{ rotationUpdateMsg }}
</gl-alert>
<oncall-schedule
v-for="(schedule, scheduleIndex) in schedules"
:key="schedule.iid"
:schedule="schedule"
:schedule-index="scheduleIndex"
@rotation-updated="onRotationUpdate"
/>
</template>
......@@ -145,7 +164,7 @@ export default {
</gl-empty-state>
<add-schedule-modal
:modal-id="$options.addScheduleModalId"
@scheduleCreated="showSuccessNotification = true"
@scheduleCreated="showScheduleCreatedNotification = true"
/>
</div>
</template>
......@@ -12,7 +12,6 @@ import {
parseHour,
parseRotationDate,
} from 'ee/oncall_schedules/utils/common_utils';
import createFlash, { FLASH_TYPES } from '~/flash';
import searchProjectMembersQuery from '~/graphql_shared/queries/project_user_members_search.query.graphql';
import { format24HourTimeStringFromInt, formatDate } from '~/lib/utils/datetime_utility';
import { s__, __ } from '~/locale';
......@@ -226,11 +225,7 @@ export default {
}
this.$refs.addEditScheduleRotationModal.hide();
this.$emit('fetch-rotation-shifts');
return createFlash({
message: this.$options.i18n.rotationCreated,
type: FLASH_TYPES.SUCCESS,
});
this.$emit('rotation-updated', i18n.rotationCreated);
},
)
.catch((error) => {
......@@ -275,11 +270,7 @@ export default {
}
this.$refs.addEditScheduleRotationModal.hide();
this.$emit('fetch-rotation-shifts');
return createFlash({
message: this.$options.i18n.editedRotation,
type: FLASH_TYPES.SUCCESS,
});
this.$emit('rotation-updated', i18n.editedRotation);
},
)
.catch((error) => {
......
......@@ -93,7 +93,7 @@ export default {
if (error) {
throw error;
}
this.$emit('fetch-rotation-shifts');
this.$emit('rotation-deleted');
this.$refs.deleteRotationModal.hide();
})
.catch((error) => {
......
......@@ -54,6 +54,3 @@ export const SHIFT_WIDTH_CALCULATION_DELAY = 250;
export const oneHourOffsetDayView = 100 / HOURS_IN_DAY;
export const oneDayOffsetWeekView = 100 / DAYS_IN_WEEK;
export const oneHourOffsetWeekView = oneDayOffsetWeekView / HOURS_IN_DAY;
// TODO: Replace with href to documentation once https://gitlab.com/groups/gitlab-org/-/epics/4638 is completed
export const escalationPolicyUrl = 'https://gitlab.com/groups/gitlab-org/-/epics/4638';
......@@ -11,7 +11,12 @@ export default () => {
if (!el) return null;
const { projectPath, emptyOncallSchedulesSvgPath, timezones } = el.dataset;
const {
projectPath,
emptyOncallSchedulesSvgPath,
timezones,
escalationPoliciesPath,
} = el.dataset;
apolloProvider.clients.defaultClient.cache.writeQuery({
query: getTimelineWidthQuery,
......@@ -27,6 +32,7 @@ export default () => {
projectPath,
emptyOncallSchedulesSvgPath,
timezones: JSON.parse(timezones),
escalationPoliciesPath,
},
render(createElement) {
return createElement(OnCallSchedulesWrapper);
......
......@@ -6,7 +6,8 @@ module IncidentManagement
{
'project-path' => project.full_path,
'empty-oncall-schedules-svg-path' => image_path('illustrations/empty-state/empty-on-call.svg'),
'timezones' => timezone_data(format: :full).to_json
'timezones' => timezone_data(format: :full).to_json,
'escalation-policies-path' => project_incident_management_escalation_policies_path(project)
}
end
end
......
import { GlEmptyState, GlLoadingIcon, GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import { GlEmptyState, GlLoadingIcon, GlSprintf, GlLink } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import AddScheduleModal from 'ee/oncall_schedules/components/add_edit_schedule_modal.vue';
import OnCallSchedule from 'ee/oncall_schedules/components/oncall_schedule.vue';
import OnCallScheduleWrapper, {
i18n,
} from 'ee/oncall_schedules/components/oncall_schedules_wrapper.vue';
import { escalationPolicyUrl } from 'ee/oncall_schedules/constants';
import getOncallSchedulesWithRotationsQuery from 'ee/oncall_schedules/graphql/queries/get_oncall_schedules.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
......@@ -19,6 +19,7 @@ describe('On-call schedule wrapper', () => {
let wrapper;
const emptyOncallSchedulesSvgPath = 'illustration/path.svg';
const projectPath = 'group/project';
const escalationPoliciesPath = 'group/project/-/escalation_policies';
function mountComponent({ loading, schedules } = {}) {
const $apollo = {
......@@ -39,6 +40,7 @@ describe('On-call schedule wrapper', () => {
provide: {
emptyOncallSchedulesSvgPath,
projectPath,
escalationPoliciesPath,
},
directives: {
GlTooltip: createMockDirective(),
......@@ -70,6 +72,7 @@ describe('On-call schedule wrapper', () => {
provide: {
emptyOncallSchedulesSvgPath,
projectPath,
escalationPoliciesPath,
},
});
}
......@@ -83,7 +86,8 @@ describe('On-call schedule wrapper', () => {
const findLoader = () => wrapper.findComponent(GlLoadingIcon);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findSchedules = () => wrapper.findAllComponents(OnCallSchedule);
const findAlert = () => wrapper.findComponent(GlAlert);
const findTipAlert = () => wrapper.findByTestId('tip-alert');
const findSuccessAlert = () => wrapper.findByTestId('success-alert');
const findAlertLink = () => wrapper.findComponent(GlLink);
const findModal = () => wrapper.findComponent(AddScheduleModal);
const findAddAdditionalButton = () => wrapper.findByTestId('add-additional-schedules-button');
......@@ -127,12 +131,25 @@ describe('On-call schedule wrapper', () => {
expect(button.attributes('title')).toBe(i18n.add.tooltip);
});
it('shows success alert on new schedule creation', async () => {
it('shows alert with a tip on new schedule creation', async () => {
await findModal().vm.$emit('scheduleCreated');
const alert = findAlert();
const alert = findTipAlert();
expect(alert.exists()).toBe(true);
expect(alert.props('title')).toBe(i18n.successNotification.title);
expect(findAlertLink().attributes('href')).toBe(escalationPolicyUrl);
expect(findAlertLink().attributes('href')).toBe(escalationPoliciesPath);
});
it("hides tip alert and shows success alert on schedule's rotation update", async () => {
await findModal().vm.$emit('scheduleCreated');
expect(findTipAlert().exists()).toBe(true);
const rotationUpdateMsg = 'Rotation updated';
findSchedules().at(0).vm.$emit('rotation-updated', rotationUpdateMsg);
await nextTick();
expect(findTipAlert().exists()).toBe(false);
const successAlert = findSuccessAlert();
expect(successAlert.exists()).toBe(true);
expect(successAlert.text()).toBe(rotationUpdateMsg);
});
});
......@@ -152,7 +169,7 @@ describe('On-call schedule wrapper', () => {
it('should render newly created schedule', async () => {
mountComponentWithApollo();
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
await nextTick();
const schedule = findSchedules().at(1);
expect(schedule.props('schedule')).toEqual(newlyCreatedSchedule);
});
......
......@@ -10,7 +10,6 @@ import createOncallScheduleRotationMutation from 'ee/oncall_schedules/graphql/mu
import getOncallSchedulesWithRotationsQuery from 'ee/oncall_schedules/graphql/queries/get_oncall_schedules.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash, { FLASH_TYPES } from '~/flash';
import searchProjectMembersQuery from '~/graphql_shared/queries/project_user_members_search.query.graphql';
import {
participants,
......@@ -331,18 +330,17 @@ describe('AddEditRotationModal', () => {
it('calls a mutation with correct parameters and creates a rotation', async () => {
createComponentWithApollo();
expect(wrapper.emitted('fetch-rotation-shifts')).toBeUndefined();
expect(wrapper.emitted('rotation-updated')).toBeUndefined();
await createRotation(wrapper);
await awaitApolloDomMock();
expect(mockHideModal).toHaveBeenCalled();
expect(createRotationHandler).toHaveBeenCalled();
expect(createFlash).toHaveBeenCalledWith({
message: i18n.rotationCreated,
type: FLASH_TYPES.SUCCESS,
});
expect(wrapper.emitted('fetch-rotation-shifts')).toHaveLength(1);
const emittedEvents = wrapper.emitted('rotation-updated');
const emittedMsg = emittedEvents[0][0];
expect(emittedEvents).toHaveLength(1);
expect(emittedMsg).toBe(i18n.rotationCreated);
});
it('displays alert if mutation had a recoverable error', async () => {
......
......@@ -133,11 +133,13 @@ describe('DeleteRotationModal', () => {
});
});
it('hides the modal on successful rotation deletion', async () => {
it('hides the modal and emits the events on successful rotation deletion', async () => {
expect(wrapper.emitted('rotation-deleted')).toBeUndefined();
mutate.mockResolvedValueOnce({ data: { oncallRotationDestroy: { errors: [] } } });
findModal().vm.$emit('primary', { preventDefault: jest.fn() });
await waitForPromises();
expect(mockHideModal).toHaveBeenCalled();
expect(wrapper.emitted('rotation-deleted')).toHaveLength(1);
});
it('does not hide the modal on deletion fail and shows the error alert', async () => {
......
......@@ -12,7 +12,8 @@ RSpec.describe IncidentManagement::OncallScheduleHelper do
is_expected.to eq(
'project-path' => project.full_path,
'empty-oncall-schedules-svg-path' => helper.image_path('illustrations/empty-state/empty-on-call.svg'),
'timezones' => helper.timezone_data(format: :full).to_json
'timezones' => helper.timezone_data(format: :full).to_json,
'escalation-policies-path' => project_incident_management_escalation_policies_path(project)
)
end
end
......
......@@ -22894,7 +22894,7 @@ msgstr ""
msgid "OnCallSchedules|You are currently a part of:"
msgstr ""
msgid "OnCallSchedules|Your schedule has been successfully created. To add individual users to this schedule, use the add a rotation button. To create an escalation policy that defines which schedule is used when, visit the %{linkStart}escalation policy%{linkEnd} page."
msgid "OnCallSchedules|Your schedule has been successfully created. To add individual users to this schedule, use the Add a rotation button. To enable notifications for this schedule, you must also create an %{linkStart}escalation policy%{linkEnd}."
msgstr ""
msgid "OnDemandScans|Could not fetch scanner profiles. Please refresh the page, or try again later."
......
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