Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
e1189e4c3b
commit
e9606d7f51
47 changed files with 1069 additions and 174 deletions
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,2 @@
|
|||
export const BRANCHES_PER_PAGE = 20;
|
||||
export const PROJECTS_PER_PAGE = 20;
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
1
db/schema_migrations/20210706213537
Normal file
1
db/schema_migrations/20210706213537
Normal file
|
@ -0,0 +1 @@
|
|||
150463cef309e6bf69240c258dc8aede53b846a08a7e2d668ee0429709022554
|
1
db/schema_migrations/20210709085759
Normal file
1
db/schema_migrations/20210709085759
Normal file
|
@ -0,0 +1 @@
|
|||
4216604d14b4ccc652ba423a95ee9bd15646b3553903dc4b79497871f5384492
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 |
BIN
doc/ci/runners/img/protected_runners_check_box_v14_1.png
Normal file
BIN
doc/ci/runners/img/protected_runners_check_box_v14_1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
Binary file not shown.
Before Width: | Height: | Size: 20 KiB |
BIN
doc/ci/runners/img/shared_runner_ip_address_14_1.png
Normal file
BIN
doc/ci/runners/img/shared_runner_ip_address_14_1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.4 KiB |
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 |
BIN
doc/user/admin_area/img/index_runners_search_or_filter_v14_1.png
Normal file
BIN
doc/user/admin_area/img/index_runners_search_or_filter_v14_1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
|
@ -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 |
|
||||
|
|
|
@ -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).
|
||||
|
||||
|
|
|
@ -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: {}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
86
spec/migrations/add_premium_and_ultimate_plan_limits_spec.rb
Normal file
86
spec/migrations/add_premium_and_ultimate_plan_limits_spec.rb
Normal 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
|
Loading…
Reference in a new issue