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 - .shared:rules:update-cache
stage: prepare stage: prepare
script: 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: static-analysis:
extends: extends:
@ -121,7 +124,11 @@ rubocop:
- | - |
# For non-merge request, or when RUN_ALL_RUBOCOP is 'true', run all RuboCop rules # 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 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 else
run_timed_command "bundle exec rubocop --parallel --force-exclusion $(cat ${RSPEC_CHANGED_FILES_PATH})" run_timed_command "bundle exec rubocop --parallel --force-exclusion $(cat ${RSPEC_CHANGED_FILES_PATH})"
fi 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 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 { __ } from '~/locale';
import EmojiMenu from './emoji_menu'; import { initSetStatusForm } from '~/profile/profile';
const defaultStatusEmoji = 'speech_balloon'; initSetStatusForm();
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 });
const userNameInput = document.getElementById('user_name'); const userNameInput = document.getElementById('user_name');
userNameInput.addEventListener('input', () => { if (userNameInput) {
const EMOJI_REGEX = emojiRegex(); userNameInput.addEventListener('input', () => {
if (EMOJI_REGEX.test(userNameInput.value)) { const EMOJI_REGEX = emojiRegex();
// set field to invalid so it gets detected by GlFieldErrors if (EMOJI_REGEX.test(userNameInput.value)) {
userNameInput.setCustomValidity(__('Invalid field')); // set field to invalid so it gets detected by GlFieldErrors
} else { userNameInput.setCustomValidity(__('Invalid field'));
userNameInput.setCustomValidity(''); } 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.'),
}),
);

View File

@ -170,7 +170,7 @@ export default {
ref="mainPipelineContainer" ref="mainPipelineContainer"
class="gl-display-flex gl-position-relative gl-bg-gray-10 gl-white-space-nowrap" class="gl-display-flex gl-position-relative gl-bg-gray-10 gl-white-space-nowrap"
:class="{ :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> <linked-graph-wrapper>

View File

@ -82,7 +82,9 @@ export default {
:stage-name="stageName" :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> </div>
</button> </button>

View File

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

View File

@ -1,11 +1,14 @@
import $ from 'jquery'; import $ from 'jquery';
import Vue from 'vue';
import { VARIANT_DANGER, VARIANT_INFO, createAlert } from '~/flash'; import { VARIANT_DANGER, VARIANT_INFO, createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import { parseRailsFormFields } from '~/lib/utils/forms';
import { Rails } from '~/lib/utils/rails_ujs'; import { Rails } from '~/lib/utils/rails_ujs';
import TimezoneDropdown, { import TimezoneDropdown, {
formatTimezone, formatTimezone,
} from '~/pages/projects/pipeline_schedules/shared/components/timezone_dropdown'; } 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 { export default class Profile {
constructor({ form } = {}) { 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: { props: {
label: { label: {
type: String, type: String,
required: true, default: null,
required: false,
}, },
value: { value: {
type: String, type: String,
@ -39,7 +40,11 @@ export default {
<template> <template>
<div class="gl-display-contents"> <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"> <dd class="gl-mb-5">
<template v-if="value || $scopedSlots.value"> <template v-if="value || $scopedSlots.value">
<slot name="value">{{ value }}</slot> <slot name="value">{{ value }}</slot>

View File

@ -1,7 +1,10 @@
<script> <script>
import { GlIntersperse } from '@gitlab/ui'; import { GlIntersperse, GlLink } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.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 { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import { ACCESS_LEVEL_REF_PROTECTED, GROUP_TYPE, PROJECT_TYPE } from '../constants'; import { ACCESS_LEVEL_REF_PROTECTED, GROUP_TYPE, PROJECT_TYPE } from '../constants';
import RunnerDetail from './runner_detail.vue'; import RunnerDetail from './runner_detail.vue';
@ -12,6 +15,8 @@ import RunnerTags from './runner_tags.vue';
export default { export default {
components: { components: {
GlIntersperse, GlIntersperse,
GlLink,
HelpPopover,
RunnerDetail, RunnerDetail,
RunnerMaintenanceNoteDetail: () => RunnerMaintenanceNoteDetail: () =>
import('ee_component/runner/components/runner_maintenance_note_detail.vue'), import('ee_component/runner/components/runner_maintenance_note_detail.vue'),
@ -24,6 +29,7 @@ export default {
RunnerTags, RunnerTags,
TimeAgo, TimeAgo,
}, },
mixins: [glFeatureFlagMixin()],
props: { props: {
runner: { runner: {
type: Object, type: Object,
@ -60,6 +66,16 @@ export default {
isProjectRunner() { isProjectRunner() {
return this.runner?.runnerType === PROJECT_TYPE; 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, ACCESS_LEVEL_REF_PROTECTED,
}; };
@ -101,6 +117,34 @@ export default {
</template> </template>
</runner-detail> </runner-detail>
<runner-detail :label="s__('Runners|Maximum job timeout')" :value="maximumTimeout" /> <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')"> <runner-detail :label="s__('Runners|Tags')">
<template v-if="tagList.length" #value> <template v-if="tagList.length" #value>
<runner-tags class="gl-vertical-align-middle" :tag-list="tagList" size="sm" /> <runner-tags class="gl-vertical-align-middle" :tag-list="tagList" size="sm" />

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@ import { __, s__ } from '~/locale';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import workItemQuery from '../graphql/work_item.query.graphql'; 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'; import { i18n, TRACKING_CATEGORY_SHOW, WIDGET_TYPE_DESCRIPTION } from '../constants';
export default { export default {
@ -142,9 +142,9 @@ export default {
this.track('updated_description'); this.track('updated_description');
const { const {
data: { workItemUpdateWidgets }, data: { workItemUpdate },
} = await this.$apollo.mutate({ } = await this.$apollo.mutate({
mutation: updateWorkItemWidgetsMutation, mutation: updateWorkItemMutation,
variables: { variables: {
input: { input: {
id: this.workItem.id, id: this.workItem.id,
@ -155,8 +155,8 @@ export default {
}, },
}); });
if (workItemUpdateWidgets.errors?.length) { if (workItemUpdate.errors?.length) {
throw new Error(workItemUpdateWidgets.errors[0]); throw new Error(workItemUpdate.errors[0]);
} }
this.isEditing = false; 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) push_frontend_feature_flag(:runner_list_stacked_layout_admin)
end end
before_action only: [:show] do
push_frontend_feature_flag(:enforce_runner_token_expires_at)
end
feature_category :runner feature_category :runner
urgency :low urgency :low
@ -23,7 +27,7 @@ class Admin::RunnersController < Admin::ApplicationController
end end
def update 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| respond_to do |format|
format.html { redirect_to edit_admin_runner_path(@runner) } format.html { redirect_to edit_admin_runner_path(@runner) }
end end
@ -40,7 +44,7 @@ class Admin::RunnersController < Admin::ApplicationController
end end
def resume 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.') redirect_to admin_runners_path, notice: _('Runner was successfully updated.')
else else
redirect_to admin_runners_path, alert: _('Runner was not updated.') redirect_to admin_runners_path, alert: _('Runner was not updated.')
@ -48,7 +52,7 @@ class Admin::RunnersController < Admin::ApplicationController
end end
def pause 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.') redirect_to admin_runners_path, notice: _('Runner was successfully updated.')
else else
redirect_to admin_runners_path, alert: _('Runner was not updated.') 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) push_frontend_feature_flag(:runner_list_stacked_layout, @group)
end end
before_action only: [:show] do
push_frontend_feature_flag(:enforce_runner_token_expires_at)
end
feature_category :runner feature_category :runner
urgency :low urgency :low
@ -26,7 +30,7 @@ class Groups::RunnersController < Groups::ApplicationController
end end
def update 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.') redirect_to group_runner_path(@group, @runner), notice: _('Runner was successfully updated.')
else else
render 'edit' render 'edit'

View File

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

View File

@ -15,7 +15,7 @@ class Projects::RunnersController < Projects::ApplicationController
end end
def update 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.') redirect_to project_runner_path(@project, @runner), notice: _('Runner was successfully updated.')
else else
render 'edit' render 'edit'
@ -31,7 +31,7 @@ class Projects::RunnersController < Projects::ApplicationController
end end
def resume 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.') redirect_to project_runners_path(@project), notice: _('Runner was successfully updated.')
else else
redirect_to project_runners_path(@project), alert: _('Runner was not updated.') redirect_to project_runners_path(@project), alert: _('Runner was not updated.')
@ -39,7 +39,7 @@ class Projects::RunnersController < Projects::ApplicationController
end end
def pause 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.') redirect_to project_runners_path(@project), notice: _('Runner was successfully updated.')
else else
redirect_to project_runners_path(@project), alert: _('Runner was not updated.') redirect_to project_runners_path(@project), alert: _('Runner was not updated.')

View File

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

View File

@ -450,6 +450,14 @@ module ApplicationSettingsHelper
end end
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 def external_authorization_service_attributes
[ [
:external_auth_client_cert, :external_auth_client_cert,

View File

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

View File

@ -9,11 +9,14 @@ module Ci
@runner = runner @runner = runner
end end
def update(params) def execute(params)
params[:active] = !params.delete(:paused) if params.include?(:paused) params[:active] = !params.delete(:paused) if params.include?(:paused)
runner.update(params).tap do |updated| if runner.update(params)
runner.tick_runner_queue if updated runner.tick_runner_queue
ServiceResponse.success
else
ServiceResponse.error(message: runner.errors.full_messages)
end end
end 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 # See also the discussion in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60700#note_612724683
USE_COMMIT_DATE_FOR_CROSS_REFERENCE_NOTE = false 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 # noteable_ref - Referenced noteable object
# #
@ -115,8 +122,8 @@ module SystemNotes
text_parts = [] text_parts = []
Gitlab::I18n.with_default_locale do Gitlab::I18n.with_default_locale do
text_parts << "requested review from #{added_users.map(&:to_reference).to_sentence}" if added_users.any? text_parts << "#{self.class.issuable_events[:review_requested]} #{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_request_removed]} #{unassigned_users.map(&:to_reference).to_sentence}" if unassigned_users.any?
end end
body = text_parts.join(' and ') 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' = 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 .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.') = 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 = f.submit _('Save changes'), pajamas_button: true

View File

@ -2,8 +2,6 @@
- page_title s_("Profiles|Edit Profile") - page_title s_("Profiles|Edit Profile")
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
- gravatar_link = link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host - 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| = 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 .row.js-search-settings-section
@ -43,39 +41,12 @@
%h4.gl-mt-0= s_("Profiles|Current status") %h4.gl-mt-0= s_("Profiles|Current status")
%p= s_("Profiles|This emoji and message will appear on your profile and throughout the interface.") %p= s_("Profiles|This emoji and message will appear on your profile and throughout the interface.")
.col-lg-8 .col-lg-8
= f.fields_for :status, @user.status do |status_form| #js-user-profile-set-status-form
- emoji_button = render Pajamas::ButtonComponent.new(button_options: { title: s_("Profiles|Add status emoji"), = f.fields_for :status, @user.status do |status_form|
class: 'js-toggle-emoji-menu emoji-menu-toggle-button has-tooltip' } ) do = status_form.hidden_field :emoji, data: { js_name: 'emoji' }
- if custom_emoji = status_form.hidden_field :message, data: { js_name: 'message' }
= emoji_icon(@user.status.emoji, class: 'gl-mr-0!') = status_form.hidden_field :availability, data: { js_name: 'availability' }
%span#js-no-emoji-placeholder.no-emoji-placeholder{ class: ('hidden' if custom_emoji) } = status_form.hidden_field :clear_status_after, data: { js_name: 'clearStatusAfter' }
= 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"]
.col-lg-12 .col-lg-12
%hr %hr
.row.user-time-preferences.js-search-settings-section .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. - **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. 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 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 GitLab can display the results of coverage report in the merge request
[diff annotations](../testing/test_coverage_visualization.md). [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. 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 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. 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 redirect_to: 'backend/ruby_style_guide.md#newlines-style-guide'
group: unassigned remove_date: '2022-12-15'
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
--- ---
# 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. <!-- This redirect file can be deleted after 2022-12-15. -->
<!-- Redirects that point to other docs in the same project expire in three months. -->
## Rule: separate code with newlines only to group together related logic <!-- 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 -->
```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
```

View File

@ -188,6 +188,8 @@ Alternatively you can use the following on each spec run,
bundle exec spring rspec some_spec.rb bundle exec spring rspec some_spec.rb
``` ```
## RuboCop tasks
## Generate initial RuboCop TODO list ## Generate initial RuboCop TODO list
One way to generate the initial list is to run the Rake task `rubocop:todo:generate`: 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) See [Resolving RuboCop exceptions](contributing/style_guides.md#resolving-rubocop-exceptions)
on how to proceed from here. 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 ## Compile Frontend Assets
You shouldn't ever need to compile frontend assets manually in development, but You shouldn't ever need to compile frontend assets manually in development, but

View File

@ -1,24 +1,11 @@
--- ---
stage: Systems redirect_to: 'https://docs.gitlab.com/charts/'
group: Distribution remove_date: '2023-09-09'
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
--- ---
# 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 <!-- This redirect file can be deleted after <2023-09-09>. -->
available for deployment on Kubernetes, OpenShift, and Kubernetes-compatible <!-- Redirects that point to other docs in the same project expire in three months. -->
platforms. The following deployment methods are available: <!-- 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 -->
- [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.

View File

@ -49,6 +49,20 @@ sole discretion of GitLab Inc.
<div class="deprecation removal-160 breaking-change"> <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 ### Non-expiring access tokens
Planned removal: GitLab <span class="removal-milestone">16.0</span> (2023-05-22) 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 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. 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. 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) params[:active] = !params.delete(:paused) if params.include?(:paused)
update_service = ::Ci::Runners::UpdateRunnerService.new(runner) 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 present runner, with: Entities::Ci::RunnerDetails, current_user: current_user
else else
render_validation_error!(runner) render_validation_error!(runner)

View File

@ -279,7 +279,7 @@ semgrep-sast:
image: image:
name: "$SAST_ANALYZER_IMAGE" name: "$SAST_ANALYZER_IMAGE"
variables: variables:
SERACH_MAX_DEPTH: 20 SEARCH_MAX_DEPTH: 20
SAST_ANALYZER_IMAGE_TAG: 3 SAST_ANALYZER_IMAGE_TAG: 3
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/semgrep:$SAST_ANALYZER_IMAGE_TAG$SAST_IMAGE_SUFFIX" SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/semgrep:$SAST_ANALYZER_IMAGE_TAG$SAST_IMAGE_SUFFIX"
rules: 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 Gitlab::GithubImport::Importer::Events::CrossReferenced
when 'assigned', 'unassigned' when 'assigned', 'unassigned'
Gitlab::GithubImport::Importer::Events::ChangedAssignee Gitlab::GithubImport::Importer::Events::ChangedAssignee
when 'review_requested', 'review_request_removed'
Gitlab::GithubImport::Importer::Events::ChangedReviewer
end end
end end
end end

View File

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

View File

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

View File

@ -6,6 +6,19 @@ unless Rails.env.production?
RuboCop::RakeTask.new RuboCop::RakeTask.new
namespace :rubocop do 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 namespace :todo do
desc 'Generate RuboCop todos' desc 'Generate RuboCop todos'
task :generate do |_task, args| task :generate do |_task, args|

View File

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

View File

@ -10,8 +10,8 @@ module Gitlab
text_field :activation_code text_field :activation_code
button :activate button :activate
label :terms_of_services, text: /I agree that/ label :terms_of_services, text: /I agree that/
link :remove_license, 'data-testid': 'license-remove-action' button :remove_license
button :confirm_ok_button button :confirm_remove_license
p :plan p :plan
p :started p :started
p :name p :name
@ -30,6 +30,11 @@ module Gitlab
terms_of_services_element.click # workaround for hidden checkbox terms_of_services_element.click # workaround for hidden checkbox
end end
def remove_license_file
remove_license
confirm_remove_license
end
# Checks if a subscription record exists in subscription history table # Checks if a subscription record exists in subscription history table
# #
# @param plan [Hash] Name of the plan # @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 # frozen_string_literal: true
require_relative 'formatter/graceful_formatter'
module RuboCop module RuboCop
class CopTodo class CopTodo
attr_accessor :previously_disabled attr_accessor :previously_disabled, :grace_period
attr_reader :cop_name, :files, :offense_count attr_reader :cop_name, :files, :offense_count
@ -12,6 +14,7 @@ module RuboCop
@offense_count = 0 @offense_count = 0
@cop_class = self.class.find_cop_by_name(cop_name) @cop_class = self.class.find_cop_by_name(cop_name)
@previously_disabled = false @previously_disabled = false
@grace_period = false
end end
def record(file, offense_count) def record(file, offense_count)
@ -35,6 +38,7 @@ module RuboCop
yaml << ' Enabled: false' yaml << ' Enabled: false'
end end
yaml << " #{RuboCop::Formatter::GracefulFormatter.grace_period_key_value}" if grace_period
yaml << ' Exclude:' yaml << ' Exclude:'
yaml.concat files.sort.map { |file| " - '#{file}'" } yaml.concat files.sort.map { |file| " - '#{file}'" }
yaml << '' 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 '../todo_dir'
require_relative '../cop_todo' require_relative '../cop_todo'
require_relative '../formatter/graceful_formatter'
module RuboCop module RuboCop
module Formatter module Formatter
@ -47,6 +48,8 @@ module RuboCop
def finished(_inspected_files) def finished(_inspected_files)
@todos.values.sort_by(&:cop_name).each do |todo| @todos.values.sort_by(&:cop_name).each do |todo|
todo.previously_disabled = previously_disabled?(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) path = @todo_dir.write(todo.cop_name, todo.to_yaml)
output.puts "Written to #{relative_path(path)}\n" output.puts "Written to #{relative_path(path)}\n"
@ -79,16 +82,31 @@ module RuboCop
raise "Multiple configurations found for cops:\n#{list}\n" raise "Multiple configurations found for cops:\n#{list}\n"
end end
def previously_disabled?(todo) def config_for(todo)
cop_name = todo.cop_name cop_name = todo.cop_name
config = @config_old_todo_yml[cop_name] || @config_old_todo_yml[cop_name] || @config_inspect_todo_dir[cop_name] || {}
@config_inspect_todo_dir[cop_name] || {} end
def previously_disabled?(todo)
config = config_for(todo)
return false if config.empty? return false if config.empty?
config['Enabled'] == false config['Enabled'] == false
end 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 def load_config_inspect_todo_dir
@todo_dir.list_inspect.each_with_object({}) do |path, combined| @todo_dir.list_inspect.each_with_object({}) do |path, combined|
config = YAML.load_file(path) config = YAML.load_file(path)

View File

@ -74,7 +74,7 @@ RSpec.describe Admin::RunnersController do
context 'with update succeeding' do context 'with update succeeding' do
before do before do
expect_next_instance_of(Ci::Runners::UpdateRunnerService, runner) do |service| 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
end end
@ -91,7 +91,7 @@ RSpec.describe Admin::RunnersController do
context 'with update failing' do context 'with update failing' do
before do before do
expect_next_instance_of(Ci::Runners::UpdateRunnerService, runner) do |service| 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
end end

View File

@ -82,13 +82,17 @@ RSpec.describe ProfilesController, :request_store do
expect(ldap_user.location).to eq('City, Country') expect(ldap_user.location).to eq('City, Country')
end end
it 'allows setting a user status' do it 'allows setting a user status', :freeze_time do
sign_in(user) 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.message).to eq('Working hard!')
expect(user.reload.status.availability).to eq('busy') 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) expect(response).to have_gitlab_http_status(:found)
end end

View File

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

View File

@ -19,10 +19,13 @@ import { createWrapper, ErrorWrapper } from '@vue/test-utils';
* @returns Wrapper * @returns Wrapper
*/ */
export const findDd = (dtLabel, wrapper) => { export const findDd = (dtLabel, wrapper) => {
const dt = wrapper.findByText(dtLabel).element; const dtw = wrapper.findByText(dtLabel);
const dd = dt.nextElementSibling; if (dtw.exists()) {
if (dt.tagName === 'DT' && dd.tagName === 'DD') { const dt = dtw.element;
return createWrapper(dd, {}); 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 // eslint-disable-next-line no-restricted-syntax
await wrapper.setData({ renderGroup: true }); 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 () => { it('renders group on appear', async () => {
wrapper.find(GlIntersectionObserver).vm.$emit('appear'); wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
await nextTick(); 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 () => { it('emits appear event on appear', async () => {
wrapper.find(GlIntersectionObserver).vm.$emit('appear'); wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
await nextTick(); await nextTick();

View File

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

View File

@ -42,7 +42,7 @@ describe('error tracking settings project dropdown', () => {
describe('empty project list', () => { describe('empty project list', () => {
it('renders the dropdown', () => { it('renders the dropdown', () => {
expect(wrapper.find('#project-dropdown').exists()).toBe(true); 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', () => { it('shows helper text', () => {
@ -57,8 +57,8 @@ describe('error tracking settings project dropdown', () => {
}); });
it('does not contain any dropdown items', () => { it('does not contain any dropdown items', () => {
expect(wrapper.find(GlDropdownItem).exists()).toBe(false); expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(false);
expect(wrapper.find(GlDropdown).props('text')).toBe('No projects available'); expect(wrapper.findComponent(GlDropdown).props('text')).toBe('No projects available');
}); });
}); });
@ -71,11 +71,11 @@ describe('error tracking settings project dropdown', () => {
it('renders the dropdown', () => { it('renders the dropdown', () => {
expect(wrapper.find('#project-dropdown').exists()).toBe(true); 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', () => { 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); 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', () => { describe('Recent Searches Dropdown Content', () => {
let wrapper; let wrapper;
const findLocalStorageNote = () => wrapper.find({ ref: 'localStorageNote' }); const findLocalStorageNote = () => wrapper.findComponent({ ref: 'localStorageNote' });
const findDropdownItems = () => wrapper.findAll({ ref: 'dropdownItem' }); const findDropdownItems = () => wrapper.findAll({ ref: 'dropdownItem' });
const findDropdownNote = () => wrapper.find({ ref: 'dropdownNote' }); const findDropdownNote = () => wrapper.findComponent({ ref: 'dropdownNote' });
const createComponent = (props) => { const createComponent = (props) => {
wrapper = shallowMount(RecentSearchesDropdownContent, { wrapper = shallowMount(RecentSearchesDropdownContent, {
@ -94,7 +94,7 @@ describe('Recent Searches Dropdown Content', () => {
}); });
it('emits requestClearRecentSearches on Clear resent searches button', () => { it('emits requestClearRecentSearches on Clear resent searches button', () => {
wrapper.find({ ref: 'clearButton' }).trigger('click'); wrapper.findComponent({ ref: 'clearButton' }).trigger('click');
expect(onRequestClearRecentSearchesSpy).toHaveBeenCalled(); expect(onRequestClearRecentSearchesSpy).toHaveBeenCalled();
}); });

View File

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

View File

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

View File

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

View File

@ -99,7 +99,7 @@ describe('import table', () => {
}); });
await waitForPromises(); 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 () => { it('does not renders loading icon when request is completed', async () => {
@ -108,7 +108,7 @@ describe('import table', () => {
}); });
await waitForPromises(); 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(); 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', () => { 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', () => { it('renders pagination dropdown', () => {
@ -293,7 +293,7 @@ describe('import table', () => {
it('updates page when page change is requested', async () => { it('updates page when page change is requested', async () => {
const REQUESTED_PAGE = 2; const REQUESTED_PAGE = 2;
wrapper.find(PaginationLinks).props().change(REQUESTED_PAGE); wrapper.findComponent(PaginationLinks).props().change(REQUESTED_PAGE);
await waitForPromises(); await waitForPromises();
expect(bulkImportSourceGroupsQueryMock).toHaveBeenCalledWith( expect(bulkImportSourceGroupsQueryMock).toHaveBeenCalledWith(
@ -316,7 +316,7 @@ describe('import table', () => {
}, },
versionValidation: FAKE_VERSION_VALIDATION, versionValidation: FAKE_VERSION_VALIDATION,
}); });
wrapper.find(PaginationLinks).props().change(REQUESTED_PAGE); wrapper.findComponent(PaginationLinks).props().change(REQUESTED_PAGE);
await waitForPromises(); await waitForPromises();
expect(wrapper.text()).toContain('Showing 21-21 of 38 groups that you own from'); expect(wrapper.text()).toContain('Showing 21-21 of 38 groups that you own from');
@ -539,8 +539,8 @@ describe('import table', () => {
}); });
await waitForPromises(); await waitForPromises();
expect(wrapper.find(GlAlert).exists()).toBe(true); expect(wrapper.findComponent(GlAlert).exists()).toBe(true);
expect(wrapper.find(GlAlert).text()).toContain('projects (require v14.8.0)'); expect(wrapper.findComponent(GlAlert).text()).toContain('projects (require v14.8.0)');
}); });
it('does not renders alert when there are no unavailable features', async () => { it('does not renders alert when there are no unavailable features', async () => {
@ -558,7 +558,7 @@ describe('import table', () => {
}); });
await waitForPromises(); 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 wrapper;
let group; let group;
const findNameInput = () => wrapper.find(GlFormInput); const findNameInput = () => wrapper.findComponent(GlFormInput);
const findNamespaceDropdown = () => wrapper.find(ImportGroupDropdown); const findNamespaceDropdown = () => wrapper.findComponent(ImportGroupDropdown);
const createComponent = (props) => { const createComponent = (props) => {
wrapper = shallowMount(ImportTargetCell, { wrapper = shallowMount(ImportTargetCell, {

View File

@ -33,12 +33,12 @@ describe('BitbucketStatusTable', () => {
it('renders import table component', () => { it('renders import table component', () => {
createComponent({ providerTitle: 'Test' }); 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', () => { it('passes alert in incompatible-repos-warning slot', () => {
createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub); 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', () => { it('passes actions slot to import project table component', () => {
@ -46,14 +46,14 @@ describe('BitbucketStatusTable', () => {
createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub, { createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub, {
actions: actionsSlotContent, actions: actionsSlotContent,
}); });
expect(wrapper.find(ImportProjectsTable).text()).toBe(actionsSlotContent); expect(wrapper.findComponent(ImportProjectsTable).text()).toBe(actionsSlotContent);
}); });
it('dismisses alert when requested', async () => { it('dismisses alert when requested', async () => {
createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub); createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub);
wrapper.find(GlAlert).vm.$emit('dismiss'); wrapper.findComponent(GlAlert).vm.$emit('dismiss');
await nextTick(); 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) .findAll(GlButton)
.filter((w) => w.props().variant === 'confirm') .filter((w) => w.props().variant === 'confirm')
.at(0); .at(0);
const findImportAllModal = () => wrapper.find({ ref: 'importAllModal' }); const findImportAllModal = () => wrapper.findComponent({ ref: 'importAllModal' });
const importAllFn = jest.fn(); const importAllFn = jest.fn();
const importAllModalShowFn = jest.fn(); const importAllModalShowFn = jest.fn();
@ -89,13 +89,13 @@ describe('ImportProjectsTable', () => {
it('renders a loading icon while repos are loading', () => { it('renders a loading icon while repos are loading', () => {
createComponent({ state: { isLoadingRepos: true } }); 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', () => { it('renders a loading icon while namespaces are loading', () => {
createComponent({ state: { isLoadingNamespaces: true } }); 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', () => { it('renders a table with provider repos', () => {
@ -109,7 +109,7 @@ describe('ImportProjectsTable', () => {
state: { namespaces: [{ fullPath: 'path' }], repositories }, 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.find('table').exists()).toBe(true);
expect( expect(
wrapper wrapper
@ -170,7 +170,7 @@ describe('ImportProjectsTable', () => {
it('renders an empty state if there are no repositories available', () => { it('renders an empty state if there are no repositories available', () => {
createComponent({ state: { repositories: [] } }); 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`); expect(wrapper.text()).toContain(`No ${providerTitle} repositories found`);
}); });
@ -231,11 +231,11 @@ describe('ImportProjectsTable', () => {
}); });
it('renders intersection observer component', () => { 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 () => { it('calls fetchRepos when intersection observer appears', async () => {
wrapper.find(GlIntersectionObserver).vm.$emit('appear'); wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
await nextTick(); await nextTick();

View File

@ -74,11 +74,13 @@ describe('ProviderRepoTableRow', () => {
}); });
it('renders empty import status', () => { 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', () => { 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', () => { it('renders import button', () => {
@ -127,11 +129,13 @@ describe('ProviderRepoTableRow', () => {
}); });
it('renders proper import status', () => { 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', () => { 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', () => { it('does not render import button', () => {
@ -139,7 +143,7 @@ describe('ProviderRepoTableRow', () => {
}); });
it('passes stats to import status component', () => { 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', () => { 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, all: 26,
}; };
const findTable = () => wrapper.find(GlTable); const findTable = () => wrapper.findComponent(GlTable);
const findTableRows = () => wrapper.findAll('table tbody tr'); const findTableRows = () => wrapper.findAll('table tbody tr');
const findAlert = () => wrapper.find(GlAlert); const findAlert = () => wrapper.findComponent(GlAlert);
const findLoader = () => wrapper.find(GlLoadingIcon); const findLoader = () => wrapper.findComponent(GlLoadingIcon);
const findTimeAgo = () => wrapper.findAll(TimeAgoTooltip); const findTimeAgo = () => wrapper.findAll(TimeAgoTooltip);
const findAssignees = () => wrapper.findAll('[data-testid="incident-assignees"]'); const findAssignees = () => wrapper.findAll('[data-testid="incident-assignees"]');
const findCreateIncidentBtn = () => wrapper.find('[data-testid="createIncidentBtn"]'); const findCreateIncidentBtn = () => wrapper.find('[data-testid="createIncidentBtn"]');
const findClosedIcon = () => wrapper.findAll("[data-testid='incident-closed']"); const findClosedIcon = () => wrapper.findAll("[data-testid='incident-closed']");
const findEmptyState = () => wrapper.find(GlEmptyState); const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findSeverity = () => wrapper.findAll(SeverityToken); const findSeverity = () => wrapper.findAll(SeverityToken);
const findEscalationStatus = () => wrapper.findAll('[data-testid="incident-escalation-status"]'); const findEscalationStatus = () => wrapper.findAll('[data-testid="incident-escalation-status"]');
const findIncidentLink = () => wrapper.findByTestId('incident-link'); const findIncidentLink = () => wrapper.findByTestId('incident-link');
@ -179,7 +179,7 @@ describe('Incidents List', () => {
}); });
it('renders an avatar component when there is an assignee', () => { 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 { src, label } = avatar.attributes();
const { name, avatarUrl } = mockIncidents[1].assignees.nodes[0]; const { name, avatarUrl } = mockIncidents[1].assignees.nodes[0];

View File

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

View File

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

View File

@ -53,7 +53,7 @@ afterEach(() => {
describe('ImportProjectMembersModal', () => { describe('ImportProjectMembersModal', () => {
const findGlModal = () => wrapper.findComponent(GlModal); 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 clickImportButton = () => findGlModal().vm.$emit('primary', { preventDefault: jest.fn() });
const closeModal = () => findGlModal().vm.$emit('hidden', { preventDefault: jest.fn() }); const closeModal = () => findGlModal().vm.$emit('hidden', { preventDefault: jest.fn() });
const findFormGroup = () => wrapper.findByTestId('form-group'); const findFormGroup = () => wrapper.findByTestId('form-group');

View File

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

View File

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

View File

@ -153,7 +153,7 @@ describe('RelatedIssuesBlock', () => {
}); });
it('sets `autoCompleteEpics` to false for add-issuable-form', () => { 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.exists()).toBe(true);
expect(iconComponent.props('name')).toBe(icon); expect(iconComponent.props('name')).toBe(icon);
}); });

View File

@ -187,7 +187,9 @@ describe('RelatedIssuesList', () => {
}); });
it('shows due date', () => { 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 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 findDueDate = () => wrapper.find('[data-testid="issuable-due-date"]');
const mountComponent = ({ const mountComponent = ({
@ -56,8 +56,8 @@ describe('CE IssueCardTimeInfo component', () => {
const milestone = findMilestone(); const milestone = findMilestone();
expect(milestone.text()).toBe(issue.milestone.title); expect(milestone.text()).toBe(issue.milestone.title);
expect(milestone.find(GlIcon).props('name')).toBe('clock'); expect(milestone.findComponent(GlIcon).props('name')).toBe('clock');
expect(milestone.find(GlLink).attributes('href')).toBe(issue.milestone.webPath); expect(milestone.findComponent(GlLink).attributes('href')).toBe(issue.milestone.webPath);
}); });
describe.each` describe.each`
@ -84,7 +84,7 @@ describe('CE IssueCardTimeInfo component', () => {
expect(dueDate.text()).toBe('Dec 12, 2020'); expect(dueDate.text()).toBe('Dec 12, 2020');
expect(dueDate.attributes('title')).toBe('Due date'); 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'); 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.text()).toBe(issue.humanTimeEstimate);
expect(timeEstimate.attributes('title')).toBe('Estimate'); 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; 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 = ({ const mountComponent = ({
shouldShowFinishedAlert = false, shouldShowFinishedAlert = false,
@ -49,7 +49,7 @@ describe('JiraIssuesImportStatus', () => {
}); });
it('does not show an alert', () => { 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, shouldShowInProgressAlert: true,
}); });
expect(wrapper.find(GlAlert).exists()).toBe(true); expect(wrapper.findComponent(GlAlert).exists()).toBe(true);
findAlert().vm.$emit('dismiss'); findAlert().vm.$emit('dismiss');
await nextTick(); 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); const count = wrapper.findAll('.suggestion-counts span').at(0);
expect(count.text()).toContain('1'); 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', () => { it('renders notes count', () => {
@ -114,7 +114,7 @@ describe('Issue title suggestions item component', () => {
const count = wrapper.findAll('.suggestion-counts span').at(1); const count = wrapper.findAll('.suggestion-counts span').at(1);
expect(count.text()).toContain('2'); 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', () => { describe('when title is not in view', () => {
beforeEach(() => { beforeEach(() => {
wrapper.vm.state.titleText = 'Sticky header title'; wrapper.vm.state.titleText = 'Sticky header title';
wrapper.find(GlIntersectionObserver).vm.$emit('disappear'); wrapper.findComponent(GlIntersectionObserver).vm.$emit('disappear');
}); });
it('shows with title', () => { it('shows with title', () => {

View File

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

View File

@ -5,7 +5,7 @@ import eventHub from '~/issues/show/event_hub';
describe('Title field component', () => { describe('Title field component', () => {
let wrapper; let wrapper;
const findInput = () => wrapper.find({ ref: 'input' }); const findInput = () => wrapper.findComponent({ ref: 'input' });
beforeEach(() => { beforeEach(() => {
jest.spyOn(eventHub, '$emit'); 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 findDropdownBy = (dataTestId) => wrapper.find(`[data-testid="${dataTestId}"]`);
const findMobileDropdown = () => findDropdownBy('mobile-dropdown'); const findMobileDropdown = () => findDropdownBy('mobile-dropdown');
@ -73,7 +73,7 @@ describe('HeaderActions component', () => {
const findMobileDropdownItems = () => findMobileDropdown().findAll(GlDropdownItem); const findMobileDropdownItems = () => findMobileDropdown().findAll(GlDropdownItem);
const findDesktopDropdownItems = () => findDesktopDropdown().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); 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', () => { describe('empty state', () => {
beforeEach(() => { beforeEach(() => {

View File

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

View File

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

View File

@ -21,15 +21,15 @@ describe('JiraImportApp', () => {
const setupIllustration = 'setup-illustration.svg'; 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 = ({ const mountComponent = ({
isJiraConfigured = true, isJiraConfigured = true,

View File

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

View File

@ -8,7 +8,7 @@ describe('JiraImportProgress', () => {
const importProject = 'JIRAPROJECT'; 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(); const getParagraphText = () => wrapper.find('p').text();

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import { GlAvatarLink, GlBadge } from '@gitlab/ui'; import { GlAvatarLink, GlBadge } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import UserAvatar from '~/members/components/avatars/user_avatar.vue'; 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'; 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'); extendedWrapper(w).findByTestId('interactive-button');
const findFileInlineButton = (w = wrapper) => extendedWrapper(w).findByTestId('inline-button'); const findFileInlineButton = (w = wrapper) => extendedWrapper(w).findByTestId('inline-button');
const findSideBySideButton = () => wrapper.findByTestId('side-by-side'); const findSideBySideButton = () => wrapper.findByTestId('side-by-side');
const findInlineConflictLines = (w = wrapper) => w.find(InlineConflictLines); const findInlineConflictLines = (w = wrapper) => w.findComponent(InlineConflictLines);
const findParallelConflictLines = (w = wrapper) => w.find(ParallelConflictLines); const findParallelConflictLines = (w = wrapper) => w.findComponent(ParallelConflictLines);
const findCommitMessageTextarea = () => wrapper.findByTestId('commit-message'); const findCommitMessageTextarea = () => wrapper.findByTestId('commit-message');
it('shows the amount of conflicts', () => { 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 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 = () => const findProjectMilestonesSection = () =>
wrapper.find('[data-testid="project-milestones-section"]'); 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 findMenuItems = () => wrapper.findAllComponents(TopNavMenuItem);
const findMenuSections = () => wrapper.find(TopNavMenuSections); const findMenuSections = () => wrapper.findComponent(TopNavMenuSections);
const findMenuSidebar = () => wrapper.find('[data-testid="menu-sidebar"]'); const findMenuSidebar = () => wrapper.find('[data-testid="menu-sidebar"]');
const findMenuSubview = () => wrapper.findComponent(KeepAliveSlots); const findMenuSubview = () => wrapper.findComponent(KeepAliveSlots);
const hasFullWidthMenuSidebar = () => findMenuSidebar().classes('gl-w-full'); 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 = () => const findButtonIcons = () =>
findButton() findButton()
.findAllComponents(GlIcon) .findAllComponents(GlIcon)

View File

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

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