Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
9578c9f9e8
commit
63b3a14f15
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 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(),
|
||||
});
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
render(h) {
|
||||
return h(ProjectsField, {
|
||||
props: {
|
||||
inputAttrs,
|
||||
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.',
|
||||
),
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
new Profile(); // eslint-disable-line no-new
|
||||
|
||||
initSearchSettings();
|
||||
// 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
|
||||
|
||||
initSearchSettings();
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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' }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Implement new preparing internal merge_status
|
||||
merge_request: 54900
|
||||
author:
|
||||
type: other
|
|
@ -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
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Allow saving repository weights after a storage has been removed
|
||||
merge_request: 53803
|
||||
author:
|
||||
type: fixed
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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('');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
|
||||
// Mock component so we don't have to deal with mocking Apollo
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
expectedComponent.default = FakeComponent;
|
||||
});
|
||||
|
||||
it(`mounts component and sets \`inputAttrs\` prop`, async () => {
|
||||
const wrapper = createWrapper(initFunction());
|
||||
it('mounts component and sets `inputAttrs` prop', async () => {
|
||||
const vueInstance = await initFunction();
|
||||
|
||||
// Wait for dynamic imports to resolve
|
||||
await waitForPromises();
|
||||
|
||||
const component = wrapper.findComponent(expectedComponent);
|
||||
const wrapper = createWrapper(vueInstance);
|
||||
const component = wrapper.findComponent(FakeComponent);
|
||||
|
||||
expect(component.exists()).toBe(true);
|
||||
expect(component.props('inputAttrs')).toEqual({
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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) }
|
||||
|
||||
before do
|
||||
stub_storage_settings({ 'default': {}, 'mepmep': {}, 'foobar': {} })
|
||||
assign(:application_setting, app_settings)
|
||||
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
|
||||
|
||||
context 'additional storage config' do
|
||||
let(:repository_storages_weighted) do
|
||||
{
|
||||
'default' => 100,
|
||||
'mepmep' => 50
|
||||
}
|
||||
end
|
||||
before do
|
||||
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 '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
|
||||
|
|
Loading…
Reference in New Issue