Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
c8eee7e7e8
commit
1086ac5177
52 changed files with 805 additions and 139 deletions
|
@ -71,6 +71,7 @@ export default {
|
|||
selectedDesigns: [],
|
||||
isDraggingDesign: false,
|
||||
reorderedDesigns: null,
|
||||
isReorderingInProgress: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -277,6 +278,7 @@ export default {
|
|||
return variables;
|
||||
},
|
||||
reorderDesigns({ moved: { newIndex, element } }) {
|
||||
this.isReorderingInProgress = true;
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: moveDesignMutation,
|
||||
|
@ -287,6 +289,9 @@ export default {
|
|||
})
|
||||
.catch(() => {
|
||||
createFlash(MOVE_DESIGN_ERROR);
|
||||
})
|
||||
.finally(() => {
|
||||
this.isReorderingInProgress = false;
|
||||
});
|
||||
},
|
||||
onDesignMove(designs) {
|
||||
|
@ -358,7 +363,7 @@ export default {
|
|||
<vue-draggable
|
||||
v-else
|
||||
:value="designs"
|
||||
:disabled="!isLatestVersion"
|
||||
:disabled="!isLatestVersion || isReorderingInProgress"
|
||||
v-bind="$options.dragOptions"
|
||||
tag="ol"
|
||||
draggable=".js-design-tile"
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
<script>
|
||||
/* eslint-disable @gitlab/vue-require-i18n-strings, vue/no-v-html */
|
||||
import { GlTooltipDirective } from '@gitlab/ui';
|
||||
/* eslint-disable @gitlab/vue-require-i18n-strings */
|
||||
import { GlSprintf, GlTooltipDirective } from '@gitlab/ui';
|
||||
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
export default {
|
||||
|
@ -11,6 +10,7 @@ export default {
|
|||
|
||||
components: {
|
||||
GlModal: DeprecatedModal2,
|
||||
GlSprintf,
|
||||
},
|
||||
|
||||
directives: {
|
||||
|
@ -24,27 +24,6 @@ export default {
|
|||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
noStopActionMessage() {
|
||||
return sprintf(
|
||||
s__(
|
||||
`Environments|Note that this action will stop the environment,
|
||||
but it will %{emphasisStart}not%{emphasisEnd} have an effect on any existing deployment
|
||||
due to no “stop environment action” being defined
|
||||
in the %{ciConfigLinkStart}.gitlab-ci.yml%{ciConfigLinkEnd} file.`,
|
||||
),
|
||||
{
|
||||
emphasisStart: '<strong>',
|
||||
emphasisEnd: '</strong>',
|
||||
ciConfigLinkStart:
|
||||
'<a href="https://docs.gitlab.com/ee/ci/yaml/" target="_blank" rel="noopener noreferrer">',
|
||||
ciConfigLinkEnd: '</a>',
|
||||
},
|
||||
false,
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onSubmit() {
|
||||
eventHub.$emit('stopEnvironment', this.environment);
|
||||
|
@ -72,7 +51,25 @@ export default {
|
|||
<p>{{ s__('Environments|Are you sure you want to stop this environment?') }}</p>
|
||||
|
||||
<div v-if="!environment.has_stop_action" class="warning_message">
|
||||
<p v-html="noStopActionMessage"></p>
|
||||
<p>
|
||||
<gl-sprintf
|
||||
:message="
|
||||
s__(`Environments|Note that this action will stop the environment,
|
||||
but it will %{emphasisStart}not%{emphasisEnd} have an effect on any existing deployment
|
||||
due to no “stop environment action” being defined
|
||||
in the %{ciConfigLinkStart}.gitlab-ci.yml%{ciConfigLinkEnd} file.`)
|
||||
"
|
||||
>
|
||||
<template #emphasis="{ content }">
|
||||
<strong>{{ content }}</strong>
|
||||
</template>
|
||||
<template #ciConfigLink="{ content }">
|
||||
<a href="https://docs.gitlab.com/ee/ci/yaml/" target="_blank" rel="noopener noreferrer">
|
||||
{{ content }}</a
|
||||
>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
<a
|
||||
href="https://docs.gitlab.com/ee/ci/environments.html#stopping-an-environment"
|
||||
target="_blank"
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script>
|
||||
/* eslint-disable vue/no-v-html */
|
||||
import { escape } from 'lodash';
|
||||
import { mapState, mapGetters, createNamespacedHelpers } from 'vuex';
|
||||
import { sprintf, s__ } from '~/locale';
|
||||
import { GlSprintf } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import consts from '../../stores/modules/commit/constants';
|
||||
import RadioGroup from './radio_group.vue';
|
||||
import NewMergeRequestOption from './new_merge_request_option.vue';
|
||||
|
@ -13,6 +13,7 @@ const { mapState: mapCommitState, mapActions: mapCommitActions } = createNamespa
|
|||
|
||||
export default {
|
||||
components: {
|
||||
GlSprintf,
|
||||
RadioGroup,
|
||||
NewMergeRequestOption,
|
||||
},
|
||||
|
@ -20,12 +21,8 @@ export default {
|
|||
...mapState(['currentBranchId', 'changedFiles', 'stagedFiles']),
|
||||
...mapCommitState(['commitAction']),
|
||||
...mapGetters(['currentBranch', 'emptyRepo', 'canPushToBranch']),
|
||||
commitToCurrentBranchText() {
|
||||
return sprintf(
|
||||
s__('IDE|Commit to %{branchName} branch'),
|
||||
{ branchName: `<strong class="monospace">${escape(this.currentBranchId)}</strong>` },
|
||||
false,
|
||||
);
|
||||
currentBranchText() {
|
||||
return escape(this.currentBranchId);
|
||||
},
|
||||
containsStagedChanges() {
|
||||
return this.changedFiles.length > 0 && this.stagedFiles.length > 0;
|
||||
|
@ -77,11 +74,13 @@ export default {
|
|||
:disabled="!canPushToBranch"
|
||||
:title="$options.currentBranchPermissionsTooltip"
|
||||
>
|
||||
<span
|
||||
class="ide-option-label"
|
||||
data-qa-selector="commit_to_current_branch_radio"
|
||||
v-html="commitToCurrentBranchText"
|
||||
></span>
|
||||
<span class="ide-option-label" data-qa-selector="commit_to_current_branch_radio">
|
||||
<gl-sprintf :message="s__('IDE|Commit to %{branchName} branch')">
|
||||
<template #branchName>
|
||||
<strong class="monospace">{{ currentBranchText }}</strong>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</span>
|
||||
</radio-group>
|
||||
<template v-if="!emptyRepo">
|
||||
<radio-group
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
import { escape, find, countBy } from 'lodash';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { deprecatedCreateFlash as Flash } from '~/flash';
|
||||
import { n__, s__, __ } from '~/locale';
|
||||
import { LEVEL_TYPES, LEVEL_ID_PROP, ACCESS_LEVEL_NONE } from './constants';
|
||||
import { n__, s__, __, sprintf } from '~/locale';
|
||||
import { LEVEL_TYPES, LEVEL_ID_PROP, ACCESS_LEVELS, ACCESS_LEVEL_NONE } from './constants';
|
||||
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
|
||||
|
||||
export default class AccessDropdown {
|
||||
|
@ -11,6 +11,7 @@ export default class AccessDropdown {
|
|||
const { $dropdown, accessLevel, accessLevelsData, hasLicense = true } = options;
|
||||
this.options = options;
|
||||
this.hasLicense = hasLicense;
|
||||
this.deployKeysOnProtectedBranchesEnabled = gon.features.deployKeysOnProtectedBranches;
|
||||
this.groups = [];
|
||||
this.accessLevel = accessLevel;
|
||||
this.accessLevelsData = accessLevelsData.roles;
|
||||
|
@ -18,6 +19,7 @@ export default class AccessDropdown {
|
|||
this.$wrap = this.$dropdown.closest(`.${this.accessLevel}-container`);
|
||||
this.usersPath = '/-/autocomplete/users.json';
|
||||
this.groupsPath = '/-/autocomplete/project_groups.json';
|
||||
this.deployKeysPath = '/-/autocomplete/deploy_keys_with_owners.json';
|
||||
this.defaultLabel = this.$dropdown.data('defaultLabel');
|
||||
|
||||
this.setSelectedItems([]);
|
||||
|
@ -146,6 +148,8 @@ export default class AccessDropdown {
|
|||
obj.access_level = item.access_level;
|
||||
} else if (item.type === LEVEL_TYPES.USER) {
|
||||
obj.user_id = item.user_id;
|
||||
} else if (item.type === LEVEL_TYPES.DEPLOY_KEY) {
|
||||
obj.deploy_key_id = item.deploy_key_id;
|
||||
} else if (item.type === LEVEL_TYPES.GROUP) {
|
||||
obj.group_id = item.group_id;
|
||||
}
|
||||
|
@ -177,6 +181,9 @@ export default class AccessDropdown {
|
|||
case LEVEL_TYPES.GROUP:
|
||||
comparator = LEVEL_ID_PROP.GROUP;
|
||||
break;
|
||||
case LEVEL_TYPES.DEPLOY_KEY:
|
||||
comparator = LEVEL_ID_PROP.DEPLOY_KEY;
|
||||
break;
|
||||
case LEVEL_TYPES.USER:
|
||||
comparator = LEVEL_ID_PROP.USER;
|
||||
break;
|
||||
|
@ -218,6 +225,11 @@ export default class AccessDropdown {
|
|||
group_id: selectedItem.id,
|
||||
type: LEVEL_TYPES.GROUP,
|
||||
};
|
||||
} else if (selectedItem.type === LEVEL_TYPES.DEPLOY_KEY) {
|
||||
itemToAdd = {
|
||||
deploy_key_id: selectedItem.id,
|
||||
type: LEVEL_TYPES.DEPLOY_KEY,
|
||||
};
|
||||
}
|
||||
|
||||
this.items.push(itemToAdd);
|
||||
|
@ -233,11 +245,12 @@ export default class AccessDropdown {
|
|||
return true;
|
||||
}
|
||||
|
||||
if (item.type === LEVEL_TYPES.USER && item.user_id === itemToDelete.id) {
|
||||
index = i;
|
||||
} else if (item.type === LEVEL_TYPES.ROLE && item.access_level === itemToDelete.id) {
|
||||
index = i;
|
||||
} else if (item.type === LEVEL_TYPES.GROUP && item.group_id === itemToDelete.id) {
|
||||
if (
|
||||
(item.type === LEVEL_TYPES.USER && item.user_id === itemToDelete.id) ||
|
||||
(item.type === LEVEL_TYPES.ROLE && item.access_level === itemToDelete.id) ||
|
||||
(item.type === LEVEL_TYPES.DEPLOY_KEY && item.deploy_key_id === itemToDelete.id) ||
|
||||
(item.type === LEVEL_TYPES.GROUP && item.group_id === itemToDelete.id)
|
||||
) {
|
||||
index = i;
|
||||
}
|
||||
|
||||
|
@ -289,6 +302,10 @@ export default class AccessDropdown {
|
|||
labelPieces.push(n__('1 user', '%d users', counts[LEVEL_TYPES.USER]));
|
||||
}
|
||||
|
||||
if (counts[LEVEL_TYPES.DEPLOY_KEY] > 0) {
|
||||
labelPieces.push(n__('1 deploy key', '%d deploy keys', counts[LEVEL_TYPES.DEPLOY_KEY]));
|
||||
}
|
||||
|
||||
if (counts[LEVEL_TYPES.GROUP] > 0) {
|
||||
labelPieces.push(n__('1 group', '%d groups', counts[LEVEL_TYPES.GROUP]));
|
||||
}
|
||||
|
@ -299,20 +316,31 @@ export default class AccessDropdown {
|
|||
getData(query, callback) {
|
||||
if (this.hasLicense) {
|
||||
Promise.all([
|
||||
this.getDeployKeys(query),
|
||||
this.getUsers(query),
|
||||
this.groupsData ? Promise.resolve(this.groupsData) : this.getGroups(),
|
||||
])
|
||||
.then(([usersResponse, groupsResponse]) => {
|
||||
.then(([deployKeysResponse, usersResponse, groupsResponse]) => {
|
||||
this.groupsData = groupsResponse;
|
||||
callback(this.consolidateData(usersResponse.data, groupsResponse.data));
|
||||
callback(
|
||||
this.consolidateData(deployKeysResponse.data, usersResponse.data, groupsResponse.data),
|
||||
);
|
||||
})
|
||||
.catch(() => Flash(__('Failed to load groups & users.')));
|
||||
.catch(() => {
|
||||
if (this.deployKeysOnProtectedBranchesEnabled) {
|
||||
Flash(__('Failed to load groups, users and deploy keys.'));
|
||||
} else {
|
||||
Flash(__('Failed to load groups & users.'));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
callback(this.consolidateData());
|
||||
this.getDeployKeys(query)
|
||||
.then(deployKeysResponse => callback(this.consolidateData(deployKeysResponse.data)))
|
||||
.catch(() => Flash(__('Failed to load deploy keys.')));
|
||||
}
|
||||
}
|
||||
|
||||
consolidateData(usersResponse = [], groupsResponse = []) {
|
||||
consolidateData(deployKeysResponse, usersResponse = [], groupsResponse = []) {
|
||||
let consolidatedData = [];
|
||||
|
||||
// ID property is handled differently locally from the server
|
||||
|
@ -328,6 +356,10 @@ export default class AccessDropdown {
|
|||
// For Users
|
||||
// In dropdown: `id`
|
||||
// For submit: `user_id`
|
||||
//
|
||||
// For Deploy Keys
|
||||
// In dropdown: `id`
|
||||
// For submit: `deploy_key_id`
|
||||
|
||||
/*
|
||||
* Build roles
|
||||
|
@ -410,6 +442,38 @@ export default class AccessDropdown {
|
|||
}
|
||||
}
|
||||
|
||||
if (this.deployKeysOnProtectedBranchesEnabled) {
|
||||
const deployKeys = deployKeysResponse.map(response => {
|
||||
const {
|
||||
id,
|
||||
fingerprint,
|
||||
title,
|
||||
owner: { avatar_url, name, username },
|
||||
} = response;
|
||||
|
||||
const shortFingerprint = `(${fingerprint.substring(0, 14)}...)`;
|
||||
|
||||
return {
|
||||
id,
|
||||
title: title.concat(' ', shortFingerprint),
|
||||
avatar_url,
|
||||
fullname: name,
|
||||
username,
|
||||
type: LEVEL_TYPES.DEPLOY_KEY,
|
||||
};
|
||||
});
|
||||
|
||||
if (this.accessLevel === ACCESS_LEVELS.PUSH) {
|
||||
if (deployKeys.length) {
|
||||
consolidatedData = consolidatedData.concat(
|
||||
[{ type: 'divider' }],
|
||||
[{ type: 'header', content: s__('AccessDropdown|Deploy Keys') }],
|
||||
deployKeys,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return consolidatedData;
|
||||
}
|
||||
|
||||
|
@ -433,6 +497,22 @@ export default class AccessDropdown {
|
|||
});
|
||||
}
|
||||
|
||||
getDeployKeys(query) {
|
||||
if (this.deployKeysOnProtectedBranchesEnabled) {
|
||||
return axios.get(this.buildUrl(gon.relative_url_root, this.deployKeysPath), {
|
||||
params: {
|
||||
search: query,
|
||||
per_page: 20,
|
||||
active: true,
|
||||
project_id: gon.current_project_id,
|
||||
push_code: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve({ data: [] });
|
||||
}
|
||||
|
||||
buildUrl(urlRoot, url) {
|
||||
let newUrl;
|
||||
if (urlRoot != null) {
|
||||
|
@ -454,6 +534,9 @@ export default class AccessDropdown {
|
|||
case LEVEL_TYPES.ROLE:
|
||||
criteria = { access_level: item.id };
|
||||
break;
|
||||
case LEVEL_TYPES.DEPLOY_KEY:
|
||||
criteria = { deploy_key_id: item.id };
|
||||
break;
|
||||
case LEVEL_TYPES.GROUP:
|
||||
criteria = { group_id: item.id };
|
||||
break;
|
||||
|
@ -470,6 +553,10 @@ export default class AccessDropdown {
|
|||
case LEVEL_TYPES.ROLE:
|
||||
groupRowEl = this.roleRowHtml(item, isActive);
|
||||
break;
|
||||
case LEVEL_TYPES.DEPLOY_KEY:
|
||||
groupRowEl =
|
||||
this.accessLevel === ACCESS_LEVELS.PUSH ? this.deployKeyRowHtml(item, isActive) : '';
|
||||
break;
|
||||
case LEVEL_TYPES.GROUP:
|
||||
groupRowEl = this.groupRowHtml(item, isActive);
|
||||
break;
|
||||
|
@ -495,6 +582,31 @@ export default class AccessDropdown {
|
|||
`;
|
||||
}
|
||||
|
||||
deployKeyRowHtml(key, isActive) {
|
||||
const isActiveClass = isActive || '';
|
||||
|
||||
return `
|
||||
<li>
|
||||
<a href="#" class="${isActiveClass}">
|
||||
<strong>${key.title}</strong>
|
||||
<p>
|
||||
${sprintf(
|
||||
__('Owned by %{image_tag}'),
|
||||
{
|
||||
image_tag: `<img src="${key.avatar_url}" class="avatar avatar-inline s26" width="30">`,
|
||||
},
|
||||
false,
|
||||
)}
|
||||
<strong class="dropdown-menu-user-full-name gl-display-inline">${escape(
|
||||
key.fullname,
|
||||
)}</strong>
|
||||
<span class="dropdown-menu-user-username gl-display-inline">${key.username}</span>
|
||||
</p>
|
||||
</a>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
|
||||
groupRowHtml(group, isActive) {
|
||||
const isActiveClass = isActive || '';
|
||||
const avatarEl = group.avatar_url
|
||||
|
|
|
@ -1,13 +1,20 @@
|
|||
export const LEVEL_TYPES = {
|
||||
ROLE: 'role',
|
||||
USER: 'user',
|
||||
DEPLOY_KEY: 'deploy_key',
|
||||
GROUP: 'group',
|
||||
};
|
||||
|
||||
export const LEVEL_ID_PROP = {
|
||||
ROLE: 'access_level',
|
||||
USER: 'user_id',
|
||||
DEPLOY_KEY: 'deploy_key_id',
|
||||
GROUP: 'group_id',
|
||||
};
|
||||
|
||||
export const ACCESS_LEVELS = {
|
||||
MERGE: 'merge_access_levels',
|
||||
PUSH: 'push_access_levels',
|
||||
};
|
||||
|
||||
export const ACCESS_LEVEL_NONE = 0;
|
||||
|
|
|
@ -7,12 +7,14 @@ export const LEVEL_TYPES = {
|
|||
ROLE: 'role',
|
||||
USER: 'user',
|
||||
GROUP: 'group',
|
||||
DEPLOY_KEY: 'deploy_key',
|
||||
};
|
||||
|
||||
export const LEVEL_ID_PROP = {
|
||||
ROLE: 'access_level',
|
||||
USER: 'user_id',
|
||||
GROUP: 'group_id',
|
||||
DEPLOY_KEY: 'deploy_key_id',
|
||||
};
|
||||
|
||||
export const ACCESS_LEVEL_NONE = 0;
|
||||
|
|
|
@ -108,6 +108,10 @@ export default class ProtectedBranchCreate {
|
|||
levelAttributes.push({
|
||||
group_id: item.group_id,
|
||||
});
|
||||
} else if (item.type === LEVEL_TYPES.DEPLOY_KEY) {
|
||||
levelAttributes.push({
|
||||
deploy_key_id: item.deploy_key_id,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ class InvitesController < ApplicationController
|
|||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
before_action :member
|
||||
before_action :ensure_member_exists
|
||||
before_action :invite_details
|
||||
skip_before_action :authenticate_user!, only: :decline
|
||||
|
||||
|
@ -59,14 +60,16 @@ class InvitesController < ApplicationController
|
|||
end
|
||||
|
||||
def member
|
||||
return @member if defined?(@member)
|
||||
strong_memoize(:member) do
|
||||
@token = params[:id]
|
||||
Member.find_by_invite_token(@token)
|
||||
end
|
||||
end
|
||||
|
||||
@token = params[:id]
|
||||
@member = Member.find_by_invite_token(@token)
|
||||
def ensure_member_exists
|
||||
return if member
|
||||
|
||||
return render_404 unless @member
|
||||
|
||||
@member
|
||||
render_404
|
||||
end
|
||||
|
||||
def authenticate_user!
|
||||
|
@ -76,10 +79,7 @@ class InvitesController < ApplicationController
|
|||
notice << "or create an account" if Gitlab::CurrentSettings.allow_signup?
|
||||
notice = notice.join(' ') + "."
|
||||
|
||||
# this is temporary finder instead of using member method due to render_404 possibility
|
||||
# will be resolved via https://gitlab.com/gitlab-org/gitlab/-/issues/245325
|
||||
initial_member = Member.find_by_invite_token(params[:id])
|
||||
redirect_params = initial_member ? { invite_email: initial_member.invite_email } : {}
|
||||
redirect_params = member ? { invite_email: member.invite_email } : {}
|
||||
|
||||
store_location_for :user, request.fullpath
|
||||
|
||||
|
@ -87,20 +87,20 @@ class InvitesController < ApplicationController
|
|||
end
|
||||
|
||||
def invite_details
|
||||
@invite_details ||= case @member.source
|
||||
@invite_details ||= case member.source
|
||||
when Project
|
||||
{
|
||||
name: @member.source.full_name,
|
||||
url: project_url(@member.source),
|
||||
name: member.source.full_name,
|
||||
url: project_url(member.source),
|
||||
title: _("project"),
|
||||
path: project_path(@member.source)
|
||||
path: project_path(member.source)
|
||||
}
|
||||
when Group
|
||||
{
|
||||
name: @member.source.name,
|
||||
url: group_url(@member.source),
|
||||
name: member.source.name,
|
||||
url: group_url(member.source),
|
||||
title: _("group"),
|
||||
path: group_path(@member.source)
|
||||
path: group_path(member.source)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -62,7 +62,7 @@ class Projects::ProtectedRefsController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def access_level_attributes
|
||||
%i[access_level id _destroy]
|
||||
%i[access_level id _destroy deploy_key_id]
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ module Projects
|
|||
before_action :define_variables, only: [:create_deploy_token]
|
||||
before_action do
|
||||
push_frontend_feature_flag(:ajax_new_deploy_token, @project)
|
||||
push_frontend_feature_flag(:deploy_keys_on_protected_branches, @project)
|
||||
end
|
||||
|
||||
def show
|
||||
|
@ -125,6 +126,7 @@ module Projects
|
|||
gon.push(protectable_tags_for_dropdown)
|
||||
gon.push(protectable_branches_for_dropdown)
|
||||
gon.push(access_levels_options)
|
||||
gon.push(current_project_id: project.id) if project
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,6 +16,10 @@ module Resolvers
|
|||
required: false,
|
||||
description: 'Filter projects by IDs'
|
||||
|
||||
argument :search_namespaces, GraphQL::BOOLEAN_TYPE,
|
||||
required: false,
|
||||
description: 'Include namespace in project search'
|
||||
|
||||
def resolve(**args)
|
||||
ProjectsFinder
|
||||
.new(current_user: current_user, params: project_finder_params(args), project_ids_relation: parse_gids(args[:ids]))
|
||||
|
@ -28,7 +32,8 @@ module Resolvers
|
|||
{
|
||||
without_deleted: true,
|
||||
non_public: params[:membership],
|
||||
search: params[:search]
|
||||
search: params[:search],
|
||||
search_namespaces: params[:search_namespaces]
|
||||
}.compact
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
module DesignManagement
|
||||
class DesignCollectionCopyStateEnum < BaseEnum
|
||||
graphql_name 'DesignCollectionCopyState'
|
||||
description 'Copy state of a DesignCollection'
|
||||
|
||||
DESCRIPTION_VARIANTS = {
|
||||
in_progress: 'is being copied',
|
||||
error: 'encountered an error during a copy',
|
||||
ready: 'has no copy in progress'
|
||||
}.freeze
|
||||
|
||||
def self.description_variant(copy_state)
|
||||
DESCRIPTION_VARIANTS[copy_state.to_sym] ||
|
||||
(raise ArgumentError, "Unknown copy state: #{copy_state}")
|
||||
end
|
||||
|
||||
::DesignManagement::DesignCollection.state_machines[:copy_state].states.keys.each do |copy_state|
|
||||
value copy_state.upcase,
|
||||
value: copy_state.to_s,
|
||||
description: "The DesignCollection #{description_variant(copy_state)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -39,6 +39,10 @@ module Types
|
|||
null: true,
|
||||
resolver: ::Resolvers::DesignManagement::DesignResolver,
|
||||
description: 'Find a specific design'
|
||||
|
||||
field :copy_state, ::Types::DesignManagement::DesignCollectionCopyStateEnum,
|
||||
null: true,
|
||||
description: 'Copy state of the design collection'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -257,11 +257,7 @@ class MergeRequest < ApplicationRecord
|
|||
scope :join_project, -> { joins(:target_project) }
|
||||
scope :join_metrics, -> do
|
||||
query = joins(:metrics)
|
||||
|
||||
if Feature.enabled?(:improved_mr_merged_at_queries, default_enabled: true)
|
||||
query = query.where(MergeRequest.arel_table[:target_project_id].eq(MergeRequest::Metrics.arel_table[:target_project_id]))
|
||||
end
|
||||
|
||||
query = query.where(MergeRequest.arel_table[:target_project_id].eq(MergeRequest::Metrics.arel_table[:target_project_id]))
|
||||
query
|
||||
end
|
||||
scope :references_project, -> { references(:target_project) }
|
||||
|
|
|
@ -509,6 +509,8 @@ class MergeRequestDiff < ApplicationRecord
|
|||
end
|
||||
|
||||
def encode_in_base64?(diff_text)
|
||||
return false if diff_text.nil?
|
||||
|
||||
(diff_text.encoding == Encoding::BINARY && !diff_text.ascii_only?) ||
|
||||
diff_text.include?("\0")
|
||||
end
|
||||
|
@ -536,7 +538,7 @@ class MergeRequestDiff < ApplicationRecord
|
|||
rows.each do |row|
|
||||
data = row.delete(:diff)
|
||||
row[:external_diff_offset] = file.pos
|
||||
row[:external_diff_size] = data.bytesize
|
||||
row[:external_diff_size] = data&.bytesize || 0
|
||||
|
||||
file.write(data)
|
||||
end
|
||||
|
|
|
@ -71,8 +71,6 @@ class User < ApplicationRecord
|
|||
|
||||
MINIMUM_INACTIVE_DAYS = 180
|
||||
|
||||
ignore_column :bio, remove_with: '13.4', remove_after: '2020-09-22'
|
||||
|
||||
# Override Devise::Models::Trackable#update_tracked_fields!
|
||||
# to limit database writes to at most once every hour
|
||||
# rubocop: disable CodeReuse/ServiceClass
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
= form_for @hook, as: :hook, url: admin_hook_path do |f|
|
||||
= render partial: 'form', locals: { form: f, hook: @hook }
|
||||
.form-actions
|
||||
%span>= f.submit _('Save changes'), class: 'btn btn-success gl-mr-3'
|
||||
%span>= f.submit _('Save changes'), class: 'btn gl-button btn-success gl-mr-3'
|
||||
= render 'shared/web_hooks/test_button', hook: @hook
|
||||
= link_to _('Delete'), admin_hook_path(@hook), method: :delete, class: 'btn btn-remove float-right', data: { confirm: _('Are you sure?') }
|
||||
= link_to _('Delete'), admin_hook_path(@hook), method: :delete, class: 'btn gl-button btn-remove float-right', data: { confirm: _('Are you sure?') }
|
||||
|
||||
%hr
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
.col-lg-8.gl-mb-3
|
||||
= form_for @hook, as: :hook, url: admin_hooks_path do |f|
|
||||
= render partial: 'form', locals: { form: f, hook: @hook }
|
||||
= f.submit _('Add system hook'), class: 'btn btn-success'
|
||||
= f.submit _('Add system hook'), class: 'btn gl-button btn-success'
|
||||
|
||||
= render 'shared/web_hooks/index', hooks: @hooks, hook_class: @hook.class
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
- select_mode_for_dropdown = Feature.enabled?(:deploy_keys_on_protected_branches, @project) ? 'js-multiselect' : ''
|
||||
|
||||
- content_for :merge_access_levels do
|
||||
.merge_access_levels-container
|
||||
= dropdown_tag('Select',
|
||||
|
@ -7,7 +9,7 @@
|
|||
- content_for :push_access_levels do
|
||||
.push_access_levels-container
|
||||
= dropdown_tag('Select',
|
||||
options: { toggle_class: 'js-allowed-to-push qa-allowed-to-push-select wide',
|
||||
options: { toggle_class: "js-allowed-to-push qa-allowed-to-push-select #{select_mode_for_dropdown} wide",
|
||||
dropdown_class: 'dropdown-menu-selectable qa-allowed-to-push-dropdown rspec-allowed-to-push-dropdown capitalize-header',
|
||||
data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes' }})
|
||||
|
||||
|
|
5
changelogs/unreleased/13426-graphql.yml
Normal file
5
changelogs/unreleased/13426-graphql.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add DesignCollection copyState GraphQL field
|
||||
merge_request: 42919
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Resolve Error when quickly reordering designs
|
||||
merge_request: 42818
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Refactor the invites controller member method
|
||||
merge_request: 42727
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix migrating some empty diffs
|
||||
merge_request: 42825
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: deploy_keys_on_protected_branches
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35638
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/247866
|
||||
group: group::progressive delivery
|
||||
type: development
|
||||
default_enabled: false
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
name: improved_mr_merged_at_queries
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39329
|
||||
rollout_issue_url:
|
||||
group: group::analytics
|
||||
type: development
|
||||
default_enabled: true
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: new_pipeline_form
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35674
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/229632
|
||||
group: group::continuous integration
|
||||
type: development
|
||||
default_enabled: false
|
||||
|
|
|
@ -1021,6 +1021,9 @@ The second facet presents the only real solution. For this, we developed
|
|||
|
||||
## Troubleshooting Gitaly
|
||||
|
||||
Check [Gitaly timeouts](../../user/admin_area/settings/gitaly_timeouts.md) when troubleshooting
|
||||
Gitaly.
|
||||
|
||||
### Checking versions when using standalone Gitaly servers
|
||||
|
||||
When using standalone Gitaly servers, you must make sure they are the same version
|
||||
|
|
|
@ -66,6 +66,8 @@ To set up GitLab and its components to accommodate up to 10,000 users:
|
|||
1. [Configure Prometheus](#configure-prometheus) to monitor your GitLab environment.
|
||||
1. [Configure the Object Storage](#configure-the-object-storage)
|
||||
used for shared data objects.
|
||||
1. [Configure Advanced Search (optional)](#configure-advanced-search) for faster,
|
||||
more advanced code search across your entire GitLab instance.
|
||||
1. [Configure NFS (Optional)](#configure-nfs-optional)
|
||||
to have shared disk storage service as an alternative to Gitaly and/or Object Storage (although
|
||||
not recommended). NFS is required for GitLab Pages, you can skip this step if you're not using
|
||||
|
@ -2033,6 +2035,25 @@ work.
|
|||
</a>
|
||||
</div>
|
||||
|
||||
## Configure Advanced Search **(STARTER ONLY)**
|
||||
|
||||
NOTE: **Note:**
|
||||
Elasticsearch cluster design and requirements are dependent on your specific data.
|
||||
For recommended best practices on how to set up your Elasticsearch cluster
|
||||
alongside your instance, read how to
|
||||
[choose the optimal cluster configuration](../../integration/elasticsearch.md#guidance-on-choosing-optimal-cluster-configuration).
|
||||
|
||||
You can leverage Elasticsearch and enable Advanced Search for faster, more
|
||||
advanced code search across your entire GitLab instance.
|
||||
|
||||
[Learn how to set it up.](../../integration/elasticsearch.md)
|
||||
|
||||
<div align="right">
|
||||
<a type="button" class="btn btn-default" href="#setup-components">
|
||||
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## Configure NFS (optional)
|
||||
|
||||
[Object storage](#configure-the-object-storage), along with [Gitaly](#configure-gitaly)
|
||||
|
|
|
@ -47,3 +47,16 @@ You can also optionally configure GitLab to use an
|
|||
[external PostgreSQL service](../postgresql/external.md) or an
|
||||
[external object storage service](../high_availability/object_storage.md) for
|
||||
added performance and reliability at a reduced complexity cost.
|
||||
|
||||
## Configure Advanced Search **(STARTER ONLY)**
|
||||
|
||||
NOTE: **Note:**
|
||||
Elasticsearch cluster design and requirements are dependent on your specific data.
|
||||
For recommended best practices on how to set up your Elasticsearch cluster
|
||||
alongside your instance, read how to
|
||||
[choose the optimal cluster configuration](../../integration/elasticsearch.md#guidance-on-choosing-optimal-cluster-configuration).
|
||||
|
||||
You can leverage Elasticsearch and enable Advanced Search for faster, more
|
||||
advanced code search across your entire GitLab instance.
|
||||
|
||||
[Learn how to set it up.](../../integration/elasticsearch.md)
|
||||
|
|
|
@ -66,6 +66,8 @@ To set up GitLab and its components to accommodate up to 25,000 users:
|
|||
1. [Configure Prometheus](#configure-prometheus) to monitor your GitLab environment.
|
||||
1. [Configure the Object Storage](#configure-the-object-storage)
|
||||
used for shared data objects.
|
||||
1. [Configure Advanced Search (optional)](#configure-advanced-search) for faster,
|
||||
more advanced code search across your entire GitLab instance.
|
||||
1. [Configure NFS (Optional)](#configure-nfs-optional)
|
||||
to have shared disk storage service as an alternative to Gitaly and/or Object Storage (although
|
||||
not recommended). NFS is required for GitLab Pages, you can skip this step if you're not using
|
||||
|
@ -2033,6 +2035,25 @@ work.
|
|||
</a>
|
||||
</div>
|
||||
|
||||
## Configure Advanced Search **(STARTER ONLY)**
|
||||
|
||||
NOTE: **Note:**
|
||||
Elasticsearch cluster design and requirements are dependent on your specific data.
|
||||
For recommended best practices on how to set up your Elasticsearch cluster
|
||||
alongside your instance, read how to
|
||||
[choose the optimal cluster configuration](../../integration/elasticsearch.md#guidance-on-choosing-optimal-cluster-configuration).
|
||||
|
||||
You can leverage Elasticsearch and enable Advanced Search for faster, more
|
||||
advanced code search across your entire GitLab instance.
|
||||
|
||||
[Learn how to set it up.](../../integration/elasticsearch.md)
|
||||
|
||||
<div align="right">
|
||||
<a type="button" class="btn btn-default" href="#setup-components">
|
||||
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## Configure NFS (optional)
|
||||
|
||||
[Object storage](#configure-the-object-storage), along with [Gitaly](#configure-gitaly)
|
||||
|
|
|
@ -55,6 +55,8 @@ To set up GitLab and its components to accommodate up to 2,000 users:
|
|||
environment.
|
||||
1. [Configure the object storage](#configure-the-object-storage) used for
|
||||
shared data objects.
|
||||
1. [Configure Advanced Search (optional)](#configure-advanced-search) for faster,
|
||||
more advanced code search across your entire GitLab instance.
|
||||
1. [Configure NFS](#configure-nfs-optional) (optional, and not recommended)
|
||||
to have shared disk storage service as an alternative to Gitaly or object
|
||||
storage. You can skip this step if you're not using GitLab Pages (which
|
||||
|
@ -851,6 +853,25 @@ functioning backups is encountered.
|
|||
</a>
|
||||
</div>
|
||||
|
||||
## Configure Advanced Search **(STARTER ONLY)**
|
||||
|
||||
NOTE: **Note:**
|
||||
Elasticsearch cluster design and requirements are dependent on your specific data.
|
||||
For recommended best practices on how to set up your Elasticsearch cluster
|
||||
alongside your instance, read how to
|
||||
[choose the optimal cluster configuration](../../integration/elasticsearch.md#guidance-on-choosing-optimal-cluster-configuration).
|
||||
|
||||
You can leverage Elasticsearch and enable Advanced Search for faster, more
|
||||
advanced code search across your entire GitLab instance.
|
||||
|
||||
[Learn how to set it up.](../../integration/elasticsearch.md)
|
||||
|
||||
<div align="right">
|
||||
<a type="button" class="btn btn-default" href="#setup-components">
|
||||
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## Configure NFS (optional)
|
||||
|
||||
For improved performance, [object storage](#configure-the-object-storage),
|
||||
|
|
|
@ -70,6 +70,8 @@ To set up GitLab and its components to accommodate up to 3,000 users:
|
|||
1. [Configure Prometheus](#configure-prometheus) to monitor your GitLab environment.
|
||||
1. [Configure the Object Storage](#configure-the-object-storage)
|
||||
used for shared data objects.
|
||||
1. [Configure Advanced Search (optional)](#configure-advanced-search) for faster,
|
||||
more advanced code search across your entire GitLab instance.
|
||||
1. [Configure NFS (Optional)](#configure-nfs-optional)
|
||||
to have shared disk storage service as an alternative to Gitaly and/or Object Storage (although
|
||||
not recommended). NFS is required for GitLab Pages, you can skip this step if you're not using
|
||||
|
@ -1759,6 +1761,25 @@ work.
|
|||
</a>
|
||||
</div>
|
||||
|
||||
## Configure Advanced Search **(STARTER ONLY)**
|
||||
|
||||
NOTE: **Note:**
|
||||
Elasticsearch cluster design and requirements are dependent on your specific data.
|
||||
For recommended best practices on how to set up your Elasticsearch cluster
|
||||
alongside your instance, read how to
|
||||
[choose the optimal cluster configuration](../../integration/elasticsearch.md#guidance-on-choosing-optimal-cluster-configuration).
|
||||
|
||||
You can leverage Elasticsearch and enable Advanced Search for faster, more
|
||||
advanced code search across your entire GitLab instance.
|
||||
|
||||
[Learn how to set it up.](../../integration/elasticsearch.md)
|
||||
|
||||
<div align="right">
|
||||
<a type="button" class="btn btn-default" href="#setup-components">
|
||||
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## Configure NFS (optional)
|
||||
|
||||
[Object storage](#configure-the-object-storage), along with [Gitaly](#configure-gitaly)
|
||||
|
|
|
@ -66,6 +66,8 @@ To set up GitLab and its components to accommodate up to 50,000 users:
|
|||
1. [Configure Prometheus](#configure-prometheus) to monitor your GitLab environment.
|
||||
1. [Configure the Object Storage](#configure-the-object-storage)
|
||||
used for shared data objects.
|
||||
1. [Configure Advanced Search (optional)](#configure-advanced-search) for faster,
|
||||
more advanced code search across your entire GitLab instance.
|
||||
1. [Configure NFS (Optional)](#configure-nfs-optional)
|
||||
to have shared disk storage service as an alternative to Gitaly and/or Object Storage (although
|
||||
not recommended). NFS is required for GitLab Pages, you can skip this step if you're not using
|
||||
|
@ -2033,6 +2035,25 @@ work.
|
|||
</a>
|
||||
</div>
|
||||
|
||||
## Configure Advanced Search **(STARTER ONLY)**
|
||||
|
||||
NOTE: **Note:**
|
||||
Elasticsearch cluster design and requirements are dependent on your specific data.
|
||||
For recommended best practices on how to set up your Elasticsearch cluster
|
||||
alongside your instance, read how to
|
||||
[choose the optimal cluster configuration](../../integration/elasticsearch.md#guidance-on-choosing-optimal-cluster-configuration).
|
||||
|
||||
You can leverage Elasticsearch and enable Advanced Search for faster, more
|
||||
advanced code search across your entire GitLab instance.
|
||||
|
||||
[Learn how to set it up.](../../integration/elasticsearch.md)
|
||||
|
||||
<div align="right">
|
||||
<a type="button" class="btn btn-default" href="#setup-components">
|
||||
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## Configure NFS (optional)
|
||||
|
||||
[Object storage](#configure-the-object-storage), along with [Gitaly](#configure-gitaly)
|
||||
|
|
|
@ -70,6 +70,8 @@ To set up GitLab and its components to accommodate up to 5,000 users:
|
|||
1. [Configure Prometheus](#configure-prometheus) to monitor your GitLab environment.
|
||||
1. [Configure the Object Storage](#configure-the-object-storage)
|
||||
used for shared data objects.
|
||||
1. [Configure Advanced Search (optional)](#configure-advanced-search) for faster,
|
||||
more advanced code search across your entire GitLab instance.
|
||||
1. [Configure NFS (Optional)](#configure-nfs-optional)
|
||||
to have shared disk storage service as an alternative to Gitaly and/or Object Storage (although
|
||||
not recommended). NFS is required for GitLab Pages, you can skip this step if you're not using
|
||||
|
@ -1758,6 +1760,25 @@ work.
|
|||
</a>
|
||||
</div>
|
||||
|
||||
## Configure Advanced Search **(STARTER ONLY)**
|
||||
|
||||
NOTE: **Note:**
|
||||
Elasticsearch cluster design and requirements are dependent on your specific data.
|
||||
For recommended best practices on how to set up your Elasticsearch cluster
|
||||
alongside your instance, read how to
|
||||
[choose the optimal cluster configuration](../../integration/elasticsearch.md#guidance-on-choosing-optimal-cluster-configuration).
|
||||
|
||||
You can leverage Elasticsearch and enable Advanced Search for faster, more
|
||||
advanced code search across your entire GitLab instance.
|
||||
|
||||
[Learn how to set it up.](../../integration/elasticsearch.md)
|
||||
|
||||
<div align="right">
|
||||
<a type="button" class="btn btn-default" href="#setup-components">
|
||||
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## Configure NFS (optional)
|
||||
|
||||
[Object storage](#configure-the-object-storage), along with [Gitaly](#configure-gitaly)
|
||||
|
|
|
@ -3919,6 +3919,11 @@ type DesignAtVersionEdge {
|
|||
A collection of designs
|
||||
"""
|
||||
type DesignCollection {
|
||||
"""
|
||||
Copy state of the design collection
|
||||
"""
|
||||
copyState: DesignCollectionCopyState
|
||||
|
||||
"""
|
||||
Find a specific design
|
||||
"""
|
||||
|
@ -4046,6 +4051,26 @@ type DesignCollection {
|
|||
): DesignVersionConnection!
|
||||
}
|
||||
|
||||
"""
|
||||
Copy state of a DesignCollection
|
||||
"""
|
||||
enum DesignCollectionCopyState {
|
||||
"""
|
||||
The DesignCollection encountered an error during a copy
|
||||
"""
|
||||
ERROR
|
||||
|
||||
"""
|
||||
The DesignCollection is being copied
|
||||
"""
|
||||
IN_PROGRESS
|
||||
|
||||
"""
|
||||
The DesignCollection has no copy in progress
|
||||
"""
|
||||
READY
|
||||
}
|
||||
|
||||
"""
|
||||
The connection type for Design.
|
||||
"""
|
||||
|
@ -14023,6 +14048,11 @@ type Query {
|
|||
Search query for project name, path, or description
|
||||
"""
|
||||
search: String
|
||||
|
||||
"""
|
||||
Include namespace in project search
|
||||
"""
|
||||
searchNamespaces: Boolean
|
||||
): ProjectConnection
|
||||
|
||||
"""
|
||||
|
|
|
@ -10789,6 +10789,20 @@
|
|||
"name": "DesignCollection",
|
||||
"description": "A collection of designs",
|
||||
"fields": [
|
||||
{
|
||||
"name": "copyState",
|
||||
"description": "Copy state of the design collection",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "ENUM",
|
||||
"name": "DesignCollectionCopyState",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "design",
|
||||
"description": "Find a specific design",
|
||||
|
@ -11106,6 +11120,35 @@
|
|||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "ENUM",
|
||||
"name": "DesignCollectionCopyState",
|
||||
"description": "Copy state of a DesignCollection",
|
||||
"fields": null,
|
||||
"inputFields": null,
|
||||
"interfaces": null,
|
||||
"enumValues": [
|
||||
{
|
||||
"name": "READY",
|
||||
"description": "The DesignCollection has no copy in progress",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "IN_PROGRESS",
|
||||
"description": "The DesignCollection is being copied",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "ERROR",
|
||||
"description": "The DesignCollection encountered an error during a copy",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "DesignConnection",
|
||||
|
@ -41107,6 +41150,16 @@
|
|||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "searchNamespaces",
|
||||
"description": "Include namespace in project search",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Boolean",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "after",
|
||||
"description": "Returns the elements in the list that come after the specified cursor.",
|
||||
|
|
|
@ -685,6 +685,7 @@ A collection of designs.
|
|||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `copyState` | DesignCollectionCopyState | Copy state of the design collection |
|
||||
| `design` | Design | Find a specific design |
|
||||
| `designAtVersion` | DesignAtVersion | Find a design as of a version |
|
||||
| `issue` | Issue! | Issue associated with the design collection |
|
||||
|
@ -3027,6 +3028,16 @@ Mode of a commit action.
|
|||
| `PASSED_VALIDATION` | Site validation process finished successfully |
|
||||
| `PENDING_VALIDATION` | Site validation process has not started |
|
||||
|
||||
### DesignCollectionCopyState
|
||||
|
||||
Copy state of a DesignCollection.
|
||||
|
||||
| Value | Description |
|
||||
| ----- | ----------- |
|
||||
| `ERROR` | The DesignCollection encountered an error during a copy |
|
||||
| `IN_PROGRESS` | The DesignCollection is being copied |
|
||||
| `READY` | The DesignCollection has no copy in progress |
|
||||
|
||||
### DesignVersionEvent
|
||||
|
||||
Mutation event of a design within a version.
|
||||
|
|
|
@ -3,36 +3,28 @@ stage: Create
|
|||
group: Gitaly
|
||||
info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers"
|
||||
type: reference
|
||||
type: reference
|
||||
---
|
||||
|
||||
# Gitaly timeouts
|
||||
# Gitaly timeouts **(CORE ONLY)**
|
||||
|
||||
![Gitaly timeouts](img/gitaly_timeouts.png)
|
||||
[Gitaly](../../../administration/gitaly/index.md) timeouts are configurable. The timeouts can be
|
||||
configured to make sure that long running Gitaly calls don't needlessly take up resources.
|
||||
|
||||
3 timeout types can be configured to make sure that long running
|
||||
Gitaly calls don't needlessly take up resources.
|
||||
To access Gitaly timeout settings:
|
||||
|
||||
- Default timeout
|
||||
1. Go to **Admin Area > Settings > Preferences**.
|
||||
1. Expand the **Gitaly** section.
|
||||
|
||||
This timeout is the default for most Gitaly calls.
|
||||
It should be shorter than the worker timeout that can be configured
|
||||
for
|
||||
[Puma](https://docs.gitlab.com/omnibus/settings/puma.html#puma-settings)
|
||||
or [Unicorn](https://docs.gitlab.com/omnibus/settings/unicorn.html).
|
||||
This makes sure that Gitaly calls made within a web request cannot
|
||||
exceed these the entire request timeout.
|
||||
## Available timeouts
|
||||
|
||||
The default for this timeout is 55 seconds.
|
||||
The following timeouts can be modified:
|
||||
|
||||
- Fast timeout
|
||||
- **Default Timeout Period**. This timeout is the default for most Gitaly calls. It should be shorter than the
|
||||
worker timeout that can be configured for [Puma](https://docs.gitlab.com/omnibus/settings/puma.html#puma-settings)
|
||||
or [Unicorn](https://docs.gitlab.com/omnibus/settings/unicorn.html). Used to make sure that Gitaly
|
||||
calls made within a web request cannot exceed the entire request timeout.
|
||||
Defaults to 55 seconds.
|
||||
|
||||
This is the timeout for very short Gitaly calls.
|
||||
|
||||
The default for this timeout is 10 seconds.
|
||||
|
||||
- Medium timeout
|
||||
|
||||
This timeout should be between the default and the fast timeout
|
||||
|
||||
The default for this timeout is 30 seconds.
|
||||
- **Fast Timeout Period**. This is the timeout for very short Gitaly calls. Defaults to 10 seconds.
|
||||
- **Medium Timeout Period**. This timeout should be between the default and the fast timeout.
|
||||
Defaults to 30 seconds.
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 24 KiB |
|
@ -19,8 +19,9 @@ then in the left sidebar go to **Security & Compliance > Configuration**.
|
|||
|
||||
For each security control the page displays:
|
||||
|
||||
- **Status** - Status of the security control: enabled, not enabled, or available.
|
||||
- **Manage** - A management option or a link to the documentation.
|
||||
- **Security Control:** Name, description, and a documentation link.
|
||||
- **Status:** The security control's status (enabled, not enabled, or available).
|
||||
- **Manage:** A management option or a documentation link.
|
||||
|
||||
## Status
|
||||
|
||||
|
|
|
@ -423,9 +423,12 @@ when:
|
|||
|
||||
### Limitations
|
||||
|
||||
Interactive Terminals is in a beta phase and continues to be improved in upcoming
|
||||
releases. In the meantime, please note that the user is limited to having only one
|
||||
active terminal at a time.
|
||||
The Web IDE has a few limitations:
|
||||
|
||||
- Interactive Terminals is in a beta phase and continues to be improved in upcoming releases. In the meantime, please note that the user is limited to having only one
|
||||
active terminal at a time.
|
||||
|
||||
- LFS files can be rendered and displayed but they cannot be updated and committed using the Web IDE. If an LFS file is modified and pushed to the repository, the LFS pointer in the repository will be overwritten with the modified LFS file content.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
|
|
|
@ -1024,6 +1024,11 @@ msgid_plural "%d days"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "1 deploy key"
|
||||
msgid_plural "%d deploy keys"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "1 group"
|
||||
msgid_plural "%d groups"
|
||||
msgstr[0] ""
|
||||
|
@ -1324,6 +1329,9 @@ msgstr ""
|
|||
msgid "Access to Pages websites are controlled based on the user's membership to a given project. By checking this box, users will be required to be logged in to have access to all Pages websites in your instance."
|
||||
msgstr ""
|
||||
|
||||
msgid "AccessDropdown|Deploy Keys"
|
||||
msgstr ""
|
||||
|
||||
msgid "AccessDropdown|Groups"
|
||||
msgstr ""
|
||||
|
||||
|
@ -10644,6 +10652,9 @@ msgstr ""
|
|||
msgid "Failed to load branches. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to load deploy keys."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to load emoji list."
|
||||
msgstr ""
|
||||
|
||||
|
@ -10659,6 +10670,9 @@ msgstr ""
|
|||
msgid "Failed to load groups & users."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to load groups, users and deploy keys."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to load labels. Please try again."
|
||||
msgstr ""
|
||||
|
||||
|
@ -11147,6 +11161,9 @@ msgstr ""
|
|||
msgid "Filter by user"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter parameters are not valid. Make sure that the end date is after the start date."
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter pipelines"
|
||||
msgstr ""
|
||||
|
||||
|
@ -11171,7 +11188,7 @@ msgstr ""
|
|||
msgid "Find File"
|
||||
msgstr ""
|
||||
|
||||
msgid "Find bugs in your code with coverage-guided fuzzing"
|
||||
msgid "Find bugs in your code with coverage-guided fuzzing."
|
||||
msgstr ""
|
||||
|
||||
msgid "Find by path"
|
||||
|
@ -17913,6 +17930,9 @@ msgstr ""
|
|||
msgid "Overwrite diverged branches"
|
||||
msgstr ""
|
||||
|
||||
msgid "Owned by %{image_tag}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Owned by anyone"
|
||||
msgstr ""
|
||||
|
||||
|
@ -22460,6 +22480,9 @@ msgstr ""
|
|||
msgid "SecurityConfiguration|Manage"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityConfiguration|More information"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityConfiguration|Not enabled"
|
||||
msgstr ""
|
||||
|
||||
|
@ -22472,9 +22495,6 @@ msgstr ""
|
|||
msgid "SecurityConfiguration|Security Control"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityConfiguration|See documentation"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityConfiguration|Status"
|
||||
msgstr ""
|
||||
|
||||
|
@ -24192,9 +24212,6 @@ msgstr ""
|
|||
msgid "Start and due date"
|
||||
msgstr ""
|
||||
|
||||
msgid "Start by choosing a group to see how your team is spending time. You can then drill down to the project level."
|
||||
msgstr ""
|
||||
|
||||
msgid "Start by choosing a group to start exploring the merge requests in that group. You can then proceed to filter by projects, labels, milestones and authors."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -17,8 +17,16 @@ RSpec.describe InvitesController, :snowplow do
|
|||
}
|
||||
end
|
||||
|
||||
before do
|
||||
controller.instance_variable_set(:@member, member)
|
||||
shared_examples 'invalid token' do
|
||||
context 'when invite token is not valid' do
|
||||
let(:params) { { id: '_bogus_token_' } }
|
||||
|
||||
it 'renders the 404 page' do
|
||||
request
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
|
@ -39,7 +47,7 @@ RSpec.describe InvitesController, :snowplow do
|
|||
end
|
||||
|
||||
it 'forces re-confirmation if email does not match signed in user' do
|
||||
member.invite_email = 'bogus@email.com'
|
||||
member.update!(invite_email: 'bogus@email.com')
|
||||
|
||||
expect do
|
||||
request
|
||||
|
@ -80,6 +88,8 @@ RSpec.describe InvitesController, :snowplow do
|
|||
expect_snowplow_event(snowplow_event.merge(action: 'accepted'))
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'invalid token'
|
||||
end
|
||||
|
||||
context 'when not logged in' do
|
||||
|
@ -139,5 +149,27 @@ RSpec.describe InvitesController, :snowplow do
|
|||
expect_snowplow_event(snowplow_event.merge(action: 'accepted'))
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'invalid token'
|
||||
end
|
||||
|
||||
describe 'POST #decline for link in UI' do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
subject(:request) { post :decline, params: params }
|
||||
|
||||
it_behaves_like 'invalid token'
|
||||
end
|
||||
|
||||
describe 'GET #decline for link in email' do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
subject(:request) { get :decline, params: params }
|
||||
|
||||
it_behaves_like 'invalid token'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,6 +9,10 @@ RSpec.describe 'Protected Branches', :js do
|
|||
let(:admin) { create(:admin) }
|
||||
let(:project) { create(:project, :repository) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(deploy_keys_on_protected_branches: false)
|
||||
end
|
||||
|
||||
context 'logged in as developer' do
|
||||
before do
|
||||
project.add_developer(user)
|
||||
|
@ -163,4 +167,14 @@ RSpec.describe 'Protected Branches', :js do
|
|||
include_examples "protected branches > access control > CE"
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the users for protected branches feature is off' do
|
||||
before do
|
||||
stub_licensed_features(protected_refs_for_users: false)
|
||||
end
|
||||
|
||||
include_examples 'when the deploy_keys_on_protected_branches FF is turned on' do
|
||||
let(:all_dropdown_sections) { %w(Roles Deploy\ Keys) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -99,6 +99,7 @@ describe('Design management index page', () => {
|
|||
const findFirstDropzoneWithDesign = () => wrapper.findAll(DesignDropzone).at(1);
|
||||
const findDesignsWrapper = () => wrapper.find('[data-testid="designs-root"]');
|
||||
const findDesigns = () => wrapper.findAll(Design);
|
||||
const draggableAttributes = () => wrapper.find(VueDraggable).vm.$attrs;
|
||||
|
||||
async function moveDesigns(localWrapper) {
|
||||
await jest.runOnlyPendingTimers();
|
||||
|
@ -676,6 +677,20 @@ describe('Design management index page', () => {
|
|||
).toBe('2');
|
||||
});
|
||||
|
||||
it('prevents reordering when reorderDesigns mutation is in progress', async () => {
|
||||
createComponentWithApollo({});
|
||||
|
||||
await moveDesigns(wrapper);
|
||||
|
||||
expect(draggableAttributes().disabled).toBe(true);
|
||||
|
||||
await jest.runOnlyPendingTimers(); // kick off the mocked GQL stuff (promises)
|
||||
await wrapper.vm.$nextTick(); // kick off the DOM update
|
||||
await wrapper.vm.$nextTick(); // kick off the DOM update for finally block
|
||||
|
||||
expect(draggableAttributes().disabled).toBe(false);
|
||||
});
|
||||
|
||||
it('displays flash if mutation had a recoverable error', async () => {
|
||||
createComponentWithApollo({
|
||||
moveHandler: jest.fn().mockResolvedValue(moveDesignMutationResponseWithErrors),
|
||||
|
|
|
@ -83,12 +83,12 @@ describe('IDE commit sidebar actions', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('commitToCurrentBranchText', () => {
|
||||
describe('currentBranchText', () => {
|
||||
it('escapes current branch', () => {
|
||||
const injectedSrc = '<img src="x" />';
|
||||
createComponent({ currentBranchId: injectedSrc });
|
||||
|
||||
expect(vm.commitToCurrentBranchText).not.toContain(injectedSrc);
|
||||
expect(vm.currentBranchText).not.toContain(injectedSrc);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ describe('AccessDropdown', () => {
|
|||
`);
|
||||
const $dropdown = $('#dummy-dropdown');
|
||||
$dropdown.data('defaultLabel', defaultLabel);
|
||||
gon.features = { deployKeysOnProtectedBranches: true };
|
||||
const options = {
|
||||
$dropdown,
|
||||
accessLevelsData: {
|
||||
|
@ -37,6 +38,9 @@ describe('AccessDropdown', () => {
|
|||
{ type: LEVEL_TYPES.GROUP },
|
||||
{ type: LEVEL_TYPES.GROUP },
|
||||
{ type: LEVEL_TYPES.GROUP },
|
||||
{ type: LEVEL_TYPES.DEPLOY_KEY },
|
||||
{ type: LEVEL_TYPES.DEPLOY_KEY },
|
||||
{ type: LEVEL_TYPES.DEPLOY_KEY },
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -49,7 +53,7 @@ describe('AccessDropdown', () => {
|
|||
|
||||
const label = dropdown.toggleLabel();
|
||||
|
||||
expect(label).toBe('1 role, 2 users, 3 groups');
|
||||
expect(label).toBe('1 role, 2 users, 3 deploy keys, 3 groups');
|
||||
expect($dropdownToggleText).not.toHaveClass('is-default');
|
||||
});
|
||||
|
||||
|
@ -122,6 +126,21 @@ describe('AccessDropdown', () => {
|
|||
expect($dropdownToggleText).not.toHaveClass('is-default');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with users and deploy keys', () => {
|
||||
beforeEach(() => {
|
||||
const selectedTypes = [LEVEL_TYPES.DEPLOY_KEY, LEVEL_TYPES.USER];
|
||||
dropdown.setSelectedItems(dummyItems.filter(item => selectedTypes.includes(item.type)));
|
||||
$dropdownToggleText.addClass('is-default');
|
||||
});
|
||||
|
||||
it('displays number of deploy keys', () => {
|
||||
const label = dropdown.toggleLabel();
|
||||
|
||||
expect(label).toBe('2 users, 3 deploy keys');
|
||||
expect($dropdownToggleText).not.toHaveClass('is-default');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('userRowHtml', () => {
|
||||
|
|
|
@ -8,10 +8,15 @@ RSpec.describe Resolvers::ProjectsResolver do
|
|||
describe '#resolve' do
|
||||
subject { resolve(described_class, obj: nil, args: filters, ctx: { current_user: current_user }) }
|
||||
|
||||
let_it_be(:group) { create(:group, name: 'public-group') }
|
||||
let_it_be(:private_group) { create(:group, name: 'private-group') }
|
||||
let_it_be(:project) { create(:project, :public) }
|
||||
let_it_be(:other_project) { create(:project, :public) }
|
||||
let_it_be(:group_project) { create(:project, :public, group: group) }
|
||||
let_it_be(:private_project) { create(:project, :private) }
|
||||
let_it_be(:other_private_project) { create(:project, :private) }
|
||||
let_it_be(:other_private_project) { create(:project, :private) }
|
||||
let_it_be(:private_group_project) { create(:project, :private, group: private_group) }
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
|
@ -20,6 +25,7 @@ RSpec.describe Resolvers::ProjectsResolver do
|
|||
before_all do
|
||||
project.add_developer(user)
|
||||
private_project.add_developer(user)
|
||||
private_group.add_developer(user)
|
||||
end
|
||||
|
||||
context 'when user is not logged in' do
|
||||
|
@ -27,7 +33,7 @@ RSpec.describe Resolvers::ProjectsResolver do
|
|||
|
||||
context 'when no filters are applied' do
|
||||
it 'returns all public projects' do
|
||||
is_expected.to contain_exactly(project, other_project)
|
||||
is_expected.to contain_exactly(project, other_project, group_project)
|
||||
end
|
||||
|
||||
context 'when search filter is provided' do
|
||||
|
@ -45,6 +51,22 @@ RSpec.describe Resolvers::ProjectsResolver do
|
|||
is_expected.to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when searchNamespaces filter is provided' do
|
||||
let(:filters) { { search: 'group', search_namespaces: true } }
|
||||
|
||||
it 'returns projects in a matching namespace' do
|
||||
is_expected.to contain_exactly(group_project)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when searchNamespaces filter false' do
|
||||
let(:filters) { { search: 'group', search_namespaces: false } }
|
||||
|
||||
it 'returns ignores namespace matches' do
|
||||
is_expected.to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -53,7 +75,7 @@ RSpec.describe Resolvers::ProjectsResolver do
|
|||
|
||||
context 'when no filters are applied' do
|
||||
it 'returns all visible projects for the user' do
|
||||
is_expected.to contain_exactly(project, other_project, private_project)
|
||||
is_expected.to contain_exactly(project, other_project, group_project, private_project, private_group_project)
|
||||
end
|
||||
|
||||
context 'when search filter is provided' do
|
||||
|
@ -68,7 +90,23 @@ RSpec.describe Resolvers::ProjectsResolver do
|
|||
let(:filters) { { membership: true } }
|
||||
|
||||
it 'returns projects that user is member of' do
|
||||
is_expected.to contain_exactly(project, private_project)
|
||||
is_expected.to contain_exactly(project, private_project, private_group_project)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when searchNamespaces filter is provided' do
|
||||
let(:filters) { { search: 'group', search_namespaces: true } }
|
||||
|
||||
it 'returns projects from matching group' do
|
||||
is_expected.to contain_exactly(group_project, private_group_project)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when searchNamespaces filter false' do
|
||||
let(:filters) { { search: 'group', search_namespaces: false } }
|
||||
|
||||
it 'returns ignores namespace matches' do
|
||||
is_expected.to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe GitlabSchema.types['DesignCollectionCopyState'] do
|
||||
it { expect(described_class.graphql_name).to eq('DesignCollectionCopyState') }
|
||||
|
||||
it 'exposes the correct event states' do
|
||||
expect(described_class.values.keys).to match_array(%w(READY IN_PROGRESS ERROR))
|
||||
end
|
||||
end
|
|
@ -6,7 +6,7 @@ RSpec.describe GitlabSchema.types['DesignCollection'] do
|
|||
it { expect(described_class).to require_graphql_authorizations(:read_design) }
|
||||
|
||||
it 'has the expected fields' do
|
||||
expected_fields = %i[project issue designs versions version designAtVersion design]
|
||||
expected_fields = %i[project issue designs versions version designAtVersion design copyState]
|
||||
|
||||
expect(described_class).to have_graphql_fields(*expected_fields)
|
||||
end
|
||||
|
|
|
@ -180,6 +180,17 @@ RSpec.describe MergeRequestDiff do
|
|||
expect(diff.external_diff_store).to eq(file_store)
|
||||
end
|
||||
|
||||
it 'migrates a nil diff file' do
|
||||
expect(diff).not_to be_stored_externally
|
||||
MergeRequestDiffFile.where(merge_request_diff_id: diff.id).update_all(diff: nil)
|
||||
|
||||
stub_external_diffs_setting(enabled: true)
|
||||
|
||||
diff.migrate_files_to_external_storage!
|
||||
|
||||
expect(diff).to be_stored_externally
|
||||
end
|
||||
|
||||
it 'safely handles a transaction error when migrating to external storage' do
|
||||
expect(diff).not_to be_stored_externally
|
||||
expect(diff.external_diff).not_to be_exists
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'when the deploy_keys_on_protected_branches FF is turned on' do
|
||||
before do
|
||||
stub_feature_flags(deploy_keys_on_protected_branches: true)
|
||||
project.add_maintainer(user)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
let(:dropdown_sections_minus_deploy_keys) { all_dropdown_sections - ['Deploy Keys'] }
|
||||
|
||||
context 'when deploy keys are enabled to this project' do
|
||||
let!(:deploy_key_1) { create(:deploy_key, title: 'title 1', projects: [project]) }
|
||||
let!(:deploy_key_2) { create(:deploy_key, title: 'title 2', projects: [project]) }
|
||||
|
||||
context 'when only one deploy key can push' do
|
||||
before do
|
||||
deploy_key_1.deploy_keys_projects.first.update!(can_push: true)
|
||||
end
|
||||
|
||||
it "shows all dropdown sections in the 'Allowed to push' main dropdown, with only one deploy key" do
|
||||
visit project_protected_branches_path(project)
|
||||
|
||||
find(".js-allowed-to-push").click
|
||||
wait_for_requests
|
||||
|
||||
within('.qa-allowed-to-push-dropdown') do
|
||||
dropdown_headers = page.all('.dropdown-header').map(&:text)
|
||||
|
||||
expect(dropdown_headers).to contain_exactly(*all_dropdown_sections)
|
||||
expect(page).to have_content('title 1')
|
||||
expect(page).not_to have_content('title 2')
|
||||
end
|
||||
end
|
||||
|
||||
it "shows all sections but not deploy keys in the 'Allowed to merge' main dropdown" do
|
||||
visit project_protected_branches_path(project)
|
||||
|
||||
find(".js-allowed-to-merge").click
|
||||
wait_for_requests
|
||||
|
||||
within('.qa-allowed-to-merge-dropdown') do
|
||||
dropdown_headers = page.all('.dropdown-header').map(&:text)
|
||||
|
||||
expect(dropdown_headers).to contain_exactly(*dropdown_sections_minus_deploy_keys)
|
||||
end
|
||||
end
|
||||
|
||||
it "shows all sections in the 'Allowed to push' update dropdown" do
|
||||
create(:protected_branch, :no_one_can_push, project: project, name: 'master')
|
||||
|
||||
visit project_protected_branches_path(project)
|
||||
|
||||
within(".js-protected-branch-edit-form") do
|
||||
find(".js-allowed-to-push").click
|
||||
wait_for_requests
|
||||
|
||||
dropdown_headers = page.all('.dropdown-header').map(&:text)
|
||||
|
||||
expect(dropdown_headers).to contain_exactly(*all_dropdown_sections)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no deploy key can push' do
|
||||
it "just shows all sections but not deploy keys in the 'Allowed to push' dropdown" do
|
||||
visit project_protected_branches_path(project)
|
||||
|
||||
find(".js-allowed-to-push").click
|
||||
wait_for_requests
|
||||
|
||||
within('.qa-allowed-to-push-dropdown') do
|
||||
dropdown_headers = page.all('.dropdown-header').map(&:text)
|
||||
|
||||
expect(dropdown_headers).to contain_exactly(*dropdown_sections_minus_deploy_keys)
|
||||
end
|
||||
end
|
||||
|
||||
it "just shows all sections but not deploy keys in the 'Allowed to push' update dropdown" do
|
||||
create(:protected_branch, :no_one_can_push, project: project, name: 'master')
|
||||
|
||||
visit project_protected_branches_path(project)
|
||||
|
||||
within(".js-protected-branch-edit-form") do
|
||||
find(".js-allowed-to-push").click
|
||||
wait_for_requests
|
||||
|
||||
dropdown_headers = page.all('.dropdown-header').map(&:text)
|
||||
|
||||
expect(dropdown_headers).to contain_exactly(*dropdown_sections_minus_deploy_keys)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue