Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-08-17 06:12:03 +00:00
parent 493d6f7512
commit a17e8b1687
19 changed files with 534 additions and 295 deletions

View File

@ -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'),
});

View File

@ -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>

View File

@ -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>

View File

@ -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 }}

View File

@ -217,6 +217,7 @@ export default {
size="small"
:icon="toggleIcon"
:aria-label="toggleLabel"
:disabled="!hasRelatedIssues"
data-testid="toggle-links"
@click="handleToggle"
/>

View File

@ -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

View File

@ -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. |

View File

@ -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).

View File

@ -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`

View File

@ -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**.

View File

@ -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).

View File

@ -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);
});
});

View File

@ -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({});
});
});
});

View File

@ -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',
},
],
},
},
};

View File

@ -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);
});
});
});

View File

@ -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);
});
});
});

View File

@ -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

View File

@ -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 }

View File

@ -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