Commit 724cffdf authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '262848-schedule-create-success' into 'master'

Handle schedule create success (cache and alert)

See merge request gitlab-org/gitlab!49158
parents 0860559f 92a8a848
...@@ -9,6 +9,7 @@ import { ...@@ -9,6 +9,7 @@ import {
GlSearchBoxByType, GlSearchBoxByType,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import { getFormattedTimezone } from '../utils/common_utils';
export const i18n = { export const i18n = {
selectTimezone: s__('OnCallSchedules|Select timezone'), selectTimezone: s__('OnCallSchedules|Select timezone'),
...@@ -90,7 +91,7 @@ export default { ...@@ -90,7 +91,7 @@ export default {
}, },
methods: { methods: {
getFormattedTimezone(tz) { getFormattedTimezone(tz) {
return __(`(UTC${tz.formatted_offset}) ${tz.abbr} ${tz.name}`); return getFormattedTimezone(tz);
}, },
isTimezoneSelected(tz) { isTimezoneSelected(tz) {
return isEqual(tz, this.form.timezone); return isEqual(tz, this.form.timezone);
......
...@@ -2,8 +2,10 @@ ...@@ -2,8 +2,10 @@
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { GlModal, GlAlert } from '@gitlab/ui'; import { GlModal, GlAlert } from '@gitlab/ui';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import getOncallSchedulesQuery from '../graphql/queries/get_oncall_schedules.query.graphql';
import createOncallScheduleMutation from '../graphql/mutations/create_oncall_schedule.mutation.graphql'; import createOncallScheduleMutation from '../graphql/mutations/create_oncall_schedule.mutation.graphql';
import AddEditScheduleForm from './add_edit_schedule_form.vue'; import AddEditScheduleForm from './add_edit_schedule_form.vue';
import { updateStoreOnScheduleCreate } from '../utils/cache_updates';
export const i18n = { export const i18n = {
cancel: __('Cancel'), cancel: __('Cancel'),
...@@ -65,23 +67,35 @@ export default { ...@@ -65,23 +67,35 @@ export default {
methods: { methods: {
createSchedule() { createSchedule() {
this.loading = true; this.loading = true;
const { projectPath } = this;
this.$apollo this.$apollo
.mutate({ .mutate({
mutation: createOncallScheduleMutation, mutation: createOncallScheduleMutation,
variables: { variables: {
oncallScheduleCreateInput: { oncallScheduleCreateInput: {
projectPath: this.projectPath, projectPath,
...this.form, ...this.form,
timezone: this.form.timezone.identifier, timezone: this.form.timezone.identifier,
}, },
}, },
update(
store,
{
data: { oncallScheduleCreate },
},
) {
updateStoreOnScheduleCreate(store, getOncallSchedulesQuery, oncallScheduleCreate, {
projectPath,
});
},
}) })
.then(({ data: { oncallScheduleCreate: { errors: [error] } } }) => { .then(({ data: { oncallScheduleCreate: { errors: [error] } } }) => {
if (error) { if (error) {
throw error; throw error;
} }
this.$refs.createScheduleModal.hide(); this.$refs.createScheduleModal.hide();
this.$emit('scheduleCreated');
}) })
.catch(error => { .catch(error => {
this.error = error; this.error = error;
......
...@@ -6,10 +6,9 @@ import DeleteScheduleModal from './delete_schedule_modal.vue'; ...@@ -6,10 +6,9 @@ import DeleteScheduleModal from './delete_schedule_modal.vue';
import EditScheduleModal from './edit_schedule_modal.vue'; import EditScheduleModal from './edit_schedule_modal.vue';
import { getTimeframeForWeeksView } from './schedule/utils'; import { getTimeframeForWeeksView } from './schedule/utils';
import { PRESET_TYPES } from './schedule/constants'; import { PRESET_TYPES } from './schedule/constants';
import { getFormattedTimezone } from '../utils'; import { getFormattedTimezone } from '../utils/common_utils';
export const i18n = { export const i18n = {
title: s__('OnCallSchedules|On-call schedule'),
scheduleForTz: s__('OnCallSchedules|On-call schedule for the %{tzShort}'), scheduleForTz: s__('OnCallSchedules|On-call schedule for the %{tzShort}'),
updateScheduleLabel: s__('OnCallSchedules|Edit schedule'), updateScheduleLabel: s__('OnCallSchedules|Edit schedule'),
destroyScheduleLabel: s__('OnCallSchedules|Delete schedule'), destroyScheduleLabel: s__('OnCallSchedules|Delete schedule'),
...@@ -51,7 +50,6 @@ export default { ...@@ -51,7 +50,6 @@ export default {
<template> <template>
<div> <div>
<h2>{{ $options.i18n.title }}</h2>
<gl-card> <gl-card>
<template #header> <template #header>
<div class="gl-display-flex gl-justify-content-space-between gl-m-0"> <div class="gl-display-flex gl-justify-content-space-between gl-m-0">
......
<script> <script>
import { GlEmptyState, GlButton, GlLoadingIcon, GlModalDirective } from '@gitlab/ui'; import { GlAlert, GlButton, GlEmptyState, GlLoadingIcon, GlModalDirective } from '@gitlab/ui';
import * as Sentry from '~/sentry/wrapper'; import * as Sentry from '~/sentry/wrapper';
import AddScheduleModal from './add_schedule_modal.vue'; import AddScheduleModal from './add_schedule_modal.vue';
import OncallSchedule from './oncall_schedule.vue'; import OncallSchedule from './oncall_schedule.vue';
...@@ -10,11 +10,18 @@ import { fetchPolicies } from '~/lib/graphql'; ...@@ -10,11 +10,18 @@ import { fetchPolicies } from '~/lib/graphql';
const addScheduleModalId = 'addScheduleModal'; const addScheduleModalId = 'addScheduleModal';
export const i18n = { export const i18n = {
title: s__('OnCallSchedules|On-call schedule'),
emptyState: { emptyState: {
title: s__('OnCallSchedules|Create on-call schedules in GitLab'), title: s__('OnCallSchedules|Create on-call schedules in GitLab'),
description: s__('OnCallSchedules|Route alerts directly to specific members of your team'), description: s__('OnCallSchedules|Route alerts directly to specific members of your team'),
button: s__('OnCallSchedules|Add a schedule'), button: s__('OnCallSchedules|Add a schedule'),
}, },
successNotification: {
title: s__('OnCallSchedules|Try adding a rotation'),
description: s__(
'OnCallSchedules|Your schedule has been successfully created and all alerts from this project will now be routed to this schedule. Currently, only one schedule can be created per project. More coming soon! To add individual users to this schedule, use the add a rotation button.',
),
},
}; };
export default { export default {
...@@ -22,8 +29,9 @@ export default { ...@@ -22,8 +29,9 @@ export default {
addScheduleModalId, addScheduleModalId,
inject: ['emptyOncallSchedulesSvgPath', 'projectPath'], inject: ['emptyOncallSchedulesSvgPath', 'projectPath'],
components: { components: {
GlEmptyState, GlAlert,
GlButton, GlButton,
GlEmptyState,
GlLoadingIcon, GlLoadingIcon,
AddScheduleModal, AddScheduleModal,
OncallSchedule, OncallSchedule,
...@@ -34,6 +42,7 @@ export default { ...@@ -34,6 +42,7 @@ export default {
data() { data() {
return { return {
schedule: {}, schedule: {},
showSuccessNotification: false,
}; };
}, },
apollo: { apollo: {
...@@ -46,7 +55,8 @@ export default { ...@@ -46,7 +55,8 @@ export default {
}; };
}, },
update(data) { update(data) {
return data?.project?.incidentManagementOncallSchedules?.nodes?.[0] ?? null; const nodes = data.project?.incidentManagementOncallSchedules?.nodes ?? [];
return nodes.length ? nodes[nodes.length - 1] : null;
}, },
error(error) { error(error) {
Sentry.captureException(error); Sentry.captureException(error);
...@@ -64,7 +74,21 @@ export default { ...@@ -64,7 +74,21 @@ export default {
<template> <template>
<div> <div>
<gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-3" /> <gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-3" />
<oncall-schedule v-else-if="schedule" :schedule="schedule" />
<template v-else-if="schedule">
<h2>{{ $options.i18n.title }}</h2>
<gl-alert
v-if="showSuccessNotification"
variant="tip"
:title="$options.i18n.successNotification.title"
class="gl-my-3"
@dismiss="showSuccessNotification = false"
>
{{ $options.i18n.successNotification.description }}
</gl-alert>
<oncall-schedule :schedule="schedule" />
</template>
<gl-empty-state <gl-empty-state
v-else v-else
:title="$options.i18n.emptyState.title" :title="$options.i18n.emptyState.title"
...@@ -77,6 +101,9 @@ export default { ...@@ -77,6 +101,9 @@ export default {
</gl-button> </gl-button>
</template> </template>
</gl-empty-state> </gl-empty-state>
<add-schedule-modal :modal-id="$options.addScheduleModalId" /> <add-schedule-modal
:modal-id="$options.addScheduleModalId"
@scheduleCreated="showSuccessNotification = true"
/>
</div> </div>
</template> </template>
...@@ -3,6 +3,27 @@ import createFlash from '~/flash'; ...@@ -3,6 +3,27 @@ import createFlash from '~/flash';
import { DELETE_SCHEDULE_ERROR, UPDATE_SCHEDULE_ERROR } from './error_messages'; import { DELETE_SCHEDULE_ERROR, UPDATE_SCHEDULE_ERROR } from './error_messages';
const addScheduleToStore = (store, query, { oncallSchedule: schedule }, variables) => {
if (!schedule) {
return;
}
const sourceData = store.readQuery({
query,
variables,
});
const data = produce(sourceData, draftData => {
draftData.project.incidentManagementOncallSchedules.nodes.push(schedule);
});
store.writeQuery({
query,
variables,
data,
});
};
const deleteScheduleFromStore = (store, query, { oncallScheduleDestroy }, variables) => { const deleteScheduleFromStore = (store, query, { oncallScheduleDestroy }, variables) => {
const schedule = oncallScheduleDestroy?.oncallSchedule; const schedule = oncallScheduleDestroy?.oncallSchedule;
if (!schedule) { if (!schedule) {
...@@ -61,6 +82,12 @@ const onError = (data, message) => { ...@@ -61,6 +82,12 @@ const onError = (data, message) => {
export const hasErrors = ({ errors = [] }) => errors?.length; export const hasErrors = ({ errors = [] }) => errors?.length;
export const updateStoreOnScheduleCreate = (store, query, data, variables) => {
if (!hasErrors(data)) {
addScheduleToStore(store, query, data, variables);
}
};
export const updateStoreAfterScheduleDelete = (store, query, data, variables) => { export const updateStoreAfterScheduleDelete = (store, query, data, variables) => {
if (hasErrors(data)) { if (hasErrors(data)) {
onError(data, DELETE_SCHEDULE_ERROR); onError(data, DELETE_SCHEDULE_ERROR);
......
...@@ -11,7 +11,7 @@ import { sprintf, __ } from '~/locale'; ...@@ -11,7 +11,7 @@ import { sprintf, __ } from '~/locale';
* @returns {String} * @returns {String}
*/ */
export const getFormattedTimezone = tz => { export const getFormattedTimezone = tz => {
return sprintf(__('(UTC%{offset}) %{timezone}'), { return sprintf(__('(UTC %{offset}) %{timezone}'), {
offset: tz.formatted_offset, offset: tz.formatted_offset,
timezone: `${tz.abbr} ${tz.name}`, timezone: `${tz.abbr} ${tz.name}`,
}); });
......
...@@ -42,7 +42,7 @@ exports[`AddEditScheduleForm renders modal layout 1`] = ` ...@@ -42,7 +42,7 @@ exports[`AddEditScheduleForm renders modal layout 1`] = `
headertext="Select timezone" headertext="Select timezone"
id="schedule-timezone" id="schedule-timezone"
size="medium" size="medium"
text="(UTC-12:00) -12 International Date Line West" text="(UTC -12:00) -12 International Date Line West"
variant="default" variant="default"
> >
<gl-search-box-by-type-stub <gl-search-box-by-type-stub
...@@ -63,7 +63,7 @@ exports[`AddEditScheduleForm renders modal layout 1`] = ` ...@@ -63,7 +63,7 @@ exports[`AddEditScheduleForm renders modal layout 1`] = `
<span <span
class="gl-white-space-nowrap" class="gl-white-space-nowrap"
> >
(UTC-12:00) -12 International Date Line West (UTC -12:00) -12 International Date Line West
</span> </span>
</gl-dropdown-item-stub> </gl-dropdown-item-stub>
<gl-dropdown-item-stub <gl-dropdown-item-stub
...@@ -78,7 +78,7 @@ exports[`AddEditScheduleForm renders modal layout 1`] = ` ...@@ -78,7 +78,7 @@ exports[`AddEditScheduleForm renders modal layout 1`] = `
<span <span
class="gl-white-space-nowrap" class="gl-white-space-nowrap"
> >
(UTC-11:00) SST American Samoa (UTC -11:00) SST American Samoa
</span> </span>
</gl-dropdown-item-stub> </gl-dropdown-item-stub>
<gl-dropdown-item-stub <gl-dropdown-item-stub
...@@ -93,7 +93,7 @@ exports[`AddEditScheduleForm renders modal layout 1`] = ` ...@@ -93,7 +93,7 @@ exports[`AddEditScheduleForm renders modal layout 1`] = `
<span <span
class="gl-white-space-nowrap" class="gl-white-space-nowrap"
> >
(UTC-11:00) SST Midway Island (UTC -11:00) SST Midway Island
</span> </span>
</gl-dropdown-item-stub> </gl-dropdown-item-stub>
<gl-dropdown-item-stub <gl-dropdown-item-stub
...@@ -108,7 +108,7 @@ exports[`AddEditScheduleForm renders modal layout 1`] = ` ...@@ -108,7 +108,7 @@ exports[`AddEditScheduleForm renders modal layout 1`] = `
<span <span
class="gl-white-space-nowrap" class="gl-white-space-nowrap"
> >
(UTC-10:00) HST Hawaii (UTC -10:00) HST Hawaii
</span> </span>
</gl-dropdown-item-stub> </gl-dropdown-item-stub>
......
...@@ -67,7 +67,7 @@ describe('AddEditScheduleForm', () => { ...@@ -67,7 +67,7 @@ describe('AddEditScheduleForm', () => {
it('formats each option', () => { it('formats each option', () => {
findDropdownOptions().wrappers.forEach((option, index) => { findDropdownOptions().wrappers.forEach((option, index) => {
const tz = mockTimezones[index]; const tz = mockTimezones[index];
const expectedValue = `(UTC${tz.formatted_offset}) ${tz.abbr} ${tz.name}`; const expectedValue = `(UTC ${tz.formatted_offset}) ${tz.abbr} ${tz.name}`;
expect(option.text()).toBe(expectedValue); expect(option.text()).toBe(expectedValue);
}); });
}); });
......
...@@ -10,13 +10,14 @@ describe('AddScheduleModal', () => { ...@@ -10,13 +10,14 @@ describe('AddScheduleModal', () => {
const projectPath = 'group/project'; const projectPath = 'group/project';
const mutate = jest.fn(); const mutate = jest.fn();
const mockHideModal = jest.fn(); const mockHideModal = jest.fn();
const formData =
getOncallSchedulesQueryResponse.data.project.incidentManagementOncallSchedules.nodes[0];
const createComponent = ({ data = {}, props = {} } = {}) => { const createComponent = ({ data = {}, props = {} } = {}) => {
wrapper = shallowMount(AddScheduleModal, { wrapper = shallowMount(AddScheduleModal, {
data() { data() {
return { return {
form: form: formData,
getOncallSchedulesQueryResponse.data.project.incidentManagementOncallSchedules.nodes[0],
...data, ...data,
}; };
}, },
...@@ -60,7 +61,14 @@ describe('AddScheduleModal', () => { ...@@ -60,7 +61,14 @@ describe('AddScheduleModal', () => {
findModal().vm.$emit('primary', { preventDefault: jest.fn() }); findModal().vm.$emit('primary', { preventDefault: jest.fn() });
expect(mutate).toHaveBeenCalledWith({ expect(mutate).toHaveBeenCalledWith({
mutation: expect.any(Object), mutation: expect.any(Object),
variables: { oncallScheduleCreateInput: expect.objectContaining({ projectPath }) }, update: expect.any(Function),
variables: {
oncallScheduleCreateInput: {
projectPath,
...formData,
timezone: formData.timezone.identifier,
},
},
}); });
}); });
......
import { getFormattedTimezone } from 'ee/oncall_schedules/utils'; import { getFormattedTimezone } from 'ee/oncall_schedules/utils/common_utils';
import mockTimezones from './mocks/mockTimezones.json'; import mockTimezones from './mocks/mockTimezones.json';
describe('getFormattedTimezone', () => { describe('getFormattedTimezone', () => {
it('formats the timezone', () => { it('formats the timezone', () => {
const tz = mockTimezones[0]; const tz = mockTimezones[0];
const expectedValue = `(UTC${tz.formatted_offset}) ${tz.abbr} ${tz.name}`; const expectedValue = `(UTC ${tz.formatted_offset}) ${tz.abbr} ${tz.name}`;
expect(getFormattedTimezone(tz)).toBe(expectedValue); expect(getFormattedTimezone(tz)).toBe(expectedValue);
}); });
}); });
...@@ -25,7 +25,9 @@ export const getOncallSchedulesQueryResponse = { ...@@ -25,7 +25,9 @@ export const getOncallSchedulesQueryResponse = {
iid: '37', iid: '37',
name: 'Test schedule', name: 'Test schedule',
description: 'Description 1 lives here', description: 'Description 1 lives here',
timezone: 'Pacific/Honolulu', timezone: {
identifier: 'Pacific/Honolulu',
},
}, },
], ],
}, },
...@@ -81,3 +83,17 @@ export const updateScheduleResponse = { ...@@ -81,3 +83,17 @@ export const updateScheduleResponse = {
}, },
}, },
}; };
export const preExistingSchedule = {
description: 'description',
iid: '1',
name: 'Monitor rotations',
timezone: 'Pacific/Honolulu',
};
export const newlyCreatedSchedule = {
description: 'description',
iid: '2',
name: 'S-Monitor rotations',
timezone: 'Kyiv/EST',
};
...@@ -3,7 +3,7 @@ import { GlCard, GlSprintf } from '@gitlab/ui'; ...@@ -3,7 +3,7 @@ import { GlCard, GlSprintf } from '@gitlab/ui';
import OnCallSchedule, { i18n } from 'ee/oncall_schedules/components/oncall_schedule.vue'; import OnCallSchedule, { i18n } from 'ee/oncall_schedules/components/oncall_schedule.vue';
import ScheduleTimelineSection from 'ee/oncall_schedules/components/schedule/components/schedule_timeline_section.vue'; import ScheduleTimelineSection from 'ee/oncall_schedules/components/schedule/components/schedule_timeline_section.vue';
import * as utils from 'ee/oncall_schedules/components/schedule/utils'; import * as utils from 'ee/oncall_schedules/components/schedule/utils';
import * as commonUtils from 'ee/oncall_schedules/utils'; import * as commonUtils from 'ee/oncall_schedules/utils/common_utils';
import { PRESET_TYPES } from 'ee/oncall_schedules/components/schedule/constants'; import { PRESET_TYPES } from 'ee/oncall_schedules/components/schedule/constants';
import mockTimezones from './mocks/mockTimezones.json'; import mockTimezones from './mocks/mockTimezones.json';
......
import { shallowMount } from '@vue/test-utils'; import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui'; import { GlEmptyState, GlLoadingIcon, GlAlert } from '@gitlab/ui';
import OnCallScheduleWrapper, { import OnCallScheduleWrapper, {
i18n, i18n,
} from 'ee/oncall_schedules/components/oncall_schedules_wrapper.vue'; } from 'ee/oncall_schedules/components/oncall_schedules_wrapper.vue';
import OnCallSchedule from 'ee/oncall_schedules/components/oncall_schedule.vue'; import OnCallSchedule from 'ee/oncall_schedules/components/oncall_schedule.vue';
import AddScheduleModal from 'ee/oncall_schedules/components/add_schedule_modal.vue';
import createMockApollo from 'jest/helpers/mock_apollo_helper';
import getOncallSchedulesQuery from 'ee/oncall_schedules/graphql/queries/get_oncall_schedules.query.graphql';
import VueApollo from 'vue-apollo';
import { preExistingSchedule, newlyCreatedSchedule } from './mocks/apollo_mock';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('On-call schedule wrapper', () => { describe('On-call schedule wrapper', () => {
let wrapper; let wrapper;
...@@ -33,14 +41,38 @@ describe('On-call schedule wrapper', () => { ...@@ -33,14 +41,38 @@ describe('On-call schedule wrapper', () => {
}); });
} }
let getOncallSchedulesQuerySpy;
function mountComponentWithApollo() {
const fakeApollo = createMockApollo([[getOncallSchedulesQuery, getOncallSchedulesQuerySpy]]);
wrapper = shallowMount(OnCallScheduleWrapper, {
localVue,
apolloProvider: fakeApollo,
data() {
return {
schedule: {},
};
},
provide: {
emptyOncallSchedulesSvgPath,
projectPath,
},
});
}
afterEach(() => { afterEach(() => {
wrapper.destroy(); if (wrapper) {
wrapper = null; wrapper.destroy();
wrapper = null;
}
}); });
const findLoader = () => wrapper.find(GlLoadingIcon); const findLoader = () => wrapper.find(GlLoadingIcon);
const findEmptyState = () => wrapper.find(GlEmptyState); const findEmptyState = () => wrapper.find(GlEmptyState);
const findSchedule = () => wrapper.find(OnCallSchedule); const findSchedule = () => wrapper.find(OnCallSchedule);
const findAlert = () => wrapper.find(GlAlert);
const findModal = () => wrapper.find(AddScheduleModal);
it('shows a loader while data is requested', () => { it('shows a loader while data is requested', () => {
mountComponent({ loading: true }); mountComponent({ loading: true });
...@@ -59,11 +91,49 @@ describe('On-call schedule wrapper', () => { ...@@ -59,11 +91,49 @@ describe('On-call schedule wrapper', () => {
}); });
}); });
it('renders On-call schedule when data received ', () => { describe('Schedule created', () => {
mountComponent({ loading: false, schedule: { name: 'monitor rotation' } }); beforeEach(() => {
const schedule = findSchedule(); mountComponent({ loading: false, schedule: { name: 'monitor rotation' } });
expect(findLoader().exists()).toBe(false); });
expect(findEmptyState().exists()).toBe(false);
expect(schedule.exists()).toBe(true); it('renders the schedule when data received ', () => {
expect(findLoader().exists()).toBe(false);
expect(findEmptyState().exists()).toBe(false);
expect(findSchedule().exists()).toBe(true);
});
it('shows success alert', async () => {
await findModal().vm.$emit('scheduleCreated');
const alert = findAlert();
expect(alert.exists()).toBe(true);
expect(alert.props('title')).toBe(i18n.successNotification.title);
expect(alert.text()).toBe(i18n.successNotification.description);
});
it('renders a newly created schedule', async () => {
await findModal().vm.$emit('scheduleCreated');
expect(findSchedule().exists()).toBe(true);
});
});
describe('Apollo', () => {
beforeEach(() => {
getOncallSchedulesQuerySpy = jest.fn().mockResolvedValue({
data: {
project: {
incidentManagementOncallSchedules: {
nodes: [preExistingSchedule, newlyCreatedSchedule],
},
},
},
});
});
it('should render newly create schedule', async () => {
mountComponentWithApollo();
jest.runOnlyPendingTimers();
await wrapper.vm.$nextTick();
expect(findSchedule().props('schedule')).toEqual(newlyCreatedSchedule);
});
}); });
}); });
...@@ -947,7 +947,7 @@ msgstr "" ...@@ -947,7 +947,7 @@ msgstr ""
msgid "(No changes)" msgid "(No changes)"
msgstr "" msgstr ""
msgid "(UTC%{offset}) %{timezone}" msgid "(UTC %{offset}) %{timezone}"
msgstr "" msgstr ""
msgid "(check progress)" msgid "(check progress)"
...@@ -19187,6 +19187,12 @@ msgstr "" ...@@ -19187,6 +19187,12 @@ msgstr ""
msgid "OnCallSchedules|The schedule could not be updated. Please try again." msgid "OnCallSchedules|The schedule could not be updated. Please try again."
msgstr "" msgstr ""
msgid "OnCallSchedules|Try adding a rotation"
msgstr ""
msgid "OnCallSchedules|Your schedule has been successfully created and all alerts from this project will now be routed to this schedule. Currently, only one schedule can be created per project. More coming soon! To add individual users to this schedule, use the add a rotation button."
msgstr ""
msgid "OnDemandScans|Could not fetch scanner profiles. Please refresh the page, or try again later." msgid "OnDemandScans|Could not fetch scanner profiles. Please refresh the page, or try again later."
msgstr "" 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