Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-07-13 18:08:57 +00:00
parent e1189e4c3b
commit e9606d7f51
47 changed files with 1069 additions and 174 deletions

View file

@ -0,0 +1,95 @@
<script>
import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import { PROJECTS_PER_PAGE } from '../constants';
import getProjectsQuery from '../graphql/queries/get_projects.query.graphql';
export default {
PROJECTS_PER_PAGE,
projectQueryPageInfo: {
endCursor: '',
},
components: {
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
GlLoadingIcon,
},
props: {
selectedProject: {
type: Object,
required: false,
default: null,
},
},
data() {
return {
initialProjectsLoading: true,
projectSearchQuery: '',
};
},
apollo: {
projects: {
query: getProjectsQuery,
variables() {
return {
search: this.projectSearchQuery,
first: this.$options.PROJECTS_PER_PAGE,
after: this.$options.projectQueryPageInfo.endCursor,
searchNamespaces: true,
sort: 'similarity',
};
},
update(data) {
return data?.projects?.nodes.filter((project) => !project.repository.empty) ?? [];
},
result() {
this.initialProjectsLoading = false;
},
error() {
this.onError({ message: __('Failed to load projects') });
},
},
},
computed: {
projectsLoading() {
return Boolean(this.$apollo.queries.projects.loading);
},
projectDropdownText() {
return this.selectedProject?.nameWithNamespace || __('Select a project');
},
},
methods: {
async onProjectSelect(project) {
this.$emit('change', project);
},
onError({ message } = {}) {
this.$emit('error', { message });
},
isProjectSelected(project) {
return project.id === this.selectedProject?.id;
},
},
};
</script>
<template>
<gl-dropdown :text="projectDropdownText" :loading="initialProjectsLoading">
<template #header>
<gl-search-box-by-type v-model.trim="projectSearchQuery" :debounce="250" />
</template>
<gl-loading-icon v-show="projectsLoading" />
<template v-if="!projectsLoading">
<gl-dropdown-item
v-for="project in projects"
:key="project.id"
is-check-item
:is-checked="isProjectSelected(project)"
@click="onProjectSelect(project)"
>
{{ project.nameWithNamespace }}
</gl-dropdown-item>
</template>
</gl-dropdown>
</template>

View file

@ -0,0 +1,134 @@
<script>
import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import { BRANCHES_PER_PAGE } from '../constants';
import getProjectQuery from '../graphql/queries/get_project.query.graphql';
export default {
BRANCHES_PER_PAGE,
components: {
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
GlLoadingIcon,
},
props: {
selectedProject: {
type: Object,
required: false,
default: null,
},
selectedBranchName: {
type: String,
required: false,
default: null,
},
},
data() {
return {
sourceBranchSearchQuery: '',
initialSourceBranchNamesLoading: false,
sourceBranchNamesLoading: false,
sourceBranchNames: [],
};
},
computed: {
hasSelectedProject() {
return Boolean(this.selectedProject);
},
hasSelectedSourceBranch() {
return Boolean(this.selectedBranchName);
},
branchDropdownText() {
return this.selectedBranchName || __('Select a branch');
},
},
watch: {
selectedProject: {
immediate: true,
async handler(selectedProject) {
if (!selectedProject) return;
this.initialSourceBranchNamesLoading = true;
await this.fetchSourceBranchNames({ projectPath: selectedProject.fullPath });
this.initialSourceBranchNamesLoading = false;
},
},
},
methods: {
onSourceBranchSelect(branchName) {
this.$emit('change', branchName);
},
onSourceBranchSearchQuery(branchSearchQuery) {
this.branchSearchQuery = branchSearchQuery;
this.fetchSourceBranchNames({
projectPath: this.selectedProject.fullPath,
searchPattern: this.branchSearchQuery,
});
},
onError({ message } = {}) {
this.$emit('error', { message });
},
async fetchSourceBranchNames({ projectPath, searchPattern } = {}) {
this.sourceBranchNamesLoading = true;
try {
const { data } = await this.$apollo.query({
query: getProjectQuery,
variables: {
projectPath,
branchNamesLimit: this.$options.BRANCHES_PER_PAGE,
branchNamesOffset: 0,
branchNamesSearchPattern: searchPattern ? `*${searchPattern}*` : '*',
},
});
const { branchNames, rootRef } = data?.project.repository || {};
this.sourceBranchNames = branchNames || [];
// Use root ref as the default selection
if (rootRef && !this.hasSelectedSourceBranch) {
this.onSourceBranchSelect(rootRef);
}
} catch (err) {
this.onError({
message: __('Something went wrong while fetching source branches.'),
});
} finally {
this.sourceBranchNamesLoading = false;
}
},
},
};
</script>
<template>
<gl-dropdown
:text="branchDropdownText"
:loading="initialSourceBranchNamesLoading"
:disabled="!hasSelectedProject"
:class="{ 'gl-font-monospace': hasSelectedSourceBranch }"
>
<template #header>
<gl-search-box-by-type
:debounce="250"
:value="sourceBranchSearchQuery"
@input="onSourceBranchSearchQuery"
/>
</template>
<gl-loading-icon v-show="sourceBranchNamesLoading" />
<template v-if="!sourceBranchNamesLoading">
<gl-dropdown-item
v-for="branchName in sourceBranchNames"
v-show="!sourceBranchNamesLoading"
:key="branchName"
:is-checked="branchName === selectedBranchName"
is-check-item
class="gl-font-monospace"
@click="onSourceBranchSelect(branchName)"
>
{{ branchName }}
</gl-dropdown-item>
</template>
</gl-dropdown>
</template>

View file

@ -0,0 +1,2 @@
export const BRANCHES_PER_PAGE = 20;
export const PROJECTS_PER_PAGE = 20;

View file

@ -0,0 +1,17 @@
query getProject(
$projectPath: ID!
$branchNamesLimit: Int!
$branchNamesOffset: Int!
$branchNamesSearchPattern: String!
) {
project(fullPath: $projectPath) {
repository {
branchNames(
limit: $branchNamesLimit
offset: $branchNamesOffset
searchPattern: $branchNamesSearchPattern
)
rootRef
}
}
}

View file

@ -0,0 +1,34 @@
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
query getProjects(
$search: String!
$after: String = ""
$first: Int!
$searchNamespaces: Boolean = false
$sort: String
$membership: Boolean = true
) {
projects(
search: $search
after: $after
first: $first
membership: $membership
searchNamespaces: $searchNamespaces
sort: $sort
) {
nodes {
id
name
nameWithNamespace
fullPath
avatarUrl
path
repository {
empty
}
}
pageInfo {
...PageInfo
}
}
}

View file

@ -41,6 +41,5 @@ class Projects::MattermostsController < Projects::ApplicationController
def integration
@integration ||= @project.find_or_initialize_integration('mattermost_slash_commands')
@service = @integration # TODO: remove when https://gitlab.com/gitlab-org/gitlab/-/issues/330300 is complete
end
end

View file

@ -16,6 +16,5 @@ class Projects::ServiceHookLogsController < Projects::HookLogsController
def integration
@integration ||= @project.find_or_initialize_integration(params[:service_id])
@service = @integration # TODO: remove when https://gitlab.com/gitlab-org/gitlab/-/issues/330300 is complete
end
end

View file

@ -112,7 +112,7 @@ class Projects::ServicesController < Projects::ApplicationController
return if !integration.is_a?(::Integrations::Prometheus) || !Feature.enabled?(:settings_operations_prometheus_service, project)
operations_link_start = "<a href=\"#{project_settings_operations_path(project)}\">"
message = s_('PrometheusService|You can now manage your Prometheus settings on the %{operations_link_start}Operations%{operations_link_end} page. Fields on this page has been deprecated.') % { operations_link_start: operations_link_start, operations_link_end: "</a>" }
message = s_('PrometheusService|You can now manage your Prometheus settings on the %{operations_link_start}Operations%{operations_link_end} page. Fields on this page have been deprecated.') % { operations_link_start: operations_link_start, operations_link_end: "</a>" }
flash.now[:alert] = message.html_safe
end
end

View file

@ -2,15 +2,21 @@
module SidebarsHelper
def sidebar_tracking_attributes_by_object(object)
case object
when Project
sidebar_project_tracking_attrs
when Group
sidebar_group_tracking_attrs
when User
sidebar_user_profile_tracking_attrs
else
{}
sidebar_attributes_for_object(object).fetch(:tracking_attrs, {})
end
def sidebar_qa_selector(object)
sidebar_attributes_for_object(object).fetch(:sidebar_qa_selector, nil)
end
def scope_qa_menu_item(object)
sidebar_attributes_for_object(object).fetch(:scope_qa_menu_item, nil)
end
def scope_avatar_classes(object)
%w[avatar-container rect-avatar s32].tap do |klasses|
klass = sidebar_attributes_for_object(object).fetch(:scope_avatar_class, nil)
klasses << klass if klass
end
end
@ -22,6 +28,43 @@ module SidebarsHelper
private
def sidebar_attributes_for_object(object)
case object
when Project
sidebar_project_attributes
when Group
sidebar_group_attributes
when User
sidebar_user_attributes
else
{}
end
end
def sidebar_project_attributes
{
tracking_attrs: sidebar_project_tracking_attrs,
sidebar_qa_selector: 'project_sidebar',
scope_qa_menu_item: 'Project scope',
scope_avatar_class: 'project_avatar'
}
end
def sidebar_group_attributes
{
tracking_attrs: sidebar_group_tracking_attrs,
sidebar_qa_selector: 'group_sidebar',
scope_qa_menu_item: 'Group scope',
scope_avatar_class: 'group_avatar'
}
end
def sidebar_user_attributes
{
tracking_attrs: sidebar_user_profile_tracking_attrs
}
end
def sidebar_project_tracking_attrs
tracking_attrs('projects_side_navigation', 'render', 'projects_side_navigation')
end

View file

@ -15,4 +15,4 @@
and try again.
%hr
.clearfix
= link_to 'Go back', edit_project_service_path(@project, @service), class: 'gl-button btn btn-lg float-right'
= link_to 'Go back', edit_project_service_path(@project, @integration), class: 'gl-button btn btn-lg float-right'

View file

@ -2,7 +2,7 @@
This service will be installed on the Mattermost instance at
%strong= link_to Gitlab.config.mattermost.host, Gitlab.config.mattermost.host
%hr
= form_for(:mattermost, method: :post, url: project_mattermost_path(@project), html: { class: 'js-requires-input'} ) do |f|
= form_for(:mattermost, method: :post, url: project_mattermost_path(@project), html: { class: 'js-requires-input' }) do |f|
%h4 Team
%p
= @teams.one? ? 'The team' : 'Select the team'
@ -42,5 +42,5 @@
%hr
.clearfix
.float-right
= link_to 'Cancel', edit_project_service_path(@project, @service), class: 'gl-button btn btn-lg'
= link_to 'Cancel', edit_project_service_path(@project, @integration), class: 'gl-button btn btn-lg'
= f.submit 'Install', class: 'gl-button btn btn-success btn-lg'

View file

@ -1,6 +1,6 @@
= nav_link(**scope_menu.active_routes, html_options: scope_menu.nav_link_html_options) do
= link_to scope_menu.link, **scope_menu.container_html_options, data: { qa_selector: 'project_scope_link' } do
%span{ class: ['avatar-container', 'rect-avatar', 'project-avatar', 's32'] }
= link_to scope_menu.link, **scope_menu.container_html_options, data: { qa_selector: 'sidebar_menu_link', qa_menu_item: scope_qa_menu_item(scope_menu.container) } do
%span{ class: scope_avatar_classes(scope_menu.container) }
= source_icon(scope_menu.container, alt: scope_menu.title, class: ['avatar', 'avatar-tile', 's32'], width: 32, height: 32)
%span.sidebar-context-title
= scope_menu.title

View file

@ -3,7 +3,7 @@
- if sidebar.render_raw_scope_menu_partial
= render sidebar.render_raw_scope_menu_partial
%ul.sidebar-top-level-items.qa-project-sidebar
%ul.sidebar-top-level-items{ data: { qa_selector: sidebar_qa_selector(sidebar.container) } }
- if sidebar.scope_menu
= render partial: 'shared/nav/scope_menu', object: sidebar.scope_menu
- if sidebar.renderable_menus.any?

View file

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/325737
milestone: '13.11'
type: development
group: group::runner
default_enabled: false
default_enabled: true

View file

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/330969
milestone: '13.12'
type: development
group: group::runner
default_enabled: false
default_enabled: true

View file

@ -4,4 +4,3 @@ filenames:
- ee/app/assets/javascripts/security_configuration/api_fuzzing/graphql/api_fuzzing_ci_configuration.query.graphql
- ee/app/assets/javascripts/security_configuration/api_fuzzing/graphql/create_api_fuzzing_configuration.mutation.graphql
- ee/app/assets/javascripts/security_configuration/dast_profiles/graphql/dast_failed_site_validations.query.graphql
- ee/app/assets/javascripts/security_configuration/graphql/configure_dependency_scanning.mutation.graphql

View file

@ -0,0 +1,40 @@
# frozen_string_literal: true
class AddPremiumAndUltimatePlanLimits < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
class Plan < ActiveRecord::Base
self.inheritance_column = :_type_disabled
has_one :limits, class_name: 'PlanLimits'
end
class PlanLimits < ActiveRecord::Base
self.inheritance_column = :_type_disabled
belongs_to :plan
end
def copy_plan_limits(from_plan_name:, to_plan_name:)
source_plan = Plan.find_by(name: from_plan_name)
target_plan = Plan.find_by(name: to_plan_name)
return unless source_plan && target_plan
return unless source_plan.limits.present?
return if target_plan.limits.present?
limits = source_plan.limits.dup
limits.plan = target_plan
limits.save!
end
def up
return unless Gitlab.com?
copy_plan_limits(from_plan_name: 'gold', to_plan_name: 'ultimate')
copy_plan_limits(from_plan_name: 'silver', to_plan_name: 'premium')
end
def down
# no-op
end
end

View file

@ -0,0 +1,17 @@
# frozen_string_literal: true
class IndexBatchedMigrationJobsByMaxValue < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
INDEX_NAME = 'index_migration_jobs_on_migration_id_and_max_value'
def up
add_concurrent_index :batched_background_migration_jobs, %i(batched_background_migration_id max_value), name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :batched_background_migration_jobs, INDEX_NAME
end
end

View file

@ -0,0 +1 @@
150463cef309e6bf69240c258dc8aede53b846a08a7e2d668ee0429709022554

View file

@ -0,0 +1 @@
4216604d14b4ccc652ba423a95ee9bd15646b3553903dc4b79497871f5384492

View file

@ -24100,6 +24100,8 @@ CREATE INDEX index_metrics_users_starred_dashboards_on_project_id ON metrics_use
CREATE INDEX index_migration_jobs_on_migration_id_and_finished_at ON batched_background_migration_jobs USING btree (batched_background_migration_id, finished_at);
CREATE INDEX index_migration_jobs_on_migration_id_and_max_value ON batched_background_migration_jobs USING btree (batched_background_migration_id, max_value);
CREATE INDEX index_milestone_releases_on_release_id ON milestone_releases USING btree (release_id);
CREATE INDEX index_milestones_on_description_trigram ON milestones USING gin (description gin_trgm_ops);

View file

@ -492,7 +492,7 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab
response. They have `rel` set to `prev`, `next`, `first`, or `last` and contain
the relevant URL. Be sure to use these links instead of generating your own URLs.
For GitLab SaaS users, [some pagination headers may not be returned](../user/gitlab_com/index.md#pagination-response-headers).
For GitLab.com users, [some pagination headers may not be returned](../user/gitlab_com/index.md#pagination-response-headers).
In the following cURL example, we limit the output to three items per page
(`per_page=3`) and we request the second page (`page=2`) of [comments](notes.md)
@ -836,7 +836,7 @@ languages. For a complete list, visit the [GitLab website](https://about.gitlab.
For administrator documentation on rate limit settings, see
[Rate limits](../security/rate_limits.md). To find the settings that are
specifically used by GitLab.com, see
[GitLab SaaS-specific rate limits](../user/gitlab_com/index.md#gitlab-saas-specific-rate-limits).
[GitLab.com-specific rate limits](../user/gitlab_com/index.md#gitlabcom-specific-rate-limits).
## Content type

View file

@ -90,7 +90,7 @@ To protect or unprotect a runner:
1. Check the **Protected** option.
1. Click **Save changes**.
![specific runners edit icon](img/protected_runners_check_box.png)
![specific runners edit icon](img/protected_runners_check_box_v14_1.png)
### Forks
@ -146,7 +146,7 @@ the GitLab instance. To determine this:
1. On the left sidebar, select **Overview > Runners**.
1. Find the runner in the table and view the **IP Address** column.
![shared runner IP address](img/shared_runner_ip_address.png)
![shared runner IP address](img/shared_runner_ip_address_14_1.png)
### Determine the IP address of a specific runner

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

View file

@ -48,7 +48,11 @@ It's recommended to create two separate migration script files.
create_or_update_plan_limit('project_hooks', 'free', 10)
create_or_update_plan_limit('project_hooks', 'bronze', 20)
create_or_update_plan_limit('project_hooks', 'silver', 30)
create_or_update_plan_limit('project_hooks', 'premium', 30)
create_or_update_plan_limit('project_hooks', 'premium_trial', 30)
create_or_update_plan_limit('project_hooks', 'gold', 100)
create_or_update_plan_limit('project_hooks', 'ultimate', 100)
create_or_update_plan_limit('project_hooks', 'ultimate_trial', 100)
end
def down
@ -56,7 +60,11 @@ It's recommended to create two separate migration script files.
create_or_update_plan_limit('project_hooks', 'free', 0)
create_or_update_plan_limit('project_hooks', 'bronze', 0)
create_or_update_plan_limit('project_hooks', 'silver', 0)
create_or_update_plan_limit('project_hooks', 'premium', 0)
create_or_update_plan_limit('project_hooks', 'premium_trial', 0)
create_or_update_plan_limit('project_hooks', 'gold', 0)
create_or_update_plan_limit('project_hooks', 'ultimate', 0)
create_or_update_plan_limit('project_hooks', 'ultimate_trial', 0)
end
end
```
@ -145,6 +153,10 @@ GitLab.com:
- `free`: Namespaces and projects with a Free subscription.
- `bronze`: Namespaces and projects with a Bronze subscription. This tier is no longer available for purchase.
- `silver`: Namespaces and projects with a Premium subscription.
- `premium`: Namespaces and projects with a Premium subscription.
- `premium_trial`: Namespaces and projects with a Premium Trial subscription.
- `gold`: Namespaces and projects with an Ultimate subscription.
- `ultimate`: Namespaces and projects with an Ultimate subscription.
- `ultimate_trial`: Namespaces and projects with an Ultimate Trial subscription.
The `test` environment doesn't have any plans.

View file

@ -82,6 +82,19 @@ When possible, use present tense instead. For example, use `after you execute th
Do not make possessive (GitLab's). This guidance follows [GitLab Brand Guidelines](https://about.gitlab.com/handbook/marketing/corporate-marketing/brand-activation/brand-guidelines/#trademark).
### GitLab.com
Refers to the GitLab instance managed by GitLab itself.
### GitLab SaaS
Refers to the product license that provides access to GitLab.com. Does not refer to the
GitLab instance managed by GitLab itself.
### GitLab self-managed
Refers to the product license for GitLab instances managed by customers themselves.
## Guest
When writing about the Guest role:
@ -187,9 +200,16 @@ Do not use. Use **check for completeness** instead. ([Vale](../testing.md#vale)
Do not use when talking about increasing GitLab performance for additional users. The words scale or scaling are sometimes acceptable, but references to increasing GitLab performance for additional users should direct readers to the GitLab [reference architectures](../../../administration/reference_architectures/index.md) page.
## simply
## setup, set up
Do not use. If the user doesn't find the process to be these things, we lose their trust.
Use **setup** as a noun, and **set up** as a verb. Examples:
- `Your remote office setup is amazing.`
- `To set up your remote office correctly, first consider the ergonomics of your work area.`
## simply, simple
Do not use. If the user doesn't find the process to be simple, we lose their trust.
## slashes

View file

@ -230,7 +230,7 @@ in your local development environment.
#### File size limits
Files uploaded to the GitLab Package Registry are [limited by format](../administration/instance_limits.md#package-registry-limits).
On GitLab SaaS, these are typically set to 5GB to help prevent timeout issues and abuse.
On GitLab.com, these are typically set to 5GB to help prevent timeout issues and abuse.
When a new package type is added to the `Packages::Package` model, a size limit must be added
similar to [this example](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52639/diffs#382f879fb09b0212e3cedd99e6c46e2083867216),
@ -238,10 +238,10 @@ or the [related test](https://gitlab.com/gitlab-org/gitlab/-/blob/fe4ba437667813
must be updated if file size limits do not apply. The only reason a size limit does not apply is if
the package format does not upload and store package files.
#### Rate Limits on GitLab SaaS
#### Rate Limits on GitLab.com
Package manager clients can make rapid requests that exceed the
[GitLab SaaS standard API rate limits](../user/gitlab_com/index.md#gitlab-saas-specific-rate-limits).
[GitLab.com standard API rate limits](../user/gitlab_com/index.md#gitlabcom-specific-rate-limits).
This results in a `429 Too Many Requests` error.
We have opened a set of paths to allow higher rate limits. Unless it is not possible,

View file

@ -9,7 +9,7 @@ type: reference, howto
NOTE:
For GitLab.com, please see
[GitLab SaaS-specific rate limits](../user/gitlab_com/index.md#gitlab-saas-specific-rate-limits).
[GitLab.com-specific rate limits](../user/gitlab_com/index.md#gitlabcom-specific-rate-limits).
Rate limiting is a common technique used to improve the security and durability
of a web application.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -276,21 +276,22 @@ To search runners' descriptions:
You can also filter runners by status, type, and tag. To filter:
1. Click in the **Search or filter results...** field.
1. Select **status:**, **type:**, or **tag:**.
1. Select **Status**, **Type**, or **Tags**.
1. Select or enter your search criteria.
![Attributes of a runner, with the **Search or filter results...** field active](img/index_runners_search_or_filter.png)
![Attributes of a runner, with the **Search or filter results...** field active](img/index_runners_search_or_filter_v14_1.png)
For each runner, the following attributes are listed:
| Attribute | Description |
|--------------|-------------|
| Type | One or more of the following states: shared, group, specific, locked, or paused |
| Runner token | Token used to identify the runner, and which the runner uses to communicate with the GitLab instance |
| Description | Description given to the runner when it was created |
| Type/State | One or more of the following states: shared, group, specific, locked, or paused |
| Runner token | Partial token used to identify the runner, and which the runner uses to communicate with the GitLab instance |
| Runner ID | Numerical ID of the runner |
| Description | Description given to the runner |
| Version | GitLab Runner version |
| IP address | IP address of the host on which the runner is registered |
| Projects | Projects to which the runner is assigned |
| Projects | Number 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 runner last contacted the GitLab instance |

View file

@ -4,15 +4,15 @@ group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# GitLab SaaS settings **(FREE SAAS)**
# GitLab.com settings **(FREE SAAS)**
This page contains information about the settings that are used on
[GitLab SaaS](https://about.gitlab.com/pricing/).
This page contains information about the settings that are used on GitLab.com, available to
[GitLab SaaS](https://about.gitlab.com/pricing/) customers.
## SSH host keys fingerprints
Below are the fingerprints for GitLab SaaS's SSH host keys. The first time you
connect to a GitLab SaaS repository, one of these keys is displayed in the output.
Below are the fingerprints for SSH host keys on GitLab.com. The first time you
connect to a GitLab.com repository, one of these keys is displayed in the output.
| Algorithm | MD5 (deprecated) | SHA256 |
|------------------|------------------|---------|
@ -34,14 +34,14 @@ gitlab.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAA
## Mail configuration
GitLab SaaS sends emails from the `mg.gitlab.com` domain by using [Mailgun](https://www.mailgun.com/),
GitLab.com sends emails from the `mg.gitlab.com` domain by using [Mailgun](https://www.mailgun.com/),
and has its own dedicated IP address (`192.237.158.143`).
The IP address for `mg.gitlab.com` is subject to change at any time.
### Service Desk custom mailbox
On GitLab SaaS, there's a mailbox configured for Service Desk with the email address:
On GitLab.com, there's a mailbox configured for Service Desk with the email address:
`contact-project+%{key}@incoming.gitlab.com`. To use this mailbox, configure the
[custom suffix](../project/service_desk.md#configuring-a-custom-email-address-suffix) in project
settings.
@ -50,7 +50,7 @@ settings.
[See our backup strategy](https://about.gitlab.com/handbook/engineering/infrastructure/production/#backups).
To back up an entire project on GitLab SaaS, you can export it either:
To back up an entire project on GitLab.com, you can export it either:
- [Through the UI](../project/settings/import_export.md).
- [Through the API](../../api/project_import_export.md#schedule-an-export). You
@ -69,7 +69,7 @@ are included when cloning.
## Alternative SSH port
GitLab SaaS can be reached by using a [different SSH port](https://about.gitlab.com/blog/2016/02/18/gitlab-dot-com-now-supports-an-alternate-git-plus-ssh-port/) for `git+ssh`.
GitLab.com can be reached by using a [different SSH port](https://about.gitlab.com/blog/2016/02/18/gitlab-dot-com-now-supports-an-alternate-git-plus-ssh-port/) for `git+ssh`.
| Setting | Value |
|------------|---------------------|
@ -91,7 +91,7 @@ Host gitlab.com
Below are the settings for [GitLab Pages](https://about.gitlab.com/stages-devops-lifecycle/pages/).
| Setting | GitLab SaaS | Default |
| Setting | GitLab.com | Default |
|---------------------------|------------------------|------------------------|
| Domain name | `gitlab.io` | - |
| IP address | `35.185.44.232` | - |
@ -108,7 +108,7 @@ Below are the current settings regarding [GitLab CI/CD](../../ci/index.md).
Any settings or feature limits not listed here are using the defaults listed in
the related documentation.
| Setting | GitLab SaaS | Default |
| Setting | GitLab.com | Default |
|-------------------------------------|-------------|---------|
| Artifacts maximum size (compressed) | 1 GB | 100 MB |
| Artifacts [expiry time](../../ci/yaml/index.md#artifactsexpire_in) | From June 22, 2020, deleted after 30 days unless otherwise specified (artifacts created before that date have no expiry). | deleted after 30 days unless otherwise specified |
@ -122,13 +122,13 @@ the related documentation.
## Account and limit settings
GitLab SaaS has the following [account limits](../admin_area/settings/account_and_limit_settings.md)
GitLab.com has the following [account limits](../admin_area/settings/account_and_limit_settings.md)
enabled. If a setting is not listed, it is set to the default value.
If you are near or over the repository size limit, you can either
[reduce your repository size with Git](../project/repository/reducing_the_repo_size_using_git.md) or [purchase additional storage](https://about.gitlab.com/pricing/licensing-faq/#can-i-buy-more-storage).
| Setting | GitLab SaaS | Default |
| Setting | GitLab.com | Default |
|-------------------------------|-------------|---------|
| [Repository size including LFS](../admin_area/settings/account_and_limit_settings.md#repository-size-limit) | 10 GB | Unlimited |
| Maximum import size | 5 GB | Unlimited ([Modified](https://gitlab.com/gitlab-org/gitlab/-/issues/251106) from 50MB to unlimited in GitLab 13.8.) |
@ -141,11 +141,11 @@ this limit.
## IP range
GitLab SaaS uses the IP ranges `34.74.90.64/28` and `34.74.226.0/24` for traffic from its Web/API
GitLab.com uses the IP ranges `34.74.90.64/28` and `34.74.226.0/24` for traffic from its Web/API
fleet. This whole range is solely allocated to GitLab. You can expect connections from webhooks or repository mirroring to come
from those IPs and allow them.
GitLab SaaS is fronted by Cloudflare. For incoming connections to GitLab SaaS, you might need to allow CIDR blocks of Cloudflare ([IPv4](https://www.cloudflare.com/ips-v4) and [IPv6](https://www.cloudflare.com/ips-v6)).
GitLab.com is fronted by Cloudflare. For incoming connections to GitLab.com, you might need to allow CIDR blocks of Cloudflare ([IPv4](https://www.cloudflare.com/ips-v4) and [IPv6](https://www.cloudflare.com/ips-v6)).
For outgoing connections from CI/CD runners, we are not providing static IP
addresses. All GitLab runners are deployed into Google Cloud Platform (GCP). Any
@ -156,7 +156,7 @@ IP-based firewall can be configured by looking up all
Add these hostnames when you configure allow-lists in local HTTP(S) proxies,
or other web-blocking software that governs end-user computers. Pages on
GitLab SaaS load content from these hostnames:
GitLab.com load content from these hostnames:
- `gitlab.com`
- `*.gitlab.com`
@ -171,7 +171,7 @@ also load certain page content directly from common public CDN hostnames.
The following limits apply for [Webhooks](../project/integrations/webhooks.md):
| Setting | GitLab SaaS | Default |
| Setting | GitLab.com | Default |
|----------------------|-------------|---------|
| [Webhook rate limit](../../administration/instance_limits.md#webhook-rate-limit) | `120` calls per minute for GitLab Free, unlimited for GitLab Premium and GitLab Ultimate | Unlimited |
| [Number of webhooks](../../administration/instance_limits.md#number-of-webhooks) | `100` per project, `50` per group | `100` per project, `50` per group |
@ -179,16 +179,16 @@ The following limits apply for [Webhooks](../project/integrations/webhooks.md):
## Shared runners
GitLab has shared runners on GitLab SaaS that you can use to run your CI jobs.
GitLab has shared runners on GitLab.com that you can use to run your CI jobs.
For more information, see [choosing a runner](../../ci/runners/index.md).
## Sidekiq
GitLab SaaS runs [Sidekiq](https://sidekiq.org) with arguments `--timeout=4 --concurrency=4`
GitLab.com runs [Sidekiq](https://sidekiq.org) with arguments `--timeout=4 --concurrency=4`
and the following environment variables:
| Setting | GitLab SaaS | Default |
| Setting | GitLab.com | Default |
|----------------------------------------|-------------|-----------|
| `SIDEKIQ_DAEMON_MEMORY_KILLER` | - | `1` |
| `SIDEKIQ_MEMORY_KILLER_MAX_RSS` | `2000000` | `2000000` |
@ -204,14 +204,14 @@ nodes and Sidekiq export nodes.
## PostgreSQL
GitLab SaaS being a fairly large installation of GitLab means we have changed
GitLab.com being a fairly large installation of GitLab means we have changed
various PostgreSQL settings to better suit our needs. For example, we use
streaming replication and servers in hot-standby mode to balance queries across
different database servers.
The list of GitLab SaaS specific settings (and their defaults) is as follows:
The list of GitLab.com specific settings (and their defaults) is as follows:
| Setting | GitLab SaaS | Default |
| Setting | GitLab.com | Default |
|:--------------------------------------|:--------------------------------------------------------------------|:--------------------------------------|
| `archive_command` | `/usr/bin/envdir /etc/wal-e.d/env /opt/wal-e/bin/wal-e wal-push %p` | empty |
| `archive_mode` | on | off |
@ -249,9 +249,9 @@ for `shared_buffers` is quite high, and we are
## Puma
GitLab SaaS uses the default of 60 seconds for [Puma request timeouts](https://docs.gitlab.com/omnibus/settings/puma.html#worker-timeout).
GitLab.com uses the default of 60 seconds for [Puma request timeouts](https://docs.gitlab.com/omnibus/settings/puma.html#worker-timeout).
## GitLab SaaS-specific rate limits
## GitLab.com-specific rate limits
NOTE:
See [Rate limits](../../security/rate_limits.md) for administrator
@ -262,7 +262,7 @@ code. The client should wait before attempting the request again. There
are also informational headers with this response detailed in [rate
limiting responses](#rate-limiting-responses).
The following table describes the rate limits for GitLab SaaS, both before and
The following table describes the rate limits for GitLab.com, both before and
after the limits change in January, 2021:
| Rate limit | Before 2021-01-18 | From 2021-01-18 | From 2021-02-12 |
@ -289,7 +289,7 @@ For information on rate limiting responses, see:
### Protected paths throttle
GitLab SaaS responds with HTTP status code `429` to POST requests at protected
GitLab.com responds with HTTP status code `429` to POST requests at protected
paths that exceed 10 requests per **minute** per IP address.
See the source below for which paths are protected. This includes user creation,
@ -302,20 +302,20 @@ See [Protected Paths](../admin_area/settings/protected_paths.md) for more detail
### IP blocks
IP blocks can occur when GitLab SaaS receives unusual traffic from a single
IP blocks can occur when GitLab.com receives unusual traffic from a single
IP address that the system views as potentially malicious. This can be based on
rate limit settings. After the unusual traffic ceases, the IP address is
automatically released depending on the type of block, as described in a
following section.
If you receive a `403 Forbidden` error for all requests to GitLab SaaS,
If you receive a `403 Forbidden` error for all requests to GitLab.com,
check for any automated processes that may be triggering a block. For
assistance, contact [GitLab Support](https://support.gitlab.com/hc/en-us)
with details, such as the affected IP address.
#### Git and container registry failed authentication ban
GitLab SaaS responds with HTTP status code `403` for 1 hour, if 30 failed
GitLab.com responds with HTTP status code `403` for 1 hour, if 30 failed
authentication requests were received in a 3-minute period from a single IP address.
This applies only to Git requests and container registry (`/jwt/auth`) requests
@ -343,7 +343,7 @@ doesn't return the following headers:
If created before GitLab 12.2 (July 2019), these items have the
[Internal visibility](../../public_access/public_access.md#internal-projects)
setting [disabled on GitLab SaaS](https://gitlab.com/gitlab-org/gitlab/-/issues/12388):
setting [disabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/12388):
- Projects
- Groups
@ -351,7 +351,7 @@ setting [disabled on GitLab SaaS](https://gitlab.com/gitlab-org/gitlab/-/issues/
### SSH maximum number of connections
GitLab SaaS defines the maximum number of concurrent, unauthenticated SSH
GitLab.com defines the maximum number of concurrent, unauthenticated SSH
connections by using the [MaxStartups setting](http://man.openbsd.org/sshd_config.5#MaxStartups).
If more than the maximum number of allowed connections occur concurrently, they
are dropped and users get
@ -367,9 +367,9 @@ for details.
See [non-configurable limits](../../security/rate_limits.md#non-configurable-limits)
for information on rate limits that are not configurable, and therefore also
used on GitLab SaaS.
used on GitLab.com.
## GitLab SaaS logging
## GitLab.com logging
We use [Fluentd](https://gitlab.com/gitlab-com/runbooks/tree/master/logging/doc#fluentd)
to parse our logs. Fluentd sends our logs to
@ -387,13 +387,13 @@ You can view more information in our runbooks such as:
### Job logs
By default, GitLab does not expire job logs. Job logs are retained indefinitely,
and can't be configured on GitLab SaaS to expire. You can erase job logs
and can't be configured on GitLab.com to expire. You can erase job logs
[manually with the Jobs API](../../api/jobs.md#erase-a-job) or by
[deleting a pipeline](../../ci/pipelines/index.md#delete-a-pipeline).
## GitLab SaaS at scale
## GitLab.com at scale
In addition to the GitLab Enterprise Edition Omnibus install, GitLab SaaS uses
In addition to the GitLab Enterprise Edition Omnibus install, GitLab.com uses
the following applications and settings to achieve scale. All settings are
publicly available at [chef cookbooks](https://gitlab.com/gitlab-cookbooks).

View file

@ -69,7 +69,7 @@ module Gitlab
# In this case, we just lower the batch size so that future calls to this
# method could eventually split the job if it continues to fail.
if midpoint >= max_value
update!(batch_size: new_batch_size, status: :pending)
update!(batch_size: new_batch_size, attempts: 0)
else
old_max_value = max_value
@ -77,7 +77,6 @@ module Gitlab
batch_size: new_batch_size,
max_value: midpoint,
attempts: 0,
status: :pending,
started_at: nil,
finished_at: nil,
metrics: {}

View file

@ -10,7 +10,7 @@ module Gitlab
self.table_name = :batched_background_migrations
has_many :batched_jobs, foreign_key: :batched_background_migration_id
has_one :last_job, -> { order(id: :desc) },
has_one :last_job, -> { order(max_value: :desc) },
class_name: 'Gitlab::Database::BackgroundMigration::BatchedJob',
foreign_key: :batched_background_migration_id

View file

@ -26414,7 +26414,7 @@ msgstr ""
msgid "PrometheusService|Waiting for your first deployment to an environment to find common metrics"
msgstr ""
msgid "PrometheusService|You can now manage your Prometheus settings on the %{operations_link_start}Operations%{operations_link_end} page. Fields on this page has been deprecated."
msgid "PrometheusService|You can now manage your Prometheus settings on the %{operations_link_start}Operations%{operations_link_end} page. Fields on this page have been deprecated."
msgstr ""
msgid "PrometheusService|You have a cluster with the Prometheus integration enabled."
@ -30603,6 +30603,9 @@ msgstr ""
msgid "Something went wrong while fetching requirements list."
msgstr ""
msgid "Something went wrong while fetching source branches."
msgstr ""
msgid "Something went wrong while fetching the environments for this merge request. Please try again."
msgstr ""

View file

@ -12,17 +12,13 @@ module QA
base.class_eval do
include QA::Page::Project::SubMenus::Common
view 'app/views/shared/nav/_scope_menu.html.haml' do
element :project_scope_link
end
end
end
def click_project
retry_on_exception do
within_sidebar do
click_element(:project_scope_link)
click_element(:sidebar_menu_link, menu_item: 'Project scope')
end
end
end

View file

@ -8,9 +8,10 @@ RSpec.describe Projects::ServicesController do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let_it_be(:service) { create(:jira_integration, project: project) }
let_it_be(:jira_integration) { create(:jira_integration, project: project) }
let(:service_params) { { username: 'username', password: 'password', url: 'http://example.com' } }
let(:integration) { jira_integration }
let(:integration_params) { { username: 'username', password: 'password', url: 'http://example.com' } }
before do
sign_in(user)
@ -29,10 +30,10 @@ RSpec.describe Projects::ServicesController do
end
context 'when validations fail' do
let(:service_params) { { active: 'true', url: '' } }
let(:integration_params) { { active: 'true', url: '' } }
it 'returns error messages in JSON response' do
put :test, params: project_params(service: service_params)
put :test, params: project_params(service: integration_params)
expect(json_response['message']).to eq 'Validations failed.'
expect(json_response['service_response']).to include "Url can't be blank"
@ -40,12 +41,14 @@ RSpec.describe Projects::ServicesController do
end
end
context 'success' do
context 'when successful' do
context 'with empty project' do
let_it_be(:project) { create(:project) }
context 'with chat notification service' do
let_it_be(:service) { project.create_microsoft_teams_integration(webhook: 'http://webhook.com') }
context 'with chat notification integration' do
let_it_be(:teams_integration) { project.create_microsoft_teams_integration(webhook: 'http://webhook.com') }
let(:integration) { teams_integration }
it 'returns success' do
allow_next(::MicrosoftTeams::Notifier).to receive(:ping).and_return(true)
@ -61,7 +64,7 @@ RSpec.describe Projects::ServicesController do
expect(Gitlab::HTTP).to receive(:get).with('/rest/api/2/serverInfo', any_args).and_call_original
put :test, params: project_params(service: service_params)
put :test, params: project_params(service: integration_params)
expect(response).to be_successful
end
@ -72,13 +75,13 @@ RSpec.describe Projects::ServicesController do
expect(Gitlab::HTTP).to receive(:get).with('/rest/api/2/serverInfo', any_args).and_call_original
put :test, params: project_params(service: service_params)
put :test, params: project_params(service: integration_params)
expect(response).to be_successful
end
context 'when service is configured for the first time' do
let(:service_params) do
let(:integration_params) do
{
'active' => '1',
'push_events' => '1',
@ -109,17 +112,17 @@ RSpec.describe Projects::ServicesController do
def do_put
put :test, params: project_params(id: 'buildkite',
service: service_params)
service: integration_params)
end
end
end
context 'failure' do
context 'when unsuccessful' do
it 'returns an error response when the integration test fails' do
stub_request(:get, 'http://example.com/rest/api/2/serverInfo')
.to_return(status: 404)
put :test, params: project_params(service: service_params)
put :test, params: project_params(service: integration_params)
expect(response).to be_successful
expect(json_response).to eq(
@ -132,7 +135,6 @@ RSpec.describe Projects::ServicesController do
context 'with the Slack integration' do
let_it_be(:integration) { build(:integrations_slack) }
let_it_be(:service) { integration } # TODO: remove when https://gitlab.com/gitlab-org/gitlab/-/issues/330300 is complete
it 'returns an error response when the URL is blocked' do
put :test, params: project_params(service: { webhook: 'http://127.0.0.1' })
@ -165,17 +167,17 @@ RSpec.describe Projects::ServicesController do
describe 'PUT #update' do
describe 'as HTML' do
let(:service_params) { { active: true } }
let(:params) { project_params(service: service_params) }
let(:integration_params) { { active: true } }
let(:params) { project_params(service: integration_params) }
let(:message) { 'Jira settings saved and active.' }
let(:redirect_url) { edit_project_service_path(project, service) }
let(:redirect_url) { edit_project_service_path(project, integration) }
before do
put :update, params: params
end
shared_examples 'service update' do
shared_examples 'integration update' do
it 'redirects to the correct url with a flash message' do
expect(response).to redirect_to(redirect_url)
expect(flash[:notice]).to eq(message)
@ -183,49 +185,49 @@ RSpec.describe Projects::ServicesController do
end
context 'when param `active` is set to true' do
let(:params) { project_params(service: service_params, redirect_to: redirect) }
let(:params) { project_params(service: integration_params, redirect_to: redirect) }
context 'when redirect_to param is present' do
let(:redirect) { '/redirect_here' }
let(:redirect_url) { redirect }
it_behaves_like 'service update'
it_behaves_like 'integration update'
end
context 'when redirect_to is an external domain' do
let(:redirect) { 'http://examle.com' }
it_behaves_like 'service update'
it_behaves_like 'integration update'
end
context 'when redirect_to param is an empty string' do
let(:redirect) { '' }
it_behaves_like 'service update'
it_behaves_like 'integration update'
end
end
context 'when param `active` is set to false' do
let(:service_params) { { active: false } }
let(:message) { 'Jira settings saved, but not active.' }
let(:integration_params) { { active: false } }
let(:message) { 'Jira settings saved, but not active.' }
it_behaves_like 'service update'
it_behaves_like 'integration update'
end
context 'when param `inherit_from_id` is set to empty string' do
let(:service_params) { { inherit_from_id: '' } }
let(:integration_params) { { inherit_from_id: '' } }
it 'sets inherit_from_id to nil' do
expect(service.reload.inherit_from_id).to eq(nil)
expect(integration.reload.inherit_from_id).to eq(nil)
end
end
context 'when param `inherit_from_id` is set to some value' do
let(:instance_service) { create(:jira_integration, :instance) }
let(:service_params) { { inherit_from_id: instance_service.id } }
let(:integration_params) { { inherit_from_id: instance_service.id } }
it 'sets inherit_from_id to value' do
expect(service.reload.inherit_from_id).to eq(instance_service.id)
expect(integration.reload.inherit_from_id).to eq(instance_service.id)
end
end
end
@ -233,11 +235,11 @@ RSpec.describe Projects::ServicesController do
describe 'as JSON' do
before do
stub_jira_integration_test
put :update, params: project_params(service: service_params, format: :json)
put :update, params: project_params(service: integration_params, format: :json)
end
context 'when update succeeds' do
let(:service_params) { { url: 'http://example.com' } }
let(:integration_params) { { url: 'http://example.com' } }
it 'returns JSON response with no errors' do
expect(response).to be_successful
@ -246,60 +248,67 @@ RSpec.describe Projects::ServicesController do
end
context 'when update fails' do
let(:service_params) { { url: '' } }
let(:integration_params) { { url: '' } }
it 'returns JSON response with errors' do
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response).to include(
'active' => true,
'errors' => { 'url' => ['must be a valid URL', %{can't be blank}] }
'errors' => { 'url' => ['must be a valid URL', %(can't be blank)] }
)
end
end
end
context 'Prometheus integration' do
let_it_be(:service) { create(:prometheus_integration, project: project) }
context 'with Prometheus integration' do
let_it_be(:prometheus_integration) { create(:prometheus_integration, project: project) }
let(:service_params) { { manual_configuration: '1', api_url: 'http://example.com' } }
let(:integration) { prometheus_integration }
let(:integration_params) { { manual_configuration: '1', api_url: 'http://example.com' } }
context 'feature flag :settings_operations_prometheus_service is enabled' do
context 'when feature flag :settings_operations_prometheus_service is enabled' do
before do
stub_feature_flags(settings_operations_prometheus_service: true)
end
it 'redirects user back to edit page with alert' do
put :update, params: project_params.merge(service: service_params)
put :update, params: project_params.merge(service: integration_params)
expect(response).to redirect_to(edit_project_service_path(project, service))
expected_alert = "You can now manage your Prometheus settings on the <a href=\"#{project_settings_operations_path(project)}\">Operations</a> page. Fields on this page has been deprecated."
expect(response).to redirect_to(edit_project_service_path(project, integration))
expected_alert = [
"You can now manage your Prometheus settings on the",
%(<a href="#{project_settings_operations_path(project)}">Operations</a> page.),
"Fields on this page have been deprecated."
].join(' ')
expect(controller).to set_flash.now[:alert].to(expected_alert)
end
it 'does not modify integration' do
expect { put :update, params: project_params.merge(service: service_params) }.not_to change { project.prometheus_integration.reload.attributes }
expect { put :update, params: project_params.merge(service: integration_params) }
.not_to change { project.prometheus_integration.reload.attributes }
end
end
context 'feature flag :settings_operations_prometheus_service is disabled' do
context 'when feature flag :settings_operations_prometheus_service is disabled' do
before do
stub_feature_flags(settings_operations_prometheus_service: false)
end
it 'modifies integration' do
expect { put :update, params: project_params.merge(service: service_params) }.to change { project.prometheus_integration.reload.attributes }
expect { put :update, params: project_params.merge(service: integration_params) }
.to change { project.prometheus_integration.reload.attributes }
end
end
end
end
describe 'GET #edit' do
context 'Jira service' do
let(:service_param) { 'jira' }
context 'with Jira service' do
let(:integration_param) { 'jira' }
before do
get :edit, params: project_params(id: service_param)
get :edit, params: project_params(id: integration_param)
end
context 'with approved services' do
@ -309,25 +318,30 @@ RSpec.describe Projects::ServicesController do
end
end
context 'Prometheus service' do
let(:service_param) { 'prometheus' }
context 'with Prometheus service' do
let(:integration_param) { 'prometheus' }
context 'feature flag :settings_operations_prometheus_service is enabled' do
context 'when feature flag :settings_operations_prometheus_service is enabled' do
before do
stub_feature_flags(settings_operations_prometheus_service: true)
get :edit, params: project_params(id: service_param)
get :edit, params: project_params(id: integration_param)
end
it 'renders deprecation warning notice' do
expected_alert = "You can now manage your Prometheus settings on the <a href=\"#{project_settings_operations_path(project)}\">Operations</a> page. Fields on this page has been deprecated."
expected_alert = [
"You can now manage your Prometheus settings on the",
%(<a href="#{project_settings_operations_path(project)}">Operations</a> page.),
"Fields on this page have been deprecated."
].join(' ')
expect(controller).to set_flash.now[:alert].to(expected_alert)
end
end
context 'feature flag :settings_operations_prometheus_service is disabled' do
context 'when feature flag :settings_operations_prometheus_service is disabled' do
before do
stub_feature_flags(settings_operations_prometheus_service: false)
get :edit, params: project_params(id: service_param)
get :edit, params: project_params(id: integration_param)
end
it 'does not render deprecation warning notice' do
@ -343,7 +357,7 @@ RSpec.describe Projects::ServicesController do
opts.reverse_merge(
namespace_id: project.namespace,
project_id: project,
id: service.to_param
id: integration.to_param
)
end
end

View file

@ -17,6 +17,6 @@ RSpec.describe 'User activates Prometheus' do
click_button('Save changes')
expect(page).not_to have_content('Prometheus settings saved and active.')
expect(page).to have_content('Fields on this page has been deprecated.')
expect(page).to have_content('Fields on this page have been deprecated.')
end
end

View file

@ -0,0 +1,180 @@
import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import ProjectDropdown from '~/jira_connect/branches/components/project_dropdown.vue';
import { PROJECTS_PER_PAGE } from '~/jira_connect/branches/constants';
import getProjectsQuery from '~/jira_connect/branches/graphql/queries/get_projects.query.graphql';
const localVue = createLocalVue();
const mockProjects = [
{
id: 'test',
name: 'test',
nameWithNamespace: 'test',
avatarUrl: 'https://gitlab.com',
path: 'test-path',
fullPath: 'test-path',
repository: {
empty: false,
},
},
{
id: 'gitlab',
name: 'GitLab',
nameWithNamespace: 'gitlab-org/gitlab',
avatarUrl: 'https://gitlab.com',
path: 'gitlab',
fullPath: 'gitlab-org/gitlab',
repository: {
empty: false,
},
},
];
const mockProjectsQueryResponse = {
data: {
projects: {
nodes: mockProjects,
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: '',
endCursor: '',
},
},
},
};
const mockGetProjectsQuerySuccess = jest.fn().mockResolvedValue(mockProjectsQueryResponse);
const mockGetProjectsQueryFailed = jest.fn().mockRejectedValue(new Error('GraphQL error'));
const mockQueryLoading = jest.fn().mockReturnValue(new Promise(() => {}));
describe('ProjectDropdown', () => {
let wrapper;
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findDropdownItemByText = (text) =>
findAllDropdownItems().wrappers.find((item) => item.text() === text);
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
function createMockApolloProvider({ mockGetProjectsQuery = mockGetProjectsQuerySuccess } = {}) {
localVue.use(VueApollo);
const mockApollo = createMockApollo([[getProjectsQuery, mockGetProjectsQuery]]);
return mockApollo;
}
function createComponent({ mockApollo, props, mountFn = shallowMount } = {}) {
wrapper = mountFn(ProjectDropdown, {
localVue,
apolloProvider: mockApollo || createMockApolloProvider(),
propsData: props,
});
}
afterEach(() => {
wrapper.destroy();
});
describe('when loading projects', () => {
beforeEach(() => {
createComponent({
mockApollo: createMockApolloProvider({ mockGetProjectsQuery: mockQueryLoading }),
});
});
it('sets dropdown `loading` prop to `true`', () => {
expect(findDropdown().props('loading')).toBe(true);
});
it('renders loading icon in dropdown', () => {
expect(findLoadingIcon().isVisible()).toBe(true);
});
});
describe('when projects query succeeds', () => {
beforeEach(async () => {
createComponent();
await waitForPromises();
await wrapper.vm.$nextTick();
});
it('sets dropdown `loading` prop to `false`', () => {
expect(findDropdown().props('loading')).toBe(false);
});
it('renders dropdown items', () => {
const dropdownItems = findAllDropdownItems();
expect(dropdownItems.wrappers).toHaveLength(mockProjects.length);
expect(dropdownItems.wrappers.map((item) => item.text())).toEqual(
mockProjects.map((project) => project.nameWithNamespace),
);
});
describe('when selecting a dropdown item', () => {
it('emits `change` event with the selected project name', async () => {
const mockProject = mockProjects[0];
const itemToSelect = findDropdownItemByText(mockProject.nameWithNamespace);
await itemToSelect.vm.$emit('click');
expect(wrapper.emitted('change')[0]).toEqual([mockProject]);
});
});
describe('when `selectedProject` prop is specified', () => {
const mockProject = mockProjects[0];
beforeEach(async () => {
wrapper.setProps({
selectedProject: mockProject,
});
});
it('sets `isChecked` prop of the corresponding dropdown item to `true`', () => {
expect(findDropdownItemByText(mockProject.nameWithNamespace).props('isChecked')).toBe(true);
});
it('sets dropdown text to `selectedBranchName` value', () => {
expect(findDropdown().props('text')).toBe(mockProject.nameWithNamespace);
});
});
});
describe('when projects query fails', () => {
beforeEach(async () => {
createComponent({
mockApollo: createMockApolloProvider({ mockGetProjectsQuery: mockGetProjectsQueryFailed }),
});
await waitForPromises();
});
it('emits `error` event', () => {
expect(wrapper.emitted('error')).toBeTruthy();
});
});
describe('when searching branches', () => {
it('triggers a refetch', async () => {
createComponent({ mountFn: mount });
await waitForPromises();
jest.clearAllMocks();
const mockSearchTerm = 'gitl';
await findSearchBox().vm.$emit('input', mockSearchTerm);
expect(mockGetProjectsQuerySuccess).toHaveBeenCalledWith({
after: '',
first: PROJECTS_PER_PAGE,
membership: true,
search: mockSearchTerm,
searchNamespaces: true,
sort: 'similarity',
});
});
});
});

View file

@ -0,0 +1,192 @@
import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import SourceBranchDropdown from '~/jira_connect/branches/components/source_branch_dropdown.vue';
import { BRANCHES_PER_PAGE } from '~/jira_connect/branches/constants';
import getProjectQuery from '~/jira_connect/branches/graphql/queries/get_project.query.graphql';
const localVue = createLocalVue();
const mockProject = {
id: 'test',
fullPath: 'test-path',
repository: {
branchNames: ['main', 'f-test', 'release'],
rootRef: 'main',
},
};
const mockProjectQueryResponse = {
data: {
project: mockProject,
},
};
const mockGetProjectQuery = jest.fn().mockResolvedValue(mockProjectQueryResponse);
const mockQueryLoading = jest.fn().mockReturnValue(new Promise(() => {}));
describe('SourceBranchDropdown', () => {
let wrapper;
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findDropdownItemByText = (text) =>
findAllDropdownItems().wrappers.find((item) => item.text() === text);
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
const assertDropdownItems = () => {
const dropdownItems = findAllDropdownItems();
expect(dropdownItems.wrappers).toHaveLength(mockProject.repository.branchNames.length);
expect(dropdownItems.wrappers.map((item) => item.text())).toEqual(
mockProject.repository.branchNames,
);
};
function createMockApolloProvider({ getProjectQueryLoading = false } = {}) {
localVue.use(VueApollo);
const mockApollo = createMockApollo([
[getProjectQuery, getProjectQueryLoading ? mockQueryLoading : mockGetProjectQuery],
]);
return mockApollo;
}
function createComponent({ mockApollo, props, mountFn = shallowMount } = {}) {
wrapper = mountFn(SourceBranchDropdown, {
localVue,
apolloProvider: mockApollo || createMockApolloProvider(),
propsData: props,
});
}
afterEach(() => {
wrapper.destroy();
});
describe('when `selectedProject` prop is not specified', () => {
beforeEach(() => {
createComponent();
});
it('sets dropdown `disabled` prop to `true`', () => {
expect(findDropdown().props('disabled')).toBe(true);
});
describe('when `selectedProject` becomes specified', () => {
beforeEach(async () => {
wrapper.setProps({
selectedProject: mockProject,
});
await waitForPromises();
});
it('sets dropdown props correctly', () => {
expect(findDropdown().props()).toMatchObject({
loading: false,
disabled: false,
text: 'Select a branch',
});
});
it('renders available source branches as dropdown items', () => {
assertDropdownItems();
});
});
});
describe('when `selectedProject` prop is specified', () => {
describe('when branches are loading', () => {
it('renders loading icon in dropdown', () => {
createComponent({
mockApollo: createMockApolloProvider({ getProjectQueryLoading: true }),
props: { selectedProject: mockProject },
});
expect(findLoadingIcon().isVisible()).toBe(true);
});
});
describe('when branches have loaded', () => {
describe('when searching branches', () => {
it('triggers a refetch', async () => {
createComponent({ mountFn: mount, props: { selectedProject: mockProject } });
await waitForPromises();
jest.clearAllMocks();
const mockSearchTerm = 'mai';
await findSearchBox().vm.$emit('input', mockSearchTerm);
expect(mockGetProjectQuery).toHaveBeenCalledWith({
branchNamesLimit: BRANCHES_PER_PAGE,
branchNamesOffset: 0,
branchNamesSearchPattern: `*${mockSearchTerm}*`,
projectPath: 'test-path',
});
});
});
describe('template', () => {
beforeEach(async () => {
createComponent({ props: { selectedProject: mockProject } });
await waitForPromises();
});
it('sets dropdown props correctly', () => {
expect(findDropdown().props()).toMatchObject({
loading: false,
disabled: false,
text: 'Select a branch',
});
});
it('omits monospace styling from dropdown', () => {
expect(findDropdown().classes()).not.toContain('gl-font-monospace');
});
it('renders available source branches as dropdown items', () => {
assertDropdownItems();
});
it("emits `change` event with the repository's `rootRef` by default", () => {
expect(wrapper.emitted('change')[0]).toEqual([mockProject.repository.rootRef]);
});
describe('when selecting a dropdown item', () => {
it('emits `change` event with the selected branch name', async () => {
const mockBranchName = mockProject.repository.branchNames[1];
const itemToSelect = findDropdownItemByText(mockBranchName);
await itemToSelect.vm.$emit('click');
expect(wrapper.emitted('change')[1]).toEqual([mockBranchName]);
});
});
describe('when `selectedBranchName` prop is specified', () => {
const mockBranchName = mockProject.repository.branchNames[2];
beforeEach(async () => {
wrapper.setProps({
selectedBranchName: mockBranchName,
});
});
it('sets `isChecked` prop of the corresponding dropdown item to `true`', () => {
expect(findDropdownItemByText(mockBranchName).props('isChecked')).toBe(true);
});
it('sets dropdown text to `selectedBranchName` value', () => {
expect(findDropdown().props('text')).toBe(mockBranchName);
});
it('adds monospace styling to dropdown', () => {
expect(findDropdown().classes()).toContain('gl-font-monospace');
});
});
});
});
});
});

View file

@ -217,8 +217,6 @@ RSpec.describe Banzai::Filter::References::ExternalIssueReferenceFilter do
context "ewm project" do
let_it_be(:integration) { create(:ewm_integration, project: project) }
let(:service) { integration } # TODO: remove when https://gitlab.com/gitlab-org/gitlab/-/issues/330300 is complete
before do
project.update!(issues_enabled: false)
end

View file

@ -126,40 +126,50 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
end
describe '#split_and_retry!' do
let!(:job) { create(:batched_background_migration_job, batch_size: 10, min_value: 6, max_value: 15, status: :failed) }
let!(:job) { create(:batched_background_migration_job, batch_size: 10, min_value: 6, max_value: 15, status: :failed, attempts: 3) }
it 'splits the job into two and marks them as pending' do
allow_next_instance_of(Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchingStrategy) do |batch_class|
allow(batch_class).to receive(:next_batch).with(anything, anything, batch_min_value: 6, batch_size: 5).and_return([6, 10])
context 'when job can be split' do
before do
allow_next_instance_of(Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchingStrategy) do |batch_class|
allow(batch_class).to receive(:next_batch).with(anything, anything, batch_min_value: 6, batch_size: 5).and_return([6, 10])
end
end
expect { job.split_and_retry! }.to change { described_class.count }.by(1)
it 'sets the correct attributes' do
expect { job.split_and_retry! }.to change { described_class.count }.by(1)
expect(job).to have_attributes(
min_value: 6,
max_value: 10,
batch_size: 5,
status: 'pending',
attempts: 0,
started_at: nil,
finished_at: nil,
metrics: {}
)
expect(job).to have_attributes(
min_value: 6,
max_value: 10,
batch_size: 5,
status: 'failed',
attempts: 0,
started_at: nil,
finished_at: nil,
metrics: {}
)
new_job = described_class.last
new_job = described_class.last
expect(new_job).to have_attributes(
batched_background_migration_id: job.batched_background_migration_id,
min_value: 11,
max_value: 15,
batch_size: 5,
status: 'pending',
attempts: 0,
started_at: nil,
finished_at: nil,
metrics: {}
)
expect(new_job.created_at).not_to eq(job.created_at)
expect(new_job).to have_attributes(
batched_background_migration_id: job.batched_background_migration_id,
min_value: 11,
max_value: 15,
batch_size: 5,
status: 'failed',
attempts: 0,
started_at: nil,
finished_at: nil,
metrics: {}
)
expect(new_job.created_at).not_to eq(job.created_at)
end
it 'splits the jobs into retriable jobs' do
migration = job.batched_migration
expect { job.split_and_retry! }.to change { migration.batched_jobs.retriable.count }.from(0).to(2)
end
end
context 'when job is not failed' do
@ -185,11 +195,12 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
end
end
it 'lowers the batch size and marks the job as pending' do
it 'lowers the batch size and resets the number of attempts' do
expect { job.split_and_retry! }.not_to change { described_class.count }
expect(job.batch_size).to eq(5)
expect(job.status).to eq('pending')
expect(job.attempts).to eq(0)
expect(job.status).to eq('failed')
end
end
end

View file

@ -10,11 +10,11 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
describe '#last_job' do
let!(:batched_migration) { create(:batched_background_migration) }
let!(:batched_job1) { create(:batched_background_migration_job, batched_migration: batched_migration) }
let!(:batched_job2) { create(:batched_background_migration_job, batched_migration: batched_migration) }
let!(:batched_job1) { create(:batched_background_migration_job, batched_migration: batched_migration, max_value: 1000) }
let!(:batched_job2) { create(:batched_background_migration_job, batched_migration: batched_migration, max_value: 500) }
it 'returns the most recent (in order of id) batched job' do
expect(batched_migration.last_job).to eq(batched_job2)
it 'returns the batched job with highest max_value' do
expect(batched_migration.last_job).to eq(batched_job1)
end
end
end

View file

@ -0,0 +1,86 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe AddPremiumAndUltimatePlanLimits, :migration do
shared_examples_for 'a migration that does not alter plans or plan limits' do
it do
expect { migrate! }.not_to change {
[
AddPremiumAndUltimatePlanLimits::Plan.count,
AddPremiumAndUltimatePlanLimits::PlanLimits.count
]
}
end
end
describe '#up' do
context 'when not .com?' do
before do
allow(Gitlab).to receive(:com?).and_return false
end
it_behaves_like 'a migration that does not alter plans or plan limits'
end
context 'when .com?' do
before do
allow(Gitlab).to receive(:com?).and_return true
end
context 'when source plan does not exist' do
it_behaves_like 'a migration that does not alter plans or plan limits'
end
context 'when target plan does not exist' do
before do
table(:plans).create!(name: 'silver', title: 'Silver')
table(:plans).create!(name: 'gold', title: 'Gold')
end
it_behaves_like 'a migration that does not alter plans or plan limits'
end
context 'when source and target plans exist' do
let!(:silver) { table(:plans).create!(name: 'silver', title: 'Silver') }
let!(:gold) { table(:plans).create!(name: 'gold', title: 'Gold') }
let!(:premium) { table(:plans).create!(name: 'premium', title: 'Premium') }
let!(:ultimate) { table(:plans).create!(name: 'ultimate', title: 'Ultimate') }
let!(:silver_limits) { table(:plan_limits).create!(plan_id: silver.id, storage_size_limit: 111) }
let!(:gold_limits) { table(:plan_limits).create!(plan_id: gold.id, storage_size_limit: 222) }
context 'when target has plan limits' do
before do
table(:plan_limits).create!(plan_id: premium.id, storage_size_limit: 999)
table(:plan_limits).create!(plan_id: ultimate.id, storage_size_limit: 999)
end
it 'does not overwrite the limits' do
expect { migrate! }.not_to change {
[
AddPremiumAndUltimatePlanLimits::Plan.count,
AddPremiumAndUltimatePlanLimits::PlanLimits.pluck(:id, :storage_size_limit).sort
]
}
end
end
context 'when target has no plan limits' do
it 'creates plan limits from the source plan' do
migrate!
expect(AddPremiumAndUltimatePlanLimits::PlanLimits.pluck(:plan_id, :storage_size_limit)).to match_array([
[silver.id, silver_limits.storage_size_limit],
[gold.id, gold_limits.storage_size_limit],
[premium.id, silver_limits.storage_size_limit],
[ultimate.id, gold_limits.storage_size_limit]
])
end
end
end
end
end
end