Commit fa153a3c authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 20096111 4d99f533
......@@ -86,7 +86,7 @@ export default {
<form ref="settingsForm" @submit.prevent="updateAlertsIntegrationSettings">
<gl-form-group class="gl-pl-0">
<gl-form-checkbox v-model="createIssueEnabled" data-qa-selector="create_issue_checkbox">
<span>{{ $options.i18n.createIssue.label }}</span>
<span>{{ $options.i18n.createIncident.label }}</span>
</gl-form-checkbox>
</gl-form-group>
......@@ -96,7 +96,7 @@ export default {
class="col-8 col-md-9 gl-px-6"
>
<label class="gl-display-inline-flex" for="alert-integration-settings-issue-template">
{{ $options.i18n.issueTemplate.label }}
{{ $options.i18n.incidentTemplate.label }}
<gl-link :href="$options.ISSUE_TEMPLATES_DOCS_LINK" target="_blank">
<gl-icon name="question" :size="12" />
</gl-link>
......
......@@ -109,7 +109,20 @@ export default {
{{ webhookUpdateAlertMsg }}
</gl-alert>
<p>{{ $options.i18n.introText }}</p>
<p>
<gl-sprintf :message="$options.i18n.introText">
<template #link="{ content }">
<gl-link
:href="$options.CONFIGURE_PAGERDUTY_WEBHOOK_DOCS_LINK"
target="_blank"
class="gl-display-inline-flex"
>
<span>{{ content }}</span>
<gl-icon name="external-link" />
</gl-link>
</template>
</gl-sprintf>
</p>
<form ref="settingsForm" @submit.prevent="updatePagerDutyIntegrationSettings">
<gl-form-group class="col-8 col-md-9 gl-p-0">
<gl-toggle
......@@ -134,23 +147,9 @@ export default {
</template>
</gl-form-input-group>
<div class="gl-text-gray-200 gl-pt-2">
<gl-sprintf :message="$options.i18n.webhookUrl.helpText">
<template #docsLink>
<gl-link
:href="$options.CONFIGURE_PAGERDUTY_WEBHOOK_DOCS_LINK"
target="_blank"
class="gl-display-inline-flex"
>
<span>{{ $options.i18n.webhookUrl.helpDocsLink }}</span>
<gl-icon name="external-link" />
</gl-link>
</template>
</gl-sprintf>
</div>
<gl-button
v-gl-modal.resetWebhookModal
class="gl-mt-3"
class="gl-mt-5"
:disabled="loading"
:loading="resettingWebhook"
data-testid="webhook-reset-btn"
......
......@@ -33,17 +33,17 @@ export const I18N_ALERT_SETTINGS_FORM = {
saveBtnLabel: __('Save changes'),
introText: __('Action to take when receiving an alert. %{docsLink}'),
introLinkText: __('More information.'),
createIssue: {
label: __('Create an issue. Issues are created for each alert triggered.'),
createIncident: {
label: __('Create an incident. Incidents are created for each alert triggered.'),
},
issueTemplate: {
label: __('Issue template (optional)'),
incidentTemplate: {
label: __('Incident template (optional)'),
},
sendEmail: {
label: __('Send a separate email notification to Developers.'),
},
autoCloseIncidents: {
label: __('Automatically close incident issues when the associated Prometheus alert resolves.'),
label: __('Automatically close incidents when the associated Prometheus alert resolves.'),
},
};
......@@ -57,17 +57,13 @@ export const ISSUE_TEMPLATES_DOCS_LINK =
export const I18N_PAGERDUTY_SETTINGS_FORM = {
introText: s__(
'PagerDutySettings|Setting up a webhook with PagerDuty will automatically create a GitLab issue for each PagerDuty incident.',
'PagerDutySettings|Create a GitLab incident for each PagerDuty incident by %{linkStart}configuring a webhook in PagerDuty%{linkEnd}',
),
activeToggle: {
label: s__('PagerDutySettings|Active'),
},
webhookUrl: {
label: s__('PagerDutySettings|Webhook URL'),
helpText: s__(
'PagerDutySettings|Create a GitLab issue for each PagerDuty incident by %{docsLink}',
),
helpDocsLink: s__('PagerDutySettings|configuring a webhook in PagerDuty'),
resetWebhookUrl: s__('PagerDutySettings|Reset webhook URL'),
copyToClipboard: __('Copy'),
updateErrMsg: s__('PagerDutySettings|Failed to update Webhook URL'),
......
......@@ -59,6 +59,9 @@ export default {
showReset() {
return this.isInstanceOrGroupLevel && this.propsSource.resetPath;
},
saveButtonKey() {
return `save-button-${this.isDisabled}`;
},
},
methods: {
...mapActions([
......@@ -117,6 +120,7 @@ export default {
<div v-if="isEditable" class="footer-block row-content-block">
<template v-if="isInstanceOrGroupLevel">
<gl-button
:key="saveButtonKey"
v-gl-modal.confirmSaveIntegration
category="primary"
variant="success"
......@@ -130,6 +134,7 @@ export default {
</template>
<gl-button
v-else
:key="saveButtonKey"
category="primary"
variant="success"
type="submit"
......
......@@ -28,3 +28,115 @@
}
}
}
//// Copied from roadmaps.scss - adapted for on-call schedules
$header-item-height: 60px;
$details-cell-width: px-to-rem(150px);
$timeline-cell-height: 32px;
$timeline-cell-width: 180px;
$border-style: 1px solid var(--gray-100, $gray-100);
$gradient-dark-gray: rgba(0, 0, 0, 0.15);
$gradient-gray: rgba(255, 255, 255, 0.001);
$scroll-top-gradient: linear-gradient(to bottom, $gradient-dark-gray 0%, $gradient-gray 100%);
$scroll-bottom-gradient: linear-gradient(to bottom, $gradient-gray 0%, $gradient-dark-gray 100%);
$column-right-gradient: linear-gradient(to right, $gradient-dark-gray 0%, $gradient-gray 100%);
$epic-details-cell-width: 150px;
.schedule-shell {
@include gl-relative;
@include gl-h-full;
@include gl-w-full;
@include gl-overflow-x-auto;
@include gl-border-gray-100;
@include gl-border-1;
@include gl-border-solid;
@include gl-rounded-base;
}
.timeline-section {
@include gl-sticky;
position: -webkit-sticky;
@include gl-top-0;
z-index: 20;
.timeline-header-blank,
.timeline-header-item {
@include float-left;
height: $header-item-height;
border-bottom: $border-style;
background-color: var(--white, $white);
}
.timeline-header-blank {
@include gl-sticky;
position: -webkit-sticky;
@include gl-top-0;
@include gl-left-0;
width: $details-cell-width;
z-index: 2;
&::after {
height: $header-item-height;
@include gl-content-empty;
@include gl-absolute;
@include gl-top-0;
right: -$grid-size;
width: $grid-size;
@include gl-pointer-events-none;
background: $column-right-gradient;
}
}
.timeline-header-item {
// container size minus left panel width divided by 2 week timeframes
width: calc((100% - #{$epic-details-cell-width}) / 2);
&:last-of-type .item-label {
@include gl-border-r-0;
}
.item-label,
.item-sublabel .sublabel-value {
color: var(--gray-400, $gray-400);
@include gl-font-weight-normal;
&.label-dark {
@include gl-text-gray-900;
}
&.label-bold {
@include gl-font-weight-bold;
}
}
.item-label {
padding: $gl-padding-8 $gl-padding;
border-right: $border-style;
border-bottom: $border-style;
}
.item-sublabel {
@include gl-relative;
@include gl-display-flex;
.sublabel-value {
@include gl-flex-grow-1;
@include gl-flex-basis-0;
text-align: center;
font-size: $code-font-size;
line-height: 1.5;
padding: 2px 0;
}
}
.current-day-indicator-header {
@include gl-bottom-0;
height: $gl-vert-padding;
width: $gl-vert-padding;
background-color: var(--red-500, $red-500);
border-radius: 50%;
transform: translateX(-3px);
}
}
}
---
title: Resolve Save button should have a different color on press
merge_request: 48975
author:
type: fixed
---
title: Use incident instead of issue for operation settings
merge_request: 48406
author:
type: fixed
<script>
import { __ } from '~/locale';
import { fetchPolicies } from '~/lib/graphql';
import IterationReportSummaryCards from './iteration_report_summary_cards.vue';
import summaryStatsQuery from '../queries/iteration_issues_summary_stats.query.graphql';
......@@ -10,7 +9,6 @@ export default {
},
apollo: {
issues: {
fetchPolicy: fetchPolicies.NO_CACHE,
query: summaryStatsQuery,
variables() {
return this.queryVariables;
......
query IterationIssuesSummaryStats($id: ID!) {
iteration(id: $id) {
id
report {
stats {
total {
......
......@@ -12,6 +12,7 @@ import {
} from '@gitlab/ui';
import { s__, __ } from '~/locale';
import createOncallScheduleMutation from '../graphql/create_oncall_schedule.mutation.graphql';
import { getFormattedTimezone } from '../utils';
export const i18n = {
selectTimezone: s__('OnCallSchedules|Select timezone'),
......@@ -145,7 +146,7 @@ export default {
this.form.timezone = tz;
},
getFormattedTimezone(tz) {
return __(`(UTC${tz.formatted_offset}) ${tz.abbr} ${tz.name}`);
return getFormattedTimezone(tz);
},
isTimezoneSelected(tz) {
return isEqual(tz, this.form.timezone);
......
<script>
import { GlSprintf, GlCard } from '@gitlab/ui';
import { s__ } from '~/locale';
import ScheduleTimelineSection from './schedule/components/schedule_timeline_section.vue';
import { getTimeframeForWeeksView } from './schedule/utils';
import { PRESET_TYPES } from './schedule/constants';
import { getFormattedTimezone } from '../utils';
export const i18n = {
title: s__('OnCallSchedules|On-call schedule'),
scheduleForTz: s__('OnCallSchedules|On-call schedule for the %{tzShort}'),
};
export default {
i18n,
presetType: PRESET_TYPES.WEEKS,
inject: ['timezones'],
components: {
GlSprintf,
GlCard,
ScheduleTimelineSection,
},
props: {
schedule: {
type: Object,
required: true,
},
},
computed: {
tzLong() {
const selectedTz = this.timezones.find(tz => tz.identifier === this.schedule.timezone);
return getFormattedTimezone(selectedTz);
},
timeframe() {
return getTimeframeForWeeksView();
},
},
};
</script>
<template>
<div>
<h2>{{ $options.i18n.title }}</h2>
<gl-card>
<template #header>
<h3 class="gl-font-weight-bold gl-font-lg gl-m-0">{{ schedule.name }}</h3>
</template>
<p class="gl-text-gray-500 gl-mb-5">
<gl-sprintf :message="$options.i18n.scheduleForTz">
<template #tzShort>{{ schedule.timezone }}</template>
</gl-sprintf>
| {{ tzLong }}
</p>
<div class="schedule-shell">
<schedule-timeline-section :preset-type="$options.presetType" :timeframe="timeframe" />
</div>
</gl-card>
</div>
</template>
<script>
import { GlEmptyState, GlButton, GlModalDirective } from '@gitlab/ui';
import { GlEmptyState, GlButton, GlLoadingIcon, GlModalDirective } from '@gitlab/ui';
import * as Sentry from '~/sentry/wrapper';
import AddScheduleModal from './add_schedule_modal.vue';
import AddRotationModal from './rotations/add_rotation_modal.vue';
import OncallSchedule from './oncall_schedule.vue';
import { s__ } from '~/locale';
import getOncallSchedules from '../graphql/get_oncall_schedules.query.graphql';
import { fetchPolicies } from '~/lib/graphql';
const addScheduleModalId = 'addScheduleModal';
......@@ -17,23 +21,54 @@ export const i18n = {
export default {
i18n,
addScheduleModalId,
inject: ['emptyOncallSchedulesSvgPath'],
inject: ['emptyOncallSchedulesSvgPath', 'projectPath'],
components: {
GlEmptyState,
GlButton,
GlLoadingIcon,
AddScheduleModal,
AddRotationModal,
OncallSchedule,
},
directives: {
GlModal: GlModalDirective,
},
methods: {},
data() {
return {
schedule: {},
};
},
apollo: {
schedule: {
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
query: getOncallSchedules,
variables() {
return {
projectPath: this.projectPath,
};
},
update(data) {
return data?.project?.incidentManagementOncallSchedules?.nodes?.[0] ?? null;
},
error(error) {
Sentry.captureException(error);
},
},
},
computed: {
isLoading() {
return this.$apollo.queries.schedule.loading;
},
},
};
</script>
<template>
<div>
<gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-3" />
<oncall-schedule v-else-if="schedule" :schedule="schedule" />
<gl-empty-state
v-else
:title="$options.i18n.emptyState.title"
:description="$options.i18n.emptyState.description"
:svg-path="emptyOncallSchedulesSvgPath"
......
<script>
import { monthInWords } from '~/lib/utils/datetime_utility';
import WeeksHeaderSubItem from './weeks_header_sub_item.vue';
export default {
components: {
WeeksHeaderSubItem,
},
props: {
timeframeIndex: {
type: Number,
required: true,
},
timeframeItem: {
type: Date,
required: true,
},
timeframe: {
type: Array,
required: true,
},
},
data() {
const currentDate = new Date();
currentDate.setHours(0, 0, 0, 0);
return {
currentDate,
};
},
computed: {
lastDayOfCurrentWeek() {
const lastDayOfCurrentWeek = new Date(this.timeframeItem.getTime());
lastDayOfCurrentWeek.setDate(lastDayOfCurrentWeek.getDate() + 7);
return lastDayOfCurrentWeek;
},
timelineHeaderLabel() {
const timeframeItemMonth = this.timeframeItem.getMonth();
const timeframeItemDate = this.timeframeItem.getDate();
if (this.timeframeIndex === 0 || (timeframeItemMonth === 0 && timeframeItemDate <= 7)) {
return `${this.timeframeItem.getFullYear()} ${monthInWords(
this.timeframeItem,
true,
)} ${timeframeItemDate}`;
}
return `${monthInWords(this.timeframeItem, true)} ${timeframeItemDate}`;
},
timelineHeaderClass() {
const currentDateTime = this.currentDate.getTime();
const lastDayOfCurrentWeekTime = this.lastDayOfCurrentWeek.getTime();
if (
currentDateTime >= this.timeframeItem.getTime() &&
currentDateTime <= lastDayOfCurrentWeekTime
) {
return 'label-dark label-bold';
}
return '';
},
},
};
</script>
<template>
<span class="timeline-header-item">
<div :class="timelineHeaderClass" class="item-label">{{ timelineHeaderLabel }}</div>
<weeks-header-sub-item :timeframe-item="timeframeItem" :current-date="currentDate" />
</span>
</template>
<script>
import { PRESET_TYPES } from '../../constants';
import CommonMixin from '../../mixins/common_mixin';
export default {
mixins: [CommonMixin],
props: {
currentDate: {
type: Date,
required: true,
},
timeframeItem: {
type: Date,
required: true,
},
},
data() {
return {
presetType: PRESET_TYPES.WEEKS,
indicatorStyle: {},
};
},
computed: {
headerSubItems() {
const timeframeItem = new Date(this.timeframeItem.getTime());
const headerSubItems = new Array(7)
.fill()
.map(
(val, i) =>
new Date(
timeframeItem.getFullYear(),
timeframeItem.getMonth(),
timeframeItem.getDate() + i,
),
);
return headerSubItems;
},
},
mounted() {
this.$nextTick(() => {
this.indicatorStyle = this.getIndicatorStyles();
});
},
methods: {
getSubItemValueClass(subItem) {
// Show dark color text only for current & upcoming dates
if (subItem.getTime() === this.currentDate.getTime()) {
return 'label-dark label-bold';
} else if (subItem > this.currentDate) {
return 'label-dark';
}
return '';
},
},
};
</script>
<template>
<div class="item-sublabel">
<span
v-for="(subItem, index) in headerSubItems"
:key="index"
:class="getSubItemValueClass(subItem)"
class="sublabel-value"
>{{ subItem.getDate() }}</span
>
<span
v-if="hasToday"
:style="indicatorStyle"
class="current-day-indicator-header preset-weeks gl-absolute"
></span>
</div>
</template>
<script>
import WeeksHeaderItem from './preset_weeks/weeks_header_item.vue';
export default {
components: {
WeeksHeaderItem,
},
props: {
presetType: {
type: String,
required: true,
},
timeframe: {
type: Array,
required: true,
},
},
};
</script>
<template>
<div class="timeline-section clearfix">
<span class="timeline-header-blank"></span>
<weeks-header-item
v-for="(timeframeItem, index) in timeframe"
:key="index"
:timeframe-index="index"
:timeframe-item="timeframeItem"
:timeframe="timeframe"
/>
</div>
</template>
export const DAYS_IN_WEEK = 7;
export const PRESET_TYPES = {
WEEKS: 'WEEKS',
};
export const PRESET_DEFAULTS = {
WEEKS: {
TIMEFRAME_LENGTH: 2,
},
};
export const PAST_DATE = new Date(new Date().getFullYear() - 100, 0, 1);
export const FUTURE_DATE = new Date(new Date().getFullYear() + 100, 0, 1);
import { DAYS_IN_WEEK } from '../constants';
export default {
computed: {
hasToday() {
const timeframeItem = new Date(this.timeframeItem.getTime());
const headerSubItems = new Array(7)
.fill()
.map(
(val, i) =>
new Date(
timeframeItem.getFullYear(),
timeframeItem.getMonth(),
timeframeItem.getDate() + i,
),
);
return (
this.currentDate.getTime() >= headerSubItems[0].getTime() &&
this.currentDate.getTime() <= headerSubItems[headerSubItems.length - 1].getTime()
);
},
},
methods: {
getIndicatorStyles() {
// as we start schedule scale from the current date the indicator will always be on the first date. So we find
// the percentage of space one day cell takes and divide it by 2 cause the tick is in the middle of the cell.
// It might be updated to more precise position - time of the day
const left = 100 / DAYS_IN_WEEK / 2;
return {
left: `${left}%`,
};
},
},
};
import { newDate } from '~/lib/utils/datetime_utility';
import { PRESET_DEFAULTS, DAYS_IN_WEEK } from './constants';
/**
* This method returns array of Dates representing 2-weeks timeframe based on provided initialDate
*
* For eg; If initialDate is 31th Dec 2017
* we show 2 weeks starting from the current date
* So returned array from this method will be;
* [
* 31 Dec 2017, 7 Jan 2018
* ]
*
* @param {Date} initialDate
*/
export const getTimeframeForWeeksView = (initialDate = new Date()) => {
const timeframe = [];
const startDate = newDate(initialDate);
startDate.setHours(0, 0, 0, 0);
const rangeLength = PRESET_DEFAULTS.WEEKS.TIMEFRAME_LENGTH;
// Iterate for the length of this preset
for (let i = 0; i < rangeLength; i += 1) {
// Push date to timeframe only when day is
// the first day of the next week (if initial date is Tuesday next date will be also Tuesday but of the next week)
timeframe.push(newDate(startDate));
// Move date to the next in a week
startDate.setDate(startDate.getDate() + DAYS_IN_WEEK);
}
return timeframe;
};
query getOncallSchedules($projectPath: ID!) {
project(fullPath: $projectPath) {
incidentManagementOncallSchedules {
nodes {
iid
name
description
timezone
}
}
}
}
import { sprintf, __ } from '~/locale';
/**
* Returns formatted timezone string, e.g. (UTC-09:00) AKST Alaska
*
* @param {Object} tz
* @param {String} tz.name
* @param {String} tz.formatted_offset
* @param {String} tz.abbr
*
* @returns {String}
*/
export const getFormattedTimezone = tz => {
return sprintf(__('(UTC%{offset}) %{timezone}'), {
offset: tz.formatted_offset,
timezone: `${tz.abbr} ${tz.name}`,
});
};
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AddScheduleModal renders modal layout 1`] = `
exports[`Add schedule modal renders modal layout 1`] = `
<gl-modal-stub
actioncancel="[object Object]"
actionprimary="[object Object]"
......
......@@ -4,7 +4,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import AddScheduleModal, { i18n } from 'ee/oncall_schedules/components/add_schedule_modal.vue';
import mockTimezones from './mocks/mockTimezones.json';
describe('AddScheduleModal', () => {
describe('Add schedule modal', () => {
let wrapper;
const projectPath = 'group/project';
const mutate = jest.fn();
......
import { shallowMount } from '@vue/test-utils';
import { GlCard, GlSprintf } from '@gitlab/ui';
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 * as utils from 'ee/oncall_schedules/components/schedule/utils';
import * as commonUtils from 'ee/oncall_schedules/utils';
import { PRESET_TYPES } from 'ee/oncall_schedules/components/schedule/constants';
import mockTimezones from './mocks/mockTimezones.json';
describe('On-call schedule', () => {
let wrapper;
const lastTz = mockTimezones[mockTimezones.length - 1];
const mockSchedule = {
description: 'monitor description',
iid: '3',
name: 'monitor schedule',
timezone: lastTz.identifier,
};
const mockWeeksTimeFrame = ['31 Dec 2020', '7 Jan 2021', '14 Jan 2021'];
const formattedTimezone = '(UTC-09:00) AKST Alaska';
function mountComponent({ schedule } = {}) {
wrapper = shallowMount(OnCallSchedule, {
propsData: {
schedule,
},
provide: {
timezones: mockTimezones,
},
stubs: {
GlCard,
GlSprintf,
},
});
}
beforeEach(() => {
jest.spyOn(utils, 'getTimeframeForWeeksView').mockReturnValue(mockWeeksTimeFrame);
jest.spyOn(commonUtils, 'getFormattedTimezone').mockReturnValue(formattedTimezone);
mountComponent({ schedule: mockSchedule });
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findCardHeader = () => wrapper.find('.gl-card-header');
const findCardDescription = () => wrapper.find('.gl-card-body');
const findScheduleTimeline = () => findCardDescription().find(ScheduleTimelineSection);
it('shows schedule title', () => {
expect(findCardHeader().text()).toBe(mockSchedule.name);
});
it('shows timezone info', () => {
const shortTz = i18n.scheduleForTz.replace('%{tzShort}', lastTz.identifier);
const longTz = formattedTimezone;
const description = findCardDescription().text();
expect(description).toContain(shortTz);
expect(description).toContain(longTz);
});
it('renders ScheduleShell', () => {
const timeline = findScheduleTimeline();
expect(timeline.exists()).toBe(true);
expect(timeline.props()).toEqual({
presetType: PRESET_TYPES.WEEKS,
timeframe: mockWeeksTimeFrame,
});
});
});
import { shallowMount } from '@vue/test-utils';
import { GlEmptyState } from '@gitlab/ui';
import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import OnCallScheduleWrapper, {
i18n,
} from 'ee/oncall_schedules/components/oncall_schedules_wrapper.vue';
import OnCallSchedule from 'ee/oncall_schedules/components/oncall_schedule.vue';
describe('AlertManagementEmptyState', () => {
describe('On-call schedule wrapper', () => {
let wrapper;
const emptyOncallSchedulesSvgPath = 'illustration/path.svg';
const projectPath = 'group/project';
function mountComponent({ loading, schedule } = {}) {
const $apollo = {
queries: {
schedule: {
loading,
},
},
};
function mountComponent() {
wrapper = shallowMount(OnCallScheduleWrapper, {
data() {
return {
schedule,
};
},
provide: {
emptyOncallSchedulesSvgPath,
projectPath,
},
mocks: { $apollo },
});
}
beforeEach(() => {
mountComponent();
});
afterEach(() => {
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
wrapper.destroy();
wrapper = null;
});
const findLoader = () => wrapper.find(GlLoadingIcon);
const findEmptyState = () => wrapper.find(GlEmptyState);
const findSchedule = () => wrapper.find(OnCallSchedule);
describe('Empty state', () => {
it('shows empty state and passed correct attributes to it', () => {
expect(findEmptyState().exists()).toBe(true);
expect(findEmptyState().attributes('title')).toBe(i18n.emptyState.title);
expect(findEmptyState().attributes('description')).toBe(i18n.emptyState.description);
expect(findEmptyState().attributes('svgpath')).toBe(emptyOncallSchedulesSvgPath);
it('shows a loader while data is requested', () => {
mountComponent({ loading: true });
expect(findLoader().exists()).toBe(true);
});
it('shows empty state and passed correct attributes to it when not loading and no schedule', () => {
mountComponent({ loading: false, schedule: null });
const emptyState = findEmptyState();
expect(emptyState.exists()).toBe(true);
expect(emptyState.attributes()).toEqual({
title: i18n.emptyState.title,
svgpath: emptyOncallSchedulesSvgPath,
description: i18n.emptyState.description,
});
});
it('renders On-call schedule when data received ', () => {
mountComponent({ loading: false, schedule: { name: 'monitor rotation' } });
const schedule = findSchedule();
expect(findLoader().exists()).toBe(false);
expect(findEmptyState().exists()).toBe(false);
expect(schedule.exists()).toBe(true);
});
});
import { shallowMount } from '@vue/test-utils';
import WeeksHeaderItemComponent from 'ee/oncall_schedules/components/schedule/components/preset_weeks/weeks_header_item.vue';
import { getTimeframeForWeeksView } from 'ee/oncall_schedules/components/schedule/utils';
describe('WeeksHeaderItemComponent', () => {
let wrapper;
const mockTimeframeIndex = 0;
const mockTimeframeInitialDate = new Date(2018, 0, 1);
const mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate);
function mountComponent({
timeframeIndex = mockTimeframeIndex,
timeframeItem = mockTimeframeWeeks[mockTimeframeIndex],
timeframe = mockTimeframeWeeks,
}) {
wrapper = shallowMount(WeeksHeaderItemComponent, {
propsData: {
timeframeIndex,
timeframeItem,
timeframe,
},
});
}
beforeEach(() => {
mountComponent({});
});
afterEach(() => {
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
});
describe('data', () => {
it('returns default data props', () => {
const currentDate = new Date();
expect(wrapper.vm.currentDate.getDate()).toBe(currentDate.getDate());
});
});
describe('computed', () => {
describe('lastDayOfCurrentWeek', () => {
it('returns date object representing last day of the week as set in `timeframeItem`', () => {
expect(wrapper.vm.lastDayOfCurrentWeek.getDate()).toBe(
mockTimeframeWeeks[mockTimeframeIndex].getDate() + 7,
);
});
});
describe('timelineHeaderLabel', () => {
it('returns string containing Year, Month and Date for the first timeframe item in the entire timeframe', () => {
expect(wrapper.vm.timelineHeaderLabel).toBe('2018 Jan 1');
});
it('returns string containing Year, Month and Date for timeframe item when it is first week of the year', () => {
mountComponent({
timeframeIndex: 3,
timeframeItem: new Date(2019, 0, 6),
});
expect(wrapper.vm.timelineHeaderLabel).toBe('2019 Jan 6');
});
it('returns string containing only Month and Date timeframe item when it is somewhere in the middle of timeframe', () => {
mountComponent({
timeframeIndex: mockTimeframeIndex + 1,
timeframeItem: mockTimeframeWeeks[mockTimeframeIndex + 1],
});
expect(wrapper.vm.timelineHeaderLabel).toBe('Jan 8');
});
});
describe('timelineHeaderClass', () => {
it('returns empty string when timeframeItem week is less than current week', () => {
expect(wrapper.vm.timelineHeaderClass).toBe('');
});
it('returns string containing `label-dark label-bold` when current week is same as timeframeItem week', () => {
wrapper.setData({ currentDate: mockTimeframeWeeks[mockTimeframeIndex] });
expect(wrapper.vm.timelineHeaderClass).toBe('label-dark label-bold');
});
});
});
describe('template', () => {
it('renders component container element with class `timeline-header-item`', () => {
expect(wrapper.classes()).toContain('timeline-header-item');
});
it('renders item label element class `item-label` and value as `timelineHeaderLabel`', () => {
expect(wrapper.find('.item-label').text()).toBe('2018 Jan 1');
});
});
});
import { shallowMount } from '@vue/test-utils';
import WeeksHeaderSubItemComponent from 'ee/oncall_schedules/components/schedule/components/preset_weeks/weeks_header_sub_item.vue';
import { getTimeframeForWeeksView } from 'ee/oncall_schedules/components/schedule/utils';
import { PRESET_TYPES } from 'ee/oncall_schedules/components/schedule/constants';
describe('MonthsHeaderSubItemComponent', () => {
let wrapper;
const mockTimeframeInitialDate = new Date(2018, 0, 1);
const mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate);
function mountComponent({
currentDate = mockTimeframeWeeks[0],
timeframeItem = mockTimeframeWeeks[0],
}) {
wrapper = shallowMount(WeeksHeaderSubItemComponent, {
propsData: {
currentDate,
timeframeItem,
},
});
}
beforeEach(() => {
mountComponent({});
});
afterEach(() => {
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
});
describe('data', () => {
it('initializes `presetType` and `indicatorStyles` data props', () => {
expect(wrapper.vm.presetType).toBe(PRESET_TYPES.WEEKS);
expect(wrapper.vm.indicatorStyle).toBeDefined();
});
});
describe('computed', () => {
describe('headerSubItems', () => {
it('returns `headerSubItems` array of dates containing days of week from timeframeItem', () => {
expect(wrapper.vm.headerSubItems).toBeInstanceOf(Array);
expect(wrapper.vm.headerSubItems).toHaveLength(7);
wrapper.vm.headerSubItems.forEach(subItem => {
expect(subItem).toBeInstanceOf(Date);
});
});
});
});
describe('methods', () => {
describe('getSubItemValueClass', () => {
it('returns string containing `label-dark` when provided subItem is greater than current week day', () => {
mountComponent({
currentDate: new Date(2018, 0, 1), // Jan 1, 2018
});
const subItem = new Date(2018, 0, 25); // Jan 25, 2018
expect(wrapper.vm.getSubItemValueClass(subItem)).toBe('label-dark');
});
it('returns string containing `label-dark label-bold` when provided subItem is same as current week day', () => {
const currentDate = new Date(2018, 0, 25);
mountComponent({
currentDate,
});
const subItem = currentDate;
expect(wrapper.vm.getSubItemValueClass(subItem)).toBe('label-dark label-bold');
});
});
});
describe('template', () => {
it('renders component container element with class `item-sublabel`', () => {
expect(wrapper.classes()).toContain('item-sublabel');
});
it('renders sub item element with class `sublabel-value`', () => {
expect(wrapper.find('.sublabel-value').exists()).toBe(true);
});
it('renders element with class `current-day-indicator-header` when hasToday is true', () => {
expect(wrapper.find('.current-day-indicator-header.preset-weeks').exists()).toBe(true);
});
});
});
import { shallowMount } from '@vue/test-utils';
import ScheduleTimelineSection from 'ee/oncall_schedules/components/schedule/components/schedule_timeline_section.vue';
import WeeksHeaderItem from 'ee/oncall_schedules/components/schedule/components/preset_weeks/weeks_header_item.vue';
import { getTimeframeForWeeksView } from 'ee/oncall_schedules/components/schedule/utils';
import { PRESET_TYPES } from 'ee/oncall_schedules/components/schedule/constants';
describe('RoadmapTimelineSectionComponent', () => {
let wrapper;
const mockTimeframeInitialDate = new Date(2018, 0, 1);
const mockTimeframeWeeks = getTimeframeForWeeksView(mockTimeframeInitialDate);
function mountComponent({
presetType = PRESET_TYPES.WEEKS,
timeframe = mockTimeframeWeeks,
} = {}) {
wrapper = shallowMount(ScheduleTimelineSection, {
propsData: {
presetType,
timeframe,
},
});
}
beforeEach(() => {
mountComponent({});
});
afterEach(() => {
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
});
it('renders component container element with class `timeline-section`', () => {
expect(wrapper.classes()).toContain('timeline-section');
});
it('renders empty header cell element with class `timeline-header-blank`', () => {
expect(wrapper.find('.timeline-header-blank').exists()).toBe(true);
});
it('renders weeks header items based on timeframe data', () => {
expect(wrapper.findAll(WeeksHeaderItem).length).toBe(mockTimeframeWeeks.length);
});
});
import { getTimeframeForWeeksView } from 'ee/oncall_schedules/components/schedule/utils';
describe('getTimeframeForWeeksView', () => {
const mockTimeframeInitialDate = new Date(2018, 0, 1);
const timeframe = getTimeframeForWeeksView(mockTimeframeInitialDate);
it('returns timeframe with total of 2 weeks', () => {
expect(timeframe).toHaveLength(2);
});
it('first timeframe item refers to the start date', () => {
const timeframeItem = timeframe[0];
const expectedMonth = {
year: 2018,
month: 0,
date: 1,
};
expect(timeframeItem.getFullYear()).toBe(expectedMonth.year);
expect(timeframeItem.getMonth()).toBe(expectedMonth.month);
expect(timeframeItem.getDate()).toBe(expectedMonth.date);
});
it('second timeframe item refers to first date of the next week week ', () => {
const timeframeItem = timeframe[timeframe.length - 1];
const expectedMonth = {
year: 2018,
month: 0,
date: 8,
};
expect(timeframeItem.getFullYear()).toBe(expectedMonth.year);
expect(timeframeItem.getMonth()).toBe(expectedMonth.month);
expect(timeframeItem.getDate()).toBe(expectedMonth.date);
});
});
import { getFormattedTimezone } from 'ee/oncall_schedules/utils';
import mockTimezones from './mocks/mockTimezones.json';
describe('getFormattedTimezone', () => {
it('formats the timezone', () => {
const tz = mockTimezones[0];
const expectedValue = `(UTC${tz.formatted_offset}) ${tz.abbr} ${tz.name}`;
expect(getFormattedTimezone(tz)).toBe(expectedValue);
});
});
......@@ -941,6 +941,9 @@ msgstr ""
msgid "(No changes)"
msgstr ""
msgid "(UTC%{offset}) %{timezone}"
msgstr ""
msgid "(check progress)"
msgstr ""
......@@ -4169,7 +4172,7 @@ msgstr ""
msgid "Automatic deployment rollbacks"
msgstr ""
msgid "Automatically close incident issues when the associated Prometheus alert resolves."
msgid "Automatically close incidents when the associated Prometheus alert resolves."
msgstr ""
msgid "Automatically create merge requests for vulnerabilities that have fixes available."
......@@ -7919,7 +7922,7 @@ msgstr ""
msgid "Create an account using:"
msgstr ""
msgid "Create an issue. Issues are created for each alert triggered."
msgid "Create an incident. Incidents are created for each alert triggered."
msgstr ""
msgid "Create and provide your GitHub %{link_start}Personal Access Token%{link_end}. You will need to select the %{code_open}repo%{code_close} scope, so we can display a list of your public and private repositories which are available to import."
......@@ -14377,6 +14380,9 @@ msgstr ""
msgid "Incident Management Limits"
msgstr ""
msgid "Incident template (optional)"
msgstr ""
msgid "IncidentManagement|%{hours} hours, %{minutes} minutes remaining"
msgstr ""
......@@ -15207,9 +15213,6 @@ msgstr ""
msgid "Issue published on status page."
msgstr ""
msgid "Issue template (optional)"
msgstr ""
msgid "Issue update failed"
msgstr ""
......@@ -19112,6 +19115,12 @@ msgstr ""
msgid "OnCallSchedules|Failed to add schedule"
msgstr ""
msgid "OnCallSchedules|On-call schedule"
msgstr ""
msgid "OnCallSchedules|On-call schedule for the %{tzShort}"
msgstr ""
msgid "OnCallSchedules|Rotation length"
msgstr ""
......@@ -19729,7 +19738,7 @@ msgstr ""
msgid "PagerDutySettings|Active"
msgstr ""
msgid "PagerDutySettings|Create a GitLab issue for each PagerDuty incident by %{docsLink}"
msgid "PagerDutySettings|Create a GitLab incident for each PagerDuty incident by %{linkStart}configuring a webhook in PagerDuty%{linkEnd}"
msgstr ""
msgid "PagerDutySettings|Failed to update Webhook URL"
......@@ -19741,18 +19750,12 @@ msgstr ""
msgid "PagerDutySettings|Resetting the webhook URL for this project will require updating this integration's settings in PagerDuty."
msgstr ""
msgid "PagerDutySettings|Setting up a webhook with PagerDuty will automatically create a GitLab issue for each PagerDuty incident."
msgstr ""
msgid "PagerDutySettings|Webhook URL"
msgstr ""
msgid "PagerDutySettings|Webhook URL update was successful"
msgstr ""
msgid "PagerDutySettings|configuring a webhook in PagerDuty"
msgstr ""
msgid "Pages"
msgstr ""
......
......@@ -23,7 +23,7 @@ RSpec.describe 'Projects > Settings > For a forked project', :js do
describe 'Settings > Operations' do
describe 'Incidents' do
let(:create_issue) { 'Create an issue. Issues are created for each alert triggered.' }
let(:create_issue) { 'Create an incident. Incidents are created for each alert triggered.' }
let(:send_email) { 'Send a separate email notification to Developers.' }
before do
......
......@@ -17,7 +17,7 @@ exports[`Alert integration settings form default state should match the default
data-qa-selector="create_issue_checkbox"
>
<span>
Create an issue. Issues are created for each alert triggered.
Create an incident. Incidents are created for each alert triggered.
</span>
</gl-form-checkbox-stub>
</gl-form-group-stub>
......@@ -32,7 +32,7 @@ exports[`Alert integration settings form default state should match the default
for="alert-integration-settings-issue-template"
>
Issue template (optional)
Incident template (optional)
<gl-link-stub
href="/help/user/project/description_templates#creating-issue-templates"
......@@ -89,7 +89,7 @@ exports[`Alert integration settings form default state should match the default
checked="true"
>
<span>
Automatically close incident issues when the associated Prometheus alert resolves.
Automatically close incidents when the associated Prometheus alert resolves.
</span>
</gl-form-checkbox-stub>
</gl-form-group-stub>
......
......@@ -5,7 +5,9 @@ exports[`Alert integration settings form should match the default snapshot 1`] =
<!---->
<p>
Setting up a webhook with PagerDuty will automatically create a GitLab issue for each PagerDuty incident.
<gl-sprintf-stub
message="Create a GitLab incident for each PagerDuty incident by %{linkStart}configuring a webhook in PagerDuty%{linkEnd}"
/>
</p>
<form>
......@@ -33,18 +35,10 @@ exports[`Alert integration settings form should match the default snapshot 1`] =
value="pagerduty.webhook.com"
/>
<div
class="gl-text-gray-200 gl-pt-2"
>
<gl-sprintf-stub
message="Create a GitLab issue for each PagerDuty incident by %{docsLink}"
/>
</div>
<gl-button-stub
buttontextclasses=""
category="primary"
class="gl-mt-3"
class="gl-mt-5"
data-testid="webhook-reset-btn"
icon=""
role="button"
......
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