Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-05-03 18:07:53 +00:00
parent 62098c11d1
commit 8bdfdd49b3
85 changed files with 1050 additions and 384 deletions

View file

@ -38,6 +38,15 @@ rules:
promise/always-return: off
promise/no-callback-in-promise: off
'@gitlab/no-global-event-off': error
'@gitlab/vue-no-new-non-primitive-in-template':
- error
- allowNames:
- 'class(es)?$'
- '^style$'
- '^to$'
- '^$'
- '^variables$'
- 'attrs?$'
no-param-reassign:
- error
- props: true

View file

@ -283,13 +283,17 @@ export default {
<paginated-table-with-search-and-tabs
:show-error-msg="showErrorMsg"
:i18n="$options.i18n"
:items="alerts.list || []"
:items="
alerts.list || [] /* eslint-disable-line @gitlab/vue-no-new-non-primitive-in-template */
"
:page-info="alerts.pageInfo"
:items-count="alertsCount"
:status-tabs="$options.statusTabs"
:track-views-options="$options.trackAlertListViewsOptions"
:server-error-message="serverErrorMessage"
:filter-search-tokens="['assignee_username']"
:filter-search-tokens="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [
'assignee_username',
] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
filter-search-key="alerts"
@page-changed="pageChanged"
@tabs-changed="statusChanged"
@ -305,7 +309,11 @@ export default {
<template #table>
<gl-table
class="alert-management-table"
:items="alerts ? alerts.list : []"
:items="
alerts
? alerts.list
: [] /* eslint-disable-line @gitlab/vue-no-new-non-primitive-in-template */
"
:fields="$options.fields"
:show-empty="true"
:busy="loading"

View file

@ -132,12 +132,12 @@ export default {
v-else
:option="options"
:include-legend-avg-max="true"
:data="[
:data="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [
{
name: $options.i18n.yAxisTitle,
data: chartUserData,
},
]"
] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
/>
</div>
</template>

View file

@ -85,10 +85,11 @@ export default {
:list="list"
:data-draggable-item-type="$options.draggableItemTypes.list"
:disabled="disabled"
:class="{ 'gl-xs-display-none!': addColumnFormVisible }"
/>
<transition name="slide" @after-enter="afterFormEnters">
<board-add-new-column v-if="addColumnFormVisible" />
<board-add-new-column v-if="addColumnFormVisible" class="gl-xs-w-full!" />
</transition>
</component>

View file

@ -135,14 +135,14 @@ export default {
:modal-id="$options.modalId"
:title="$options.i18n.modalAction"
size="sm"
:action-primary="{
:action-primary="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
text: $options.i18n.modalAction,
attributes: [{ variant: 'danger' }],
}"
:action-secondary="{
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
:action-secondary="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
text: $options.i18n.modalCancel,
attributes: [{ variant: 'default' }],
}"
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
@primary="handleModalPrimary"
>
<p>{{ $options.i18n.modalCopy }}</p>

View file

@ -33,7 +33,7 @@ export default {
class="issues-details-filters filtered-search-block gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row row-content-block second-block"
>
<div
class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-flex-grow-1 gl-lg-mb-0! mb-md-2 mb-sm-0 gl-w-full"
class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-flex-grow-1 gl-lg-mb-0 gl-mb-3 gl-w-full"
>
<boards-selector />
<new-board-button />
@ -41,7 +41,7 @@ export default {
<issue-board-filtered-search v-else />
</div>
<div
class="filter-dropdown-container gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-align-items-flex-start"
class="filter-dropdown-container gl-md-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-align-items-flex-start"
>
<toggle-labels />
<toggle-epics-swimlanes v-if="swimlanesFeatureAvailable && isSignedIn" />

View file

@ -107,7 +107,9 @@ export default {
ref="modal"
:modal-id="modalId"
:title="__('Please solve the captcha')"
:action-cancel="{ text: __('Cancel') }"
:action-cancel="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
text: __('Cancel'),
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
@shown="shown"
@hide="hide"
@hidden="$emit('hidden')"

View file

@ -212,7 +212,6 @@ export default {
<template #table-header-actions>
<div v-if="canRenderPipelineButton" class="gl-text-right">
<gl-button
variant="confirm"
data-testid="run_pipeline_button"
:loading="state.isRunningMergeRequestPipeline"
@click="tryRunPipeline"

View file

@ -97,7 +97,9 @@ export default {
:editor="tiptapEditor"
plugin-key="bubbleMenuCodeBlock"
:should-show="shouldShow"
:tippy-options="{ getReferenceClientRect }"
:tippy-options="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
getReferenceClientRect,
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
>
<editor-state-observer @transaction="updateSelectedLanguage">
<gl-button-group>

View file

@ -84,7 +84,9 @@ export default {
content-type="link"
icon-name="link"
editor-command="toggleLink"
:editor-command-params="{ href: '' }"
:editor-command-params="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
href: '',
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
category="tertiary"
size="medium"
:label="__('Insert link')"

View file

@ -116,7 +116,9 @@ export default {
:editor="tiptapEditor"
plugin-key="bubbleMenuLink"
:should-show="() => shouldShow()"
:tippy-options="{ placement: 'bottom' }"
:tippy-options="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
placement: 'bottom',
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
>
<editor-state-observer @transaction="updateLinkToState">
<gl-button-group v-if="!isEditing" class="gl-display-flex gl-align-items-center">

View file

@ -124,7 +124,9 @@ export default {
no-caret
text-sr-only
:text="$options.i18n.editTableActions"
:popper-opts="{ positionFixed: true }"
:popper-opts="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
positionFixed: true,
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
@hide="handleHide($event)"
>
<gl-dropdown-item @click="runCommand('addColumnBefore')">

View file

@ -349,7 +349,9 @@ export default {
class="gl-display-flex gl-overflow-hidden gl-flex-grow-1 gl-flex-direction-column gl-relative"
>
<design-destroyer
:filenames="[design.filename]"
:filenames="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [
design.filename,
] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
:project-path="projectPath"
:iid="issueIid"
@done="$router.push({ name: $options.DESIGNS_ROUTE_NAME })"

View file

@ -51,7 +51,7 @@ export default {
__(
'To preserve performance only %{strongStart}%{visible} of %{total}%{strongEnd} files are displayed.',
),
{ visible, total },
{ visible, total } /* eslint-disable-line @gitlab/vue-no-new-non-primitive-in-template */,
)
"
>

View file

@ -132,10 +132,10 @@ export default {
<design-note-pin
v-if="canComment && currentCommentForm"
:position="{
:position="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
left: `${currentCommentForm.xPercent}%`,
top: `${currentCommentForm.yPercent}%`,
}"
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
/>
</div>
</template>

View file

@ -54,7 +54,9 @@ export default {
:key="`${tab.name}-${i}`"
:active="tab.isActive"
:title-item-class="tab.isActive ? 'gl-outline-none' : ''"
:title-link-attributes="{ 'data-testid': `environments-tab-${tab.scope}` }"
:title-link-attributes="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
'data-testid': `environments-tab-${tab.scope}`,
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
@click="onChangeTab(tab.scope)"
>
<template #title>

View file

@ -216,7 +216,9 @@ export default {
modal-id="ide-commit-error-modal"
:title="lastCommitError.title"
:action-primary="commitErrorPrimaryAction.button"
:action-cancel="{ text: __('Cancel') }"
:action-cancel="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
text: __('Cancel'),
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
@ok="commitErrorPrimaryAction.callback"
>
<div v-safe-html="lastCommitError.messageHTML"></div>

View file

@ -338,7 +338,9 @@ export default {
:show-items="showList"
:show-error-msg="showErrorMsg"
:i18n="$options.i18n"
:items="incidents.list || []"
:items="
incidents.list || [] /* eslint-disable-line @gitlab/vue-no-new-non-primitive-in-template */
"
:page-info="incidents.pageInfo"
:items-count="incidentsCount"
:status-tabs="$options.statusTabs"
@ -372,7 +374,10 @@ export default {
<template #table>
<gl-table
:items="incidents.list || []"
:items="
incidents.list ||
[] /* eslint-disable-line @gitlab/vue-no-new-non-primitive-in-template */
"
:fields="availableFields"
:busy="loading"
stacked="md"

View file

@ -88,6 +88,11 @@ export default {
type: Array,
required: true,
},
usersLimitDataset: {
type: Object,
required: false,
default: () => ({}),
},
},
data() {
return {
@ -146,6 +151,18 @@ export default {
isOnLearnGitlab() {
return this.source === LEARN_GITLAB;
},
reachedLimit() {
if (this.usersLimitDataset.freeUsersLimit && this.usersLimitDataset.membersCount) {
return this.usersLimitDataset.membersCount >= this.usersLimitDataset.freeUsersLimit;
}
return false;
},
formGroupDescription() {
return this.reachedLimit
? this.$options.labels.placeHolderDisabled
: this.$options.labels.placeHolder;
},
},
mounted() {
eventHub.$on('openModal', (options) => {
@ -274,12 +291,15 @@ export default {
:help-link="helpLink"
:label-intro-text="labelIntroText"
:label-search-field="$options.labels.searchField"
:form-group-description="$options.labels.placeHolder"
:form-group-description="formGroupDescription"
:submit-disabled="inviteDisabled"
:invalid-feedback-message="invalidFeedbackMessage"
:is-loading="isLoading"
:new-users-to-invite="newUsersToInvite"
:root-group-id="rootId"
:reached-limit="reachedLimit"
:members-path="usersLimitDataset.membersPath"
:purchase-path="usersLimitDataset.purchasePath"
@reset="resetFields"
@submit="sendInvite"
@access-level="onAccessLevelUpdate"
@ -294,7 +314,10 @@ export default {
</template>
<template #user-limit-notification>
<user-limit-notification />
<user-limit-notification
:reached-limit="reachedLimit"
:users-limit-dataset="usersLimitDataset"
/>
</template>
<template #select="{ validationState, labelId }">

View file

@ -8,7 +8,9 @@ import {
GlLink,
GlSprintf,
GlFormInput,
GlIcon,
} from '@gitlab/ui';
import Tracking from '~/tracking';
import { sprintf } from '~/locale';
import ContentTransition from '~/vue_shared/components/content_transition.vue';
import {
@ -16,8 +18,13 @@ import {
ACCESS_EXPIRE_DATE,
READ_MORE_TEXT,
INVITE_BUTTON_TEXT,
INVITE_BUTTON_TEXT_DISABLED,
CANCEL_BUTTON_TEXT,
CANCEL_BUTTON_TEXT_DISABLED,
HEADER_CLOSE_LABEL,
ON_SHOW_TRACK_LABEL,
ON_CLOSE_TRACK_LABEL,
ON_SUBMIT_TRACK_LABEL,
} from '../constants';
const DEFAULT_SLOT = 'default';
@ -41,8 +48,10 @@ export default {
GlDropdownItem,
GlSprintf,
GlFormInput,
GlIcon,
ContentTransition,
},
mixins: [Tracking.mixin()],
inheritAttrs: false,
props: {
modalTitle: {
@ -122,6 +131,21 @@ export default {
required: false,
default: false,
},
reachedLimit: {
type: Boolean,
required: false,
default: false,
},
membersPath: {
type: String,
required: false,
default: '',
},
purchasePath: {
type: String,
required: false,
default: '',
},
},
data() {
// Be sure to check out reset!
@ -151,20 +175,25 @@ export default {
},
actionPrimary() {
return {
text: this.submitButtonText,
text: this.reachedLimit ? INVITE_BUTTON_TEXT_DISABLED : this.submitButtonText,
attributes: {
variant: 'confirm',
disabled: this.submitDisabled,
loading: this.isLoading,
disabled: this.reachedLimit ? false : this.submitDisabled,
loading: this.reachedLimit ? false : this.isLoading,
'data-qa-selector': 'invite_button',
...(this.reachedLimit && { href: this.membersPath }),
},
};
},
actionCancel() {
return {
text: this.cancelButtonText,
text: this.reachedLimit ? CANCEL_BUTTON_TEXT_DISABLED : this.cancelButtonText,
...(this.reachedLimit && { attributes: { href: this.purchasePath } }),
};
},
selectLabelClass() {
return `col-form-label ${this.reachedLimit ? 'gl-text-gray-500' : ''}`;
},
},
watch: {
selectedAccessLevel: {
@ -183,15 +212,24 @@ export default {
this.$emit('reset');
},
onShowModal() {
if (this.reachedLimit) {
this.track('render', { category: 'default', label: ON_SHOW_TRACK_LABEL });
}
},
onCloseModal(e) {
if (this.preventCancelDefault) {
if (this.preventCancelDefault || this.reachedLimit) {
e.preventDefault();
} else {
this.onReset();
this.$refs.modal.hide();
}
this.$emit('cancel');
if (this.reachedLimit) {
this.track('click_button', { category: 'default', label: ON_CLOSE_TRACK_LABEL });
} else {
this.$emit('cancel');
}
},
changeSelectedItem(item) {
this.selectedAccessLevel = item;
@ -200,10 +238,14 @@ export default {
// We never want to hide when submitting
e.preventDefault();
this.$emit('submit', {
accessLevel: this.selectedAccessLevel,
expiresAt: this.selectedDate,
});
if (this.reachedLimit) {
this.track('click_button', { category: 'default', label: ON_SUBMIT_TRACK_LABEL });
} else {
this.$emit('submit', {
accessLevel: this.selectedAccessLevel,
expiresAt: this.selectedDate,
});
}
},
},
HEADER_CLOSE_LABEL,
@ -227,6 +269,7 @@ export default {
:header-close-label="$options.HEADER_CLOSE_LABEL"
:action-primary="actionPrimary"
:action-cancel="actionCancel"
@shown="onShowModal"
@primary="onSubmit"
@cancel="onCloseModal"
@hidden="onReset"
@ -255,64 +298,73 @@ export default {
<gl-form-group
:invalid-feedback="invalidFeedbackMessage"
:state="validationState"
:description="formGroupDescription"
data-testid="members-form-group"
>
<label :id="selectLabelId" class="col-form-label">{{ labelSearchField }}</label>
<slot name="select" v-bind="{ validationState, labelId: selectLabelId }"></slot>
<template #description>
<gl-icon v-if="reachedLimit" name="lock" />
{{ formGroupDescription }}
</template>
<label :id="selectLabelId" :class="selectLabelClass">{{ labelSearchField }}</label>
<gl-form-input v-if="reachedLimit" data-testid="disabled-input" disabled />
<slot v-else name="select" v-bind="{ validationState, labelId: selectLabelId }"></slot>
</gl-form-group>
<label class="gl-font-weight-bold">{{ $options.ACCESS_LEVEL }}</label>
<div class="gl-mt-2 gl-w-half gl-xs-w-full">
<gl-dropdown
class="gl-shadow-none gl-w-full"
data-qa-selector="access_level_dropdown"
v-bind="$attrs"
:text="selectedRoleName"
>
<template v-for="(key, item) in accessLevels">
<gl-dropdown-item
:key="key"
active-class="is-active"
is-check-item
:is-checked="key === selectedAccessLevel"
@click="changeSelectedItem(key)"
>
<div>{{ item }}</div>
</gl-dropdown-item>
</template>
</gl-dropdown>
</div>
<template v-if="!reachedLimit">
<label class="gl-font-weight-bold">{{ $options.ACCESS_LEVEL }}</label>
<div class="gl-mt-2 gl-w-half gl-xs-w-full">
<gl-sprintf :message="$options.READ_MORE_TEXT">
<template #link="{ content }">
<gl-link :href="helpLink" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</div>
<div class="gl-mt-2 gl-w-half gl-xs-w-full">
<gl-dropdown
class="gl-shadow-none gl-w-full"
data-qa-selector="access_level_dropdown"
v-bind="$attrs"
:text="selectedRoleName"
>
<template v-for="(key, item) in accessLevels">
<gl-dropdown-item
:key="key"
active-class="is-active"
is-check-item
:is-checked="key === selectedAccessLevel"
@click="changeSelectedItem(key)"
>
<div>{{ item }}</div>
</gl-dropdown-item>
</template>
</gl-dropdown>
</div>
<label class="gl-mt-5 gl-display-block" for="expires_at">{{
$options.ACCESS_EXPIRE_DATE
}}</label>
<div class="gl-mt-2 gl-w-half gl-xs-w-full gl-display-inline-block">
<gl-datepicker
v-model="selectedDate"
class="gl-display-inline!"
:min-date="minDate"
:target="null"
>
<template #default="{ formattedDate }">
<gl-form-input
class="gl-w-full"
:value="formattedDate"
:placeholder="__(`YYYY-MM-DD`)"
/>
</template>
</gl-datepicker>
</div>
<slot name="form-after"></slot>
<div class="gl-mt-2 gl-w-half gl-xs-w-full">
<gl-sprintf :message="$options.READ_MORE_TEXT">
<template #link="{ content }">
<gl-link :href="helpLink" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</div>
<label class="gl-mt-5 gl-display-block" for="expires_at">{{
$options.ACCESS_EXPIRE_DATE
}}</label>
<div class="gl-mt-2 gl-w-half gl-xs-w-full gl-display-inline-block">
<gl-datepicker
v-model="selectedDate"
class="gl-display-inline!"
:min-date="minDate"
:target="null"
>
<template #default="{ formattedDate }">
<gl-form-input
class="gl-w-full"
:value="formattedDate"
:placeholder="__(`YYYY-MM-DD`)"
/>
</template>
</gl-datepicker>
</div>
<slot name="form-after"></slot>
</template>
</template>
<template v-for="{ key } in extraSlots" #[key]>
<slot :name="key"></slot>
</template>

View file

@ -134,10 +134,10 @@ export default {
:hide-dropdown-with-no-items="hideDropdownWithNoItems"
:placeholder="placeholderText"
:aria-labelledby="ariaLabelledby"
:text-input-attrs="{
:text-input-attrs="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
'data-testid': 'members-token-select-input',
'data-qa-selector': 'members_token_select_input',
}"
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
@blur="handleBlur"
@text-input="handleTextInput"
@input="handleInput"

View file

@ -1,35 +1,50 @@
<script>
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import { s__, n__, sprintf } from '~/locale';
import { n__, sprintf } from '~/locale';
import {
WARNING_ALERT_TITLE,
DANGER_ALERT_TITLE,
REACHED_LIMIT_MESSAGE,
CLOSE_TO_LIMIT_MESSAGE,
} from '../constants';
const CLOSE_TO_LIMIT_COUNT = 2;
const WARNING_ALERT_TITLE = s__(
'InviteMembersModal|You only have space for %{count} more %{members} in %{name}',
);
const DANGER_ALERT_TITLE = s__(
"InviteMembersModal|You've reached your %{count} %{members} limit for %{name}",
);
const CLOSE_TO_LIMIT_MESSAGE = s__(
'InviteMembersModal|To get more members an owner of this namespace can %{trialLinkStart}start a trial%{trialLinkEnd} or %{upgradeLinkStart}upgrade%{upgradeLinkEnd} to a paid tier.',
);
const REACHED_LIMIT_MESSAGE = s__(
'InviteMembersModal|New members will be unable to participate. You can manage your members by removing ones you no longer need.',
).concat(' ', CLOSE_TO_LIMIT_MESSAGE);
export default {
name: 'UserLimitNotification',
components: { GlAlert, GlSprintf, GlLink },
inject: ['name', 'newTrialRegistrationPath', 'purchasePath', 'freeUsersLimit', 'membersCount'],
inject: ['name'],
props: {
reachedLimit: {
type: Boolean,
required: true,
},
usersLimitDataset: {
type: Object,
required: false,
default: () => ({}),
},
},
computed: {
reachedLimit() {
return this.isLimit();
freeUsersLimit() {
return this.usersLimitDataset.freeUsersLimit;
},
membersCount() {
return this.usersLimitDataset.membersCount;
},
newTrialRegistrationPath() {
return this.usersLimitDataset.newTrialRegistrationPath;
},
purchasePath() {
return this.usersLimitDataset.purchasePath;
},
closeToLimit() {
return this.isLimit(CLOSE_TO_LIMIT_COUNT);
if (this.freeUsersLimit && this.membersCount) {
return this.membersCount >= this.freeUsersLimit - CLOSE_TO_LIMIT_COUNT;
}
return false;
},
warningAlertTitle() {
return sprintf(WARNING_ALERT_TITLE, {
@ -60,13 +75,6 @@ export default {
},
},
methods: {
isLimit(deviation = 0) {
if (this.freeUsersLimit && this.membersCount) {
return this.membersCount >= this.freeUsersLimit - deviation;
}
return false;
},
pluralMembers(count) {
return n__('member', 'members', count);
},

View file

@ -35,8 +35,11 @@ export const MEMBERS_TO_PROJECT_DEFAULT_INTRO_TEXT = s__(
export const MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT = s__(
"InviteMembersModal|Congratulations on creating your project, you're almost there!",
);
export const MEMBERS_SEARCH_FIELD = s__('InviteMembersModal|GitLab member or email address');
export const MEMBERS_SEARCH_FIELD = s__('InviteMembersModal|Username or email address');
export const MEMBERS_PLACEHOLDER = s__('InviteMembersModal|Select members or type email addresses');
export const MEMBERS_PLACEHOLDER_DISABLED = s__(
'InviteMembersModal|This feature is disabled until this group has space for more members.',
);
export const MEMBERS_TASKS_TO_BE_DONE_TITLE = s__(
'InviteMembersModal|Create issues for your new team member to work on (optional)',
);
@ -66,7 +69,9 @@ export const READ_MORE_TEXT = s__(
`InviteMembersModal|%{linkStart}Read more%{linkEnd} about role permissions`,
);
export const INVITE_BUTTON_TEXT = s__('InviteMembersModal|Invite');
export const INVITE_BUTTON_TEXT_DISABLED = s__('InviteMembersModal|Manage members');
export const CANCEL_BUTTON_TEXT = s__('InviteMembersModal|Cancel');
export const CANCEL_BUTTON_TEXT_DISABLED = s__('InviteMembersModal|Explore paid plans');
export const HEADER_CLOSE_LABEL = s__('InviteMembersModal|Close invite team members');
export const MEMBER_MODAL_LABELS = {
@ -94,6 +99,7 @@ export const MEMBER_MODAL_LABELS = {
},
searchField: MEMBERS_SEARCH_FIELD,
placeHolder: MEMBERS_PLACEHOLDER,
placeHolderDisabled: MEMBERS_PLACEHOLDER_DISABLED,
tasksToBeDone: {
title: MEMBERS_TASKS_TO_BE_DONE_TITLE,
noProjects: MEMBERS_TASKS_TO_BE_DONE_NO_PROJECTS,
@ -118,3 +124,19 @@ export const GROUP_MODAL_LABELS = {
};
export const LEARN_GITLAB = 'learn_gitlab';
export const ON_SHOW_TRACK_LABEL = 'locked_modal_viewed';
export const ON_CLOSE_TRACK_LABEL = 'explore_paid_plans_clicked';
export const ON_SUBMIT_TRACK_LABEL = 'manage_members_clicked';
export const WARNING_ALERT_TITLE = s__(
'InviteMembersModal|You only have space for %{count} more %{members} in %{name}',
);
export const DANGER_ALERT_TITLE = s__(
"InviteMembersModal|You've reached your %{count} %{members} limit for %{name}",
);
export const REACHED_LIMIT_MESSAGE = s__(
'InviteMembersModal|You cannot add more members, but you can remove members who no longer need access. To get more members and access to additional paid features, an owner of this namespace can start a trial or upgrade to a paid tier.',
);
export const CLOSE_TO_LIMIT_MESSAGE = s__(
'InviteMembersModal|To get more members an owner of this namespace can %{trialLinkStart}start a trial%{trialLinkEnd} or %{upgradeLinkStart}upgrade%{upgradeLinkEnd} to a paid tier.',
);

View file

@ -1,7 +1,7 @@
import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import InviteMembersModal from '~/invite_members/components/invite_members_modal.vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import { parseBoolean, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
Vue.use(GlToast);
@ -26,10 +26,6 @@ export default (function initInviteMembersModal() {
provide: {
name: el.dataset.name,
newProjectPath: el.dataset.newProjectPath,
newTrialRegistrationPath: el.dataset.newTrialRegistrationPath,
purchasePath: el.dataset.purchasePath,
freeUsersLimit: el.dataset.freeUsersLimit && parseInt(el.dataset.freeUsersLimit, 10),
membersCount: el.dataset.membersCount && parseInt(el.dataset.membersCount, 10),
},
render: (createElement) =>
createElement(InviteMembersModal, {
@ -42,6 +38,9 @@ export default (function initInviteMembersModal() {
projects: JSON.parse(el.dataset.projects || '[]'),
usersFilter: el.dataset.usersFilter,
filterId: parseInt(el.dataset.filterId, 10),
usersLimitDataset: convertObjectPropsToCamelCase(
JSON.parse(el.dataset.usersLimitDataset || '{}'),
),
},
}),
});

View file

@ -304,7 +304,10 @@ export default {
:text="data.value || $options.currentUsername"
class="w-100"
:aria-label="
sprintf($options.dropdownLabel, { jiraDisplayName: data.item.jiraDisplayName })
/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
sprintf($options.dropdownLabel, {
jiraDisplayName: data.item.jiraDisplayName,
}) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */
"
@hide="resetDropdown"
>

View file

@ -54,7 +54,9 @@ export default {
<gl-tab
v-for="tab in tabs"
:key="tab.text"
:title-link-attributes="{ 'data-testid': tab.testId }"
:title-link-attributes="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
'data-testid': tab.testId,
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
@click="$emit('fetchJobsByStatus', tab.scope)"
>
<template #title>

View file

@ -492,7 +492,9 @@ export default {
v-if="!groupSingleEmptyState(groupData.key)"
:value="groupData.panels"
group="metrics-dashboard"
:component-data="{ attrs: { class: 'row mx-0 w-100' } }"
:component-data="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
attrs: { class: 'row mx-0 w-100' },
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
:disabled="!isRearrangingPanels"
@input="updatePanels(groupData.key, $event)"
>

View file

@ -83,11 +83,13 @@ export default {
modal-id="delete-tag-modal"
ok-variant="danger"
size="sm"
:action-primary="{
:action-primary="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
text: __('Delete'),
attributes: [{ variant: 'danger' }, { disabled: disablePrimaryButton }],
}"
:action-cancel="{ text: __('Cancel') }"
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
:action-cancel="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
text: __('Cancel'),
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
@primary="$emit('confirmDelete')"
@cancel="$emit('cancelDelete')"
@change="projectPath = ''"

View file

@ -168,7 +168,9 @@ export default {
<div>
<persisted-search
class="gl-mb-5"
:sortable-fields="[$options.searchConfig.NAME_SORT_FIELD]"
:sortable-fields="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [
$options.searchConfig.NAME_SORT_FIELD,
] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
:default-order="$options.searchConfig.NAME_SORT_FIELD.orderBy"
default-sort="asc"
@update="handleSearchUpdate"

View file

@ -370,7 +370,10 @@ export default {
ref="deleteModal"
size="sm"
modal-id="delete-image-modal"
:action-primary="{ text: __('Remove'), attributes: { variant: 'danger' } }"
:action-primary="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
text: __('Remove'),
attributes: { variant: 'danger' },
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
@primary="doDelete"
@cancel="track('cancel_delete')"
>

View file

@ -191,7 +191,10 @@ export default {
<package-list-row
v-for="v in packageEntity.versions"
:key="v.id"
:package-entity="{ name: packageEntity.name, ...v }"
:package-entity="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
name: packageEntity.name,
...v,
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
:package-link="v.id.toString()"
:disable-delete="true"
:show-package-type="false"

View file

@ -33,7 +33,7 @@ export default {
<registry-search
:filter="filter"
:sorting="sorting"
:tokens="[]"
:tokens="[] /* eslint-disable-line @gitlab/vue-no-new-non-primitive-in-template */"
:sortable-fields="sortableFields"
@sorting:changed="updateSorting"
@filter:changed="setFilter"

View file

@ -21,9 +21,7 @@ const PANELS = [
name: 'import-group-pane',
selector: '#import-group-pane',
title: s__('GroupsNew|Import group'),
description: s__(
'GroupsNew|Export groups with all their related data and move to a new GitLab instance.',
),
description: s__('GroupsNew|Import a group and related data from another GitLab instance.'),
illustration: importGroupIllustration,
details: 'Migrate your existing groups from another instance of GitLab.',
},

View file

@ -46,7 +46,9 @@ export default {
:value="mergedYaml"
:file-name="ciConfigPath"
:file-global-id="fileGlobalId"
:editor-options="{ readOnly: true }"
:editor-options="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
readOnly: true,
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
v-on="$listeners"
/>
</div>

View file

@ -87,7 +87,9 @@ export default {
<div v-if="pipelineStages.length > 0" class="stage-cell gl-mr-5">
<linked-pipelines-mini-list
v-if="upstreamPipeline"
:triggered-by="[upstreamPipeline]"
:triggered-by="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [
upstreamPipeline,
] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
data-testid="pipeline-editor-mini-graph-upstream"
/>
<pipeline-mini-graph class="gl-display-inline" :stages="pipelineStages" />

View file

@ -204,7 +204,9 @@ export default {
<h3 class="gl-font-lg gl-text-gray-900 gl-mt-5">{{ $options.i18n.noWalkthroughTitle }}</h3>
<p>{{ $options.i18n.noWalkthroughExplanation }}</p>
<ci-templates
:filter-templates="[$options.iOSTemplateName]"
:filter-templates="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [
$options.iOSTemplateName,
] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
:disabled="!isRunnerSetupFinished"
/>
<p>

View file

@ -114,7 +114,9 @@ export default {
variant="link"
:aria-label="stageAriaLabel(stage.title)"
:lazy="true"
:popper-opts="{ placement: 'bottom' }"
:popper-opts="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
placement: 'bottom',
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
:toggle-class="['mini-pipeline-graph-dropdown-toggle', triggerButtonClass]"
menu-class="mini-pipeline-graph-dropdown-menu"
@show="onShowDropdown"

View file

@ -174,7 +174,9 @@ export default {
<div></div>
<linked-pipelines-mini-list
v-if="item.triggered_by"
:triggered-by="[item.triggered_by]"
:triggered-by="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [
item.triggered_by,
] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
data-testid="mini-graph-upstream"
/>
<pipeline-mini-graph

View file

@ -126,7 +126,9 @@ export default {
<div v-else>
<linked-pipelines-mini-list
v-if="upstreamPipeline"
:triggered-by="[upstreamPipeline]"
:triggered-by="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [
upstreamPipeline,
] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
data-testid="commit-box-mini-graph-upstream"
/>

View file

@ -195,7 +195,10 @@ export default {
:path-id-separator="pathIdSeparator"
:input-value="inputValue"
:auto-complete-sources="transformedAutocompleteSources"
:auto-complete-options="{ issues: autoCompleteIssues, epics: autoCompleteEpics }"
:auto-complete-options="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
issues: autoCompleteIssues,
epics: autoCompleteEpics,
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
:issuable-type="issuableType"
@pendingIssuableRemoveRequest="onPendingIssuableRemoveRequest"
@formCancel="onFormCancel"

View file

@ -115,14 +115,14 @@ export default {
<gl-modal
size="sm"
:modal-id="$options.modalId"
:action-primary="{
:action-primary="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
text: $options.i18n.modalAction,
attributes: [{ variant: 'danger' }],
}"
:action-secondary="{
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
:action-secondary="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
text: $options.i18n.modalCancel,
attributes: [{ variant: 'default' }],
}"
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
:title="$options.i18n.modalTitle"
@primary="handleModalPrimary"
>

View file

@ -18,7 +18,6 @@ import {
PARAM_KEY_BEFORE,
DEFAULT_SORT,
RUNNER_PAGE_SIZE,
STATUS_NEVER_CONTACTED,
} from './constants';
import { getPaginationVariables } from './utils';
@ -84,7 +83,6 @@ const getPaginationFromParams = (params) => {
};
// Outdated URL parameters
const STATUS_NOT_CONNECTED = 'NOT_CONNECTED';
const STATUS_ACTIVE = 'ACTIVE';
const STATUS_PAUSED = 'PAUSED';
@ -116,10 +114,6 @@ export const updateOutdatedUrl = (url = window.location.href) => {
const status = params[PARAM_KEY_STATUS]?.[0] || null;
switch (status) {
case STATUS_NOT_CONNECTED:
return updateUrlParams(url, {
[PARAM_KEY_STATUS]: [STATUS_NEVER_CONTACTED],
});
case STATUS_ACTIVE:
return updateUrlParams(url, {
[PARAM_KEY_PAUSED]: ['false'],

View file

@ -132,7 +132,7 @@ export default {
<gl-area-chart
ref="areaChart"
v-bind="$attrs"
:data="[]"
:data="[] /* eslint-disable-line @gitlab/vue-no-new-non-primitive-in-template */"
:option="chartOptions"
:format-tooltip-text="formatTooltipText"
:width="width"

View file

@ -167,7 +167,9 @@ export default {
:content="editableContent"
:initial-edit-type="editorMode"
:image-root="imageRoot"
:options="{ customRenderers }"
:options="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
customRenderers,
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
class="mb-9 pb-6 h-100"
@modeChange="onModeChange"
@input="onInputChange"

View file

@ -311,7 +311,10 @@ export default {
data-testid="extension-list-item"
>
<gl-intersection-observer
:options="{ rootMargin: '100px', thresholds: 0.1 }"
:options="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
rootMargin: '100px',
thresholds: 0.1,
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
class="gl-w-full"
@appear="appear(index)"
@disappear="disappear(index)"

View file

@ -328,7 +328,7 @@ export default {
</script>
<template>
<div class="vue-filtered-search-bar-container d-md-flex">
<div class="vue-filtered-search-bar-container gl-md-display-flex">
<gl-form-checkbox
v-if="showCheckbox"
class="gl-align-self-center"

View file

@ -211,10 +211,22 @@ export default {
@select="handleTokenValueSelected"
>
<template #view-token="viewTokenProps">
<slot name="view-token" :view-token-props="{ ...viewTokenProps, activeTokenValue }"></slot>
<slot
name="view-token"
:view-token-props="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
...viewTokenProps,
activeTokenValue,
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
></slot>
</template>
<template #view="viewTokenProps">
<slot name="view" :view-token-props="{ ...viewTokenProps, activeTokenValue }"></slot>
<slot
name="view"
:view-token-props="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
...viewTokenProps,
activeTokenValue,
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
></slot>
</template>
<template v-if="suggestionsEnabled" #suggestions>
<template v-if="showDefaultSuggestions">

View file

@ -198,7 +198,10 @@ export default {
<toolbar-button
tag="**"
:button-title="
sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), { modifierKey })
/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), {
modifierKey,
}) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */
"
:shortcuts="$options.shortcuts.bold"
icon="bold"
@ -206,7 +209,10 @@ export default {
<toolbar-button
tag="_"
:button-title="
sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), { modifierKey })
/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), {
modifierKey,
}) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */
"
:shortcuts="$options.shortcuts.italic"
icon="italic"
@ -215,8 +221,9 @@ export default {
v-if="!restrictedToolBarItems.includes('strikethrough')"
tag="~~"
:button-title="
/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
sprintf(s__('MarkdownEditor|Add strikethrough text (%{modifierKey}⇧X)'), {
modifierKey,
modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */,
})
"
:shortcuts="$options.shortcuts.strikethrough"
@ -273,7 +280,10 @@ export default {
tag="[{text}](url)"
tag-select="url"
:button-title="
sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), { modifierKey })
/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), {
modifierKey,
}) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */
"
:shortcuts="$options.shortcuts.link"
icon="link"

View file

@ -90,7 +90,9 @@ export default {
modal-id="upload-metric-modal"
size="sm"
:action-primary="actionPrimaryProps"
:action-cancel="{ text: $options.i18n.modalCancel }"
:action-cancel="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
text: $options.i18n.modalCancel,
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
:title="$options.i18n.modalTitle"
:visible="modalVisible"
@hidden="clearInputs"

View file

@ -159,7 +159,9 @@ export default {
size="sm"
:visible="modalVisible"
:action-primary="deleteActionPrimaryProps"
:action-cancel="{ text: $options.i18n.modalCancel }"
:action-cancel="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
text: $options.i18n.modalCancel,
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
@primary.prevent="onDelete"
@hidden="resetEditFields"
>
@ -177,7 +179,9 @@ export default {
modal-id="edit-metric-modal"
size="sm"
:action-primary="updateActionPrimaryProps"
:action-cancel="{ text: $options.i18n.modalCancel }"
:action-cancel="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
text: $options.i18n.modalCancel,
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
:visible="editModalVisible"
data-testid="metric-image-edit-modal"
@hidden="resetEditFields"

View file

@ -61,7 +61,9 @@ export default {
v-for="(tab, i) in tabs"
:key="i"
:title-link-class="`js-${scope}-tab-${tab.scope} gl-display-inline-flex`"
:title-link-attributes="{ 'data-testid': `${scope}-tab-${tab.scope}` }"
:title-link-attributes="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
'data-testid': `${scope}-tab-${tab.scope}`,
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
:active="tab.isActive"
@click="onTabClick(tab)"
>

View file

@ -49,7 +49,7 @@
margin: 0 0 10px;
}
.dropdown-menu-toggle,
.dropdown-menu-toggle.dropdown-menu-toggle,
.update-issues-btn .btn {
width: 100%;
}

View file

@ -738,7 +738,7 @@ body {
}
}
@media (max-width: 767.98px) {
.dropdown-menu-toggle {
.dropdown-menu-toggle.dropdown-menu-toggle {
width: 100%;
}
}

View file

@ -723,7 +723,7 @@ body {
}
}
@media (max-width: 767.98px) {
.dropdown-menu-toggle {
.dropdown-menu-toggle.dropdown-menu-toggle {
width: 100%;
}
}

View file

@ -16,6 +16,10 @@ module Resolvers
}).freeze
def ready?(**args)
# TODO remove when cleaning up packages_graphql_pipelines_resolver
# https://gitlab.com/gitlab-org/gitlab/-/issues/358432
context.scoped_set!(:packages_access_level, :group)
context[self.class] ||= { executions: 0 }
context[self.class][:executions] += 1
raise GraphQL::ExecutionError, "Packages can be requested only for one group at a time" if context[self.class][:executions] > 1
@ -26,7 +30,10 @@ module Resolvers
def resolve(sort:, **filters)
return unless packages_available?
::Packages::GroupPackagesFinder.new(current_user, object, filters.merge(GROUP_SORT_TO_PARAMS_MAP.fetch(sort))).execute
params = filters.merge(GROUP_SORT_TO_PARAMS_MAP.fetch(sort))
params[:preload_pipelines] = false
::Packages::GroupPackagesFinder.new(current_user, object, params).execute
end
end
end

View file

@ -12,7 +12,86 @@ module Resolvers
alias_method :package, :object
# this resolver can be called for 100 packages max and we want to limit the
# number of build infos returned for _each_ package when using the new finder.
MAX_PAGE_SIZE = 20
def resolve(first: nil, last: nil, after: nil, before: nil, lookahead:)
case detect_mode
when :object_field
package.pipelines
when :new_finder
resolve_with_new_finder(first: first, last: last, after: after, before: before, lookahead: lookahead)
else
resolve_with_old_finder(first: first, last: last, after: after, before: before, lookahead: lookahead)
end
end
# we manage the pagination manually, so opt out of the connection field extension
def self.field_options
super.merge(
connection: false,
extras: [:lookahead]
)
end
private
# TODO remove when cleaning up packages_graphql_pipelines_resolver
# https://gitlab.com/gitlab-org/gitlab/-/issues/358432
def detect_mode
return :new_finder if Feature.enabled?(:packages_graphql_pipelines_resolver, default_enabled: :yaml)
return :object_field if context[:packages_access_level] == :group || context[:packages_access_level] == :project
:old_finder
end
# This returns a promise for a connection of promises for pipelines:
# Lazy[Connection[Lazy[Pipeline]]] structure
# TODO rename to #resolve when cleaning up packages_graphql_pipelines_resolver
# https://gitlab.com/gitlab-org/gitlab/-/issues/358432
def resolve_with_new_finder(first:, last:, after:, before:, lookahead:)
default_value = default_value_for(first: first, last: last, after: after, before: before)
BatchLoader::GraphQL.for(package.id)
.batch(default_value: default_value) do |package_ids, loader|
build_infos = ::Packages::BuildInfosForManyPackagesFinder.new(
package_ids,
first: first,
last: last,
after: decode_cursor(after),
before: decode_cursor(before),
max_page_size: MAX_PAGE_SIZE,
support_next_page: lookahead.selects?(:page_info)
).execute
build_infos.each do |build_info|
loader.call(build_info.package_id) do |connection|
connection.items << lazy_load_pipeline(build_info.pipeline_id)
connection
end
end
end
end
def lazy_load_pipeline(id)
::Gitlab::Graphql::Loaders::BatchModelLoader.new(::Ci::Pipeline, id)
.find
end
def default_value_for(first:, last:, after:, before:)
Gitlab::Graphql::Pagination::ActiveRecordArrayConnection.new(
[],
first: first,
last: last,
after: after,
before: before,
max_page_size: MAX_PAGE_SIZE
)
end
# TODO remove when cleaning up packages_graphql_pipelines_resolver
# https://gitlab.com/gitlab-org/gitlab/-/issues/358432
def resolve_with_old_finder(first:, last:, after:, before:, lookahead:)
finder = ::Packages::BuildInfosFinder.new(
package,
first: first,
@ -29,16 +108,6 @@ module Resolvers
::Ci::Pipeline.id_in(build_infos.pluck_pipeline_ids)
end
# we manage the pagination manually, so opt out of the connection field extension
def self.field_options
super.merge(
connection: false,
extras: [:lookahead]
)
end
private
def decode_cursor(encoded)
return unless encoded

View file

@ -5,10 +5,21 @@ module Resolvers
class ProjectPackagesResolver < PackagesBaseResolver
# The GraphQL type is defined in the extended class
# TODO remove when cleaning up packages_graphql_pipelines_resolver
# https://gitlab.com/gitlab-org/gitlab/-/issues/358432
def ready?(**args)
context.scoped_set!(:packages_access_level, :project)
super
end
def resolve(sort:, **filters)
return unless packages_available?
::Packages::PackagesFinder.new(object, filters.merge(SORT_TO_PARAMS_MAP.fetch(sort))).execute
params = filters.merge(SORT_TO_PARAMS_MAP.fetch(sort))
params[:preload_pipelines] = false
::Packages::PackagesFinder.new(object, params).execute
end
end
end

View file

@ -17,13 +17,6 @@ module Types
field :dependency_links, Types::Packages::PackageDependencyLinkType.connection_type, null: true, description: 'Dependency link.'
# this is an override of Types::Packages::PackageType.pipelines
# in order to use a custom resolver: Resolvers::PackagePipelinesResolver
field :pipelines,
resolver: Resolvers::PackagePipelinesResolver,
description: 'Pipelines that built the package.',
deprecated: { reason: 'Due to scalability concerns, this field is going to be removed', milestone: '14.6' }
field :composer_config_repository_url, GraphQL::Types::String, null: true, description: 'Url of the Composer setup endpoint.'
field :composer_url, GraphQL::Types::String, null: true, description: 'Url of the Composer endpoint.'
field :conan_url, GraphQL::Types::String, null: true, description: 'Url of the Conan project endpoint.'

View file

@ -19,9 +19,9 @@ module Types
description: 'Package metadata.'
field :name, GraphQL::Types::String, null: false, description: 'Name of the package.'
field :package_type, Types::Packages::PackageTypeEnum, null: false, description: 'Package type.'
field :pipelines, Types::Ci::PipelineType.connection_type, null: true,
description: 'Pipelines that built the package.',
deprecated: { reason: 'Due to scalability concerns, this field is going to be removed', milestone: '14.6' }
field :pipelines,
resolver: Resolvers::PackagePipelinesResolver,
description: "Pipelines that built the package. Max page size #{Resolvers::PackagePipelinesResolver::MAX_PAGE_SIZE}."
field :project, Types::ProjectType, null: false, description: 'Project where the package is stored.'
field :status, Types::Packages::PackageStatusEnum, null: false, description: 'Package status.'
field :tags, Types::Packages::PackageTagType.connection_type, null: true, description: 'Package tags.'

View file

@ -14,11 +14,21 @@ module Integrations
# - https://gitlab.com/gitlab-org/slack-notifier#middleware
# - https://gitlab.com/gitlab-org/gitlab/-/issues/347048
notifier = ::Slack::Messenger.new(webhook, opts.merge(http_client: HTTPClient))
notifier.ping(
responses = notifier.ping(
message.pretext,
attachments: message.attachments,
fallback: message.fallback
)
responses.each do |response|
unless response.success?
log_error('SlackMattermostNotifier HTTP error response',
request_host: response.request.uri.host,
response_code: response.code,
response_body: response.body
)
end
end
end
class HTTPClient

View file

@ -462,7 +462,7 @@ class ContainerRepository < ApplicationRecord
end
def start_expiration_policy!
update!(expiration_policy_started_at: Time.zone.now)
update!(expiration_policy_started_at: Time.zone.now, last_cleanup_deleted_tags_count: nil)
end
def size

View file

@ -7,6 +7,6 @@ class Packages::BuildInfo < ApplicationRecord
scope :pluck_pipeline_ids, -> { pluck(:pipeline_id) }
scope :without_empty_pipelines, -> { where.not(pipeline_id: nil) }
scope :order_by_pipeline_id, -> (direction) { order(pipeline_id: direction) }
scope :with_pipeline_id_less_than, -> (pipeline_id) { where("pipeline_id < ?", pipeline_id) }
scope :with_pipeline_id_greater_than, -> (pipeline_id) { where("pipeline_id > ?", pipeline_id) }
scope :with_pipeline_id_less_than, -> (pipeline_id) { where("#{table_name}.pipeline_id < ?", pipeline_id) }
scope :with_pipeline_id_greater_than, -> (pipeline_id) { where("#{table_name}.pipeline_id > ?", pipeline_id) }
end

View file

@ -35,7 +35,8 @@ module ContainerExpirationPolicies
if service_result[:status] == :success
repository.update!(
expiration_policy_cleanup_status: :cleanup_unscheduled,
expiration_policy_completed_at: Time.zone.now
expiration_policy_completed_at: Time.zone.now,
last_cleanup_deleted_tags_count: service_result[:deleted_size]
)
success(:finished, service_result)

View file

@ -0,0 +1,8 @@
---
name: packages_graphql_pipelines_resolver
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82496
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/358432
milestone: '14.10'
type: development
group: group::package
default_enabled: false

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddLastCleanupDeletedTagsCountToContainerRepository < Gitlab::Database::Migration[2.0]
def change
add_column :container_repositories, :last_cleanup_deleted_tags_count, :integer
end
end

View file

@ -0,0 +1 @@
71cde7610713f9e2e21f87a2176cc4ec5fdc797021edab144adfaaf463acb8ef

View file

@ -13783,6 +13783,7 @@ CREATE TABLE container_repositories (
migration_state text DEFAULT 'default'::text NOT NULL,
migration_aborted_in_state text,
migration_plan text,
last_cleanup_deleted_tags_count integer,
CONSTRAINT check_05e9012f36 CHECK ((char_length(migration_plan) <= 255)),
CONSTRAINT check_13c58fe73a CHECK ((char_length(migration_state) <= 255)),
CONSTRAINT check_97f0249439 CHECK ((char_length(migration_aborted_in_state) <= 255))

View file

@ -13928,7 +13928,7 @@ Represents a package in the Package Registry. Note that this type is in beta and
| <a id="packagemetadata"></a>`metadata` | [`PackageMetadata`](#packagemetadata) | Package metadata. |
| <a id="packagename"></a>`name` | [`String!`](#string) | Name of the package. |
| <a id="packagepackagetype"></a>`packageType` | [`PackageTypeEnum!`](#packagetypeenum) | Package type. |
| <a id="packagepipelines"></a>`pipelines` **{warning-solid}** | [`PipelineConnection`](#pipelineconnection) | **Deprecated** in 14.6. Due to scalability concerns, this field is going to be removed. |
| <a id="packagepipelines"></a>`pipelines` | [`PipelineConnection`](#pipelineconnection) | Pipelines that built the package. Max page size 20. (see [Connections](#connections)) |
| <a id="packageproject"></a>`project` | [`Project!`](#project) | Project where the package is stored. |
| <a id="packagestatus"></a>`status` | [`PackageStatus!`](#packagestatus) | Package status. |
| <a id="packagetags"></a>`tags` | [`PackageTagConnection`](#packagetagconnection) | Package tags. (see [Connections](#connections)) |
@ -13995,7 +13995,7 @@ Represents a package details in the Package Registry. Note that this type is in
| <a id="packagedetailstypenugeturl"></a>`nugetUrl` | [`String`](#string) | Url of the Nuget project endpoint. |
| <a id="packagedetailstypepackagefiles"></a>`packageFiles` | [`PackageFileConnection`](#packagefileconnection) | Package files. (see [Connections](#connections)) |
| <a id="packagedetailstypepackagetype"></a>`packageType` | [`PackageTypeEnum!`](#packagetypeenum) | Package type. |
| <a id="packagedetailstypepipelines"></a>`pipelines` **{warning-solid}** | [`PipelineConnection`](#pipelineconnection) | **Deprecated** in 14.6. Due to scalability concerns, this field is going to be removed. |
| <a id="packagedetailstypepipelines"></a>`pipelines` | [`PipelineConnection`](#pipelineconnection) | Pipelines that built the package. Max page size 20. (see [Connections](#connections)) |
| <a id="packagedetailstypeproject"></a>`project` | [`Project!`](#project) | Project where the package is stored. |
| <a id="packagedetailstypepypisetupurl"></a>`pypiSetupUrl` | [`String`](#string) | Url of the PyPi project setup endpoint. |
| <a id="packagedetailstypepypiurl"></a>`pypiUrl` | [`String`](#string) | Url of the PyPi project endpoint. |

View file

@ -5931,6 +5931,9 @@ msgstr ""
msgid "Billings|Extend trial"
msgstr ""
msgid "Billings|In a seat"
msgstr ""
msgid "Billings|Reactivate trial"
msgstr ""
@ -5943,6 +5946,9 @@ msgstr ""
msgid "Billings|Shared runners cannot be enabled until a valid credit card is on file."
msgstr ""
msgid "Billings|To make this member active, you must first remove an existing active member, or toggle them to over limit."
msgstr ""
msgid "Billings|To use free CI/CD minutes on shared runners, youll need to validate your account with a credit card. If you prefer not to provide one, you can run pipelines by bringing your own runners and disabling shared runners for your project. This is required to discourage and reduce abuse on GitLab infrastructure. %{strongStart}GitLab will not charge your card, it will only be used for validation.%{strongEnd} %{linkStart}Learn more%{linkEnd}."
msgstr ""
@ -18553,15 +18559,15 @@ msgstr ""
msgid "GroupsNew|Create this in the %{pat_link_start}user settings%{pat_link_end} of the source GitLab instance. For %{short_living_link_start}security reasons%{short_living_link_end}, use a short expiration date when creating the token."
msgstr ""
msgid "GroupsNew|Export groups with all their related data and move to a new GitLab instance."
msgstr ""
msgid "GroupsNew|GitLab source URL"
msgstr ""
msgid "GroupsNew|Groups can also be nested by creating %{linkStart}subgroups%{linkEnd}."
msgstr ""
msgid "GroupsNew|Import a group and related data from another GitLab instance."
msgstr ""
msgid "GroupsNew|Import group"
msgstr ""
@ -19545,6 +19551,9 @@ msgstr ""
msgid "Improve customer support with Service Desk"
msgstr ""
msgid "In a seat"
msgstr ""
msgid "In case of pull mirroring, your user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches."
msgstr ""
@ -21056,10 +21065,10 @@ msgstr ""
msgid "InviteMembersModal|Create issues for your new team member to work on (optional)"
msgstr ""
msgid "InviteMembersModal|GitLab is better with colleagues!"
msgid "InviteMembersModal|Explore paid plans"
msgstr ""
msgid "InviteMembersModal|GitLab member or email address"
msgid "InviteMembersModal|GitLab is better with colleagues!"
msgstr ""
msgid "InviteMembersModal|How about inviting a colleague or two to join you?"
@ -21074,10 +21083,10 @@ msgstr ""
msgid "InviteMembersModal|Invite members"
msgstr ""
msgid "InviteMembersModal|Members were successfully added"
msgid "InviteMembersModal|Manage members"
msgstr ""
msgid "InviteMembersModal|New members will be unable to participate. You can manage your members by removing ones you no longer need."
msgid "InviteMembersModal|Members were successfully added"
msgstr ""
msgid "InviteMembersModal|Search for a group to invite"
@ -21095,12 +21104,21 @@ msgstr ""
msgid "InviteMembersModal|Something went wrong"
msgstr ""
msgid "InviteMembersModal|This feature is disabled until this group has space for more members."
msgstr ""
msgid "InviteMembersModal|To assign issues to a new team member, you need a project for the issues. %{linkStart}Create a project to get started.%{linkEnd}"
msgstr ""
msgid "InviteMembersModal|To get more members an owner of this namespace can %{trialLinkStart}start a trial%{trialLinkEnd} or %{upgradeLinkStart}upgrade%{upgradeLinkEnd} to a paid tier."
msgstr ""
msgid "InviteMembersModal|Username or email address"
msgstr ""
msgid "InviteMembersModal|You cannot add more members, but you can remove members who no longer need access. To get more members and access to additional paid features, an owner of this namespace can start a trial or upgrade to a paid tier."
msgstr ""
msgid "InviteMembersModal|You only have space for %{count} more %{members} in %{name}"
msgstr ""
@ -25824,6 +25842,9 @@ msgstr ""
msgid "No triggers exist yet. Use the form above to create one."
msgstr ""
msgid "No user provided"
msgstr ""
msgid "No vulnerabilities present"
msgstr ""
@ -39241,27 +39262,12 @@ msgstr ""
msgid "ThreatMonitoring|All Environments"
msgstr ""
msgid "ThreatMonitoring|Anomalous Requests"
msgstr ""
msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
msgstr ""
msgid "ThreatMonitoring|Date and time"
msgstr ""
msgid "ThreatMonitoring|Dismissed"
msgstr ""
msgid "ThreatMonitoring|Dropped Packets"
msgstr ""
msgid "ThreatMonitoring|Environment"
msgstr ""
@ -39289,33 +39295,15 @@ msgstr ""
msgid "ThreatMonitoring|No alerts to display."
msgstr ""
msgid "ThreatMonitoring|No environments detected"
msgstr ""
msgid "ThreatMonitoring|Operations Per Second"
msgstr ""
msgid "ThreatMonitoring|Packet Activity"
msgstr ""
msgid "ThreatMonitoring|Requests"
msgstr ""
msgid "ThreatMonitoring|Resolved"
msgstr ""
msgid "ThreatMonitoring|Show last"
msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch environments"
msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
msgid "ThreatMonitoring|Statistics"
msgstr ""
msgid "ThreatMonitoring|Status"
msgstr ""
@ -39331,18 +39319,6 @@ msgstr ""
msgid "ThreatMonitoring|Threat Monitoring help page link"
msgstr ""
msgid "ThreatMonitoring|Time"
msgstr ""
msgid "ThreatMonitoring|To view this data, ensure you have configured an environment for this project and that at least one threat monitoring feature is enabled. %{linkStart}More information%{linkEnd}"
msgstr ""
msgid "ThreatMonitoring|Total Packets"
msgstr ""
msgid "ThreatMonitoring|Total Requests"
msgstr ""
msgid "ThreatMonitoring|Unreviewed"
msgstr ""
@ -43506,6 +43482,9 @@ msgstr ""
msgid "You do not have permission to run the Web Terminal. Please contact a project administrator."
msgstr ""
msgid "You do not have permission to set a member awaiting"
msgstr ""
msgid "You do not have permission to update the environment."
msgstr ""

View file

@ -58,7 +58,7 @@
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/svgs": "2.11.0",
"@gitlab/ui": "39.6.0",
"@gitlab/visual-review-tools": "1.7.0",
"@gitlab/visual-review-tools": "1.7.1",
"@rails/actioncable": "6.1.4-7",
"@rails/ujs": "6.1.4-7",
"@sentry/browser": "5.30.0",

View file

@ -1,4 +1,4 @@
import { GlLink, GlModal, GlSprintf } from '@gitlab/ui';
import { GlLink, GlModal, GlSprintf, GlFormGroup } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import { stubComponent } from 'helpers/stub_component';
@ -15,6 +15,7 @@ import {
MEMBERS_MODAL_CELEBRATE_INTRO,
MEMBERS_MODAL_CELEBRATE_TITLE,
MEMBERS_PLACEHOLDER,
MEMBERS_PLACEHOLDER_DISABLED,
MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT,
LEARN_GITLAB,
} from '~/invite_members/constants';
@ -28,6 +29,8 @@ import {
propsData,
inviteSource,
newProjectPath,
freeUsersLimit,
membersCount,
user1,
user2,
user3,
@ -45,12 +48,13 @@ describe('InviteMembersModal', () => {
let wrapper;
let mock;
const createComponent = (props = {}) => {
const createComponent = (props = {}, stubs = {}) => {
wrapper = shallowMountExtended(InviteMembersModal, {
provide: {
newProjectPath,
},
propsData: {
usersLimitDataset: {},
...propsData,
...props,
},
@ -62,16 +66,17 @@ describe('InviteMembersModal', () => {
template: '<div><slot></slot><slot name="modal-footer"></slot></div>',
}),
GlEmoji,
...stubs,
},
});
};
const createInviteMembersToProjectWrapper = () => {
createComponent({ isProject: true });
const createInviteMembersToProjectWrapper = (usersLimitDataset = {}, stubs = {}) => {
createComponent({ usersLimitDataset, isProject: true }, stubs);
};
const createInviteMembersToGroupWrapper = () => {
createComponent({ isProject: false });
const createInviteMembersToGroupWrapper = (usersLimitDataset = {}, stubs = {}) => {
createComponent({ usersLimitDataset, isProject: false }, stubs);
};
beforeEach(() => {
@ -95,7 +100,7 @@ describe('InviteMembersModal', () => {
const findMembersFormGroup = () => wrapper.findByTestId('members-form-group');
const membersFormGroupInvalidFeedback = () =>
findMembersFormGroup().attributes('invalid-feedback');
const membersFormGroupDescription = () => findMembersFormGroup().attributes('description');
const membersFormGroupText = () => findMembersFormGroup().text();
const findMembersSelect = () => wrapper.findComponent(MembersTokenSelect);
const findTasksToBeDone = () => wrapper.findByTestId('invite-members-modal-tasks-to-be-done');
const findTasks = () => wrapper.findByTestId('invite-members-modal-tasks');
@ -259,16 +264,33 @@ describe('InviteMembersModal', () => {
expect(wrapper.findComponent(ModalConfetti).exists()).toBe(false);
});
it('includes the correct invitee, type, and formatted name', () => {
it('includes the correct invitee', () => {
expect(findIntroText()).toBe("You're inviting members to the test name project.");
expect(findCelebrationEmoji().exists()).toBe(false);
expect(membersFormGroupDescription()).toBe(MEMBERS_PLACEHOLDER);
});
describe('members form group description', () => {
it('renders correct description', () => {
createInviteMembersToProjectWrapper({ freeUsersLimit, membersCount }, { GlFormGroup });
expect(membersFormGroupText()).toContain(MEMBERS_PLACEHOLDER);
});
describe('when reached user limit', () => {
it('renders correct description', () => {
createInviteMembersToProjectWrapper(
{ freeUsersLimit, membersCount: 5 },
{ GlFormGroup },
);
expect(membersFormGroupText()).toContain(MEMBERS_PLACEHOLDER_DISABLED);
});
});
});
});
describe('when inviting members with celebration', () => {
beforeEach(async () => {
createComponent({ isProject: true });
createInviteMembersToProjectWrapper();
await triggerOpenModal({ mode: 'celebrate' });
});
@ -285,7 +307,28 @@ describe('InviteMembersModal', () => {
`${MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT} ${MEMBERS_MODAL_CELEBRATE_INTRO}`,
);
expect(findCelebrationEmoji().exists()).toBe(true);
expect(membersFormGroupDescription()).toBe(MEMBERS_PLACEHOLDER);
});
describe('members form group description', () => {
it('renders correct description', async () => {
createInviteMembersToProjectWrapper({ freeUsersLimit, membersCount }, { GlFormGroup });
await triggerOpenModal({ mode: 'celebrate' });
expect(membersFormGroupText()).toContain(MEMBERS_PLACEHOLDER);
});
describe('when reached user limit', () => {
it('renders correct description', async () => {
createInviteMembersToProjectWrapper(
{ freeUsersLimit, membersCount: 5 },
{ GlFormGroup },
);
await triggerOpenModal({ mode: 'celebrate' });
expect(membersFormGroupText()).toContain(MEMBERS_PLACEHOLDER_DISABLED);
});
});
});
});
});
@ -295,7 +338,20 @@ describe('InviteMembersModal', () => {
createInviteMembersToGroupWrapper();
expect(findIntroText()).toBe("You're inviting members to the test name group.");
expect(membersFormGroupDescription()).toBe(MEMBERS_PLACEHOLDER);
});
describe('members form group description', () => {
it('renders correct description', () => {
createInviteMembersToGroupWrapper({ freeUsersLimit, membersCount }, { GlFormGroup });
expect(membersFormGroupText()).toContain(MEMBERS_PLACEHOLDER);
});
describe('when reached user limit', () => {
it('renders correct description', () => {
createInviteMembersToGroupWrapper({ freeUsersLimit, membersCount: 5 }, { GlFormGroup });
expect(membersFormGroupText()).toContain(MEMBERS_PLACEHOLDER_DISABLED);
});
});
});
});
});

View file

@ -6,18 +6,30 @@ import {
GlSprintf,
GlLink,
GlModal,
GlIcon,
} from '@gitlab/ui';
import { stubComponent } from 'helpers/stub_component';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import InviteModalBase from '~/invite_members/components/invite_modal_base.vue';
import ContentTransition from '~/vue_shared/components/content_transition.vue';
import { CANCEL_BUTTON_TEXT, INVITE_BUTTON_TEXT } from '~/invite_members/constants';
import { propsData } from '../mock_data/modal_base';
import {
CANCEL_BUTTON_TEXT,
INVITE_BUTTON_TEXT_DISABLED,
INVITE_BUTTON_TEXT,
CANCEL_BUTTON_TEXT_DISABLED,
ON_SHOW_TRACK_LABEL,
ON_CLOSE_TRACK_LABEL,
ON_SUBMIT_TRACK_LABEL,
} from '~/invite_members/constants';
import { propsData, membersPath, purchasePath } from '../mock_data/modal_base';
describe('InviteModalBase', () => {
let wrapper;
const createComponent = (props = {}) => {
const createComponent = (props = {}, stubs = {}) => {
wrapper = shallowMountExtended(InviteModalBase, {
propsData: {
...propsData,
@ -33,8 +45,9 @@ describe('InviteModalBase', () => {
GlDropdownItem: true,
GlSprintf,
GlFormGroup: stubComponent(GlFormGroup, {
props: ['state', 'invalidFeedback', 'description'],
props: ['state', 'invalidFeedback'],
}),
...stubs,
},
});
};
@ -48,8 +61,12 @@ describe('InviteModalBase', () => {
const findDropdownItems = () => findDropdown().findAllComponents(GlDropdownItem);
const findDatepicker = () => wrapper.findComponent(GlDatepicker);
const findLink = () => wrapper.findComponent(GlLink);
const findIcon = () => wrapper.findComponent(GlIcon);
const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text();
const findMembersFormGroup = () => wrapper.findByTestId('members-form-group');
const findDisabledInput = () => wrapper.findByTestId('disabled-input');
const findCancelButton = () => wrapper.find('.js-modal-action-cancel');
const findActionButton = () => wrapper.find('.js-modal-action-primary');
describe('rendering the modal', () => {
beforeEach(() => {
@ -106,11 +123,89 @@ describe('InviteModalBase', () => {
it('renders the members form group', () => {
expect(findMembersFormGroup().props()).toEqual({
description: propsData.formGroupDescription,
invalidFeedback: '',
state: null,
});
});
it('renders description', () => {
createComponent({}, { GlFormGroup });
expect(findMembersFormGroup().text()).toContain(propsData.formGroupDescription);
});
describe('when users limit is reached', () => {
let trackingSpy;
const expectTracking = (action, label) =>
expect(trackingSpy).toHaveBeenCalledWith('default', action, {
label,
category: 'default',
});
beforeEach(() => {
createComponent(
{ membersPath, purchasePath, reachedLimit: true },
{ GlModal, GlFormGroup },
);
});
it('renders correct blocks', () => {
expect(findIcon().exists()).toBe(true);
expect(findDisabledInput().exists()).toBe(true);
expect(findDropdown().exists()).toBe(false);
expect(findDatepicker().exists()).toBe(false);
});
it('renders correct buttons', () => {
const cancelButton = findCancelButton();
const actionButton = findActionButton();
expect(cancelButton.attributes('href')).toBe(purchasePath);
expect(cancelButton.text()).toBe(CANCEL_BUTTON_TEXT_DISABLED);
expect(actionButton.attributes('href')).toBe(membersPath);
expect(actionButton.text()).toBe(INVITE_BUTTON_TEXT_DISABLED);
});
it('tracks actions', () => {
createComponent({ reachedLimit: true }, { GlFormGroup, GlModal });
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
const modal = wrapper.findComponent(GlModal);
modal.vm.$emit('shown');
expectTracking('render', ON_SHOW_TRACK_LABEL);
modal.vm.$emit('cancel', { preventDefault: jest.fn() });
expectTracking('click_button', ON_CLOSE_TRACK_LABEL);
modal.vm.$emit('primary', { preventDefault: jest.fn() });
expectTracking('click_button', ON_SUBMIT_TRACK_LABEL);
unmockTracking();
});
});
describe('when users limit is not reached', () => {
const textRegex = /Select a role.+Read more about role permissions Access expiration date \(optional\)/;
beforeEach(() => {
createComponent({ reachedLimit: false }, { GlModal, GlFormGroup });
});
it('renders correct blocks', () => {
expect(findIcon().exists()).toBe(false);
expect(findDisabledInput().exists()).toBe(false);
expect(findDropdown().exists()).toBe(true);
expect(findDatepicker().exists()).toBe(true);
expect(wrapper.findComponent(GlModal).text()).toMatch(textRegex);
});
it('renders correct buttons', () => {
expect(findCancelButton().text()).toBe(CANCEL_BUTTON_TEXT);
expect(findActionButton().text()).toBe(INVITE_BUTTON_TEXT);
});
});
});
it('with isLoading, shows loading for invite button', () => {
@ -127,7 +222,6 @@ describe('InviteModalBase', () => {
});
expect(findMembersFormGroup().props()).toEqual({
description: propsData.formGroupDescription,
invalidFeedback: 'invalid message!',
state: false,
});

View file

@ -1,22 +1,27 @@
import { GlAlert, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import UserLimitNotification from '~/invite_members/components/user_limit_notification.vue';
import { REACHED_LIMIT_MESSAGE } from '~/invite_members/constants';
import { freeUsersLimit, membersCount } from '../mock_data/member_modal';
describe('UserLimitNotification', () => {
let wrapper;
const findAlert = () => wrapper.findComponent(GlAlert);
const createComponent = (providers = {}) => {
const createComponent = (reachedLimit = false, usersLimitDataset = {}) => {
wrapper = shallowMountExtended(UserLimitNotification, {
provide: {
name: 'my group',
newTrialRegistrationPath: 'newTrialRegistrationPath',
purchasePath: 'purchasePath',
freeUsersLimit: 5,
membersCount: 1,
...providers,
propsData: {
reachedLimit,
usersLimitDataset: {
freeUsersLimit,
membersCount,
newTrialRegistrationPath: 'newTrialRegistrationPath',
purchasePath: 'purchasePath',
...usersLimitDataset,
},
},
provide: { name: 'my group' },
stubs: { GlSprintf },
});
};
@ -37,7 +42,7 @@ describe('UserLimitNotification', () => {
describe('when close to limit', () => {
beforeEach(() => {
createComponent({ membersCount: 3 });
createComponent(false, { membersCount: 3 });
});
it("renders user's limit notification", () => {
@ -55,17 +60,14 @@ describe('UserLimitNotification', () => {
describe('when limit is reached', () => {
beforeEach(() => {
createComponent({ membersCount: 5 });
createComponent(true);
});
it("renders user's limit notification", () => {
const alert = findAlert();
expect(alert.attributes('title')).toEqual("You've reached your 5 members limit for my group");
expect(alert.text()).toEqual(
'New members will be unable to participate. You can manage your members by removing ones you no longer need. To get more members an owner of this namespace can start a trial or upgrade to a paid tier.',
);
expect(alert.text()).toEqual(REACHED_LIMIT_MESSAGE);
});
});
});

View file

@ -18,6 +18,8 @@ export const propsData = {
export const inviteSource = 'unknown';
export const newProjectPath = 'projects/new';
export const freeUsersLimit = 5;
export const membersCount = 1;
export const user1 = { id: 1, name: 'Name One', username: 'one_1', avatar_url: '' };
export const user2 = { id: 2, name: 'Name Two', username: 'one_2', avatar_url: '' };

View file

@ -9,3 +9,6 @@ export const propsData = {
labelSearchField: '_label_search_field_',
formGroupDescription: '_form_group_description_',
};
export const membersPath = '/members_path';
export const purchasePath = '/purchase_path';

View file

@ -1,7 +1,7 @@
import { GlDropdownItem, GlIcon, GlDropdown } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { nextTick } from 'vue';
import Vue, { nextTick } from 'vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { useFakeDate } from 'helpers/fake_date';
import createMockApollo from 'helpers/mock_apollo_helper';
@ -28,7 +28,6 @@ import { imageTagsCountMock } from '../../mock_data';
describe('Details Header', () => {
let wrapper;
let apolloProvider;
let localVue;
const defaultImage = {
name: 'foo',
@ -64,28 +63,18 @@ describe('Details Header', () => {
const mountComponent = ({
propsData = { image: defaultImage },
resolver = jest.fn().mockResolvedValue(imageTagsCountMock()),
$apollo = undefined,
} = {}) => {
const mocks = {};
Vue.use(VueApollo);
if ($apollo) {
mocks.$apollo = $apollo;
} else {
localVue = createLocalVue();
localVue.use(VueApollo);
const requestHandlers = [[getContainerRepositoryMetadata, resolver]];
apolloProvider = createMockApollo(requestHandlers);
}
const requestHandlers = [[getContainerRepositoryMetadata, resolver]];
apolloProvider = createMockApollo(requestHandlers);
wrapper = shallowMount(component, {
localVue,
apolloProvider,
propsData,
directives: {
GlTooltip: createMockDirective(),
},
mocks,
stubs: {
TitleArea,
GlDropdown,
@ -98,7 +87,6 @@ describe('Details Header', () => {
// if we want to mix createMockApollo and manual mocks we need to reset everything
wrapper.destroy();
apolloProvider = undefined;
localVue = undefined;
wrapper = null;
});
@ -194,10 +182,7 @@ describe('Details Header', () => {
describe('metadata items', () => {
describe('tags count', () => {
it('displays "-- tags" while loading', async () => {
// here we are forced to mock apollo because `waitForMetadataItems` waits
// for two ticks, de facto allowing the promise to resolve, so there is
// no way to catch the component as both rendered and in loading state
mountComponent({ $apollo: { queries: { containerRepository: { loading: true } } } });
mountComponent();
await waitForMetadataItems();

View file

@ -1,6 +1,6 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { nextTick } from 'vue';
import Vue, { nextTick } from 'vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { GlCard, GlLoadingIcon } from 'jest/packages_and_registries/shared/stubs';
@ -14,8 +14,6 @@ import expirationPolicyQuery from '~/packages_and_registries/settings/project/gr
import Tracking from '~/tracking';
import { expirationPolicyPayload, expirationPolicyMutationPayload } from '../mock_data';
const localVue = createLocalVue();
describe('Settings Form', () => {
let wrapper;
let fakeApollo;
@ -59,7 +57,6 @@ describe('Settings Form', () => {
data,
config,
provide = defaultProvidedValues,
mocks,
} = {}) => {
wrapper = shallowMount(component, {
stubs: {
@ -77,7 +74,6 @@ describe('Settings Form', () => {
$toast: {
show: jest.fn(),
},
...mocks,
},
...config,
});
@ -88,7 +84,7 @@ describe('Settings Form', () => {
mutationResolver,
queryPayload = expirationPolicyPayload(),
} = {}) => {
localVue.use(VueApollo);
Vue.use(VueApollo);
const requestHandlers = [
[updateContainerExpirationPolicyMutation, mutationResolver],
@ -120,7 +116,6 @@ describe('Settings Form', () => {
value,
},
config: {
localVue,
apolloProvider: fakeApollo,
},
});
@ -356,8 +351,8 @@ describe('Settings Form', () => {
});
it('parses the error messages', async () => {
const mutate = jest.fn().mockRejectedValue({
graphQLErrors: [
const mutate = jest.fn().mockResolvedValue({
errors: [
{
extensions: {
problems: [{ path: ['nameRegexKeep'], message: 'baz' }],
@ -365,7 +360,9 @@ describe('Settings Form', () => {
},
],
});
mountComponent({ mocks: { $apollo: { mutate } } });
mountComponentWithApollo({
mutationResolver: mutate,
});
await submitForm();

View file

@ -220,13 +220,11 @@ describe('search_params.js', () => {
});
it.each`
query | updatedQuery
${'status[]=NOT_CONNECTED'} | ${'status[]=NEVER_CONTACTED'}
${'status[]=NOT_CONNECTED&a=b'} | ${'status[]=NEVER_CONTACTED&a=b'}
${'status[]=ACTIVE'} | ${'paused[]=false'}
${'status[]=ACTIVE&a=b'} | ${'a=b&paused[]=false'}
${'status[]=ACTIVE'} | ${'paused[]=false'}
${'status[]=PAUSED'} | ${'paused[]=true'}
query | updatedQuery
${'status[]=ACTIVE'} | ${'paused[]=false'}
${'status[]=ACTIVE&a=b'} | ${'a=b&paused[]=false'}
${'status[]=ACTIVE'} | ${'paused[]=false'}
${'status[]=PAUSED'} | ${'paused[]=true'}
`('updates "$query" to "$updatedQuery"', ({ query, updatedQuery }) => {
const mockUrl = 'http://test.host/admin/runners?';

View file

@ -9,10 +9,23 @@ RSpec.describe Resolvers::PackagePipelinesResolver do
let_it_be(:pipelines) { create_list(:ci_pipeline, 3, project: package.project) }
let(:user) { package.project.first_owner }
let(:args) { {} }
describe '#resolve' do
subject { resolve(described_class, obj: package, args: args, ctx: { current_user: user }) }
let(:returned_pipeline_ids) { graphql_dig_at(subject, 'data', 'package', 'pipelines', 'nodes', 'id') }
let(:returned_errors) { graphql_dig_at(subject, 'errors', 'message') }
let(:pagination_args) { {} }
let(:query) do
pipelines_nodes = 'nodes { id }'
graphql_query_for(
:package,
{ id: global_id_of(package) },
query_graphql_field('pipelines', pagination_args, pipelines_nodes)
)
end
subject do
GitlabSchema.execute(query, context: { current_user: user })
end
before do
pipelines.each do |pipeline|
@ -20,67 +33,138 @@ RSpec.describe Resolvers::PackagePipelinesResolver do
end
end
it { is_expected.to contain_exactly(*pipelines) }
shared_examples 'returning the expected pipelines' do
it 'contains the expected pipelines' do
expect_to_contain_exactly(*pipelines)
end
context 'with invalid after' do
let(:args) { { first: 1, after: 'not_json_string' } }
context 'with valid after' do
let(:pagination_args) { { first: 1, after: encode_cursor(id: pipelines[1].id) } }
it 'generates an argument error' do
expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError) do
subject
it 'contains the expected pipelines' do
expect_to_contain_exactly(pipelines[0])
end
end
context 'with valid before' do
let(:pagination_args) { { last: 1, before: encode_cursor(id: pipelines[1].id) } }
it 'contains the expected pipelines' do
expect_to_contain_exactly(pipelines[2])
end
end
context 'with invalid after' do
let(:pagination_args) { { first: 1, after: 'not_json_string' } }
it 'generates an argument error' do
expect(returned_errors).to include('Please provide a valid cursor')
end
end
context 'with invalid after key' do
let(:pagination_args) { { first: 1, after: encode_cursor(foo: 3) } }
it 'generates an argument error' do
expect(returned_errors).to include('Please provide a valid cursor')
end
end
context 'with invalid before' do
let(:pagination_args) { { last: 1, before: 'not_json_string' } }
it 'generates an argument error' do
expect(returned_errors).to include('Please provide a valid cursor')
end
end
context 'with invalid before key' do
let(:pagination_args) { { last: 1, before: encode_cursor(foo: 3) } }
it 'generates an argument error' do
expect(returned_errors).to include('Please provide a valid cursor')
end
end
context 'with unauthorized user' do
let_it_be(:user) { create(:user) }
it 'returns nothing' do
expect(returned_pipeline_ids).to eq(nil)
end
end
context 'with many packages' do
let_it_be_with_reload(:other_package) { create(:package, project: package.project) }
let_it_be(:other_pipelines) { create_list(:ci_pipeline, 3, project: package.project) }
let(:returned_pipeline_ids) do
graphql_dig_at(subject, 'data', 'project', 'packages', 'nodes', 'pipelines', 'nodes', 'id')
end
let(:query) do
pipelines_query = query_graphql_field('pipelines', pagination_args, 'nodes { id }')
<<~QUERY
{
project(fullPath: "#{package.project.full_path}") {
packages {
nodes { #{pipelines_query} }
}
}
}
QUERY
end
before do
other_pipelines.each do |pipeline|
create(:package_build_info, package: other_package, pipeline: pipeline)
end
end
it 'contains the expected pipelines' do
expect_to_contain_exactly(*(pipelines + other_pipelines))
end
it 'handles n+1 situations' do
control = ActiveRecord::QueryRecorder.new do
GitlabSchema.execute(query, context: { current_user: user })
end
create_package_with_pipelines(package.project)
expectation = expect { GitlabSchema.execute(query, context: { current_user: user }) }
if Feature.enabled?(:packages_graphql_pipelines_resolver, default_enabled: :yaml)
expectation.not_to exceed_query_limit(control)
else
expectation.to exceed_query_limit(control)
end
end
def create_package_with_pipelines(project)
extra_package = create(:package, project: project)
create_list(:ci_pipeline, 3, project: project).each do |pipeline|
create(:package_build_info, package: extra_package, pipeline: pipeline)
end
end
end
end
context 'with invalid after key' do
let(:args) { { first: 1, after: encode_cursor(foo: 3) } }
it 'generates an argument error' do
expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError) do
subject
end
context 'with packages_graphql_pipelines_resolver enabled' do
before do
expect_detect_mode([:new_finder])
end
it_behaves_like 'returning the expected pipelines'
end
context 'with invalid before' do
let(:args) { { last: 1, before: 'not_json_string' } }
it 'generates an argument error' do
expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError) do
subject
end
end
end
context 'with invalid before key' do
let(:args) { { last: 1, before: encode_cursor(foo: 3) } }
it 'generates an argument error' do
expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError) do
subject
end
end
end
context 'field options' do
let(:field) do
field_options = described_class.field_options.merge(
owner: resolver_parent,
name: 'dummy_field'
)
::Types::BaseField.new(**field_options)
context 'with packages_graphql_pipelines_resolver disabled' do
before do
stub_feature_flags(packages_graphql_pipelines_resolver: false)
expect_detect_mode([:old_finder, :object_field])
end
it 'sets them properly' do
expect(field).not_to be_connection
expect(field.extras).to match_array([:lookahead])
end
end
context 'with unauthorized user' do
let_it_be(:user) { create(:user) }
it { is_expected.to be_nil }
it_behaves_like 'returning the expected pipelines'
end
def encode_cursor(json)
@ -89,5 +173,37 @@ RSpec.describe Resolvers::PackagePipelinesResolver do
nonce: true
)
end
def expect_to_contain_exactly(*pipelines)
ids = pipelines.map { |pipeline| global_id_of(pipeline) }
expect(returned_pipeline_ids).to contain_exactly(*ids)
end
def expect_detect_mode(modes)
allow_next_instance_of(described_class) do |resolver|
detect_mode_method = resolver.method(:detect_mode)
allow(resolver).to receive(:detect_mode) do
result = detect_mode_method.call
expect(modes).to include(result)
result
end
end
end
end
describe '.field options' do
let(:field) do
field_options = described_class.field_options.merge(
owner: resolver_parent,
name: 'dummy_field'
)
::Types::BaseField.new(**field_options)
end
it 'sets them properly' do
expect(field).not_to be_connection
expect(field.extras).to match_array([:lookahead])
end
end
end

View file

@ -631,10 +631,15 @@ RSpec.describe ContainerRepository, :aggregate_failures do
describe '#start_expiration_policy!' do
subject { repository.start_expiration_policy! }
before do
repository.update_column(:last_cleanup_deleted_tags_count, 10)
end
it 'sets the expiration policy started at to now' do
freeze_time do
expect { subject }
.to change { repository.expiration_policy_started_at }.from(nil).to(Time.zone.now)
.and change { repository.last_cleanup_deleted_tags_count }.from(10).to(nil)
end
end
end

View file

@ -25,7 +25,7 @@ RSpec.describe ContainerExpirationPolicies::CleanupService do
it 'completely clean up the repository' do
expect(Projects::ContainerRepository::CleanupTagsService)
.to receive(:new).with(repository, nil, cleanup_tags_service_params).and_return(cleanup_tags_service)
expect(cleanup_tags_service).to receive(:execute).and_return(status: :success)
expect(cleanup_tags_service).to receive(:execute).and_return(status: :success, deleted_size: 1)
response = subject
@ -36,6 +36,7 @@ RSpec.describe ContainerExpirationPolicies::CleanupService do
expect(repository.reload.cleanup_unscheduled?).to be_truthy
expect(repository.expiration_policy_completed_at).not_to eq(nil)
expect(repository.expiration_policy_started_at).not_to eq(nil)
expect(repository.last_cleanup_deleted_tags_count).to eq(1)
end
end
end
@ -58,6 +59,7 @@ RSpec.describe ContainerExpirationPolicies::CleanupService do
expect(repository.reload.cleanup_unfinished?).to be_truthy
expect(repository.expiration_policy_started_at).not_to eq(nil)
expect(repository.expiration_policy_completed_at).to eq(nil)
expect(repository.last_cleanup_deleted_tags_count).to eq(nil)
end
end
@ -94,6 +96,7 @@ RSpec.describe ContainerExpirationPolicies::CleanupService do
expect(repository.reload.cleanup_unfinished?).to be_truthy
expect(repository.expiration_policy_started_at).not_to eq(nil)
expect(repository.expiration_policy_completed_at).to eq(nil)
expect(repository.last_cleanup_deleted_tags_count).to eq(nil)
end
end
end
@ -138,6 +141,7 @@ RSpec.describe ContainerExpirationPolicies::CleanupService do
expect(repository.reload.cleanup_unfinished?).to be_truthy
expect(repository.expiration_policy_started_at).not_to eq(nil)
expect(repository.expiration_policy_completed_at).to eq(nil)
expect(repository.last_cleanup_deleted_tags_count).to eq(nil)
end
end

View file

@ -4,7 +4,11 @@ RSpec.shared_examples 'group and projects packages resolver' do
context 'without sort' do
let_it_be(:npm_package) { create(:package, project: project) }
it { is_expected.to contain_exactly(npm_package) }
it 'returns the proper packages' do
expect(::Packages::Package).not_to receive(:preload_pipelines)
expect(subject).to contain_exactly(npm_package)
end
end
context 'with sorting and filtering' do

View file

@ -45,9 +45,33 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
end
it "notifies about #{event_type} events" do
expect(chat_integration).not_to receive(:log_error)
chat_integration.execute(data)
expect(WebMock).to have_requested(:post, stubbed_resolved_hostname)
end
context 'when the response is not successful' do
let!(:stubbed_resolved_hostname) do
stub_full_request(webhook_url, method: :post)
.to_return(status: 409, body: 'error message')
.request_pattern.uri_pattern.to_s
end
it 'logs an error' do
expect(chat_integration).to receive(:log_error).with(
'SlackMattermostNotifier HTTP error response',
request_host: 'example.gitlab.com',
response_code: 409,
response_body: 'error message'
)
chat_integration.execute(data)
expect(WebMock).to have_requested(:post, stubbed_resolved_hostname)
end
end
end
shared_examples "untriggered #{integration_name} integration" do |event_type: nil, branches_to_be_notified: nil|
@ -59,8 +83,9 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
stub_full_request(webhook_url, method: :post).request_pattern.uri_pattern.to_s
end
it "notifies about #{event_type} events" do
it "does not notify about #{event_type} events" do
chat_integration.execute(data)
expect(WebMock).not_to have_requested(:post, stubbed_resolved_hostname)
end
end

View file

@ -191,4 +191,91 @@ RSpec.shared_examples 'group and project packages query' do
it { is_expected.to include({ "name" => versionless_package.name }) }
end
end
context 'when reading pipelines' do
let(:npm_pipelines) { create_list(:ci_pipeline, 6, project: project1) }
let(:npm_pipeline_gids) { npm_pipelines.sort_by(&:id).map(&:to_gid).map(&:to_s).reverse }
let(:composer_pipelines) { create_list(:ci_pipeline, 6, project: project2) }
let(:composer_pipeline_gids) { composer_pipelines.sort_by(&:id).map(&:to_gid).map(&:to_s).reverse }
let(:npm_end_cursor) { graphql_data_npm_package.dig('pipelines', 'pageInfo', 'endCursor') }
let(:npm_start_cursor) { graphql_data_npm_package.dig('pipelines', 'pageInfo', 'startCursor') }
let(:pipelines_nodes) do
<<~QUERY
nodes {
id
}
pageInfo {
startCursor
endCursor
}
QUERY
end
before do
resource.add_maintainer(current_user)
npm_pipelines.each do |pipeline|
create(:package_build_info, package: npm_package, pipeline: pipeline)
end
composer_pipelines.each do |pipeline|
create(:package_build_info, package: composer_package, pipeline: pipeline)
end
end
it 'loads the second page with pagination first correctly' do
run_query(first: 2)
expect(npm_pipeline_ids).to eq(npm_pipeline_gids[0..1])
expect(composer_pipeline_ids).to eq(composer_pipeline_gids[0..1])
run_query(first: 2, after: npm_end_cursor)
expect(npm_pipeline_ids).to eq(npm_pipeline_gids[2..3])
expect(composer_pipeline_ids).to be_empty
end
it 'loads the second page with pagination last correctly' do
run_query(last: 2)
expect(npm_pipeline_ids).to eq(npm_pipeline_gids[4..5])
expect(composer_pipeline_ids).to eq(composer_pipeline_gids[4..5])
run_query(last: 2, before: npm_start_cursor)
expect(npm_pipeline_ids).to eq(npm_pipeline_gids[2..3])
expect(composer_pipeline_ids).to eq(composer_pipeline_gids[4..5])
end
def run_query(args)
pipelines_field = query_graphql_field('pipelines', args, pipelines_nodes)
packages_nodes = <<~QUERY
nodes {
id
#{pipelines_field}
}
QUERY
query = graphql_query_for(
resource_type,
{ 'fullPath' => resource.full_path },
query_graphql_field('packages', {}, packages_nodes)
)
post_graphql(query, current_user: current_user)
end
def npm_pipeline_ids
graphql_data_npm_package.dig('pipelines', 'nodes').map { |pipeline| pipeline['id'] }
end
def composer_pipeline_ids
graphql_data_composer_package.dig('pipelines', 'nodes').map { |pipeline| pipeline['id'] }
end
def graphql_data_npm_package
graphql_data_at(resource_type, :packages, :nodes).find { |pkg| pkg['id'] == npm_package.to_gid.to_s }
end
def graphql_data_composer_package
graphql_data_at(resource_type, :packages, :nodes).find { |pkg| pkg['id'] == composer_package.to_gid.to_s }
end
end
end

View file

@ -982,10 +982,10 @@
portal-vue "^2.1.6"
vue-runtime-helpers "^1.1.2"
"@gitlab/visual-review-tools@1.7.0":
version "1.7.0"
resolved "https://registry.yarnpkg.com/@gitlab/visual-review-tools/-/visual-review-tools-1.7.0.tgz#18a59911484ddbbee433d3701316b5ab8e4077a5"
integrity sha512-o9gM3W6guSl00aS0hJcXePuR/mkmq38F5FhUgTlMBkB5+R68aO87md3cSvSMfJin0MjPlWktBNAfaz+y5CQ39g==
"@gitlab/visual-review-tools@1.7.1":
version "1.7.1"
resolved "https://registry.yarnpkg.com/@gitlab/visual-review-tools/-/visual-review-tools-1.7.1.tgz#9cc51c40bb530a621d0f5cb48ef3e891a79e92cc"
integrity sha512-64lbKhJierSKOQxZQ30gimUDZhOXjtC7GdovSJwKMECqUB5pmDzmQn4fY0Nxn8jREWluiur8N3+z3jr2HJJofg==
"@graphql-eslint/eslint-plugin@3.10.2":
version "3.10.2"