diff options
85 files changed, 1038 insertions, 372 deletions
diff --git a/.eslintrc.yml b/.eslintrc.yml index 3a08547c53d..d29af93319e 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -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 diff --git a/app/assets/javascripts/alert_management/components/alert_management_table.vue b/app/assets/javascripts/alert_management/components/alert_management_table.vue index 929f5d10956..8de793bc798 100644 --- a/app/assets/javascripts/alert_management/components/alert_management_table.vue +++ b/app/assets/javascripts/alert_management/components/alert_management_table.vue @@ -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" diff --git a/app/assets/javascripts/analytics/usage_trends/components/users_chart.vue b/app/assets/javascripts/analytics/usage_trends/components/users_chart.vue index 728b4f08aa2..dfe94aeb884 100644 --- a/app/assets/javascripts/analytics/usage_trends/components/users_chart.vue +++ b/app/assets/javascripts/analytics/usage_trends/components/users_chart.vue @@ -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> diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue index 1d6a71aca47..5c6d7748edd 100644 --- a/app/assets/javascripts/boards/components/board_content.vue +++ b/app/assets/javascripts/boards/components/board_content.vue @@ -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> diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue index 24071c6f0b4..c559e4cdbd3 100644 --- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue +++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue @@ -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> diff --git a/app/assets/javascripts/boards/components/board_top_bar.vue b/app/assets/javascripts/boards/components/board_top_bar.vue index f90ac1e9079..54a6e3000a4 100644 --- a/app/assets/javascripts/boards/components/board_top_bar.vue +++ b/app/assets/javascripts/boards/components/board_top_bar.vue @@ -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" /> diff --git a/app/assets/javascripts/captcha/captcha_modal.vue b/app/assets/javascripts/captcha/captcha_modal.vue index b8b90b04beb..36aa098d5ff 100644 --- a/app/assets/javascripts/captcha/captcha_modal.vue +++ b/app/assets/javascripts/captcha/captcha_modal.vue @@ -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')" diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue index b92f3d5a97b..29530ddb7a2 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -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" diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/code_block.vue b/app/assets/javascripts/content_editor/components/bubble_menus/code_block.vue index c42f06664cc..210f259b20f 100644 --- a/app/assets/javascripts/content_editor/components/bubble_menus/code_block.vue +++ b/app/assets/javascripts/content_editor/components/bubble_menus/code_block.vue @@ -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> diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/formatting.vue b/app/assets/javascripts/content_editor/components/bubble_menus/formatting.vue index 2971a1e40ba..46c15de6b2c 100644 --- a/app/assets/javascripts/content_editor/components/bubble_menus/formatting.vue +++ b/app/assets/javascripts/content_editor/components/bubble_menus/formatting.vue @@ -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')" diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/link.vue b/app/assets/javascripts/content_editor/components/bubble_menus/link.vue index 87853fda7b8..2f446832516 100644 --- a/app/assets/javascripts/content_editor/components/bubble_menus/link.vue +++ b/app/assets/javascripts/content_editor/components/bubble_menus/link.vue @@ -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"> diff --git a/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue b/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue index 41c083111c5..209e4629830 100644 --- a/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue +++ b/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue @@ -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')"> diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue index 837320b9423..2b395921ee1 100644 --- a/app/assets/javascripts/design_management/pages/design/index.vue +++ b/app/assets/javascripts/design_management/pages/design/index.vue @@ -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 })" diff --git a/app/assets/javascripts/diffs/components/hidden_files_warning.vue b/app/assets/javascripts/diffs/components/hidden_files_warning.vue index b9962682848..f6a8c679f3b 100644 --- a/app/assets/javascripts/diffs/components/hidden_files_warning.vue +++ b/app/assets/javascripts/diffs/components/hidden_files_warning.vue @@ -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 */, ) " > diff --git a/app/assets/javascripts/diffs/components/image_diff_overlay.vue b/app/assets/javascripts/diffs/components/image_diff_overlay.vue index 8871be1f9af..bd040cd1ba1 100644 --- a/app/assets/javascripts/diffs/components/image_diff_overlay.vue +++ b/app/assets/javascripts/diffs/components/image_diff_overlay.vue @@ -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> diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue index 3c608ad0ba9..adb14ce3d6f 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_view.vue +++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue @@ -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> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue index 38f3b094b7c..cb906374fe1 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue @@ -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> diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue index bfc5bd823a2..922e870caa7 100644 --- a/app/assets/javascripts/incidents/components/incidents_list.vue +++ b/app/assets/javascripts/incidents/components/incidents_list.vue @@ -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" diff --git a/app/assets/javascripts/invite_members/components/invite_members_modal.vue b/app/assets/javascripts/invite_members/components/invite_members_modal.vue index a9aa0e9b760..994d2ff5271 100644 --- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue +++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue @@ -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 }"> diff --git a/app/assets/javascripts/invite_members/components/invite_modal_base.vue b/app/assets/javascripts/invite_members/components/invite_modal_base.vue index d9297614a7e..05c0e4dc74d 100644 --- a/app/assets/javascripts/invite_members/components/invite_modal_base.vue +++ b/app/assets/javascripts/invite_members/components/invite_modal_base.vue @@ -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> diff --git a/app/assets/javascripts/invite_members/components/members_token_select.vue b/app/assets/javascripts/invite_members/components/members_token_select.vue index 0a191f6d406..30c9294344e 100644 --- a/app/assets/javascripts/invite_members/components/members_token_select.vue +++ b/app/assets/javascripts/invite_members/components/members_token_select.vue @@ -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" diff --git a/app/assets/javascripts/invite_members/components/user_limit_notification.vue b/app/assets/javascripts/invite_members/components/user_limit_notification.vue index beef1aef8a1..2199fef8544 100644 --- a/app/assets/javascripts/invite_members/components/user_limit_notification.vue +++ b/app/assets/javascripts/invite_members/components/user_limit_notification.vue @@ -1,35 +1,50 @@ <script> import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui'; -import { s__, n__, sprintf } from '~/locale'; +import { n__, sprintf } from '~/locale'; -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}", -); +import { + WARNING_ALERT_TITLE, + DANGER_ALERT_TITLE, + REACHED_LIMIT_MESSAGE, + CLOSE_TO_LIMIT_MESSAGE, +} from '../constants'; -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); +const CLOSE_TO_LIMIT_COUNT = 2; 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); }, diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js index 3cd0bfc0181..71dc214a3e8 100644 --- a/app/assets/javascripts/invite_members/constants.js +++ b/app/assets/javascripts/invite_members/constants.js @@ -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.', +); diff --git a/app/assets/javascripts/invite_members/init_invite_members_modal.js b/app/assets/javascripts/invite_members/init_invite_members_modal.js index 958121ad735..a4be3f205a3 100644 --- a/app/assets/javascripts/invite_members/init_invite_members_modal.js +++ b/app/assets/javascripts/invite_members/init_invite_members_modal.js @@ -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 || '{}'), + ), }, }), }); diff --git a/app/assets/javascripts/jira_import/components/jira_import_form.vue b/app/assets/javascripts/jira_import/components/jira_import_form.vue index af4a26a7352..8a36a4d2466 100644 --- a/app/assets/javascripts/jira_import/components/jira_import_form.vue +++ b/app/assets/javascripts/jira_import/components/jira_import_form.vue @@ -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" > diff --git a/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue b/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue index 0a25dc5bea5..27e3b8028b7 100644 --- a/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue +++ b/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue @@ -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> diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index c4392dd3748..6a85833db27 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -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)" > diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_modal.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_modal.vue index 7a8a1bbcf09..2da8ca2d8a8 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_modal.vue +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_modal.vue @@ -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 = ''" diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue index 7659ba5f9ea..9e8eb92d87a 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue @@ -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" diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue index d1cab406984..bb116d5f842 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue @@ -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')" > diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue index f198d2e1bfa..425fb4596fd 100644 --- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue @@ -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" diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/infrastructure_search.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/infrastructure_search.vue index c611f92036d..d3c38da1531 100644 --- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/infrastructure_search.vue +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/infrastructure_search.vue @@ -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" diff --git a/app/assets/javascripts/pages/groups/new/components/app.vue b/app/assets/javascripts/pages/groups/new/components/app.vue index c3ac074cd7a..713287f65b4 100644 --- a/app/assets/javascripts/pages/groups/new/components/app.vue +++ b/app/assets/javascripts/pages/groups/new/components/app.vue @@ -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.', }, diff --git a/app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue b/app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue index 853e839a7ab..42e2d34fa3a 100644 --- a/app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue +++ b/app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue @@ -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> diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue b/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue index 25a78aab933..1f2fdb7ce36 100644 --- a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue +++ b/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue @@ -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" /> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ios_templates.vue b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ios_templates.vue index b0f5b26367e..8ff311e90e7 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ios_templates.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ios_templates.vue @@ -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> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue index afcb04cd7eb..23e2a36ddeb 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue @@ -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" diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue index 77b9c2b5203..d77c0a39bea 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue @@ -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 diff --git a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue index 8511f9bdb0f..efac9f3044a 100644 --- a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue +++ b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue @@ -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" /> diff --git a/app/assets/javascripts/related_issues/components/add_issuable_form.vue b/app/assets/javascripts/related_issues/components/add_issuable_form.vue index 9ee2e7a4ffd..42de419aec4 100644 --- a/app/assets/javascripts/related_issues/components/add_issuable_form.vue +++ b/app/assets/javascripts/related_issues/components/add_issuable_form.vue @@ -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" diff --git a/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue b/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue index 1234054c660..09d46ce3e66 100644 --- a/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue +++ b/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue @@ -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" > diff --git a/app/assets/javascripts/runner/runner_search_utils.js b/app/assets/javascripts/runner/runner_search_utils.js index 5e3c412ddb6..0d688ed65ef 100644 --- a/app/assets/javascripts/runner/runner_search_utils.js +++ b/app/assets/javascripts/runner/runner_search_utils.js @@ -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'], diff --git a/app/assets/javascripts/serverless/components/area.vue b/app/assets/javascripts/serverless/components/area.vue index a9584c070fe..0b158ff3e95 100644 --- a/app/assets/javascripts/serverless/components/area.vue +++ b/app/assets/javascripts/serverless/components/area.vue @@ -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" diff --git a/app/assets/javascripts/static_site_editor/components/edit_area.vue b/app/assets/javascripts/static_site_editor/components/edit_area.vue index ea775eff358..2f2efe290ec 100644 --- a/app/assets/javascripts/static_site_editor/components/edit_area.vue +++ b/app/assets/javascripts/static_site_editor/components/edit_area.vue @@ -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" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue index efd277c1756..bbe15f659f5 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue @@ -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)" diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue index 230e1b009c8..33d507dad57 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue @@ -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" diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue index e7923e0b55e..c3a0a97a7ba 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue @@ -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"> diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index 7993910ec12..ba2b5eaa4f9 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -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" diff --git a/app/assets/javascripts/vue_shared/components/metric_images/metric_images_tab.vue b/app/assets/javascripts/vue_shared/components/metric_images/metric_images_tab.vue index 3e796a73f72..e23721da223 100644 --- a/app/assets/javascripts/vue_shared/components/metric_images/metric_images_tab.vue +++ b/app/assets/javascripts/vue_shared/components/metric_images/metric_images_tab.vue @@ -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" diff --git a/app/assets/javascripts/vue_shared/components/metric_images/metric_images_table.vue b/app/assets/javascripts/vue_shared/components/metric_images/metric_images_table.vue index 8eb8e52728d..bbbaaeb8a9e 100644 --- a/app/assets/javascripts/vue_shared/components/metric_images/metric_images_table.vue +++ b/app/assets/javascripts/vue_shared/components/metric_images/metric_images_table.vue @@ -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" diff --git a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue index a069d1cd756..21212e82de4 100644 --- a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue +++ b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue @@ -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)" > diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 5dd71cec8d1..37b61d36911 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -49,7 +49,7 @@ margin: 0 0 10px; } - .dropdown-menu-toggle, + .dropdown-menu-toggle.dropdown-menu-toggle, .update-issues-btn .btn { width: 100%; } diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss index 9f93f80aa61..ec7ffdb3dce 100644 --- a/app/assets/stylesheets/startup/startup-dark.scss +++ b/app/assets/stylesheets/startup/startup-dark.scss @@ -738,7 +738,7 @@ body { } } @media (max-width: 767.98px) { - .dropdown-menu-toggle { + .dropdown-menu-toggle.dropdown-menu-toggle { width: 100%; } } diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss index 999f1772118..aacf7cf471a 100644 --- a/app/assets/stylesheets/startup/startup-general.scss +++ b/app/assets/stylesheets/startup/startup-general.scss @@ -723,7 +723,7 @@ body { } } @media (max-width: 767.98px) { - .dropdown-menu-toggle { + .dropdown-menu-toggle.dropdown-menu-toggle { width: 100%; } } diff --git a/app/graphql/resolvers/group_packages_resolver.rb b/app/graphql/resolvers/group_packages_resolver.rb index d91fe84317d..8cb1b36e2c0 100644 --- a/app/graphql/resolvers/group_packages_resolver.rb +++ b/app/graphql/resolvers/group_packages_resolver.rb @@ -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 diff --git a/app/graphql/resolvers/package_pipelines_resolver.rb b/app/graphql/resolvers/package_pipelines_resolver.rb index 59a1cd173a4..e087ce735a1 100644 --- a/app/graphql/resolvers/package_pipelines_resolver.rb +++ b/app/graphql/resolvers/package_pipelines_resolver.rb @@ -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 diff --git a/app/graphql/resolvers/project_packages_resolver.rb b/app/graphql/resolvers/project_packages_resolver.rb index 6d66c2fe460..67b4734972a 100644 --- a/app/graphql/resolvers/project_packages_resolver.rb +++ b/app/graphql/resolvers/project_packages_resolver.rb @@ -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 diff --git a/app/graphql/types/packages/package_details_type.rb b/app/graphql/types/packages/package_details_type.rb index 444ecb5e792..d5c4dbf648f 100644 --- a/app/graphql/types/packages/package_details_type.rb +++ b/app/graphql/types/packages/package_details_type.rb @@ -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.' diff --git a/app/graphql/types/packages/package_type.rb b/app/graphql/types/packages/package_type.rb index 41c6dde20a9..953637a5dfd 100644 --- a/app/graphql/types/packages/package_type.rb +++ b/app/graphql/types/packages/package_type.rb @@ -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.' diff --git a/app/models/concerns/integrations/slack_mattermost_notifier.rb b/app/models/concerns/integrations/slack_mattermost_notifier.rb index be13701289a..3bdaa852ddf 100644 --- a/app/models/concerns/integrations/slack_mattermost_notifier.rb +++ b/app/models/concerns/integrations/slack_mattermost_notifier.rb @@ -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 diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb index 8825357af2a..6bd6bac04bc 100644 --- a/app/models/container_repository.rb +++ b/app/models/container_repository.rb @@ -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 diff --git a/app/models/packages/build_info.rb b/app/models/packages/build_info.rb index 38245bef7a5..61e2194006b 100644 --- a/app/models/packages/build_info.rb +++ b/app/models/packages/build_info.rb @@ -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 diff --git a/app/services/container_expiration_policies/cleanup_service.rb b/app/services/container_expiration_policies/cleanup_service.rb index 0da5e552c48..34889e58127 100644 --- a/app/services/container_expiration_policies/cleanup_service.rb +++ b/app/services/container_expiration_policies/cleanup_service.rb @@ -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) diff --git a/config/feature_flags/development/packages_graphql_pipelines_resolver.yml b/config/feature_flags/development/packages_graphql_pipelines_resolver.yml new file mode 100644 index 00000000000..6509b9896b4 --- /dev/null +++ b/config/feature_flags/development/packages_graphql_pipelines_resolver.yml @@ -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 diff --git a/db/migrate/20220421180321_add_last_cleanup_deleted_tags_count_to_container_repository.rb b/db/migrate/20220421180321_add_last_cleanup_deleted_tags_count_to_container_repository.rb new file mode 100644 index 00000000000..141bc4b49e6 --- /dev/null +++ b/db/migrate/20220421180321_add_last_cleanup_deleted_tags_count_to_container_repository.rb @@ -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 diff --git a/db/schema_migrations/20220421180321 b/db/schema_migrations/20220421180321 new file mode 100644 index 00000000000..b2ffb29adf1 --- /dev/null +++ b/db/schema_migrations/20220421180321 @@ -0,0 +1 @@ +71cde7610713f9e2e21f87a2176cc4ec5fdc797021edab144adfaaf463acb8ef
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 5ba0dbf661c..79fc48f1ab3 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -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)) diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 8122cffefcd..ac4df4f55ad 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -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. | diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 1d017af2712..ea53d7d5f9e 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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, you’ll 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 "" diff --git a/package.json b/package.json index 28e9c41693d..00421ca30b0 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/spec/frontend/invite_members/components/invite_members_modal_spec.js b/spec/frontend/invite_members/components/invite_members_modal_spec.js index 84317da39e6..13985ce7d74 100644 --- a/spec/frontend/invite_members/components/invite_members_modal_spec.js +++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js @@ -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); + }); + }); }); }); }); diff --git a/spec/frontend/invite_members/components/invite_modal_base_spec.js b/spec/frontend/invite_members/components/invite_modal_base_spec.js index 8355ae67f20..ad4cb5699b6 100644 --- a/spec/frontend/invite_members/components/invite_modal_base_spec.js +++ b/spec/frontend/invite_members/components/invite_modal_base_spec.js @@ -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, }); diff --git a/spec/frontend/invite_members/components/user_limit_notification_spec.js b/spec/frontend/invite_members/components/user_limit_notification_spec.js index c779cf2ee3f..9f61833a4cd 100644 --- a/spec/frontend/invite_members/components/user_limit_notification_spec.js +++ b/spec/frontend/invite_members/components/user_limit_notification_spec.js @@ -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); }); }); }); diff --git a/spec/frontend/invite_members/mock_data/member_modal.js b/spec/frontend/invite_members/mock_data/member_modal.js index 1b0cc57fb5b..474234cfacb 100644 --- a/spec/frontend/invite_members/mock_data/member_modal.js +++ b/spec/frontend/invite_members/mock_data/member_modal.js @@ -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: '' }; diff --git a/spec/frontend/invite_members/mock_data/modal_base.js b/spec/frontend/invite_members/mock_data/modal_base.js index ea5a8d2b00d..565e8d4df1e 100644 --- a/spec/frontend/invite_members/mock_data/modal_base.js +++ b/spec/frontend/invite_members/mock_data/modal_base.js @@ -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'; diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js index a8d0d15007c..ca666e38291 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js @@ -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(); diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js index 266f953c3e0..465e6dc73e2 100644 --- a/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js +++ b/spec/frontend/packages_and_registries/settings/project/settings/components/settings_form_spec.js @@ -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(); diff --git a/spec/frontend/runner/runner_search_utils_spec.js b/spec/frontend/runner/runner_search_utils_spec.js index 7834e76fe48..a3c1458ed26 100644 --- a/spec/frontend/runner/runner_search_utils_spec.js +++ b/spec/frontend/runner/runner_search_utils_spec.js @@ -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?'; diff --git a/spec/graphql/resolvers/package_pipelines_resolver_spec.rb b/spec/graphql/resolvers/package_pipelines_resolver_spec.rb index c757c876616..463785f3026 100644 --- a/spec/graphql/resolvers/package_pipelines_resolver_spec.rb +++ b/spec/graphql/resolvers/package_pipelines_resolver_spec.rb @@ -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 - end - context 'with invalid after key' do - let(:args) { { first: 1, after: encode_cursor(foo: 3) } } + context 'with valid before' do + let(:pagination_args) { { last: 1, before: 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[2]) end end - end - context 'with invalid before' do - let(:args) { { last: 1, before: 'not_json_string' } } + context 'with invalid after' do + let(:pagination_args) { { first: 1, after: 'not_json_string' } } - it 'generates an argument error' do - expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError) do - subject + it 'generates an argument error' do + expect(returned_errors).to include('Please provide a valid cursor') end end - end - context 'with invalid before key' do - let(:args) { { last: 1, before: encode_cursor(foo: 3) } } + context 'with invalid after key' do + let(:pagination_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 + it 'generates an argument error' do + expect(returned_errors).to include('Please provide a valid cursor') 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 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 - it 'sets them properly' do - expect(field).not_to be_connection - expect(field.extras).to match_array([:lookahead]) + 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 unauthorized user' do - let_it_be(:user) { create(:user) } + 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 { 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 diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb index 9affb9e4fa2..7d0dfad91b2 100644 --- a/spec/models/container_repository_spec.rb +++ b/spec/models/container_repository_spec.rb @@ -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 diff --git a/spec/services/container_expiration_policies/cleanup_service_spec.rb b/spec/services/container_expiration_policies/cleanup_service_spec.rb index a1f76e5e5dd..c265ce74d14 100644 --- a/spec/services/container_expiration_policies/cleanup_service_spec.rb +++ b/spec/services/container_expiration_policies/cleanup_service_spec.rb @@ -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 diff --git a/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb b/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb index 3d6fec85490..da8562161e7 100644 --- a/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb +++ b/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb @@ -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 diff --git a/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb index da5c35c970a..2e062cda4e9 100644 --- a/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb @@ -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 diff --git a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb index 127b1a6d4c4..5d6e95f2fbc 100644 --- a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb @@ -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 diff --git a/yarn.lock b/yarn.lock index 0631b7dbd9b..bffac85940a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" |