Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-09-12 21:10:38 +00:00
parent e4cfc16da3
commit 3b69a04945
170 changed files with 2269 additions and 855 deletions

View File

@ -19,7 +19,10 @@ update-static-analysis-cache:
- .shared:rules:update-cache
stage: prepare
script:
- run_timed_command "bundle exec rubocop --parallel" # For the moment we only cache `tmp/rubocop_cache` so we don't need to run all the tasks.
# Silence cop offenses for rules with "grace period".
# This will notify Slack if offenses were silenced.
# For the moment we only cache `tmp/rubocop_cache` so we don't need to run all the tasks.
- run_timed_command "bundle exec rake rubocop:check:graceful"
static-analysis:
extends:
@ -121,7 +124,11 @@ rubocop:
- |
# For non-merge request, or when RUN_ALL_RUBOCOP is 'true', run all RuboCop rules
if [ -z "${CI_MERGE_REQUEST_IID}" ] || [ "${RUN_ALL_RUBOCOP}" == "true" ]; then
run_timed_command "bundle exec rubocop --parallel"
# Silence cop offenses for rules with "grace period".
# We won't notify Slack if offenses were silenced to avoid frequent messages.
# Job `update-static-analysis-cache` takes care of Slack notifications every 2 hours.
unset CI_SLACK_WEBHOOK_URL
run_timed_command "bundle exec rake rubocop:check:graceful"
else
run_timed_command "bundle exec rubocop --parallel --force-exclusion $(cat ${RSPEC_CHANGED_FILES_PATH})"
fi

View File

@ -0,0 +1,52 @@
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
export default {
components: {
GlLink,
GlSprintf,
},
props: {
message: {
type: String,
required: true,
},
},
i18n: {
fieldHelpText: s__(
'AdminSettings|If no unit is written, it defaults to seconds. For example, these are all equivalent: %{oneDayInSeconds}, %{oneDayInHoursHumanReadable}, or %{oneDayHumanReadable}. Minimum value is two hours. %{linkStart}Learn more.%{linkEnd}',
),
},
computed: {
helpUrl() {
return helpPagePath('ci/runners/configure_runners', {
anchor: 'authentication-token-security',
});
},
},
};
</script>
<template>
<p>
{{ message }}
<gl-sprintf :message="$options.i18n.fieldHelpText">
<template #oneDayInSeconds>
<!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
<code>86400</code>
</template>
<template #oneDayInHoursHumanReadable>
<!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
<code>24 hours</code>
</template>
<template #oneDayHumanReadable>
<!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
<code>1 day</code>
</template>
<template #link>
<gl-link :href="helpUrl" target="_blank">{{ __('Learn more.') }}</gl-link>
</template>
</gl-sprintf>
</p>
</template>

View File

@ -0,0 +1,123 @@
<script>
import { GlFormGroup } from '@gitlab/ui';
import { s__ } from '~/locale';
import ChronicDurationInput from '~/vue_shared/components/chronic_duration_input.vue';
import ExpirationIntervalDescription from './expiration_interval_description.vue';
export default {
components: {
ChronicDurationInput,
ExpirationIntervalDescription,
GlFormGroup,
},
props: {
instanceRunnerExpirationInterval: {
type: Number,
required: false,
default: null,
},
groupRunnerExpirationInterval: {
type: Number,
required: false,
default: null,
},
projectRunnerExpirationInterval: {
type: Number,
required: false,
default: null,
},
},
data() {
return {
perInput: {
instance: {
value: this.instanceRunnerExpirationInterval,
valid: null,
feedback: '',
},
group: {
value: this.groupRunnerExpirationInterval,
valid: null,
feedback: '',
},
project: {
value: this.projectRunnerExpirationInterval,
valid: null,
feedback: '',
},
},
};
},
methods: {
updateValidity(obj, event) {
/* eslint-disable no-param-reassign */
obj.valid = event.valid;
obj.feedback = event.feedback;
/* eslint-enable no-param-reassign */
},
},
i18n: {
instanceRunnerTitle: s__('AdminSettings|Instance runners expiration'),
instanceRunnerDescription: s__(
'AdminSettings|Set the expiration time of authentication tokens of newly registered instance runners. Authentication tokens are automatically reset at these intervals.',
),
groupRunnerTitle: s__('AdminSettings|Group runners expiration'),
groupRunnerDescription: s__(
'AdminSettings|Set the expiration time of authentication tokens of newly registered group runners.',
),
projectRunnerTitle: s__('AdminSettings|Project runners expiration'),
projectRunnerDescription: s__(
'AdminSettings|Set the expiration time of authentication tokens of newly registered project runners.',
),
},
};
</script>
<template>
<div>
<gl-form-group
:label="$options.i18n.instanceRunnerTitle"
:invalid-feedback="perInput.instance.feedback"
:state="perInput.instance.valid"
>
<template #description>
<expiration-interval-description :message="$options.i18n.instanceRunnerDescription" />
</template>
<chronic-duration-input
v-model="perInput.instance.value"
name="application_setting[runner_token_expiration_interval]"
:state="perInput.instance.valid"
@valid="updateValidity(perInput.instance, $event)"
/>
</gl-form-group>
<gl-form-group
:label="$options.i18n.groupRunnerTitle"
:invalid-feedback="perInput.group.feedback"
:state="perInput.group.valid"
>
<template #description>
<expiration-interval-description :message="$options.i18n.groupRunnerDescription" />
</template>
<chronic-duration-input
v-model="perInput.group.value"
name="application_setting[group_runner_token_expiration_interval]"
:state="perInput.group.valid"
@valid="updateValidity(perInput.group, $event)"
/>
</gl-form-group>
<gl-form-group
:label="$options.i18n.projectRunnerTitle"
:invalid-feedback="perInput.project.feedback"
:state="perInput.project.valid"
>
<template #description>
<expiration-interval-description :message="$options.i18n.projectRunnerDescription" />
</template>
<chronic-duration-input
v-model="perInput.project.value"
name="application_setting[project_runner_token_expiration_interval]"
:state="perInput.project.valid"
@valid="updateValidity(perInput.project, $event)"
/>
</gl-form-group>
</div>
</template>

View File

@ -0,0 +1,32 @@
import Vue from 'vue';
import { parseInterval } from '~/runner/utils';
import ExpirationIntervals from './components/expiration_intervals.vue';
const initRunnerTokenExpirationIntervals = (selector = '#js-runner-token-expiration-intervals') => {
const el = document.querySelector(selector);
if (!el) {
return null;
}
const {
instanceRunnerTokenExpirationInterval,
groupRunnerTokenExpirationInterval,
projectRunnerTokenExpirationInterval,
} = el.dataset;
return new Vue({
el,
render(h) {
return h(ExpirationIntervals, {
props: {
instanceRunnerExpirationInterval: parseInterval(instanceRunnerTokenExpirationInterval),
groupRunnerExpirationInterval: parseInterval(groupRunnerTokenExpirationInterval),
projectRunnerExpirationInterval: parseInterval(projectRunnerTokenExpirationInterval),
},
});
},
});
};
export default initRunnerTokenExpirationIntervals;

View File

@ -0,0 +1,3 @@
import initRunnerTokenExpirationIntervals from '~/admin/application_settings/runner_token_expiration/index';
initRunnerTokenExpirationIntervals();

View File

@ -1,19 +0,0 @@
import '~/commons/bootstrap';
import { AwardsHandler } from '~/awards_handler';
class EmojiMenu extends AwardsHandler {
constructor(emoji, toggleButtonSelector, menuClass, selectEmojiCallback) {
super(emoji);
this.selectEmojiCallback = selectEmojiCallback;
this.toggleButtonSelector = toggleButtonSelector;
this.menuClass = menuClass;
}
postEmoji($emojiButton, awardUrl, selectedEmoji, callback) {
this.selectEmojiCallback(selectedEmoji, this.emoji.glEmojiTag(selectedEmoji));
callback();
}
}
export default EmojiMenu;

View File

@ -1,90 +1,18 @@
import emojiRegex from 'emoji-regex';
import $ from 'jquery';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import * as Emoji from '~/emoji';
import createFlash from '~/flash';
import { __ } from '~/locale';
import EmojiMenu from './emoji_menu';
import { initSetStatusForm } from '~/profile/profile';
const defaultStatusEmoji = 'speech_balloon';
const toggleEmojiMenuButtonSelector = '.js-toggle-emoji-menu';
const toggleEmojiMenuButton = document.querySelector(toggleEmojiMenuButtonSelector);
const statusEmojiField = document.getElementById('js-status-emoji-field');
const statusMessageField = document.getElementById('js-status-message-field');
const toggleNoEmojiPlaceholder = (isVisible) => {
const placeholderElement = document.getElementById('js-no-emoji-placeholder');
placeholderElement.classList.toggle('hidden', !isVisible);
};
const findStatusEmoji = () => toggleEmojiMenuButton.querySelector('gl-emoji');
const removeStatusEmoji = () => {
const statusEmoji = findStatusEmoji();
if (statusEmoji) {
statusEmoji.remove();
}
};
const selectEmojiCallback = (emoji, emojiTag) => {
statusEmojiField.value = emoji;
toggleNoEmojiPlaceholder(false);
removeStatusEmoji();
// eslint-disable-next-line no-unsanitized/property
toggleEmojiMenuButton.innerHTML += emojiTag;
};
const clearEmojiButton = document.getElementById('js-clear-user-status-button');
clearEmojiButton.addEventListener('click', () => {
statusEmojiField.value = '';
statusMessageField.value = '';
removeStatusEmoji();
toggleNoEmojiPlaceholder(true);
});
const emojiAutocomplete = new GfmAutoComplete();
emojiAutocomplete.setup($(statusMessageField), { emojis: true });
initSetStatusForm();
const userNameInput = document.getElementById('user_name');
userNameInput.addEventListener('input', () => {
const EMOJI_REGEX = emojiRegex();
if (EMOJI_REGEX.test(userNameInput.value)) {
// set field to invalid so it gets detected by GlFieldErrors
userNameInput.setCustomValidity(__('Invalid field'));
} else {
userNameInput.setCustomValidity('');
}
});
Emoji.initEmojiMap()
.then(() => {
const emojiMenu = new EmojiMenu(
Emoji,
toggleEmojiMenuButtonSelector,
'js-status-emoji-menu',
selectEmojiCallback,
);
emojiMenu.bindEvents();
const defaultEmojiTag = Emoji.glEmojiTag(defaultStatusEmoji);
statusMessageField.addEventListener('input', () => {
const hasStatusMessage = statusMessageField.value.trim() !== '';
const statusEmoji = findStatusEmoji();
if (hasStatusMessage && statusEmoji) {
return;
}
if (hasStatusMessage) {
toggleNoEmojiPlaceholder(false);
// eslint-disable-next-line no-unsanitized/property
toggleEmojiMenuButton.innerHTML += defaultEmojiTag;
} else if (statusEmoji.dataset.name === defaultStatusEmoji) {
toggleNoEmojiPlaceholder(true);
removeStatusEmoji();
}
});
})
.catch(() =>
createFlash({
message: __('Failed to load emoji list.'),
}),
);
if (userNameInput) {
userNameInput.addEventListener('input', () => {
const EMOJI_REGEX = emojiRegex();
if (EMOJI_REGEX.test(userNameInput.value)) {
// set field to invalid so it gets detected by GlFieldErrors
userNameInput.setCustomValidity(__('Invalid field'));
} else {
userNameInput.setCustomValidity('');
}
});
}

View File

@ -170,7 +170,7 @@ export default {
ref="mainPipelineContainer"
class="gl-display-flex gl-position-relative gl-bg-gray-10 gl-white-space-nowrap"
:class="{
'gl-pipeline-min-h gl-py-5 gl-overflow-auto gl-border-t-solid gl-border-t-1 gl-border-gray-100': !isLinkedPipeline,
'gl-pipeline-min-h gl-py-5 gl-overflow-auto': !isLinkedPipeline,
}"
>
<linked-graph-wrapper>

View File

@ -82,7 +82,9 @@ export default {
:stage-name="stageName"
/>
<div class="gl-font-weight-100 gl-font-size-lg gl-ml-n4">{{ group.size }}</div>
<div class="gl-font-weight-100 gl-font-size-lg gl-ml-n4 gl-align-self-center">
{{ group.size }}
</div>
</div>
</button>

View File

@ -64,8 +64,7 @@ export default {
},
},
jobClasses: [
'gl-py-3',
'gl-px-4',
'gl-p-3',
'gl-border-gray-100',
'gl-border-solid',
'gl-border-1',

View File

@ -1,11 +1,14 @@
import $ from 'jquery';
import Vue from 'vue';
import { VARIANT_DANGER, VARIANT_INFO, createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { parseBoolean } from '~/lib/utils/common_utils';
import { parseRailsFormFields } from '~/lib/utils/forms';
import { Rails } from '~/lib/utils/rails_ujs';
import TimezoneDropdown, {
formatTimezone,
} from '~/pages/projects/pipeline_schedules/shared/components/timezone_dropdown';
import UserProfileSetStatusWrapper from '~/set_status_modal/user_profile_set_status_wrapper.vue';
export default class Profile {
constructor({ form } = {}) {
@ -116,3 +119,24 @@ export default class Profile {
}
}
}
export const initSetStatusForm = () => {
const el = document.getElementById('js-user-profile-set-status-form');
if (!el) {
return null;
}
const fields = parseRailsFormFields(el);
return new Vue({
el,
name: 'UserProfileStatusForm',
provide: {
fields,
},
render(h) {
return h(UserProfileSetStatusWrapper);
},
});
};

View File

@ -21,7 +21,8 @@ export default {
props: {
label: {
type: String,
required: true,
default: null,
required: false,
},
value: {
type: String,
@ -39,7 +40,11 @@ export default {
<template>
<div class="gl-display-contents">
<dt class="gl-mb-5 gl-mr-6 gl-max-w-26">{{ label }}</dt>
<dt class="gl-mb-5 gl-mr-6 gl-max-w-26">
<template v-if="label || $scopedSlots.label">
<slot name="label">{{ label }}</slot>
</template>
</dt>
<dd class="gl-mb-5">
<template v-if="value || $scopedSlots.value">
<slot name="value">{{ value }}</slot>

View File

@ -1,7 +1,10 @@
<script>
import { GlIntersperse } from '@gitlab/ui';
import { GlIntersperse, GlLink } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import { ACCESS_LEVEL_REF_PROTECTED, GROUP_TYPE, PROJECT_TYPE } from '../constants';
import RunnerDetail from './runner_detail.vue';
@ -12,6 +15,8 @@ import RunnerTags from './runner_tags.vue';
export default {
components: {
GlIntersperse,
GlLink,
HelpPopover,
RunnerDetail,
RunnerMaintenanceNoteDetail: () =>
import('ee_component/runner/components/runner_maintenance_note_detail.vue'),
@ -24,6 +29,7 @@ export default {
RunnerTags,
TimeAgo,
},
mixins: [glFeatureFlagMixin()],
props: {
runner: {
type: Object,
@ -60,6 +66,16 @@ export default {
isProjectRunner() {
return this.runner?.runnerType === PROJECT_TYPE;
},
tokenExpirationHelpPopoverOptions() {
return {
title: s__('Runners|Runner authentication token expiration'),
};
},
tokenExpirationHelpUrl() {
return helpPagePath('ci/runners/configure_runners', {
anchor: 'authentication-token-security',
});
},
},
ACCESS_LEVEL_REF_PROTECTED,
};
@ -101,6 +117,34 @@ export default {
</template>
</runner-detail>
<runner-detail :label="s__('Runners|Maximum job timeout')" :value="maximumTimeout" />
<runner-detail
v-if="glFeatures.enforceRunnerTokenExpiresAt"
:empty-value="s__('Runners|Never expires')"
>
<template #label>
{{ s__('Runners|Token expiry') }}
<help-popover :options="tokenExpirationHelpPopoverOptions">
<p>
{{
s__(
'Runners|Runner authentication tokens will expire based on a set interval. They will automatically rotate once expired.',
)
}}
</p>
<p class="gl-mb-0">
<gl-link
:href="tokenExpirationHelpUrl"
target="_blank"
class="gl-reset-font-size"
>{{ __('Learn more') }}</gl-link
>
</p>
</help-popover>
</template>
<template v-if="runner.tokenExpiresAt" #value>
<time-ago :time="runner.tokenExpiresAt" />
</template>
</runner-detail>
<runner-detail :label="s__('Runners|Tags')">
<template v-if="tagList.length" #value>
<runner-tags class="gl-vertical-align-middle" :tag-list="tagList" size="sm" />

View File

@ -17,6 +17,7 @@ fragment RunnerDetailsShared on CiRunner {
createdAt
status(legacyMode: null)
contactedAt
tokenExpiresAt
version
editAdminUrl
userPermissions {

View File

@ -70,3 +70,14 @@ export const getPaginationVariables = (pagination, pageSize = 10) => {
// Get the first N items
return { first: pageSize };
};
/**
* Turns a server-provided interval integer represented as a string into an
* integer that the frontend can use.
*
* @param {String} interval - String to convert
* @returns Parsed integer
*/
export const parseInterval = (interval) => {
return typeof interval === 'string' ? parseInt(interval, 10) : null;
};

View File

@ -0,0 +1,14 @@
import { timeRanges } from '~/vue_shared/constants';
import { __ } from '~/locale';
export const NEVER_TIME_RANGE = {
label: __('Never'),
name: 'never',
};
export const TIME_RANGES_WITH_NEVER = [NEVER_TIME_RANGE, ...timeRanges];
export const AVAILABILITY_STATUS = {
BUSY: 'busy',
NOT_SET: 'not_set',
};

View File

@ -9,26 +9,14 @@ import {
GlDropdown,
GlDropdownItem,
GlSprintf,
GlFormGroup,
GlSafeHtmlDirective,
} from '@gitlab/ui';
import $ from 'jquery';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import * as Emoji from '~/emoji';
import { __, s__ } from '~/locale';
import { timeRanges } from '~/vue_shared/constants';
export const AVAILABILITY_STATUS = {
BUSY: 'busy',
NOT_SET: 'not_set',
};
const statusTimeRanges = [
{
label: __('Never'),
name: 'never',
},
...timeRanges,
];
import { s__ } from '~/locale';
import { TIME_RANGES_WITH_NEVER, AVAILABILITY_STATUS } from './constants';
export default {
components: {
@ -40,6 +28,7 @@ export default {
GlDropdown,
GlDropdownItem,
GlSprintf,
GlFormGroup,
EmojiPicker: () => import('~/emoji/components/picker.vue'),
},
directives: {
@ -136,7 +125,8 @@ export default {
this.clearEmoji();
},
},
statusTimeRanges,
TIME_RANGES_WITH_NEVER,
AVAILABILITY_STATUS,
safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
i18n: {
statusMessagePlaceholder: s__(`SetStatusModal|What's your status?`),
@ -153,14 +143,11 @@ export default {
<template>
<div>
<input :value="emoji" class="js-status-emoji-field" type="hidden" name="user[status][emoji]" />
<gl-form-input-group class="gl-mb-5">
<gl-form-input
ref="statusMessageField"
:value="message"
:placeholder="$options.i18n.statusMessagePlaceholder"
class="js-status-message-field"
name="user[status][message]"
@keyup="setDefaultEmoji"
@input="$emit('message-input', $event)"
@keyup.enter.prevent
@ -216,28 +203,29 @@ export default {
</template>
</gl-form-checkbox>
<div class="form-group">
<div class="gl-display-flex gl-align-items-baseline">
<span class="gl-mr-3">{{ $options.i18n.clearStatusAfterDropdownLabel }}</span>
<gl-dropdown :text="clearStatusAfter.label" data-testid="clear-status-at-dropdown">
<gl-dropdown-item
v-for="after in $options.statusTimeRanges"
:key="after.name"
:data-testid="after.name"
@click="$emit('clear-status-after-click', after)"
>{{ after.label }}</gl-dropdown-item
>
</gl-dropdown>
</div>
<p
v-if="currentClearStatusAfter.length"
class="gl-mt-3 gl-text-gray-400 gl-font-sm"
data-testid="clear-status-at-message"
<gl-form-group :label="$options.i18n.clearStatusAfterDropdownLabel" class="gl-mb-0">
<gl-dropdown
block
:text="clearStatusAfter.label"
data-testid="clear-status-at-dropdown"
toggle-class="gl-mb-0 gl-form-input-md"
>
<gl-sprintf :message="$options.i18n.clearStatusAfterMessage">
<template #date>{{ currentClearStatusAfter }}</template>
</gl-sprintf>
</p>
</div>
<gl-dropdown-item
v-for="after in $options.TIME_RANGES_WITH_NEVER"
:key="after.name"
:data-testid="after.name"
@click="$emit('clear-status-after-click', after)"
>{{ after.label }}</gl-dropdown-item
>
</gl-dropdown>
<template v-if="currentClearStatusAfter.length" #description>
<span data-testid="clear-status-at-message">
<gl-sprintf :message="$options.i18n.clearStatusAfterMessage">
<template #date>{{ currentClearStatusAfter }}</template>
</gl-sprintf>
</span>
</template>
</gl-form-group>
</div>
</template>

View File

@ -3,28 +3,15 @@ import { GlToast, GlTooltipDirective, GlSafeHtmlDirective, GlModal } from '@gitl
import Vue from 'vue';
import createFlash from '~/flash';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import { __, s__ } from '~/locale';
import { s__ } from '~/locale';
import { updateUserStatus } from '~/rest_api';
import { timeRanges } from '~/vue_shared/constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { isUserBusy } from './utils';
import { NEVER_TIME_RANGE, AVAILABILITY_STATUS } from './constants';
import SetStatusForm from './set_status_form.vue';
export const AVAILABILITY_STATUS = {
BUSY: 'busy',
NOT_SET: 'not_set',
};
Vue.use(GlToast);
const statusTimeRanges = [
{
label: __('Never'),
name: 'never',
},
...timeRanges,
];
export default {
components: {
GlModal,
@ -67,7 +54,7 @@ export default {
message: this.currentMessage,
modalId: 'set-user-status-modal',
availability: isUserBusy(this.currentAvailability),
clearStatusAfter: statusTimeRanges[0],
clearStatusAfter: NEVER_TIME_RANGE,
};
},
mounted() {
@ -91,7 +78,7 @@ export default {
message,
availability: availability ? AVAILABILITY_STATUS.BUSY : AVAILABILITY_STATUS.NOT_SET,
clearStatusAfter:
clearStatusAfter.label === statusTimeRanges[0].label ? null : clearStatusAfter.shortcut,
clearStatusAfter.label === NEVER_TIME_RANGE.label ? null : clearStatusAfter.shortcut,
})
.then(this.onUpdateSuccess)
.catch(this.onUpdateFail);
@ -123,7 +110,6 @@ export default {
this.availability = value;
},
},
statusTimeRanges,
safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
actionPrimary: { text: s__('SetStatusModal|Set status') },
actionSecondary: { text: s__('SetStatusModal|Remove status') },

View File

@ -0,0 +1,100 @@
<script>
import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
import dateFormat from '~/lib/dateformat';
import SetStatusForm from './set_status_form.vue';
import { isUserBusy } from './utils';
import { NEVER_TIME_RANGE, AVAILABILITY_STATUS } from './constants';
export default {
components: { SetStatusForm },
inject: ['fields'],
data() {
return {
emoji: this.fields.emoji.value,
message: this.fields.message.value,
availability: isUserBusy(this.fields.availability.value),
clearStatusAfter: NEVER_TIME_RANGE,
currentClearStatusAfter: this.fields.clearStatusAfter.value,
};
},
computed: {
clearStatusAfterInputValue() {
return this.clearStatusAfter.label === NEVER_TIME_RANGE.label
? null
: this.clearStatusAfter.shortcut;
},
availabilityInputValue() {
return this.availability
? this.$options.AVAILABILITY_STATUS.BUSY
: this.$options.AVAILABILITY_STATUS.NOT_SET;
},
},
mounted() {
this.$options.formEl = document.querySelector('form.js-edit-user');
if (!this.$options.formEl) return;
this.$options.formEl.addEventListener('ajax:success', this.handleFormSuccess);
},
beforeDestroy() {
if (!this.$options.formEl) return;
this.$options.formEl.removeEventListener('ajax:success', this.handleFormSuccess);
},
methods: {
handleMessageInput(value) {
this.message = value;
},
handleEmojiClick(emoji) {
this.emoji = emoji;
},
handleClearStatusAfterClick(after) {
this.clearStatusAfter = after;
},
handleAvailabilityInput(value) {
this.availability = value;
},
handleFormSuccess() {
if (!this.clearStatusAfter?.duration?.seconds) {
this.currentClearStatusAfter = '';
return;
}
const now = new Date();
const currentClearStatusAfterDate = new Date(
now.getTime() + secondsToMilliseconds(this.clearStatusAfter.duration.seconds),
);
this.currentClearStatusAfter = dateFormat(
currentClearStatusAfterDate,
"UTC:yyyy-mm-dd HH:MM:ss 'UTC'",
);
this.clearStatusAfter = NEVER_TIME_RANGE;
},
},
AVAILABILITY_STATUS,
formEl: null,
};
</script>
<template>
<div>
<input :value="emoji" type="hidden" :name="fields.emoji.name" />
<input :value="message" type="hidden" :name="fields.message.name" />
<input :value="availabilityInputValue" type="hidden" :name="fields.availability.name" />
<input :value="clearStatusAfterInputValue" type="hidden" :name="fields.clearStatusAfter.name" />
<set-status-form
default-emoji="speech_balloon"
:emoji="emoji"
:message="message"
:availability="availability"
:clear-status-after="clearStatusAfter"
:current-clear-status-after="currentClearStatusAfter"
@message-input="handleMessageInput"
@emoji-click="handleEmojiClick"
@clear-status-after-click="handleClearStatusAfterClick"
@availability-input="handleAvailabilityInput"
/>
</div>
</template>

View File

@ -1,7 +1,4 @@
export const AVAILABILITY_STATUS = {
BUSY: 'busy',
NOT_SET: 'not_set',
};
import { AVAILABILITY_STATUS } from './constants';
export const isUserBusy = (status = '') =>
Boolean(status.length && status.toLowerCase().trim() === AVAILABILITY_STATUS.BUSY);

View File

@ -8,7 +8,7 @@ import { __, s__ } from '~/locale';
import Tracking from '~/tracking';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import workItemQuery from '../graphql/work_item.query.graphql';
import updateWorkItemWidgetsMutation from '../graphql/update_work_item_widgets.mutation.graphql';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import { i18n, TRACKING_CATEGORY_SHOW, WIDGET_TYPE_DESCRIPTION } from '../constants';
export default {
@ -142,9 +142,9 @@ export default {
this.track('updated_description');
const {
data: { workItemUpdateWidgets },
data: { workItemUpdate },
} = await this.$apollo.mutate({
mutation: updateWorkItemWidgetsMutation,
mutation: updateWorkItemMutation,
variables: {
input: {
id: this.workItem.id,
@ -155,8 +155,8 @@ export default {
},
});
if (workItemUpdateWidgets.errors?.length) {
throw new Error(workItemUpdateWidgets.errors[0]);
if (workItemUpdate.errors?.length) {
throw new Error(workItemUpdate.errors[0]);
}
this.isEditing = false;

View File

@ -1,10 +0,0 @@
#import "./work_item.fragment.graphql"
mutation workItemUpdateWidgets($input: WorkItemUpdateWidgetsInput!) {
workItemUpdateWidgets(input: $input) {
workItem {
...WorkItem
}
errors
}
}

View File

@ -9,6 +9,10 @@ class Admin::RunnersController < Admin::ApplicationController
push_frontend_feature_flag(:runner_list_stacked_layout_admin)
end
before_action only: [:show] do
push_frontend_feature_flag(:enforce_runner_token_expires_at)
end
feature_category :runner
urgency :low
@ -23,7 +27,7 @@ class Admin::RunnersController < Admin::ApplicationController
end
def update
if Ci::Runners::UpdateRunnerService.new(@runner).update(runner_params)
if Ci::Runners::UpdateRunnerService.new(@runner).execute(runner_params).success?
respond_to do |format|
format.html { redirect_to edit_admin_runner_path(@runner) }
end
@ -40,7 +44,7 @@ class Admin::RunnersController < Admin::ApplicationController
end
def resume
if Ci::Runners::UpdateRunnerService.new(@runner).update(active: true)
if Ci::Runners::UpdateRunnerService.new(@runner).execute(active: true).success?
redirect_to admin_runners_path, notice: _('Runner was successfully updated.')
else
redirect_to admin_runners_path, alert: _('Runner was not updated.')
@ -48,7 +52,7 @@ class Admin::RunnersController < Admin::ApplicationController
end
def pause
if Ci::Runners::UpdateRunnerService.new(@runner).update(active: false)
if Ci::Runners::UpdateRunnerService.new(@runner).execute(active: false).success?
redirect_to admin_runners_path, notice: _('Runner was successfully updated.')
else
redirect_to admin_runners_path, alert: _('Runner was not updated.')

View File

@ -8,6 +8,10 @@ class Groups::RunnersController < Groups::ApplicationController
push_frontend_feature_flag(:runner_list_stacked_layout, @group)
end
before_action only: [:show] do
push_frontend_feature_flag(:enforce_runner_token_expires_at)
end
feature_category :runner
urgency :low
@ -26,7 +30,7 @@ class Groups::RunnersController < Groups::ApplicationController
end
def update
if Ci::Runners::UpdateRunnerService.new(@runner).update(runner_params)
if Ci::Runners::UpdateRunnerService.new(@runner).execute(runner_params).success?
redirect_to group_runner_path(@group, @runner), notice: _('Runner was successfully updated.')
else
render 'edit'

View File

@ -137,7 +137,7 @@ class ProfilesController < Profiles::ApplicationController
:pronouns,
:pronunciation,
:validation_password,
status: [:emoji, :message, :availability]
status: [:emoji, :message, :availability, :clear_status_after]
]
end

View File

@ -15,7 +15,7 @@ class Projects::RunnersController < Projects::ApplicationController
end
def update
if Ci::Runners::UpdateRunnerService.new(@runner).update(runner_params)
if Ci::Runners::UpdateRunnerService.new(@runner).execute(runner_params).success?
redirect_to project_runner_path(@project, @runner), notice: _('Runner was successfully updated.')
else
render 'edit'
@ -31,7 +31,7 @@ class Projects::RunnersController < Projects::ApplicationController
end
def resume
if Ci::Runners::UpdateRunnerService.new(@runner).update(active: true)
if Ci::Runners::UpdateRunnerService.new(@runner).execute(active: true).success?
redirect_to project_runners_path(@project), notice: _('Runner was successfully updated.')
else
redirect_to project_runners_path(@project), alert: _('Runner was not updated.')
@ -39,7 +39,7 @@ class Projects::RunnersController < Projects::ApplicationController
end
def pause
if Ci::Runners::UpdateRunnerService.new(@runner).update(active: false)
if Ci::Runners::UpdateRunnerService.new(@runner).execute(active: false).success?
redirect_to project_runners_path(@project), notice: _('Runner was successfully updated.')
else
redirect_to project_runners_path(@project), alert: _('Runner was not updated.')

View File

@ -59,9 +59,8 @@ module Mutations
def resolve(id:, **runner_attrs)
runner = authorized_find!(id)
unless ::Ci::Runners::UpdateRunnerService.new(runner).update(runner_attrs)
return { runner: nil, errors: runner.errors.full_messages }
end
result = ::Ci::Runners::UpdateRunnerService.new(runner).execute(runner_attrs)
return { runner: nil, errors: result.errors } if result.error?
{ runner: runner, errors: [] }
end

View File

@ -450,6 +450,14 @@ module ApplicationSettingsHelper
end
end
def runner_token_expiration_interval_attributes
{
instance_runner_token_expiration_interval: @application_setting.runner_token_expiration_interval,
group_runner_token_expiration_interval: @application_setting.group_runner_token_expiration_interval,
project_runner_token_expiration_interval: @application_setting.project_runner_token_expiration_interval
}
end
def external_authorization_service_attributes
[
:external_auth_client_cert,

View File

@ -29,6 +29,10 @@ class UserStatus < ApplicationRecord
cache_markdown_field :message, pipeline: :emoji
def clear_status_after
clear_status_at
end
def clear_status_after=(value)
self.clear_status_at = CLEAR_STATUS_QUICK_OPTIONS[value]&.from_now
end

View File

@ -9,11 +9,14 @@ module Ci
@runner = runner
end
def update(params)
def execute(params)
params[:active] = !params.delete(:paused) if params.include?(:paused)
runner.update(params).tap do |updated|
runner.tick_runner_queue if updated
if runner.update(params)
runner.tick_runner_queue
ServiceResponse.success
else
ServiceResponse.error(message: runner.errors.full_messages)
end
end
end

View File

@ -14,6 +14,13 @@ module SystemNotes
# See also the discussion in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60700#note_612724683
USE_COMMIT_DATE_FOR_CROSS_REFERENCE_NOTE = false
def self.issuable_events
{
review_requested: s_('IssuableEvents|requested review from'),
review_request_removed: s_('IssuableEvents|removed review request for')
}.freeze
end
#
# noteable_ref - Referenced noteable object
#
@ -115,8 +122,8 @@ module SystemNotes
text_parts = []
Gitlab::I18n.with_default_locale do
text_parts << "requested review from #{added_users.map(&:to_reference).to_sentence}" if added_users.any?
text_parts << "removed review request for #{unassigned_users.map(&:to_reference).to_sentence}" if unassigned_users.any?
text_parts << "#{self.class.issuable_events[:review_requested]} #{added_users.map(&:to_reference).to_sentence}" if added_users.any?
text_parts << "#{self.class.issuable_events[:review_request_removed]} #{unassigned_users.map(&:to_reference).to_sentence}" if unassigned_users.any?
end
body = text_parts.join(' and ')

View File

@ -53,6 +53,8 @@
= link_to sprite_icon('question-o'), help_page_path('ci/pipelines/settings', anchor: 'specify-a-custom-cicd-configuration-file'), target: '_blank', rel: 'noopener noreferrer'
.form-group
= f.gitlab_ui_checkbox_component :suggest_pipeline_enabled, s_('AdminSettings|Enable pipeline suggestion banner'), help_text: s_('AdminSettings|Display a banner on merge requests in projects with no pipelines to initiate steps to add a .gitlab-ci.yml file.')
- if Feature.enabled?(:enforce_runner_token_expires_at)
#js-runner-token-expiration-intervals{ data: runner_token_expiration_interval_attributes }
= f.submit _('Save changes'), pajamas_button: true

View File

@ -2,8 +2,6 @@
- page_title s_("Profiles|Edit Profile")
- @content_class = "limit-container-width" unless fluid_layout
- gravatar_link = link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host
- availability = availability_values
- custom_emoji = @user.status&.customized?
= gitlab_ui_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user js-edit-user gl-mt-3 js-quick-submit gl-show-field-errors js-password-prompt-form', remote: true }, authenticity_token: true do |f|
.row.js-search-settings-section
@ -43,39 +41,12 @@
%h4.gl-mt-0= s_("Profiles|Current status")
%p= s_("Profiles|This emoji and message will appear on your profile and throughout the interface.")
.col-lg-8
= f.fields_for :status, @user.status do |status_form|
- emoji_button = render Pajamas::ButtonComponent.new(button_options: { title: s_("Profiles|Add status emoji"),
class: 'js-toggle-emoji-menu emoji-menu-toggle-button has-tooltip' } ) do
- if custom_emoji
= emoji_icon(@user.status.emoji, class: 'gl-mr-0!')
%span#js-no-emoji-placeholder.no-emoji-placeholder{ class: ('hidden' if custom_emoji) }
= sprite_icon('slight-smile', css_class: 'award-control-icon-neutral')
= sprite_icon('smiley', css_class: 'award-control-icon-positive')
= sprite_icon('smile', css_class: 'award-control-icon-super-positive')
- reset_message_button = render Pajamas::ButtonComponent.new(icon: 'close',
button_options: { id: 'js-clear-user-status-button',
class: 'has-tooltip',
title: s_("Profiles|Clear status") } )
= status_form.hidden_field :emoji, id: 'js-status-emoji-field'
.form-group.gl-form-group
= status_form.label :message, s_("Profiles|Your status")
.input-group{ role: 'group' }
.input-group-prepend
= emoji_button
= status_form.text_field :message,
id: 'js-status-message-field',
class: 'form-control gl-form-input input-lg',
placeholder: s_("Profiles|What's your status?")
.input-group-append
= reset_message_button
.form-group.gl-form-group
= status_form.gitlab_ui_checkbox_component :availability,
s_("Profiles|Busy"),
help_text: s_('Profiles|An indicator appears next to your name and avatar.'),
checkbox_options: { data: { testid: "user-availability-checkbox" } },
checked_value: availability["busy"],
unchecked_value: availability["not_set"]
#js-user-profile-set-status-form
= f.fields_for :status, @user.status do |status_form|
= status_form.hidden_field :emoji, data: { js_name: 'emoji' }
= status_form.hidden_field :message, data: { js_name: 'message' }
= status_form.hidden_field :availability, data: { js_name: 'availability' }
= status_form.hidden_field :clear_status_after, data: { js_name: 'clearStatusAfter' }
.col-lg-12
%hr
.row.user-time-preferences.js-search-settings-section

View File

@ -0,0 +1,11 @@
- name: "Container Scanning variables that reference Docker"
announcement_milestone: "15.4"
announcement_date: "2022-09-22"
removal_milestone: "16.0"
removal_date: "2023-05-22"
breaking_change: true
reporter: sam.white
stage: secure
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/371840
body: |
All Container Scanning variables that are prefixed by `DOCKER_` in variable name are deprecated. This includes the `DOCKER_IMAGE`, `DOCKER_PASSWORD`, `DOCKER_USER`, and `DOCKERFILE_PATH` variables. Support for these variables will be removed in the GitLab 16.0 release. Use the [new variable names](https://docs.gitlab.com/ee/user/application_security/container_scanning/#available-cicd-variables) `CS_IMAGE`, `CS_REGISTRY_PASSWORD`, `CS_REGISTRY_USER`, and `CS_DOCKERFILE_PATH` in place of the deprecated names.

View File

@ -912,3 +912,43 @@ To determine which runners need to be upgraded:
- **Outdated - available**: Newer versions are available but upgrading is not critical.
1. Filter the list by status to view which individual runners need to be upgraded.
## Authentication token security
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30942) in GitLab 15.3 [with a flag](../../administration/feature_flags.md) named `enforce_runner_token_expires_at`. Disabled by default.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to
[enable the feature flag](../../administration/feature_flags.md) named `enforce_runner_token_expires_at`.
On GitLab.com, this feature is not available.
Each runner has an [authentication token](../../api/runners.md#registration-and-authentication-tokens)
to connect with the GitLab instance.
To help prevent the token from being compromised, you can have the
token rotate automatically at specified intervals. When the tokens are rotated,
they are updated for each runner, regardless of the runner's status (`online` or `offline`).
No manual intervention should be required, and no running jobs should be affected.
If you need to manually update the authentication token, you can run a
command to [reset the token](https://docs.gitlab.com/runner/commands/#gitlab-runner-reset-token).
### Automatically rotate authentication tokens
You can specify an interval for authentication tokens to rotate.
This rotation helps ensure the security of the tokens assigned to your runners.
Prerequisites:
- Ensure your runners are using [GitLab Runner 15.3 or later](https://docs.gitlab.com/runner/#gitlab-runner-versions).
To automatically rotate runner authentication tokens:
1. On the top bar, select **Menu > Admin**.
1. On the left sidebar, select **Settings > CI/CD**.
1. Expand **Continuous Integration and Deployment**
1. Set a **Runners expiration** time for runners, leave empty for no expiration.
1. Select **Save**.
Before the interval expires, runners automatically request a new authentication token.

View File

@ -99,7 +99,8 @@ artifacts:
path: coverage/cobertura-coverage.xml
```
The collected coverage report is uploaded to GitLab as an artifact.
The collected coverage report is uploaded to GitLab as an artifact. You can use
only one report per job.
GitLab can display the results of coverage report in the merge request
[diff annotations](../testing/test_coverage_visualization.md).

View File

@ -72,3 +72,106 @@ end
Public attributes should only be used if they are accessed outside of the class.
There is not a strong opinion on what strategy is used when attributes are only
accessed internally, as long as there is consistency in related code.
## Newlines style guide
This style guide recommends best practices for newlines in Ruby code.
### Rule: separate code with newlines only to group together related logic
```ruby
# bad
def method
issue = Issue.new
issue.save
render json: issue
end
```
```ruby
# good
def method
issue = Issue.new
issue.save
render json: issue
end
```
### Rule: separate code and block with newlines
#### Newline before block
```ruby
# bad
def method
issue = Issue.new
if issue.save
render json: issue
end
end
```
```ruby
# good
def method
issue = Issue.new
if issue.save
render json: issue
end
end
```
### Rule: Newline after block
```ruby
# bad
def method
if issue.save
issue.send_email
end
render json: issue
end
```
```ruby
# good
def method
if issue.save
issue.send_email
end
render json: issue
end
```
#### Exception: no need for newline when code block starts or ends right inside another code block
```ruby
# bad
def method
if issue
if issue.valid?
issue.save
end
end
end
```
```ruby
# good
def method
if issue
if issue.valid?
issue.save
end
end
end
```

View File

@ -1,108 +1,11 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
redirect_to: 'backend/ruby_style_guide.md#newlines-style-guide'
remove_date: '2022-12-15'
---
# Newlines style guide
This document was moved to [another location](backend/ruby_style_guide.md#newlines-style-guide).
This style guide recommends best practices for newlines in Ruby code.
## Rule: separate code with newlines only to group together related logic
```ruby
# bad
def method
issue = Issue.new
issue.save
render json: issue
end
```
```ruby
# good
def method
issue = Issue.new
issue.save
render json: issue
end
```
## Rule: separate code and block with newlines
### Newline before block
```ruby
# bad
def method
issue = Issue.new
if issue.save
render json: issue
end
end
```
```ruby
# good
def method
issue = Issue.new
if issue.save
render json: issue
end
end
```
## Newline after block
```ruby
# bad
def method
if issue.save
issue.send_email
end
render json: issue
end
```
```ruby
# good
def method
if issue.save
issue.send_email
end
render json: issue
end
```
### Exception: no need for newline when code block starts or ends right inside another code block
```ruby
# bad
def method
if issue
if issue.valid?
issue.save
end
end
end
```
```ruby
# good
def method
if issue
if issue.valid?
issue.save
end
end
end
```
<!-- This redirect file can be deleted after 2022-12-15. -->
<!-- Redirects that point to other docs in the same project expire in three months. -->
<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->

View File

@ -188,6 +188,8 @@ Alternatively you can use the following on each spec run,
bundle exec spring rspec some_spec.rb
```
## RuboCop tasks
## Generate initial RuboCop TODO list
One way to generate the initial list is to run the Rake task `rubocop:todo:generate`:
@ -209,6 +211,18 @@ Some shells require brackets to be escaped or quoted.
See [Resolving RuboCop exceptions](contributing/style_guides.md#resolving-rubocop-exceptions)
on how to proceed from here.
### Run RuboCop in graceful mode
You can run RuboCop in "graceful mode". This means all enabled cop rules are
silenced which have "grace period" activated (via `Details: grace period`).
Run:
```shell
bundle exec rake 'rubocop:check:graceful'
bundle exec rake 'rubocop:check:graceful[Gitlab/NamespacedClass]'
```
## Compile Frontend Assets
You shouldn't ever need to compile frontend assets manually in development, but

View File

@ -1,24 +1,11 @@
---
stage: Systems
group: Distribution
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
comments: false
description: Install a cloud-native version of GitLab
type: index
redirect_to: 'https://docs.gitlab.com/charts/'
remove_date: '2023-09-09'
---
# Cloud-native GitLab **(FREE SELF)**
This document was moved to [another location](https://docs.gitlab.com/charts/).
A [cloud-native](https://gitlab.com/gitlab-org/build/CNG) version of GitLab is
available for deployment on Kubernetes, OpenShift, and Kubernetes-compatible
platforms. The following deployment methods are available:
- [GitLab Helm chart](https://docs.gitlab.com/charts/): A cloud-native version of GitLab
and all of its components. Use this installation method if your infrastructure is built
on Kubernetes and you're familiar with how it works. This method of deployment has different
management, observability, and concepts than traditional deployments.
- [GitLab Operator](https://docs.gitlab.com/operator/): An installation and management method
that follows the
[Kubernetes Operator pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/).
Use the GitLab Operator to run GitLab in an
[OpenShift](../openshift_and_gitlab/index.md) or another Kubernetes-compatible platform.
<!-- This redirect file can be deleted after <2023-09-09>. -->
<!-- Redirects that point to other docs in the same project expire in three months. -->
<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->

View File

@ -49,6 +49,20 @@ sole discretion of GitLab Inc.
<div class="deprecation removal-160 breaking-change">
### Container Scanning variables that reference Docker
Planned removal: GitLab <span class="removal-milestone">16.0</span> (2023-05-22)
WARNING:
This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
Review the details carefully before upgrading.
All Container Scanning variables that are prefixed by `DOCKER_` in variable name are deprecated. This includes the `DOCKER_IMAGE`, `DOCKER_PASSWORD`, `DOCKER_USER`, and `DOCKERFILE_PATH` variables. Support for these variables will be removed in the GitLab 16.0 release. Use the [new variable names](https://docs.gitlab.com/ee/user/application_security/container_scanning/#available-cicd-variables) `CS_IMAGE`, `CS_REGISTRY_PASSWORD`, `CS_REGISTRY_USER`, and `CS_DOCKERFILE_PATH` in place of the deprecated names.
</div>
<div class="deprecation removal-160 breaking-change">
### Non-expiring access tokens
Planned removal: GitLab <span class="removal-milestone">16.0</span> (2023-05-22)

View File

@ -205,7 +205,7 @@ To set your current status:
1. Select a value from the **Clear status after** dropdown list.
1. Select **Set status**. Alternatively, you can select **Remove status** to remove your user status entirely.
You can also set your current status by [using the API](../../api/users.md#user-status).
You can also set your current status from [your user settings](#access-your-user-settings) or by [using the API](../../api/users.md#user-status).
If you select the **Busy** checkbox, remember to clear it when you become available again.

View File

@ -93,7 +93,7 @@ module API
params[:active] = !params.delete(:paused) if params.include?(:paused)
update_service = ::Ci::Runners::UpdateRunnerService.new(runner)
if update_service.update(declared_params(include_missing: false))
if update_service.execute(declared_params(include_missing: false)).success?
present runner, with: Entities::Ci::RunnerDetails, current_user: current_user
else
render_validation_error!(runner)

View File

@ -279,7 +279,7 @@ semgrep-sast:
image:
name: "$SAST_ANALYZER_IMAGE"
variables:
SERACH_MAX_DEPTH: 20
SEARCH_MAX_DEPTH: 20
SAST_ANALYZER_IMAGE_TAG: 3
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/semgrep:$SAST_ANALYZER_IMAGE_TAG$SAST_IMAGE_SUFFIX"
rules:

View File

@ -0,0 +1,54 @@
# frozen_string_literal: true
module Gitlab
module GithubImport
module Importer
module Events
class ChangedReviewer < BaseImporter
def execute(issue_event)
requested_reviewer_id = author_id(issue_event, author_key: :requested_reviewer)
review_requester_id = author_id(issue_event, author_key: :review_requester)
note_body = parse_body(issue_event, requested_reviewer_id)
create_note(issue_event, note_body, review_requester_id)
end
private
def create_note(issue_event, note_body, review_requester_id)
Note.create!(
system: true,
noteable_type: issuable_type(issue_event),
noteable_id: issuable_db_id(issue_event),
project: project,
author_id: review_requester_id,
note: note_body,
system_note_metadata: SystemNoteMetadata.new(
{
action: 'reviewer',
created_at: issue_event.created_at,
updated_at: issue_event.created_at
}
),
created_at: issue_event.created_at,
updated_at: issue_event.created_at
)
end
def parse_body(issue_event, requested_reviewer_id)
requested_reviewer = User.find(requested_reviewer_id).to_reference
if issue_event.event == 'review_request_removed'
"#{SystemNotes::IssuablesService.issuable_events[:review_request_removed]}" \
" #{requested_reviewer}"
else
"#{SystemNotes::IssuablesService.issuable_events[:review_requested]}" \
" #{requested_reviewer}"
end
end
end
end
end
end
end

View File

@ -45,6 +45,8 @@ module Gitlab
Gitlab::GithubImport::Importer::Events::CrossReferenced
when 'assigned', 'unassigned'
Gitlab::GithubImport::Importer::Events::ChangedAssignee
when 'review_requested', 'review_request_removed'
Gitlab::GithubImport::Importer::Events::ChangedReviewer
end
end
end

View File

@ -10,7 +10,8 @@ module Gitlab
attr_reader :attributes
expose_attribute :id, :actor, :event, :commit_id, :label_title, :old_title, :new_title,
:milestone_title, :issue, :source, :assignee, :created_at
:milestone_title, :issue, :source, :assignee, :review_requester,
:requested_reviewer, :created_at
# attributes - A Hash containing the event details. The keys of this
# Hash (and any nested hashes) must be symbols.
@ -47,6 +48,8 @@ module Gitlab
issue: event.issue&.to_h&.symbolize_keys,
source: event.source,
assignee: user_representation(event.assignee),
requested_reviewer: user_representation(event.requested_reviewer),
review_requester: user_representation(event.review_requester),
created_at: event.created_at
)
end
@ -56,6 +59,8 @@ module Gitlab
hash = Representation.symbolize_hash(raw_hash)
hash[:actor] = user_representation(hash[:actor], source: :hash)
hash[:assignee] = user_representation(hash[:assignee], source: :hash)
hash[:requested_reviewer] = user_representation(hash[:requested_reviewer], source: :hash)
hash[:review_requester] = user_representation(hash[:review_requester], source: :hash)
new(hash)
end

View File

@ -45,6 +45,10 @@ module Gitlab
object&.actor
when :assignee
object&.assignee
when :requested_reviewer
object&.requested_reviewer
when :review_requester
object&.review_requester
else
object&.author
end

View File

@ -6,6 +6,19 @@ unless Rails.env.production?
RuboCop::RakeTask.new
namespace :rubocop do
namespace :check do
desc 'Run RuboCop check gracefully'
task :graceful do |_task, args|
require_relative '../../rubocop/check_graceful_task'
# Don't reveal TODOs in this run.
ENV.delete('REVEAL_RUBOCOP_TODO')
result = RuboCop::CheckGracefulTask.new($stdout).run(args.extras)
exit result if result.nonzero?
end
end
namespace :todo do
desc 'Generate RuboCop todos'
task :generate do |_task, args|

View File

@ -2766,9 +2766,15 @@ msgstr ""
msgid "AdminSettings|Git abuse rate limit"
msgstr ""
msgid "AdminSettings|Group runners expiration"
msgstr ""
msgid "AdminSettings|I have read and agree to the Let's Encrypt %{link_start}Terms of Service%{link_end} (PDF)."
msgstr ""
msgid "AdminSettings|If no unit is written, it defaults to seconds. For example, these are all equivalent: %{oneDayInSeconds}, %{oneDayInHoursHumanReadable}, or %{oneDayHumanReadable}. Minimum value is two hours. %{linkStart}Learn more.%{linkEnd}"
msgstr ""
msgid "AdminSettings|If not specified at the group or instance level, the default is %{default_initial_branch_name}. Does not affect existing repositories."
msgstr ""
@ -2781,6 +2787,9 @@ msgstr ""
msgid "AdminSettings|Inactive project deletion"
msgstr ""
msgid "AdminSettings|Instance runners expiration"
msgstr ""
msgid "AdminSettings|Keep the latest artifacts for all jobs in the latest successful pipelines"
msgstr ""
@ -2841,6 +2850,9 @@ msgstr ""
msgid "AdminSettings|Project export"
msgstr ""
msgid "AdminSettings|Project runners expiration"
msgstr ""
msgid "AdminSettings|Protect CI/CD variables by default"
msgstr ""
@ -2892,6 +2904,15 @@ msgstr ""
msgid "AdminSettings|Set limit to 0 to disable it."
msgstr ""
msgid "AdminSettings|Set the expiration time of authentication tokens of newly registered group runners."
msgstr ""
msgid "AdminSettings|Set the expiration time of authentication tokens of newly registered instance runners. Authentication tokens are automatically reset at these intervals."
msgstr ""
msgid "AdminSettings|Set the expiration time of authentication tokens of newly registered project runners."
msgstr ""
msgid "AdminSettings|Set the initial name and protections for the default branch of new repositories created in the instance."
msgstr ""
@ -16080,9 +16101,6 @@ msgstr ""
msgid "Failed to load deploy keys."
msgstr ""
msgid "Failed to load emoji list."
msgstr ""
msgid "Failed to load error details from Sentry."
msgstr ""
@ -21928,6 +21946,12 @@ msgstr ""
msgid "Is using seat"
msgstr ""
msgid "IssuableEvents|removed review request for"
msgstr ""
msgid "IssuableEvents|requested review from"
msgstr ""
msgid "IssuableStatus|%{wi_type} created %{created_at} by "
msgstr ""
@ -30166,15 +30190,9 @@ msgstr ""
msgid "Profiles|Add key"
msgstr ""
msgid "Profiles|Add status emoji"
msgstr ""
msgid "Profiles|An error occurred while updating your username, please try again."
msgstr ""
msgid "Profiles|An indicator appears next to your name and avatar."
msgstr ""
msgid "Profiles|Avatar cropper"
msgstr ""
@ -30187,9 +30205,6 @@ msgstr ""
msgid "Profiles|Bio"
msgstr ""
msgid "Profiles|Busy"
msgstr ""
msgid "Profiles|Change username"
msgstr ""
@ -30205,9 +30220,6 @@ msgstr ""
msgid "Profiles|City, country"
msgstr ""
msgid "Profiles|Clear status"
msgstr ""
msgid "Profiles|Commit email"
msgstr ""
@ -30457,9 +30469,6 @@ msgstr ""
msgid "Profiles|Website url"
msgstr ""
msgid "Profiles|What's your status?"
msgstr ""
msgid "Profiles|Who you represent or work for."
msgstr ""
@ -30505,9 +30514,6 @@ msgstr ""
msgid "Profiles|Your name was automatically set based on your %{provider_label} account, so people you know can recognize you."
msgstr ""
msgid "Profiles|Your status"
msgstr ""
msgid "Profiles|https://website.com"
msgstr ""
@ -34124,6 +34130,9 @@ msgstr ""
msgid "Runners|Never contacted:"
msgstr ""
msgid "Runners|Never expires"
msgstr ""
msgid "Runners|New group runners view"
msgstr ""
@ -34219,6 +34228,12 @@ msgstr ""
msgid "Runners|Runner assigned to project."
msgstr ""
msgid "Runners|Runner authentication token expiration"
msgstr ""
msgid "Runners|Runner authentication tokens will expire based on a set interval. They will automatically rotate once expired."
msgstr ""
msgid "Runners|Runner cannot be deleted, please contact your administrator"
msgstr ""
@ -34350,6 +34365,9 @@ msgstr ""
msgid "Runners|To register them, go to the %{link_start}group's Runners page%{link_end}."
msgstr ""
msgid "Runners|Token expiry"
msgstr ""
msgid "Runners|Up to date"
msgstr ""

View File

@ -10,8 +10,8 @@ module Gitlab
text_field :activation_code
button :activate
label :terms_of_services, text: /I agree that/
link :remove_license, 'data-testid': 'license-remove-action'
button :confirm_ok_button
button :remove_license
button :confirm_remove_license
p :plan
p :started
p :name
@ -30,6 +30,11 @@ module Gitlab
terms_of_services_element.click # workaround for hidden checkbox
end
def remove_license_file
remove_license
confirm_remove_license
end
# Checks if a subscription record exists in subscription history table
#
# @param plan [Hash] Name of the plan

View File

@ -0,0 +1,83 @@
# frozen_string_literal: true
require_relative 'formatter/graceful_formatter'
require_relative '../lib/gitlab/popen'
module RuboCop
class CheckGracefulTask
def initialize(output)
@output = output
end
def run(args)
options = %w[
--parallel
--format RuboCop::Formatter::GracefulFormatter
]
available_cops = RuboCop::Cop::Registry.global.to_h
cop_names, paths = args.partition { available_cops.key?(_1) }
if cop_names.any?
list = cop_names.sort.join(',')
options.concat ['--only', list]
end
options.concat(paths)
puts <<~MSG
Running RuboCop in graceful mode:
rubocop #{options.join(' ')}
This might take a while...
MSG
status_orig = RuboCop::CLI.new.run(options)
status = RuboCop::Formatter::GracefulFormatter.adjusted_exit_status(status_orig)
# We had to adjust the status which means we have silenced offenses. Notify Slack!
notify_slack unless status_orig == status
status
end
private
def env_values(*keys)
env = ENV.slice(*keys)
missing_keys = keys - env.keys
if missing_keys.any?
puts "Missing ENV keys: #{missing_keys.join(', ')}"
return
end
env.values
end
def notify_slack
job_name, job_url, _ = env_values('CI_JOB_NAME', 'CI_JOB_URL', 'CI_SLACK_WEBHOOK_URL')
unless job_name
puts 'Skipping Slack notification.'
return
end
channel = 'f_rubocop'
message = ":warning: `#{job_name}` passed :green: but contained silenced offenses. See #{job_url}"
emoji = 'rubocop'
user_name = 'GitLab Bot'
puts "Notifying Slack ##{channel}."
_output, result = Gitlab::Popen.popen(['scripts/slack', channel, message, emoji, user_name])
puts "Failed to notify Slack channel ##{channel}." if result.nonzero?
end
def puts(...)
@output.puts(...)
end
end
end

View File

@ -1,8 +1,10 @@
# frozen_string_literal: true
require_relative 'formatter/graceful_formatter'
module RuboCop
class CopTodo
attr_accessor :previously_disabled
attr_accessor :previously_disabled, :grace_period
attr_reader :cop_name, :files, :offense_count
@ -12,6 +14,7 @@ module RuboCop
@offense_count = 0
@cop_class = self.class.find_cop_by_name(cop_name)
@previously_disabled = false
@grace_period = false
end
def record(file, offense_count)
@ -35,6 +38,7 @@ module RuboCop
yaml << ' Enabled: false'
end
yaml << " #{RuboCop::Formatter::GracefulFormatter.grace_period_key_value}" if grace_period
yaml << ' Exclude:'
yaml.concat files.sort.map { |file| " - '#{file}'" }
yaml << ''

View File

@ -0,0 +1,109 @@
# frozen_string_literal: true
require 'rubocop'
module RuboCop
module Formatter
class GracefulFormatter < ::RuboCop::Formatter::ProgressFormatter
CONFIG_DETAILS_KEY = 'Details'
CONFIG_DETAILS_VALUE = 'grace period'
class << self
attr_accessor :active_offenses
end
def started(...)
super
self.class.active_offenses = 0
@silenced_offenses_for_files = {}
@config = RuboCop::ConfigStore.new.for_pwd
end
def file_finished(file, offenses)
silenced_offenses, active_offenses = offenses.partition { silenced?(_1) }
@silenced_offenses_for_files[file] = silenced_offenses if silenced_offenses.any?
super(file, active_offenses)
end
def finished(inspected_files)
# See the note below why are using this ivar in the first place.
unless defined?(@total_offense_count)
raise <<~MESSAGE
RuboCop has changed its internals and the instance variable
`@total_offense_count` is no longer defined but we were relying on it.
Please change the implementation.
See https://github.com/rubocop/rubocop/blob/65a757b0f/lib/rubocop/formatter/simple_text_formatter.rb#L24
MESSAGE
end
super
# Internally, RuboCop has no notion of "silenced offenses". We cannot
# override this meaning in a formatter that's why we track what we
# consider to be an active offense.
# This is needed for `adjusted_exit_status` method below.
self.class.active_offenses = @total_offense_count
report_silenced_offenses(inspected_files)
end
# We consider this run a success without any active offenses.
def self.adjusted_exit_status(status)
return status unless status == RuboCop::CLI::STATUS_OFFENSES
return RuboCop::CLI::STATUS_SUCCESS if active_offenses == 0
status
end
def self.grace_period?(cop_name, config)
details = config[CONFIG_DETAILS_KEY]
return false unless details
return true if details == CONFIG_DETAILS_VALUE
warn "#{cop_name}: Unhandled value #{details.inspect} for `Details` key."
false
end
def self.grace_period_key_value
"#{CONFIG_DETAILS_KEY}: #{CONFIG_DETAILS_VALUE}"
end
private
def silenced?(offense)
cop_config = @config.for_cop(offense.cop_name)
self.class.grace_period?(offense.cop_name, cop_config)
end
def report_silenced_offenses(inspected_files)
return if @silenced_offenses_for_files.empty?
output.puts
output.puts 'Silenced offenses:'
output.puts
@silenced_offenses_for_files.each do |file, offenses|
report_file(file, offenses)
end
silenced_offense_count = @silenced_offenses_for_files.values.sum(&:size)
silenced_text = colorize("#{silenced_offense_count} offenses", :yellow)
output.puts
output.puts "#{inspected_files.size} files inspected, #{silenced_text} silenced"
end
def report_file_as_mark(_offenses)
# Skip progress bar. No dots. No C/Ws.
end
end
end
end

View File

@ -6,6 +6,7 @@ require 'yaml'
require_relative '../todo_dir'
require_relative '../cop_todo'
require_relative '../formatter/graceful_formatter'
module RuboCop
module Formatter
@ -47,6 +48,8 @@ module RuboCop
def finished(_inspected_files)
@todos.values.sort_by(&:cop_name).each do |todo|
todo.previously_disabled = previously_disabled?(todo)
todo.grace_period = grace_period?(todo)
validate_todo!(todo)
path = @todo_dir.write(todo.cop_name, todo.to_yaml)
output.puts "Written to #{relative_path(path)}\n"
@ -79,16 +82,31 @@ module RuboCop
raise "Multiple configurations found for cops:\n#{list}\n"
end
def previously_disabled?(todo)
def config_for(todo)
cop_name = todo.cop_name
config = @config_old_todo_yml[cop_name] ||
@config_inspect_todo_dir[cop_name] || {}
@config_old_todo_yml[cop_name] || @config_inspect_todo_dir[cop_name] || {}
end
def previously_disabled?(todo)
config = config_for(todo)
return false if config.empty?
config['Enabled'] == false
end
def grace_period?(todo)
config = config_for(todo)
GracefulFormatter.grace_period?(todo.cop_name, config)
end
def validate_todo!(todo)
return unless todo.previously_disabled && todo.grace_period
raise "#{todo.cop_name}: Cop must be enabled to use `#{GracefulFormatter.grace_period_key_value}`."
end
def load_config_inspect_todo_dir
@todo_dir.list_inspect.each_with_object({}) do |path, combined|
config = YAML.load_file(path)

View File

@ -74,7 +74,7 @@ RSpec.describe Admin::RunnersController do
context 'with update succeeding' do
before do
expect_next_instance_of(Ci::Runners::UpdateRunnerService, runner) do |service|
expect(service).to receive(:update).with(anything).and_call_original
expect(service).to receive(:execute).with(anything).and_call_original
end
end
@ -91,7 +91,7 @@ RSpec.describe Admin::RunnersController do
context 'with update failing' do
before do
expect_next_instance_of(Ci::Runners::UpdateRunnerService, runner) do |service|
expect(service).to receive(:update).with(anything).and_return(false)
expect(service).to receive(:execute).with(anything).and_return(ServiceResponse.error(message: 'failure'))
end
end

View File

@ -82,13 +82,17 @@ RSpec.describe ProfilesController, :request_store do
expect(ldap_user.location).to eq('City, Country')
end
it 'allows setting a user status' do
it 'allows setting a user status', :freeze_time do
sign_in(user)
put :update, params: { user: { status: { message: 'Working hard!', availability: 'busy' } } }
put(
:update,
params: { user: { status: { message: 'Working hard!', availability: 'busy', clear_status_after: '8_hours' } } }
)
expect(user.reload.status.message).to eq('Working hard!')
expect(user.reload.status.availability).to eq('busy')
expect(user.reload.status.clear_status_after).to eq(8.hours.from_now)
expect(response).to have_gitlab_http_status(:found)
end

View File

@ -180,7 +180,7 @@ RSpec.describe 'User edit profile' do
end
it 'adds emoji to user status' do
emoji = 'biohazard'
emoji = 'basketball'
select_emoji(emoji)
submit_settings
@ -193,7 +193,7 @@ RSpec.describe 'User edit profile' do
it 'adds message to user status' do
message = 'I have something to say'
fill_in 'js-status-message-field', with: message
fill_in s_("SetStatusModal|What's your status?"), with: message
submit_settings
visit_user
@ -208,7 +208,7 @@ RSpec.describe 'User edit profile' do
emoji = '8ball'
message = 'Playing outside'
select_emoji(emoji)
fill_in 'js-status-message-field', with: message
fill_in s_("SetStatusModal|What's your status?"), with: message
submit_settings
visit_user
@ -230,7 +230,7 @@ RSpec.describe 'User edit profile' do
end
visit(profile_path)
click_button 'js-clear-user-status-button'
click_button s_('SetStatusModal|Clear status')
submit_settings
visit_user
@ -240,9 +240,9 @@ RSpec.describe 'User edit profile' do
it 'displays a default emoji if only message is entered' do
message = 'a status without emoji'
fill_in 'js-status-message-field', with: message
fill_in s_("SetStatusModal|What's your status?"), with: message
within('.js-toggle-emoji-menu') do
within('.emoji-menu-toggle-button') do
expect(page).to have_emoji('speech_balloon')
end
end
@ -406,7 +406,7 @@ RSpec.describe 'User edit profile' do
it 'adds message to user status' do
message = 'I have something to say'
open_user_status_modal
find('.js-status-message-field').native.send_keys(message)
find_field(s_("SetStatusModal|What's your status?")).native.send_keys(message)
set_user_status_in_modal
visit_user
@ -422,7 +422,7 @@ RSpec.describe 'User edit profile' do
message = 'Playing outside'
open_user_status_modal
select_emoji(emoji, true)
find('.js-status-message-field').native.send_keys(message)
find_field(s_("SetStatusModal|What's your status?")).native.send_keys(message)
set_user_status_in_modal
visit_user
@ -446,7 +446,7 @@ RSpec.describe 'User edit profile' do
open_edit_status_modal
find('.js-clear-user-status-button').click
click_button s_('SetStatusModal|Clear status')
set_user_status_in_modal
visit_user
@ -491,7 +491,7 @@ RSpec.describe 'User edit profile' do
it 'displays a default emoji if only message is entered' do
message = 'a status without emoji'
open_user_status_modal
find('.js-status-message-field').native.send_keys(message)
find_field(s_("SetStatusModal|What's your status?")).native.send_keys(message)
expect(page).to have_emoji('speech_balloon')
end

View File

@ -19,10 +19,13 @@ import { createWrapper, ErrorWrapper } from '@vue/test-utils';
* @returns Wrapper
*/
export const findDd = (dtLabel, wrapper) => {
const dt = wrapper.findByText(dtLabel).element;
const dd = dt.nextElementSibling;
if (dt.tagName === 'DT' && dd.tagName === 'DD') {
return createWrapper(dd, {});
const dtw = wrapper.findByText(dtLabel);
if (dtw.exists()) {
const dt = dtw.element;
const dd = dt.nextElementSibling;
if (dt.tagName === 'DT' && dd.tagName === 'DD') {
return createWrapper(dd, {});
}
}
return ErrorWrapper(dtLabel);
return new ErrorWrapper(dtLabel);
};

View File

@ -30,19 +30,19 @@ describe('Emoji category component', () => {
// eslint-disable-next-line no-restricted-syntax
await wrapper.setData({ renderGroup: true });
expect(wrapper.find(EmojiGroup).attributes('rendergroup')).toBe('true');
expect(wrapper.findComponent(EmojiGroup).attributes('rendergroup')).toBe('true');
});
it('renders group on appear', async () => {
wrapper.find(GlIntersectionObserver).vm.$emit('appear');
wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
await nextTick();
expect(wrapper.find(EmojiGroup).attributes('rendergroup')).toBe('true');
expect(wrapper.findComponent(EmojiGroup).attributes('rendergroup')).toBe('true');
});
it('emits appear event on appear', async () => {
wrapper.find(GlIntersectionObserver).vm.$emit('appear');
wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
await nextTick();

View File

@ -76,8 +76,8 @@ describe('error tracking settings app', () => {
describe('section', () => {
it('renders the form and dropdown', () => {
expect(wrapper.find(ErrorTrackingForm).exists()).toBe(true);
expect(wrapper.find(ProjectDropdown).exists()).toBe(true);
expect(wrapper.findComponent(ErrorTrackingForm).exists()).toBe(true);
expect(wrapper.findComponent(ProjectDropdown).exists()).toBe(true);
});
it('renders the Save Changes button', () => {

View File

@ -42,7 +42,7 @@ describe('error tracking settings project dropdown', () => {
describe('empty project list', () => {
it('renders the dropdown', () => {
expect(wrapper.find('#project-dropdown').exists()).toBe(true);
expect(wrapper.find(GlDropdown).exists()).toBe(true);
expect(wrapper.findComponent(GlDropdown).exists()).toBe(true);
});
it('shows helper text', () => {
@ -57,8 +57,8 @@ describe('error tracking settings project dropdown', () => {
});
it('does not contain any dropdown items', () => {
expect(wrapper.find(GlDropdownItem).exists()).toBe(false);
expect(wrapper.find(GlDropdown).props('text')).toBe('No projects available');
expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(false);
expect(wrapper.findComponent(GlDropdown).props('text')).toBe('No projects available');
});
});
@ -71,11 +71,11 @@ describe('error tracking settings project dropdown', () => {
it('renders the dropdown', () => {
expect(wrapper.find('#project-dropdown').exists()).toBe(true);
expect(wrapper.find(GlDropdown).exists()).toBe(true);
expect(wrapper.findComponent(GlDropdown).exists()).toBe(true);
});
it('contains a number of dropdown items', () => {
expect(wrapper.find(GlDropdownItem).exists()).toBe(true);
expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(true);
expect(wrapper.findAll(GlDropdownItem).length).toBe(2);
});
});

View File

@ -6,9 +6,9 @@ import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered
describe('Recent Searches Dropdown Content', () => {
let wrapper;
const findLocalStorageNote = () => wrapper.find({ ref: 'localStorageNote' });
const findLocalStorageNote = () => wrapper.findComponent({ ref: 'localStorageNote' });
const findDropdownItems = () => wrapper.findAll({ ref: 'dropdownItem' });
const findDropdownNote = () => wrapper.find({ ref: 'dropdownNote' });
const findDropdownNote = () => wrapper.findComponent({ ref: 'dropdownNote' });
const createComponent = (props) => {
wrapper = shallowMount(RecentSearchesDropdownContent, {
@ -94,7 +94,7 @@ describe('Recent Searches Dropdown Content', () => {
});
it('emits requestClearRecentSearches on Clear resent searches button', () => {
wrapper.find({ ref: 'clearButton' }).trigger('click');
wrapper.findComponent({ ref: 'clearButton' }).trigger('click');
expect(onRequestClearRecentSearchesSpy).toHaveBeenCalled();
});

View File

@ -16,10 +16,10 @@ describe('FrequentItemsListItemComponent', () => {
let trackingSpy;
let store;
const findTitle = () => wrapper.find({ ref: 'frequentItemsItemTitle' });
const findTitle = () => wrapper.findComponent({ ref: 'frequentItemsItemTitle' });
const findAvatar = () => wrapper.findComponent(ProjectAvatar);
const findAllTitles = () => wrapper.findAll({ ref: 'frequentItemsItemTitle' });
const findNamespace = () => wrapper.find({ ref: 'frequentItemsItemNamespace' });
const findNamespace = () => wrapper.findComponent({ ref: 'frequentItemsItemNamespace' });
const findAllButtons = () => wrapper.findAllComponents(GlButton);
const findAllNamespace = () => wrapper.findAll({ ref: 'frequentItemsItemNamespace' });
const findAllAvatars = () => wrapper.findAllComponents(ProjectAvatar);

View File

@ -23,7 +23,7 @@ describe('FrequentItemsSearchInputComponent', () => {
},
});
const findSearchBoxByType = () => wrapper.find(GlSearchBoxByType);
const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType);
beforeEach(() => {
store = createStore();

View File

@ -42,7 +42,7 @@ describe('Import entities group dropdown component', () => {
createComponent({ namespaces });
namespacesTracker.mockReset();
wrapper.find(GlSearchBoxByType).vm.$emit('input', 'match');
wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'match');
await nextTick();

View File

@ -99,7 +99,7 @@ describe('import table', () => {
});
await waitForPromises();
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('does not renders loading icon when request is completed', async () => {
@ -108,7 +108,7 @@ describe('import table', () => {
});
await waitForPromises();
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
});
});
@ -123,7 +123,7 @@ describe('import table', () => {
});
await waitForPromises();
expect(wrapper.find(GlEmptyState).props().title).toBe(i18n.NO_GROUPS_FOUND);
expect(wrapper.findComponent(GlEmptyState).props().title).toBe(i18n.NO_GROUPS_FOUND);
});
});
@ -268,7 +268,7 @@ describe('import table', () => {
});
it('correctly passes pagination info from query', () => {
expect(wrapper.find(PaginationLinks).props().pageInfo).toStrictEqual(FAKE_PAGE_INFO);
expect(wrapper.findComponent(PaginationLinks).props().pageInfo).toStrictEqual(FAKE_PAGE_INFO);
});
it('renders pagination dropdown', () => {
@ -293,7 +293,7 @@ describe('import table', () => {
it('updates page when page change is requested', async () => {
const REQUESTED_PAGE = 2;
wrapper.find(PaginationLinks).props().change(REQUESTED_PAGE);
wrapper.findComponent(PaginationLinks).props().change(REQUESTED_PAGE);
await waitForPromises();
expect(bulkImportSourceGroupsQueryMock).toHaveBeenCalledWith(
@ -316,7 +316,7 @@ describe('import table', () => {
},
versionValidation: FAKE_VERSION_VALIDATION,
});
wrapper.find(PaginationLinks).props().change(REQUESTED_PAGE);
wrapper.findComponent(PaginationLinks).props().change(REQUESTED_PAGE);
await waitForPromises();
expect(wrapper.text()).toContain('Showing 21-21 of 38 groups that you own from');
@ -539,8 +539,8 @@ describe('import table', () => {
});
await waitForPromises();
expect(wrapper.find(GlAlert).exists()).toBe(true);
expect(wrapper.find(GlAlert).text()).toContain('projects (require v14.8.0)');
expect(wrapper.findComponent(GlAlert).exists()).toBe(true);
expect(wrapper.findComponent(GlAlert).text()).toContain('projects (require v14.8.0)');
});
it('does not renders alert when there are no unavailable features', async () => {
@ -558,7 +558,7 @@ describe('import table', () => {
});
await waitForPromises();
expect(wrapper.find(GlAlert).exists()).toBe(false);
expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
});
});
});

View File

@ -22,8 +22,8 @@ describe('import target cell', () => {
let wrapper;
let group;
const findNameInput = () => wrapper.find(GlFormInput);
const findNamespaceDropdown = () => wrapper.find(ImportGroupDropdown);
const findNameInput = () => wrapper.findComponent(GlFormInput);
const findNamespaceDropdown = () => wrapper.findComponent(ImportGroupDropdown);
const createComponent = (props) => {
wrapper = shallowMount(ImportTargetCell, {

View File

@ -33,12 +33,12 @@ describe('BitbucketStatusTable', () => {
it('renders import table component', () => {
createComponent({ providerTitle: 'Test' });
expect(wrapper.find(ImportProjectsTable).exists()).toBe(true);
expect(wrapper.findComponent(ImportProjectsTable).exists()).toBe(true);
});
it('passes alert in incompatible-repos-warning slot', () => {
createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub);
expect(wrapper.find(GlAlert).exists()).toBe(true);
expect(wrapper.findComponent(GlAlert).exists()).toBe(true);
});
it('passes actions slot to import project table component', () => {
@ -46,14 +46,14 @@ describe('BitbucketStatusTable', () => {
createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub, {
actions: actionsSlotContent,
});
expect(wrapper.find(ImportProjectsTable).text()).toBe(actionsSlotContent);
expect(wrapper.findComponent(ImportProjectsTable).text()).toBe(actionsSlotContent);
});
it('dismisses alert when requested', async () => {
createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub);
wrapper.find(GlAlert).vm.$emit('dismiss');
wrapper.findComponent(GlAlert).vm.$emit('dismiss');
await nextTick();
expect(wrapper.find(GlAlert).exists()).toBe(false);
expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
});
});

View File

@ -33,7 +33,7 @@ describe('ImportProjectsTable', () => {
.findAll(GlButton)
.filter((w) => w.props().variant === 'confirm')
.at(0);
const findImportAllModal = () => wrapper.find({ ref: 'importAllModal' });
const findImportAllModal = () => wrapper.findComponent({ ref: 'importAllModal' });
const importAllFn = jest.fn();
const importAllModalShowFn = jest.fn();
@ -89,13 +89,13 @@ describe('ImportProjectsTable', () => {
it('renders a loading icon while repos are loading', () => {
createComponent({ state: { isLoadingRepos: true } });
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('renders a loading icon while namespaces are loading', () => {
createComponent({ state: { isLoadingNamespaces: true } });
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('renders a table with provider repos', () => {
@ -109,7 +109,7 @@ describe('ImportProjectsTable', () => {
state: { namespaces: [{ fullPath: 'path' }], repositories },
});
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find('table').exists()).toBe(true);
expect(
wrapper
@ -170,7 +170,7 @@ describe('ImportProjectsTable', () => {
it('renders an empty state if there are no repositories available', () => {
createComponent({ state: { repositories: [] } });
expect(wrapper.find(ProviderRepoTableRow).exists()).toBe(false);
expect(wrapper.findComponent(ProviderRepoTableRow).exists()).toBe(false);
expect(wrapper.text()).toContain(`No ${providerTitle} repositories found`);
});
@ -231,11 +231,11 @@ describe('ImportProjectsTable', () => {
});
it('renders intersection observer component', () => {
expect(wrapper.find(GlIntersectionObserver).exists()).toBe(true);
expect(wrapper.findComponent(GlIntersectionObserver).exists()).toBe(true);
});
it('calls fetchRepos when intersection observer appears', async () => {
wrapper.find(GlIntersectionObserver).vm.$emit('appear');
wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
await nextTick();

View File

@ -74,11 +74,13 @@ describe('ProviderRepoTableRow', () => {
});
it('renders empty import status', () => {
expect(wrapper.find(ImportStatus).props().status).toBe(STATUSES.NONE);
expect(wrapper.findComponent(ImportStatus).props().status).toBe(STATUSES.NONE);
});
it('renders a group namespace select', () => {
expect(wrapper.find(ImportGroupDropdown).props().namespaces).toBe(availableNamespaces);
expect(wrapper.findComponent(ImportGroupDropdown).props().namespaces).toBe(
availableNamespaces,
);
});
it('renders import button', () => {
@ -127,11 +129,13 @@ describe('ProviderRepoTableRow', () => {
});
it('renders proper import status', () => {
expect(wrapper.find(ImportStatus).props().status).toBe(repo.importedProject.importStatus);
expect(wrapper.findComponent(ImportStatus).props().status).toBe(
repo.importedProject.importStatus,
);
});
it('does not renders a namespace select', () => {
expect(wrapper.find(GlDropdown).exists()).toBe(false);
expect(wrapper.findComponent(GlDropdown).exists()).toBe(false);
});
it('does not render import button', () => {
@ -139,7 +143,7 @@ describe('ProviderRepoTableRow', () => {
});
it('passes stats to import status component', () => {
expect(wrapper.find(ImportStatus).props().stats).toBe(FAKE_STATS);
expect(wrapper.findComponent(ImportStatus).props().stats).toBe(FAKE_STATS);
});
});
@ -165,7 +169,7 @@ describe('ProviderRepoTableRow', () => {
});
it('renders badge with error', () => {
expect(wrapper.find(GlBadge).text()).toBe('Incompatible project');
expect(wrapper.findComponent(GlBadge).text()).toBe('Incompatible project');
});
});
});

View File

@ -40,15 +40,15 @@ describe('Incidents List', () => {
all: 26,
};
const findTable = () => wrapper.find(GlTable);
const findTable = () => wrapper.findComponent(GlTable);
const findTableRows = () => wrapper.findAll('table tbody tr');
const findAlert = () => wrapper.find(GlAlert);
const findLoader = () => wrapper.find(GlLoadingIcon);
const findAlert = () => wrapper.findComponent(GlAlert);
const findLoader = () => wrapper.findComponent(GlLoadingIcon);
const findTimeAgo = () => wrapper.findAll(TimeAgoTooltip);
const findAssignees = () => wrapper.findAll('[data-testid="incident-assignees"]');
const findCreateIncidentBtn = () => wrapper.find('[data-testid="createIncidentBtn"]');
const findClosedIcon = () => wrapper.findAll("[data-testid='incident-closed']");
const findEmptyState = () => wrapper.find(GlEmptyState);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findSeverity = () => wrapper.findAll(SeverityToken);
const findEscalationStatus = () => wrapper.findAll('[data-testid="incident-escalation-status"]');
const findIncidentLink = () => wrapper.findByTestId('incident-link');
@ -179,7 +179,7 @@ describe('Incidents List', () => {
});
it('renders an avatar component when there is an assignee', () => {
const avatar = findAssignees().at(1).find(GlAvatar);
const avatar = findAssignees().at(1).findComponent(GlAvatar);
const { src, label } = avatar.attributes();
const { name, avatarUrl } = mockIncidents[1].assignees.nodes[0];

View File

@ -20,8 +20,8 @@ describe('IncidentsSettingTabs', () => {
}
});
const findToggleButton = () => wrapper.find({ ref: 'toggleBtn' });
const findSectionHeader = () => wrapper.find({ ref: 'sectionHeader' });
const findToggleButton = () => wrapper.findComponent({ ref: 'toggleBtn' });
const findSectionHeader = () => wrapper.findComponent({ ref: 'sectionHeader' });
const findIntegrationTabs = () => wrapper.findAll(GlTab);
it('renders header text', () => {

View File

@ -86,7 +86,7 @@ describe('TriggerFields', () => {
expect(checkboxes).toHaveLength(2);
checkboxes.wrappers.forEach((checkbox, index) => {
const checkBox = checkbox.find(GlFormCheckbox);
const checkBox = checkbox.findComponent(GlFormCheckbox);
expect(checkbox.find('label').text()).toBe(expectedResults[index].labelText);
expect(checkbox.find('[type=hidden]').attributes('name')).toBe(

View File

@ -53,7 +53,7 @@ afterEach(() => {
describe('ImportProjectMembersModal', () => {
const findGlModal = () => wrapper.findComponent(GlModal);
const findIntroText = () => wrapper.find({ ref: 'modalIntro' }).text();
const findIntroText = () => wrapper.findComponent({ ref: 'modalIntro' }).text();
const clickImportButton = () => findGlModal().vm.$emit('primary', { preventDefault: jest.fn() });
const closeModal = () => findGlModal().vm.$emit('hidden', { preventDefault: jest.fn() });
const findFormGroup = () => wrapper.findByTestId('form-group');

View File

@ -144,7 +144,7 @@ describe('IssueMilestoneComponent', () => {
});
it('renders milestone icon', () => {
expect(wrapper.find(GlIcon).props('name')).toBe('clock');
expect(wrapper.findComponent(GlIcon).props('name')).toBe('clock');
});
it('renders milestone title', () => {

View File

@ -31,11 +31,11 @@ describe('IssueToken', () => {
}
});
const findLink = () => wrapper.find({ ref: 'link' });
const findReference = () => wrapper.find({ ref: 'reference' });
const findLink = () => wrapper.findComponent({ ref: 'link' });
const findReference = () => wrapper.findComponent({ ref: 'reference' });
const findReferenceIcon = () => wrapper.find('[data-testid="referenceIcon"]');
const findRemoveBtn = () => wrapper.find('[data-testid="removeBtn"]');
const findTitle = () => wrapper.find({ ref: 'title' });
const findTitle = () => wrapper.findComponent({ ref: 'title' });
describe('with reference supplied', () => {
beforeEach(() => {

View File

@ -153,7 +153,7 @@ describe('RelatedIssuesBlock', () => {
});
it('sets `autoCompleteEpics` to false for add-issuable-form', () => {
expect(wrapper.find(AddIssuableForm).props('autoCompleteEpics')).toBe(false);
expect(wrapper.findComponent(AddIssuableForm).props('autoCompleteEpics')).toBe(false);
});
});
@ -227,7 +227,7 @@ describe('RelatedIssuesBlock', () => {
},
});
const iconComponent = wrapper.find(GlIcon);
const iconComponent = wrapper.findComponent(GlIcon);
expect(iconComponent.exists()).toBe(true);
expect(iconComponent.props('name')).toBe(icon);
});

View File

@ -187,7 +187,9 @@ describe('RelatedIssuesList', () => {
});
it('shows due date', () => {
expect(wrapper.find(IssueDueDate).find('.board-card-info-text').text()).toBe('Nov 22, 2010');
expect(wrapper.findComponent(IssueDueDate).find('.board-card-info-text').text()).toBe(
'Nov 22, 2010',
);
});
});
});

View File

@ -21,7 +21,7 @@ describe('CE IssueCardTimeInfo component', () => {
};
const findMilestone = () => wrapper.find('[data-testid="issuable-milestone"]');
const findMilestoneTitle = () => findMilestone().find(GlLink).attributes('title');
const findMilestoneTitle = () => findMilestone().findComponent(GlLink).attributes('title');
const findDueDate = () => wrapper.find('[data-testid="issuable-due-date"]');
const mountComponent = ({
@ -56,8 +56,8 @@ describe('CE IssueCardTimeInfo component', () => {
const milestone = findMilestone();
expect(milestone.text()).toBe(issue.milestone.title);
expect(milestone.find(GlIcon).props('name')).toBe('clock');
expect(milestone.find(GlLink).attributes('href')).toBe(issue.milestone.webPath);
expect(milestone.findComponent(GlIcon).props('name')).toBe('clock');
expect(milestone.findComponent(GlLink).attributes('href')).toBe(issue.milestone.webPath);
});
describe.each`
@ -84,7 +84,7 @@ describe('CE IssueCardTimeInfo component', () => {
expect(dueDate.text()).toBe('Dec 12, 2020');
expect(dueDate.attributes('title')).toBe('Due date');
expect(dueDate.find(GlIcon).props('name')).toBe('calendar');
expect(dueDate.findComponent(GlIcon).props('name')).toBe('calendar');
expect(dueDate.classes()).not.toContain('gl-text-red-500');
});
});
@ -118,6 +118,6 @@ describe('CE IssueCardTimeInfo component', () => {
expect(timeEstimate.text()).toBe(issue.humanTimeEstimate);
expect(timeEstimate.attributes('title')).toBe('Estimate');
expect(timeEstimate.find(GlIcon).props('name')).toBe('timer');
expect(timeEstimate.findComponent(GlIcon).props('name')).toBe('timer');
});
});

View File

@ -11,9 +11,9 @@ describe('JiraIssuesImportStatus', () => {
};
let wrapper;
const findAlert = () => wrapper.find(GlAlert);
const findAlert = () => wrapper.findComponent(GlAlert);
const findAlertLabel = () => wrapper.find(GlAlert).find(GlLabel);
const findAlertLabel = () => wrapper.findComponent(GlAlert).findComponent(GlLabel);
const mountComponent = ({
shouldShowFinishedAlert = false,
@ -49,7 +49,7 @@ describe('JiraIssuesImportStatus', () => {
});
it('does not show an alert', () => {
expect(wrapper.find(GlAlert).exists()).toBe(false);
expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
});
});
@ -105,12 +105,12 @@ describe('JiraIssuesImportStatus', () => {
shouldShowInProgressAlert: true,
});
expect(wrapper.find(GlAlert).exists()).toBe(true);
expect(wrapper.findComponent(GlAlert).exists()).toBe(true);
findAlert().vm.$emit('dismiss');
await nextTick();
expect(wrapper.find(GlAlert).exists()).toBe(false);
expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
});
});
});

View File

@ -105,7 +105,7 @@ describe('Issue title suggestions item component', () => {
const count = wrapper.findAll('.suggestion-counts span').at(0);
expect(count.text()).toContain('1');
expect(count.find(GlIcon).props('name')).toBe('thumb-up');
expect(count.findComponent(GlIcon).props('name')).toBe('thumb-up');
});
it('renders notes count', () => {
@ -114,7 +114,7 @@ describe('Issue title suggestions item component', () => {
const count = wrapper.findAll('.suggestion-counts span').at(1);
expect(count.text()).toContain('2');
expect(count.find(GlIcon).props('name')).toBe('comment');
expect(count.findComponent(GlIcon).props('name')).toBe('comment');
});
});

View File

@ -461,7 +461,7 @@ describe('Issuable output', () => {
describe('when title is not in view', () => {
beforeEach(() => {
wrapper.vm.state.titleText = 'Sticky header title';
wrapper.find(GlIntersectionObserver).vm.$emit('disappear');
wrapper.findComponent(GlIntersectionObserver).vm.$emit('disappear');
});
it('shows with title', () => {

View File

@ -6,7 +6,7 @@ import MarkdownField from '~/vue_shared/components/markdown/field.vue';
describe('Description field component', () => {
let wrapper;
const findTextarea = () => wrapper.find({ ref: 'textarea' });
const findTextarea = () => wrapper.findComponent({ ref: 'textarea' });
const mountComponent = (description = 'test') =>
shallowMount(DescriptionField, {

View File

@ -5,7 +5,7 @@ import eventHub from '~/issues/show/event_hub';
describe('Title field component', () => {
let wrapper;
const findInput = () => wrapper.find({ ref: 'input' });
const findInput = () => wrapper.findComponent({ ref: 'input' });
beforeEach(() => {
jest.spyOn(eventHub, '$emit');

View File

@ -65,7 +65,7 @@ describe('HeaderActions component', () => {
},
};
const findToggleIssueStateButton = () => wrapper.find(GlButton);
const findToggleIssueStateButton = () => wrapper.findComponent(GlButton);
const findDropdownBy = (dataTestId) => wrapper.find(`[data-testid="${dataTestId}"]`);
const findMobileDropdown = () => findDropdownBy('mobile-dropdown');
@ -73,7 +73,7 @@ describe('HeaderActions component', () => {
const findMobileDropdownItems = () => findMobileDropdown().findAll(GlDropdownItem);
const findDesktopDropdownItems = () => findDesktopDropdown().findAll(GlDropdownItem);
const findModal = () => wrapper.find(GlModal);
const findModal = () => wrapper.findComponent(GlModal);
const findModalLinkAt = (index) => findModal().findAll(GlLink).at(index);

View File

@ -41,7 +41,7 @@ describe('Highlight Bar', () => {
}
});
const findLink = () => wrapper.find(GlLink);
const findLink = () => wrapper.findComponent(GlLink);
describe('empty state', () => {
beforeEach(() => {

View File

@ -64,9 +64,9 @@ describe('Incident Tabs component', () => {
const findTabs = () => wrapper.findAll(GlTab);
const findSummaryTab = () => findTabs().at(0);
const findAlertDetailsTab = () => wrapper.find('[data-testid="alert-details-tab"]');
const findAlertDetailsComponent = () => wrapper.find(AlertDetailsTable);
const findDescriptionComponent = () => wrapper.find(DescriptionComponent);
const findHighlightBarComponent = () => wrapper.find(HighlightBar);
const findAlertDetailsComponent = () => wrapper.findComponent(AlertDetailsTable);
const findDescriptionComponent = () => wrapper.findComponent(DescriptionComponent);
const findHighlightBarComponent = () => wrapper.findComponent(HighlightBar);
const findTimelineTab = () => wrapper.findComponent(TimelineTab);
describe('empty state', () => {

View File

@ -62,8 +62,8 @@ describe('Sentry Error Stack Trace', () => {
describe('loading', () => {
it('should show spinner while loading', () => {
mountComponent();
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
expect(wrapper.find(Stacktrace).exists()).toBe(false);
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
expect(wrapper.findComponent(Stacktrace).exists()).toBe(false);
});
});
@ -74,8 +74,8 @@ describe('Sentry Error Stack Trace', () => {
it('should show stacktrace', () => {
mountComponent({ stubs: {} });
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.find(Stacktrace).exists()).toBe(true);
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
expect(wrapper.findComponent(Stacktrace).exists()).toBe(true);
});
});
});

View File

@ -21,15 +21,15 @@ describe('JiraImportApp', () => {
const setupIllustration = 'setup-illustration.svg';
const getFormComponent = () => wrapper.find(JiraImportForm);
const getFormComponent = () => wrapper.findComponent(JiraImportForm);
const getProgressComponent = () => wrapper.find(JiraImportProgress);
const getProgressComponent = () => wrapper.findComponent(JiraImportProgress);
const getSetupComponent = () => wrapper.find(JiraImportSetup);
const getSetupComponent = () => wrapper.findComponent(JiraImportSetup);
const getAlert = () => wrapper.find(GlAlert);
const getAlert = () => wrapper.findComponent(GlAlert);
const getLoadingIcon = () => wrapper.find(GlLoadingIcon);
const getLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const mountComponent = ({
isJiraConfigured = true,

View File

@ -289,7 +289,7 @@ describe('JiraImportForm', () => {
it('updates the user list', () => {
expect(getUserDropdown().findAll(GlDropdownItem)).toHaveLength(1);
expect(getUserDropdown().find(GlDropdownItem).text()).toContain(
expect(getUserDropdown().findComponent(GlDropdownItem).text()).toContain(
'fchopin (Frederic Chopin)',
);
});

View File

@ -8,7 +8,7 @@ describe('JiraImportProgress', () => {
const importProject = 'JIRAPROJECT';
const getGlEmptyStateProp = (attribute) => wrapper.find(GlEmptyState).props(attribute);
const getGlEmptyStateProp = (attribute) => wrapper.findComponent(GlEmptyState).props(attribute);
const getParagraphText = () => wrapper.find('p').text();

View File

@ -6,7 +6,7 @@ import { illustration, jiraIntegrationPath } from '../mock_data';
describe('JiraImportSetup', () => {
let wrapper;
const getGlEmptyStateProp = (attribute) => wrapper.find(GlEmptyState).props(attribute);
const getGlEmptyStateProp = (attribute) => wrapper.findComponent(GlEmptyState).props(attribute);
beforeEach(() => {
wrapper = shallowMount(JiraImportSetup, {

View File

@ -34,7 +34,7 @@ describe('~/labels/components/delete_label_modal', () => {
wrapper.destroy();
});
const findModal = () => wrapper.find(GlModal);
const findModal = () => wrapper.findComponent(GlModal);
const findPrimaryModalButton = () => wrapper.findByTestId('delete-button');
describe('template', () => {

View File

@ -1,7 +1,7 @@
import { GlAvatarLink, GlBadge } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import UserAvatar from '~/members/components/avatars/user_avatar.vue';
import { AVAILABILITY_STATUS } from '~/set_status_modal/utils';
import { AVAILABILITY_STATUS } from '~/set_status_modal/constants';
import { member as memberMock, member2faEnabled, orphanedMember } from '../../mock_data';

View File

@ -49,8 +49,8 @@ describe('Merge Conflict Resolver App', () => {
extendedWrapper(w).findByTestId('interactive-button');
const findFileInlineButton = (w = wrapper) => extendedWrapper(w).findByTestId('inline-button');
const findSideBySideButton = () => wrapper.findByTestId('side-by-side');
const findInlineConflictLines = (w = wrapper) => w.find(InlineConflictLines);
const findParallelConflictLines = (w = wrapper) => w.find(ParallelConflictLines);
const findInlineConflictLines = (w = wrapper) => w.findComponent(InlineConflictLines);
const findParallelConflictLines = (w = wrapper) => w.findComponent(ParallelConflictLines);
const findCommitMessageTextarea = () => wrapper.findByTestId('commit-message');
it('shows the amount of conflicts', () => {

View File

@ -96,9 +96,9 @@ describe('Milestone combobox component', () => {
const findNoResults = () => wrapper.find('[data-testid="milestone-combobox-no-results"]');
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findSearchBox = () => wrapper.find(GlSearchBoxByType);
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
const findProjectMilestonesSection = () =>
wrapper.find('[data-testid="project-milestones-section"]');

View File

@ -25,7 +25,7 @@ describe('~/nav/components/top_nav_dropdown_menu.vue', () => {
};
const findMenuItems = () => wrapper.findAllComponents(TopNavMenuItem);
const findMenuSections = () => wrapper.find(TopNavMenuSections);
const findMenuSections = () => wrapper.findComponent(TopNavMenuSections);
const findMenuSidebar = () => wrapper.find('[data-testid="menu-sidebar"]');
const findMenuSubview = () => wrapper.findComponent(KeepAliveSlots);
const hasFullWidthMenuSidebar = () => findMenuSidebar().classes('gl-w-full');

View File

@ -26,7 +26,7 @@ describe('~/nav/components/top_nav_menu_item.vue', () => {
});
};
const findButton = () => wrapper.find(GlButton);
const findButton = () => wrapper.findComponent(GlButton);
const findButtonIcons = () =>
findButton()
.findAllComponents(GlIcon)

View File

@ -27,7 +27,7 @@ describe('LaTeX output cell', () => {
${1} | ${false}
`('sets `Prompt.show-output` to $expectation when index is $index', ({ index, expectation }) => {
const wrapper = createComponent(inlineLatex, index);
const prompt = wrapper.find(Prompt);
const prompt = wrapper.findComponent(Prompt);
expect(prompt.props().count).toEqual(count);
expect(prompt.props().showOutput).toEqual(expectation);

Some files were not shown because too many files have changed in this diff Show More