Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-07-18 15:29:59 +00:00
parent ef1f98e770
commit 3284638f52
31 changed files with 687 additions and 485 deletions

View File

@ -1254,42 +1254,6 @@ rspec-ee system pg15 es8:
- .rspec-ee-system-parallel
# PG16
rspec-ee unit pg16 opensearch1:
extends:
- .rspec-ee-base-pg16-opensearch1
- .rspec-ee-unit-parallel
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
rspec-ee unit pg16 opensearch2:
extends:
- .rspec-ee-base-pg16-opensearch2
- .rspec-ee-unit-parallel
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
rspec-ee integration pg16 opensearch1:
extends:
- .rspec-ee-base-pg16-opensearch1
- .rspec-ee-integration-parallel
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
rspec-ee integration pg16 opensearch2:
extends:
- .rspec-ee-base-pg16-opensearch2
- .rspec-ee-integration-parallel
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
rspec-ee system pg16 opensearch1:
extends:
- .rspec-ee-base-pg16-opensearch1
- .rspec-ee-system-parallel
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
rspec-ee system pg16 opensearch2:
extends:
- .rspec-ee-base-pg16-opensearch2
- .rspec-ee-system-parallel
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
rspec-ee migration pg16:
extends:
- .rspec-ee-base-pg16
@ -1310,35 +1274,74 @@ rspec-ee unit pg16:
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
- .rspec-ee-unit-parallel
rspec-ee unit pg16 es8:
extends:
- .rspec-ee-base-pg16-es8
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
- .rspec-ee-unit-parallel
rspec-ee integration pg16:
extends:
- .rspec-ee-base-pg16
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
- .rspec-ee-integration-parallel
rspec-ee integration pg16 es8:
extends:
- .rspec-ee-base-pg16-es8
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
- .rspec-ee-integration-parallel
rspec-ee system pg16:
extends:
- .rspec-ee-base-pg16
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
- .rspec-ee-system-parallel
rspec-ee system pg16 es8:
extends:
- .rspec-ee-base-pg16-es8
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
- .rspec-ee-system-parallel
# We have too many jobs in nightly pipeline, more than 2k+,
# which exceeds the limit of jobs a pipeline can have. Disable below for now.
#
# rspec-ee unit pg16 opensearch1:
# extends:
# - .rspec-ee-base-pg16-opensearch1
# - .rspec-ee-unit-parallel
# - .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
# rspec-ee unit pg16 opensearch2:
# extends:
# - .rspec-ee-base-pg16-opensearch2
# - .rspec-ee-unit-parallel
# - .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
# rspec-ee integration pg16 opensearch1:
# extends:
# - .rspec-ee-base-pg16-opensearch1
# - .rspec-ee-integration-parallel
# - .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
# rspec-ee integration pg16 opensearch2:
# extends:
# - .rspec-ee-base-pg16-opensearch2
# - .rspec-ee-integration-parallel
# - .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
# rspec-ee system pg16 opensearch1:
# extends:
# - .rspec-ee-base-pg16-opensearch1
# - .rspec-ee-system-parallel
# - .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
# rspec-ee system pg16 opensearch2:
# extends:
# - .rspec-ee-base-pg16-opensearch2
# - .rspec-ee-system-parallel
# - .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
# rspec-ee unit pg16 es8:
# extends:
# - .rspec-ee-base-pg16-es8
# - .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
# - .rspec-ee-unit-parallel
# rspec-ee integration pg16 es8:
# extends:
# - .rspec-ee-base-pg16-es8
# - .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
# - .rspec-ee-integration-parallel
# rspec-ee system pg16 es8:
# extends:
# - .rspec-ee-base-pg16-es8
# - .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
# - .rspec-ee-system-parallel
# EE: default branch nightly scheduled jobs #
#####################################

View File

@ -1,6 +1,7 @@
export const BYTES_IN_KIB = 1024;
export const DEFAULT_DEBOUNCE_AND_THROTTLE_MS = 250;
export const THOUSAND = 1000;
export const MILLION = THOUSAND ** 2;
export const TRUNCATE_WIDTH_DEFAULT_WIDTH = 80;
export const TRUNCATE_WIDTH_DEFAULT_FONT_SIZE = 12;
export const BV_SHOW_MODAL = 'bv::show::modal';

View File

@ -1,5 +1,5 @@
import { formatNumber, sprintf, __ } from '~/locale';
import { BYTES_IN_KIB, THOUSAND } from './constants';
import { BYTES_IN_KIB, THOUSAND, MILLION } from './constants';
/**
* Function that allows a number with an X amount of decimals
@ -127,16 +127,18 @@ export function numberToHumanSize(size, digits = 2, locale) {
*
* @param number Number to format
* @param digits The number of digits to appear after the decimal point
* @param uppercase Whether to use uppercase suffix (K, M)
* @return {string} Formatted number
*/
export function numberToMetricPrefix(number, digits = 1) {
export function numberToMetricPrefix(number, uppercase = false) {
if (number < THOUSAND) {
return number.toString();
}
if (number < THOUSAND ** 2) {
return `${Number((number / THOUSAND).toFixed(digits))}k`;
const digits = 1;
if (number < MILLION) {
return `${Number((number / THOUSAND).toFixed(digits))}${uppercase ? 'K' : 'k'}`;
}
return `${Number((number / THOUSAND ** 2).toFixed(digits))}m`;
return `${Number((number / MILLION).toFixed(digits))}${uppercase ? 'M' : 'm'}`;
}
/**
* A simple method that returns the value of a + b

View File

@ -6,6 +6,7 @@ import {
MEMBER_MODEL_TYPE_GROUP_MEMBER,
MEMBER_MODEL_TYPE_PROJECT_MEMBER,
} from '~/members/constants';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { I18N } from './constants';
import LeaveDropdownItem from './leave_dropdown_item.vue';
import RemoveMemberDropdownItem from './remove_member_dropdown_item.vue';
@ -29,6 +30,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagMixin()],
props: {
member: {
type: Object,
@ -94,7 +96,11 @@ export default {
: this.$options.i18n.leaveGroup;
},
showLdapOverride() {
return this.permissions.canOverride && !this.member.isOverridden;
return (
!this.glFeatures.showRoleDetailsInDrawer &&
this.permissions.canOverride &&
!this.member.isOverridden
);
},
showBan() {
return !this.isCurrentUser && this.permissions.canBan;

View File

@ -5,28 +5,14 @@ import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
import { helpPagePath } from '~/helpers/help_page_helper';
import MembersTableCell from 'ee_else_ce/members/components/table/members_table_cell.vue';
import { ACCESS_LEVEL_LABELS } from '~/access_level/constants';
import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
import {
GROUP_LINK_ACCESS_LEVEL_PROPERTY_NAME,
MEMBER_ACCESS_LEVEL_PROPERTY_NAME,
MEMBERS_TAB_TYPES,
} from '~/members/constants';
import {
getRoleDropdownItems,
getMemberRole,
} from 'ee_else_ce/members/components/table/drawer/utils';
import * as Sentry from '~/ci/runner/sentry_utils';
import RoleUpdater from 'ee_else_ce/members/components/table/drawer/role_updater.vue';
import RoleSelector from '~/members/components/role_selector.vue';
import MemberAvatar from '../member_avatar.vue';
// The API to update members uses different property names for the access level, depending on if it's a user or a group.
// Users use 'access_level', groups use 'group_access'.
const ACCESS_LEVEL_PROPERTY_NAME = {
[MEMBERS_TAB_TYPES.user]: MEMBER_ACCESS_LEVEL_PROPERTY_NAME,
[MEMBERS_TAB_TYPES.group]: GROUP_LINK_ACCESS_LEVEL_PROPERTY_NAME,
};
export default {
components: {
MemberAvatar,
@ -37,11 +23,9 @@ export default {
GlIcon,
GlAlert,
RoleSelector,
RoleUpdater,
RoleBadges: () => import('ee_component/members/components/table/role_badges.vue'),
GuestOverageConfirmation: () =>
import('ee_component/members/components/table/drawer/guest_overage_confirmation.vue'),
},
inject: ['group'],
props: {
member: {
type: Object,
@ -53,7 +37,7 @@ export default {
return {
selectedRole: null,
isSavingRole: false,
saveError: null,
alert: null,
};
},
computed: {
@ -63,12 +47,16 @@ export default {
initialRole() {
return getMemberRole(this.roles.flatten, this.member);
},
isRoleChanged() {
return this.selectedRole !== this.initialRole;
},
},
watch: {
'member.accessLevel': {
member: {
immediate: true,
handler() {
if (this.member) {
this.alert = null;
this.selectedRole = this.initialRole;
}
},
@ -76,72 +64,18 @@ export default {
isSavingRole() {
this.$emit('busy', this.isSavingRole);
},
selectedRole() {
this.saveError = null;
},
},
methods: {
closeDrawer() {
// Don't close the drawer if the role API call is still underway.
// Don't let the drawer close if the role is still saving.
if (!this.isSavingRole) {
this.$emit('close');
this.alert = null;
}
},
checkGuestOverage() {
this.saveError = null;
this.isSavingRole = true;
const checkOverageFn = this.$refs.guestOverageConfirmation?.checkOverage;
// If guestOverageConfirmation is real instead of the CE dummy, check the guest overage. Otherwise, just update
// the role.
if (checkOverageFn) {
checkOverageFn();
} else {
this.updateRole();
}
},
async updateRole() {
try {
const accessLevelProp = ACCESS_LEVEL_PROPERTY_NAME[this.member.namespace];
const { data } = await axios.put(this.member.memberPath, {
[accessLevelProp]: this.selectedRole.accessLevel,
member_role_id: this.selectedRole.memberRoleId,
});
// EE has a flow where the role is not changed immediately, but goes through an approval process. In that case
// we need to restore the role back to what the member had initially.
if (data?.enqueued) {
this.$toast.show(s__('Members|Role change request was sent to the administrator.'));
this.resetRole();
} else {
this.$toast.show(s__('Members|Role was successfully updated.'));
const { member } = this;
// Update the access level on the member object so that the members table shows the new role.
member.accessLevel = {
stringValue: this.selectedRole.text,
integerValue: this.selectedRole.accessLevel,
description: this.selectedRole.description,
memberRoleId: this.selectedRole.memberRoleId,
};
// Update the license usage info to show/hide the "Is using seat" badge.
if (data?.using_license !== undefined) {
member.usingLicense = data?.using_license;
}
}
} catch (error) {
this.saveError = s__('MemberRole|Could not update role.');
Sentry.captureException(error);
} finally {
this.isSavingRole = false;
}
},
resetRole() {
this.selectedRole = this.initialRole;
this.isSavingRole = false;
},
showCheckOverageError() {
this.saveError = s__('MemberRole|Could not check guest overage.');
this.isSavingRole = false;
setRole(role) {
this.selectedRole = role;
this.alert = null;
},
},
getContentWrapperHeight,
@ -183,13 +117,14 @@ export default {
<dl>
<dt class="gl-mb-3" data-testid="role-header">{{ s__('MemberRole|Role') }}</dt>
<dd class="gl-flex gl-flex-wrap gl-gap-x-2 gl-gap-y-3">
<dd class="gl-flex gl-flex-wrap gl-items-baseline gl-gap-3">
<role-selector
v-if="permissions.canUpdate"
v-model="selectedRole"
:value="selectedRole"
:roles="roles"
:loading="isSavingRole"
class="gl-w-full"
@input="setRole"
/>
<span v-else data-testid="role-text">{{ selectedRole.text }}</span>
<role-badges :member="member" :role="selectedRole" />
@ -207,7 +142,7 @@ export default {
{{ s__('MemberRole|Permissions') }}
</dt>
<dd class="gl-display-flex gl-mb-5">
<span v-if="selectedRole.permissions" class="gl-mr-3" data-testid="base-role">
<span v-if="selectedRole.memberRoleId" class="gl-mr-3" data-testid="base-role">
<gl-sprintf :message="s__('MemberRole|Base role: %{role}')">
<template #role>
{{ $options.ACCESS_LEVEL_LABELS[selectedRole.accessLevel] }}
@ -245,36 +180,44 @@ export default {
</div>
<template #footer>
<div v-if="selectedRole !== initialRole">
<gl-alert v-if="saveError" class="gl-mb-5" variant="danger" :dismissible="false">
{{ saveError }}
<role-updater
v-if="alert || isRoleChanged"
#default="{ saveRole }"
class="gl-flex gl-flex-col gl-gap-5"
:member="member"
:role="selectedRole"
@busy="isSavingRole = $event"
@alert="alert = $event"
@reset="selectedRole = initialRole"
>
<gl-alert
v-if="alert"
:variant="alert.variant"
:dismissible="alert.dismissible"
@dismiss="alert = null"
>
{{ alert.message }}
</gl-alert>
<gl-button
variant="confirm"
:loading="isSavingRole"
data-testid="save-button"
@click="checkGuestOverage"
>
{{ s__('MemberRole|Update role') }}
</gl-button>
<gl-button
class="gl-ml-2"
:disabled="isSavingRole"
data-testid="cancel-button"
@click="resetRole"
>
{{ __('Cancel') }}
</gl-button>
<guest-overage-confirmation
ref="guestOverageConfirmation"
:group-path="group.path"
:member="member"
:role="selectedRole"
@confirm="updateRole"
@cancel="resetRole"
@error="showCheckOverageError"
/>
</div>
<div v-if="isRoleChanged">
<gl-button
variant="confirm"
:loading="isSavingRole"
data-testid="save-button"
@click="saveRole"
>
{{ s__('MemberRole|Update role') }}
</gl-button>
<gl-button
class="gl-ml-2"
:disabled="isSavingRole"
data-testid="cancel-button"
@click="setRole(initialRole)"
>
{{ __('Cancel') }}
</gl-button>
</div>
</role-updater>
</template>
</gl-drawer>
</members-table-cell>

View File

@ -0,0 +1,47 @@
<script>
import { captureException } from '~/sentry/sentry_browser_wrapper';
import { I18N_ROLE_SAVE_SUCCESS, I18N_ROLE_SAVE_ERROR } from '~/members/constants';
import { callRoleUpdateApi, setMemberRole } from './utils';
export default {
props: {
member: {
type: Object,
required: true,
},
role: {
type: Object,
required: true,
},
},
methods: {
async saveRole() {
try {
this.emitBusy(true);
this.emitAlert(null);
await callRoleUpdateApi(this.member, this.role);
setMemberRole(this.member, this.role);
this.emitAlert({ message: I18N_ROLE_SAVE_SUCCESS, variant: 'success' });
} catch (error) {
captureException(error);
this.emitAlert({ message: I18N_ROLE_SAVE_ERROR, variant: 'danger', dismissible: false });
} finally {
this.emitBusy(false);
}
},
emitBusy(isBusy) {
this.$emit('busy', isBusy);
},
emitAlert(alert) {
this.$emit('alert', alert);
},
},
render() {
return this.$scopedSlots.default({
saveRole: this.saveRole,
});
},
};
</script>

View File

@ -1,20 +1,14 @@
import axios from 'axios';
import { roleDropdownItems } from '~/members/utils';
import { roleDropdownItems, initialSelectedRole } from '~/members/utils';
import {
GROUP_LINK_ACCESS_LEVEL_PROPERTY_NAME,
MEMBER_ACCESS_LEVEL_PROPERTY_NAME,
MEMBERS_TAB_TYPES,
} from '~/members/constants';
export const getMemberRole = (roles, member) => {
const { stringValue, integerValue, memberRoleId = null } = member.accessLevel;
const role = roles.find(({ accessLevel }) => accessLevel === member.accessLevel.integerValue);
return role || { text: stringValue, value: integerValue, memberRoleId };
};
// EE version has a special implementation, CE version just returns the basic version.
// EE overrides these.
export const getRoleDropdownItems = roleDropdownItems;
export const getMemberRole = initialSelectedRole;
// The API to update members uses different property names for the access level, depending on if it's a user or a group.
// Users use 'access_level', groups use 'group_access'.

View File

@ -82,6 +82,7 @@ export default {
return state[this.namespace].members.map((member) => ({
...member,
memberPath: state[this.namespace].memberPath.replace(':id', member.id),
ldapOverridePath: state[this.namespace].ldapOverridePath?.replace(':id', member.id),
namespace: this.namespace,
}));
},

View File

@ -4,7 +4,7 @@ import { parseBoolean, convertObjectPropsToCamelCase } from '~/lib/utils/common_
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { createAlert } from '~/alert';
import { __, sprintf } from '~/locale';
import { ACCESS_LEVEL_DEVELOPER_INTEGER } from '~/access_level/constants';
import { ACCESS_LEVEL_REPORTER_INTEGER } from '~/access_level/constants';
import groupsAutocompleteQuery from '~/graphql_shared/queries/groups_autocomplete.query.graphql';
import Api from '~/api';
import { getProjects } from '~/rest_api';
@ -159,7 +159,7 @@ export default {
return Api.projectGroups(this.projectPath, {
search,
with_shared: true,
shared_min_access_level: ACCESS_LEVEL_DEVELOPER_INTEGER,
shared_min_access_level: ACCESS_LEVEL_REPORTER_INTEGER,
}).then((data) =>
data?.map((group) => ({
text: group.full_name,

View File

@ -72,7 +72,7 @@ module DiffHelper
spinner = render(Pajamas::SpinnerComponent.new(size: :sm, class: 'gl-display-none gl-text-align-right', data: { visible_when_loading: true }))
expand_html = content_tag(:div, [expand_button, spinner].join.html_safe, data: { expand_wrapper: true })
else
expand_html = content_tag(:div, '...', data: { visible_when_loading: false, **expand_data })
expand_html = '...'
end
if old_pos

View File

@ -1,2 +1,5 @@
= render Pajamas::ButtonComponent.new(href: project, variant: :danger, method: :delete, button_options: { data: { confirm: remove_project_message(project) } }) do
= _('Delete')
= render Pajamas::ButtonComponent.new(href: project,
method: :delete,
category: :tertiary,
icon: 'remove',
button_options: { class: 'has-tooltip', title: _('Delete'), data: { confirm: remove_project_message(project), confirm_btn_variant: 'danger' } })

View File

@ -3,25 +3,21 @@
- add_page_specific_style 'page_bundles/projects'
- @force_desktop_expanded_sidebar = true
= render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card js-search-settings-section' }, header_options: { class: 'gl-new-card-header gl-display-flex' }, body_options: { class: 'gl-new-card-body' }) do |c|
- c.with_header do
.gl-new-card-title-wrapper
%h3.gl-new-card-title
= _('Projects')
.gl-new-card-count
= sprite_icon('project', css_class: 'gl-mr-2')
= @projects.size
.gl-new-card-actions
- if can? current_user, :admin_group, @group
= render Pajamas::ButtonComponent.new(href: new_project_path(namespace_id: @group.id), size: :small) do
= _("New project")
= render ::Layouts::CrudComponent.new(_('Projects'),
icon: 'project',
count: @projects.size,
options: { class: 'js-search-settings-section' }) do |c|
- c.with_actions do
- if can? current_user, :admin_group, @group
= render Pajamas::ButtonComponent.new(href: new_project_path(namespace_id: @group.id), size: :small) do
= _("New project")
- c.with_body do
%ul.content-list{ class: 'gl-px-3!' }
%ul.content-list
- @projects.each do |project|
%li.project-row.gl-align-items-center{ class: 'gl-display-flex!' }
= render Pajamas::AvatarComponent.new(project, alt: project.name, size: 48, class: 'gl-flex-shrink-0 gl-mr-5')
.gl-min-w-0.gl-flex-grow-1
.title
.title.gl-mr-5
= link_to project_path(project), class: 'js-prefetch-document' do
%span.project-full-name
%span.namespace-name
@ -38,7 +34,7 @@
= render 'shared/projects/badges', project: project, css_class: 'gl-mr-3'
.stats.gl-text-gray-500.gl-flex-shrink-0.gl-hidden.sm:gl-flex.gl-gap-3
.stats.gl-text-secondary.gl-flex-shrink-0.gl-hidden.sm:gl-flex.gl-gap-3
= gl_badge_tag storage_counter(project.statistics&.storage_size)
.controls.gl-flex-shrink-0.gl-ml-5
= render Pajamas::ButtonComponent.new(href: project_project_members_path(project),
@ -46,10 +42,11 @@
button_options: { class: 'gl-mr-2' }) do
= _('View members')
= render Pajamas::ButtonComponent.new(href: edit_project_path(project),
size: :small) do
= _('Edit')
category: :tertiary,
icon: 'pencil',
button_options: { class: 'has-tooltip', title: _('Edit') })
= render 'delete_project_button', project: project
- if @projects.blank?
.nothing-here-block= _("This group has no projects yet")
= paginate @projects, theme: "gitlab"
- c.with_pagination do
= paginate @projects, theme: "gitlab"

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class CreateIndexPurlTypeAndPackageNameOnAffectedPackages < Gitlab::Database::Migration[2.2]
INDEX_NAME = 'index_pm_affected_packages_on_purl_type_and_package_name'
disable_ddl_transaction!
milestone '17.3'
def up
add_concurrent_index(:pm_affected_packages, [:purl_type, :package_name], name: INDEX_NAME)
end
def down
remove_concurrent_index_by_name(:pm_affected_packages, INDEX_NAME)
end
end

View File

@ -0,0 +1 @@
92e9bbbcc3b79f03d6acadc9d4186b675bc7332a915bd96bd93b7da9eca40244

View File

@ -28617,6 +28617,8 @@ CREATE UNIQUE INDEX index_pm_advisories_on_advisory_xid_and_source_xid ON pm_adv
CREATE INDEX index_pm_affected_packages_on_pm_advisory_id ON pm_affected_packages USING btree (pm_advisory_id);
CREATE INDEX index_pm_affected_packages_on_purl_type_and_package_name ON pm_affected_packages USING btree (purl_type, package_name);
CREATE INDEX index_pm_package_version_licenses_on_pm_license_id ON pm_package_version_licenses USING btree (pm_license_id);
CREATE INDEX index_pm_package_version_licenses_on_pm_package_version_id ON pm_package_version_licenses USING btree (pm_package_version_id);

View File

@ -353,7 +353,7 @@ You can administer all runners in the GitLab instance from the Admin area's **Ru
To access the **Runners** page:
1. On the left sidebar, at the bottom, select **Admin area**.
1. Select **Overview > Runners**.
1. Select **CI/CD > Runners**.
#### Search and filter runners

View File

@ -356,6 +356,8 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://git
- `204: No Content` if successfully revoked.
- `400: Bad Request` if not revoked successfully.
- `401: Unauthorized` if the access token is invalid.
- `403: Forbidden` if the access token does not have the required permissions.
### Using a request header
@ -379,6 +381,7 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://git
- `204: No Content` if successfully revoked.
- `400: Bad Request` if not revoked successfully.
- `401: Unauthorized` if the access token is invalid.
## Create a personal access token (administrator only)

View File

@ -434,17 +434,22 @@ Follow these best practices for best results:
- Consider adding a retry limit if there is potential for the migration to fail.
This ensures that migrations can be halted if an issue occurs.
## Deleting advanced search migrations in a major version upgrade
## Cleaning up advanced search migrations
Because our advanced search migrations usually require us to support multiple
Because advanced search migrations usually require us to support multiple
code paths for a long period of time, it's important to clean those up when we
safely can.
We choose to use GitLab major version upgrades as a safe time to remove
We choose to use GitLab [required stops](../database/required_stops.md) as a safe time to remove
backwards compatibility for indices that have not been fully migrated. We
[document this in our upgrade documentation](../../update/index.md#upgrading-to-a-new-major-version).
We also choose to replace the migration code with the halted migration
and remove tests so that:
[GitLab Housekeeper](../../../gems/gitlab-housekeeper/README.md)
is used to automate the cleanup process. This process includes
marking existing migrations as obsolete and deleting obsolete migrations.
When a migration is marked as obsolete, the migration code is replaced with
obsolete migration code and tests are replaced with obsolete migration shared
examples so that:
- We don't need to maintain any code that is called from our advanced search
migrations.
@ -453,14 +458,13 @@ and remove tests so that:
- Operators who have not run this migration and who upgrade directly to the
target version see a message prompting them to reindex from scratch.
To be extra safe, we do not delete migrations that were created in the last
minor version before the major upgrade. So, if we are upgrading to `%14.0`,
we should not delete migrations that were only added in `%13.12`. This
extra safety net allows for migrations that might
take multiple weeks to finish on GitLab.com. It would be bad if we upgraded
GitLab.com to `%14.0` before the migrations in `%13.12` were finished. Because
our deployments to GitLab.com are automated and we don't have
automated checks to prevent this, the extra precaution is warranted.
To be extra safe, we do not clean up migrations that were created in the last
minor version before the last required stop. For example, if the last required stop
was `%14.0`, we should not clean up migrations that were only added in `%13.12`.
This extra safety net allows for migrations that might take multiple weeks to
finish on GitLab.com. Because our deployments to GitLab.com
are automated and we do not have automated checks to prevent this cleanup,
the extra precaution is warranted.
Additionally, even if we did have automated checks to prevent it, we wouldn't
actually want to hold up GitLab.com deployments on advanced search migrations,
as they may still have another week to go, and that's too long to block
@ -468,29 +472,42 @@ deployments.
### Process for marking migrations as obsolete
For every migration that was created 2 minor versions before the major version
being upgraded to, we do the following:
Run the [`Keeps::MarkOldAdvancedSearchMigrationsAsObsolete` Keep](../../../gems/gitlab-housekeeper/README.md#running-for-real)
manually to mark migrations as obsolete.
1. Confirm the migration has actually completed successfully for GitLab.com.
1. Replace the content of the migration with:
For every migration that was created two versions before the last required stop,
the Keep:
1. Retains the content of the migration and adds a prepend to the bottom:
```ruby
include Elastic::MigrationObsolete
ClassName.prepend ::Elastic::MigrationObsolete
```
1. When marking a skippable migration as obsolete, keep the `skip_if` condition.
1. Delete any spec files to support this migration.
1. Verify that there are no references of the migration in the `.rubocop_todo/` directory.
1. Remove any logic handling backwards compatibility for this migration. You
can find this by looking for
`Elastic::DataMigrationService.migration_has_finished?(:migration_name_in_lowercase)`.
1. Create a merge request with these changes. Noting that we should not
accidentally merge this before the major release is started.
1. Replaces the spec file content with the `'a deprecated Advanced Search migration'` shared example.
1. Randomly selects a Global Search backend engineer as an assignee.
1. Updates the dictionary file to mark the migration as obsolete.
### Process for removing migrations
The MR assignee must:
1. Select migrations that were marked as obsolete before the current major release
1. If the step above includes all obsolete migrations, keep one last migration as a safeguard for customers with unapplied migrations
1. Delete migration files and spec files for those migrations
1. Verify that there are no references of the migrations in the `.rubocop_todo/` directory.
1. Create a merge request and assign it to a team member from the global search team.
1. Ensure the dictionary file has the correct `marked_obsolete_by_url` and `marked_obsolete_in_milestone`.
1. Verify that no references to the migration or spec files exist in the `.rubocop_todo/` directory.
1. Remove any logic-handling backwards compatibility for this migration by
looking for `Elastic::DataMigrationService.migration_has_finished?(:migration_name_in_lowercase)`.
1. Push any required changes to the merge request.
### Process for removing obsolete migrations
Run the [`Keeps::DeleteObsoleteAdvancedSearchMigrations` Keep](../../../gems/gitlab-housekeeper/README.md#running-for-real)
manually to remove obsolete migrations and specs. The Keep removes all but the most
recent obsolete migration.
1. Select obsolete migrations that were marked as obsolete before the last required stop.
1. If the first step includes all obsolete migrations, keep one obsolete migration as a safeguard for customers with unapplied migrations.
1. Delete migration files and spec files for those migrations.
1. Create a merge request and assign it to a Global Search team member.
The MR assignee must:
1. Verify that no references to the migration or spec files exist in the `.rubocop_todo/` directory.
1. Push any required changes to the merge request.

View File

@ -143,6 +143,25 @@ You can include additional instructions to be considered. For example:
- Focus on performance, for example `/refactor improving performance`.
- Focus on potential vulnerabilities, for example `/refactor avoiding memory leaks and exploits`.
## Fix code in the IDE
DETAILS:
**Tier:** GitLab.com and Self-managed: For a limited time, Premium and Ultimate. In the future, [GitLab Duo Pro or Enterprise](../../subscriptions/subscription-add-ons.md). <br>GitLab Dedicated: GitLab Duo Pro or Enterprise.
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
**Editors:** Web IDE, VS Code, JetBrains IDEs
**LLMs:** Anthropic: [`claude-3-5-sonnet-20240620`](https://console.cloud.google.com/vertex-ai/publishers/anthropic/model-garden/claude-3-5-sonnet)
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/429915) for GitLab.com, self-managed and GitLab Dedicated in GitLab 17.3.
`/fix` is a special command to generate a fix suggestion for the selected code in your editor.
You can include additional instructions to be considered. For example:
- Focus on grammar and typos, for example, `/fix grammar mistakes and typos`.
- Focus on a concrete algorithm or problem description, for example, `/fix duplicate database inserts` or `/fix race conditions`.
- Focus on potential bugs that are not directly visible, for example, `/fix potential bugs`.
- Focus on code performance problems, for example, `/fix performance problems`.
- Focus on fixing the build when the code does not compile, for example, `/fix the build`.
## Write tests in the IDE
DETAILS:
@ -257,3 +276,4 @@ Use the following commands to quickly accomplish specific tasks.
| /explain | [Explain code](../gitlab_duo_chat/examples.md#explain-code-in-the-ide) |
| /vulnerability_explain | [Explain current vulnerability](../gitlab_duo/index.md#vulnerability-explanation) |
| /refactor | [Refactor the code](../gitlab_duo_chat/examples.md#refactor-code-in-the-ide) |
| /fix | [Fix the code](../gitlab_duo_chat/examples.md#fix-code-in-the-ide) |

View File

@ -66,7 +66,7 @@ In the IDEs, GitLab Duo Chat knows about these areas:
| Issues | Ask about the URL. |
In addition, in the IDEs, when you use any of the slash commands,
like `/explain`, `/refactor`, or `/tests,` Duo Chat has access to the
like `/explain`, `/refactor`, `/fix`, or `/tests,` Duo Chat has access to the
code you selected.
Duo Chat always has access to:

View File

@ -81,6 +81,7 @@ For more information about slash commands, refer to the documentation:
- [/tests](../gitlab_duo_chat/examples.md#write-tests-in-the-ide)
- [/refactor](../gitlab_duo_chat/examples.md#refactor-code-in-the-ide)
- [/fix](../gitlab_duo_chat/examples.md#fix-code-in-the-ide)
- [/explain](../gitlab_duo_chat/examples.md#explain-code-in-the-ide)
## `Error M4001`

View File

@ -97,7 +97,7 @@ The following features are extended from standard Markdown:
When you use GitLab Flavored Markdown, you are creating digital content.
This content should be as accessible as possible to your audience.
The following list is not exhaustive, but it provides guidance for some of the GLFM styles to pay
The following list is not exhaustive, but it provides guidance for some of the GitLab Flavored Markdown styles to pay
particular attention to:
### Accessible headings
@ -118,10 +118,10 @@ Don't use `image of` or `video of` in the description. For more information, see
## Line breaks
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#line-breaks).
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#line-breaks).
A line break is inserted (a new paragraph starts) if the previous text is
ended with two newlines, like when you press <kbd>Enter</kbd> twice in a row. If you only
ended with two newlines. For example, when you press <kbd>Enter</kbd> twice in a row. If you only
use one newline (press <kbd>Enter</kbd> once), the next sentence remains part of the
same paragraph. Use this approach if you want to keep long lines from wrapping, and keep
them editable:
@ -171,7 +171,7 @@ A new line due to the previous backslash.
## Emphasis
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#emphasis).
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#emphasis).
You can emphasize text in multiple ways. Use italics, bold, strikethrough,
or combine these emphasis styles together.
@ -202,7 +202,7 @@ Strikethrough with double tildes. ~~Scratch this.~~
### Multiple underscores in words and mid-word emphasis
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#multiple-underscores-in-words).
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#multiple-underscores-in-words).
Avoid italicizing a portion of a word, especially when you're
dealing with code and names that often appear with multiple underscores.
@ -244,7 +244,7 @@ do*this*and*do*that*and*another thing
### Inline diff
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#inline-diff).
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#inline-diff).
With inline diff tags, you can display `{+ additions +}` or `[- deletions -]`.
@ -270,7 +270,7 @@ However, you cannot mix the wrapping tags:
- [- deletion -}
```
Diff highlighting doesn't work with `` `inline code` ``. If your text includes backticks (`` ` ``), escape
Diff highlighting doesn't work with `` `inline code` ``. If your text includes backticks (`` ` ``), [escape](#escape-characters)
each backtick with a backslash <code>&#92;</code>:
```markdown
@ -308,7 +308,7 @@ Alt-H2
> - Heading link generation [changed](https://gitlab.com/gitlab-org/gitlab/-/issues/440733) in GitLab 17.0.
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#heading-ids-and-links).
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#heading-ids-and-links).
All Markdown-rendered headings automatically
get IDs that can be linked to, except in comments.
@ -349,7 +349,7 @@ Would generate the following link IDs:
## Links
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#links).
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#links).
You can create links two ways: inline-style and reference-style. For example:
@ -412,7 +412,7 @@ points the link to `wikis/style` only when the link is inside of a wiki Markdown
### URL auto-linking
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#url-auto-linking).
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#url-auto-linking).
Almost any URL you put into your text is auto-linked:
@ -484,7 +484,7 @@ Reference-style:
### Videos
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#videos).
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#videos).
Image tags that link to files with a video extension are automatically converted to
a video player. The valid video extensions are `.mp4`, `.m4v`, `.mov`, `.webm`, and `.ogv`:
@ -502,7 +502,7 @@ Here's an example video:
> - Support for images [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/28118) in GitLab 15.7.
> - Support for videos [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17139) in GitLab 15.9.
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#change-the-image-or-video-dimensions).
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#change-the-image-or-video-dimensions).
You can control the width and height of an image or video by following the image with
an attribute list.
@ -529,7 +529,7 @@ resized to 75% of its dimensions.
### Audio
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#audio).
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#audio).
Similar to videos, link tags for files with an audio extension are automatically converted to
an audio player. The valid audio extensions are `.mp3`, `.oga`, `.ogg`, `.spx`, and `.wav`:
@ -544,12 +544,12 @@ Here's an example audio clip:
## Lists
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#lists).
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#lists).
You can create ordered and unordered lists.
For an ordered list, add the number you want the list
to start with, like `1.`, followed by a space, at the start of each line for ordered lists.
to start with, like `1.`, followed by a space, at the start of each line.
After the first number, it does not matter what number you use. Ordered lists are
numbered automatically by vertical order, so repeating `1.` for all items in the
same list is common. If you start with a number other than `1.`, it uses that as the first
@ -582,7 +582,7 @@ See https://docs.gitlab.com/ee/development/documentation/styleguide/index.html#l
1. And another item.
For an unordered list, add a `-`, `*` or `+`, followed by a space, at the start of
each line for unordered lists, but you should not use a mix of them.
each line. Don't mix the characters in the same list.
```markdown
Unordered lists can:
@ -646,7 +646,8 @@ Example:
---
If the first item's paragraph isn't indented with the proper number of spaces,
the paragraph appears outside the list, instead of properly indented under the list item.
the paragraph appears outside the list.
Use the correct number of spaces to properly indent under the list item.
For example:
```markdown
@ -684,8 +685,8 @@ Ordered lists that are the first sub-item of an unordered list item must have a
---
CommonMark ignores blank lines between ordered and unordered list items, and considers them part of a single list. These are rendered as a
_[loose](https://spec.commonmark.org/0.30/#loose)_ list. Each list item is enclosed in a paragraph tag and, therefore, has paragraph spacing and margins.
CommonMark ignores blank lines between ordered and unordered list items, and considers them part of a single list. The items are rendered as a
_[loose](https://spec.commonmark.org/0.30/#loose)_ list. Each list item is enclosed in a paragraph tag and therefore has paragraph spacing and margins.
This makes the list look like there is extra spacing between each item.
For example:
@ -703,7 +704,7 @@ CommonMark ignores the blank line and renders this as one list with paragraph sp
> - Inapplicable checkboxes [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85982) in GitLab 15.3.
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#task-lists).
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#task-lists).
You can add task lists anywhere Markdown is supported.
@ -738,7 +739,7 @@ To include task lists in tables, [use HTML list tags or HTML tables](#task-lists
## Blockquotes
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#blockquotes).
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#blockquotes).
Use a blockquote to highlight information, such as a side note. It's generated
by starting the lines of the blockquote with `>`:
@ -761,7 +762,7 @@ Quote break.
### Multiline blockquote
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#multiline-blockquote).
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#multiline-blockquote).
Create multi-line blockquotes fenced by `>>>`:
@ -785,7 +786,7 @@ you can quote that without having to manually prepend `>` to every line!
## Code spans and blocks
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#code-spans-and-blocks).
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#code-spans-and-blocks).
Highlight anything that should be viewed as code and not standard text.
@ -848,7 +849,7 @@ Tildes are OK too.
### Syntax highlighting
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#syntax-highlighting).
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#syntax-highlighting).
GitLab uses the [Rouge Ruby library](https://github.com/rouge-ruby/rouge) for more colorful syntax
highlighting in code blocks. For a list of supported languages visit the
@ -924,7 +925,7 @@ In wikis, you can also add and edit diagrams created with the [diagrams.net edit
> - Support for Entity Relationship diagrams and mind maps [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/384386) in GitLab 16.0.
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#mermaid).
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#mermaid).
Visit the [official page](https://mermaidjs.github.io/) for more details. The
[Mermaid Live Editor](https://mermaid-js.github.io/mermaid-live-editor/) helps you
@ -1004,7 +1005,7 @@ For more information, see the [Kroki integration](../administration/integration/
> - LaTeX-compatible fencing [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/21757) in GitLab 15.4 [with a flag](../administration/feature_flags.md) named `markdown_dollar_math`. Disabled by default. Enabled on GitLab.com.
> - LaTeX-compatible fencing [generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/371180) in GitLab 15.8. Feature flag `markdown_dollar_math` removed.
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#math).
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#math).
Math written in LaTeX syntax is rendered with [KaTeX](https://github.com/KaTeX/KaTeX).
_KaTeX only supports a [subset](https://katex.org/docs/supported.html) of LaTeX._
@ -1058,7 +1059,7 @@ $$
## Tables
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#tables-1).
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#tables-1).
When creating tables:
@ -1076,7 +1077,7 @@ When creating tables:
by pipes (`|`).
- You **can** have blank cells.
- Column widths are calculated dynamically based on the content of the cells.
- To use the pipe character (`|`) in the text and not as table delimiter, you must escape it with a backslash (`\|`).
- To use the pipe character (`|`) in the text and not as table delimiter, you must [escape](#escape-characters) it with a backslash (`\|`).
Example:
@ -1096,7 +1097,7 @@ Example:
### Alignment
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#alignment).
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#alignment).
Additionally, you can choose the alignment of text in columns by adding colons (`:`)
to the sides of the "dash" lines in the second row. This affects every cell in the column:
@ -1118,7 +1119,7 @@ the headers are always left-aligned in Chrome and Firefox, and centered in Safar
### Cells with multiple lines
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#cells-with-multiple-lines).
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#cells-with-multiple-lines).
You can use HTML formatting to adjust the rendering of tables. For example, you can
use `<br>` tags to force a cell to have multiple lines:
@ -1396,7 +1397,7 @@ Second section content.
## Colors
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#colors).
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#colors).
Markdown does not support changing text color.
@ -1435,7 +1436,7 @@ display a color chip next to the color code. For example:
## Emoji
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#emoji).
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#emoji).
Sometimes you want to <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/public/-/emojis/2/monkey.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":monkey:" alt=":monkey:">
around a bit and add some <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/public/-/emojis/2/star2.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":star2:" alt=":star2:">
@ -1559,9 +1560,61 @@ $example = array(
---
```
## Escape characters
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#escape-characters).
Markdown reserves the following ASCII characters to format the page:
```plaintext
! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ ` { | } ~
```
To use one of these reserved characters in your text, add the backslash character (` \ `) immediately before the
reserved character. When you place the backslash before a reserved character, the Markdown parser omits the
backslash and treats the reserved character as regular text.
Examples:
```plaintext
\# Not a heading
| Food | Do you like this food? (circle) |
|-----------------|---------------------------------|
| Pizza | Yes \| No |
\**Not bold, just italic text placed between some asterisks*\*
```
When rendered, the escaped characters look like this:
---
\# Not a heading
| Food | Do you like this food? (circle)|
|-----------------|--------------------------------|
| Pizza | Yes \| No |
\**Not bold, just italic text placed between some asterisks*\*
---
Exceptions:
A backslash doesn't always escape the following character. The backslash appears as regular text in the following cases:
- When the backslash appears before a non-reserved character, such as `A`, `3`, or a space.
- When the backslash appears inside of these Markdown elements:
- Code blocks
- Code spans
- Auto-links
- Inline HTML
## Footnotes
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#footnotes).
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#footnotes).
Footnotes add a link to a note rendered at the end of a Markdown file.
@ -1602,7 +1655,7 @@ These are used to force the Vale ReferenceLinks check to skip these examples.
## Horizontal rule
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#horizontal-rule).
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#horizontal-rule).
Create a horizontal rule by using three or more hyphens, asterisks, or underscores:
@ -1622,7 +1675,7 @@ ___
## Inline HTML
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#inline-html).
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#inline-html).
You can also use raw HTML in your Markdown, and it usually works pretty well.
@ -1687,7 +1740,7 @@ Markdown is fine in GitLab.
### Collapsible section
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#details-and-summary).
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#details-and-summary).
Content can be collapsed using HTML's [`<details>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details)
and [`<summary>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/summary)
@ -1752,7 +1805,7 @@ These details <em>remain</em> <b>hidden</b> until expanded.
### Keyboard HTML tag
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#keyboard-html-tag).
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#keyboard-html-tag).
The `<kbd>` element is used to identify text that represents user keyboard input. Text surrounded by `<kbd>` tags is typically displayed in the browser's default monospace font.
@ -1764,7 +1817,7 @@ Press <kbd>Enter</kbd> to go to the next page.
### Superscripts / Subscripts
[View this topic in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#superscripts-subscripts).
[View this topic rendered in GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/user/markdown.md#superscripts-subscripts).
For superscripts and subscripts, use the standard HTML syntax:
@ -1812,8 +1865,8 @@ it links to `<your_wiki>/documentation/file.md`:
### Wiki - hierarchical link
A hierarchical link can be constructed relative to the current wiki page by using `./<page>`,
`../<page>`, and so on.
A hierarchical link can be constructed relative to the current wiki page by using relative paths like `./<page>` or
`../<page>`.
If this example is on a page at `<your_wiki>/documentation/main`,
it links to `<your_wiki>/documentation/related`:

View File

@ -30,15 +30,14 @@ With GitLab Duo Code Suggestions, you get:
- Code generation, which generates code based on a natural language code
comment block. Write a comment like `# check if code suggestions are
enabled for current user`, then press <kbd>Enter</kbd> to generate code based
on the context of your comment and the rest of your code.
Code generation requests are slower than code completion requests, but provide
more accurate responses because:
on the context of your comment and the rest of your code. Code generation requests
are slower than code completion requests, but provide more accurate responses because:
- A larger LLM is used.
- Additional context is sent in the request, for example,
the libraries used by the project.
Code generation is used when the:
- User writes a comment and hits <kbd>Enter</kbd>.
- File being edited is less than five lines of code.
- User enters an empty function or method.

View File

@ -57,14 +57,18 @@ gitlab-advanced-sast:
when: never
- if: $SAST_EXCLUDED_ANALYZERS =~ /gitlab-advanced-sast/
when: never
- if: $CI_PIPELINE_SOURCE == "merge_request_event" # Add the job to merge request pipelines if there's an open merge request.
# Add the job to merge request pipelines if there's an open merge request.
- if: $CI_PIPELINE_SOURCE == "merge_request_event" &&
$GITLAB_FEATURES =~ /\bsast_advanced\b/
exists:
- '**/*.py'
- '**/*.go'
- '**/*.java'
- if: $CI_OPEN_MERGE_REQUESTS # Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
when: never
- if: $CI_COMMIT_BRANCH # If there's no open merge request, add it to a *branch* pipeline instead.
# If there's no open merge request, add it to a *branch* pipeline instead.
- if: $CI_COMMIT_BRANCH &&
$GITLAB_FEATURES =~ /\bsast_advanced\b/
exists:
- '**/*.py'
- '**/*.go'

View File

@ -32218,9 +32218,6 @@ msgstr ""
msgid "MemberRole|Change role"
msgstr ""
msgid "MemberRole|Could not check guest overage."
msgstr ""
msgid "MemberRole|Could not fetch available permissions."
msgstr ""
@ -32314,6 +32311,9 @@ msgstr ""
msgid "MemberRole|Permissions"
msgstr ""
msgid "MemberRole|Reverted to LDAP group sync settings. The role will be updated after the next LDAP sync."
msgstr ""
msgid "MemberRole|Role"
msgstr ""
@ -32356,6 +32356,9 @@ msgstr ""
msgid "MemberRole|The Reporter role is suitable for team members who need to stay informed about a project or group but do not actively contribute code."
msgstr ""
msgid "MemberRole|This member is an LDAP user. Changing their role will override the settings from the LDAP group sync."
msgstr ""
msgid "MemberRole|This role has been manually selected and will not sync to the LDAP sync role."
msgstr ""
@ -32365,6 +32368,9 @@ msgstr ""
msgid "MemberRole|Update role"
msgstr ""
msgid "MemberRole|Use LDAP sync role"
msgstr ""
msgid "MemberRole|View permissions"
msgstr ""

View File

@ -113,5 +113,6 @@ FactoryBot.define do
factory :project_audit_event, traits: [:project_event]
factory :group_audit_event, traits: [:group_event]
factory :instance_audit_event, traits: [:instance_event]
end
end

View File

@ -145,6 +145,7 @@ describe('Number Utils', () => {
${123456789} | ${'123.5m'}
`('returns $expected given $number', ({ number, expected }) => {
expect(numberToMetricPrefix(number)).toBe(expected);
expect(numberToMetricPrefix(number, true)).toBe(expected.toUpperCase());
});
});

View File

@ -1,35 +1,45 @@
import { GlDrawer, GlSprintf, GlAlert } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import axios from 'axios';
import { GlDrawer, GlAlert } from '@gitlab/ui';
import { nextTick } from 'vue';
import { cloneDeep } from 'lodash';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RoleDetailsDrawer from '~/members/components/table/drawer/role_details_drawer.vue';
import MembersTableCell from '~/members/components/table/members_table_cell.vue';
import MemberAvatar from '~/members/components/table/member_avatar.vue';
import RoleSelector from '~/members/components/role_selector.vue';
import { roleDropdownItems } from '~/members/utils';
import waitForPromises from 'helpers/wait_for_promises';
import RoleUpdater from 'ee_else_ce/members/components/table/drawer/role_updater.vue';
import { RENDER_ALL_SLOTS_TEMPLATE, stubComponent } from 'helpers/stub_component';
import { member as memberData, updateableMember } from '../../../mock_data';
jest.mock('~/lib/utils/dom_utils', () => ({
getContentWrapperHeight: () => '123',
}));
describe('Role details drawer', () => {
const dropdownItems = roleDropdownItems(updateableMember);
const toastShowMock = jest.fn();
const currentRole = dropdownItems.flatten[5];
const newRole = dropdownItems.flatten[2];
let axiosMock;
const saveRoleStub = jest.fn();
let wrapper;
const createWrapper = ({ member = updateableMember } = {}) => {
wrapper = shallowMountExtended(RoleDetailsDrawer, {
propsData: { member },
provide: {
currentUserId: 1,
canManageMembers: true,
group: 'group/path',
stubs: {
GlDrawer: stubComponent(GlDrawer, { template: RENDER_ALL_SLOTS_TEMPLATE }),
RoleUpdater: stubComponent(RoleUpdater, {
template: '<div><slot :save-role="saveRole"></slot></div>',
methods: { saveRole: saveRoleStub },
}),
MembersTableCell: stubComponent(MembersTableCell, {
render() {
return this.$scopedSlots.default({
memberType: 'user',
isCurrentUser: false,
permissions: { canUpdate: member.canUpdate },
});
},
}),
},
stubs: { GlDrawer, MembersTableCell, GlSprintf },
mocks: { $toast: { show: toastShowMock } },
});
};
@ -37,26 +47,18 @@ describe('Role details drawer', () => {
const findRoleText = () => wrapper.findByTestId('role-text');
const findRoleSelector = () => wrapper.findComponent(RoleSelector);
const findRoleDescription = () => wrapper.findByTestId('description-value');
const findRoleUpdater = () => wrapper.findComponent(RoleUpdater);
const findSaveButton = () => wrapper.findByTestId('save-button');
const findCancelButton = () => wrapper.findByTestId('cancel-button');
const findAlert = () => wrapper.findComponent(GlAlert);
const createWrapperChangeRoleAndClickSave = async () => {
createWrapper({ member: cloneDeep(updateableMember) });
const createWrapperAndChangeRole = () => {
createWrapper();
findRoleSelector().vm.$emit('input', newRole);
await nextTick();
findSaveButton().vm.$emit('click');
return waitForPromises();
return nextTick;
};
beforeEach(() => {
axiosMock = new MockAdapter(axios);
});
afterEach(() => {
axiosMock.restore();
});
it('does not show the drawer when there is no member', () => {
createWrapper({ member: null });
@ -64,42 +66,30 @@ describe('Role details drawer', () => {
});
describe('when there is a member', () => {
beforeEach(() => {
createWrapper({ member: memberData });
});
beforeEach(createWrapper);
it('shows the drawer with expected props', () => {
expect(findDrawer().props()).toMatchObject({ headerSticky: true, open: true, zIndex: 252 });
});
it('shows the user avatar', () => {
expect(wrapper.findComponent(MembersTableCell).props('member')).toBe(memberData);
expect(wrapper.findComponent(MemberAvatar).props()).toMatchObject({
memberType: 'user',
isCurrentUser: false,
member: memberData,
it('shows the drawer', () => {
expect(findDrawer().props()).toMatchObject({
headerHeight: '123',
headerSticky: true,
open: true,
zIndex: 252,
});
});
it('does not show footer buttons', () => {
expect(findSaveButton().exists()).toBe(false);
expect(findCancelButton().exists()).toBe(false);
});
it('emits close event when drawer is closed', () => {
findDrawer().vm.$emit('close');
expect(wrapper.emitted('close')).toHaveLength(1);
it('shows the user avatar', () => {
expect(wrapper.findComponent(MembersTableCell).props('member')).toBe(updateableMember);
expect(wrapper.findComponent(MemberAvatar).props()).toEqual({
memberType: 'user',
isCurrentUser: false,
member: updateableMember,
});
});
describe('role name', () => {
it('shows the header', () => {
expect(wrapper.findByTestId('role-header').text()).toBe('Role');
});
it('shows the role name', () => {
expect(findRoleText().text()).toContain('Owner');
});
});
describe('role description', () => {
@ -110,17 +100,6 @@ describe('Role details drawer', () => {
it('shows the role description', () => {
expect(findRoleDescription().text()).toBe(currentRole.description);
});
it('shows "No description" when there is no role description', async () => {
// Create a member that's assigned to a non-existent custom role.
const member = { ...updateableMember, accessLevel: { memberRoleId: 999 } };
wrapper.setProps({ member });
await nextTick();
const noDescriptionSpan = findRoleDescription().find('span');
expect(noDescriptionSpan.text()).toBe('No description');
expect(noDescriptionSpan.classes('gl-text-gray-400')).toBe(true);
});
});
describe('role permissions', () => {
@ -142,7 +121,7 @@ describe('Role details drawer', () => {
});
});
describe('role selector', () => {
describe('role name/selector', () => {
it('shows role name when the member cannot be edited', () => {
createWrapper({ member: memberData });
@ -162,53 +141,32 @@ describe('Role details drawer', () => {
});
});
describe('when the user only has read access', () => {
it('shows the custom role name', () => {
const member = {
...memberData,
accessLevel: { stringValue: 'Custom role', memberRoleId: 102 },
};
createWrapper({ member });
expect(findRoleText().text()).toBe('Custom role');
});
});
describe('when role is changed', () => {
beforeEach(() => {
createWrapper();
findRoleSelector().vm.$emit('input', newRole);
beforeEach(createWrapperAndChangeRole);
it('shows role updater', () => {
expect(findRoleUpdater().props()).toEqual({ member: updateableMember, role: newRole });
});
it('shows save button', () => {
expect(findSaveButton().text()).toBe('Update role');
expect(findSaveButton().props()).toMatchObject({
variant: 'confirm',
loading: false,
});
expect(findSaveButton().props()).toMatchObject({ variant: 'confirm', loading: false });
});
it('shows cancel button', () => {
expect(findCancelButton().props('variant')).toBe('default');
expect(findCancelButton().props()).toMatchObject({
variant: 'default',
loading: false,
});
expect(findCancelButton().text()).toBe('Cancel');
expect(findCancelButton().props()).toMatchObject({ variant: 'default', loading: false });
});
it('shows the new role in the role selector', () => {
expect(findRoleSelector().props('value')).toBe(newRole);
});
});
it('does not call update role API', () => {
expect(axiosMock.history.put).toHaveLength(0);
});
describe('when cancel button is clicked', () => {
beforeEach(createWrapperAndChangeRole);
it('does not emit any events', () => {
expect(Object.keys(wrapper.emitted())).toHaveLength(0);
});
it('resets back to initial role when cancel button is clicked', async () => {
it('resets back to initial role', async () => {
findCancelButton().vm.$emit('click');
await nextTick();
@ -217,118 +175,113 @@ describe('Role details drawer', () => {
});
describe('when update role button is clicked', () => {
beforeEach(() => {
axiosMock.onPut('user/path/238').replyOnce(200);
createWrapperChangeRoleAndClickSave();
it('calls saveRole method on the role updater', async () => {
await createWrapperAndChangeRole();
findSaveButton().vm.$emit('click');
return nextTick();
expect(saveRoleStub).toHaveBeenCalledTimes(1);
});
});
it('calls update role API with expected data', () => {
const expectedData = JSON.stringify({
access_level: newRole.accessLevel,
member_role_id: newRole.memberRoleId,
describe('role updater', () => {
beforeEach(createWrapperAndChangeRole);
describe.each([true, false])('when busy event is %s', (busy) => {
beforeEach(() => {
findRoleUpdater().vm.$emit('busy', busy);
});
expect(axiosMock.history.put[0].data).toBe(expectedData);
it('sets loading on role selector', () => {
expect(findRoleSelector().props('loading')).toBe(busy);
});
it('sets loading on save button', () => {
expect(findSaveButton().props('loading')).toBe(busy);
});
it('sets disabled on cancel button', () => {
expect(findCancelButton().props('disabled')).toBe(busy);
});
});
it('disables footer buttons', () => {
expect(findSaveButton().props('loading')).toBe(true);
expect(findCancelButton().props('disabled')).toBe(true);
// This needs to be a separate test from the describe.each() block above because watchers aren't invoked if the
// value didn't change, so setting the busy state to false when it's already false will cause the test to fail.
// Here, we'll set it to true first, then false, which changes the value both times, thus invoking the watcher.
it('emits busy event when loading state is changed', async () => {
findRoleUpdater().vm.$emit('busy', true);
await nextTick();
expect(wrapper.emitted('busy')[0][0]).toBe(true);
findRoleUpdater().vm.$emit('busy', false);
await nextTick();
expect(wrapper.emitted('busy')[1][0]).toBe(false);
});
it('disables role dropdown', () => {
expect(findRoleSelector().props('loading')).toBe(true);
it('resets the selected role on a reset event', async () => {
await createWrapperAndChangeRole();
findRoleUpdater().vm.$emit('reset');
await nextTick();
expect(findRoleSelector().props('value')).toEqual(currentRole);
});
});
describe('alert', () => {
beforeEach(async () => {
await createWrapperAndChangeRole();
findRoleUpdater().vm.$emit('alert', {
message: 'alert message',
variant: 'info',
dismissible: false,
});
});
it('emits busy event as true', () => {
const busyEvents = wrapper.emitted('busy');
expect(busyEvents).toHaveLength(1);
expect(busyEvents[0][0]).toBe(true);
it('shows an alert when role updater changes the alert', () => {
expect(findAlert().text()).toBe('alert message');
expect(findAlert().props()).toMatchObject({ variant: 'info', dismissible: false });
});
it('does not close the drawer when it is trying to close', () => {
it('keeps alert when role updater resets selected role', async () => {
// Some workflows treat a role reset as a success. We shouldn't clear the alert in this case because it would
// clear out the success message.
findRoleUpdater().vm.$emit('reset');
await nextTick();
expect(findAlert().exists()).toBe(true);
});
it.each`
phrase | setupFn
${'when the role updater emits an empty alert'} | ${() => findRoleUpdater().vm.$emit('alert', null)}
${'when selected role is changed'} | ${() => findRoleSelector().vm.$emit('input', currentRole)}
${'when drawer is closed'} | ${() => findDrawer().vm.$emit('close')}
${'when member is changed'} | ${() => wrapper.setProps({ member: memberData })}
${'when alert is dismissed'} | ${() => findAlert().vm.$emit('dismiss')}
`('clears alert when $phrase', async ({ setupFn }) => {
setupFn();
await nextTick();
expect(findAlert().exists()).toBe(false);
});
});
describe('when drawer is closing', () => {
it('emits close event', () => {
createWrapper();
findDrawer().vm.$emit('close');
expect(wrapper.emitted('close')).toHaveLength(1);
});
it('does not allow the drawer to close when the role is saving', async () => {
await createWrapperAndChangeRole();
findRoleUpdater().vm.$emit('busy', true);
findDrawer().vm.$emit('close');
await nextTick();
expect(wrapper.emitted('close')).toBeUndefined();
});
});
describe('when update role API call is finished', () => {
beforeEach(() => {
axiosMock.onPut('user/path/238').replyOnce(200);
return createWrapperChangeRoleAndClickSave();
});
it('hides footer buttons', () => {
expect(findSaveButton().exists()).toBe(false);
expect(findCancelButton().exists()).toBe(false);
});
it('enables role selector', () => {
expect(findRoleSelector().props('loading')).toBe(false);
});
it('emits busy event with false', () => {
const busyEvents = wrapper.emitted('busy');
expect(busyEvents).toHaveLength(2);
expect(busyEvents[1][0]).toBe(false);
});
it('shows toast', () => {
expect(toastShowMock).toHaveBeenCalledTimes(1);
expect(toastShowMock).toHaveBeenCalledWith('Role was successfully updated.');
});
});
describe('when role admin approval is enabled and role is updated', () => {
beforeEach(() => {
axiosMock.onPut('user/path/238').replyOnce(200, { enqueued: true });
return createWrapperChangeRoleAndClickSave();
});
it('resets role back to initial role', () => {
expect(findRoleSelector().props('value')).toEqual(currentRole);
});
it('shows toast', () => {
expect(toastShowMock).toHaveBeenCalledTimes(1);
expect(toastShowMock).toHaveBeenCalledWith(
'Role change request was sent to the administrator.',
);
});
});
describe('when update role API fails', () => {
beforeEach(() => {
axiosMock.onPut('user/path/238').replyOnce(500);
return createWrapperChangeRoleAndClickSave();
});
it('enables save and cancel buttons', () => {
expect(findSaveButton().props('loading')).toBe(false);
expect(findCancelButton().props('disabled')).toBe(false);
});
it('enables role dropdown', () => {
expect(findRoleSelector().props('loading')).toBe(false);
});
it('emits busy event with false', () => {
const busyEvents = wrapper.emitted('busy');
expect(busyEvents).toHaveLength(2);
expect(busyEvents[1][0]).toBe(false);
});
it('shows error message', () => {
const alert = wrapper.findComponent(GlAlert);
expect(alert.text()).toBe('Could not update role.');
expect(alert.props('variant')).toBe('danger');
});
});
});

View File

@ -0,0 +1,126 @@
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RoleUpdater from '~/members/components/table/drawer/role_updater.vue';
import { callRoleUpdateApi, setMemberRole } from '~/members/components/table/drawer/utils';
import { captureException } from '~/sentry/sentry_browser_wrapper';
import { member } from '../../../mock_data';
jest.mock('~/members/components/table/drawer/utils');
jest.mock('~/sentry/sentry_browser_wrapper');
describe('Role updater CE', () => {
let wrapper;
const role = {};
const createWrapper = ({ slotContent = '' } = {}) => {
wrapper = shallowMountExtended(RoleUpdater, {
propsData: { member, role },
scopedSlots: { default: slotContent },
});
};
it('renders slot content', () => {
const slotContent = '<span>slot content</span>';
createWrapper({ slotContent });
expect(wrapper.html()).toContain(slotContent);
});
describe('when save is started', () => {
beforeEach(() => {
createWrapper();
// NOTE: We can't call wrapper.vm.saveRole() here because the tests will run on the next tick, so the microtask
// queue will be flushed beforehand and the entire saveRole function finishes executing. We need to instead call
// saveRole on the same frame as the expect() checks so that the microtask queue doesn't get a chance to flush.
});
it('emits busy = true event', () => {
wrapper.vm.saveRole();
expect(wrapper.emitted('busy')).toHaveLength(1);
expect(wrapper.emitted('busy')[0][0]).toBe(true);
});
it('calls role update API', () => {
wrapper.vm.saveRole();
expect(callRoleUpdateApi).toHaveBeenCalledTimes(1);
expect(callRoleUpdateApi).toHaveBeenCalledWith(member, role);
});
it('does not update member', () => {
wrapper.vm.saveRole();
expect(setMemberRole).not.toHaveBeenCalled();
});
it('emits alert event to clear alert', () => {
wrapper.vm.saveRole();
expect(wrapper.emitted('alert')).toHaveLength(1);
expect(wrapper.emitted('alert')[0][0]).toBe(null);
});
it('does not emit busy = false event', () => {
wrapper.vm.saveRole();
expect(wrapper.emitted('busy')).not.toHaveLength(2);
});
});
describe('when save is successful', () => {
beforeEach(() => {
createWrapper();
wrapper.vm.saveRole();
return nextTick();
});
it('updates member role', () => {
expect(setMemberRole).toHaveBeenCalledTimes(1);
expect(setMemberRole).toHaveBeenCalledWith(member, role);
});
it('emits success alert', () => {
expect(wrapper.emitted('alert')).toHaveLength(2);
expect(wrapper.emitted('alert')[1][0]).toEqual({
message: 'Role was successfully updated.',
variant: 'success',
});
});
it('emits busy = false event', () => {
expect(wrapper.emitted('busy')).toHaveLength(2);
expect(wrapper.emitted('busy')[1][0]).toBe(false);
});
});
describe('when save has an error', () => {
const error = new Error();
beforeEach(() => {
callRoleUpdateApi.mockRejectedValue(error);
createWrapper();
wrapper.vm.saveRole();
});
it('emits error alert', () => {
expect(wrapper.emitted('alert')).toHaveLength(2);
expect(wrapper.emitted('alert')[1][0]).toEqual({
message: 'Could not update role.',
variant: 'danger',
dismissible: false,
});
});
it('captures sentry exception', () => {
expect(captureException).toHaveBeenCalledTimes(1);
expect(captureException).toHaveBeenCalledWith(error);
});
it('emits busy = false event', () => {
expect(wrapper.emitted('busy')).toHaveLength(2);
expect(wrapper.emitted('busy')[1][0]).toBe(false);
});
});
});

View File

@ -309,12 +309,15 @@ describe('MembersTable', () => {
expect(findRoleDetailsDrawer().props('member')).toBe(null);
});
it('disables role button when drawer is busy', async () => {
findRoleDetailsDrawer().vm.$emit('busy', true);
await nextTick();
it.each([true, false])(
'enables/disables role button when drawer busy state is %s',
async (busy) => {
findRoleDetailsDrawer().vm.$emit('busy', busy);
await nextTick();
expect(findRoleButton().props('disabled')).toBe(true);
});
expect(findRoleButton().props('disabled')).toBe(busy);
},
);
});
});

View File

@ -14,7 +14,7 @@ import DeployKeyItem from '~/vue_shared/components/list_selector/deploy_key_item
import groupsAutocompleteQuery from '~/graphql_shared/queries/groups_autocomplete.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { ACCESS_LEVEL_DEVELOPER_INTEGER } from '~/access_level/constants';
import { ACCESS_LEVEL_REPORTER_INTEGER } from '~/access_level/constants';
import { USERS_RESPONSE_MOCK, GROUPS_RESPONSE_MOCK, SUBGROUPS_RESPONSE_MOCK } from './mock_data';
jest.mock('~/alert');
@ -275,7 +275,7 @@ describe('List Selector spec', () => {
it('calls query with correct variables when Search box receives an input', () => {
expect(Api.projectGroups).toHaveBeenCalledWith(USERS_MOCK_PROPS.projectPath, {
search,
shared_min_access_level: ACCESS_LEVEL_DEVELOPER_INTEGER,
shared_min_access_level: ACCESS_LEVEL_REPORTER_INTEGER,
with_shared: true,
});
});