Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
ef1f98e770
commit
3284638f52
|
|
@ -1254,42 +1254,6 @@ rspec-ee system pg15 es8:
|
||||||
- .rspec-ee-system-parallel
|
- .rspec-ee-system-parallel
|
||||||
|
|
||||||
# PG16
|
# 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:
|
rspec-ee migration pg16:
|
||||||
extends:
|
extends:
|
||||||
- .rspec-ee-base-pg16
|
- .rspec-ee-base-pg16
|
||||||
|
|
@ -1310,35 +1274,74 @@ rspec-ee unit pg16:
|
||||||
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
|
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
|
||||||
- .rspec-ee-unit-parallel
|
- .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:
|
rspec-ee integration pg16:
|
||||||
extends:
|
extends:
|
||||||
- .rspec-ee-base-pg16
|
- .rspec-ee-base-pg16
|
||||||
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
|
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
|
||||||
- .rspec-ee-integration-parallel
|
- .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:
|
rspec-ee system pg16:
|
||||||
extends:
|
extends:
|
||||||
- .rspec-ee-base-pg16
|
- .rspec-ee-base-pg16
|
||||||
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
|
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
|
||||||
- .rspec-ee-system-parallel
|
- .rspec-ee-system-parallel
|
||||||
|
|
||||||
rspec-ee system pg16 es8:
|
# We have too many jobs in nightly pipeline, more than 2k+,
|
||||||
extends:
|
# which exceeds the limit of jobs a pipeline can have. Disable below for now.
|
||||||
- .rspec-ee-base-pg16-es8
|
#
|
||||||
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
|
# rspec-ee unit pg16 opensearch1:
|
||||||
- .rspec-ee-system-parallel
|
# 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 #
|
# EE: default branch nightly scheduled jobs #
|
||||||
#####################################
|
#####################################
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
export const BYTES_IN_KIB = 1024;
|
export const BYTES_IN_KIB = 1024;
|
||||||
export const DEFAULT_DEBOUNCE_AND_THROTTLE_MS = 250;
|
export const DEFAULT_DEBOUNCE_AND_THROTTLE_MS = 250;
|
||||||
export const THOUSAND = 1000;
|
export const THOUSAND = 1000;
|
||||||
|
export const MILLION = THOUSAND ** 2;
|
||||||
export const TRUNCATE_WIDTH_DEFAULT_WIDTH = 80;
|
export const TRUNCATE_WIDTH_DEFAULT_WIDTH = 80;
|
||||||
export const TRUNCATE_WIDTH_DEFAULT_FONT_SIZE = 12;
|
export const TRUNCATE_WIDTH_DEFAULT_FONT_SIZE = 12;
|
||||||
export const BV_SHOW_MODAL = 'bv::show::modal';
|
export const BV_SHOW_MODAL = 'bv::show::modal';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { formatNumber, sprintf, __ } from '~/locale';
|
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
|
* 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 number Number to format
|
||||||
* @param digits The number of digits to appear after the decimal point
|
* @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
|
* @return {string} Formatted number
|
||||||
*/
|
*/
|
||||||
export function numberToMetricPrefix(number, digits = 1) {
|
export function numberToMetricPrefix(number, uppercase = false) {
|
||||||
if (number < THOUSAND) {
|
if (number < THOUSAND) {
|
||||||
return number.toString();
|
return number.toString();
|
||||||
}
|
}
|
||||||
if (number < THOUSAND ** 2) {
|
const digits = 1;
|
||||||
return `${Number((number / THOUSAND).toFixed(digits))}k`;
|
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
|
* A simple method that returns the value of a + b
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
MEMBER_MODEL_TYPE_GROUP_MEMBER,
|
MEMBER_MODEL_TYPE_GROUP_MEMBER,
|
||||||
MEMBER_MODEL_TYPE_PROJECT_MEMBER,
|
MEMBER_MODEL_TYPE_PROJECT_MEMBER,
|
||||||
} from '~/members/constants';
|
} from '~/members/constants';
|
||||||
|
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||||
import { I18N } from './constants';
|
import { I18N } from './constants';
|
||||||
import LeaveDropdownItem from './leave_dropdown_item.vue';
|
import LeaveDropdownItem from './leave_dropdown_item.vue';
|
||||||
import RemoveMemberDropdownItem from './remove_member_dropdown_item.vue';
|
import RemoveMemberDropdownItem from './remove_member_dropdown_item.vue';
|
||||||
|
|
@ -29,6 +30,7 @@ export default {
|
||||||
directives: {
|
directives: {
|
||||||
GlTooltip: GlTooltipDirective,
|
GlTooltip: GlTooltipDirective,
|
||||||
},
|
},
|
||||||
|
mixins: [glFeatureFlagMixin()],
|
||||||
props: {
|
props: {
|
||||||
member: {
|
member: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|
@ -94,7 +96,11 @@ export default {
|
||||||
: this.$options.i18n.leaveGroup;
|
: this.$options.i18n.leaveGroup;
|
||||||
},
|
},
|
||||||
showLdapOverride() {
|
showLdapOverride() {
|
||||||
return this.permissions.canOverride && !this.member.isOverridden;
|
return (
|
||||||
|
!this.glFeatures.showRoleDetailsInDrawer &&
|
||||||
|
this.permissions.canOverride &&
|
||||||
|
!this.member.isOverridden
|
||||||
|
);
|
||||||
},
|
},
|
||||||
showBan() {
|
showBan() {
|
||||||
return !this.isCurrentUser && this.permissions.canBan;
|
return !this.isCurrentUser && this.permissions.canBan;
|
||||||
|
|
|
||||||
|
|
@ -5,28 +5,14 @@ import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
|
||||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||||
import MembersTableCell from 'ee_else_ce/members/components/table/members_table_cell.vue';
|
import MembersTableCell from 'ee_else_ce/members/components/table/members_table_cell.vue';
|
||||||
import { ACCESS_LEVEL_LABELS } from '~/access_level/constants';
|
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 {
|
import {
|
||||||
getRoleDropdownItems,
|
getRoleDropdownItems,
|
||||||
getMemberRole,
|
getMemberRole,
|
||||||
} from 'ee_else_ce/members/components/table/drawer/utils';
|
} 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 RoleSelector from '~/members/components/role_selector.vue';
|
||||||
import MemberAvatar from '../member_avatar.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 {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
MemberAvatar,
|
MemberAvatar,
|
||||||
|
|
@ -37,11 +23,9 @@ export default {
|
||||||
GlIcon,
|
GlIcon,
|
||||||
GlAlert,
|
GlAlert,
|
||||||
RoleSelector,
|
RoleSelector,
|
||||||
|
RoleUpdater,
|
||||||
RoleBadges: () => import('ee_component/members/components/table/role_badges.vue'),
|
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: {
|
props: {
|
||||||
member: {
|
member: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|
@ -53,7 +37,7 @@ export default {
|
||||||
return {
|
return {
|
||||||
selectedRole: null,
|
selectedRole: null,
|
||||||
isSavingRole: false,
|
isSavingRole: false,
|
||||||
saveError: null,
|
alert: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -63,12 +47,16 @@ export default {
|
||||||
initialRole() {
|
initialRole() {
|
||||||
return getMemberRole(this.roles.flatten, this.member);
|
return getMemberRole(this.roles.flatten, this.member);
|
||||||
},
|
},
|
||||||
|
isRoleChanged() {
|
||||||
|
return this.selectedRole !== this.initialRole;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'member.accessLevel': {
|
member: {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
handler() {
|
handler() {
|
||||||
if (this.member) {
|
if (this.member) {
|
||||||
|
this.alert = null;
|
||||||
this.selectedRole = this.initialRole;
|
this.selectedRole = this.initialRole;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -76,72 +64,18 @@ export default {
|
||||||
isSavingRole() {
|
isSavingRole() {
|
||||||
this.$emit('busy', this.isSavingRole);
|
this.$emit('busy', this.isSavingRole);
|
||||||
},
|
},
|
||||||
selectedRole() {
|
|
||||||
this.saveError = null;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
closeDrawer() {
|
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) {
|
if (!this.isSavingRole) {
|
||||||
this.$emit('close');
|
this.$emit('close');
|
||||||
|
this.alert = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
checkGuestOverage() {
|
setRole(role) {
|
||||||
this.saveError = null;
|
this.selectedRole = role;
|
||||||
this.isSavingRole = true;
|
this.alert = null;
|
||||||
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;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getContentWrapperHeight,
|
getContentWrapperHeight,
|
||||||
|
|
@ -183,13 +117,14 @@ export default {
|
||||||
|
|
||||||
<dl>
|
<dl>
|
||||||
<dt class="gl-mb-3" data-testid="role-header">{{ s__('MemberRole|Role') }}</dt>
|
<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
|
<role-selector
|
||||||
v-if="permissions.canUpdate"
|
v-if="permissions.canUpdate"
|
||||||
v-model="selectedRole"
|
:value="selectedRole"
|
||||||
:roles="roles"
|
:roles="roles"
|
||||||
:loading="isSavingRole"
|
:loading="isSavingRole"
|
||||||
class="gl-w-full"
|
class="gl-w-full"
|
||||||
|
@input="setRole"
|
||||||
/>
|
/>
|
||||||
<span v-else data-testid="role-text">{{ selectedRole.text }}</span>
|
<span v-else data-testid="role-text">{{ selectedRole.text }}</span>
|
||||||
<role-badges :member="member" :role="selectedRole" />
|
<role-badges :member="member" :role="selectedRole" />
|
||||||
|
|
@ -207,7 +142,7 @@ export default {
|
||||||
{{ s__('MemberRole|Permissions') }}
|
{{ s__('MemberRole|Permissions') }}
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="gl-display-flex gl-mb-5">
|
<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}')">
|
<gl-sprintf :message="s__('MemberRole|Base role: %{role}')">
|
||||||
<template #role>
|
<template #role>
|
||||||
{{ $options.ACCESS_LEVEL_LABELS[selectedRole.accessLevel] }}
|
{{ $options.ACCESS_LEVEL_LABELS[selectedRole.accessLevel] }}
|
||||||
|
|
@ -245,36 +180,44 @@ export default {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div v-if="selectedRole !== initialRole">
|
<role-updater
|
||||||
<gl-alert v-if="saveError" class="gl-mb-5" variant="danger" :dismissible="false">
|
v-if="alert || isRoleChanged"
|
||||||
{{ saveError }}
|
#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-alert>
|
||||||
<gl-button
|
|
||||||
variant="confirm"
|
<div v-if="isRoleChanged">
|
||||||
:loading="isSavingRole"
|
<gl-button
|
||||||
data-testid="save-button"
|
variant="confirm"
|
||||||
@click="checkGuestOverage"
|
:loading="isSavingRole"
|
||||||
>
|
data-testid="save-button"
|
||||||
{{ s__('MemberRole|Update role') }}
|
@click="saveRole"
|
||||||
</gl-button>
|
>
|
||||||
<gl-button
|
{{ s__('MemberRole|Update role') }}
|
||||||
class="gl-ml-2"
|
</gl-button>
|
||||||
:disabled="isSavingRole"
|
<gl-button
|
||||||
data-testid="cancel-button"
|
class="gl-ml-2"
|
||||||
@click="resetRole"
|
:disabled="isSavingRole"
|
||||||
>
|
data-testid="cancel-button"
|
||||||
{{ __('Cancel') }}
|
@click="setRole(initialRole)"
|
||||||
</gl-button>
|
>
|
||||||
<guest-overage-confirmation
|
{{ __('Cancel') }}
|
||||||
ref="guestOverageConfirmation"
|
</gl-button>
|
||||||
:group-path="group.path"
|
</div>
|
||||||
:member="member"
|
</role-updater>
|
||||||
:role="selectedRole"
|
|
||||||
@confirm="updateRole"
|
|
||||||
@cancel="resetRole"
|
|
||||||
@error="showCheckOverageError"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</gl-drawer>
|
</gl-drawer>
|
||||||
</members-table-cell>
|
</members-table-cell>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -1,20 +1,14 @@
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { roleDropdownItems } from '~/members/utils';
|
import { roleDropdownItems, initialSelectedRole } from '~/members/utils';
|
||||||
import {
|
import {
|
||||||
GROUP_LINK_ACCESS_LEVEL_PROPERTY_NAME,
|
GROUP_LINK_ACCESS_LEVEL_PROPERTY_NAME,
|
||||||
MEMBER_ACCESS_LEVEL_PROPERTY_NAME,
|
MEMBER_ACCESS_LEVEL_PROPERTY_NAME,
|
||||||
MEMBERS_TAB_TYPES,
|
MEMBERS_TAB_TYPES,
|
||||||
} from '~/members/constants';
|
} from '~/members/constants';
|
||||||
|
|
||||||
export const getMemberRole = (roles, member) => {
|
// EE overrides these.
|
||||||
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.
|
|
||||||
export const getRoleDropdownItems = roleDropdownItems;
|
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.
|
// 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'.
|
// Users use 'access_level', groups use 'group_access'.
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ export default {
|
||||||
return state[this.namespace].members.map((member) => ({
|
return state[this.namespace].members.map((member) => ({
|
||||||
...member,
|
...member,
|
||||||
memberPath: state[this.namespace].memberPath.replace(':id', member.id),
|
memberPath: state[this.namespace].memberPath.replace(':id', member.id),
|
||||||
|
ldapOverridePath: state[this.namespace].ldapOverridePath?.replace(':id', member.id),
|
||||||
namespace: this.namespace,
|
namespace: this.namespace,
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { parseBoolean, convertObjectPropsToCamelCase } from '~/lib/utils/common_
|
||||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||||
import { createAlert } from '~/alert';
|
import { createAlert } from '~/alert';
|
||||||
import { __, sprintf } from '~/locale';
|
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 groupsAutocompleteQuery from '~/graphql_shared/queries/groups_autocomplete.query.graphql';
|
||||||
import Api from '~/api';
|
import Api from '~/api';
|
||||||
import { getProjects } from '~/rest_api';
|
import { getProjects } from '~/rest_api';
|
||||||
|
|
@ -159,7 +159,7 @@ export default {
|
||||||
return Api.projectGroups(this.projectPath, {
|
return Api.projectGroups(this.projectPath, {
|
||||||
search,
|
search,
|
||||||
with_shared: true,
|
with_shared: true,
|
||||||
shared_min_access_level: ACCESS_LEVEL_DEVELOPER_INTEGER,
|
shared_min_access_level: ACCESS_LEVEL_REPORTER_INTEGER,
|
||||||
}).then((data) =>
|
}).then((data) =>
|
||||||
data?.map((group) => ({
|
data?.map((group) => ({
|
||||||
text: group.full_name,
|
text: group.full_name,
|
||||||
|
|
|
||||||
|
|
@ -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 }))
|
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 })
|
expand_html = content_tag(:div, [expand_button, spinner].join.html_safe, data: { expand_wrapper: true })
|
||||||
else
|
else
|
||||||
expand_html = content_tag(:div, '...', data: { visible_when_loading: false, **expand_data })
|
expand_html = '...'
|
||||||
end
|
end
|
||||||
|
|
||||||
if old_pos
|
if old_pos
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,5 @@
|
||||||
= render Pajamas::ButtonComponent.new(href: project, variant: :danger, method: :delete, button_options: { data: { confirm: remove_project_message(project) } }) do
|
= render Pajamas::ButtonComponent.new(href: project,
|
||||||
= _('Delete')
|
method: :delete,
|
||||||
|
category: :tertiary,
|
||||||
|
icon: 'remove',
|
||||||
|
button_options: { class: 'has-tooltip', title: _('Delete'), data: { confirm: remove_project_message(project), confirm_btn_variant: 'danger' } })
|
||||||
|
|
|
||||||
|
|
@ -3,25 +3,21 @@
|
||||||
- add_page_specific_style 'page_bundles/projects'
|
- add_page_specific_style 'page_bundles/projects'
|
||||||
- @force_desktop_expanded_sidebar = true
|
- @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|
|
= render ::Layouts::CrudComponent.new(_('Projects'),
|
||||||
- c.with_header do
|
icon: 'project',
|
||||||
.gl-new-card-title-wrapper
|
count: @projects.size,
|
||||||
%h3.gl-new-card-title
|
options: { class: 'js-search-settings-section' }) do |c|
|
||||||
= _('Projects')
|
- c.with_actions do
|
||||||
.gl-new-card-count
|
- if can? current_user, :admin_group, @group
|
||||||
= sprite_icon('project', css_class: 'gl-mr-2')
|
= render Pajamas::ButtonComponent.new(href: new_project_path(namespace_id: @group.id), size: :small) do
|
||||||
= @projects.size
|
= _("New project")
|
||||||
.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")
|
|
||||||
- c.with_body do
|
- c.with_body do
|
||||||
%ul.content-list{ class: 'gl-px-3!' }
|
%ul.content-list
|
||||||
- @projects.each do |project|
|
- @projects.each do |project|
|
||||||
%li.project-row.gl-align-items-center{ class: 'gl-display-flex!' }
|
%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')
|
= 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
|
.gl-min-w-0.gl-flex-grow-1
|
||||||
.title
|
.title.gl-mr-5
|
||||||
= link_to project_path(project), class: 'js-prefetch-document' do
|
= link_to project_path(project), class: 'js-prefetch-document' do
|
||||||
%span.project-full-name
|
%span.project-full-name
|
||||||
%span.namespace-name
|
%span.namespace-name
|
||||||
|
|
@ -38,7 +34,7 @@
|
||||||
|
|
||||||
= render 'shared/projects/badges', project: project, css_class: 'gl-mr-3'
|
= 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)
|
= gl_badge_tag storage_counter(project.statistics&.storage_size)
|
||||||
.controls.gl-flex-shrink-0.gl-ml-5
|
.controls.gl-flex-shrink-0.gl-ml-5
|
||||||
= render Pajamas::ButtonComponent.new(href: project_project_members_path(project),
|
= render Pajamas::ButtonComponent.new(href: project_project_members_path(project),
|
||||||
|
|
@ -46,10 +42,11 @@
|
||||||
button_options: { class: 'gl-mr-2' }) do
|
button_options: { class: 'gl-mr-2' }) do
|
||||||
= _('View members')
|
= _('View members')
|
||||||
= render Pajamas::ButtonComponent.new(href: edit_project_path(project),
|
= render Pajamas::ButtonComponent.new(href: edit_project_path(project),
|
||||||
size: :small) do
|
category: :tertiary,
|
||||||
= _('Edit')
|
icon: 'pencil',
|
||||||
|
button_options: { class: 'has-tooltip', title: _('Edit') })
|
||||||
= render 'delete_project_button', project: project
|
= render 'delete_project_button', project: project
|
||||||
- if @projects.blank?
|
- if @projects.blank?
|
||||||
.nothing-here-block= _("This group has no projects yet")
|
.nothing-here-block= _("This group has no projects yet")
|
||||||
|
- c.with_pagination do
|
||||||
= paginate @projects, theme: "gitlab"
|
= paginate @projects, theme: "gitlab"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
92e9bbbcc3b79f03d6acadc9d4186b675bc7332a915bd96bd93b7da9eca40244
|
||||||
|
|
@ -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_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_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);
|
CREATE INDEX index_pm_package_version_licenses_on_pm_package_version_id ON pm_package_version_licenses USING btree (pm_package_version_id);
|
||||||
|
|
|
||||||
|
|
@ -353,7 +353,7 @@ You can administer all runners in the GitLab instance from the Admin area's **Ru
|
||||||
To access the **Runners** page:
|
To access the **Runners** page:
|
||||||
|
|
||||||
1. On the left sidebar, at the bottom, select **Admin area**.
|
1. On the left sidebar, at the bottom, select **Admin area**.
|
||||||
1. Select **Overview > Runners**.
|
1. Select **CI/CD > Runners**.
|
||||||
|
|
||||||
#### Search and filter runners
|
#### Search and filter runners
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -356,6 +356,8 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://git
|
||||||
|
|
||||||
- `204: No Content` if successfully revoked.
|
- `204: No Content` if successfully revoked.
|
||||||
- `400: Bad Request` if not revoked successfully.
|
- `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
|
### 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.
|
- `204: No Content` if successfully revoked.
|
||||||
- `400: Bad Request` if not revoked successfully.
|
- `400: Bad Request` if not revoked successfully.
|
||||||
|
- `401: Unauthorized` if the access token is invalid.
|
||||||
|
|
||||||
## Create a personal access token (administrator only)
|
## Create a personal access token (administrator only)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
- 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.
|
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
|
code paths for a long period of time, it's important to clean those up when we
|
||||||
safely can.
|
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
|
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).
|
[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
|
- We don't need to maintain any code that is called from our advanced search
|
||||||
migrations.
|
migrations.
|
||||||
|
|
@ -453,14 +458,13 @@ and remove tests so that:
|
||||||
- Operators who have not run this migration and who upgrade directly to the
|
- Operators who have not run this migration and who upgrade directly to the
|
||||||
target version see a message prompting them to reindex from scratch.
|
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
|
To be extra safe, we do not clean up migrations that were created in the last
|
||||||
minor version before the major upgrade. So, if we are upgrading to `%14.0`,
|
minor version before the last required stop. For example, if the last required stop
|
||||||
we should not delete migrations that were only added in `%13.12`. This
|
was `%14.0`, we should not clean up migrations that were only added in `%13.12`.
|
||||||
extra safety net allows for migrations that might
|
This extra safety net allows for migrations that might take multiple weeks to
|
||||||
take multiple weeks to finish on GitLab.com. It would be bad if we upgraded
|
finish on GitLab.com. Because our deployments to GitLab.com
|
||||||
GitLab.com to `%14.0` before the migrations in `%13.12` were finished. Because
|
are automated and we do not have automated checks to prevent this cleanup,
|
||||||
our deployments to GitLab.com are automated and we don't have
|
the extra precaution is warranted.
|
||||||
automated checks to prevent this, the extra precaution is warranted.
|
|
||||||
Additionally, even if we did have automated checks to prevent it, we wouldn't
|
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,
|
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
|
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
|
### Process for marking migrations as obsolete
|
||||||
|
|
||||||
For every migration that was created 2 minor versions before the major version
|
Run the [`Keeps::MarkOldAdvancedSearchMigrationsAsObsolete` Keep](../../../gems/gitlab-housekeeper/README.md#running-for-real)
|
||||||
being upgraded to, we do the following:
|
manually to mark migrations as obsolete.
|
||||||
|
|
||||||
1. Confirm the migration has actually completed successfully for GitLab.com.
|
For every migration that was created two versions before the last required stop,
|
||||||
1. Replace the content of the migration with:
|
the Keep:
|
||||||
|
|
||||||
|
1. Retains the content of the migration and adds a prepend to the bottom:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
include Elastic::MigrationObsolete
|
ClassName.prepend ::Elastic::MigrationObsolete
|
||||||
```
|
```
|
||||||
|
|
||||||
1. When marking a skippable migration as obsolete, keep the `skip_if` condition.
|
1. Replaces the spec file content with the `'a deprecated Advanced Search migration'` shared example.
|
||||||
1. Delete any spec files to support this migration.
|
1. Randomly selects a Global Search backend engineer as an assignee.
|
||||||
1. Verify that there are no references of the migration in the `.rubocop_todo/` directory.
|
1. Updates the dictionary file to mark the migration as obsolete.
|
||||||
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.
|
|
||||||
|
|
||||||
### Process for removing migrations
|
The MR assignee must:
|
||||||
|
|
||||||
1. Select migrations that were marked as obsolete before the current major release
|
1. Ensure the dictionary file has the correct `marked_obsolete_by_url` and `marked_obsolete_in_milestone`.
|
||||||
1. If the step above includes all obsolete migrations, keep one last migration as a safeguard for customers with unapplied migrations
|
1. Verify that no references to the migration or spec files exist in the `.rubocop_todo/` directory.
|
||||||
1. Delete migration files and spec files for those migrations
|
1. Remove any logic-handling backwards compatibility for this migration by
|
||||||
1. Verify that there are no references of the migrations in the `.rubocop_todo/` directory.
|
looking for `Elastic::DataMigrationService.migration_has_finished?(:migration_name_in_lowercase)`.
|
||||||
1. Create a merge request and assign it to a team member from the global search team.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,25 @@ You can include additional instructions to be considered. For example:
|
||||||
- Focus on performance, for example `/refactor improving performance`.
|
- Focus on performance, for example `/refactor improving performance`.
|
||||||
- Focus on potential vulnerabilities, for example `/refactor avoiding memory leaks and exploits`.
|
- 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
|
## Write tests in the IDE
|
||||||
|
|
||||||
DETAILS:
|
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) |
|
| /explain | [Explain code](../gitlab_duo_chat/examples.md#explain-code-in-the-ide) |
|
||||||
| /vulnerability_explain | [Explain current vulnerability](../gitlab_duo/index.md#vulnerability-explanation) |
|
| /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) |
|
| /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) |
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ In the IDEs, GitLab Duo Chat knows about these areas:
|
||||||
| Issues | Ask about the URL. |
|
| Issues | Ask about the URL. |
|
||||||
|
|
||||||
In addition, in the IDEs, when you use any of the slash commands,
|
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.
|
code you selected.
|
||||||
|
|
||||||
Duo Chat always has access to:
|
Duo Chat always has access to:
|
||||||
|
|
|
||||||
|
|
@ -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)
|
- [/tests](../gitlab_duo_chat/examples.md#write-tests-in-the-ide)
|
||||||
- [/refactor](../gitlab_duo_chat/examples.md#refactor-code-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)
|
- [/explain](../gitlab_duo_chat/examples.md#explain-code-in-the-ide)
|
||||||
|
|
||||||
## `Error M4001`
|
## `Error M4001`
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ The following features are extended from standard Markdown:
|
||||||
|
|
||||||
When you use GitLab Flavored Markdown, you are creating digital content.
|
When you use GitLab Flavored Markdown, you are creating digital content.
|
||||||
This content should be as accessible as possible to your audience.
|
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:
|
particular attention to:
|
||||||
|
|
||||||
### Accessible headings
|
### Accessible headings
|
||||||
|
|
@ -118,10 +118,10 @@ Don't use `image of` or `video of` in the description. For more information, see
|
||||||
|
|
||||||
## Line breaks
|
## 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
|
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
|
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
|
same paragraph. Use this approach if you want to keep long lines from wrapping, and keep
|
||||||
them editable:
|
them editable:
|
||||||
|
|
@ -171,7 +171,7 @@ A new line due to the previous backslash.
|
||||||
|
|
||||||
## Emphasis
|
## 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,
|
You can emphasize text in multiple ways. Use italics, bold, strikethrough,
|
||||||
or combine these emphasis styles together.
|
or combine these emphasis styles together.
|
||||||
|
|
@ -202,7 +202,7 @@ Strikethrough with double tildes. ~~Scratch this.~~
|
||||||
|
|
||||||
### Multiple underscores in words and mid-word emphasis
|
### 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
|
Avoid italicizing a portion of a word, especially when you're
|
||||||
dealing with code and names that often appear with multiple underscores.
|
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
|
### 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 -]`.
|
With inline diff tags, you can display `{+ additions +}` or `[- deletions -]`.
|
||||||
|
|
||||||
|
|
@ -270,7 +270,7 @@ However, you cannot mix the wrapping tags:
|
||||||
- [- deletion -}
|
- [- 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>\</code>:
|
each backtick with a backslash <code>\</code>:
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
|
|
@ -308,7 +308,7 @@ Alt-H2
|
||||||
|
|
||||||
> - Heading link generation [changed](https://gitlab.com/gitlab-org/gitlab/-/issues/440733) in GitLab 17.0.
|
> - 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
|
All Markdown-rendered headings automatically
|
||||||
get IDs that can be linked to, except in comments.
|
get IDs that can be linked to, except in comments.
|
||||||
|
|
@ -349,7 +349,7 @@ Would generate the following link IDs:
|
||||||
|
|
||||||
## Links
|
## 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:
|
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
|
### 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:
|
Almost any URL you put into your text is auto-linked:
|
||||||
|
|
||||||
|
|
@ -484,7 +484,7 @@ Reference-style:
|
||||||
|
|
||||||
### Videos
|
### 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
|
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`:
|
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 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.
|
> - 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
|
You can control the width and height of an image or video by following the image with
|
||||||
an attribute list.
|
an attribute list.
|
||||||
|
|
@ -529,7 +529,7 @@ resized to 75% of its dimensions.
|
||||||
|
|
||||||
### Audio
|
### 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
|
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`:
|
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
|
## 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.
|
You can create ordered and unordered lists.
|
||||||
|
|
||||||
For an ordered list, add the number you want the list
|
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
|
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
|
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
|
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.
|
1. And another item.
|
||||||
|
|
||||||
For an unordered list, add a `-`, `*` or `+`, followed by a space, at the start of
|
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
|
```markdown
|
||||||
Unordered lists can:
|
Unordered lists can:
|
||||||
|
|
@ -646,7 +646,8 @@ Example:
|
||||||
---
|
---
|
||||||
|
|
||||||
If the first item's paragraph isn't indented with the proper number of spaces,
|
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:
|
For example:
|
||||||
|
|
||||||
```markdown
|
```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
|
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.
|
_[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.
|
This makes the list look like there is extra spacing between each item.
|
||||||
|
|
||||||
For example:
|
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.
|
> - 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.
|
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
|
## 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
|
Use a blockquote to highlight information, such as a side note. It's generated
|
||||||
by starting the lines of the blockquote with `>`:
|
by starting the lines of the blockquote with `>`:
|
||||||
|
|
@ -761,7 +762,7 @@ Quote break.
|
||||||
|
|
||||||
### Multiline blockquote
|
### 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 `>>>`:
|
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
|
## 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.
|
Highlight anything that should be viewed as code and not standard text.
|
||||||
|
|
||||||
|
|
@ -848,7 +849,7 @@ Tildes are OK too.
|
||||||
|
|
||||||
### Syntax highlighting
|
### 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
|
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
|
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.
|
> - 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
|
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
|
[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 [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.
|
> - 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).
|
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._
|
_KaTeX only supports a [subset](https://katex.org/docs/supported.html) of LaTeX._
|
||||||
|
|
@ -1058,7 +1059,7 @@ $$
|
||||||
|
|
||||||
## Tables
|
## 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:
|
When creating tables:
|
||||||
|
|
||||||
|
|
@ -1076,7 +1077,7 @@ When creating tables:
|
||||||
by pipes (`|`).
|
by pipes (`|`).
|
||||||
- You **can** have blank cells.
|
- You **can** have blank cells.
|
||||||
- Column widths are calculated dynamically based on the content of the 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:
|
Example:
|
||||||
|
|
||||||
|
|
@ -1096,7 +1097,7 @@ Example:
|
||||||
|
|
||||||
### Alignment
|
### 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 (`:`)
|
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:
|
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
|
### 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
|
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:
|
use `<br>` tags to force a cell to have multiple lines:
|
||||||
|
|
@ -1396,7 +1397,7 @@ Second section content.
|
||||||
|
|
||||||
## Colors
|
## 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.
|
Markdown does not support changing text color.
|
||||||
|
|
||||||
|
|
@ -1435,7 +1436,7 @@ display a color chip next to the color code. For example:
|
||||||
|
|
||||||
## Emoji
|
## 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:">
|
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:">
|
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
|
## 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.
|
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
|
## 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:
|
Create a horizontal rule by using three or more hyphens, asterisks, or underscores:
|
||||||
|
|
||||||
|
|
@ -1622,7 +1675,7 @@ ___
|
||||||
|
|
||||||
## Inline HTML
|
## 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.
|
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
|
### 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)
|
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)
|
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
|
### 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.
|
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
|
### 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:
|
For superscripts and subscripts, use the standard HTML syntax:
|
||||||
|
|
||||||
|
|
@ -1812,8 +1865,8 @@ it links to `<your_wiki>/documentation/file.md`:
|
||||||
|
|
||||||
### Wiki - hierarchical link
|
### Wiki - hierarchical link
|
||||||
|
|
||||||
A hierarchical link can be constructed relative to the current wiki page by using `./<page>`,
|
A hierarchical link can be constructed relative to the current wiki page by using relative paths like `./<page>` or
|
||||||
`../<page>`, and so on.
|
`../<page>`.
|
||||||
|
|
||||||
If this example is on a page at `<your_wiki>/documentation/main`,
|
If this example is on a page at `<your_wiki>/documentation/main`,
|
||||||
it links to `<your_wiki>/documentation/related`:
|
it links to `<your_wiki>/documentation/related`:
|
||||||
|
|
|
||||||
|
|
@ -30,15 +30,14 @@ With GitLab Duo Code Suggestions, you get:
|
||||||
- Code generation, which generates code based on a natural language code
|
- Code generation, which generates code based on a natural language code
|
||||||
comment block. Write a comment like `# check if code suggestions are
|
comment block. Write a comment like `# check if code suggestions are
|
||||||
enabled for current user`, then press <kbd>Enter</kbd> to generate code based
|
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.
|
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:
|
||||||
Code generation requests are slower than code completion requests, but provide
|
|
||||||
more accurate responses because:
|
|
||||||
- A larger LLM is used.
|
- A larger LLM is used.
|
||||||
- Additional context is sent in the request, for example,
|
- Additional context is sent in the request, for example,
|
||||||
the libraries used by the project.
|
the libraries used by the project.
|
||||||
|
|
||||||
Code generation is used when the:
|
Code generation is used when the:
|
||||||
|
|
||||||
- User writes a comment and hits <kbd>Enter</kbd>.
|
- User writes a comment and hits <kbd>Enter</kbd>.
|
||||||
- File being edited is less than five lines of code.
|
- File being edited is less than five lines of code.
|
||||||
- User enters an empty function or method.
|
- User enters an empty function or method.
|
||||||
|
|
|
||||||
|
|
@ -57,14 +57,18 @@ gitlab-advanced-sast:
|
||||||
when: never
|
when: never
|
||||||
- if: $SAST_EXCLUDED_ANALYZERS =~ /gitlab-advanced-sast/
|
- if: $SAST_EXCLUDED_ANALYZERS =~ /gitlab-advanced-sast/
|
||||||
when: never
|
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:
|
exists:
|
||||||
- '**/*.py'
|
- '**/*.py'
|
||||||
- '**/*.go'
|
- '**/*.go'
|
||||||
- '**/*.java'
|
- '**/*.java'
|
||||||
- if: $CI_OPEN_MERGE_REQUESTS # Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
|
- if: $CI_OPEN_MERGE_REQUESTS # Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
|
||||||
when: never
|
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:
|
exists:
|
||||||
- '**/*.py'
|
- '**/*.py'
|
||||||
- '**/*.go'
|
- '**/*.go'
|
||||||
|
|
|
||||||
|
|
@ -32218,9 +32218,6 @@ msgstr ""
|
||||||
msgid "MemberRole|Change role"
|
msgid "MemberRole|Change role"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "MemberRole|Could not check guest overage."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "MemberRole|Could not fetch available permissions."
|
msgid "MemberRole|Could not fetch available permissions."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -32314,6 +32311,9 @@ msgstr ""
|
||||||
msgid "MemberRole|Permissions"
|
msgid "MemberRole|Permissions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "MemberRole|Reverted to LDAP group sync settings. The role will be updated after the next LDAP sync."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "MemberRole|Role"
|
msgid "MemberRole|Role"
|
||||||
msgstr ""
|
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."
|
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 ""
|
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."
|
msgid "MemberRole|This role has been manually selected and will not sync to the LDAP sync role."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
@ -32365,6 +32368,9 @@ msgstr ""
|
||||||
msgid "MemberRole|Update role"
|
msgid "MemberRole|Update role"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "MemberRole|Use LDAP sync role"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "MemberRole|View permissions"
|
msgid "MemberRole|View permissions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -113,5 +113,6 @@ FactoryBot.define do
|
||||||
|
|
||||||
factory :project_audit_event, traits: [:project_event]
|
factory :project_audit_event, traits: [:project_event]
|
||||||
factory :group_audit_event, traits: [:group_event]
|
factory :group_audit_event, traits: [:group_event]
|
||||||
|
factory :instance_audit_event, traits: [:instance_event]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,7 @@ describe('Number Utils', () => {
|
||||||
${123456789} | ${'123.5m'}
|
${123456789} | ${'123.5m'}
|
||||||
`('returns $expected given $number', ({ number, expected }) => {
|
`('returns $expected given $number', ({ number, expected }) => {
|
||||||
expect(numberToMetricPrefix(number)).toBe(expected);
|
expect(numberToMetricPrefix(number)).toBe(expected);
|
||||||
|
expect(numberToMetricPrefix(number, true)).toBe(expected.toUpperCase());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,45 @@
|
||||||
import { GlDrawer, GlSprintf, GlAlert } from '@gitlab/ui';
|
import { GlDrawer, GlAlert } from '@gitlab/ui';
|
||||||
import MockAdapter from 'axios-mock-adapter';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { nextTick } from 'vue';
|
import { nextTick } from 'vue';
|
||||||
import { cloneDeep } from 'lodash';
|
|
||||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||||
import RoleDetailsDrawer from '~/members/components/table/drawer/role_details_drawer.vue';
|
import RoleDetailsDrawer from '~/members/components/table/drawer/role_details_drawer.vue';
|
||||||
import MembersTableCell from '~/members/components/table/members_table_cell.vue';
|
import MembersTableCell from '~/members/components/table/members_table_cell.vue';
|
||||||
import MemberAvatar from '~/members/components/table/member_avatar.vue';
|
import MemberAvatar from '~/members/components/table/member_avatar.vue';
|
||||||
import RoleSelector from '~/members/components/role_selector.vue';
|
import RoleSelector from '~/members/components/role_selector.vue';
|
||||||
import { roleDropdownItems } from '~/members/utils';
|
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';
|
import { member as memberData, updateableMember } from '../../../mock_data';
|
||||||
|
|
||||||
|
jest.mock('~/lib/utils/dom_utils', () => ({
|
||||||
|
getContentWrapperHeight: () => '123',
|
||||||
|
}));
|
||||||
|
|
||||||
describe('Role details drawer', () => {
|
describe('Role details drawer', () => {
|
||||||
const dropdownItems = roleDropdownItems(updateableMember);
|
const dropdownItems = roleDropdownItems(updateableMember);
|
||||||
const toastShowMock = jest.fn();
|
|
||||||
const currentRole = dropdownItems.flatten[5];
|
const currentRole = dropdownItems.flatten[5];
|
||||||
const newRole = dropdownItems.flatten[2];
|
const newRole = dropdownItems.flatten[2];
|
||||||
let axiosMock;
|
const saveRoleStub = jest.fn();
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
const createWrapper = ({ member = updateableMember } = {}) => {
|
const createWrapper = ({ member = updateableMember } = {}) => {
|
||||||
wrapper = shallowMountExtended(RoleDetailsDrawer, {
|
wrapper = shallowMountExtended(RoleDetailsDrawer, {
|
||||||
propsData: { member },
|
propsData: { member },
|
||||||
provide: {
|
stubs: {
|
||||||
currentUserId: 1,
|
GlDrawer: stubComponent(GlDrawer, { template: RENDER_ALL_SLOTS_TEMPLATE }),
|
||||||
canManageMembers: true,
|
RoleUpdater: stubComponent(RoleUpdater, {
|
||||||
group: 'group/path',
|
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 findRoleText = () => wrapper.findByTestId('role-text');
|
||||||
const findRoleSelector = () => wrapper.findComponent(RoleSelector);
|
const findRoleSelector = () => wrapper.findComponent(RoleSelector);
|
||||||
const findRoleDescription = () => wrapper.findByTestId('description-value');
|
const findRoleDescription = () => wrapper.findByTestId('description-value');
|
||||||
|
const findRoleUpdater = () => wrapper.findComponent(RoleUpdater);
|
||||||
const findSaveButton = () => wrapper.findByTestId('save-button');
|
const findSaveButton = () => wrapper.findByTestId('save-button');
|
||||||
const findCancelButton = () => wrapper.findByTestId('cancel-button');
|
const findCancelButton = () => wrapper.findByTestId('cancel-button');
|
||||||
|
const findAlert = () => wrapper.findComponent(GlAlert);
|
||||||
|
|
||||||
const createWrapperChangeRoleAndClickSave = async () => {
|
const createWrapperAndChangeRole = () => {
|
||||||
createWrapper({ member: cloneDeep(updateableMember) });
|
createWrapper();
|
||||||
findRoleSelector().vm.$emit('input', newRole);
|
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', () => {
|
it('does not show the drawer when there is no member', () => {
|
||||||
createWrapper({ member: null });
|
createWrapper({ member: null });
|
||||||
|
|
||||||
|
|
@ -64,42 +66,30 @@ describe('Role details drawer', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when there is a member', () => {
|
describe('when there is a member', () => {
|
||||||
beforeEach(() => {
|
beforeEach(createWrapper);
|
||||||
createWrapper({ member: memberData });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows the drawer with expected props', () => {
|
it('shows the drawer', () => {
|
||||||
expect(findDrawer().props()).toMatchObject({ headerSticky: true, open: true, zIndex: 252 });
|
expect(findDrawer().props()).toMatchObject({
|
||||||
});
|
headerHeight: '123',
|
||||||
|
headerSticky: true,
|
||||||
it('shows the user avatar', () => {
|
open: true,
|
||||||
expect(wrapper.findComponent(MembersTableCell).props('member')).toBe(memberData);
|
zIndex: 252,
|
||||||
expect(wrapper.findComponent(MemberAvatar).props()).toMatchObject({
|
|
||||||
memberType: 'user',
|
|
||||||
isCurrentUser: false,
|
|
||||||
member: memberData,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not show footer buttons', () => {
|
it('shows the user avatar', () => {
|
||||||
expect(findSaveButton().exists()).toBe(false);
|
expect(wrapper.findComponent(MembersTableCell).props('member')).toBe(updateableMember);
|
||||||
expect(findCancelButton().exists()).toBe(false);
|
expect(wrapper.findComponent(MemberAvatar).props()).toEqual({
|
||||||
});
|
memberType: 'user',
|
||||||
|
isCurrentUser: false,
|
||||||
it('emits close event when drawer is closed', () => {
|
member: updateableMember,
|
||||||
findDrawer().vm.$emit('close');
|
});
|
||||||
|
|
||||||
expect(wrapper.emitted('close')).toHaveLength(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('role name', () => {
|
describe('role name', () => {
|
||||||
it('shows the header', () => {
|
it('shows the header', () => {
|
||||||
expect(wrapper.findByTestId('role-header').text()).toBe('Role');
|
expect(wrapper.findByTestId('role-header').text()).toBe('Role');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows the role name', () => {
|
|
||||||
expect(findRoleText().text()).toContain('Owner');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('role description', () => {
|
describe('role description', () => {
|
||||||
|
|
@ -110,17 +100,6 @@ describe('Role details drawer', () => {
|
||||||
it('shows the role description', () => {
|
it('shows the role description', () => {
|
||||||
expect(findRoleDescription().text()).toBe(currentRole.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', () => {
|
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', () => {
|
it('shows role name when the member cannot be edited', () => {
|
||||||
createWrapper({ member: memberData });
|
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', () => {
|
describe('when role is changed', () => {
|
||||||
beforeEach(() => {
|
beforeEach(createWrapperAndChangeRole);
|
||||||
createWrapper();
|
|
||||||
findRoleSelector().vm.$emit('input', newRole);
|
it('shows role updater', () => {
|
||||||
|
expect(findRoleUpdater().props()).toEqual({ member: updateableMember, role: newRole });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows save button', () => {
|
it('shows save button', () => {
|
||||||
expect(findSaveButton().text()).toBe('Update role');
|
expect(findSaveButton().text()).toBe('Update role');
|
||||||
expect(findSaveButton().props()).toMatchObject({
|
expect(findSaveButton().props()).toMatchObject({ variant: 'confirm', loading: false });
|
||||||
variant: 'confirm',
|
|
||||||
loading: false,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows cancel button', () => {
|
it('shows cancel button', () => {
|
||||||
expect(findCancelButton().props('variant')).toBe('default');
|
expect(findCancelButton().text()).toBe('Cancel');
|
||||||
expect(findCancelButton().props()).toMatchObject({
|
expect(findCancelButton().props()).toMatchObject({ variant: 'default', loading: false });
|
||||||
variant: 'default',
|
|
||||||
loading: false,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows the new role in the role selector', () => {
|
it('shows the new role in the role selector', () => {
|
||||||
expect(findRoleSelector().props('value')).toBe(newRole);
|
expect(findRoleSelector().props('value')).toBe(newRole);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('does not call update role API', () => {
|
describe('when cancel button is clicked', () => {
|
||||||
expect(axiosMock.history.put).toHaveLength(0);
|
beforeEach(createWrapperAndChangeRole);
|
||||||
});
|
|
||||||
|
|
||||||
it('does not emit any events', () => {
|
it('resets back to initial role', async () => {
|
||||||
expect(Object.keys(wrapper.emitted())).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('resets back to initial role when cancel button is clicked', async () => {
|
|
||||||
findCancelButton().vm.$emit('click');
|
findCancelButton().vm.$emit('click');
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
|
|
@ -217,118 +175,113 @@ describe('Role details drawer', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when update role button is clicked', () => {
|
describe('when update role button is clicked', () => {
|
||||||
beforeEach(() => {
|
it('calls saveRole method on the role updater', async () => {
|
||||||
axiosMock.onPut('user/path/238').replyOnce(200);
|
await createWrapperAndChangeRole();
|
||||||
createWrapperChangeRoleAndClickSave();
|
findSaveButton().vm.$emit('click');
|
||||||
|
|
||||||
return nextTick();
|
expect(saveRoleStub).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('calls update role API with expected data', () => {
|
describe('role updater', () => {
|
||||||
const expectedData = JSON.stringify({
|
beforeEach(createWrapperAndChangeRole);
|
||||||
access_level: newRole.accessLevel,
|
|
||||||
member_role_id: newRole.memberRoleId,
|
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', () => {
|
// This needs to be a separate test from the describe.each() block above because watchers aren't invoked if the
|
||||||
expect(findSaveButton().props('loading')).toBe(true);
|
// value didn't change, so setting the busy state to false when it's already false will cause the test to fail.
|
||||||
expect(findCancelButton().props('disabled')).toBe(true);
|
// 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', () => {
|
it('resets the selected role on a reset event', async () => {
|
||||||
expect(findRoleSelector().props('loading')).toBe(true);
|
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', () => {
|
it('shows an alert when role updater changes the alert', () => {
|
||||||
const busyEvents = wrapper.emitted('busy');
|
expect(findAlert().text()).toBe('alert message');
|
||||||
|
expect(findAlert().props()).toMatchObject({ variant: 'info', dismissible: false });
|
||||||
expect(busyEvents).toHaveLength(1);
|
|
||||||
expect(busyEvents[0][0]).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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');
|
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();
|
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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -309,12 +309,15 @@ describe('MembersTable', () => {
|
||||||
expect(findRoleDetailsDrawer().props('member')).toBe(null);
|
expect(findRoleDetailsDrawer().props('member')).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('disables role button when drawer is busy', async () => {
|
it.each([true, false])(
|
||||||
findRoleDetailsDrawer().vm.$emit('busy', true);
|
'enables/disables role button when drawer busy state is %s',
|
||||||
await nextTick();
|
async (busy) => {
|
||||||
|
findRoleDetailsDrawer().vm.$emit('busy', busy);
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
expect(findRoleButton().props('disabled')).toBe(true);
|
expect(findRoleButton().props('disabled')).toBe(busy);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 groupsAutocompleteQuery from '~/graphql_shared/queries/groups_autocomplete.query.graphql';
|
||||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||||
import waitForPromises from 'helpers/wait_for_promises';
|
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';
|
import { USERS_RESPONSE_MOCK, GROUPS_RESPONSE_MOCK, SUBGROUPS_RESPONSE_MOCK } from './mock_data';
|
||||||
|
|
||||||
jest.mock('~/alert');
|
jest.mock('~/alert');
|
||||||
|
|
@ -275,7 +275,7 @@ describe('List Selector spec', () => {
|
||||||
it('calls query with correct variables when Search box receives an input', () => {
|
it('calls query with correct variables when Search box receives an input', () => {
|
||||||
expect(Api.projectGroups).toHaveBeenCalledWith(USERS_MOCK_PROPS.projectPath, {
|
expect(Api.projectGroups).toHaveBeenCalledWith(USERS_MOCK_PROPS.projectPath, {
|
||||||
search,
|
search,
|
||||||
shared_min_access_level: ACCESS_LEVEL_DEVELOPER_INTEGER,
|
shared_min_access_level: ACCESS_LEVEL_REPORTER_INTEGER,
|
||||||
with_shared: true,
|
with_shared: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue