Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
493d6f7512
commit
a17e8b1687
|
@ -1,4 +1,4 @@
|
||||||
import { s__ } from '~/locale';
|
import { __, s__ } from '~/locale';
|
||||||
|
|
||||||
export const timelineTabI18n = Object.freeze({
|
export const timelineTabI18n = Object.freeze({
|
||||||
title: s__('Incident|Timeline'),
|
title: s__('Incident|Timeline'),
|
||||||
|
@ -12,6 +12,9 @@ export const timelineFormI18n = Object.freeze({
|
||||||
'Incident|Something went wrong while creating the incident timeline event.',
|
'Incident|Something went wrong while creating the incident timeline event.',
|
||||||
),
|
),
|
||||||
areaPlaceholder: s__('Incident|Timeline text...'),
|
areaPlaceholder: s__('Incident|Timeline text...'),
|
||||||
|
save: __('Save'),
|
||||||
|
cancel: __('Cancel'),
|
||||||
|
description: __('Description'),
|
||||||
saveAndAdd: s__('Incident|Save and add another event'),
|
saveAndAdd: s__('Incident|Save and add another event'),
|
||||||
areaLabel: s__('Incident|Timeline text'),
|
areaLabel: s__('Incident|Timeline text'),
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
<script>
|
||||||
|
import { produce } from 'immer';
|
||||||
|
import { sortBy } from 'lodash';
|
||||||
|
import { sprintf } from '~/locale';
|
||||||
|
import { createAlert } from '~/flash';
|
||||||
|
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
||||||
|
import { TYPE_ISSUE } from '~/graphql_shared/constants';
|
||||||
|
import { timelineFormI18n } from './constants';
|
||||||
|
import TimelineEventsForm from './timeline_events_form.vue';
|
||||||
|
|
||||||
|
import CreateTimelineEvent from './graphql/queries/create_timeline_event.mutation.graphql';
|
||||||
|
import getTimelineEvents from './graphql/queries/get_timeline_events.query.graphql';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CreateTimelineEvent',
|
||||||
|
i18n: timelineFormI18n,
|
||||||
|
components: {
|
||||||
|
TimelineEventsForm,
|
||||||
|
},
|
||||||
|
inject: ['fullPath', 'issuableId'],
|
||||||
|
props: {
|
||||||
|
hasTimelineEvents: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return { createTimelineEventActive: false };
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clearForm() {
|
||||||
|
this.$refs.eventForm.clear();
|
||||||
|
},
|
||||||
|
focusDate() {
|
||||||
|
this.$refs.eventForm.focusDate();
|
||||||
|
},
|
||||||
|
updateCache(store, { data }) {
|
||||||
|
const { timelineEvent: event, errors } = data?.timelineEventCreate || {};
|
||||||
|
|
||||||
|
if (errors.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const variables = {
|
||||||
|
incidentId: convertToGraphQLId(TYPE_ISSUE, this.issuableId),
|
||||||
|
fullPath: this.fullPath,
|
||||||
|
};
|
||||||
|
|
||||||
|
const sourceData = store.readQuery({
|
||||||
|
query: getTimelineEvents,
|
||||||
|
variables,
|
||||||
|
});
|
||||||
|
|
||||||
|
const newData = produce(sourceData, (draftData) => {
|
||||||
|
const { nodes: draftEventList } = draftData.project.incidentManagementTimelineEvents;
|
||||||
|
draftEventList.push(event);
|
||||||
|
// ISOStrings sort correctly in lexical order
|
||||||
|
const sortedEvents = sortBy(draftEventList, 'occurredAt');
|
||||||
|
draftData.project.incidentManagementTimelineEvents.nodes = sortedEvents;
|
||||||
|
});
|
||||||
|
|
||||||
|
store.writeQuery({
|
||||||
|
query: getTimelineEvents,
|
||||||
|
variables,
|
||||||
|
data: newData,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
createIncidentTimelineEvent(eventDetails, addAnotherEvent = false) {
|
||||||
|
this.createTimelineEventActive = true;
|
||||||
|
return this.$apollo
|
||||||
|
.mutate({
|
||||||
|
mutation: CreateTimelineEvent,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
incidentId: convertToGraphQLId(TYPE_ISSUE, this.issuableId),
|
||||||
|
note: eventDetails.note,
|
||||||
|
occurredAt: eventDetails.occurredAt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
update: this.updateCache,
|
||||||
|
})
|
||||||
|
.then(({ data = {} }) => {
|
||||||
|
this.createTimelineEventActive = false;
|
||||||
|
const errors = data.timelineEventCreate?.errors;
|
||||||
|
if (errors.length) {
|
||||||
|
createAlert({
|
||||||
|
message: sprintf(this.$options.i18n.createError, { error: errors.join('. ') }, false),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (addAnotherEvent) {
|
||||||
|
this.$refs.eventForm.clear();
|
||||||
|
} else {
|
||||||
|
this.$emit('hide-new-timeline-events-form');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
createAlert({
|
||||||
|
message: this.$options.i18n.createErrorGeneric,
|
||||||
|
captureError: true,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<timeline-events-form
|
||||||
|
ref="eventForm"
|
||||||
|
:is-event-processed="createTimelineEventActive"
|
||||||
|
:has-timeline-events="hasTimelineEvents"
|
||||||
|
@save-event="createIncidentTimelineEvent"
|
||||||
|
@cancel="$emit('hide-new-timeline-events-form')"
|
||||||
|
/>
|
||||||
|
</template>
|
|
@ -1,21 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlDatepicker, GlFormInput, GlFormGroup, GlButton, GlIcon } from '@gitlab/ui';
|
import { GlDatepicker, GlFormInput, GlFormGroup, GlButton, GlIcon } from '@gitlab/ui';
|
||||||
import { produce } from 'immer';
|
|
||||||
import { sortBy } from 'lodash';
|
|
||||||
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
|
||||||
import { TYPE_ISSUE } from '~/graphql_shared/constants';
|
|
||||||
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
|
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
|
||||||
import { createAlert } from '~/flash';
|
|
||||||
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
|
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
|
||||||
import { sprintf } from '~/locale';
|
|
||||||
import { getUtcShiftedDateNow } from './utils';
|
|
||||||
import { timelineFormI18n } from './constants';
|
import { timelineFormI18n } from './constants';
|
||||||
|
import { getUtcShiftedDateNow } from './utils';
|
||||||
import CreateTimelineEvent from './graphql/queries/create_timeline_event.mutation.graphql';
|
|
||||||
import getTimelineEvents from './graphql/queries/get_timeline_events.query.graphql';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'IncidentTimelineEventForm',
|
name: 'TimelineEventsForm',
|
||||||
restrictedToolBarItems: [
|
restrictedToolBarItems: [
|
||||||
'quote',
|
'quote',
|
||||||
'strikethrough',
|
'strikethrough',
|
||||||
|
@ -38,112 +29,55 @@ export default {
|
||||||
directives: {
|
directives: {
|
||||||
autofocusonshow,
|
autofocusonshow,
|
||||||
},
|
},
|
||||||
inject: ['fullPath', 'issuableId'],
|
|
||||||
props: {
|
props: {
|
||||||
hasTimelineEvents: {
|
hasTimelineEvents: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
isEventProcessed: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
// Create shifted date to force the datepicker to format in UTC
|
// if occurredAt is undefined, returns "now" in UTC
|
||||||
const utcShiftedDate = getUtcShiftedDateNow();
|
const placeholderDate = getUtcShiftedDateNow();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentDate: utcShiftedDate,
|
|
||||||
currentHour: utcShiftedDate.getHours(),
|
|
||||||
currentMinute: utcShiftedDate.getMinutes(),
|
|
||||||
timelineText: '',
|
timelineText: '',
|
||||||
createTimelineEventActive: false,
|
placeholderDate,
|
||||||
|
hourPickerInput: placeholderDate.getHours(),
|
||||||
|
minutePickerInput: placeholderDate.getMinutes(),
|
||||||
datepickerTextInput: null,
|
datepickerTextInput: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
occurredAt() {
|
||||||
|
const [years, months, days] = this.datepickerTextInput.split('-');
|
||||||
|
const utcDate = new Date(
|
||||||
|
Date.UTC(years, months - 1, days, this.hourPickerInput, this.minutePickerInput),
|
||||||
|
);
|
||||||
|
|
||||||
|
return utcDate.toISOString();
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
clear() {
|
clear() {
|
||||||
const utcShiftedDate = getUtcShiftedDateNow();
|
const utcShiftedDateNow = getUtcShiftedDateNow();
|
||||||
this.currentDate = utcShiftedDate;
|
this.placeholderDate = utcShiftedDateNow;
|
||||||
this.currentHour = utcShiftedDate.getHours();
|
this.hourPickerInput = utcShiftedDateNow.getHours();
|
||||||
this.currentMinute = utcShiftedDate.getMinutes();
|
this.minutePickerInput = utcShiftedDateNow.getMinutes();
|
||||||
},
|
this.timelineText = '';
|
||||||
hideIncidentTimelineEventForm() {
|
|
||||||
this.$emit('hide-incident-timeline-event-form');
|
|
||||||
},
|
},
|
||||||
focusDate() {
|
focusDate() {
|
||||||
this.$refs.datepicker.$el.focus();
|
this.$refs.datepicker.$el.focus();
|
||||||
},
|
},
|
||||||
updateCache(store, { data }) {
|
handleSave(addAnotherEvent) {
|
||||||
const { timelineEvent: event, errors } = data?.timelineEventCreate || {};
|
const eventDetails = {
|
||||||
|
note: this.timelineText,
|
||||||
if (errors.length) {
|
occurredAt: this.occurredAt,
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const variables = {
|
|
||||||
incidentId: convertToGraphQLId(TYPE_ISSUE, this.issuableId),
|
|
||||||
fullPath: this.fullPath,
|
|
||||||
};
|
};
|
||||||
|
this.$emit('save-event', eventDetails, addAnotherEvent);
|
||||||
const sourceData = store.readQuery({
|
|
||||||
query: getTimelineEvents,
|
|
||||||
variables,
|
|
||||||
});
|
|
||||||
|
|
||||||
const newData = produce(sourceData, (draftData) => {
|
|
||||||
const { nodes: draftEventList } = draftData.project.incidentManagementTimelineEvents;
|
|
||||||
draftEventList.push(event);
|
|
||||||
// ISOStrings sort correctly in lexical order
|
|
||||||
const sortedEvents = sortBy(draftEventList, 'occurredAt');
|
|
||||||
draftData.project.incidentManagementTimelineEvents.nodes = sortedEvents;
|
|
||||||
});
|
|
||||||
|
|
||||||
store.writeQuery({
|
|
||||||
query: getTimelineEvents,
|
|
||||||
variables,
|
|
||||||
data: newData,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
createIncidentTimelineEvent(addOneEvent) {
|
|
||||||
this.createTimelineEventActive = true;
|
|
||||||
return this.$apollo
|
|
||||||
.mutate({
|
|
||||||
mutation: CreateTimelineEvent,
|
|
||||||
variables: {
|
|
||||||
input: {
|
|
||||||
incidentId: convertToGraphQLId(TYPE_ISSUE, this.issuableId),
|
|
||||||
note: this.timelineText,
|
|
||||||
occurredAt: this.createDateString(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
update: this.updateCache,
|
|
||||||
})
|
|
||||||
.then(({ data = {} }) => {
|
|
||||||
const errors = data.timelineEventCreate?.errors;
|
|
||||||
if (errors.length) {
|
|
||||||
createAlert({
|
|
||||||
message: sprintf(this.$options.i18n.createError, { error: errors.join('. ') }, false),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
createAlert({
|
|
||||||
message: this.$options.i18n.createErrorGeneric,
|
|
||||||
captureError: true,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.createTimelineEventActive = false;
|
|
||||||
this.timelineText = '';
|
|
||||||
if (addOneEvent) {
|
|
||||||
this.hideIncidentTimelineEventForm();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
createDateString() {
|
|
||||||
const [years, months, days] = this.datepickerTextInput.split('-');
|
|
||||||
const utcDate = new Date(
|
|
||||||
Date.UTC(years, months - 1, days, this.currentHour, this.currentMinute),
|
|
||||||
);
|
|
||||||
return utcDate.toISOString();
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -165,7 +99,7 @@ export default {
|
||||||
class="gl-display-flex gl-flex-direction-column gl-sm-flex-direction-row datetime-picker"
|
class="gl-display-flex gl-flex-direction-column gl-sm-flex-direction-row datetime-picker"
|
||||||
>
|
>
|
||||||
<gl-form-group :label="__('Date')" class="gl-mt-5 gl-mr-5">
|
<gl-form-group :label="__('Date')" class="gl-mt-5 gl-mr-5">
|
||||||
<gl-datepicker id="incident-date" #default="{ formattedDate }" v-model="currentDate">
|
<gl-datepicker id="incident-date" #default="{ formattedDate }" v-model="placeholderDate">
|
||||||
<gl-form-input
|
<gl-form-input
|
||||||
id="incident-date"
|
id="incident-date"
|
||||||
ref="datepicker"
|
ref="datepicker"
|
||||||
|
@ -184,7 +118,7 @@ export default {
|
||||||
<label label-for="timeline-input-hours" class="sr-only"></label>
|
<label label-for="timeline-input-hours" class="sr-only"></label>
|
||||||
<gl-form-input
|
<gl-form-input
|
||||||
id="timeline-input-hours"
|
id="timeline-input-hours"
|
||||||
v-model="currentHour"
|
v-model="hourPickerInput"
|
||||||
data-testid="input-hours"
|
data-testid="input-hours"
|
||||||
size="xs"
|
size="xs"
|
||||||
type="number"
|
type="number"
|
||||||
|
@ -194,7 +128,7 @@ export default {
|
||||||
<label label-for="timeline-input-minutes" class="sr-only"></label>
|
<label label-for="timeline-input-minutes" class="sr-only"></label>
|
||||||
<gl-form-input
|
<gl-form-input
|
||||||
id="timeline-input-minutes"
|
id="timeline-input-minutes"
|
||||||
v-model="currentMinute"
|
v-model="minutePickerInput"
|
||||||
class="gl-ml-3"
|
class="gl-ml-3"
|
||||||
data-testid="input-minutes"
|
data-testid="input-minutes"
|
||||||
size="xs"
|
size="xs"
|
||||||
|
@ -223,9 +157,10 @@ export default {
|
||||||
<textarea
|
<textarea
|
||||||
v-model="timelineText"
|
v-model="timelineText"
|
||||||
class="note-textarea js-gfm-input js-autosize markdown-area"
|
class="note-textarea js-gfm-input js-autosize markdown-area"
|
||||||
|
data-testid="input-note"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
data-supports-quick-actions="false"
|
data-supports-quick-actions="false"
|
||||||
:aria-label="__('Description')"
|
:aria-label="$options.i18n.description"
|
||||||
:placeholder="$options.i18n.areaPlaceholder"
|
:placeholder="$options.i18n.areaPlaceholder"
|
||||||
>
|
>
|
||||||
</textarea>
|
</textarea>
|
||||||
|
@ -238,26 +173,22 @@ export default {
|
||||||
variant="confirm"
|
variant="confirm"
|
||||||
category="primary"
|
category="primary"
|
||||||
class="gl-mr-3"
|
class="gl-mr-3"
|
||||||
:loading="createTimelineEventActive"
|
:loading="isEventProcessed"
|
||||||
@click="createIncidentTimelineEvent(true)"
|
@click="handleSave(false)"
|
||||||
>
|
>
|
||||||
{{ __('Save') }}
|
{{ $options.i18n.save }}
|
||||||
</gl-button>
|
</gl-button>
|
||||||
<gl-button
|
<gl-button
|
||||||
variant="confirm"
|
variant="confirm"
|
||||||
category="secondary"
|
category="secondary"
|
||||||
class="gl-mr-3 gl-ml-n2"
|
class="gl-mr-3 gl-ml-n2"
|
||||||
:loading="createTimelineEventActive"
|
:loading="isEventProcessed"
|
||||||
@click="createIncidentTimelineEvent(false)"
|
@click="handleSave(true)"
|
||||||
>
|
>
|
||||||
{{ $options.i18n.saveAndAdd }}
|
{{ $options.i18n.saveAndAdd }}
|
||||||
</gl-button>
|
</gl-button>
|
||||||
<gl-button
|
<gl-button class="gl-ml-n2" :disabled="isEventProcessed" @click="$emit('cancel')">
|
||||||
class="gl-ml-n2"
|
{{ $options.i18n.cancel }}
|
||||||
:disabled="createTimelineEventActive"
|
|
||||||
@click="hideIncidentTimelineEventForm"
|
|
||||||
>
|
|
||||||
{{ __('Cancel') }}
|
|
||||||
</gl-button>
|
</gl-button>
|
||||||
<div class="gl-border-b gl-pt-5"></div>
|
<div class="gl-border-b gl-pt-5"></div>
|
||||||
</gl-form-group>
|
</gl-form-group>
|
||||||
|
|
|
@ -7,7 +7,7 @@ import getTimelineEvents from './graphql/queries/get_timeline_events.query.graph
|
||||||
import { displayAndLogError } from './utils';
|
import { displayAndLogError } from './utils';
|
||||||
import { timelineTabI18n } from './constants';
|
import { timelineTabI18n } from './constants';
|
||||||
|
|
||||||
import IncidentTimelineEventForm from './timeline_events_form.vue';
|
import CreateTimelineEvent from './create_timeline_event.vue';
|
||||||
import IncidentTimelineEventsList from './timeline_events_list.vue';
|
import IncidentTimelineEventsList from './timeline_events_list.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -16,7 +16,7 @@ export default {
|
||||||
GlEmptyState,
|
GlEmptyState,
|
||||||
GlLoadingIcon,
|
GlLoadingIcon,
|
||||||
GlTab,
|
GlTab,
|
||||||
IncidentTimelineEventForm,
|
CreateTimelineEvent,
|
||||||
IncidentTimelineEventsList,
|
IncidentTimelineEventsList,
|
||||||
},
|
},
|
||||||
i18n: timelineTabI18n,
|
i18n: timelineTabI18n,
|
||||||
|
@ -61,10 +61,10 @@ export default {
|
||||||
this.isEventFormVisible = false;
|
this.isEventFormVisible = false;
|
||||||
},
|
},
|
||||||
async showEventForm() {
|
async showEventForm() {
|
||||||
this.$refs.eventForm.clear();
|
this.$refs.createEventForm.clearForm();
|
||||||
this.isEventFormVisible = true;
|
this.isEventFormVisible = true;
|
||||||
await this.$nextTick();
|
await this.$nextTick();
|
||||||
this.$refs.eventForm.focusDate();
|
this.$refs.createEventForm.focusDate();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -82,14 +82,15 @@ export default {
|
||||||
v-if="hasTimelineEvents"
|
v-if="hasTimelineEvents"
|
||||||
:timeline-event-loading="timelineEventLoading"
|
:timeline-event-loading="timelineEventLoading"
|
||||||
:timeline-events="timelineEvents"
|
:timeline-events="timelineEvents"
|
||||||
|
@hide-new-timeline-events-form="hideEventForm"
|
||||||
/>
|
/>
|
||||||
<incident-timeline-event-form
|
<create-timeline-event
|
||||||
v-show="isEventFormVisible"
|
v-show="isEventFormVisible"
|
||||||
ref="eventForm"
|
ref="createEventForm"
|
||||||
:has-timeline-events="hasTimelineEvents"
|
:has-timeline-events="hasTimelineEvents"
|
||||||
class="timeline-event-note timeline-event-note-form"
|
class="timeline-event-note timeline-event-note-form"
|
||||||
:class="{ 'gl-pl-0': !hasTimelineEvents }"
|
:class="{ 'gl-pl-0': !hasTimelineEvents }"
|
||||||
@hide-incident-timeline-event-form="hideEventForm"
|
@hide-new-timeline-events-form="hideEventForm"
|
||||||
/>
|
/>
|
||||||
<gl-button v-if="canUpdate" variant="default" class="gl-mb-3 gl-mt-7" @click="showEventForm">
|
<gl-button v-if="canUpdate" variant="default" class="gl-mb-3 gl-mt-7" @click="showEventForm">
|
||||||
{{ $options.i18n.addEventButton }}
|
{{ $options.i18n.addEventButton }}
|
||||||
|
|
|
@ -217,6 +217,7 @@ export default {
|
||||||
size="small"
|
size="small"
|
||||||
:icon="toggleIcon"
|
:icon="toggleIcon"
|
||||||
:aria-label="toggleLabel"
|
:aria-label="toggleLabel"
|
||||||
|
:disabled="!hasRelatedIssues"
|
||||||
data-testid="toggle-links"
|
data-testid="toggle-links"
|
||||||
@click="handleToggle"
|
@click="handleToggle"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -80,7 +80,7 @@ module Database
|
||||||
end
|
end
|
||||||
|
|
||||||
def max_id
|
def max_id
|
||||||
@max_id ||= source_model.minimum(source_sort_column)
|
@max_id ||= source_model.maximum(source_sort_column)
|
||||||
end
|
end
|
||||||
# rubocop: enable CodeReuse/ActiveRecord
|
# rubocop: enable CodeReuse/ActiveRecord
|
||||||
|
|
||||||
|
|
|
@ -205,6 +205,7 @@ successfully, you must replicate their data using some other means.
|
||||||
|[External merge request diffs](../../merge_request_diffs.md) | **Yes** (13.5) | **Yes** (14.6) | [**Yes** (15.1)](https://gitlab.com/groups/gitlab-org/-/epics/5551) | [No](object_storage.md#verification-of-files-in-object-storage) | Replication is behind the feature flag `geo_merge_request_diff_replication`, enabled by default. Verification was behind the feature flag `geo_merge_request_diff_verification`, removed in 14.7.|
|
|[External merge request diffs](../../merge_request_diffs.md) | **Yes** (13.5) | **Yes** (14.6) | [**Yes** (15.1)](https://gitlab.com/groups/gitlab-org/-/epics/5551) | [No](object_storage.md#verification-of-files-in-object-storage) | Replication is behind the feature flag `geo_merge_request_diff_replication`, enabled by default. Verification was behind the feature flag `geo_merge_request_diff_verification`, removed in 14.7.|
|
||||||
|[Versioned snippets](../../../user/snippets.md#versioned-snippets) | [**Yes** (13.7)](https://gitlab.com/groups/gitlab-org/-/epics/2809) | [**Yes** (14.2)](https://gitlab.com/groups/gitlab-org/-/epics/2810) | N/A | N/A | Verification was implemented behind the feature flag `geo_snippet_repository_verification` in 13.11, and the feature flag was removed in 14.2. |
|
|[Versioned snippets](../../../user/snippets.md#versioned-snippets) | [**Yes** (13.7)](https://gitlab.com/groups/gitlab-org/-/epics/2809) | [**Yes** (14.2)](https://gitlab.com/groups/gitlab-org/-/epics/2810) | N/A | N/A | Verification was implemented behind the feature flag `geo_snippet_repository_verification` in 13.11, and the feature flag was removed in 14.2. |
|
||||||
|[GitLab Pages](../../pages/index.md) | [**Yes** (14.3)](https://gitlab.com/groups/gitlab-org/-/epics/589) | **Yes** (14.6) | [**Yes** (15.1)](https://gitlab.com/groups/gitlab-org/-/epics/5551) | [No](object_storage.md#verification-of-files-in-object-storage) | Behind feature flag `geo_pages_deployment_replication`, enabled by default. Verification was behind the feature flag `geo_pages_deployment_verification`, removed in 14.7. |
|
|[GitLab Pages](../../pages/index.md) | [**Yes** (14.3)](https://gitlab.com/groups/gitlab-org/-/epics/589) | **Yes** (14.6) | [**Yes** (15.1)](https://gitlab.com/groups/gitlab-org/-/epics/5551) | [No](object_storage.md#verification-of-files-in-object-storage) | Behind feature flag `geo_pages_deployment_replication`, enabled by default. Verification was behind the feature flag `geo_pages_deployment_verification`, removed in 14.7. |
|
||||||
|
|[Project-level Secure files](../../../ci/secure_files/index.md) | **Yes** (15.3) | **Yes** (15.3) | **Yes** (15.3) | [No](object_storage.md#verification-of-files-in-object-storage) | |
|
||||||
|[Incident Metric Images](../../../operations/incident_management/incidents.md#metrics) | [Planned](https://gitlab.com/gitlab-org/gitlab/-/issues/362561) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/362561) | No | No | |
|
|[Incident Metric Images](../../../operations/incident_management/incidents.md#metrics) | [Planned](https://gitlab.com/gitlab-org/gitlab/-/issues/362561) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/362561) | No | No | |
|
||||||
|[Alert Metric Images](../../../operations/incident_management/alerts.md#metrics-tab) | [Planned](https://gitlab.com/gitlab-org/gitlab/-/issues/362564) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/362564) | No | No | |
|
|[Alert Metric Images](../../../operations/incident_management/alerts.md#metrics-tab) | [Planned](https://gitlab.com/gitlab-org/gitlab/-/issues/362564) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/362564) | No | No | |
|
||||||
|[Server-side Git hooks](../../server_hooks.md) | [Not planned](https://gitlab.com/groups/gitlab-org/-/epics/1867) | No | N/A | N/A | Not planned because of current implementation complexity, low customer interest, and availability of alternatives to hooks. |
|
|[Server-side Git hooks](../../server_hooks.md) | [Not planned](https://gitlab.com/groups/gitlab-org/-/epics/1867) | No | N/A | N/A | Not planned because of current implementation complexity, low customer interest, and availability of alternatives to hooks. |
|
||||||
|
|
|
@ -656,10 +656,12 @@ On each node perform the following:
|
||||||
# Set the network addresses that the exporters used for monitoring will listen on
|
# Set the network addresses that the exporters used for monitoring will listen on
|
||||||
node_exporter['listen_address'] = '0.0.0.0:9100'
|
node_exporter['listen_address'] = '0.0.0.0:9100'
|
||||||
gitlab_workhorse['prometheus_listen_addr'] = '0.0.0.0:9229'
|
gitlab_workhorse['prometheus_listen_addr'] = '0.0.0.0:9229'
|
||||||
sidekiq['listen_address'] = "0.0.0.0"
|
|
||||||
# Set number of Sidekiq threads per queue process to the recommend number of 10
|
|
||||||
sidekiq['max_concurrency'] = 10
|
|
||||||
puma['listen'] = '0.0.0.0'
|
puma['listen'] = '0.0.0.0'
|
||||||
|
sidekiq['listen_address'] = "0.0.0.0"
|
||||||
|
|
||||||
|
# Configure Sidekiq with 2 workers and 10 max concurrency
|
||||||
|
sidekiq['max_concurrency'] = 10
|
||||||
|
sidekiq['queue_groups'] = ['*'] * 2
|
||||||
|
|
||||||
# Add the monitoring node's IP address to the monitoring whitelist and allow it to
|
# Add the monitoring node's IP address to the monitoring whitelist and allow it to
|
||||||
# scrape the NGINX metrics. Replace placeholder `monitoring.gitlab.example.com` with
|
# scrape the NGINX metrics. Replace placeholder `monitoring.gitlab.example.com` with
|
||||||
|
@ -1009,8 +1011,8 @@ future with further specific cloud provider details.
|
||||||
| Service | Nodes | Configuration | GCP | AWS | Min Allocatable CPUs and Memory |
|
| Service | Nodes | Configuration | GCP | AWS | Min Allocatable CPUs and Memory |
|
||||||
|-----------------------------------------------|-------|------------------------|-----------------|--------------|---------------------------------|
|
|-----------------------------------------------|-------|------------------------|-----------------|--------------|---------------------------------|
|
||||||
| Webservice | 3 | 8 vCPU, 7.2 GB memory | `n1-highcpu-8` | `c5.2xlarge` | 23.7 vCPU, 16.9 GB memory |
|
| Webservice | 3 | 8 vCPU, 7.2 GB memory | `n1-highcpu-8` | `c5.2xlarge` | 23.7 vCPU, 16.9 GB memory |
|
||||||
| Sidekiq | 2 | 2 vCPU, 7.5 GB memory | `n1-standard-2` | `m5.large` | 3.9 vCPU, 11.8 GB memory |
|
| Sidekiq | 1 | 4 vCPU, 15 GB memory | `n1-standard-4` | `m5.xlarge` | 3.9 vCPU, 11.8 GB memory |
|
||||||
| Supporting services such as NGINX, Prometheus | 2 | 1 vCPU, 3.75 GB memory | `n1-standard-1` | `m5.large` | 1.9 vCPU, 5.5 GB memory |
|
| Supporting services such as NGINX, Prometheus | 2 | 2 vCPU, 7.5 GB memory | `n1-standard-2` | `m5.large` | 1.9 vCPU, 5.5 GB memory |
|
||||||
|
|
||||||
- For this setup, we **recommend** and regularly [test](index.md#validation-and-test-results)
|
- For this setup, we **recommend** and regularly [test](index.md#validation-and-test-results)
|
||||||
[Google Kubernetes Engine (GKE)](https://cloud.google.com/kubernetes-engine) and [Amazon Elastic Kubernetes Service (EKS)](https://aws.amazon.com/eks/). Other Kubernetes services may also work, but your mileage may vary.
|
[Google Kubernetes Engine (GKE)](https://cloud.google.com/kubernetes-engine) and [Amazon Elastic Kubernetes Service (EKS)](https://aws.amazon.com/eks/). Other Kubernetes services may also work, but your mileage may vary.
|
||||||
|
@ -1046,8 +1048,8 @@ card "Kubernetes via Helm Charts" as kubernetes {
|
||||||
|
|
||||||
together {
|
together {
|
||||||
collections "**Webservice** x3" as gitlab #32CD32
|
collections "**Webservice** x3" as gitlab #32CD32
|
||||||
collections "**Sidekiq** x2" as sidekiq #ff8dd1
|
card "**Sidekiq**" as sidekiq #ff8dd1
|
||||||
card "**Supporting Services**" as support
|
collections "**Supporting Services** x2" as support
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1080,13 +1082,13 @@ documents how to apply the calculated configuration to the Helm Chart.
|
||||||
#### Webservice
|
#### Webservice
|
||||||
|
|
||||||
Webservice pods typically need about 1 vCPU and 1.25 GB of memory _per worker_.
|
Webservice pods typically need about 1 vCPU and 1.25 GB of memory _per worker_.
|
||||||
Each Webservice pod consumes roughly 2 vCPUs and 2.5 GB of memory using
|
Each Webservice pod consumes roughly 4 vCPUs and 5 GB of memory using
|
||||||
the [recommended topology](#cluster-topology) because two worker processes
|
the [recommended topology](#cluster-topology) because two worker processes
|
||||||
are created by default and each pod has other small processes running.
|
are created by default and each pod has other small processes running.
|
||||||
|
|
||||||
For 2,000 users we recommend a total Puma worker count of around 12.
|
For 2,000 users we recommend a total Puma worker count of around 12.
|
||||||
With the [provided recommendations](#cluster-topology) this allows the deployment of up to 6
|
With the [provided recommendations](#cluster-topology) this allows the deployment of up to 3
|
||||||
Webservice pods with 2 workers per pod and 2 pods per node. Expand available resources using
|
Webservice pods with 4 workers per pod and 1 pod per node. Expand available resources using
|
||||||
the ratio of 1 vCPU to 1.25 GB of memory _per each worker process_ for each additional
|
the ratio of 1 vCPU to 1.25 GB of memory _per each worker process_ for each additional
|
||||||
Webservice pod.
|
Webservice pod.
|
||||||
|
|
||||||
|
@ -1097,7 +1099,7 @@ For further information on resource usage, see the [Webservice resources](https:
|
||||||
Sidekiq pods should generally have 1 vCPU and 2 GB of memory.
|
Sidekiq pods should generally have 1 vCPU and 2 GB of memory.
|
||||||
|
|
||||||
[The provided starting point](#cluster-topology) allows the deployment of up to
|
[The provided starting point](#cluster-topology) allows the deployment of up to
|
||||||
2 Sidekiq pods. Expand available resources using the 1 vCPU to 2GB memory
|
4 Sidekiq pods. Expand available resources using the 1 vCPU to 2 GB memory
|
||||||
ratio for each additional pod.
|
ratio for each additional pod.
|
||||||
|
|
||||||
For further information on resource usage, see the [Sidekiq resources](https://docs.gitlab.com/charts/charts/gitlab/sidekiq/#resources).
|
For further information on resource usage, see the [Sidekiq resources](https://docs.gitlab.com/charts/charts/gitlab/sidekiq/#resources).
|
||||||
|
|
|
@ -11795,7 +11795,7 @@ Represents epic board list metadata.
|
||||||
| Name | Type | Description |
|
| Name | Type | Description |
|
||||||
| ---- | ---- | ----------- |
|
| ---- | ---- | ----------- |
|
||||||
| <a id="epiclistmetadataepicscount"></a>`epicsCount` | [`Int`](#int) | Count of epics in the list. |
|
| <a id="epiclistmetadataepicscount"></a>`epicsCount` | [`Int`](#int) | Count of epics in the list. |
|
||||||
| <a id="epiclistmetadatatotalweight"></a>`totalWeight` | [`Int`](#int) | Total weight of all issues in the list. Available only when feature flag `epic_board_total_weight` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. |
|
| <a id="epiclistmetadatatotalweight"></a>`totalWeight` **{warning-solid}** | [`Int`](#int) | **Introduced** in 14.7. This feature is in Alpha. It can be changed or removed at any time. Total weight of all issues in the list. |
|
||||||
|
|
||||||
### `EpicPermissions`
|
### `EpicPermissions`
|
||||||
|
|
||||||
|
|
|
@ -25,8 +25,8 @@ multiple projects.
|
||||||
If you are using a self-managed instance of GitLab:
|
If you are using a self-managed instance of GitLab:
|
||||||
|
|
||||||
- Your administrator can install and register shared runners by
|
- Your administrator can install and register shared runners by
|
||||||
going to your project's **Settings > CI/CD**, expanding the **Runners** section,
|
going to your project's **Settings > CI/CD**, expanding **Runners**,
|
||||||
and clicking **Show runner installation instructions**.
|
and selecting **Show runner installation instructions**.
|
||||||
These instructions are also available [in the documentation](https://docs.gitlab.com/runner/install/index.html).
|
These instructions are also available [in the documentation](https://docs.gitlab.com/runner/install/index.html).
|
||||||
- The administrator can also configure a maximum number of shared runner
|
- The administrator can also configure a maximum number of shared runner
|
||||||
[CI/CD minutes for each group](../pipelines/cicd_minutes.md#set-the-quota-of-cicd-minutes-for-a-specific-namespace).
|
[CI/CD minutes for each group](../pipelines/cicd_minutes.md#set-the-quota-of-cicd-minutes-for-a-specific-namespace).
|
||||||
|
@ -51,15 +51,19 @@ For existing projects, an administrator must
|
||||||
|
|
||||||
To enable shared runners for a project:
|
To enable shared runners for a project:
|
||||||
|
|
||||||
1. Go to the project's **Settings > CI/CD** and expand the **Runners** section.
|
1. On the top bar, select **Menu > Projects** and find your project.
|
||||||
1. Select **Enable shared runners for this project**.
|
1. On the left sidebar, select **Settings > CI/CD**.
|
||||||
|
1. Expand **Runners**.
|
||||||
|
1. Turn on the **Enable shared runners for this project** toggle.
|
||||||
|
|
||||||
### Enable shared runners for a group
|
### Enable shared runners for a group
|
||||||
|
|
||||||
To enable shared runners for a group:
|
To enable shared runners for a group:
|
||||||
|
|
||||||
1. Go to the group's **Settings > CI/CD** and expand the **Runners** section.
|
1. On the top bar, select **Menu > Groups** and find your group.
|
||||||
1. Select **Enable shared runners for this group**.
|
1. On the left sidebar, select **Settings > CI/CD**.
|
||||||
|
1. Expand **Runners**.
|
||||||
|
1. Turn on the **Enable shared runners for this group** toggle.
|
||||||
|
|
||||||
### Disable shared runners for a project
|
### Disable shared runners for a project
|
||||||
|
|
||||||
|
@ -69,8 +73,10 @@ or group.
|
||||||
|
|
||||||
To disable shared runners for a project:
|
To disable shared runners for a project:
|
||||||
|
|
||||||
1. Go to the project's **Settings > CI/CD** and expand the **Runners** section.
|
1. On the top bar, select **Menu > Projects** and find your project.
|
||||||
1. In the **Shared runners** area, select **Enable shared runners for this project** so the toggle is grayed-out.
|
1. On the left sidebar, select **Settings > CI/CD**.
|
||||||
|
1. Expand **Runners**.
|
||||||
|
1. In the **Shared runners** area, turn off the **Enable shared runners for this project** toggle.
|
||||||
|
|
||||||
Shared runners are automatically disabled for a project:
|
Shared runners are automatically disabled for a project:
|
||||||
|
|
||||||
|
@ -81,9 +87,11 @@ Shared runners are automatically disabled for a project:
|
||||||
|
|
||||||
To disable shared runners for a group:
|
To disable shared runners for a group:
|
||||||
|
|
||||||
1. Go to the group's **Settings > CI/CD** and expand the **Runners** section.
|
1. On the top bar, select **Menu > Groups** and find your group.
|
||||||
1. In the **Shared runners** area, turn off the **Enable shared runners for this group** toggle.
|
1. On the left sidebar, select **Settings > CI/CD**.
|
||||||
1. Optionally, to allow shared runners to be enabled for individual projects or subgroups,
|
1. Expand **Runners**.
|
||||||
|
1. Turn off the **Enable shared runners for this group** toggle.
|
||||||
|
1. Optional. To allow shared runners to be enabled for individual projects or subgroups,
|
||||||
select **Allow projects and subgroups to override the group setting**.
|
select **Allow projects and subgroups to override the group setting**.
|
||||||
|
|
||||||
NOTE:
|
NOTE:
|
||||||
|
@ -147,7 +155,7 @@ The fair usage algorithm assigns jobs in this order:
|
||||||
|
|
||||||
## Group runners
|
## Group runners
|
||||||
|
|
||||||
Use *Group runners* when you want all projects in a group
|
Use _group runners_ when you want all projects in a group
|
||||||
to have access to a set of runners.
|
to have access to a set of runners.
|
||||||
|
|
||||||
Group runners process jobs by using a first in, first out ([FIFO](https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics))) queue.
|
Group runners process jobs by using a first in, first out ([FIFO](https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics))) queue.
|
||||||
|
@ -162,7 +170,7 @@ You must have the Owner role for the group.
|
||||||
To create a group runner:
|
To create a group runner:
|
||||||
|
|
||||||
1. [Install GitLab Runner](https://docs.gitlab.com/runner/install/).
|
1. [Install GitLab Runner](https://docs.gitlab.com/runner/install/).
|
||||||
1. Go to the group you want to make the runner work for.
|
1. On the top bar, select **Menu > Groups** and find your group.
|
||||||
1. On the left sidebar, select **CI/CD > Runners**.
|
1. On the left sidebar, select **CI/CD > Runners**.
|
||||||
1. Note the URL and token.
|
1. Note the URL and token.
|
||||||
1. [Register the runner](https://docs.gitlab.com/runner/register/).
|
1. [Register the runner](https://docs.gitlab.com/runner/register/).
|
||||||
|
@ -175,21 +183,8 @@ You can view and manage all runners for a group, its subgroups, and projects.
|
||||||
You can do this for your self-managed GitLab instance or for GitLab.com.
|
You can do this for your self-managed GitLab instance or for GitLab.com.
|
||||||
You must have the Owner role for the group.
|
You must have the Owner role for the group.
|
||||||
|
|
||||||
1. Go to the group where you want to view the runners.
|
1. On the top bar, select **Menu > Groups** and find your group.
|
||||||
1. On the left sidebar, select **CI/CD > Runners**.
|
1. On the left sidebar, select **CI/CD > Runners**.
|
||||||
1. The following fields are displayed.
|
|
||||||
|
|
||||||
| Attribute | Description |
|
|
||||||
| ------------ | ----------- |
|
|
||||||
| Type | Displays the runner type: `group` or `specific`, and the optional state `paused` |
|
|
||||||
| Runner token | Token used to identify the runner, and that the runner uses to communicate with the GitLab instance |
|
|
||||||
| Description | Description given to the runner when it was created |
|
|
||||||
| Version | GitLab Runner version |
|
|
||||||
| IP address | IP address of the host on which the runner is registered |
|
|
||||||
| Projects | The count of projects to which the runner is assigned |
|
|
||||||
| Jobs | Total of jobs run by the runner |
|
|
||||||
| Tags | Tags associated with the runner |
|
|
||||||
| Last contact | Timestamp indicating when the GitLab instance last contacted the runner |
|
|
||||||
|
|
||||||
From this page, you can edit, pause, and remove runners from the group, its subgroups, and projects.
|
From this page, you can edit, pause, and remove runners from the group, its subgroups, and projects.
|
||||||
|
|
||||||
|
@ -198,7 +193,7 @@ From this page, you can edit, pause, and remove runners from the group, its subg
|
||||||
You can pause or remove a group runner for your self-managed GitLab instance or for GitLab.com.
|
You can pause or remove a group runner for your self-managed GitLab instance or for GitLab.com.
|
||||||
You must have the Owner role for the group.
|
You must have the Owner role for the group.
|
||||||
|
|
||||||
1. Go to the group you want to remove or pause the runner for.
|
1. On the top bar, select **Menu > Groups** and find your group.
|
||||||
1. On the left sidebar, select **CI/CD > Runners**.
|
1. On the left sidebar, select **CI/CD > Runners**.
|
||||||
1. Select **Pause** or **Remove runner**.
|
1. Select **Pause** or **Remove runner**.
|
||||||
- If you pause a group runner that is used by multiple projects, the runner pauses for all projects.
|
- If you pause a group runner that is used by multiple projects, the runner pauses for all projects.
|
||||||
|
@ -208,7 +203,7 @@ You must have the Owner role for the group.
|
||||||
|
|
||||||
## Specific runners
|
## Specific runners
|
||||||
|
|
||||||
Use *Specific runners* when you want to use runners for specific projects. For example,
|
Use _specific runners_ when you want to use runners for specific projects. For example,
|
||||||
when you have:
|
when you have:
|
||||||
|
|
||||||
- Jobs with specific requirements, like a deploy job that requires credentials.
|
- Jobs with specific requirements, like a deploy job that requires credentials.
|
||||||
|
@ -257,9 +252,8 @@ To enable a specific runner for a project:
|
||||||
|
|
||||||
1. On the top bar, select **Menu > Projects** and find the project where you want to enable the runner.
|
1. On the top bar, select **Menu > Projects** and find the project where you want to enable the runner.
|
||||||
1. On the left sidebar, select **Settings > CI/CD**.
|
1. On the left sidebar, select **Settings > CI/CD**.
|
||||||
1. Expand **General pipelines**.
|
|
||||||
1. Expand **Runners**.
|
1. Expand **Runners**.
|
||||||
1. By the runner you want, select **Enable for this project**.
|
1. In the **Specific runners** area, by the runner you want, select **Enable for this project**.
|
||||||
|
|
||||||
You can edit a specific runner from any of the projects it's enabled for.
|
You can edit a specific runner from any of the projects it's enabled for.
|
||||||
The modifications, which include unlocking and editing tags and the description,
|
The modifications, which include unlocking and editing tags and the description,
|
||||||
|
@ -275,9 +269,10 @@ but can also be changed later.
|
||||||
|
|
||||||
To lock or unlock a specific runner:
|
To lock or unlock a specific runner:
|
||||||
|
|
||||||
1. Go to the project's **Settings > CI/CD**.
|
1. On the top bar, select **Menu > Projects** and find the project where you want to enable the runner.
|
||||||
1. Expand the **Runners** section.
|
1. On the left sidebar, select **Settings > CI/CD**.
|
||||||
|
1. Expand **Runners**.
|
||||||
1. Find the specific runner you want to lock or unlock. Make sure it's enabled. You cannot lock shared or group runners.
|
1. Find the specific runner you want to lock or unlock. Make sure it's enabled. You cannot lock shared or group runners.
|
||||||
1. Select **Edit** (**{pencil}**).
|
1. Select **Edit** (**{pencil}**).
|
||||||
1. Check the **Lock to current projects** option.
|
1. Select the **Lock to current projects** checkbox.
|
||||||
1. Select **Save changes**.
|
1. Select **Save changes**.
|
||||||
|
|
|
@ -239,6 +239,13 @@ Use **CI/CD minutes** instead of **CI minutes**, **pipeline minutes**, **pipelin
|
||||||
Do not use **click**. Instead, use **select** with buttons, links, menu items, and lists.
|
Do not use **click**. Instead, use **select** with buttons, links, menu items, and lists.
|
||||||
**Select** applies to more devices, while **click** is more specific to a mouse.
|
**Select** applies to more devices, while **click** is more specific to a mouse.
|
||||||
|
|
||||||
|
## cloud native
|
||||||
|
|
||||||
|
When you're talking about using a Kubernetes cluster to host GitLab, you're talking about a **cloud-native version of GitLab**.
|
||||||
|
This version is different than the larger, more monolithic **Omnibus package** that is used to deploy GitLab.
|
||||||
|
|
||||||
|
You can also use **cloud-native GitLab** for short. It should be hyphenated and lowercase.
|
||||||
|
|
||||||
## collapse
|
## collapse
|
||||||
|
|
||||||
Use **collapse** instead of **close** when you are talking about expanding or collapsing a section in the UI.
|
Use **collapse** instead of **close** when you are talking about expanding or collapsing a section in the UI.
|
||||||
|
@ -434,6 +441,17 @@ Do not make **GitLab** possessive (GitLab's). This guidance follows [GitLab Trad
|
||||||
|
|
||||||
**GitLab.com** refers to the GitLab instance managed by GitLab itself.
|
**GitLab.com** refers to the GitLab instance managed by GitLab itself.
|
||||||
|
|
||||||
|
## GitLab Helm chart, GitLab chart
|
||||||
|
|
||||||
|
To deploy a cloud-native version of GitLab, use:
|
||||||
|
|
||||||
|
- The GitLab Helm chart (long version)
|
||||||
|
- The GitLab chart (short version)
|
||||||
|
|
||||||
|
Do not use **the `gitlab` chart**, **the GitLab Chart**, or **the cloud-native chart**.
|
||||||
|
|
||||||
|
You use the **GitLab Helm chart** to deploy **cloud-native GitLab** in a Kubernetes cluster.
|
||||||
|
|
||||||
## GitLab Flavored Markdown
|
## GitLab Flavored Markdown
|
||||||
|
|
||||||
When possible, spell out [**GitLab Flavored Markdown**](../../../user/markdown.md).
|
When possible, spell out [**GitLab Flavored Markdown**](../../../user/markdown.md).
|
||||||
|
|
|
@ -20,8 +20,7 @@ describe('RelatedIssuesBlock', () => {
|
||||||
|
|
||||||
const findToggleButton = () => wrapper.findByTestId('toggle-links');
|
const findToggleButton = () => wrapper.findByTestId('toggle-links');
|
||||||
const findRelatedIssuesBody = () => wrapper.findByTestId('related-issues-body');
|
const findRelatedIssuesBody = () => wrapper.findByTestId('related-issues-body');
|
||||||
const findIssueCountBadgeAddButton = () =>
|
const findIssueCountBadgeAddButton = () => wrapper.findByTestId('related-issues-plus-button');
|
||||||
wrapper.find('[data-testid="related-issues-plus-button"]');
|
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
if (wrapper) {
|
if (wrapper) {
|
||||||
|
@ -240,6 +239,7 @@ describe('RelatedIssuesBlock', () => {
|
||||||
wrapper = shallowMountExtended(RelatedIssuesBlock, {
|
wrapper = shallowMountExtended(RelatedIssuesBlock, {
|
||||||
propsData: {
|
propsData: {
|
||||||
pathIdSeparator: PathIdSeparator.Issue,
|
pathIdSeparator: PathIdSeparator.Issue,
|
||||||
|
relatedIssues: [issuable1, issuable2, issuable3],
|
||||||
issuableType: issuableTypesMap.ISSUE,
|
issuableType: issuableTypesMap.ISSUE,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -247,6 +247,7 @@ describe('RelatedIssuesBlock', () => {
|
||||||
|
|
||||||
it('is expanded by default', () => {
|
it('is expanded by default', () => {
|
||||||
expect(findToggleButton().props('icon')).toBe('chevron-lg-up');
|
expect(findToggleButton().props('icon')).toBe('chevron-lg-up');
|
||||||
|
expect(findToggleButton().props('disabled')).toBe(false);
|
||||||
expect(findRelatedIssuesBody().exists()).toBe(true);
|
expect(findRelatedIssuesBody().exists()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -258,4 +259,16 @@ describe('RelatedIssuesBlock', () => {
|
||||||
expect(findRelatedIssuesBody().exists()).toBe(false);
|
expect(findRelatedIssuesBody().exists()).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('toggle button is disabled when issue has no related items', () => {
|
||||||
|
wrapper = shallowMountExtended(RelatedIssuesBlock, {
|
||||||
|
propsData: {
|
||||||
|
pathIdSeparator: PathIdSeparator.Issue,
|
||||||
|
relatedIssues: [],
|
||||||
|
issuableType: 'issue',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(findToggleButton().props('disabled')).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,189 @@
|
||||||
|
import VueApollo from 'vue-apollo';
|
||||||
|
import Vue from 'vue';
|
||||||
|
import { GlDatepicker } from '@gitlab/ui';
|
||||||
|
import { __, s__ } from '~/locale';
|
||||||
|
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||||
|
import waitForPromises from 'helpers/wait_for_promises';
|
||||||
|
import CreateTimelineEvent from '~/issues/show/components/incidents/create_timeline_event.vue';
|
||||||
|
import TimelineEventsForm from '~/issues/show/components/incidents/timeline_events_form.vue';
|
||||||
|
import createTimelineEventMutation from '~/issues/show/components/incidents/graphql/queries/create_timeline_event.mutation.graphql';
|
||||||
|
import getTimelineEvents from '~/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql';
|
||||||
|
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||||
|
import { createAlert } from '~/flash';
|
||||||
|
import { useFakeDate } from 'helpers/fake_date';
|
||||||
|
import {
|
||||||
|
timelineEventsCreateEventResponse,
|
||||||
|
timelineEventsCreateEventError,
|
||||||
|
mockGetTimelineData,
|
||||||
|
} from './mock_data';
|
||||||
|
|
||||||
|
Vue.use(VueApollo);
|
||||||
|
|
||||||
|
jest.mock('~/flash');
|
||||||
|
|
||||||
|
const fakeDate = '2020-07-08T00:00:00.000Z';
|
||||||
|
|
||||||
|
const mockInputData = {
|
||||||
|
incidentId: 'gid://gitlab/Issue/1',
|
||||||
|
note: 'test',
|
||||||
|
occurredAt: '2020-07-08T00:00:00.000Z',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Create Timeline events', () => {
|
||||||
|
useFakeDate(fakeDate);
|
||||||
|
let wrapper;
|
||||||
|
let responseSpy;
|
||||||
|
let apolloProvider;
|
||||||
|
|
||||||
|
const findSubmitButton = () => wrapper.findByText(__('Save'));
|
||||||
|
const findSubmitAndAddButton = () =>
|
||||||
|
wrapper.findByText(s__('Incident|Save and add another event'));
|
||||||
|
const findCancelButton = () => wrapper.findByText(__('Cancel'));
|
||||||
|
const findDatePicker = () => wrapper.findComponent(GlDatepicker);
|
||||||
|
const findNoteInput = () => wrapper.findByTestId('input-note');
|
||||||
|
const setNoteInput = () => {
|
||||||
|
const textarea = findNoteInput().element;
|
||||||
|
textarea.value = mockInputData.note;
|
||||||
|
textarea.dispatchEvent(new Event('input'));
|
||||||
|
};
|
||||||
|
const findHourInput = () => wrapper.findByTestId('input-hours');
|
||||||
|
const findMinuteInput = () => wrapper.findByTestId('input-minutes');
|
||||||
|
const setDatetime = () => {
|
||||||
|
const inputDate = new Date(mockInputData.occurredAt);
|
||||||
|
findDatePicker().vm.$emit('input', inputDate);
|
||||||
|
findHourInput().vm.$emit('input', inputDate.getHours());
|
||||||
|
findMinuteInput().vm.$emit('input', inputDate.getMinutes());
|
||||||
|
};
|
||||||
|
const fillForm = () => {
|
||||||
|
setDatetime();
|
||||||
|
setNoteInput();
|
||||||
|
};
|
||||||
|
|
||||||
|
function createMockApolloProvider() {
|
||||||
|
const requestHandlers = [[createTimelineEventMutation, responseSpy]];
|
||||||
|
const mockApollo = createMockApollo(requestHandlers);
|
||||||
|
|
||||||
|
mockApollo.clients.defaultClient.cache.writeQuery({
|
||||||
|
query: getTimelineEvents,
|
||||||
|
data: mockGetTimelineData,
|
||||||
|
variables: {
|
||||||
|
fullPath: 'group/project',
|
||||||
|
incidentId: 'gid://gitlab/Issue/1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return mockApollo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mountComponent = () => {
|
||||||
|
wrapper = mountExtended(CreateTimelineEvent, {
|
||||||
|
propsData: {
|
||||||
|
hasTimelineEvents: true,
|
||||||
|
},
|
||||||
|
provide: {
|
||||||
|
fullPath: 'group/project',
|
||||||
|
issuableId: '1',
|
||||||
|
},
|
||||||
|
apolloProvider,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
responseSpy = jest.fn().mockResolvedValue(timelineEventsCreateEventResponse);
|
||||||
|
apolloProvider = createMockApolloProvider();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
createAlert.mockReset();
|
||||||
|
wrapper.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createIncidentTimelineEvent', () => {
|
||||||
|
const closeFormEvent = { 'hide-new-timeline-events-form': [[]] };
|
||||||
|
|
||||||
|
const expectedData = {
|
||||||
|
input: mockInputData,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mountComponent();
|
||||||
|
fillForm();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('on submit', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
findSubmitButton().trigger('click');
|
||||||
|
await waitForPromises();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call the mutation with the right variables', () => {
|
||||||
|
expect(responseSpy).toHaveBeenCalledWith(expectedData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should close the form on successful addition', () => {
|
||||||
|
expect(wrapper.emitted()).toEqual(closeFormEvent);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('on submit and add', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
findSubmitAndAddButton().trigger('click');
|
||||||
|
await waitForPromises();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep the form open for save and add another', () => {
|
||||||
|
expect(wrapper.emitted()).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('on cancel', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
findCancelButton().trigger('click');
|
||||||
|
await waitForPromises();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should close the form', () => {
|
||||||
|
expect(wrapper.emitted()).toEqual(closeFormEvent);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('error handling', () => {
|
||||||
|
it('should show an error when submission returns an error', async () => {
|
||||||
|
const expectedAlertArgs = {
|
||||||
|
message: `Error creating incident timeline event: ${timelineEventsCreateEventError.data.timelineEventCreate.errors[0]}`,
|
||||||
|
};
|
||||||
|
responseSpy.mockResolvedValueOnce(timelineEventsCreateEventError);
|
||||||
|
mountComponent();
|
||||||
|
|
||||||
|
findSubmitButton().trigger('click');
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(createAlert).toHaveBeenCalledWith(expectedAlertArgs);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show an error when submission fails', async () => {
|
||||||
|
const expectedAlertArgs = {
|
||||||
|
captureError: true,
|
||||||
|
error: new Error(),
|
||||||
|
message: 'Something went wrong while creating the incident timeline event.',
|
||||||
|
};
|
||||||
|
responseSpy.mockRejectedValueOnce();
|
||||||
|
mountComponent();
|
||||||
|
|
||||||
|
findSubmitButton().trigger('click');
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(createAlert).toHaveBeenCalledWith(expectedAlertArgs);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep the form open on failed addition', async () => {
|
||||||
|
responseSpy.mockResolvedValueOnce(timelineEventsCreateEventError);
|
||||||
|
mountComponent();
|
||||||
|
|
||||||
|
await wrapper.findComponent(TimelineEventsForm).vm.$emit('save-event', mockInputData);
|
||||||
|
await waitForPromises;
|
||||||
|
expect(wrapper.emitted()).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -72,10 +72,14 @@ export const timelineEventsQueryEmptyResponse = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const timelineEventsCreateEventResponse = {
|
export const timelineEventsCreateEventResponse = {
|
||||||
timelineEvent: {
|
data: {
|
||||||
...mockEvents[0],
|
timelineEventCreate: {
|
||||||
|
timelineEvent: {
|
||||||
|
...mockEvents[0],
|
||||||
|
},
|
||||||
|
errors: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
errors: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const timelineEventsCreateEventError = {
|
export const timelineEventsCreateEventError = {
|
||||||
|
@ -103,3 +107,21 @@ const timelineEventDeleteData = (errors = []) => {
|
||||||
export const timelineEventsDeleteEventResponse = timelineEventDeleteData();
|
export const timelineEventsDeleteEventResponse = timelineEventDeleteData();
|
||||||
|
|
||||||
export const timelineEventsDeleteEventError = timelineEventDeleteData(['Item does not exist']);
|
export const timelineEventsDeleteEventError = timelineEventDeleteData(['Item does not exist']);
|
||||||
|
|
||||||
|
export const mockGetTimelineData = {
|
||||||
|
project: {
|
||||||
|
id: 'gid://gitlab/Project/19',
|
||||||
|
incidentManagementTimelineEvents: {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 'gid://gitlab/IncidentManagement::TimelineEvent/8',
|
||||||
|
note: 'another one2',
|
||||||
|
noteHtml: '<p>another one2</p>',
|
||||||
|
action: 'comment',
|
||||||
|
occurredAt: '2022-07-01T12:47:00Z',
|
||||||
|
createdAt: '2022-07-20T12:47:40Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
@ -3,49 +3,33 @@ import Vue, { nextTick } from 'vue';
|
||||||
import { GlDatepicker } from '@gitlab/ui';
|
import { GlDatepicker } from '@gitlab/ui';
|
||||||
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
|
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
|
||||||
import waitForPromises from 'helpers/wait_for_promises';
|
import waitForPromises from 'helpers/wait_for_promises';
|
||||||
import IncidentTimelineEventForm from '~/issues/show/components/incidents/timeline_events_form.vue';
|
import TimelineEventsForm from '~/issues/show/components/incidents/timeline_events_form.vue';
|
||||||
import createTimelineEventMutation from '~/issues/show/components/incidents/graphql/queries/create_timeline_event.mutation.graphql';
|
|
||||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
|
||||||
import { createAlert } from '~/flash';
|
import { createAlert } from '~/flash';
|
||||||
import { useFakeDate } from 'helpers/fake_date';
|
import { useFakeDate } from 'helpers/fake_date';
|
||||||
import { timelineEventsCreateEventResponse, timelineEventsCreateEventError } from './mock_data';
|
|
||||||
|
|
||||||
Vue.use(VueApollo);
|
Vue.use(VueApollo);
|
||||||
|
|
||||||
jest.mock('~/flash');
|
jest.mock('~/flash');
|
||||||
|
|
||||||
const addEventResponse = jest.fn().mockResolvedValue(timelineEventsCreateEventResponse);
|
const fakeDate = '2020-07-08T00:00:00.000Z';
|
||||||
|
|
||||||
function createMockApolloProvider(response = addEventResponse) {
|
|
||||||
const requestHandlers = [[createTimelineEventMutation, response]];
|
|
||||||
return createMockApollo(requestHandlers);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Timeline events form', () => {
|
describe('Timeline events form', () => {
|
||||||
// July 8 2020
|
// July 8 2020
|
||||||
useFakeDate(2020, 6, 8);
|
useFakeDate(fakeDate);
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
const mountComponent = ({ mockApollo, mountMethod = shallowMountExtended, stubs }) => {
|
const mountComponent = ({ mountMethod = shallowMountExtended }) => {
|
||||||
wrapper = mountMethod(IncidentTimelineEventForm, {
|
wrapper = mountMethod(TimelineEventsForm, {
|
||||||
propsData: {
|
propsData: {
|
||||||
hasTimelineEvents: true,
|
hasTimelineEvents: true,
|
||||||
|
isEventProcessed: false,
|
||||||
},
|
},
|
||||||
provide: {
|
|
||||||
fullPath: 'group/project',
|
|
||||||
issuableId: '1',
|
|
||||||
},
|
|
||||||
apolloProvider: mockApollo,
|
|
||||||
stubs,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
addEventResponse.mockReset();
|
|
||||||
createAlert.mockReset();
|
createAlert.mockReset();
|
||||||
if (wrapper) {
|
wrapper.destroy();
|
||||||
wrapper.destroy();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const findSubmitButton = () => wrapper.findByText('Save');
|
const findSubmitButton = () => wrapper.findByText('Save');
|
||||||
|
@ -75,24 +59,28 @@ describe('Timeline events form', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('form button behaviour', () => {
|
describe('form button behaviour', () => {
|
||||||
const closeFormEvent = { 'hide-incident-timeline-event-form': [[]] };
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mountComponent({ mockApollo: createMockApolloProvider(), mountMethod: mountExtended });
|
mountComponent({ mountMethod: mountExtended });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should close the form on submit', async () => {
|
it('should save event on submit', async () => {
|
||||||
await submitForm();
|
await submitForm();
|
||||||
expect(wrapper.emitted()).toEqual(closeFormEvent);
|
|
||||||
|
expect(wrapper.emitted()).toEqual({
|
||||||
|
'save-event': [[{ note: '', occurredAt: fakeDate }, false]],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not close the form on "submit and add another"', async () => {
|
it('should save event on "submit and add another"', async () => {
|
||||||
await submitFormAndAddAnother();
|
await submitFormAndAddAnother();
|
||||||
expect(wrapper.emitted()).toEqual({});
|
expect(wrapper.emitted()).toEqual({
|
||||||
|
'save-event': [[{ note: '', occurredAt: fakeDate }, true]],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should close the form on cancel', async () => {
|
it('should emit cancel on cancel', async () => {
|
||||||
await cancelForm();
|
await cancelForm();
|
||||||
expect(wrapper.emitted()).toEqual(closeFormEvent);
|
expect(wrapper.emitted()).toEqual({ cancel: [[]] });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should clear the form', async () => {
|
it('should clear the form', async () => {
|
||||||
|
@ -111,71 +99,4 @@ describe('Timeline events form', () => {
|
||||||
expect(findMinuteInput().element.value).toBe('0');
|
expect(findMinuteInput().element.value).toBe('0');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('addTimelineEventQuery', () => {
|
|
||||||
const expectedData = {
|
|
||||||
input: {
|
|
||||||
incidentId: 'gid://gitlab/Issue/1',
|
|
||||||
note: '',
|
|
||||||
occurredAt: '2020-07-08T00:00:00.000Z',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let mockApollo;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockApollo = createMockApolloProvider();
|
|
||||||
mountComponent({ mockApollo, mountMethod: mountExtended });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call the mutation with the right variables', async () => {
|
|
||||||
await submitForm();
|
|
||||||
|
|
||||||
expect(addEventResponse).toHaveBeenCalledWith(expectedData);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call the mutation with user selected variables', async () => {
|
|
||||||
const expectedUserSelectedData = {
|
|
||||||
input: {
|
|
||||||
...expectedData.input,
|
|
||||||
occurredAt: '2021-08-12T05:45:00.000Z',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
setDatetime();
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
await submitForm();
|
|
||||||
|
|
||||||
expect(addEventResponse).toHaveBeenCalledWith(expectedUserSelectedData);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('error handling', () => {
|
|
||||||
it('should show an error when submission returns an error', async () => {
|
|
||||||
const expectedAlertArgs = {
|
|
||||||
message: 'Error creating incident timeline event: Create error',
|
|
||||||
};
|
|
||||||
addEventResponse.mockResolvedValueOnce(timelineEventsCreateEventError);
|
|
||||||
mountComponent({ mockApollo: createMockApolloProvider(), mountMethod: mountExtended });
|
|
||||||
|
|
||||||
await submitForm();
|
|
||||||
|
|
||||||
expect(createAlert).toHaveBeenCalledWith(expectedAlertArgs);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show an error when submission fails', async () => {
|
|
||||||
const expectedAlertArgs = {
|
|
||||||
captureError: true,
|
|
||||||
error: new Error(),
|
|
||||||
message: 'Something went wrong while creating the incident timeline event.',
|
|
||||||
};
|
|
||||||
addEventResponse.mockRejectedValueOnce();
|
|
||||||
mountComponent({ mockApollo: createMockApolloProvider(), mountMethod: mountExtended });
|
|
||||||
|
|
||||||
await submitForm();
|
|
||||||
|
|
||||||
expect(createAlert).toHaveBeenCalledWith(expectedAlertArgs);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_help
|
||||||
import waitForPromises from 'helpers/wait_for_promises';
|
import waitForPromises from 'helpers/wait_for_promises';
|
||||||
import TimelineEventsTab from '~/issues/show/components/incidents/timeline_events_tab.vue';
|
import TimelineEventsTab from '~/issues/show/components/incidents/timeline_events_tab.vue';
|
||||||
import IncidentTimelineEventsList from '~/issues/show/components/incidents/timeline_events_list.vue';
|
import IncidentTimelineEventsList from '~/issues/show/components/incidents/timeline_events_list.vue';
|
||||||
import IncidentTimelineEventForm from '~/issues/show/components/incidents/timeline_events_form.vue';
|
import CreateTimelineEvent from '~/issues/show/components/incidents/create_timeline_event.vue';
|
||||||
import timelineEventsQuery from '~/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql';
|
import timelineEventsQuery from '~/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql';
|
||||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||||
import { createAlert } from '~/flash';
|
import { createAlert } from '~/flash';
|
||||||
|
@ -53,7 +53,7 @@ describe('TimelineEventsTab', () => {
|
||||||
const findLoadingSpinner = () => wrapper.findComponent(GlLoadingIcon);
|
const findLoadingSpinner = () => wrapper.findComponent(GlLoadingIcon);
|
||||||
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
|
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
|
||||||
const findTimelineEventsList = () => wrapper.findComponent(IncidentTimelineEventsList);
|
const findTimelineEventsList = () => wrapper.findComponent(IncidentTimelineEventsList);
|
||||||
const findTimelineEventForm = () => wrapper.findComponent(IncidentTimelineEventForm);
|
const findCreateTimelineEvent = () => wrapper.findComponent(CreateTimelineEvent);
|
||||||
const findAddEventButton = () => wrapper.findByText(timelineTabI18n.addEventButton);
|
const findAddEventButton = () => wrapper.findByText(timelineTabI18n.addEventButton);
|
||||||
|
|
||||||
describe('Timeline events tab', () => {
|
describe('Timeline events tab', () => {
|
||||||
|
@ -143,18 +143,18 @@ describe('TimelineEventsTab', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not show a form by default', () => {
|
it('should not show a form by default', () => {
|
||||||
expect(findTimelineEventForm().isVisible()).toBe(false);
|
expect(findCreateTimelineEvent().isVisible()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show a form when button is clicked', async () => {
|
it('should show a form when button is clicked', async () => {
|
||||||
await findAddEventButton().trigger('click');
|
await findAddEventButton().trigger('click');
|
||||||
|
|
||||||
expect(findTimelineEventForm().isVisible()).toBe(true);
|
expect(findCreateTimelineEvent().isVisible()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should clear the form when button is clicked', async () => {
|
it('should clear the form when button is clicked', async () => {
|
||||||
const mockClear = jest.fn();
|
const mockClear = jest.fn();
|
||||||
wrapper.vm.$refs.eventForm.clear = mockClear;
|
wrapper.vm.$refs.createEventForm.clearForm = mockClear;
|
||||||
|
|
||||||
await findAddEventButton().trigger('click');
|
await findAddEventButton().trigger('click');
|
||||||
|
|
||||||
|
@ -165,9 +165,9 @@ describe('TimelineEventsTab', () => {
|
||||||
// open the form
|
// open the form
|
||||||
await findAddEventButton().trigger('click');
|
await findAddEventButton().trigger('click');
|
||||||
|
|
||||||
await findTimelineEventForm().vm.$emit('hide-incident-timeline-event-form');
|
await findCreateTimelineEvent().vm.$emit('hide-new-timeline-events-form');
|
||||||
|
|
||||||
expect(findTimelineEventForm().isVisible()).toBe(false);
|
expect(findCreateTimelineEvent().isVisible()).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -24,9 +24,27 @@ RSpec.describe Database::ConsistencyCheckService do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#random_start_id' do
|
describe '#min_id' do
|
||||||
let(:batch_size) { 5 }
|
before do
|
||||||
|
create_list(:namespace, 3)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the id of the first record in the database' do
|
||||||
|
expect(subject.send(:min_id)).to eq(Namespace.first.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#max_id' do
|
||||||
|
before do
|
||||||
|
create_list(:namespace, 3)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the id of the first record in the database' do
|
||||||
|
expect(subject.send(:max_id)).to eq(Namespace.last.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#random_start_id' do
|
||||||
before do
|
before do
|
||||||
create_list(:namespace, 50) # This will also create Ci::NameSpaceMirror objects
|
create_list(:namespace, 50) # This will also create Ci::NameSpaceMirror objects
|
||||||
end
|
end
|
||||||
|
@ -58,12 +76,11 @@ RSpec.describe Database::ConsistencyCheckService do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'no cursor has been saved before' do
|
context 'no cursor has been saved before' do
|
||||||
let(:selected_start_id) { Namespace.order(:id).limit(5).pluck(:id).last }
|
let(:min_id) { Namespace.first.id }
|
||||||
let(:expected_next_start_id) { selected_start_id + batch_size * max_batches }
|
let(:max_id) { Namespace.last.id }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
create_list(:namespace, 50) # This will also create Ci::NameSpaceMirror objects
|
create_list(:namespace, 50) # This will also create Ci::NameSpaceMirror objects
|
||||||
expect(consistency_check_service).to receive(:random_start_id).and_return(selected_start_id)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'picks a random start_id' do
|
it 'picks a random start_id' do
|
||||||
|
@ -72,17 +89,21 @@ RSpec.describe Database::ConsistencyCheckService do
|
||||||
matches: 10,
|
matches: 10,
|
||||||
mismatches: 0,
|
mismatches: 0,
|
||||||
mismatches_details: [],
|
mismatches_details: [],
|
||||||
start_id: selected_start_id,
|
start_id: be_between(min_id, max_id),
|
||||||
next_start_id: expected_next_start_id
|
next_start_id: be_between(min_id, max_id)
|
||||||
}
|
}
|
||||||
expect(consistency_check_service.execute).to eq(expected_result)
|
expect(consistency_check_service).to receive(:rand).with(min_id..max_id).and_call_original
|
||||||
|
result = consistency_check_service.execute
|
||||||
|
expect(result).to match(expected_result)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'calls the ConsistencyCheckService with the expected parameters' do
|
it 'calls the ConsistencyCheckService with the expected parameters' do
|
||||||
|
expect(consistency_check_service).to receive(:random_start_id).and_return(min_id)
|
||||||
|
|
||||||
allow_next_instance_of(Gitlab::Database::ConsistencyChecker) do |instance|
|
allow_next_instance_of(Gitlab::Database::ConsistencyChecker) do |instance|
|
||||||
expect(instance).to receive(:execute).with(start_id: selected_start_id).and_return({
|
expect(instance).to receive(:execute).with(start_id: min_id).and_return({
|
||||||
batches: 2,
|
batches: 2,
|
||||||
next_start_id: expected_next_start_id,
|
next_start_id: min_id + batch_size,
|
||||||
matches: 10,
|
matches: 10,
|
||||||
mismatches: 0,
|
mismatches: 0,
|
||||||
mismatches_details: []
|
mismatches_details: []
|
||||||
|
@ -98,17 +119,19 @@ RSpec.describe Database::ConsistencyCheckService do
|
||||||
|
|
||||||
expected_result = {
|
expected_result = {
|
||||||
batches: 2,
|
batches: 2,
|
||||||
start_id: selected_start_id,
|
|
||||||
next_start_id: expected_next_start_id,
|
|
||||||
matches: 10,
|
matches: 10,
|
||||||
mismatches: 0,
|
mismatches: 0,
|
||||||
mismatches_details: []
|
mismatches_details: [],
|
||||||
|
start_id: be_between(min_id, max_id),
|
||||||
|
next_start_id: be_between(min_id, max_id)
|
||||||
}
|
}
|
||||||
expect(consistency_check_service.execute).to eq(expected_result)
|
result = consistency_check_service.execute
|
||||||
|
expect(result).to match(expected_result)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'saves the next_start_id in Redis for he next iteration' do
|
it 'saves the next_start_id in Redis for he next iteration' do
|
||||||
expect(consistency_check_service).to receive(:save_next_start_id).with(expected_next_start_id).and_call_original
|
expect(consistency_check_service).to receive(:save_next_start_id)
|
||||||
|
.with(be_between(min_id, max_id)).and_call_original
|
||||||
consistency_check_service.execute
|
consistency_check_service.execute
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,7 +6,7 @@ RSpec.describe Git::BranchPushService, services: true do
|
||||||
include RepoHelpers
|
include RepoHelpers
|
||||||
|
|
||||||
let_it_be(:user) { create(:user) }
|
let_it_be(:user) { create(:user) }
|
||||||
let_it_be(:project, reload: true) { create(:project, :repository) }
|
let_it_be_with_refind(:project) { create(:project, :repository) }
|
||||||
|
|
||||||
let(:blankrev) { Gitlab::Git::BLANK_SHA }
|
let(:blankrev) { Gitlab::Git::BLANK_SHA }
|
||||||
let(:oldrev) { sample_commit.parent_id }
|
let(:oldrev) { sample_commit.parent_id }
|
||||||
|
|
|
@ -16,7 +16,9 @@ RSpec.shared_examples 'date sidebar widget' do
|
||||||
page.within('[data-testid="sidebar-due-date"]') do
|
page.within('[data-testid="sidebar-due-date"]') do
|
||||||
today = Date.today.day
|
today = Date.today.day
|
||||||
|
|
||||||
click_button 'Edit'
|
button = find_button('Edit')
|
||||||
|
scroll_to(button)
|
||||||
|
button.click
|
||||||
|
|
||||||
click_button today.to_s
|
click_button today.to_s
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue