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({
|
||||
title: s__('Incident|Timeline'),
|
||||
|
@ -12,6 +12,9 @@ export const timelineFormI18n = Object.freeze({
|
|||
'Incident|Something went wrong while creating the incident timeline event.',
|
||||
),
|
||||
areaPlaceholder: s__('Incident|Timeline text...'),
|
||||
save: __('Save'),
|
||||
cancel: __('Cancel'),
|
||||
description: __('Description'),
|
||||
saveAndAdd: s__('Incident|Save and add another event'),
|
||||
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>
|
||||
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 { createAlert } from '~/flash';
|
||||
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
|
||||
import { sprintf } from '~/locale';
|
||||
import { getUtcShiftedDateNow } from './utils';
|
||||
import { timelineFormI18n } from './constants';
|
||||
|
||||
import CreateTimelineEvent from './graphql/queries/create_timeline_event.mutation.graphql';
|
||||
import getTimelineEvents from './graphql/queries/get_timeline_events.query.graphql';
|
||||
import { getUtcShiftedDateNow } from './utils';
|
||||
|
||||
export default {
|
||||
name: 'IncidentTimelineEventForm',
|
||||
name: 'TimelineEventsForm',
|
||||
restrictedToolBarItems: [
|
||||
'quote',
|
||||
'strikethrough',
|
||||
|
@ -38,112 +29,55 @@ export default {
|
|||
directives: {
|
||||
autofocusonshow,
|
||||
},
|
||||
inject: ['fullPath', 'issuableId'],
|
||||
props: {
|
||||
hasTimelineEvents: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
isEventProcessed: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
// Create shifted date to force the datepicker to format in UTC
|
||||
const utcShiftedDate = getUtcShiftedDateNow();
|
||||
// if occurredAt is undefined, returns "now" in UTC
|
||||
const placeholderDate = getUtcShiftedDateNow();
|
||||
|
||||
return {
|
||||
currentDate: utcShiftedDate,
|
||||
currentHour: utcShiftedDate.getHours(),
|
||||
currentMinute: utcShiftedDate.getMinutes(),
|
||||
timelineText: '',
|
||||
createTimelineEventActive: false,
|
||||
placeholderDate,
|
||||
hourPickerInput: placeholderDate.getHours(),
|
||||
minutePickerInput: placeholderDate.getMinutes(),
|
||||
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: {
|
||||
clear() {
|
||||
const utcShiftedDate = getUtcShiftedDateNow();
|
||||
this.currentDate = utcShiftedDate;
|
||||
this.currentHour = utcShiftedDate.getHours();
|
||||
this.currentMinute = utcShiftedDate.getMinutes();
|
||||
},
|
||||
hideIncidentTimelineEventForm() {
|
||||
this.$emit('hide-incident-timeline-event-form');
|
||||
const utcShiftedDateNow = getUtcShiftedDateNow();
|
||||
this.placeholderDate = utcShiftedDateNow;
|
||||
this.hourPickerInput = utcShiftedDateNow.getHours();
|
||||
this.minutePickerInput = utcShiftedDateNow.getMinutes();
|
||||
this.timelineText = '';
|
||||
},
|
||||
focusDate() {
|
||||
this.$refs.datepicker.$el.focus();
|
||||
},
|
||||
updateCache(store, { data }) {
|
||||
const { timelineEvent: event, errors } = data?.timelineEventCreate || {};
|
||||
|
||||
if (errors.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const variables = {
|
||||
incidentId: convertToGraphQLId(TYPE_ISSUE, this.issuableId),
|
||||
fullPath: this.fullPath,
|
||||
handleSave(addAnotherEvent) {
|
||||
const eventDetails = {
|
||||
note: this.timelineText,
|
||||
occurredAt: this.occurredAt,
|
||||
};
|
||||
|
||||
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();
|
||||
this.$emit('save-event', eventDetails, addAnotherEvent);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -165,7 +99,7 @@ export default {
|
|||
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-datepicker id="incident-date" #default="{ formattedDate }" v-model="currentDate">
|
||||
<gl-datepicker id="incident-date" #default="{ formattedDate }" v-model="placeholderDate">
|
||||
<gl-form-input
|
||||
id="incident-date"
|
||||
ref="datepicker"
|
||||
|
@ -184,7 +118,7 @@ export default {
|
|||
<label label-for="timeline-input-hours" class="sr-only"></label>
|
||||
<gl-form-input
|
||||
id="timeline-input-hours"
|
||||
v-model="currentHour"
|
||||
v-model="hourPickerInput"
|
||||
data-testid="input-hours"
|
||||
size="xs"
|
||||
type="number"
|
||||
|
@ -194,7 +128,7 @@ export default {
|
|||
<label label-for="timeline-input-minutes" class="sr-only"></label>
|
||||
<gl-form-input
|
||||
id="timeline-input-minutes"
|
||||
v-model="currentMinute"
|
||||
v-model="minutePickerInput"
|
||||
class="gl-ml-3"
|
||||
data-testid="input-minutes"
|
||||
size="xs"
|
||||
|
@ -223,9 +157,10 @@ export default {
|
|||
<textarea
|
||||
v-model="timelineText"
|
||||
class="note-textarea js-gfm-input js-autosize markdown-area"
|
||||
data-testid="input-note"
|
||||
dir="auto"
|
||||
data-supports-quick-actions="false"
|
||||
:aria-label="__('Description')"
|
||||
:aria-label="$options.i18n.description"
|
||||
:placeholder="$options.i18n.areaPlaceholder"
|
||||
>
|
||||
</textarea>
|
||||
|
@ -238,26 +173,22 @@ export default {
|
|||
variant="confirm"
|
||||
category="primary"
|
||||
class="gl-mr-3"
|
||||
:loading="createTimelineEventActive"
|
||||
@click="createIncidentTimelineEvent(true)"
|
||||
:loading="isEventProcessed"
|
||||
@click="handleSave(false)"
|
||||
>
|
||||
{{ __('Save') }}
|
||||
{{ $options.i18n.save }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
variant="confirm"
|
||||
category="secondary"
|
||||
class="gl-mr-3 gl-ml-n2"
|
||||
:loading="createTimelineEventActive"
|
||||
@click="createIncidentTimelineEvent(false)"
|
||||
:loading="isEventProcessed"
|
||||
@click="handleSave(true)"
|
||||
>
|
||||
{{ $options.i18n.saveAndAdd }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
class="gl-ml-n2"
|
||||
:disabled="createTimelineEventActive"
|
||||
@click="hideIncidentTimelineEventForm"
|
||||
>
|
||||
{{ __('Cancel') }}
|
||||
<gl-button class="gl-ml-n2" :disabled="isEventProcessed" @click="$emit('cancel')">
|
||||
{{ $options.i18n.cancel }}
|
||||
</gl-button>
|
||||
<div class="gl-border-b gl-pt-5"></div>
|
||||
</gl-form-group>
|
||||
|
|
|
@ -7,7 +7,7 @@ import getTimelineEvents from './graphql/queries/get_timeline_events.query.graph
|
|||
import { displayAndLogError } from './utils';
|
||||
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';
|
||||
|
||||
export default {
|
||||
|
@ -16,7 +16,7 @@ export default {
|
|||
GlEmptyState,
|
||||
GlLoadingIcon,
|
||||
GlTab,
|
||||
IncidentTimelineEventForm,
|
||||
CreateTimelineEvent,
|
||||
IncidentTimelineEventsList,
|
||||
},
|
||||
i18n: timelineTabI18n,
|
||||
|
@ -61,10 +61,10 @@ export default {
|
|||
this.isEventFormVisible = false;
|
||||
},
|
||||
async showEventForm() {
|
||||
this.$refs.eventForm.clear();
|
||||
this.$refs.createEventForm.clearForm();
|
||||
this.isEventFormVisible = true;
|
||||
await this.$nextTick();
|
||||
this.$refs.eventForm.focusDate();
|
||||
this.$refs.createEventForm.focusDate();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -82,14 +82,15 @@ export default {
|
|||
v-if="hasTimelineEvents"
|
||||
:timeline-event-loading="timelineEventLoading"
|
||||
:timeline-events="timelineEvents"
|
||||
@hide-new-timeline-events-form="hideEventForm"
|
||||
/>
|
||||
<incident-timeline-event-form
|
||||
<create-timeline-event
|
||||
v-show="isEventFormVisible"
|
||||
ref="eventForm"
|
||||
ref="createEventForm"
|
||||
:has-timeline-events="hasTimelineEvents"
|
||||
class="timeline-event-note timeline-event-note-form"
|
||||
: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">
|
||||
{{ $options.i18n.addEventButton }}
|
||||
|
|
|
@ -217,6 +217,7 @@ export default {
|
|||
size="small"
|
||||
:icon="toggleIcon"
|
||||
:aria-label="toggleLabel"
|
||||
:disabled="!hasRelatedIssues"
|
||||
data-testid="toggle-links"
|
||||
@click="handleToggle"
|
||||
/>
|
||||
|
|
|
@ -80,7 +80,7 @@ module Database
|
|||
end
|
||||
|
||||
def max_id
|
||||
@max_id ||= source_model.minimum(source_sort_column)
|
||||
@max_id ||= source_model.maximum(source_sort_column)
|
||||
end
|
||||
# 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.|
|
||||
|[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. |
|
||||
|[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 | |
|
||||
|[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. |
|
||||
|
|
|
@ -656,10 +656,12 @@ On each node perform the following:
|
|||
# Set the network addresses that the exporters used for monitoring will listen on
|
||||
node_exporter['listen_address'] = '0.0.0.0:9100'
|
||||
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'
|
||||
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
|
||||
# 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 |
|
||||
|-----------------------------------------------|-------|------------------------|-----------------|--------------|---------------------------------|
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 | 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)
|
||||
[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 {
|
||||
collections "**Webservice** x3" as gitlab #32CD32
|
||||
collections "**Sidekiq** x2" as sidekiq #ff8dd1
|
||||
card "**Supporting Services**" as support
|
||||
card "**Sidekiq**" as sidekiq #ff8dd1
|
||||
collections "**Supporting Services** x2" as support
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1080,13 +1082,13 @@ documents how to apply the calculated configuration to the Helm Chart.
|
|||
#### Webservice
|
||||
|
||||
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
|
||||
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.
|
||||
With the [provided recommendations](#cluster-topology) this allows the deployment of up to 6
|
||||
Webservice pods with 2 workers per pod and 2 pods per node. Expand available resources using
|
||||
With the [provided recommendations](#cluster-topology) this allows the deployment of up to 3
|
||||
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
|
||||
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.
|
||||
|
||||
[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.
|
||||
|
||||
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 |
|
||||
| ---- | ---- | ----------- |
|
||||
| <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`
|
||||
|
||||
|
|
|
@ -25,8 +25,8 @@ multiple projects.
|
|||
If you are using a self-managed instance of GitLab:
|
||||
|
||||
- Your administrator can install and register shared runners by
|
||||
going to your project's **Settings > CI/CD**, expanding the **Runners** section,
|
||||
and clicking **Show runner installation instructions**.
|
||||
going to your project's **Settings > CI/CD**, expanding **Runners**,
|
||||
and selecting **Show runner installation instructions**.
|
||||
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
|
||||
[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:
|
||||
|
||||
1. Go to the project's **Settings > CI/CD** and expand the **Runners** section.
|
||||
1. Select **Enable shared runners for this project**.
|
||||
1. On the top bar, select **Menu > Projects** and find your 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
|
||||
|
||||
To enable shared runners for a group:
|
||||
|
||||
1. Go to the group's **Settings > CI/CD** and expand the **Runners** section.
|
||||
1. Select **Enable shared runners for this group**.
|
||||
1. On the top bar, select **Menu > Groups** and find your 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
|
||||
|
||||
|
@ -69,8 +73,10 @@ or group.
|
|||
|
||||
To disable shared runners for a project:
|
||||
|
||||
1. Go to the project's **Settings > CI/CD** and expand the **Runners** section.
|
||||
1. In the **Shared runners** area, select **Enable shared runners for this project** so the toggle is grayed-out.
|
||||
1. On the top bar, select **Menu > Projects** and find your project.
|
||||
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:
|
||||
|
||||
|
@ -81,9 +87,11 @@ Shared runners are automatically disabled for a project:
|
|||
|
||||
To disable shared runners for a group:
|
||||
|
||||
1. Go to the group's **Settings > CI/CD** and expand the **Runners** section.
|
||||
1. In the **Shared runners** area, turn off the **Enable shared runners for this group** toggle.
|
||||
1. Optionally, to allow shared runners to be enabled for individual projects or subgroups,
|
||||
1. On the top bar, select **Menu > Groups** and find your group.
|
||||
1. On the left sidebar, select **Settings > CI/CD**.
|
||||
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**.
|
||||
|
||||
NOTE:
|
||||
|
@ -147,7 +155,7 @@ The fair usage algorithm assigns jobs in this order:
|
|||
|
||||
## 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.
|
||||
|
||||
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:
|
||||
|
||||
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. Note the URL and token.
|
||||
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 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. 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.
|
||||
|
||||
|
@ -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 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. Select **Pause** or **Remove runner**.
|
||||
- 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
|
||||
|
||||
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:
|
||||
|
||||
- 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 left sidebar, select **Settings > CI/CD**.
|
||||
1. Expand **General pipelines**.
|
||||
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.
|
||||
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:
|
||||
|
||||
1. Go to the project's **Settings > CI/CD**.
|
||||
1. Expand the **Runners** section.
|
||||
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. 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. Select **Edit** (**{pencil}**).
|
||||
1. Check the **Lock to current projects** option.
|
||||
1. Select the **Lock to current projects** checkbox.
|
||||
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.
|
||||
**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
|
||||
|
||||
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 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
|
||||
|
||||
When possible, spell out [**GitLab Flavored Markdown**](../../../user/markdown.md).
|
||||
|
|
|
@ -20,8 +20,7 @@ describe('RelatedIssuesBlock', () => {
|
|||
|
||||
const findToggleButton = () => wrapper.findByTestId('toggle-links');
|
||||
const findRelatedIssuesBody = () => wrapper.findByTestId('related-issues-body');
|
||||
const findIssueCountBadgeAddButton = () =>
|
||||
wrapper.find('[data-testid="related-issues-plus-button"]');
|
||||
const findIssueCountBadgeAddButton = () => wrapper.findByTestId('related-issues-plus-button');
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
|
@ -240,6 +239,7 @@ describe('RelatedIssuesBlock', () => {
|
|||
wrapper = shallowMountExtended(RelatedIssuesBlock, {
|
||||
propsData: {
|
||||
pathIdSeparator: PathIdSeparator.Issue,
|
||||
relatedIssues: [issuable1, issuable2, issuable3],
|
||||
issuableType: issuableTypesMap.ISSUE,
|
||||
},
|
||||
});
|
||||
|
@ -247,6 +247,7 @@ describe('RelatedIssuesBlock', () => {
|
|||
|
||||
it('is expanded by default', () => {
|
||||
expect(findToggleButton().props('icon')).toBe('chevron-lg-up');
|
||||
expect(findToggleButton().props('disabled')).toBe(false);
|
||||
expect(findRelatedIssuesBody().exists()).toBe(true);
|
||||
});
|
||||
|
||||
|
@ -258,4 +259,16 @@ describe('RelatedIssuesBlock', () => {
|
|||
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 = {
|
||||
timelineEvent: {
|
||||
...mockEvents[0],
|
||||
data: {
|
||||
timelineEventCreate: {
|
||||
timelineEvent: {
|
||||
...mockEvents[0],
|
||||
},
|
||||
errors: [],
|
||||
},
|
||||
},
|
||||
errors: [],
|
||||
};
|
||||
|
||||
export const timelineEventsCreateEventError = {
|
||||
|
@ -103,3 +107,21 @@ const timelineEventDeleteData = (errors = []) => {
|
|||
export const timelineEventsDeleteEventResponse = timelineEventDeleteData();
|
||||
|
||||
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 { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import IncidentTimelineEventForm 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 TimelineEventsForm from '~/issues/show/components/incidents/timeline_events_form.vue';
|
||||
import { createAlert } from '~/flash';
|
||||
import { useFakeDate } from 'helpers/fake_date';
|
||||
import { timelineEventsCreateEventResponse, timelineEventsCreateEventError } from './mock_data';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
jest.mock('~/flash');
|
||||
|
||||
const addEventResponse = jest.fn().mockResolvedValue(timelineEventsCreateEventResponse);
|
||||
|
||||
function createMockApolloProvider(response = addEventResponse) {
|
||||
const requestHandlers = [[createTimelineEventMutation, response]];
|
||||
return createMockApollo(requestHandlers);
|
||||
}
|
||||
const fakeDate = '2020-07-08T00:00:00.000Z';
|
||||
|
||||
describe('Timeline events form', () => {
|
||||
// July 8 2020
|
||||
useFakeDate(2020, 6, 8);
|
||||
useFakeDate(fakeDate);
|
||||
let wrapper;
|
||||
|
||||
const mountComponent = ({ mockApollo, mountMethod = shallowMountExtended, stubs }) => {
|
||||
wrapper = mountMethod(IncidentTimelineEventForm, {
|
||||
const mountComponent = ({ mountMethod = shallowMountExtended }) => {
|
||||
wrapper = mountMethod(TimelineEventsForm, {
|
||||
propsData: {
|
||||
hasTimelineEvents: true,
|
||||
isEventProcessed: false,
|
||||
},
|
||||
provide: {
|
||||
fullPath: 'group/project',
|
||||
issuableId: '1',
|
||||
},
|
||||
apolloProvider: mockApollo,
|
||||
stubs,
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
addEventResponse.mockReset();
|
||||
createAlert.mockReset();
|
||||
if (wrapper) {
|
||||
wrapper.destroy();
|
||||
}
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
const findSubmitButton = () => wrapper.findByText('Save');
|
||||
|
@ -75,24 +59,28 @@ describe('Timeline events form', () => {
|
|||
};
|
||||
|
||||
describe('form button behaviour', () => {
|
||||
const closeFormEvent = { 'hide-incident-timeline-event-form': [[]] };
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
expect(wrapper.emitted()).toEqual(closeFormEvent);
|
||||
expect(wrapper.emitted()).toEqual({ cancel: [[]] });
|
||||
});
|
||||
|
||||
it('should clear the form', async () => {
|
||||
|
@ -111,71 +99,4 @@ describe('Timeline events form', () => {
|
|||
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 TimelineEventsTab from '~/issues/show/components/incidents/timeline_events_tab.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 createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { createAlert } from '~/flash';
|
||||
|
@ -53,7 +53,7 @@ describe('TimelineEventsTab', () => {
|
|||
const findLoadingSpinner = () => wrapper.findComponent(GlLoadingIcon);
|
||||
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
|
||||
const findTimelineEventsList = () => wrapper.findComponent(IncidentTimelineEventsList);
|
||||
const findTimelineEventForm = () => wrapper.findComponent(IncidentTimelineEventForm);
|
||||
const findCreateTimelineEvent = () => wrapper.findComponent(CreateTimelineEvent);
|
||||
const findAddEventButton = () => wrapper.findByText(timelineTabI18n.addEventButton);
|
||||
|
||||
describe('Timeline events tab', () => {
|
||||
|
@ -143,18 +143,18 @@ describe('TimelineEventsTab', () => {
|
|||
});
|
||||
|
||||
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 () => {
|
||||
await findAddEventButton().trigger('click');
|
||||
|
||||
expect(findTimelineEventForm().isVisible()).toBe(true);
|
||||
expect(findCreateTimelineEvent().isVisible()).toBe(true);
|
||||
});
|
||||
|
||||
it('should clear the form when button is clicked', async () => {
|
||||
const mockClear = jest.fn();
|
||||
wrapper.vm.$refs.eventForm.clear = mockClear;
|
||||
wrapper.vm.$refs.createEventForm.clearForm = mockClear;
|
||||
|
||||
await findAddEventButton().trigger('click');
|
||||
|
||||
|
@ -165,9 +165,9 @@ describe('TimelineEventsTab', () => {
|
|||
// open the form
|
||||
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
|
||||
|
||||
describe '#random_start_id' do
|
||||
let(:batch_size) { 5 }
|
||||
describe '#min_id' do
|
||||
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
|
||||
create_list(:namespace, 50) # This will also create Ci::NameSpaceMirror objects
|
||||
end
|
||||
|
@ -58,12 +76,11 @@ RSpec.describe Database::ConsistencyCheckService do
|
|||
end
|
||||
|
||||
context 'no cursor has been saved before' do
|
||||
let(:selected_start_id) { Namespace.order(:id).limit(5).pluck(:id).last }
|
||||
let(:expected_next_start_id) { selected_start_id + batch_size * max_batches }
|
||||
let(:min_id) { Namespace.first.id }
|
||||
let(:max_id) { Namespace.last.id }
|
||||
|
||||
before do
|
||||
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
|
||||
|
||||
it 'picks a random start_id' do
|
||||
|
@ -72,17 +89,21 @@ RSpec.describe Database::ConsistencyCheckService do
|
|||
matches: 10,
|
||||
mismatches: 0,
|
||||
mismatches_details: [],
|
||||
start_id: selected_start_id,
|
||||
next_start_id: expected_next_start_id
|
||||
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)
|
||||
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
|
||||
|
||||
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|
|
||||
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,
|
||||
next_start_id: expected_next_start_id,
|
||||
next_start_id: min_id + batch_size,
|
||||
matches: 10,
|
||||
mismatches: 0,
|
||||
mismatches_details: []
|
||||
|
@ -98,17 +119,19 @@ RSpec.describe Database::ConsistencyCheckService do
|
|||
|
||||
expected_result = {
|
||||
batches: 2,
|
||||
start_id: selected_start_id,
|
||||
next_start_id: expected_next_start_id,
|
||||
matches: 10,
|
||||
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
|
||||
|
||||
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
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,7 +6,7 @@ RSpec.describe Git::BranchPushService, services: true do
|
|||
include RepoHelpers
|
||||
|
||||
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(:oldrev) { sample_commit.parent_id }
|
||||
|
|
|
@ -16,7 +16,9 @@ RSpec.shared_examples 'date sidebar widget' do
|
|||
page.within('[data-testid="sidebar-due-date"]') do
|
||||
today = Date.today.day
|
||||
|
||||
click_button 'Edit'
|
||||
button = find_button('Edit')
|
||||
scroll_to(button)
|
||||
button.click
|
||||
|
||||
click_button today.to_s
|
||||
|
||||
|
|
Loading…
Reference in New Issue