summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-03-03 21:11:13 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-03-03 21:11:13 +0000
commit63b3a14f15ee5c202d78b7bd72030f4f437ef982 (patch)
tree7994560f5a6bc1d421ce0fe395031e52d2b4719a
parent9578c9f9e88421a5dc4d9215f40d932bd30cbabc (diff)
downloadgitlab-ce-63b3a14f15ee5c202d78b7bd72030f4f437ef982.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/access_tokens/components/projects_field.vue22
-rw-r--r--app/assets/javascripts/access_tokens/components/projects_token_selector.vue141
-rw-r--r--app/assets/javascripts/access_tokens/graphql/queries/get_projects.query.graphql22
-rw-r--r--app/assets/javascripts/access_tokens/index.js49
-rw-r--r--app/assets/javascripts/pages/profiles/index.js28
-rw-r--r--app/controllers/admin/application_settings_controller.rb5
-rw-r--r--app/controllers/projects_controller.rb22
-rw-r--r--app/helpers/application_settings_helper.rb9
-rw-r--r--app/models/application_setting.rb12
-rw-r--r--app/models/application_setting_implementation.rb19
-rw-r--r--app/models/merge_request.rb10
-rw-r--r--app/services/merge_requests/after_create_service.rb7
-rw-r--r--app/services/merge_requests/create_service.rb2
-rw-r--r--app/views/admin/application_settings/_repository_storage.html.haml9
-rw-r--r--app/views/projects/edit.html.haml4
-rw-r--r--app/views/projects/settings/_general.html.haml3
-rw-r--r--app/views/projects/update.js.haml10
-rw-r--r--changelogs/unreleased/291012-preparing-mr-state.yml5
-rw-r--r--changelogs/unreleased/bump_auto_deploy_image_version.yml4
-rw-r--r--changelogs/unreleased/fix_repo_storage_weights_admin.yml5
-rw-r--r--doc/development/testing_guide/best_practices.md1
-rw-r--r--doc/user/group/index.md4
-rw-r--r--doc/user/project/clusters/add_eks_clusters.md8
-rw-r--r--lib/bitbucket/collection.rb8
-rw-r--r--lib/bitbucket_server/collection.rb8
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml2
-rw-r--r--locale/gitlab.pot12
-rw-r--r--spec/controllers/admin/application_settings_controller_spec.rb4
-rw-r--r--spec/factories_spec.rb4
-rw-r--r--spec/features/admin/admin_settings_spec.rb15
-rw-r--r--spec/frontend/access_tokens/components/projects_field_spec.js54
-rw-r--r--spec/frontend/access_tokens/components/projects_token_selector_spec.js224
-rw-r--r--spec/frontend/access_tokens/index_spec.js32
-rw-r--r--spec/frontend/fixtures/projects.rb33
-rw-r--r--spec/helpers/application_settings_helper_spec.rb15
-rw-r--r--spec/models/application_setting_spec.rb29
-rw-r--r--spec/models/merge_request_spec.rb6
-rw-r--r--spec/services/merge_requests/after_create_service_spec.rb22
-rw-r--r--spec/services/merge_requests/create_service_spec.rb4
-rw-r--r--spec/support/shared_examples/models/application_setting_shared_examples.rb15
-rw-r--r--spec/views/admin/application_settings/_repository_storage.html.haml_spec.rb47
41 files changed, 732 insertions, 203 deletions
diff --git a/app/assets/javascripts/access_tokens/components/projects_field.vue b/app/assets/javascripts/access_tokens/components/projects_field.vue
index e58f74b6ad4..dfe4e07662b 100644
--- a/app/assets/javascripts/access_tokens/components/projects_field.vue
+++ b/app/assets/javascripts/access_tokens/components/projects_field.vue
@@ -1,11 +1,12 @@
<script>
import { GlFormGroup, GlFormRadio, GlFormText } from '@gitlab/ui';
+import ProjectsTokenSelector from './projects_token_selector.vue';
export default {
name: 'ProjectsField',
ALL_PROJECTS: 'ALL_PROJECTS',
SELECTED_PROJECTS: 'SELECTED_PROJECTS',
- components: { GlFormGroup, GlFormRadio, GlFormText },
+ components: { GlFormGroup, GlFormRadio, GlFormText, ProjectsTokenSelector },
props: {
inputAttrs: {
type: Object,
@@ -15,8 +16,24 @@ export default {
data() {
return {
selectedRadio: this.$options.ALL_PROJECTS,
+ selectedProjects: [],
};
},
+ computed: {
+ allProjectsRadioSelected() {
+ return this.selectedRadio === this.$options.ALL_PROJECTS;
+ },
+ hiddenInputValue() {
+ return this.allProjectsRadioSelected
+ ? null
+ : this.selectedProjects.map((project) => project.id).join(',');
+ },
+ },
+ methods: {
+ handleTokenSelectorFocus() {
+ this.selectedRadio = this.$options.SELECTED_PROJECTS;
+ },
+ },
};
</script>
@@ -32,7 +49,8 @@ export default {
<gl-form-radio v-model="selectedRadio" :value="$options.SELECTED_PROJECTS">{{
__('Selected projects')
}}</gl-form-radio>
- <input :id="inputAttrs.id" type="hidden" :name="inputAttrs.name" />
+ <input :id="inputAttrs.id" type="hidden" :name="inputAttrs.name" :value="hiddenInputValue" />
+ <projects-token-selector v-model="selectedProjects" @focus="handleTokenSelectorFocus" />
</gl-form-group>
</div>
</template>
diff --git a/app/assets/javascripts/access_tokens/components/projects_token_selector.vue b/app/assets/javascripts/access_tokens/components/projects_token_selector.vue
new file mode 100644
index 00000000000..839f32bd488
--- /dev/null
+++ b/app/assets/javascripts/access_tokens/components/projects_token_selector.vue
@@ -0,0 +1,141 @@
+<script>
+import {
+ GlTokenSelector,
+ GlAvatar,
+ GlAvatarLabeled,
+ GlIntersectionObserver,
+ GlLoadingIcon,
+} from '@gitlab/ui';
+import produce from 'immer';
+
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+
+import getProjectsQuery from '../graphql/queries/get_projects.query.graphql';
+
+const DEBOUNCE_DELAY = 250;
+const PROJECTS_PER_PAGE = 20;
+
+export default {
+ name: 'ProjectsTokenSelector',
+ components: {
+ GlTokenSelector,
+ GlAvatar,
+ GlAvatarLabeled,
+ GlIntersectionObserver,
+ GlLoadingIcon,
+ },
+ model: {
+ prop: 'selectedProjects',
+ },
+ props: {
+ selectedProjects: {
+ type: Array,
+ required: true,
+ },
+ },
+ apollo: {
+ projects: {
+ query: getProjectsQuery,
+ debounce: DEBOUNCE_DELAY,
+ variables() {
+ return {
+ search: this.searchQuery,
+ after: null,
+ first: PROJECTS_PER_PAGE,
+ };
+ },
+ update({ projects }) {
+ return {
+ list: projects.nodes.map((project) => ({
+ ...project,
+ id: getIdFromGraphQLId(project.id),
+ })),
+ pageInfo: projects.pageInfo,
+ };
+ },
+ result() {
+ this.isLoadingMoreProjects = false;
+ this.isSearching = false;
+ },
+ },
+ },
+ data() {
+ return {
+ projects: {
+ list: [],
+ pageInfo: {},
+ },
+ searchQuery: '',
+ isLoadingMoreProjects: false,
+ isSearching: false,
+ };
+ },
+ methods: {
+ handleSearch(query) {
+ this.isSearching = true;
+ this.searchQuery = query;
+ },
+ loadMoreProjects() {
+ this.isLoadingMoreProjects = true;
+
+ this.$apollo.queries.projects.fetchMore({
+ variables: {
+ after: this.projects.pageInfo.endCursor,
+ first: PROJECTS_PER_PAGE,
+ },
+ updateQuery(previousResult, { fetchMoreResult: { projects: newProjects } }) {
+ const { projects: previousProjects } = previousResult;
+
+ return produce(previousResult, (draftData) => {
+ /* eslint-disable no-param-reassign */
+ draftData.projects.nodes = [...previousProjects.nodes, ...newProjects.nodes];
+ draftData.projects.pageInfo = newProjects.pageInfo;
+ /* eslint-enable no-param-reassign */
+ });
+ },
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-relative">
+ <gl-token-selector
+ :selected-tokens="selectedProjects"
+ :dropdown-items="projects.list"
+ :loading="isSearching"
+ :placeholder="__('Select projects')"
+ menu-class="gl-w-full! gl-max-w-full!"
+ @input="$emit('input', $event)"
+ @focus="$emit('focus', $event)"
+ @text-input="handleSearch"
+ @keydown.enter.prevent
+ >
+ <template #token-content="{ token: project }">
+ <gl-avatar
+ :entity-id="project.id"
+ :entity-name="project.name"
+ :src="project.avatarUrl"
+ :size="16"
+ />
+ {{ project.nameWithNamespace }}
+ </template>
+ <template #dropdown-item-content="{ dropdownItem: project }">
+ <gl-avatar-labeled
+ :entity-id="project.id"
+ :entity-name="project.name"
+ :size="32"
+ :src="project.avatarUrl"
+ :label="project.name"
+ :sub-label="project.nameWithNamespace"
+ />
+ </template>
+ <template #dropdown-footer>
+ <gl-intersection-observer v-if="projects.pageInfo.hasNextPage" @appear="loadMoreProjects">
+ <gl-loading-icon v-if="isLoadingMoreProjects" size="md" />
+ </gl-intersection-observer>
+ </template>
+ </gl-token-selector>
+ </div>
+</template>
diff --git a/app/assets/javascripts/access_tokens/graphql/queries/get_projects.query.graphql b/app/assets/javascripts/access_tokens/graphql/queries/get_projects.query.graphql
new file mode 100644
index 00000000000..40d475cb571
--- /dev/null
+++ b/app/assets/javascripts/access_tokens/graphql/queries/get_projects.query.graphql
@@ -0,0 +1,22 @@
+#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
+
+query getProjects($search: String!, $after: String = "", $first: Int!) {
+ projects(
+ search: $search
+ after: $after
+ first: $first
+ membership: true
+ searchNamespaces: true
+ sort: "UPDATED_ASC"
+ ) {
+ nodes {
+ id
+ name
+ nameWithNamespace
+ avatarUrl
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+}
diff --git a/app/assets/javascripts/access_tokens/index.js b/app/assets/javascripts/access_tokens/index.js
index e29ec5adb42..6ab17092999 100644
--- a/app/assets/javascripts/access_tokens/index.js
+++ b/app/assets/javascripts/access_tokens/index.js
@@ -1,4 +1,6 @@
import Vue from 'vue';
+import createFlash from '~/flash';
+import { __ } from '~/locale';
import ExpiresAtField from './components/expires_at_field.vue';
@@ -43,17 +45,46 @@ export const initProjectsField = () => {
const inputAttrs = getInputAttrs(el);
if (window.gon.features.personalAccessTokensScopedToProjects) {
- const ProjectsField = () => import('./components/projects_field.vue');
-
- return new Vue({
- el,
- render(h) {
- return h(ProjectsField, {
- props: {
- inputAttrs,
+ return new Promise((resolve) => {
+ Promise.all([
+ import('./components/projects_field.vue'),
+ import('vue-apollo'),
+ import('~/lib/graphql'),
+ ])
+ .then(
+ ([
+ { default: ProjectsField },
+ { default: VueApollo },
+ { default: createDefaultClient },
+ ]) => {
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
+ Vue.use(VueApollo);
+
+ resolve(
+ new Vue({
+ el,
+ apolloProvider,
+ render(h) {
+ return h(ProjectsField, {
+ props: {
+ inputAttrs,
+ },
+ });
+ },
+ }),
+ );
},
+ )
+ .catch(() => {
+ createFlash({
+ message: __(
+ 'An error occurred while loading the access tokens form, please try again.',
+ ),
+ });
});
- },
});
}
diff --git a/app/assets/javascripts/pages/profiles/index.js b/app/assets/javascripts/pages/profiles/index.js
index 535fe5fa4eb..80bc32dd43f 100644
--- a/app/assets/javascripts/pages/profiles/index.js
+++ b/app/assets/javascripts/pages/profiles/index.js
@@ -3,21 +3,19 @@ import '~/profile/gl_crop';
import Profile from '~/profile/profile';
import initSearchSettings from '~/search_settings';
-document.addEventListener('DOMContentLoaded', () => {
- // eslint-disable-next-line func-names
- $(document).on('input.ssh_key', '#key_key', function () {
- const $title = $('#key_title');
- const comment = $(this)
- .val()
- .match(/^\S+ \S+ (.+)\n?$/);
+// eslint-disable-next-line func-names
+$(document).on('input.ssh_key', '#key_key', function () {
+ const $title = $('#key_title');
+ const comment = $(this)
+ .val()
+ .match(/^\S+ \S+ (.+)\n?$/);
- // Extract the SSH Key title from its comment
- if (comment && comment.length > 1) {
- $title.val(comment[1]).change();
- }
- });
+ // Extract the SSH Key title from its comment
+ if (comment && comment.length > 1) {
+ $title.val(comment[1]).change();
+ }
+});
- new Profile(); // eslint-disable-line no-new
+new Profile(); // eslint-disable-line no-new
- initSearchSettings();
-});
+initSearchSettings();
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 7c6a444ce7a..7f7d38a09c5 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -237,6 +237,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
[
*::ApplicationSettingsHelper.visible_attributes,
*::ApplicationSettingsHelper.external_authorization_service_attributes,
+ *ApplicationSetting.repository_storages_weighted_attributes,
*ApplicationSetting.kroki_formats_attributes.keys.map { |key| "kroki_formats_#{key}".to_sym },
:lets_encrypt_notification_email,
:lets_encrypt_terms_of_service_accepted,
@@ -247,8 +248,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:default_branch_name,
disabled_oauth_sign_in_sources: [],
import_sources: [],
- restricted_visibility_levels: [],
- repository_storages_weighted: {}
+ repository_storages: [],
+ restricted_visibility_levels: []
]
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index e3743ef24f2..8b9d42e00e6 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -90,21 +90,13 @@ class ProjectsController < Projects::ApplicationController
# Refresh the repo in case anything changed
@repository = @project.repository
- respond_to do |format|
- if result[:status] == :success
- flash[:notice] = _("Project '%{project_name}' was successfully updated.") % { project_name: @project.name }
-
- format.html do
- redirect_to(edit_project_path(@project, anchor: 'js-general-project-settings'))
- end
- else
- flash[:alert] = result[:message]
- @project.reset
-
- format.html { render_edit }
- end
-
- format.js
+ if result[:status] == :success
+ flash[:notice] = _("Project '%{project_name}' was successfully updated.") % { project_name: @project.name }
+ redirect_to(edit_project_path(@project, anchor: 'js-general-project-settings'))
+ else
+ flash[:alert] = result[:message]
+ @project.reset
+ render 'edit'
end
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 551e08d1e31..244238e246d 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -37,8 +37,13 @@ module ApplicationSettingsHelper
end
def storage_weights
- Gitlab.config.repositories.storages.keys.each_with_object(OpenStruct.new) do |storage, weights|
- weights[storage.to_sym] = @application_setting.repository_storages_weighted[storage] || 0
+ ApplicationSetting.repository_storages_weighted_attributes.map do |attribute|
+ storage = attribute.to_s.delete_prefix('repository_storages_weighted_')
+ {
+ name: attribute,
+ label: storage,
+ value: @application_setting.repository_storages_weighted[storage] || 0
+ }
end
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 44eb2fefb3f..4959401eb27 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -25,6 +25,10 @@ class ApplicationSetting < ApplicationRecord
alias_attribute :instance_group_id, :instance_administrators_group_id
alias_attribute :instance_administrators_group, :instance_group
+ def self.repository_storages_weighted_attributes
+ @repository_storages_weighted_atributes ||= Gitlab.config.repositories.storages.keys.map { |k| "repository_storages_weighted_#{k}".to_sym }.freeze
+ end
+
def self.kroki_formats_attributes
{
blockdiag: {
@@ -40,6 +44,7 @@ class ApplicationSetting < ApplicationRecord
end
store_accessor :kroki_formats, *ApplicationSetting.kroki_formats_attributes.keys, prefix: true
+ store_accessor :repository_storages_weighted, *Gitlab.config.repositories.storages.keys, prefix: true
# Include here so it can override methods from
# `add_authentication_token_field`
@@ -498,7 +503,6 @@ class ApplicationSetting < ApplicationRecord
inclusion: { in: [true, false], message: _('must be a boolean value') }
before_validation :ensure_uuid!
- before_validation :coerce_repository_storages_weighted, if: :repository_storages_weighted_changed?
before_save :ensure_runners_registration_token
before_save :ensure_health_check_access_token
@@ -579,6 +583,12 @@ class ApplicationSetting < ApplicationRecord
recaptcha_enabled || login_recaptcha_protection_enabled
end
+ repository_storages_weighted_attributes.each do |attribute|
+ define_method :"#{attribute}=" do |value|
+ super(value.to_i)
+ end
+ end
+
kroki_formats_attributes.keys.each do |key|
define_method :"kroki_formats_#{key}=" do |value|
super(::Gitlab::Utils.to_boolean(value))
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 731a3b5fed8..08c16930b13 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -298,6 +298,10 @@ module ApplicationSettingImplementation
Array(read_attribute(:repository_storages))
end
+ def repository_storages_weighted
+ read_attribute(:repository_storages_weighted)
+ end
+
def commit_email_hostname
super.presence || self.class.default_commit_email_hostname
end
@@ -329,10 +333,9 @@ module ApplicationSettingImplementation
def normalized_repository_storage_weights
strong_memoize(:normalized_repository_storage_weights) do
- repository_storages_weights = repository_storages_weighted.slice(*Gitlab.config.repositories.storages.keys)
- weights_total = repository_storages_weights.values.reduce(:+)
+ weights_total = repository_storages_weighted.values.reduce(:+)
- repository_storages_weights.transform_values do |w|
+ repository_storages_weighted.transform_values do |w|
next w if weights_total == 0
w.to_f / weights_total
@@ -470,20 +473,16 @@ module ApplicationSettingImplementation
invalid.empty?
end
- def coerce_repository_storages_weighted
- repository_storages_weighted.transform_values!(&:to_i)
- end
-
def check_repository_storages_weighted
invalid = repository_storages_weighted.keys - Gitlab.config.repositories.storages.keys
- errors.add(:repository_storages_weighted, _("can't include: %{invalid_storages}") % { invalid_storages: invalid.join(", ") }) unless
+ errors.add(:repository_storages_weighted, "can't include: %{invalid_storages}" % { invalid_storages: invalid.join(", ") }) unless
invalid.empty?
repository_storages_weighted.each do |key, val|
next unless val.present?
- errors.add(:repository_storages_weighted, _("value for '%{storage}' must be an integer") % { storage: key }) unless val.is_a?(Integer)
- errors.add(:repository_storages_weighted, _("value for '%{storage}' must be between 0 and 100") % { storage: key }) unless val.between?(0, 100)
+ errors.add(:"repository_storages_weighted_#{key}", "value must be an integer") unless val.is_a?(Integer)
+ errors.add(:"repository_storages_weighted_#{key}", "value must be between 0 and 100") unless val.between?(0, 100)
end
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 07092e50680..349297c776c 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -191,8 +191,12 @@ class MergeRequest < ApplicationRecord
end
state_machine :merge_status, initial: :unchecked do
+ event :mark_as_preparing do
+ transition unchecked: :preparing
+ end
+
event :mark_as_unchecked do
- transition [:can_be_merged, :checking, :unchecked] => :unchecked
+ transition [:preparing, :can_be_merged, :checking, :unchecked] => :unchecked
transition [:cannot_be_merged, :cannot_be_merged_rechecking, :cannot_be_merged_recheck] => :cannot_be_merged_recheck
end
@@ -237,7 +241,7 @@ class MergeRequest < ApplicationRecord
# Returns current merge_status except it returns `cannot_be_merged_rechecking` as `checking`
# to avoid exposing unnecessary internal state
def public_merge_status
- cannot_be_merged_rechecking? ? 'checking' : merge_status
+ cannot_be_merged_rechecking? || preparing? ? 'checking' : merge_status
end
validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_or_merged_without_fork?]
@@ -1054,6 +1058,8 @@ class MergeRequest < ApplicationRecord
end
def mergeable?(skip_ci_check: false, skip_discussions_check: false)
+ return false if preparing?
+
return false unless mergeable_state?(skip_ci_check: skip_ci_check,
skip_discussions_check: skip_discussions_check)
diff --git a/app/services/merge_requests/after_create_service.rb b/app/services/merge_requests/after_create_service.rb
index 03fcb5a4c1b..9bbb47b8058 100644
--- a/app/services/merge_requests/after_create_service.rb
+++ b/app/services/merge_requests/after_create_service.rb
@@ -3,6 +3,13 @@
module MergeRequests
class AfterCreateService < MergeRequests::BaseService
def execute(merge_request)
+ prepare_merge_request(merge_request)
+ merge_request.mark_as_unchecked! if merge_request.preparing?
+ end
+
+ private
+
+ def prepare_merge_request(merge_request)
event_service.open_mr(merge_request, current_user)
merge_request_activity_counter.track_create_mr_action(user: current_user)
notification_service.new_merge_request(merge_request, current_user)
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index ac84a13f437..f4d14aaf7d1 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -14,6 +14,8 @@ module MergeRequests
end
def after_create(issuable)
+ issuable.mark_as_preparing
+
# Add new items to MergeRequests::AfterCreateService if they can
# be performed in Sidekiq
NewMergeRequestWorker.perform_async(issuable.id, current_user.id)
diff --git a/app/views/admin/application_settings/_repository_storage.html.haml b/app/views/admin/application_settings/_repository_storage.html.haml
index 437189cf47f..0862d1bf0b6 100644
--- a/app/views/admin/application_settings/_repository_storage.html.haml
+++ b/app/views/admin/application_settings/_repository_storage.html.haml
@@ -18,9 +18,8 @@
= _('Enter weights for storages for new repositories.')
= link_to sprite_icon('question-o'), help_page_path('administration/repository_storage_paths')
.form-check
- = f.fields_for :repository_storages_weighted, storage_weights do |storage_form|
- - Gitlab.config.repositories.storages.keys.each do |storage|
- = storage_form.text_field storage, class: 'form-text-input'
- = storage_form.label storage, storage, class: 'label-bold form-check-label'
- %br
+ - storage_weights.each do |attribute|
+ = f.text_field attribute[:name], class: 'form-text-input', value: attribute[:value]
+ = f.label attribute[:label], attribute[:label], class: 'label-bold form-check-label'
+ %br
= f.submit _('Save changes'), class: "gl-button btn btn-success qa-save-changes-button"
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 2922ba4fb6e..9388c5fad6d 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -19,7 +19,7 @@
%p= _('Choose visibility level, enable/disable project features and their permissions, disable email notifications, and show default award emoji.')
.settings-content
- = form_for @project, remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f|
+ = form_for @project, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f|
%input{ name: 'update_section', type: 'hidden', value: 'js-shared-permissions' }
%template.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data_json(@project)
.js-project-permissions-form
@@ -36,7 +36,7 @@
.settings-content
= render_if_exists 'shared/promotions/promote_mr_features'
- = form_for @project, remote: true, html: { multipart: true, class: "merge-request-settings-form js-mr-settings-form" }, authenticity_token: true do |f|
+ = form_for @project, html: { multipart: true, class: "merge-request-settings-form js-mr-settings-form" }, authenticity_token: true do |f|
%input{ name: 'update_section', type: 'hidden', value: 'js-merge-request-settings' }
= render 'projects/merge_request_settings', form: f
= f.submit _('Save changes'), class: "btn gl-button btn-success rspec-save-merge-request-changes", data: { qa_selector: 'save_merge_request_changes_button' }
diff --git a/app/views/projects/settings/_general.html.haml b/app/views/projects/settings/_general.html.haml
index bd365123b81..d1a95886115 100644
--- a/app/views/projects/settings/_general.html.haml
+++ b/app/views/projects/settings/_general.html.haml
@@ -1,6 +1,5 @@
-= form_for [@project], remote: true, html: { multipart: true, class: "edit-project js-general-settings-form" }, authenticity_token: true do |f|
+= form_for [@project], html: { multipart: true, class: "edit-project js-general-settings-form" }, authenticity_token: true do |f|
%input{ name: 'update_section', type: 'hidden', value: 'js-general-settings' }
- = form_errors(@project)
%fieldset
.row
diff --git a/app/views/projects/update.js.haml b/app/views/projects/update.js.haml
deleted file mode 100644
index c5eecc900b2..00000000000
--- a/app/views/projects/update.js.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-- if @project.valid?
- :plain
- location.href = "#{edit_project_path(@project, anchor: params[:update_section])}";
- location.reload();
-- else
- :plain
- $(".flash-container").html("#{escape_javascript(render('errors'))}");
- $('.save-project-loader').hide();
- $('.project-edit-container').show();
- $('.edit-project .js-btn-success-general-project-settings').enable();
diff --git a/changelogs/unreleased/291012-preparing-mr-state.yml b/changelogs/unreleased/291012-preparing-mr-state.yml
new file mode 100644
index 00000000000..d52200f6697
--- /dev/null
+++ b/changelogs/unreleased/291012-preparing-mr-state.yml
@@ -0,0 +1,5 @@
+---
+title: Implement new preparing internal merge_status
+merge_request: 54900
+author:
+type: other
diff --git a/changelogs/unreleased/bump_auto_deploy_image_version.yml b/changelogs/unreleased/bump_auto_deploy_image_version.yml
new file mode 100644
index 00000000000..533f4072cd5
--- /dev/null
+++ b/changelogs/unreleased/bump_auto_deploy_image_version.yml
@@ -0,0 +1,4 @@
+title: Bump auto-deploy-image tag in Deploy.latest.gitlab-ci.yml to v2.6.0, which includes changes to ciliumnetworkpolicies.
+merge_request: 54983
+author:
+type: added
diff --git a/changelogs/unreleased/fix_repo_storage_weights_admin.yml b/changelogs/unreleased/fix_repo_storage_weights_admin.yml
deleted file mode 100644
index c82ef578e01..00000000000
--- a/changelogs/unreleased/fix_repo_storage_weights_admin.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow saving repository weights after a storage has been removed
-merge_request: 53803
-author:
-type: fixed
diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md
index b2f15c22429..ad16f3201e2 100644
--- a/doc/development/testing_guide/best_practices.md
+++ b/doc/development/testing_guide/best_practices.md
@@ -986,6 +986,7 @@ GitLab uses [factory_bot](https://github.com/thoughtbot/factory_bot) as a test f
See [issue #262624](https://gitlab.com/gitlab-org/gitlab/-/issues/262624) for further context.
- Factories don't have to be limited to `ActiveRecord` objects.
[See example](https://gitlab.com/gitlab-org/gitlab-foss/commit/0b8cefd3b2385a21cfed779bd659978c0402766d).
+- Factories and their traits should produce valid objects that are [verified by specs](https://gitlab.com/gitlab-org/gitlab/-/blob/master/spec/factories_spec.rb).
### Fixtures
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index e0361f6033c..52d42584407 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -720,8 +720,8 @@ To enable prevent project forking:
access each project's settings, and remove any project, all from the same screen.
- **Webhooks**: Configure [webhooks](../project/integrations/webhooks.md) for your group.
- **Kubernetes cluster integration**: Connect your GitLab group with [Kubernetes clusters](clusters/index.md).
-- **Audit Events**: View [Audit Events](../../administration/audit_events.md)
- for the group. **(PREMIUM SELF)**
+- **Audit Events**: View [Audit Events](../../administration/audit_events.md#group-events)
+ for the group.
- **Pipelines quota**: Keep track of the [pipeline quota](../admin_area/settings/continuous_integration.md) for the group.
- **Integrations**: Configure [integrations](../admin_area/settings/project_integration_management.md) for your group.
diff --git a/doc/user/project/clusters/add_eks_clusters.md b/doc/user/project/clusters/add_eks_clusters.md
index 141e8267d75..e329ec4f903 100644
--- a/doc/user/project/clusters/add_eks_clusters.md
+++ b/doc/user/project/clusters/add_eks_clusters.md
@@ -140,13 +140,11 @@ To create and add a new Kubernetes cluster to your project, group, or instance:
1. Click **Review policy**.
1. Enter a suitable name for this policy, and click **Create Policy**. You can now close this window.
-1. In the [IAM Management Console](https://console.aws.amazon.com/iam/home), create an EKS management IAM role.
- To do so, follow the [Amazon EKS cluster IAM role](https://docs.aws.amazon.com/eks/latest/userguide/service_IAM_role.html) instructions
- to create a IAM role suitable for managing the AWS EKS cluster's resources on your behalf.
+1. In the [IAM Management Console](https://console.aws.amazon.com/iam/home), create an **EKS IAM role** following the [Amazon EKS cluster IAM role instructions](https://docs.aws.amazon.com/eks/latest/userguide/service_IAM_role.html). This role should exist so that Kubernetes clusters managed by Amazon EKS can make calls to other AWS services on your behalf to manage the resources that you use with the service.
In addition to the policies that guide suggests, you must also include the `AmazonEKSClusterPolicy`
policy for this role in order for GitLab to manage the EKS cluster correctly.
-1. In the [IAM Management Console](https://console.aws.amazon.com/iam/home), create an IAM role:
- 1. From the left panel, select **Roles**.
+1. In the [IAM Management Console](https://console.aws.amazon.com/iam/home), create another IAM role which will be used by GitLab to authenticate with AWS. Follow these steps to create it:
+ 1. On the AWS IAM console, select **Roles** from the left panel.
1. Click **Create role**.
1. Under `Select type of trusted entity`, select **Another AWS account**.
1. Enter the Account ID from GitLab into the `Account ID` field.
diff --git a/lib/bitbucket/collection.rb b/lib/bitbucket/collection.rb
index 9c496daccaa..a6a31de3ce6 100644
--- a/lib/bitbucket/collection.rb
+++ b/lib/bitbucket/collection.rb
@@ -11,13 +11,5 @@ module Bitbucket
lazy
end
-
- def method_missing(method, *args)
- return super unless self.respond_to?(method)
-
- self.__send__(method, *args) do |item| # rubocop:disable GitlabSecurity/PublicSend
- block_given? ? yield(item) : item
- end
- end
end
end
diff --git a/lib/bitbucket_server/collection.rb b/lib/bitbucket_server/collection.rb
index f549acbd87f..2880f587202 100644
--- a/lib/bitbucket_server/collection.rb
+++ b/lib/bitbucket_server/collection.rb
@@ -35,13 +35,5 @@ module BitbucketServer
current_page + 1
end
-
- def method_missing(method, *args)
- return super unless self.respond_to?(method)
-
- self.__send__(method, *args) do |item| # rubocop:disable GitlabSecurity/PublicSend
- block_given? ? yield(item) : item
- end
- end
end
end
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml
index abf6bf1cb05..7b897bfe7c1 100644
--- a/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml
@@ -1,5 +1,5 @@
.auto-deploy:
- image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v2.0.0"
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v2.6.0"
dependencies: []
review:
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 46fd7065c11..c030018c6c2 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3467,6 +3467,9 @@ msgstr ""
msgid "An error occurred while loading project creation UI"
msgstr ""
+msgid "An error occurred while loading the access tokens form, please try again."
+msgstr ""
+
msgid "An error occurred while loading the data. Please try again."
msgstr ""
@@ -34923,9 +34926,6 @@ msgstr ""
msgid "can't be enabled because signed commits are required for this project"
msgstr ""
-msgid "can't include: %{invalid_storages}"
-msgstr ""
-
msgid "cannot be a date in the past"
msgstr ""
@@ -36332,12 +36332,6 @@ msgstr ""
msgid "v%{version} published %{timeAgo}"
msgstr ""
-msgid "value for '%{storage}' must be an integer"
-msgstr ""
-
-msgid "value for '%{storage}' must be between 0 and 100"
-msgstr ""
-
msgid "verify ownership"
msgstr ""
diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb
index 2b562e2dd64..71abf3191b8 100644
--- a/spec/controllers/admin/application_settings_controller_spec.rb
+++ b/spec/controllers/admin/application_settings_controller_spec.rb
@@ -144,10 +144,10 @@ RSpec.describe Admin::ApplicationSettingsController do
end
it 'updates repository_storages_weighted setting' do
- put :update, params: { application_setting: { repository_storages_weighted: { default: 75 } } }
+ put :update, params: { application_setting: { repository_storages_weighted_default: 75 } }
expect(response).to redirect_to(general_admin_application_settings_path)
- expect(ApplicationSetting.current.repository_storages_weighted).to eq('default' => 75)
+ expect(ApplicationSetting.current.repository_storages_weighted_default).to eq(75)
end
it 'updates kroki_formats setting' do
diff --git a/spec/factories_spec.rb b/spec/factories_spec.rb
index 4081ef7b2ce..edce92d1948 100644
--- a/spec/factories_spec.rb
+++ b/spec/factories_spec.rb
@@ -5,6 +5,10 @@ require 'spec_helper'
RSpec.describe 'factories' do
include Database::DatabaseHelpers
+ # https://gitlab.com/groups/gitlab-org/-/epics/5464 tracks the remaining
+ # skipped traits.
+ #
+ # Consider adding a code comment if a trait cannot produce a valid object.
def skipped_traits
[
[:audit_event, :unauthenticated],
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 249621f5835..52f39f65bd0 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -384,20 +384,7 @@ RSpec.describe 'Admin updates settings' do
click_button 'Save changes'
end
- expect(current_settings.repository_storages_weighted).to eq('default' => 50)
- end
-
- it 'still saves when settings are outdated' do
- current_settings.update_attribute :repository_storages_weighted, { 'default' => 100, 'outdated' => 100 }
-
- visit repository_admin_application_settings_path
-
- page.within('.as-repository-storage') do
- fill_in 'application_setting_repository_storages_weighted_default', with: 50
- click_button 'Save changes'
- end
-
- expect(current_settings.repository_storages_weighted).to eq('default' => 50)
+ expect(current_settings.repository_storages_weighted_default).to be 50
end
end
diff --git a/spec/frontend/access_tokens/components/projects_field_spec.js b/spec/frontend/access_tokens/components/projects_field_spec.js
index 7e9f06b9022..75b85fff8d7 100644
--- a/spec/frontend/access_tokens/components/projects_field_spec.js
+++ b/spec/frontend/access_tokens/components/projects_field_spec.js
@@ -1,6 +1,7 @@
-import { within } from '@testing-library/dom';
+import { within, fireEvent } from '@testing-library/dom';
import { mount } from '@vue/test-utils';
import ProjectsField from '~/access_tokens/components/projects_field.vue';
+import ProjectsTokenSelector from '~/access_tokens/components/projects_token_selector.vue';
describe('ProjectsField', () => {
let wrapper;
@@ -18,6 +19,10 @@ describe('ProjectsField', () => {
const queryByLabelText = (text) => within(wrapper.element).queryByLabelText(text);
const queryByText = (text) => within(wrapper.element).queryByText(text);
+ const findAllProjectsRadio = () => queryByLabelText('All projects');
+ const findSelectedProjectsRadio = () => queryByLabelText('Selected projects');
+ const findProjectsTokenSelector = () => wrapper.findComponent(ProjectsTokenSelector);
+ const findHiddenInput = () => wrapper.find('input[type="hidden"]');
beforeEach(() => {
createComponent();
@@ -34,25 +39,66 @@ describe('ProjectsField', () => {
});
it('renders "All projects" radio selected by default', () => {
- const allProjectsRadio = queryByLabelText('All projects');
+ const allProjectsRadio = findAllProjectsRadio();
expect(allProjectsRadio).not.toBe(null);
expect(allProjectsRadio.checked).toBe(true);
});
it('renders "Selected projects" radio unchecked by default', () => {
- const selectedProjectsRadio = queryByLabelText('Selected projects');
+ const selectedProjectsRadio = findSelectedProjectsRadio();
expect(selectedProjectsRadio).not.toBe(null);
expect(selectedProjectsRadio.checked).toBe(false);
});
+ it('renders `projects-token-selector` component', () => {
+ expect(findProjectsTokenSelector().exists()).toBe(true);
+ });
+
it('renders hidden input with correct `name` and `id` attributes', () => {
- expect(wrapper.find('input[type="hidden"]').attributes()).toEqual(
+ expect(findHiddenInput().attributes()).toEqual(
expect.objectContaining({
id: 'projects',
name: 'projects',
}),
);
});
+
+ describe('when `projects-token-selector` is focused', () => {
+ beforeEach(() => {
+ findProjectsTokenSelector().vm.$emit('focus');
+ });
+
+ it('auto selects the "Selected projects" radio', () => {
+ expect(findSelectedProjectsRadio().checked).toBe(true);
+ });
+
+ describe('when `projects-token-selector` is changed', () => {
+ beforeEach(() => {
+ findProjectsTokenSelector().vm.$emit('input', [
+ {
+ id: 1,
+ },
+ {
+ id: 2,
+ },
+ ]);
+ });
+
+ it('updates the hidden input value to a comma separated list of project IDs', () => {
+ expect(findHiddenInput().attributes('value')).toBe('1,2');
+ });
+
+ describe('when radio is changed back to "All projects"', () => {
+ beforeEach(() => {
+ fireEvent.click(findAllProjectsRadio());
+ });
+
+ it('removes the hidden input value', () => {
+ expect(findHiddenInput().attributes('value')).toBe('');
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/access_tokens/components/projects_token_selector_spec.js b/spec/frontend/access_tokens/components/projects_token_selector_spec.js
new file mode 100644
index 00000000000..712bba9e0ca
--- /dev/null
+++ b/spec/frontend/access_tokens/components/projects_token_selector_spec.js
@@ -0,0 +1,224 @@
+import {
+ GlAvatar,
+ GlAvatarLabeled,
+ GlIntersectionObserver,
+ GlToken,
+ GlTokenSelector,
+ GlLoadingIcon,
+} from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import produce from 'immer';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+
+import { getJSONFixture } from 'helpers/fixtures';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import ProjectsTokenSelector from '~/access_tokens/components/projects_token_selector.vue';
+import getProjectsQuery from '~/access_tokens/graphql/queries/get_projects.query.graphql';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+
+describe('ProjectsTokenSelector', () => {
+ const getProjectsQueryResponse = getJSONFixture(
+ 'graphql/projects/access_tokens/get_projects.query.graphql.json',
+ );
+ const getProjectsQueryResponsePage2 = produce(
+ getProjectsQueryResponse,
+ (getProjectsQueryResponseDraft) => {
+ /* eslint-disable no-param-reassign */
+ getProjectsQueryResponseDraft.data.projects.pageInfo.hasNextPage = false;
+ getProjectsQueryResponseDraft.data.projects.pageInfo.endCursor = null;
+ getProjectsQueryResponseDraft.data.projects.nodes.splice(1, 1);
+ getProjectsQueryResponseDraft.data.projects.nodes[0].id = 'gid://gitlab/Project/100';
+ /* eslint-enable no-param-reassign */
+ },
+ );
+
+ const runDebounce = () => jest.runAllTimers();
+
+ const { pageInfo, nodes: projects } = getProjectsQueryResponse.data.projects;
+ const project1 = projects[0];
+ const project2 = projects[1];
+
+ let wrapper;
+
+ let resolveGetProjectsQuery;
+ const getProjectsQueryRequestHandler = jest.fn(
+ () =>
+ new Promise((resolve) => {
+ resolveGetProjectsQuery = resolve;
+ }),
+ );
+
+ const createComponent = ({
+ propsData = {},
+ apolloProvider = createMockApollo([[getProjectsQuery, getProjectsQueryRequestHandler]]),
+ resolveQueries = true,
+ } = {}) => {
+ Vue.use(VueApollo);
+
+ wrapper = extendedWrapper(
+ mount(ProjectsTokenSelector, {
+ apolloProvider,
+ propsData: {
+ selectedProjects: [],
+ ...propsData,
+ },
+ stubs: ['gl-intersection-observer'],
+ }),
+ );
+
+ runDebounce();
+
+ if (resolveQueries) {
+ resolveGetProjectsQuery(getProjectsQueryResponse);
+
+ return waitForPromises();
+ }
+
+ return Promise.resolve();
+ };
+
+ const findTokenSelector = () => wrapper.findComponent(GlTokenSelector);
+ const findTokenSelectorInput = () => findTokenSelector().find('input[type="text"]');
+ const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
+
+ it('renders dropdown items with project avatars', async () => {
+ await createComponent();
+
+ wrapper.findAllComponents(GlAvatarLabeled).wrappers.forEach((avatarLabeledWrapper, index) => {
+ const project = projects[index];
+
+ expect(avatarLabeledWrapper.attributes()).toEqual(
+ expect.objectContaining({
+ 'entity-id': `${getIdFromGraphQLId(project.id)}`,
+ 'entity-name': project.name,
+ ...(project.avatarUrl && { src: project.avatarUrl }),
+ }),
+ );
+
+ expect(avatarLabeledWrapper.props()).toEqual(
+ expect.objectContaining({
+ label: project.name,
+ subLabel: project.nameWithNamespace,
+ }),
+ );
+ });
+ });
+
+ it('renders tokens with project avatars', () => {
+ createComponent({
+ propsData: {
+ selectedProjects: [{ ...project2, id: getIdFromGraphQLId(project2.id) }],
+ },
+ });
+
+ const token = wrapper.findComponent(GlToken);
+ const avatar = token.findComponent(GlAvatar);
+
+ expect(token.text()).toContain(project2.nameWithNamespace);
+ expect(avatar.attributes('src')).toBe(project2.avatarUrl);
+ expect(avatar.props()).toEqual(
+ expect.objectContaining({
+ entityId: getIdFromGraphQLId(project2.id),
+ entityName: project2.name,
+ }),
+ );
+ });
+
+ describe('when `enter` key is pressed', () => {
+ it('calls `preventDefault` so form is not submitted when user selects a project from the dropdown', () => {
+ createComponent();
+
+ const event = {
+ preventDefault: jest.fn(),
+ };
+
+ findTokenSelectorInput().trigger('keydown.enter', event);
+
+ expect(event.preventDefault).toHaveBeenCalled();
+ });
+ });
+
+ describe('when text input is typed in', () => {
+ const searchTerm = 'foo bar';
+
+ beforeEach(async () => {
+ await createComponent();
+
+ await findTokenSelectorInput().setValue(searchTerm);
+ runDebounce();
+ });
+
+ it('makes GraphQL request with `search` variable set', async () => {
+ expect(getProjectsQueryRequestHandler).toHaveBeenLastCalledWith({
+ search: searchTerm,
+ after: null,
+ first: 20,
+ });
+ });
+
+ it('sets loading state while waiting for GraphQL request to resolve', async () => {
+ expect(findTokenSelector().props('loading')).toBe(true);
+
+ resolveGetProjectsQuery(getProjectsQueryResponse);
+ await waitForPromises();
+
+ expect(findTokenSelector().props('loading')).toBe(false);
+ });
+ });
+
+ describe('when there is a next page of projects and user scrolls to the bottom of the dropdown', () => {
+ beforeEach(async () => {
+ await createComponent();
+
+ findIntersectionObserver().vm.$emit('appear');
+ });
+
+ it('makes GraphQL request with `after` variable set', async () => {
+ expect(getProjectsQueryRequestHandler).toHaveBeenLastCalledWith({
+ after: pageInfo.endCursor,
+ first: 20,
+ search: '',
+ });
+ });
+
+ it('displays loading icon while waiting for GraphQL request to resolve', async () => {
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+
+ resolveGetProjectsQuery(getProjectsQueryResponsePage2);
+ await waitForPromises();
+
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
+ });
+ });
+
+ describe('when there is not a next page of projects', () => {
+ it('does not render `GlIntersectionObserver`', async () => {
+ createComponent({ resolveQueries: false });
+
+ resolveGetProjectsQuery(getProjectsQueryResponsePage2);
+ await waitForPromises();
+
+ expect(findIntersectionObserver().exists()).toBe(false);
+ });
+ });
+
+ describe('when `GlTokenSelector` emits `input` event', () => {
+ it('emits `input` event used by `v-model`', () => {
+ findTokenSelector().vm.$emit('input', project1);
+
+ expect(wrapper.emitted('input')[0]).toEqual([project1]);
+ });
+ });
+
+ describe('when `GlTokenSelector` emits `focus` event', () => {
+ it('emits `focus` event', () => {
+ const event = { fakeEvent: 'foo' };
+ findTokenSelector().vm.$emit('focus', event);
+
+ expect(wrapper.emitted('focus')[0]).toEqual([event]);
+ });
+ });
+});
diff --git a/spec/frontend/access_tokens/index_spec.js b/spec/frontend/access_tokens/index_spec.js
index 2225e23e09c..7aeca263f21 100644
--- a/spec/frontend/access_tokens/index_spec.js
+++ b/spec/frontend/access_tokens/index_spec.js
@@ -1,19 +1,27 @@
import { createWrapper } from '@vue/test-utils';
-
-import waitForPromises from 'helpers/wait_for_promises';
+import Vue from 'vue';
import { initExpiresAtField, initProjectsField } from '~/access_tokens';
-import ExpiresAtField from '~/access_tokens/components/expires_at_field.vue';
-import ProjectsField from '~/access_tokens/components/projects_field.vue';
+import * as ExpiresAtField from '~/access_tokens/components/expires_at_field.vue';
+import * as ProjectsField from '~/access_tokens/components/projects_field.vue';
describe('access tokens', () => {
+ const FakeComponent = Vue.component('FakeComponent', {
+ props: {
+ inputAttrs: {
+ type: Object,
+ required: true,
+ },
+ },
+ render: () => null,
+ });
+
beforeEach(() => {
window.gon = { features: { personalAccessTokensScopedToProjects: true } };
});
afterEach(() => {
document.body.innerHTML = '';
- window.gon = {};
});
describe.each`
@@ -34,15 +42,17 @@ describe('access tokens', () => {
mountEl.appendChild(input);
document.body.appendChild(mountEl);
- });
- it(`mounts component and sets \`inputAttrs\` prop`, async () => {
- const wrapper = createWrapper(initFunction());
+ // Mock component so we don't have to deal with mocking Apollo
+ // eslint-disable-next-line no-param-reassign
+ expectedComponent.default = FakeComponent;
+ });
- // Wait for dynamic imports to resolve
- await waitForPromises();
+ it('mounts component and sets `inputAttrs` prop', async () => {
+ const vueInstance = await initFunction();
- const component = wrapper.findComponent(expectedComponent);
+ const wrapper = createWrapper(vueInstance);
+ const component = wrapper.findComponent(FakeComponent);
expect(component.exists()).toBe(true);
expect(component.props('inputAttrs')).toEqual({
diff --git a/spec/frontend/fixtures/projects.rb b/spec/frontend/fixtures/projects.rb
index aa2f7dbed36..778ae218160 100644
--- a/spec/frontend/fixtures/projects.rb
+++ b/spec/frontend/fixtures/projects.rb
@@ -3,13 +3,14 @@
require 'spec_helper'
RSpec.describe 'Projects (JavaScript fixtures)', type: :controller do
+ include ApiHelpers
include JavaScriptFixturesHelpers
runners_token = 'runnerstoken:intabulasreferre'
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
- let(:project) { create(:project, namespace: namespace, path: 'builds-project', runners_token: runners_token) }
- let(:project_with_repo) { create(:project, :repository, description: 'Code and stuff') }
+ let(:project) { create(:project, namespace: namespace, path: 'builds-project', runners_token: runners_token, avatar: fixture_file_upload('spec/fixtures/dk.png', 'image/png')) }
+ let(:project_with_repo) { create(:project, :repository, description: 'Code and stuff', avatar: fixture_file_upload('spec/fixtures/dk.png', 'image/png')) }
let(:project_variable_populated) { create(:project, namespace: namespace, path: 'builds-project2', runners_token: runners_token) }
let(:user) { project.owner }
@@ -22,7 +23,6 @@ RSpec.describe 'Projects (JavaScript fixtures)', type: :controller do
before do
project_with_repo.add_maintainer(user)
sign_in(user)
- allow(SecureRandom).to receive(:hex).and_return('securerandomhex:thereisnospoon')
end
after do
@@ -48,4 +48,31 @@ RSpec.describe 'Projects (JavaScript fixtures)', type: :controller do
expect(response).to be_successful
end
end
+
+ describe GraphQL::Query, type: :request do
+ include GraphqlHelpers
+
+ context 'access token projects query' do
+ before do
+ project_variable_populated.add_maintainer(user)
+ end
+
+ before(:all) do
+ clean_frontend_fixtures('graphql/projects/access_tokens')
+ end
+
+ fragment_paths = ['graphql_shared/fragments/pageInfo.fragment.graphql']
+ base_input_path = 'access_tokens/graphql/queries/'
+ base_output_path = 'graphql/projects/access_tokens/'
+ query_name = 'get_projects.query.graphql'
+
+ it "#{base_output_path}#{query_name}.json" do
+ query = get_graphql_query_as_string("#{base_input_path}#{query_name}", fragment_paths)
+
+ post_graphql(query, current_user: user, variables: { search: '', first: 2 })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+ end
end
diff --git a/spec/helpers/application_settings_helper_spec.rb b/spec/helpers/application_settings_helper_spec.rb
index c74ee3ce0ec..2cd01451e0d 100644
--- a/spec/helpers/application_settings_helper_spec.rb
+++ b/spec/helpers/application_settings_helper_spec.rb
@@ -130,15 +130,20 @@ RSpec.describe ApplicationSettingsHelper do
before do
helper.instance_variable_set(:@application_setting, application_setting)
stub_storage_settings({ 'default': {}, 'storage_1': {}, 'storage_2': {} })
+ allow(ApplicationSetting).to receive(:repository_storages_weighted_attributes).and_return(
+ [:repository_storages_weighted_default,
+ :repository_storages_weighted_storage_1,
+ :repository_storages_weighted_storage_2])
+
stub_application_setting(repository_storages_weighted: { 'default' => 100, 'storage_1' => 50, 'storage_2' => nil })
end
it 'returns storages correctly' do
- expect(helper.storage_weights).to eq(OpenStruct.new(
- default: 100,
- storage_1: 50,
- storage_2: 0
- ))
+ expect(helper.storage_weights).to eq([
+ { name: :repository_storages_weighted_default, label: 'default', value: 100 },
+ { name: :repository_storages_weighted_storage_1, label: 'storage_1', value: 50 },
+ { name: :repository_storages_weighted_storage_2, label: 'storage_2', value: 0 }
+ ])
end
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 808932ce7e4..22bdaabce23 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -105,14 +105,14 @@ RSpec.describe ApplicationSetting do
it { is_expected.not_to allow_value(false).for(:hashed_storage_enabled) }
- it { is_expected.to allow_value('default' => 0).for(:repository_storages_weighted) }
- it { is_expected.to allow_value('default' => 50).for(:repository_storages_weighted) }
- it { is_expected.to allow_value('default' => 100).for(:repository_storages_weighted) }
- it { is_expected.to allow_value('default' => '90').for(:repository_storages_weighted) }
- it { is_expected.to allow_value('default' => nil).for(:repository_storages_weighted) }
- it { is_expected.not_to allow_value('default' => -1).for(:repository_storages_weighted).with_message("value for 'default' must be between 0 and 100") }
- it { is_expected.not_to allow_value('default' => 101).for(:repository_storages_weighted).with_message("value for 'default' must be between 0 and 100") }
- it { is_expected.not_to allow_value('default' => 100, shouldntexist: 50).for(:repository_storages_weighted).with_message("can't include: shouldntexist") }
+ it { is_expected.not_to allow_value(101).for(:repository_storages_weighted_default) }
+ it { is_expected.to allow_value('90').for(:repository_storages_weighted_default) }
+ it { is_expected.not_to allow_value(-1).for(:repository_storages_weighted_default) }
+ it { is_expected.to allow_value(100).for(:repository_storages_weighted_default) }
+ it { is_expected.to allow_value(0).for(:repository_storages_weighted_default) }
+ it { is_expected.to allow_value(50).for(:repository_storages_weighted_default) }
+ it { is_expected.to allow_value(nil).for(:repository_storages_weighted_default) }
+ it { is_expected.not_to allow_value({ default: 100, shouldntexist: 50 }).for(:repository_storages_weighted) }
it { is_expected.to allow_value(400).for(:notes_create_limit) }
it { is_expected.not_to allow_value('two').for(:notes_create_limit) }
@@ -984,6 +984,12 @@ RSpec.describe ApplicationSetting do
it_behaves_like 'application settings examples'
+ describe 'repository_storages_weighted_attributes' do
+ it 'returns the keys for repository_storages_weighted' do
+ expect(subject.class.repository_storages_weighted_attributes).to eq([:repository_storages_weighted_default])
+ end
+ end
+
describe 'kroki_format_supported?' do
it 'returns true when Excalidraw is enabled' do
subject.kroki_formats_excalidraw = true
@@ -1027,4 +1033,11 @@ RSpec.describe ApplicationSetting do
expect(subject.kroki_formats_excalidraw).to eq(true)
end
end
+
+ it 'does not allow to set weight for non existing storage' do
+ setting.repository_storages_weighted = { invalid_storage: 100 }
+
+ expect(setting).not_to be_valid
+ expect(setting.errors.messages[:repository_storages_weighted]).to match_array(["can't include: invalid_storage"])
+ end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 2157214be23..176667c9361 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -2885,6 +2885,11 @@ RSpec.describe MergeRequest, factory_default: :keep do
describe '#mergeable?' do
subject { build_stubbed(:merge_request) }
+ it 'returns false if still preparing' do
+ expect(subject).to receive(:preparing?) { true }
+ expect(subject.mergeable?).to be_falsey
+ end
+
it 'returns false if #mergeable_state? is false' do
expect(subject).to receive(:mergeable_state?) { false }
@@ -3075,6 +3080,7 @@ RSpec.describe MergeRequest, factory_default: :keep do
subject { build(:merge_request, merge_status: status) }
where(:status, :public_status) do
+ 'preparing' | 'checking'
'cannot_be_merged_rechecking' | 'checking'
'checking' | 'checking'
'cannot_be_merged' | 'cannot_be_merged'
diff --git a/spec/services/merge_requests/after_create_service_spec.rb b/spec/services/merge_requests/after_create_service_spec.rb
index f21feb70bc5..22af346634f 100644
--- a/spec/services/merge_requests/after_create_service_spec.rb
+++ b/spec/services/merge_requests/after_create_service_spec.rb
@@ -67,5 +67,27 @@ RSpec.describe MergeRequests::AfterCreateService do
it_behaves_like 'records an onboarding progress action', :merge_request_created do
let(:namespace) { merge_request.target_project.namespace }
end
+
+ context 'when merge request is in unchecked state' do
+ before do
+ merge_request.mark_as_unchecked!
+ execute_service
+ end
+
+ it 'does not change its state' do
+ expect(merge_request.reload).to be_unchecked
+ end
+ end
+
+ context 'when merge request is in preparing state' do
+ before do
+ merge_request.mark_as_preparing!
+ execute_service
+ end
+
+ it 'marks the merge request as unchecked' do
+ expect(merge_request.reload).to be_unchecked
+ end
+ end
end
end
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index 4f47a22b07c..e63e678bdd8 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -67,6 +67,10 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do
expect(Event.where(attributes).count).to eq(1)
end
+ it 'sets the merge_status to preparing' do
+ expect(merge_request.reload).to be_preparing
+ end
+
describe 'when marked with /wip' do
context 'in title and in description' do
let(:opts) do
diff --git a/spec/support/shared_examples/models/application_setting_shared_examples.rb b/spec/support/shared_examples/models/application_setting_shared_examples.rb
index 60a02d85a1e..92fd4363134 100644
--- a/spec/support/shared_examples/models/application_setting_shared_examples.rb
+++ b/spec/support/shared_examples/models/application_setting_shared_examples.rb
@@ -289,7 +289,6 @@ RSpec.shared_examples 'application settings examples' do
describe '#pick_repository_storage' do
before do
- allow(Gitlab.config.repositories.storages).to receive(:keys).and_return(%w(default backup))
allow(setting).to receive(:repository_storages_weighted).and_return({ 'default' => 20, 'backup' => 80 })
end
@@ -305,19 +304,15 @@ RSpec.shared_examples 'application settings examples' do
describe '#normalized_repository_storage_weights' do
using RSpec::Parameterized::TableSyntax
- where(:config_storages, :storages, :normalized) do
- %w(default backup) | { 'default' => 0, 'backup' => 100 } | { 'default' => 0.0, 'backup' => 1.0 }
- %w(default backup) | { 'default' => 100, 'backup' => 100 } | { 'default' => 0.5, 'backup' => 0.5 }
- %w(default backup) | { 'default' => 20, 'backup' => 80 } | { 'default' => 0.2, 'backup' => 0.8 }
- %w(default backup) | { 'default' => 0, 'backup' => 0 } | { 'default' => 0.0, 'backup' => 0.0 }
- %w(default) | { 'default' => 0, 'backup' => 100 } | { 'default' => 0.0 }
- %w(default) | { 'default' => 100, 'backup' => 100 } | { 'default' => 1.0 }
- %w(default) | { 'default' => 20, 'backup' => 80 } | { 'default' => 1.0 }
+ where(:storages, :normalized) do
+ { 'default' => 0, 'backup' => 100 } | { 'default' => 0.0, 'backup' => 1.0 }
+ { 'default' => 100, 'backup' => 100 } | { 'default' => 0.5, 'backup' => 0.5 }
+ { 'default' => 20, 'backup' => 80 } | { 'default' => 0.2, 'backup' => 0.8 }
+ { 'default' => 0, 'backup' => 0 } | { 'default' => 0.0, 'backup' => 0.0 }
end
with_them do
before do
- allow(Gitlab.config.repositories.storages).to receive(:keys).and_return(config_storages)
allow(setting).to receive(:repository_storages_weighted).and_return(storages)
end
diff --git a/spec/views/admin/application_settings/_repository_storage.html.haml_spec.rb b/spec/views/admin/application_settings/_repository_storage.html.haml_spec.rb
index dc8f259eb56..2915fe1964f 100644
--- a/spec/views/admin/application_settings/_repository_storage.html.haml_spec.rb
+++ b/spec/views/admin/application_settings/_repository_storage.html.haml_spec.rb
@@ -3,49 +3,34 @@
require 'spec_helper'
RSpec.describe 'admin/application_settings/_repository_storage.html.haml' do
- let(:app_settings) { build(:application_setting, repository_storages_weighted: repository_storages_weighted) }
+ let(:app_settings) { create(:application_setting) }
+ let(:repository_storages_weighted_attributes) { [:repository_storages_weighted_default, :repository_storages_weighted_mepmep, :repository_storages_weighted_foobar]}
+ let(:repository_storages_weighted) do
+ {
+ "default" => 100,
+ "mepmep" => 50
+ }
+ end
before do
- stub_storage_settings({ 'default': {}, 'mepmep': {}, 'foobar': {} })
+ allow(app_settings).to receive(:repository_storages_weighted).and_return(repository_storages_weighted)
+ allow(app_settings).to receive(:repository_storages_weighted_mepmep).and_return(100)
+ allow(app_settings).to receive(:repository_storages_weighted_foobar).and_return(50)
assign(:application_setting, app_settings)
+ allow(ApplicationSetting).to receive(:repository_storages_weighted_attributes).and_return(repository_storages_weighted_attributes)
end
- context 'additional storage config' do
- let(:repository_storages_weighted) do
- {
- 'default' => 100,
- 'mepmep' => 50
- }
- end
-
+ context 'when multiple storages are available' do
it 'lists them all' do
render
- Gitlab.config.repositories.storages.keys.each do |storage_name|
+ # lists storages that are saved with weights
+ repository_storages_weighted.each do |storage_name, storage_weight|
expect(rendered).to have_content(storage_name)
end
+ # lists storage not saved with weight
expect(rendered).to have_content('foobar')
end
end
-
- context 'fewer storage configs' do
- let(:repository_storages_weighted) do
- {
- 'default' => 100,
- 'mepmep' => 50,
- 'something_old' => 100
- }
- end
-
- it 'lists only configured storages' do
- render
-
- Gitlab.config.repositories.storages.keys.each do |storage_name|
- expect(rendered).to have_content(storage_name)
- end
-
- expect(rendered).not_to have_content('something_old')
- end
- end
end