Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
e4cfc16da3
commit
3b69a04945
|
@ -19,7 +19,10 @@ update-static-analysis-cache:
|
|||
- .shared:rules:update-cache
|
||||
stage: prepare
|
||||
script:
|
||||
- run_timed_command "bundle exec rubocop --parallel" # For the moment we only cache `tmp/rubocop_cache` so we don't need to run all the tasks.
|
||||
# Silence cop offenses for rules with "grace period".
|
||||
# This will notify Slack if offenses were silenced.
|
||||
# For the moment we only cache `tmp/rubocop_cache` so we don't need to run all the tasks.
|
||||
- run_timed_command "bundle exec rake rubocop:check:graceful"
|
||||
|
||||
static-analysis:
|
||||
extends:
|
||||
|
@ -121,7 +124,11 @@ rubocop:
|
|||
- |
|
||||
# For non-merge request, or when RUN_ALL_RUBOCOP is 'true', run all RuboCop rules
|
||||
if [ -z "${CI_MERGE_REQUEST_IID}" ] || [ "${RUN_ALL_RUBOCOP}" == "true" ]; then
|
||||
run_timed_command "bundle exec rubocop --parallel"
|
||||
# Silence cop offenses for rules with "grace period".
|
||||
# We won't notify Slack if offenses were silenced to avoid frequent messages.
|
||||
# Job `update-static-analysis-cache` takes care of Slack notifications every 2 hours.
|
||||
unset CI_SLACK_WEBHOOK_URL
|
||||
run_timed_command "bundle exec rake rubocop:check:graceful"
|
||||
else
|
||||
run_timed_command "bundle exec rubocop --parallel --force-exclusion $(cat ${RSPEC_CHANGED_FILES_PATH})"
|
||||
fi
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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;
|
|
@ -0,0 +1,3 @@
|
|||
import initRunnerTokenExpirationIntervals from '~/admin/application_settings/runner_token_expiration/index';
|
||||
|
||||
initRunnerTokenExpirationIntervals();
|
|
@ -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;
|
|
@ -1,90 +1,18 @@
|
|||
import emojiRegex from 'emoji-regex';
|
||||
import $ from 'jquery';
|
||||
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
|
||||
import * as Emoji from '~/emoji';
|
||||
import createFlash from '~/flash';
|
||||
import { __ } from '~/locale';
|
||||
import EmojiMenu from './emoji_menu';
|
||||
import { initSetStatusForm } from '~/profile/profile';
|
||||
|
||||
const defaultStatusEmoji = 'speech_balloon';
|
||||
const toggleEmojiMenuButtonSelector = '.js-toggle-emoji-menu';
|
||||
const toggleEmojiMenuButton = document.querySelector(toggleEmojiMenuButtonSelector);
|
||||
const statusEmojiField = document.getElementById('js-status-emoji-field');
|
||||
const statusMessageField = document.getElementById('js-status-message-field');
|
||||
|
||||
const toggleNoEmojiPlaceholder = (isVisible) => {
|
||||
const placeholderElement = document.getElementById('js-no-emoji-placeholder');
|
||||
placeholderElement.classList.toggle('hidden', !isVisible);
|
||||
};
|
||||
|
||||
const findStatusEmoji = () => toggleEmojiMenuButton.querySelector('gl-emoji');
|
||||
const removeStatusEmoji = () => {
|
||||
const statusEmoji = findStatusEmoji();
|
||||
if (statusEmoji) {
|
||||
statusEmoji.remove();
|
||||
}
|
||||
};
|
||||
|
||||
const selectEmojiCallback = (emoji, emojiTag) => {
|
||||
statusEmojiField.value = emoji;
|
||||
toggleNoEmojiPlaceholder(false);
|
||||
removeStatusEmoji();
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
toggleEmojiMenuButton.innerHTML += emojiTag;
|
||||
};
|
||||
|
||||
const clearEmojiButton = document.getElementById('js-clear-user-status-button');
|
||||
clearEmojiButton.addEventListener('click', () => {
|
||||
statusEmojiField.value = '';
|
||||
statusMessageField.value = '';
|
||||
removeStatusEmoji();
|
||||
toggleNoEmojiPlaceholder(true);
|
||||
});
|
||||
|
||||
const emojiAutocomplete = new GfmAutoComplete();
|
||||
emojiAutocomplete.setup($(statusMessageField), { emojis: true });
|
||||
initSetStatusForm();
|
||||
|
||||
const userNameInput = document.getElementById('user_name');
|
||||
userNameInput.addEventListener('input', () => {
|
||||
const EMOJI_REGEX = emojiRegex();
|
||||
if (EMOJI_REGEX.test(userNameInput.value)) {
|
||||
// set field to invalid so it gets detected by GlFieldErrors
|
||||
userNameInput.setCustomValidity(__('Invalid field'));
|
||||
} else {
|
||||
userNameInput.setCustomValidity('');
|
||||
}
|
||||
});
|
||||
|
||||
Emoji.initEmojiMap()
|
||||
.then(() => {
|
||||
const emojiMenu = new EmojiMenu(
|
||||
Emoji,
|
||||
toggleEmojiMenuButtonSelector,
|
||||
'js-status-emoji-menu',
|
||||
selectEmojiCallback,
|
||||
);
|
||||
emojiMenu.bindEvents();
|
||||
|
||||
const defaultEmojiTag = Emoji.glEmojiTag(defaultStatusEmoji);
|
||||
statusMessageField.addEventListener('input', () => {
|
||||
const hasStatusMessage = statusMessageField.value.trim() !== '';
|
||||
const statusEmoji = findStatusEmoji();
|
||||
if (hasStatusMessage && statusEmoji) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasStatusMessage) {
|
||||
toggleNoEmojiPlaceholder(false);
|
||||
// eslint-disable-next-line no-unsanitized/property
|
||||
toggleEmojiMenuButton.innerHTML += defaultEmojiTag;
|
||||
} else if (statusEmoji.dataset.name === defaultStatusEmoji) {
|
||||
toggleNoEmojiPlaceholder(true);
|
||||
removeStatusEmoji();
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() =>
|
||||
createFlash({
|
||||
message: __('Failed to load emoji list.'),
|
||||
}),
|
||||
);
|
||||
if (userNameInput) {
|
||||
userNameInput.addEventListener('input', () => {
|
||||
const EMOJI_REGEX = emojiRegex();
|
||||
if (EMOJI_REGEX.test(userNameInput.value)) {
|
||||
// set field to invalid so it gets detected by GlFieldErrors
|
||||
userNameInput.setCustomValidity(__('Invalid field'));
|
||||
} else {
|
||||
userNameInput.setCustomValidity('');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -170,7 +170,7 @@ export default {
|
|||
ref="mainPipelineContainer"
|
||||
class="gl-display-flex gl-position-relative gl-bg-gray-10 gl-white-space-nowrap"
|
||||
:class="{
|
||||
'gl-pipeline-min-h gl-py-5 gl-overflow-auto gl-border-t-solid gl-border-t-1 gl-border-gray-100': !isLinkedPipeline,
|
||||
'gl-pipeline-min-h gl-py-5 gl-overflow-auto': !isLinkedPipeline,
|
||||
}"
|
||||
>
|
||||
<linked-graph-wrapper>
|
||||
|
|
|
@ -82,7 +82,9 @@ export default {
|
|||
:stage-name="stageName"
|
||||
/>
|
||||
|
||||
<div class="gl-font-weight-100 gl-font-size-lg gl-ml-n4">{{ group.size }}</div>
|
||||
<div class="gl-font-weight-100 gl-font-size-lg gl-ml-n4 gl-align-self-center">
|
||||
{{ group.size }}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
|
|
|
@ -64,8 +64,7 @@ export default {
|
|||
},
|
||||
},
|
||||
jobClasses: [
|
||||
'gl-py-3',
|
||||
'gl-px-4',
|
||||
'gl-p-3',
|
||||
'gl-border-gray-100',
|
||||
'gl-border-solid',
|
||||
'gl-border-1',
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import $ from 'jquery';
|
||||
import Vue from 'vue';
|
||||
import { VARIANT_DANGER, VARIANT_INFO, createAlert } from '~/flash';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import { parseRailsFormFields } from '~/lib/utils/forms';
|
||||
import { Rails } from '~/lib/utils/rails_ujs';
|
||||
import TimezoneDropdown, {
|
||||
formatTimezone,
|
||||
} from '~/pages/projects/pipeline_schedules/shared/components/timezone_dropdown';
|
||||
import UserProfileSetStatusWrapper from '~/set_status_modal/user_profile_set_status_wrapper.vue';
|
||||
|
||||
export default class Profile {
|
||||
constructor({ form } = {}) {
|
||||
|
@ -116,3 +119,24 @@ export default class Profile {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const initSetStatusForm = () => {
|
||||
const el = document.getElementById('js-user-profile-set-status-form');
|
||||
|
||||
if (!el) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fields = parseRailsFormFields(el);
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
name: 'UserProfileStatusForm',
|
||||
provide: {
|
||||
fields,
|
||||
},
|
||||
render(h) {
|
||||
return h(UserProfileSetStatusWrapper);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -21,7 +21,8 @@ export default {
|
|||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: null,
|
||||
required: false,
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
|
@ -39,7 +40,11 @@ export default {
|
|||
|
||||
<template>
|
||||
<div class="gl-display-contents">
|
||||
<dt class="gl-mb-5 gl-mr-6 gl-max-w-26">{{ label }}</dt>
|
||||
<dt class="gl-mb-5 gl-mr-6 gl-max-w-26">
|
||||
<template v-if="label || $scopedSlots.label">
|
||||
<slot name="label">{{ label }}</slot>
|
||||
</template>
|
||||
</dt>
|
||||
<dd class="gl-mb-5">
|
||||
<template v-if="value || $scopedSlots.value">
|
||||
<slot name="value">{{ value }}</slot>
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
<script>
|
||||
import { GlIntersperse } from '@gitlab/ui';
|
||||
import { GlIntersperse, GlLink } from '@gitlab/ui';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import { s__ } from '~/locale';
|
||||
import HelpPopover from '~/vue_shared/components/help_popover.vue';
|
||||
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
|
||||
import { ACCESS_LEVEL_REF_PROTECTED, GROUP_TYPE, PROJECT_TYPE } from '../constants';
|
||||
import RunnerDetail from './runner_detail.vue';
|
||||
|
@ -12,6 +15,8 @@ import RunnerTags from './runner_tags.vue';
|
|||
export default {
|
||||
components: {
|
||||
GlIntersperse,
|
||||
GlLink,
|
||||
HelpPopover,
|
||||
RunnerDetail,
|
||||
RunnerMaintenanceNoteDetail: () =>
|
||||
import('ee_component/runner/components/runner_maintenance_note_detail.vue'),
|
||||
|
@ -24,6 +29,7 @@ export default {
|
|||
RunnerTags,
|
||||
TimeAgo,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
props: {
|
||||
runner: {
|
||||
type: Object,
|
||||
|
@ -60,6 +66,16 @@ export default {
|
|||
isProjectRunner() {
|
||||
return this.runner?.runnerType === PROJECT_TYPE;
|
||||
},
|
||||
tokenExpirationHelpPopoverOptions() {
|
||||
return {
|
||||
title: s__('Runners|Runner authentication token expiration'),
|
||||
};
|
||||
},
|
||||
tokenExpirationHelpUrl() {
|
||||
return helpPagePath('ci/runners/configure_runners', {
|
||||
anchor: 'authentication-token-security',
|
||||
});
|
||||
},
|
||||
},
|
||||
ACCESS_LEVEL_REF_PROTECTED,
|
||||
};
|
||||
|
@ -101,6 +117,34 @@ export default {
|
|||
</template>
|
||||
</runner-detail>
|
||||
<runner-detail :label="s__('Runners|Maximum job timeout')" :value="maximumTimeout" />
|
||||
<runner-detail
|
||||
v-if="glFeatures.enforceRunnerTokenExpiresAt"
|
||||
:empty-value="s__('Runners|Never expires')"
|
||||
>
|
||||
<template #label>
|
||||
{{ s__('Runners|Token expiry') }}
|
||||
<help-popover :options="tokenExpirationHelpPopoverOptions">
|
||||
<p>
|
||||
{{
|
||||
s__(
|
||||
'Runners|Runner authentication tokens will expire based on a set interval. They will automatically rotate once expired.',
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<p class="gl-mb-0">
|
||||
<gl-link
|
||||
:href="tokenExpirationHelpUrl"
|
||||
target="_blank"
|
||||
class="gl-reset-font-size"
|
||||
>{{ __('Learn more') }}</gl-link
|
||||
>
|
||||
</p>
|
||||
</help-popover>
|
||||
</template>
|
||||
<template v-if="runner.tokenExpiresAt" #value>
|
||||
<time-ago :time="runner.tokenExpiresAt" />
|
||||
</template>
|
||||
</runner-detail>
|
||||
<runner-detail :label="s__('Runners|Tags')">
|
||||
<template v-if="tagList.length" #value>
|
||||
<runner-tags class="gl-vertical-align-middle" :tag-list="tagList" size="sm" />
|
||||
|
|
|
@ -17,6 +17,7 @@ fragment RunnerDetailsShared on CiRunner {
|
|||
createdAt
|
||||
status(legacyMode: null)
|
||||
contactedAt
|
||||
tokenExpiresAt
|
||||
version
|
||||
editAdminUrl
|
||||
userPermissions {
|
||||
|
|
|
@ -70,3 +70,14 @@ export const getPaginationVariables = (pagination, pageSize = 10) => {
|
|||
// Get the first N items
|
||||
return { first: pageSize };
|
||||
};
|
||||
|
||||
/**
|
||||
* Turns a server-provided interval integer represented as a string into an
|
||||
* integer that the frontend can use.
|
||||
*
|
||||
* @param {String} interval - String to convert
|
||||
* @returns Parsed integer
|
||||
*/
|
||||
export const parseInterval = (interval) => {
|
||||
return typeof interval === 'string' ? parseInt(interval, 10) : null;
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
};
|
|
@ -9,26 +9,14 @@ import {
|
|||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlSprintf,
|
||||
GlFormGroup,
|
||||
GlSafeHtmlDirective,
|
||||
} from '@gitlab/ui';
|
||||
import $ from 'jquery';
|
||||
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
|
||||
import * as Emoji from '~/emoji';
|
||||
import { __, s__ } from '~/locale';
|
||||
import { timeRanges } from '~/vue_shared/constants';
|
||||
|
||||
export const AVAILABILITY_STATUS = {
|
||||
BUSY: 'busy',
|
||||
NOT_SET: 'not_set',
|
||||
};
|
||||
|
||||
const statusTimeRanges = [
|
||||
{
|
||||
label: __('Never'),
|
||||
name: 'never',
|
||||
},
|
||||
...timeRanges,
|
||||
];
|
||||
import { s__ } from '~/locale';
|
||||
import { TIME_RANGES_WITH_NEVER, AVAILABILITY_STATUS } from './constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -40,6 +28,7 @@ export default {
|
|||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlSprintf,
|
||||
GlFormGroup,
|
||||
EmojiPicker: () => import('~/emoji/components/picker.vue'),
|
||||
},
|
||||
directives: {
|
||||
|
@ -136,7 +125,8 @@ export default {
|
|||
this.clearEmoji();
|
||||
},
|
||||
},
|
||||
statusTimeRanges,
|
||||
TIME_RANGES_WITH_NEVER,
|
||||
AVAILABILITY_STATUS,
|
||||
safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
|
||||
i18n: {
|
||||
statusMessagePlaceholder: s__(`SetStatusModal|What's your status?`),
|
||||
|
@ -153,14 +143,11 @@ export default {
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<input :value="emoji" class="js-status-emoji-field" type="hidden" name="user[status][emoji]" />
|
||||
<gl-form-input-group class="gl-mb-5">
|
||||
<gl-form-input
|
||||
ref="statusMessageField"
|
||||
:value="message"
|
||||
:placeholder="$options.i18n.statusMessagePlaceholder"
|
||||
class="js-status-message-field"
|
||||
name="user[status][message]"
|
||||
@keyup="setDefaultEmoji"
|
||||
@input="$emit('message-input', $event)"
|
||||
@keyup.enter.prevent
|
||||
|
@ -216,28 +203,29 @@ export default {
|
|||
</template>
|
||||
</gl-form-checkbox>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="gl-display-flex gl-align-items-baseline">
|
||||
<span class="gl-mr-3">{{ $options.i18n.clearStatusAfterDropdownLabel }}</span>
|
||||
<gl-dropdown :text="clearStatusAfter.label" data-testid="clear-status-at-dropdown">
|
||||
<gl-dropdown-item
|
||||
v-for="after in $options.statusTimeRanges"
|
||||
:key="after.name"
|
||||
:data-testid="after.name"
|
||||
@click="$emit('clear-status-after-click', after)"
|
||||
>{{ after.label }}</gl-dropdown-item
|
||||
>
|
||||
</gl-dropdown>
|
||||
</div>
|
||||
<p
|
||||
v-if="currentClearStatusAfter.length"
|
||||
class="gl-mt-3 gl-text-gray-400 gl-font-sm"
|
||||
data-testid="clear-status-at-message"
|
||||
<gl-form-group :label="$options.i18n.clearStatusAfterDropdownLabel" class="gl-mb-0">
|
||||
<gl-dropdown
|
||||
block
|
||||
:text="clearStatusAfter.label"
|
||||
data-testid="clear-status-at-dropdown"
|
||||
toggle-class="gl-mb-0 gl-form-input-md"
|
||||
>
|
||||
<gl-sprintf :message="$options.i18n.clearStatusAfterMessage">
|
||||
<template #date>{{ currentClearStatusAfter }}</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
</div>
|
||||
<gl-dropdown-item
|
||||
v-for="after in $options.TIME_RANGES_WITH_NEVER"
|
||||
:key="after.name"
|
||||
:data-testid="after.name"
|
||||
@click="$emit('clear-status-after-click', after)"
|
||||
>{{ after.label }}</gl-dropdown-item
|
||||
>
|
||||
</gl-dropdown>
|
||||
|
||||
<template v-if="currentClearStatusAfter.length" #description>
|
||||
<span data-testid="clear-status-at-message">
|
||||
<gl-sprintf :message="$options.i18n.clearStatusAfterMessage">
|
||||
<template #date>{{ currentClearStatusAfter }}</template>
|
||||
</gl-sprintf>
|
||||
</span>
|
||||
</template>
|
||||
</gl-form-group>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -3,28 +3,15 @@ import { GlToast, GlTooltipDirective, GlSafeHtmlDirective, GlModal } from '@gitl
|
|||
import Vue from 'vue';
|
||||
import createFlash from '~/flash';
|
||||
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
|
||||
import { __, s__ } from '~/locale';
|
||||
import { s__ } from '~/locale';
|
||||
import { updateUserStatus } from '~/rest_api';
|
||||
import { timeRanges } from '~/vue_shared/constants';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import { isUserBusy } from './utils';
|
||||
import { NEVER_TIME_RANGE, AVAILABILITY_STATUS } from './constants';
|
||||
import SetStatusForm from './set_status_form.vue';
|
||||
|
||||
export const AVAILABILITY_STATUS = {
|
||||
BUSY: 'busy',
|
||||
NOT_SET: 'not_set',
|
||||
};
|
||||
|
||||
Vue.use(GlToast);
|
||||
|
||||
const statusTimeRanges = [
|
||||
{
|
||||
label: __('Never'),
|
||||
name: 'never',
|
||||
},
|
||||
...timeRanges,
|
||||
];
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlModal,
|
||||
|
@ -67,7 +54,7 @@ export default {
|
|||
message: this.currentMessage,
|
||||
modalId: 'set-user-status-modal',
|
||||
availability: isUserBusy(this.currentAvailability),
|
||||
clearStatusAfter: statusTimeRanges[0],
|
||||
clearStatusAfter: NEVER_TIME_RANGE,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
|
@ -91,7 +78,7 @@ export default {
|
|||
message,
|
||||
availability: availability ? AVAILABILITY_STATUS.BUSY : AVAILABILITY_STATUS.NOT_SET,
|
||||
clearStatusAfter:
|
||||
clearStatusAfter.label === statusTimeRanges[0].label ? null : clearStatusAfter.shortcut,
|
||||
clearStatusAfter.label === NEVER_TIME_RANGE.label ? null : clearStatusAfter.shortcut,
|
||||
})
|
||||
.then(this.onUpdateSuccess)
|
||||
.catch(this.onUpdateFail);
|
||||
|
@ -123,7 +110,6 @@ export default {
|
|||
this.availability = value;
|
||||
},
|
||||
},
|
||||
statusTimeRanges,
|
||||
safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
|
||||
actionPrimary: { text: s__('SetStatusModal|Set status') },
|
||||
actionSecondary: { text: s__('SetStatusModal|Remove status') },
|
||||
|
|
|
@ -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>
|
|
@ -1,7 +1,4 @@
|
|||
export const AVAILABILITY_STATUS = {
|
||||
BUSY: 'busy',
|
||||
NOT_SET: 'not_set',
|
||||
};
|
||||
import { AVAILABILITY_STATUS } from './constants';
|
||||
|
||||
export const isUserBusy = (status = '') =>
|
||||
Boolean(status.length && status.toLowerCase().trim() === AVAILABILITY_STATUS.BUSY);
|
||||
|
|
|
@ -8,7 +8,7 @@ import { __, s__ } from '~/locale';
|
|||
import Tracking from '~/tracking';
|
||||
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
|
||||
import workItemQuery from '../graphql/work_item.query.graphql';
|
||||
import updateWorkItemWidgetsMutation from '../graphql/update_work_item_widgets.mutation.graphql';
|
||||
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
|
||||
import { i18n, TRACKING_CATEGORY_SHOW, WIDGET_TYPE_DESCRIPTION } from '../constants';
|
||||
|
||||
export default {
|
||||
|
@ -142,9 +142,9 @@ export default {
|
|||
this.track('updated_description');
|
||||
|
||||
const {
|
||||
data: { workItemUpdateWidgets },
|
||||
data: { workItemUpdate },
|
||||
} = await this.$apollo.mutate({
|
||||
mutation: updateWorkItemWidgetsMutation,
|
||||
mutation: updateWorkItemMutation,
|
||||
variables: {
|
||||
input: {
|
||||
id: this.workItem.id,
|
||||
|
@ -155,8 +155,8 @@ export default {
|
|||
},
|
||||
});
|
||||
|
||||
if (workItemUpdateWidgets.errors?.length) {
|
||||
throw new Error(workItemUpdateWidgets.errors[0]);
|
||||
if (workItemUpdate.errors?.length) {
|
||||
throw new Error(workItemUpdate.errors[0]);
|
||||
}
|
||||
|
||||
this.isEditing = false;
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
#import "./work_item.fragment.graphql"
|
||||
|
||||
mutation workItemUpdateWidgets($input: WorkItemUpdateWidgetsInput!) {
|
||||
workItemUpdateWidgets(input: $input) {
|
||||
workItem {
|
||||
...WorkItem
|
||||
}
|
||||
errors
|
||||
}
|
||||
}
|
|
@ -9,6 +9,10 @@ class Admin::RunnersController < Admin::ApplicationController
|
|||
push_frontend_feature_flag(:runner_list_stacked_layout_admin)
|
||||
end
|
||||
|
||||
before_action only: [:show] do
|
||||
push_frontend_feature_flag(:enforce_runner_token_expires_at)
|
||||
end
|
||||
|
||||
feature_category :runner
|
||||
urgency :low
|
||||
|
||||
|
@ -23,7 +27,7 @@ class Admin::RunnersController < Admin::ApplicationController
|
|||
end
|
||||
|
||||
def update
|
||||
if Ci::Runners::UpdateRunnerService.new(@runner).update(runner_params)
|
||||
if Ci::Runners::UpdateRunnerService.new(@runner).execute(runner_params).success?
|
||||
respond_to do |format|
|
||||
format.html { redirect_to edit_admin_runner_path(@runner) }
|
||||
end
|
||||
|
@ -40,7 +44,7 @@ class Admin::RunnersController < Admin::ApplicationController
|
|||
end
|
||||
|
||||
def resume
|
||||
if Ci::Runners::UpdateRunnerService.new(@runner).update(active: true)
|
||||
if Ci::Runners::UpdateRunnerService.new(@runner).execute(active: true).success?
|
||||
redirect_to admin_runners_path, notice: _('Runner was successfully updated.')
|
||||
else
|
||||
redirect_to admin_runners_path, alert: _('Runner was not updated.')
|
||||
|
@ -48,7 +52,7 @@ class Admin::RunnersController < Admin::ApplicationController
|
|||
end
|
||||
|
||||
def pause
|
||||
if Ci::Runners::UpdateRunnerService.new(@runner).update(active: false)
|
||||
if Ci::Runners::UpdateRunnerService.new(@runner).execute(active: false).success?
|
||||
redirect_to admin_runners_path, notice: _('Runner was successfully updated.')
|
||||
else
|
||||
redirect_to admin_runners_path, alert: _('Runner was not updated.')
|
||||
|
|
|
@ -8,6 +8,10 @@ class Groups::RunnersController < Groups::ApplicationController
|
|||
push_frontend_feature_flag(:runner_list_stacked_layout, @group)
|
||||
end
|
||||
|
||||
before_action only: [:show] do
|
||||
push_frontend_feature_flag(:enforce_runner_token_expires_at)
|
||||
end
|
||||
|
||||
feature_category :runner
|
||||
urgency :low
|
||||
|
||||
|
@ -26,7 +30,7 @@ class Groups::RunnersController < Groups::ApplicationController
|
|||
end
|
||||
|
||||
def update
|
||||
if Ci::Runners::UpdateRunnerService.new(@runner).update(runner_params)
|
||||
if Ci::Runners::UpdateRunnerService.new(@runner).execute(runner_params).success?
|
||||
redirect_to group_runner_path(@group, @runner), notice: _('Runner was successfully updated.')
|
||||
else
|
||||
render 'edit'
|
||||
|
|
|
@ -137,7 +137,7 @@ class ProfilesController < Profiles::ApplicationController
|
|||
:pronouns,
|
||||
:pronunciation,
|
||||
:validation_password,
|
||||
status: [:emoji, :message, :availability]
|
||||
status: [:emoji, :message, :availability, :clear_status_after]
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ class Projects::RunnersController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def update
|
||||
if Ci::Runners::UpdateRunnerService.new(@runner).update(runner_params)
|
||||
if Ci::Runners::UpdateRunnerService.new(@runner).execute(runner_params).success?
|
||||
redirect_to project_runner_path(@project, @runner), notice: _('Runner was successfully updated.')
|
||||
else
|
||||
render 'edit'
|
||||
|
@ -31,7 +31,7 @@ class Projects::RunnersController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def resume
|
||||
if Ci::Runners::UpdateRunnerService.new(@runner).update(active: true)
|
||||
if Ci::Runners::UpdateRunnerService.new(@runner).execute(active: true).success?
|
||||
redirect_to project_runners_path(@project), notice: _('Runner was successfully updated.')
|
||||
else
|
||||
redirect_to project_runners_path(@project), alert: _('Runner was not updated.')
|
||||
|
@ -39,7 +39,7 @@ class Projects::RunnersController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def pause
|
||||
if Ci::Runners::UpdateRunnerService.new(@runner).update(active: false)
|
||||
if Ci::Runners::UpdateRunnerService.new(@runner).execute(active: false).success?
|
||||
redirect_to project_runners_path(@project), notice: _('Runner was successfully updated.')
|
||||
else
|
||||
redirect_to project_runners_path(@project), alert: _('Runner was not updated.')
|
||||
|
|
|
@ -59,9 +59,8 @@ module Mutations
|
|||
def resolve(id:, **runner_attrs)
|
||||
runner = authorized_find!(id)
|
||||
|
||||
unless ::Ci::Runners::UpdateRunnerService.new(runner).update(runner_attrs)
|
||||
return { runner: nil, errors: runner.errors.full_messages }
|
||||
end
|
||||
result = ::Ci::Runners::UpdateRunnerService.new(runner).execute(runner_attrs)
|
||||
return { runner: nil, errors: result.errors } if result.error?
|
||||
|
||||
{ runner: runner, errors: [] }
|
||||
end
|
||||
|
|
|
@ -450,6 +450,14 @@ module ApplicationSettingsHelper
|
|||
end
|
||||
end
|
||||
|
||||
def runner_token_expiration_interval_attributes
|
||||
{
|
||||
instance_runner_token_expiration_interval: @application_setting.runner_token_expiration_interval,
|
||||
group_runner_token_expiration_interval: @application_setting.group_runner_token_expiration_interval,
|
||||
project_runner_token_expiration_interval: @application_setting.project_runner_token_expiration_interval
|
||||
}
|
||||
end
|
||||
|
||||
def external_authorization_service_attributes
|
||||
[
|
||||
:external_auth_client_cert,
|
||||
|
|
|
@ -29,6 +29,10 @@ class UserStatus < ApplicationRecord
|
|||
|
||||
cache_markdown_field :message, pipeline: :emoji
|
||||
|
||||
def clear_status_after
|
||||
clear_status_at
|
||||
end
|
||||
|
||||
def clear_status_after=(value)
|
||||
self.clear_status_at = CLEAR_STATUS_QUICK_OPTIONS[value]&.from_now
|
||||
end
|
||||
|
|
|
@ -9,11 +9,14 @@ module Ci
|
|||
@runner = runner
|
||||
end
|
||||
|
||||
def update(params)
|
||||
def execute(params)
|
||||
params[:active] = !params.delete(:paused) if params.include?(:paused)
|
||||
|
||||
runner.update(params).tap do |updated|
|
||||
runner.tick_runner_queue if updated
|
||||
if runner.update(params)
|
||||
runner.tick_runner_queue
|
||||
ServiceResponse.success
|
||||
else
|
||||
ServiceResponse.error(message: runner.errors.full_messages)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,6 +14,13 @@ module SystemNotes
|
|||
# See also the discussion in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60700#note_612724683
|
||||
USE_COMMIT_DATE_FOR_CROSS_REFERENCE_NOTE = false
|
||||
|
||||
def self.issuable_events
|
||||
{
|
||||
review_requested: s_('IssuableEvents|requested review from'),
|
||||
review_request_removed: s_('IssuableEvents|removed review request for')
|
||||
}.freeze
|
||||
end
|
||||
|
||||
#
|
||||
# noteable_ref - Referenced noteable object
|
||||
#
|
||||
|
@ -115,8 +122,8 @@ module SystemNotes
|
|||
text_parts = []
|
||||
|
||||
Gitlab::I18n.with_default_locale do
|
||||
text_parts << "requested review from #{added_users.map(&:to_reference).to_sentence}" if added_users.any?
|
||||
text_parts << "removed review request for #{unassigned_users.map(&:to_reference).to_sentence}" if unassigned_users.any?
|
||||
text_parts << "#{self.class.issuable_events[:review_requested]} #{added_users.map(&:to_reference).to_sentence}" if added_users.any?
|
||||
text_parts << "#{self.class.issuable_events[:review_request_removed]} #{unassigned_users.map(&:to_reference).to_sentence}" if unassigned_users.any?
|
||||
end
|
||||
|
||||
body = text_parts.join(' and ')
|
||||
|
|
|
@ -53,6 +53,8 @@
|
|||
= link_to sprite_icon('question-o'), help_page_path('ci/pipelines/settings', anchor: 'specify-a-custom-cicd-configuration-file'), target: '_blank', rel: 'noopener noreferrer'
|
||||
.form-group
|
||||
= f.gitlab_ui_checkbox_component :suggest_pipeline_enabled, s_('AdminSettings|Enable pipeline suggestion banner'), help_text: s_('AdminSettings|Display a banner on merge requests in projects with no pipelines to initiate steps to add a .gitlab-ci.yml file.')
|
||||
- if Feature.enabled?(:enforce_runner_token_expires_at)
|
||||
#js-runner-token-expiration-intervals{ data: runner_token_expiration_interval_attributes }
|
||||
|
||||
= f.submit _('Save changes'), pajamas_button: true
|
||||
|
||||
|
|
|
@ -2,8 +2,6 @@
|
|||
- page_title s_("Profiles|Edit Profile")
|
||||
- @content_class = "limit-container-width" unless fluid_layout
|
||||
- gravatar_link = link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host
|
||||
- availability = availability_values
|
||||
- custom_emoji = @user.status&.customized?
|
||||
|
||||
= gitlab_ui_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user js-edit-user gl-mt-3 js-quick-submit gl-show-field-errors js-password-prompt-form', remote: true }, authenticity_token: true do |f|
|
||||
.row.js-search-settings-section
|
||||
|
@ -43,39 +41,12 @@
|
|||
%h4.gl-mt-0= s_("Profiles|Current status")
|
||||
%p= s_("Profiles|This emoji and message will appear on your profile and throughout the interface.")
|
||||
.col-lg-8
|
||||
= f.fields_for :status, @user.status do |status_form|
|
||||
- emoji_button = render Pajamas::ButtonComponent.new(button_options: { title: s_("Profiles|Add status emoji"),
|
||||
class: 'js-toggle-emoji-menu emoji-menu-toggle-button has-tooltip' } ) do
|
||||
- if custom_emoji
|
||||
= emoji_icon(@user.status.emoji, class: 'gl-mr-0!')
|
||||
%span#js-no-emoji-placeholder.no-emoji-placeholder{ class: ('hidden' if custom_emoji) }
|
||||
= sprite_icon('slight-smile', css_class: 'award-control-icon-neutral')
|
||||
= sprite_icon('smiley', css_class: 'award-control-icon-positive')
|
||||
= sprite_icon('smile', css_class: 'award-control-icon-super-positive')
|
||||
- reset_message_button = render Pajamas::ButtonComponent.new(icon: 'close',
|
||||
button_options: { id: 'js-clear-user-status-button',
|
||||
class: 'has-tooltip',
|
||||
title: s_("Profiles|Clear status") } )
|
||||
|
||||
= status_form.hidden_field :emoji, id: 'js-status-emoji-field'
|
||||
.form-group.gl-form-group
|
||||
= status_form.label :message, s_("Profiles|Your status")
|
||||
.input-group{ role: 'group' }
|
||||
.input-group-prepend
|
||||
= emoji_button
|
||||
= status_form.text_field :message,
|
||||
id: 'js-status-message-field',
|
||||
class: 'form-control gl-form-input input-lg',
|
||||
placeholder: s_("Profiles|What's your status?")
|
||||
.input-group-append
|
||||
= reset_message_button
|
||||
.form-group.gl-form-group
|
||||
= status_form.gitlab_ui_checkbox_component :availability,
|
||||
s_("Profiles|Busy"),
|
||||
help_text: s_('Profiles|An indicator appears next to your name and avatar.'),
|
||||
checkbox_options: { data: { testid: "user-availability-checkbox" } },
|
||||
checked_value: availability["busy"],
|
||||
unchecked_value: availability["not_set"]
|
||||
#js-user-profile-set-status-form
|
||||
= f.fields_for :status, @user.status do |status_form|
|
||||
= status_form.hidden_field :emoji, data: { js_name: 'emoji' }
|
||||
= status_form.hidden_field :message, data: { js_name: 'message' }
|
||||
= status_form.hidden_field :availability, data: { js_name: 'availability' }
|
||||
= status_form.hidden_field :clear_status_after, data: { js_name: 'clearStatusAfter' }
|
||||
.col-lg-12
|
||||
%hr
|
||||
.row.user-time-preferences.js-search-settings-section
|
||||
|
|
|
@ -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.
|
|
@ -912,3 +912,43 @@ To determine which runners need to be upgraded:
|
|||
- **Outdated - available**: Newer versions are available but upgrading is not critical.
|
||||
|
||||
1. Filter the list by status to view which individual runners need to be upgraded.
|
||||
|
||||
## Authentication token security
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30942) in GitLab 15.3 [with a flag](../../administration/feature_flags.md) named `enforce_runner_token_expires_at`. Disabled by default.
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to
|
||||
[enable the feature flag](../../administration/feature_flags.md) named `enforce_runner_token_expires_at`.
|
||||
On GitLab.com, this feature is not available.
|
||||
|
||||
Each runner has an [authentication token](../../api/runners.md#registration-and-authentication-tokens)
|
||||
to connect with the GitLab instance.
|
||||
|
||||
To help prevent the token from being compromised, you can have the
|
||||
token rotate automatically at specified intervals. When the tokens are rotated,
|
||||
they are updated for each runner, regardless of the runner's status (`online` or `offline`).
|
||||
|
||||
No manual intervention should be required, and no running jobs should be affected.
|
||||
|
||||
If you need to manually update the authentication token, you can run a
|
||||
command to [reset the token](https://docs.gitlab.com/runner/commands/#gitlab-runner-reset-token).
|
||||
|
||||
### Automatically rotate authentication tokens
|
||||
|
||||
You can specify an interval for authentication tokens to rotate.
|
||||
This rotation helps ensure the security of the tokens assigned to your runners.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- Ensure your runners are using [GitLab Runner 15.3 or later](https://docs.gitlab.com/runner/#gitlab-runner-versions).
|
||||
|
||||
To automatically rotate runner authentication tokens:
|
||||
|
||||
1. On the top bar, select **Menu > Admin**.
|
||||
1. On the left sidebar, select **Settings > CI/CD**.
|
||||
1. Expand **Continuous Integration and Deployment**
|
||||
1. Set a **Runners expiration** time for runners, leave empty for no expiration.
|
||||
1. Select **Save**.
|
||||
|
||||
Before the interval expires, runners automatically request a new authentication token.
|
||||
|
|
|
@ -99,7 +99,8 @@ artifacts:
|
|||
path: coverage/cobertura-coverage.xml
|
||||
```
|
||||
|
||||
The collected coverage report is uploaded to GitLab as an artifact.
|
||||
The collected coverage report is uploaded to GitLab as an artifact. You can use
|
||||
only one report per job.
|
||||
|
||||
GitLab can display the results of coverage report in the merge request
|
||||
[diff annotations](../testing/test_coverage_visualization.md).
|
||||
|
|
|
@ -72,3 +72,106 @@ end
|
|||
Public attributes should only be used if they are accessed outside of the class.
|
||||
There is not a strong opinion on what strategy is used when attributes are only
|
||||
accessed internally, as long as there is consistency in related code.
|
||||
|
||||
## Newlines style guide
|
||||
|
||||
This style guide recommends best practices for newlines in Ruby code.
|
||||
|
||||
### Rule: separate code with newlines only to group together related logic
|
||||
|
||||
```ruby
|
||||
# bad
|
||||
def method
|
||||
issue = Issue.new
|
||||
|
||||
issue.save
|
||||
|
||||
render json: issue
|
||||
end
|
||||
```
|
||||
|
||||
```ruby
|
||||
# good
|
||||
def method
|
||||
issue = Issue.new
|
||||
issue.save
|
||||
|
||||
render json: issue
|
||||
end
|
||||
```
|
||||
|
||||
### Rule: separate code and block with newlines
|
||||
|
||||
#### Newline before block
|
||||
|
||||
```ruby
|
||||
# bad
|
||||
def method
|
||||
issue = Issue.new
|
||||
if issue.save
|
||||
render json: issue
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```ruby
|
||||
# good
|
||||
def method
|
||||
issue = Issue.new
|
||||
|
||||
if issue.save
|
||||
render json: issue
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Rule: Newline after block
|
||||
|
||||
```ruby
|
||||
# bad
|
||||
def method
|
||||
if issue.save
|
||||
issue.send_email
|
||||
end
|
||||
render json: issue
|
||||
end
|
||||
```
|
||||
|
||||
```ruby
|
||||
# good
|
||||
def method
|
||||
if issue.save
|
||||
issue.send_email
|
||||
end
|
||||
|
||||
render json: issue
|
||||
end
|
||||
```
|
||||
|
||||
#### Exception: no need for newline when code block starts or ends right inside another code block
|
||||
|
||||
```ruby
|
||||
# bad
|
||||
def method
|
||||
|
||||
if issue
|
||||
|
||||
if issue.valid?
|
||||
issue.save
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
```
|
||||
|
||||
```ruby
|
||||
# good
|
||||
def method
|
||||
if issue
|
||||
if issue.valid?
|
||||
issue.save
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
|
|
@ -1,108 +1,11 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
redirect_to: 'backend/ruby_style_guide.md#newlines-style-guide'
|
||||
remove_date: '2022-12-15'
|
||||
---
|
||||
|
||||
# Newlines style guide
|
||||
This document was moved to [another location](backend/ruby_style_guide.md#newlines-style-guide).
|
||||
|
||||
This style guide recommends best practices for newlines in Ruby code.
|
||||
|
||||
## Rule: separate code with newlines only to group together related logic
|
||||
|
||||
```ruby
|
||||
# bad
|
||||
def method
|
||||
issue = Issue.new
|
||||
|
||||
issue.save
|
||||
|
||||
render json: issue
|
||||
end
|
||||
```
|
||||
|
||||
```ruby
|
||||
# good
|
||||
def method
|
||||
issue = Issue.new
|
||||
issue.save
|
||||
|
||||
render json: issue
|
||||
end
|
||||
```
|
||||
|
||||
## Rule: separate code and block with newlines
|
||||
|
||||
### Newline before block
|
||||
|
||||
```ruby
|
||||
# bad
|
||||
def method
|
||||
issue = Issue.new
|
||||
if issue.save
|
||||
render json: issue
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```ruby
|
||||
# good
|
||||
def method
|
||||
issue = Issue.new
|
||||
|
||||
if issue.save
|
||||
render json: issue
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Newline after block
|
||||
|
||||
```ruby
|
||||
# bad
|
||||
def method
|
||||
if issue.save
|
||||
issue.send_email
|
||||
end
|
||||
render json: issue
|
||||
end
|
||||
```
|
||||
|
||||
```ruby
|
||||
# good
|
||||
def method
|
||||
if issue.save
|
||||
issue.send_email
|
||||
end
|
||||
|
||||
render json: issue
|
||||
end
|
||||
```
|
||||
|
||||
### Exception: no need for newline when code block starts or ends right inside another code block
|
||||
|
||||
```ruby
|
||||
# bad
|
||||
def method
|
||||
|
||||
if issue
|
||||
|
||||
if issue.valid?
|
||||
issue.save
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
```
|
||||
|
||||
```ruby
|
||||
# good
|
||||
def method
|
||||
if issue
|
||||
if issue.valid?
|
||||
issue.save
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
<!-- This redirect file can be deleted after 2022-12-15. -->
|
||||
<!-- Redirects that point to other docs in the same project expire in three months. -->
|
||||
<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
|
||||
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->
|
||||
|
|
|
@ -188,6 +188,8 @@ Alternatively you can use the following on each spec run,
|
|||
bundle exec spring rspec some_spec.rb
|
||||
```
|
||||
|
||||
## RuboCop tasks
|
||||
|
||||
## Generate initial RuboCop TODO list
|
||||
|
||||
One way to generate the initial list is to run the Rake task `rubocop:todo:generate`:
|
||||
|
@ -209,6 +211,18 @@ Some shells require brackets to be escaped or quoted.
|
|||
See [Resolving RuboCop exceptions](contributing/style_guides.md#resolving-rubocop-exceptions)
|
||||
on how to proceed from here.
|
||||
|
||||
### Run RuboCop in graceful mode
|
||||
|
||||
You can run RuboCop in "graceful mode". This means all enabled cop rules are
|
||||
silenced which have "grace period" activated (via `Details: grace period`).
|
||||
|
||||
Run:
|
||||
|
||||
```shell
|
||||
bundle exec rake 'rubocop:check:graceful'
|
||||
bundle exec rake 'rubocop:check:graceful[Gitlab/NamespacedClass]'
|
||||
```
|
||||
|
||||
## Compile Frontend Assets
|
||||
|
||||
You shouldn't ever need to compile frontend assets manually in development, but
|
||||
|
|
|
@ -1,24 +1,11 @@
|
|||
---
|
||||
stage: Systems
|
||||
group: Distribution
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
comments: false
|
||||
description: Install a cloud-native version of GitLab
|
||||
type: index
|
||||
redirect_to: 'https://docs.gitlab.com/charts/'
|
||||
remove_date: '2023-09-09'
|
||||
---
|
||||
|
||||
# Cloud-native GitLab **(FREE SELF)**
|
||||
This document was moved to [another location](https://docs.gitlab.com/charts/).
|
||||
|
||||
A [cloud-native](https://gitlab.com/gitlab-org/build/CNG) version of GitLab is
|
||||
available for deployment on Kubernetes, OpenShift, and Kubernetes-compatible
|
||||
platforms. The following deployment methods are available:
|
||||
|
||||
- [GitLab Helm chart](https://docs.gitlab.com/charts/): A cloud-native version of GitLab
|
||||
and all of its components. Use this installation method if your infrastructure is built
|
||||
on Kubernetes and you're familiar with how it works. This method of deployment has different
|
||||
management, observability, and concepts than traditional deployments.
|
||||
- [GitLab Operator](https://docs.gitlab.com/operator/): An installation and management method
|
||||
that follows the
|
||||
[Kubernetes Operator pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/).
|
||||
Use the GitLab Operator to run GitLab in an
|
||||
[OpenShift](../openshift_and_gitlab/index.md) or another Kubernetes-compatible platform.
|
||||
<!-- This redirect file can be deleted after <2023-09-09>. -->
|
||||
<!-- Redirects that point to other docs in the same project expire in three months. -->
|
||||
<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
|
||||
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->
|
|
@ -49,6 +49,20 @@ sole discretion of GitLab Inc.
|
|||
|
||||
<div class="deprecation removal-160 breaking-change">
|
||||
|
||||
### Container Scanning variables that reference Docker
|
||||
|
||||
Planned removal: GitLab <span class="removal-milestone">16.0</span> (2023-05-22)
|
||||
|
||||
WARNING:
|
||||
This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
|
||||
Review the details carefully before upgrading.
|
||||
|
||||
All Container Scanning variables that are prefixed by `DOCKER_` in variable name are deprecated. This includes the `DOCKER_IMAGE`, `DOCKER_PASSWORD`, `DOCKER_USER`, and `DOCKERFILE_PATH` variables. Support for these variables will be removed in the GitLab 16.0 release. Use the [new variable names](https://docs.gitlab.com/ee/user/application_security/container_scanning/#available-cicd-variables) `CS_IMAGE`, `CS_REGISTRY_PASSWORD`, `CS_REGISTRY_USER`, and `CS_DOCKERFILE_PATH` in place of the deprecated names.
|
||||
|
||||
</div>
|
||||
|
||||
<div class="deprecation removal-160 breaking-change">
|
||||
|
||||
### Non-expiring access tokens
|
||||
|
||||
Planned removal: GitLab <span class="removal-milestone">16.0</span> (2023-05-22)
|
||||
|
|
|
@ -205,7 +205,7 @@ To set your current status:
|
|||
1. Select a value from the **Clear status after** dropdown list.
|
||||
1. Select **Set status**. Alternatively, you can select **Remove status** to remove your user status entirely.
|
||||
|
||||
You can also set your current status by [using the API](../../api/users.md#user-status).
|
||||
You can also set your current status from [your user settings](#access-your-user-settings) or by [using the API](../../api/users.md#user-status).
|
||||
|
||||
If you select the **Busy** checkbox, remember to clear it when you become available again.
|
||||
|
||||
|
|
|
@ -93,7 +93,7 @@ module API
|
|||
params[:active] = !params.delete(:paused) if params.include?(:paused)
|
||||
update_service = ::Ci::Runners::UpdateRunnerService.new(runner)
|
||||
|
||||
if update_service.update(declared_params(include_missing: false))
|
||||
if update_service.execute(declared_params(include_missing: false)).success?
|
||||
present runner, with: Entities::Ci::RunnerDetails, current_user: current_user
|
||||
else
|
||||
render_validation_error!(runner)
|
||||
|
|
|
@ -279,7 +279,7 @@ semgrep-sast:
|
|||
image:
|
||||
name: "$SAST_ANALYZER_IMAGE"
|
||||
variables:
|
||||
SERACH_MAX_DEPTH: 20
|
||||
SEARCH_MAX_DEPTH: 20
|
||||
SAST_ANALYZER_IMAGE_TAG: 3
|
||||
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/semgrep:$SAST_ANALYZER_IMAGE_TAG$SAST_IMAGE_SUFFIX"
|
||||
rules:
|
||||
|
|
|
@ -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
|
|
@ -45,6 +45,8 @@ module Gitlab
|
|||
Gitlab::GithubImport::Importer::Events::CrossReferenced
|
||||
when 'assigned', 'unassigned'
|
||||
Gitlab::GithubImport::Importer::Events::ChangedAssignee
|
||||
when 'review_requested', 'review_request_removed'
|
||||
Gitlab::GithubImport::Importer::Events::ChangedReviewer
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,7 +10,8 @@ module Gitlab
|
|||
attr_reader :attributes
|
||||
|
||||
expose_attribute :id, :actor, :event, :commit_id, :label_title, :old_title, :new_title,
|
||||
:milestone_title, :issue, :source, :assignee, :created_at
|
||||
:milestone_title, :issue, :source, :assignee, :review_requester,
|
||||
:requested_reviewer, :created_at
|
||||
|
||||
# attributes - A Hash containing the event details. The keys of this
|
||||
# Hash (and any nested hashes) must be symbols.
|
||||
|
@ -47,6 +48,8 @@ module Gitlab
|
|||
issue: event.issue&.to_h&.symbolize_keys,
|
||||
source: event.source,
|
||||
assignee: user_representation(event.assignee),
|
||||
requested_reviewer: user_representation(event.requested_reviewer),
|
||||
review_requester: user_representation(event.review_requester),
|
||||
created_at: event.created_at
|
||||
)
|
||||
end
|
||||
|
@ -56,6 +59,8 @@ module Gitlab
|
|||
hash = Representation.symbolize_hash(raw_hash)
|
||||
hash[:actor] = user_representation(hash[:actor], source: :hash)
|
||||
hash[:assignee] = user_representation(hash[:assignee], source: :hash)
|
||||
hash[:requested_reviewer] = user_representation(hash[:requested_reviewer], source: :hash)
|
||||
hash[:review_requester] = user_representation(hash[:review_requester], source: :hash)
|
||||
|
||||
new(hash)
|
||||
end
|
||||
|
|
|
@ -45,6 +45,10 @@ module Gitlab
|
|||
object&.actor
|
||||
when :assignee
|
||||
object&.assignee
|
||||
when :requested_reviewer
|
||||
object&.requested_reviewer
|
||||
when :review_requester
|
||||
object&.review_requester
|
||||
else
|
||||
object&.author
|
||||
end
|
||||
|
|
|
@ -6,6 +6,19 @@ unless Rails.env.production?
|
|||
RuboCop::RakeTask.new
|
||||
|
||||
namespace :rubocop do
|
||||
namespace :check do
|
||||
desc 'Run RuboCop check gracefully'
|
||||
task :graceful do |_task, args|
|
||||
require_relative '../../rubocop/check_graceful_task'
|
||||
|
||||
# Don't reveal TODOs in this run.
|
||||
ENV.delete('REVEAL_RUBOCOP_TODO')
|
||||
|
||||
result = RuboCop::CheckGracefulTask.new($stdout).run(args.extras)
|
||||
exit result if result.nonzero?
|
||||
end
|
||||
end
|
||||
|
||||
namespace :todo do
|
||||
desc 'Generate RuboCop todos'
|
||||
task :generate do |_task, args|
|
||||
|
|
|
@ -2766,9 +2766,15 @@ msgstr ""
|
|||
msgid "AdminSettings|Git abuse rate limit"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminSettings|Group runners expiration"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminSettings|I have read and agree to the Let's Encrypt %{link_start}Terms of Service%{link_end} (PDF)."
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminSettings|If no unit is written, it defaults to seconds. For example, these are all equivalent: %{oneDayInSeconds}, %{oneDayInHoursHumanReadable}, or %{oneDayHumanReadable}. Minimum value is two hours. %{linkStart}Learn more.%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminSettings|If not specified at the group or instance level, the default is %{default_initial_branch_name}. Does not affect existing repositories."
|
||||
msgstr ""
|
||||
|
||||
|
@ -2781,6 +2787,9 @@ msgstr ""
|
|||
msgid "AdminSettings|Inactive project deletion"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminSettings|Instance runners expiration"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminSettings|Keep the latest artifacts for all jobs in the latest successful pipelines"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2841,6 +2850,9 @@ msgstr ""
|
|||
msgid "AdminSettings|Project export"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminSettings|Project runners expiration"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminSettings|Protect CI/CD variables by default"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2892,6 +2904,15 @@ msgstr ""
|
|||
msgid "AdminSettings|Set limit to 0 to disable it."
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminSettings|Set the expiration time of authentication tokens of newly registered group runners."
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminSettings|Set the expiration time of authentication tokens of newly registered instance runners. Authentication tokens are automatically reset at these intervals."
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminSettings|Set the expiration time of authentication tokens of newly registered project runners."
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminSettings|Set the initial name and protections for the default branch of new repositories created in the instance."
|
||||
msgstr ""
|
||||
|
||||
|
@ -16080,9 +16101,6 @@ msgstr ""
|
|||
msgid "Failed to load deploy keys."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to load emoji list."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to load error details from Sentry."
|
||||
msgstr ""
|
||||
|
||||
|
@ -21928,6 +21946,12 @@ msgstr ""
|
|||
msgid "Is using seat"
|
||||
msgstr ""
|
||||
|
||||
msgid "IssuableEvents|removed review request for"
|
||||
msgstr ""
|
||||
|
||||
msgid "IssuableEvents|requested review from"
|
||||
msgstr ""
|
||||
|
||||
msgid "IssuableStatus|%{wi_type} created %{created_at} by "
|
||||
msgstr ""
|
||||
|
||||
|
@ -30166,15 +30190,9 @@ msgstr ""
|
|||
msgid "Profiles|Add key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|Add status emoji"
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|An error occurred while updating your username, please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|An indicator appears next to your name and avatar."
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|Avatar cropper"
|
||||
msgstr ""
|
||||
|
||||
|
@ -30187,9 +30205,6 @@ msgstr ""
|
|||
msgid "Profiles|Bio"
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|Busy"
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|Change username"
|
||||
msgstr ""
|
||||
|
||||
|
@ -30205,9 +30220,6 @@ msgstr ""
|
|||
msgid "Profiles|City, country"
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|Clear status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|Commit email"
|
||||
msgstr ""
|
||||
|
||||
|
@ -30457,9 +30469,6 @@ msgstr ""
|
|||
msgid "Profiles|Website url"
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|What's your status?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|Who you represent or work for."
|
||||
msgstr ""
|
||||
|
||||
|
@ -30505,9 +30514,6 @@ msgstr ""
|
|||
msgid "Profiles|Your name was automatically set based on your %{provider_label} account, so people you know can recognize you."
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|Your status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|https://website.com"
|
||||
msgstr ""
|
||||
|
||||
|
@ -34124,6 +34130,9 @@ msgstr ""
|
|||
msgid "Runners|Never contacted:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Never expires"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|New group runners view"
|
||||
msgstr ""
|
||||
|
||||
|
@ -34219,6 +34228,12 @@ msgstr ""
|
|||
msgid "Runners|Runner assigned to project."
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Runner authentication token expiration"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Runner authentication tokens will expire based on a set interval. They will automatically rotate once expired."
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Runner cannot be deleted, please contact your administrator"
|
||||
msgstr ""
|
||||
|
||||
|
@ -34350,6 +34365,9 @@ msgstr ""
|
|||
msgid "Runners|To register them, go to the %{link_start}group's Runners page%{link_end}."
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Token expiry"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Up to date"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -10,8 +10,8 @@ module Gitlab
|
|||
text_field :activation_code
|
||||
button :activate
|
||||
label :terms_of_services, text: /I agree that/
|
||||
link :remove_license, 'data-testid': 'license-remove-action'
|
||||
button :confirm_ok_button
|
||||
button :remove_license
|
||||
button :confirm_remove_license
|
||||
p :plan
|
||||
p :started
|
||||
p :name
|
||||
|
@ -30,6 +30,11 @@ module Gitlab
|
|||
terms_of_services_element.click # workaround for hidden checkbox
|
||||
end
|
||||
|
||||
def remove_license_file
|
||||
remove_license
|
||||
confirm_remove_license
|
||||
end
|
||||
|
||||
# Checks if a subscription record exists in subscription history table
|
||||
#
|
||||
# @param plan [Hash] Name of the plan
|
||||
|
|
|
@ -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
|
|
@ -1,8 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'formatter/graceful_formatter'
|
||||
|
||||
module RuboCop
|
||||
class CopTodo
|
||||
attr_accessor :previously_disabled
|
||||
attr_accessor :previously_disabled, :grace_period
|
||||
|
||||
attr_reader :cop_name, :files, :offense_count
|
||||
|
||||
|
@ -12,6 +14,7 @@ module RuboCop
|
|||
@offense_count = 0
|
||||
@cop_class = self.class.find_cop_by_name(cop_name)
|
||||
@previously_disabled = false
|
||||
@grace_period = false
|
||||
end
|
||||
|
||||
def record(file, offense_count)
|
||||
|
@ -35,6 +38,7 @@ module RuboCop
|
|||
yaml << ' Enabled: false'
|
||||
end
|
||||
|
||||
yaml << " #{RuboCop::Formatter::GracefulFormatter.grace_period_key_value}" if grace_period
|
||||
yaml << ' Exclude:'
|
||||
yaml.concat files.sort.map { |file| " - '#{file}'" }
|
||||
yaml << ''
|
||||
|
|
|
@ -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
|
|
@ -6,6 +6,7 @@ require 'yaml'
|
|||
|
||||
require_relative '../todo_dir'
|
||||
require_relative '../cop_todo'
|
||||
require_relative '../formatter/graceful_formatter'
|
||||
|
||||
module RuboCop
|
||||
module Formatter
|
||||
|
@ -47,6 +48,8 @@ module RuboCop
|
|||
def finished(_inspected_files)
|
||||
@todos.values.sort_by(&:cop_name).each do |todo|
|
||||
todo.previously_disabled = previously_disabled?(todo)
|
||||
todo.grace_period = grace_period?(todo)
|
||||
validate_todo!(todo)
|
||||
path = @todo_dir.write(todo.cop_name, todo.to_yaml)
|
||||
|
||||
output.puts "Written to #{relative_path(path)}\n"
|
||||
|
@ -79,16 +82,31 @@ module RuboCop
|
|||
raise "Multiple configurations found for cops:\n#{list}\n"
|
||||
end
|
||||
|
||||
def previously_disabled?(todo)
|
||||
def config_for(todo)
|
||||
cop_name = todo.cop_name
|
||||
|
||||
config = @config_old_todo_yml[cop_name] ||
|
||||
@config_inspect_todo_dir[cop_name] || {}
|
||||
@config_old_todo_yml[cop_name] || @config_inspect_todo_dir[cop_name] || {}
|
||||
end
|
||||
|
||||
def previously_disabled?(todo)
|
||||
config = config_for(todo)
|
||||
return false if config.empty?
|
||||
|
||||
config['Enabled'] == false
|
||||
end
|
||||
|
||||
def grace_period?(todo)
|
||||
config = config_for(todo)
|
||||
|
||||
GracefulFormatter.grace_period?(todo.cop_name, config)
|
||||
end
|
||||
|
||||
def validate_todo!(todo)
|
||||
return unless todo.previously_disabled && todo.grace_period
|
||||
|
||||
raise "#{todo.cop_name}: Cop must be enabled to use `#{GracefulFormatter.grace_period_key_value}`."
|
||||
end
|
||||
|
||||
def load_config_inspect_todo_dir
|
||||
@todo_dir.list_inspect.each_with_object({}) do |path, combined|
|
||||
config = YAML.load_file(path)
|
||||
|
|
|
@ -74,7 +74,7 @@ RSpec.describe Admin::RunnersController do
|
|||
context 'with update succeeding' do
|
||||
before do
|
||||
expect_next_instance_of(Ci::Runners::UpdateRunnerService, runner) do |service|
|
||||
expect(service).to receive(:update).with(anything).and_call_original
|
||||
expect(service).to receive(:execute).with(anything).and_call_original
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -91,7 +91,7 @@ RSpec.describe Admin::RunnersController do
|
|||
context 'with update failing' do
|
||||
before do
|
||||
expect_next_instance_of(Ci::Runners::UpdateRunnerService, runner) do |service|
|
||||
expect(service).to receive(:update).with(anything).and_return(false)
|
||||
expect(service).to receive(:execute).with(anything).and_return(ServiceResponse.error(message: 'failure'))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -82,13 +82,17 @@ RSpec.describe ProfilesController, :request_store do
|
|||
expect(ldap_user.location).to eq('City, Country')
|
||||
end
|
||||
|
||||
it 'allows setting a user status' do
|
||||
it 'allows setting a user status', :freeze_time do
|
||||
sign_in(user)
|
||||
|
||||
put :update, params: { user: { status: { message: 'Working hard!', availability: 'busy' } } }
|
||||
put(
|
||||
:update,
|
||||
params: { user: { status: { message: 'Working hard!', availability: 'busy', clear_status_after: '8_hours' } } }
|
||||
)
|
||||
|
||||
expect(user.reload.status.message).to eq('Working hard!')
|
||||
expect(user.reload.status.availability).to eq('busy')
|
||||
expect(user.reload.status.clear_status_after).to eq(8.hours.from_now)
|
||||
expect(response).to have_gitlab_http_status(:found)
|
||||
end
|
||||
|
||||
|
|
|
@ -180,7 +180,7 @@ RSpec.describe 'User edit profile' do
|
|||
end
|
||||
|
||||
it 'adds emoji to user status' do
|
||||
emoji = 'biohazard'
|
||||
emoji = 'basketball'
|
||||
select_emoji(emoji)
|
||||
submit_settings
|
||||
|
||||
|
@ -193,7 +193,7 @@ RSpec.describe 'User edit profile' do
|
|||
|
||||
it 'adds message to user status' do
|
||||
message = 'I have something to say'
|
||||
fill_in 'js-status-message-field', with: message
|
||||
fill_in s_("SetStatusModal|What's your status?"), with: message
|
||||
submit_settings
|
||||
|
||||
visit_user
|
||||
|
@ -208,7 +208,7 @@ RSpec.describe 'User edit profile' do
|
|||
emoji = '8ball'
|
||||
message = 'Playing outside'
|
||||
select_emoji(emoji)
|
||||
fill_in 'js-status-message-field', with: message
|
||||
fill_in s_("SetStatusModal|What's your status?"), with: message
|
||||
submit_settings
|
||||
|
||||
visit_user
|
||||
|
@ -230,7 +230,7 @@ RSpec.describe 'User edit profile' do
|
|||
end
|
||||
|
||||
visit(profile_path)
|
||||
click_button 'js-clear-user-status-button'
|
||||
click_button s_('SetStatusModal|Clear status')
|
||||
submit_settings
|
||||
|
||||
visit_user
|
||||
|
@ -240,9 +240,9 @@ RSpec.describe 'User edit profile' do
|
|||
|
||||
it 'displays a default emoji if only message is entered' do
|
||||
message = 'a status without emoji'
|
||||
fill_in 'js-status-message-field', with: message
|
||||
fill_in s_("SetStatusModal|What's your status?"), with: message
|
||||
|
||||
within('.js-toggle-emoji-menu') do
|
||||
within('.emoji-menu-toggle-button') do
|
||||
expect(page).to have_emoji('speech_balloon')
|
||||
end
|
||||
end
|
||||
|
@ -406,7 +406,7 @@ RSpec.describe 'User edit profile' do
|
|||
it 'adds message to user status' do
|
||||
message = 'I have something to say'
|
||||
open_user_status_modal
|
||||
find('.js-status-message-field').native.send_keys(message)
|
||||
find_field(s_("SetStatusModal|What's your status?")).native.send_keys(message)
|
||||
set_user_status_in_modal
|
||||
|
||||
visit_user
|
||||
|
@ -422,7 +422,7 @@ RSpec.describe 'User edit profile' do
|
|||
message = 'Playing outside'
|
||||
open_user_status_modal
|
||||
select_emoji(emoji, true)
|
||||
find('.js-status-message-field').native.send_keys(message)
|
||||
find_field(s_("SetStatusModal|What's your status?")).native.send_keys(message)
|
||||
set_user_status_in_modal
|
||||
|
||||
visit_user
|
||||
|
@ -446,7 +446,7 @@ RSpec.describe 'User edit profile' do
|
|||
|
||||
open_edit_status_modal
|
||||
|
||||
find('.js-clear-user-status-button').click
|
||||
click_button s_('SetStatusModal|Clear status')
|
||||
set_user_status_in_modal
|
||||
|
||||
visit_user
|
||||
|
@ -491,7 +491,7 @@ RSpec.describe 'User edit profile' do
|
|||
it 'displays a default emoji if only message is entered' do
|
||||
message = 'a status without emoji'
|
||||
open_user_status_modal
|
||||
find('.js-status-message-field').native.send_keys(message)
|
||||
find_field(s_("SetStatusModal|What's your status?")).native.send_keys(message)
|
||||
|
||||
expect(page).to have_emoji('speech_balloon')
|
||||
end
|
||||
|
|
|
@ -19,10 +19,13 @@ import { createWrapper, ErrorWrapper } from '@vue/test-utils';
|
|||
* @returns Wrapper
|
||||
*/
|
||||
export const findDd = (dtLabel, wrapper) => {
|
||||
const dt = wrapper.findByText(dtLabel).element;
|
||||
const dd = dt.nextElementSibling;
|
||||
if (dt.tagName === 'DT' && dd.tagName === 'DD') {
|
||||
return createWrapper(dd, {});
|
||||
const dtw = wrapper.findByText(dtLabel);
|
||||
if (dtw.exists()) {
|
||||
const dt = dtw.element;
|
||||
const dd = dt.nextElementSibling;
|
||||
if (dt.tagName === 'DT' && dd.tagName === 'DD') {
|
||||
return createWrapper(dd, {});
|
||||
}
|
||||
}
|
||||
return ErrorWrapper(dtLabel);
|
||||
return new ErrorWrapper(dtLabel);
|
||||
};
|
||||
|
|
|
@ -30,19 +30,19 @@ describe('Emoji category component', () => {
|
|||
// eslint-disable-next-line no-restricted-syntax
|
||||
await wrapper.setData({ renderGroup: true });
|
||||
|
||||
expect(wrapper.find(EmojiGroup).attributes('rendergroup')).toBe('true');
|
||||
expect(wrapper.findComponent(EmojiGroup).attributes('rendergroup')).toBe('true');
|
||||
});
|
||||
|
||||
it('renders group on appear', async () => {
|
||||
wrapper.find(GlIntersectionObserver).vm.$emit('appear');
|
||||
wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.find(EmojiGroup).attributes('rendergroup')).toBe('true');
|
||||
expect(wrapper.findComponent(EmojiGroup).attributes('rendergroup')).toBe('true');
|
||||
});
|
||||
|
||||
it('emits appear event on appear', async () => {
|
||||
wrapper.find(GlIntersectionObserver).vm.$emit('appear');
|
||||
wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
|
||||
|
||||
await nextTick();
|
||||
|
||||
|
|
|
@ -76,8 +76,8 @@ describe('error tracking settings app', () => {
|
|||
|
||||
describe('section', () => {
|
||||
it('renders the form and dropdown', () => {
|
||||
expect(wrapper.find(ErrorTrackingForm).exists()).toBe(true);
|
||||
expect(wrapper.find(ProjectDropdown).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(ErrorTrackingForm).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(ProjectDropdown).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders the Save Changes button', () => {
|
||||
|
|
|
@ -42,7 +42,7 @@ describe('error tracking settings project dropdown', () => {
|
|||
describe('empty project list', () => {
|
||||
it('renders the dropdown', () => {
|
||||
expect(wrapper.find('#project-dropdown').exists()).toBe(true);
|
||||
expect(wrapper.find(GlDropdown).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(GlDropdown).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('shows helper text', () => {
|
||||
|
@ -57,8 +57,8 @@ describe('error tracking settings project dropdown', () => {
|
|||
});
|
||||
|
||||
it('does not contain any dropdown items', () => {
|
||||
expect(wrapper.find(GlDropdownItem).exists()).toBe(false);
|
||||
expect(wrapper.find(GlDropdown).props('text')).toBe('No projects available');
|
||||
expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(false);
|
||||
expect(wrapper.findComponent(GlDropdown).props('text')).toBe('No projects available');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -71,11 +71,11 @@ describe('error tracking settings project dropdown', () => {
|
|||
|
||||
it('renders the dropdown', () => {
|
||||
expect(wrapper.find('#project-dropdown').exists()).toBe(true);
|
||||
expect(wrapper.find(GlDropdown).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(GlDropdown).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('contains a number of dropdown items', () => {
|
||||
expect(wrapper.find(GlDropdownItem).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(true);
|
||||
expect(wrapper.findAll(GlDropdownItem).length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,9 +6,9 @@ import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered
|
|||
describe('Recent Searches Dropdown Content', () => {
|
||||
let wrapper;
|
||||
|
||||
const findLocalStorageNote = () => wrapper.find({ ref: 'localStorageNote' });
|
||||
const findLocalStorageNote = () => wrapper.findComponent({ ref: 'localStorageNote' });
|
||||
const findDropdownItems = () => wrapper.findAll({ ref: 'dropdownItem' });
|
||||
const findDropdownNote = () => wrapper.find({ ref: 'dropdownNote' });
|
||||
const findDropdownNote = () => wrapper.findComponent({ ref: 'dropdownNote' });
|
||||
|
||||
const createComponent = (props) => {
|
||||
wrapper = shallowMount(RecentSearchesDropdownContent, {
|
||||
|
@ -94,7 +94,7 @@ describe('Recent Searches Dropdown Content', () => {
|
|||
});
|
||||
|
||||
it('emits requestClearRecentSearches on Clear resent searches button', () => {
|
||||
wrapper.find({ ref: 'clearButton' }).trigger('click');
|
||||
wrapper.findComponent({ ref: 'clearButton' }).trigger('click');
|
||||
|
||||
expect(onRequestClearRecentSearchesSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
|
|
@ -16,10 +16,10 @@ describe('FrequentItemsListItemComponent', () => {
|
|||
let trackingSpy;
|
||||
let store;
|
||||
|
||||
const findTitle = () => wrapper.find({ ref: 'frequentItemsItemTitle' });
|
||||
const findTitle = () => wrapper.findComponent({ ref: 'frequentItemsItemTitle' });
|
||||
const findAvatar = () => wrapper.findComponent(ProjectAvatar);
|
||||
const findAllTitles = () => wrapper.findAll({ ref: 'frequentItemsItemTitle' });
|
||||
const findNamespace = () => wrapper.find({ ref: 'frequentItemsItemNamespace' });
|
||||
const findNamespace = () => wrapper.findComponent({ ref: 'frequentItemsItemNamespace' });
|
||||
const findAllButtons = () => wrapper.findAllComponents(GlButton);
|
||||
const findAllNamespace = () => wrapper.findAll({ ref: 'frequentItemsItemNamespace' });
|
||||
const findAllAvatars = () => wrapper.findAllComponents(ProjectAvatar);
|
||||
|
|
|
@ -23,7 +23,7 @@ describe('FrequentItemsSearchInputComponent', () => {
|
|||
},
|
||||
});
|
||||
|
||||
const findSearchBoxByType = () => wrapper.find(GlSearchBoxByType);
|
||||
const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType);
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore();
|
||||
|
|
|
@ -42,7 +42,7 @@ describe('Import entities group dropdown component', () => {
|
|||
createComponent({ namespaces });
|
||||
|
||||
namespacesTracker.mockReset();
|
||||
wrapper.find(GlSearchBoxByType).vm.$emit('input', 'match');
|
||||
wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'match');
|
||||
|
||||
await nextTick();
|
||||
|
||||
|
|
|
@ -99,7 +99,7 @@ describe('import table', () => {
|
|||
});
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not renders loading icon when request is completed', async () => {
|
||||
|
@ -108,7 +108,7 @@ describe('import table', () => {
|
|||
});
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
|
||||
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -123,7 +123,7 @@ describe('import table', () => {
|
|||
});
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.find(GlEmptyState).props().title).toBe(i18n.NO_GROUPS_FOUND);
|
||||
expect(wrapper.findComponent(GlEmptyState).props().title).toBe(i18n.NO_GROUPS_FOUND);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -268,7 +268,7 @@ describe('import table', () => {
|
|||
});
|
||||
|
||||
it('correctly passes pagination info from query', () => {
|
||||
expect(wrapper.find(PaginationLinks).props().pageInfo).toStrictEqual(FAKE_PAGE_INFO);
|
||||
expect(wrapper.findComponent(PaginationLinks).props().pageInfo).toStrictEqual(FAKE_PAGE_INFO);
|
||||
});
|
||||
|
||||
it('renders pagination dropdown', () => {
|
||||
|
@ -293,7 +293,7 @@ describe('import table', () => {
|
|||
|
||||
it('updates page when page change is requested', async () => {
|
||||
const REQUESTED_PAGE = 2;
|
||||
wrapper.find(PaginationLinks).props().change(REQUESTED_PAGE);
|
||||
wrapper.findComponent(PaginationLinks).props().change(REQUESTED_PAGE);
|
||||
|
||||
await waitForPromises();
|
||||
expect(bulkImportSourceGroupsQueryMock).toHaveBeenCalledWith(
|
||||
|
@ -316,7 +316,7 @@ describe('import table', () => {
|
|||
},
|
||||
versionValidation: FAKE_VERSION_VALIDATION,
|
||||
});
|
||||
wrapper.find(PaginationLinks).props().change(REQUESTED_PAGE);
|
||||
wrapper.findComponent(PaginationLinks).props().change(REQUESTED_PAGE);
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.text()).toContain('Showing 21-21 of 38 groups that you own from');
|
||||
|
@ -539,8 +539,8 @@ describe('import table', () => {
|
|||
});
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.find(GlAlert).exists()).toBe(true);
|
||||
expect(wrapper.find(GlAlert).text()).toContain('projects (require v14.8.0)');
|
||||
expect(wrapper.findComponent(GlAlert).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(GlAlert).text()).toContain('projects (require v14.8.0)');
|
||||
});
|
||||
|
||||
it('does not renders alert when there are no unavailable features', async () => {
|
||||
|
@ -558,7 +558,7 @@ describe('import table', () => {
|
|||
});
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.find(GlAlert).exists()).toBe(false);
|
||||
expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -22,8 +22,8 @@ describe('import target cell', () => {
|
|||
let wrapper;
|
||||
let group;
|
||||
|
||||
const findNameInput = () => wrapper.find(GlFormInput);
|
||||
const findNamespaceDropdown = () => wrapper.find(ImportGroupDropdown);
|
||||
const findNameInput = () => wrapper.findComponent(GlFormInput);
|
||||
const findNamespaceDropdown = () => wrapper.findComponent(ImportGroupDropdown);
|
||||
|
||||
const createComponent = (props) => {
|
||||
wrapper = shallowMount(ImportTargetCell, {
|
||||
|
|
|
@ -33,12 +33,12 @@ describe('BitbucketStatusTable', () => {
|
|||
|
||||
it('renders import table component', () => {
|
||||
createComponent({ providerTitle: 'Test' });
|
||||
expect(wrapper.find(ImportProjectsTable).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(ImportProjectsTable).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('passes alert in incompatible-repos-warning slot', () => {
|
||||
createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub);
|
||||
expect(wrapper.find(GlAlert).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(GlAlert).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('passes actions slot to import project table component', () => {
|
||||
|
@ -46,14 +46,14 @@ describe('BitbucketStatusTable', () => {
|
|||
createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub, {
|
||||
actions: actionsSlotContent,
|
||||
});
|
||||
expect(wrapper.find(ImportProjectsTable).text()).toBe(actionsSlotContent);
|
||||
expect(wrapper.findComponent(ImportProjectsTable).text()).toBe(actionsSlotContent);
|
||||
});
|
||||
|
||||
it('dismisses alert when requested', async () => {
|
||||
createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub);
|
||||
wrapper.find(GlAlert).vm.$emit('dismiss');
|
||||
wrapper.findComponent(GlAlert).vm.$emit('dismiss');
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.find(GlAlert).exists()).toBe(false);
|
||||
expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -33,7 +33,7 @@ describe('ImportProjectsTable', () => {
|
|||
.findAll(GlButton)
|
||||
.filter((w) => w.props().variant === 'confirm')
|
||||
.at(0);
|
||||
const findImportAllModal = () => wrapper.find({ ref: 'importAllModal' });
|
||||
const findImportAllModal = () => wrapper.findComponent({ ref: 'importAllModal' });
|
||||
|
||||
const importAllFn = jest.fn();
|
||||
const importAllModalShowFn = jest.fn();
|
||||
|
@ -89,13 +89,13 @@ describe('ImportProjectsTable', () => {
|
|||
it('renders a loading icon while repos are loading', () => {
|
||||
createComponent({ state: { isLoadingRepos: true } });
|
||||
|
||||
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders a loading icon while namespaces are loading', () => {
|
||||
createComponent({ state: { isLoadingNamespaces: true } });
|
||||
|
||||
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders a table with provider repos', () => {
|
||||
|
@ -109,7 +109,7 @@ describe('ImportProjectsTable', () => {
|
|||
state: { namespaces: [{ fullPath: 'path' }], repositories },
|
||||
});
|
||||
|
||||
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
|
||||
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
|
||||
expect(wrapper.find('table').exists()).toBe(true);
|
||||
expect(
|
||||
wrapper
|
||||
|
@ -170,7 +170,7 @@ describe('ImportProjectsTable', () => {
|
|||
it('renders an empty state if there are no repositories available', () => {
|
||||
createComponent({ state: { repositories: [] } });
|
||||
|
||||
expect(wrapper.find(ProviderRepoTableRow).exists()).toBe(false);
|
||||
expect(wrapper.findComponent(ProviderRepoTableRow).exists()).toBe(false);
|
||||
expect(wrapper.text()).toContain(`No ${providerTitle} repositories found`);
|
||||
});
|
||||
|
||||
|
@ -231,11 +231,11 @@ describe('ImportProjectsTable', () => {
|
|||
});
|
||||
|
||||
it('renders intersection observer component', () => {
|
||||
expect(wrapper.find(GlIntersectionObserver).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(GlIntersectionObserver).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('calls fetchRepos when intersection observer appears', async () => {
|
||||
wrapper.find(GlIntersectionObserver).vm.$emit('appear');
|
||||
wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
|
||||
|
||||
await nextTick();
|
||||
|
||||
|
|
|
@ -74,11 +74,13 @@ describe('ProviderRepoTableRow', () => {
|
|||
});
|
||||
|
||||
it('renders empty import status', () => {
|
||||
expect(wrapper.find(ImportStatus).props().status).toBe(STATUSES.NONE);
|
||||
expect(wrapper.findComponent(ImportStatus).props().status).toBe(STATUSES.NONE);
|
||||
});
|
||||
|
||||
it('renders a group namespace select', () => {
|
||||
expect(wrapper.find(ImportGroupDropdown).props().namespaces).toBe(availableNamespaces);
|
||||
expect(wrapper.findComponent(ImportGroupDropdown).props().namespaces).toBe(
|
||||
availableNamespaces,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders import button', () => {
|
||||
|
@ -127,11 +129,13 @@ describe('ProviderRepoTableRow', () => {
|
|||
});
|
||||
|
||||
it('renders proper import status', () => {
|
||||
expect(wrapper.find(ImportStatus).props().status).toBe(repo.importedProject.importStatus);
|
||||
expect(wrapper.findComponent(ImportStatus).props().status).toBe(
|
||||
repo.importedProject.importStatus,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not renders a namespace select', () => {
|
||||
expect(wrapper.find(GlDropdown).exists()).toBe(false);
|
||||
expect(wrapper.findComponent(GlDropdown).exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not render import button', () => {
|
||||
|
@ -139,7 +143,7 @@ describe('ProviderRepoTableRow', () => {
|
|||
});
|
||||
|
||||
it('passes stats to import status component', () => {
|
||||
expect(wrapper.find(ImportStatus).props().stats).toBe(FAKE_STATS);
|
||||
expect(wrapper.findComponent(ImportStatus).props().stats).toBe(FAKE_STATS);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -165,7 +169,7 @@ describe('ProviderRepoTableRow', () => {
|
|||
});
|
||||
|
||||
it('renders badge with error', () => {
|
||||
expect(wrapper.find(GlBadge).text()).toBe('Incompatible project');
|
||||
expect(wrapper.findComponent(GlBadge).text()).toBe('Incompatible project');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -40,15 +40,15 @@ describe('Incidents List', () => {
|
|||
all: 26,
|
||||
};
|
||||
|
||||
const findTable = () => wrapper.find(GlTable);
|
||||
const findTable = () => wrapper.findComponent(GlTable);
|
||||
const findTableRows = () => wrapper.findAll('table tbody tr');
|
||||
const findAlert = () => wrapper.find(GlAlert);
|
||||
const findLoader = () => wrapper.find(GlLoadingIcon);
|
||||
const findAlert = () => wrapper.findComponent(GlAlert);
|
||||
const findLoader = () => wrapper.findComponent(GlLoadingIcon);
|
||||
const findTimeAgo = () => wrapper.findAll(TimeAgoTooltip);
|
||||
const findAssignees = () => wrapper.findAll('[data-testid="incident-assignees"]');
|
||||
const findCreateIncidentBtn = () => wrapper.find('[data-testid="createIncidentBtn"]');
|
||||
const findClosedIcon = () => wrapper.findAll("[data-testid='incident-closed']");
|
||||
const findEmptyState = () => wrapper.find(GlEmptyState);
|
||||
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
|
||||
const findSeverity = () => wrapper.findAll(SeverityToken);
|
||||
const findEscalationStatus = () => wrapper.findAll('[data-testid="incident-escalation-status"]');
|
||||
const findIncidentLink = () => wrapper.findByTestId('incident-link');
|
||||
|
@ -179,7 +179,7 @@ describe('Incidents List', () => {
|
|||
});
|
||||
|
||||
it('renders an avatar component when there is an assignee', () => {
|
||||
const avatar = findAssignees().at(1).find(GlAvatar);
|
||||
const avatar = findAssignees().at(1).findComponent(GlAvatar);
|
||||
const { src, label } = avatar.attributes();
|
||||
const { name, avatarUrl } = mockIncidents[1].assignees.nodes[0];
|
||||
|
||||
|
|
|
@ -20,8 +20,8 @@ describe('IncidentsSettingTabs', () => {
|
|||
}
|
||||
});
|
||||
|
||||
const findToggleButton = () => wrapper.find({ ref: 'toggleBtn' });
|
||||
const findSectionHeader = () => wrapper.find({ ref: 'sectionHeader' });
|
||||
const findToggleButton = () => wrapper.findComponent({ ref: 'toggleBtn' });
|
||||
const findSectionHeader = () => wrapper.findComponent({ ref: 'sectionHeader' });
|
||||
|
||||
const findIntegrationTabs = () => wrapper.findAll(GlTab);
|
||||
it('renders header text', () => {
|
||||
|
|
|
@ -86,7 +86,7 @@ describe('TriggerFields', () => {
|
|||
expect(checkboxes).toHaveLength(2);
|
||||
|
||||
checkboxes.wrappers.forEach((checkbox, index) => {
|
||||
const checkBox = checkbox.find(GlFormCheckbox);
|
||||
const checkBox = checkbox.findComponent(GlFormCheckbox);
|
||||
|
||||
expect(checkbox.find('label').text()).toBe(expectedResults[index].labelText);
|
||||
expect(checkbox.find('[type=hidden]').attributes('name')).toBe(
|
||||
|
|
|
@ -53,7 +53,7 @@ afterEach(() => {
|
|||
|
||||
describe('ImportProjectMembersModal', () => {
|
||||
const findGlModal = () => wrapper.findComponent(GlModal);
|
||||
const findIntroText = () => wrapper.find({ ref: 'modalIntro' }).text();
|
||||
const findIntroText = () => wrapper.findComponent({ ref: 'modalIntro' }).text();
|
||||
const clickImportButton = () => findGlModal().vm.$emit('primary', { preventDefault: jest.fn() });
|
||||
const closeModal = () => findGlModal().vm.$emit('hidden', { preventDefault: jest.fn() });
|
||||
const findFormGroup = () => wrapper.findByTestId('form-group');
|
||||
|
|
|
@ -144,7 +144,7 @@ describe('IssueMilestoneComponent', () => {
|
|||
});
|
||||
|
||||
it('renders milestone icon', () => {
|
||||
expect(wrapper.find(GlIcon).props('name')).toBe('clock');
|
||||
expect(wrapper.findComponent(GlIcon).props('name')).toBe('clock');
|
||||
});
|
||||
|
||||
it('renders milestone title', () => {
|
||||
|
|
|
@ -31,11 +31,11 @@ describe('IssueToken', () => {
|
|||
}
|
||||
});
|
||||
|
||||
const findLink = () => wrapper.find({ ref: 'link' });
|
||||
const findReference = () => wrapper.find({ ref: 'reference' });
|
||||
const findLink = () => wrapper.findComponent({ ref: 'link' });
|
||||
const findReference = () => wrapper.findComponent({ ref: 'reference' });
|
||||
const findReferenceIcon = () => wrapper.find('[data-testid="referenceIcon"]');
|
||||
const findRemoveBtn = () => wrapper.find('[data-testid="removeBtn"]');
|
||||
const findTitle = () => wrapper.find({ ref: 'title' });
|
||||
const findTitle = () => wrapper.findComponent({ ref: 'title' });
|
||||
|
||||
describe('with reference supplied', () => {
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -153,7 +153,7 @@ describe('RelatedIssuesBlock', () => {
|
|||
});
|
||||
|
||||
it('sets `autoCompleteEpics` to false for add-issuable-form', () => {
|
||||
expect(wrapper.find(AddIssuableForm).props('autoCompleteEpics')).toBe(false);
|
||||
expect(wrapper.findComponent(AddIssuableForm).props('autoCompleteEpics')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -227,7 +227,7 @@ describe('RelatedIssuesBlock', () => {
|
|||
},
|
||||
});
|
||||
|
||||
const iconComponent = wrapper.find(GlIcon);
|
||||
const iconComponent = wrapper.findComponent(GlIcon);
|
||||
expect(iconComponent.exists()).toBe(true);
|
||||
expect(iconComponent.props('name')).toBe(icon);
|
||||
});
|
||||
|
|
|
@ -187,7 +187,9 @@ describe('RelatedIssuesList', () => {
|
|||
});
|
||||
|
||||
it('shows due date', () => {
|
||||
expect(wrapper.find(IssueDueDate).find('.board-card-info-text').text()).toBe('Nov 22, 2010');
|
||||
expect(wrapper.findComponent(IssueDueDate).find('.board-card-info-text').text()).toBe(
|
||||
'Nov 22, 2010',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,7 +21,7 @@ describe('CE IssueCardTimeInfo component', () => {
|
|||
};
|
||||
|
||||
const findMilestone = () => wrapper.find('[data-testid="issuable-milestone"]');
|
||||
const findMilestoneTitle = () => findMilestone().find(GlLink).attributes('title');
|
||||
const findMilestoneTitle = () => findMilestone().findComponent(GlLink).attributes('title');
|
||||
const findDueDate = () => wrapper.find('[data-testid="issuable-due-date"]');
|
||||
|
||||
const mountComponent = ({
|
||||
|
@ -56,8 +56,8 @@ describe('CE IssueCardTimeInfo component', () => {
|
|||
const milestone = findMilestone();
|
||||
|
||||
expect(milestone.text()).toBe(issue.milestone.title);
|
||||
expect(milestone.find(GlIcon).props('name')).toBe('clock');
|
||||
expect(milestone.find(GlLink).attributes('href')).toBe(issue.milestone.webPath);
|
||||
expect(milestone.findComponent(GlIcon).props('name')).toBe('clock');
|
||||
expect(milestone.findComponent(GlLink).attributes('href')).toBe(issue.milestone.webPath);
|
||||
});
|
||||
|
||||
describe.each`
|
||||
|
@ -84,7 +84,7 @@ describe('CE IssueCardTimeInfo component', () => {
|
|||
|
||||
expect(dueDate.text()).toBe('Dec 12, 2020');
|
||||
expect(dueDate.attributes('title')).toBe('Due date');
|
||||
expect(dueDate.find(GlIcon).props('name')).toBe('calendar');
|
||||
expect(dueDate.findComponent(GlIcon).props('name')).toBe('calendar');
|
||||
expect(dueDate.classes()).not.toContain('gl-text-red-500');
|
||||
});
|
||||
});
|
||||
|
@ -118,6 +118,6 @@ describe('CE IssueCardTimeInfo component', () => {
|
|||
|
||||
expect(timeEstimate.text()).toBe(issue.humanTimeEstimate);
|
||||
expect(timeEstimate.attributes('title')).toBe('Estimate');
|
||||
expect(timeEstimate.find(GlIcon).props('name')).toBe('timer');
|
||||
expect(timeEstimate.findComponent(GlIcon).props('name')).toBe('timer');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,9 +11,9 @@ describe('JiraIssuesImportStatus', () => {
|
|||
};
|
||||
let wrapper;
|
||||
|
||||
const findAlert = () => wrapper.find(GlAlert);
|
||||
const findAlert = () => wrapper.findComponent(GlAlert);
|
||||
|
||||
const findAlertLabel = () => wrapper.find(GlAlert).find(GlLabel);
|
||||
const findAlertLabel = () => wrapper.findComponent(GlAlert).findComponent(GlLabel);
|
||||
|
||||
const mountComponent = ({
|
||||
shouldShowFinishedAlert = false,
|
||||
|
@ -49,7 +49,7 @@ describe('JiraIssuesImportStatus', () => {
|
|||
});
|
||||
|
||||
it('does not show an alert', () => {
|
||||
expect(wrapper.find(GlAlert).exists()).toBe(false);
|
||||
expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -105,12 +105,12 @@ describe('JiraIssuesImportStatus', () => {
|
|||
shouldShowInProgressAlert: true,
|
||||
});
|
||||
|
||||
expect(wrapper.find(GlAlert).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(GlAlert).exists()).toBe(true);
|
||||
|
||||
findAlert().vm.$emit('dismiss');
|
||||
|
||||
await nextTick();
|
||||
expect(wrapper.find(GlAlert).exists()).toBe(false);
|
||||
expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -105,7 +105,7 @@ describe('Issue title suggestions item component', () => {
|
|||
const count = wrapper.findAll('.suggestion-counts span').at(0);
|
||||
|
||||
expect(count.text()).toContain('1');
|
||||
expect(count.find(GlIcon).props('name')).toBe('thumb-up');
|
||||
expect(count.findComponent(GlIcon).props('name')).toBe('thumb-up');
|
||||
});
|
||||
|
||||
it('renders notes count', () => {
|
||||
|
@ -114,7 +114,7 @@ describe('Issue title suggestions item component', () => {
|
|||
const count = wrapper.findAll('.suggestion-counts span').at(1);
|
||||
|
||||
expect(count.text()).toContain('2');
|
||||
expect(count.find(GlIcon).props('name')).toBe('comment');
|
||||
expect(count.findComponent(GlIcon).props('name')).toBe('comment');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -461,7 +461,7 @@ describe('Issuable output', () => {
|
|||
describe('when title is not in view', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.vm.state.titleText = 'Sticky header title';
|
||||
wrapper.find(GlIntersectionObserver).vm.$emit('disappear');
|
||||
wrapper.findComponent(GlIntersectionObserver).vm.$emit('disappear');
|
||||
});
|
||||
|
||||
it('shows with title', () => {
|
||||
|
|
|
@ -6,7 +6,7 @@ import MarkdownField from '~/vue_shared/components/markdown/field.vue';
|
|||
describe('Description field component', () => {
|
||||
let wrapper;
|
||||
|
||||
const findTextarea = () => wrapper.find({ ref: 'textarea' });
|
||||
const findTextarea = () => wrapper.findComponent({ ref: 'textarea' });
|
||||
|
||||
const mountComponent = (description = 'test') =>
|
||||
shallowMount(DescriptionField, {
|
||||
|
|
|
@ -5,7 +5,7 @@ import eventHub from '~/issues/show/event_hub';
|
|||
describe('Title field component', () => {
|
||||
let wrapper;
|
||||
|
||||
const findInput = () => wrapper.find({ ref: 'input' });
|
||||
const findInput = () => wrapper.findComponent({ ref: 'input' });
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(eventHub, '$emit');
|
||||
|
|
|
@ -65,7 +65,7 @@ describe('HeaderActions component', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const findToggleIssueStateButton = () => wrapper.find(GlButton);
|
||||
const findToggleIssueStateButton = () => wrapper.findComponent(GlButton);
|
||||
|
||||
const findDropdownBy = (dataTestId) => wrapper.find(`[data-testid="${dataTestId}"]`);
|
||||
const findMobileDropdown = () => findDropdownBy('mobile-dropdown');
|
||||
|
@ -73,7 +73,7 @@ describe('HeaderActions component', () => {
|
|||
const findMobileDropdownItems = () => findMobileDropdown().findAll(GlDropdownItem);
|
||||
const findDesktopDropdownItems = () => findDesktopDropdown().findAll(GlDropdownItem);
|
||||
|
||||
const findModal = () => wrapper.find(GlModal);
|
||||
const findModal = () => wrapper.findComponent(GlModal);
|
||||
|
||||
const findModalLinkAt = (index) => findModal().findAll(GlLink).at(index);
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ describe('Highlight Bar', () => {
|
|||
}
|
||||
});
|
||||
|
||||
const findLink = () => wrapper.find(GlLink);
|
||||
const findLink = () => wrapper.findComponent(GlLink);
|
||||
|
||||
describe('empty state', () => {
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -64,9 +64,9 @@ describe('Incident Tabs component', () => {
|
|||
const findTabs = () => wrapper.findAll(GlTab);
|
||||
const findSummaryTab = () => findTabs().at(0);
|
||||
const findAlertDetailsTab = () => wrapper.find('[data-testid="alert-details-tab"]');
|
||||
const findAlertDetailsComponent = () => wrapper.find(AlertDetailsTable);
|
||||
const findDescriptionComponent = () => wrapper.find(DescriptionComponent);
|
||||
const findHighlightBarComponent = () => wrapper.find(HighlightBar);
|
||||
const findAlertDetailsComponent = () => wrapper.findComponent(AlertDetailsTable);
|
||||
const findDescriptionComponent = () => wrapper.findComponent(DescriptionComponent);
|
||||
const findHighlightBarComponent = () => wrapper.findComponent(HighlightBar);
|
||||
const findTimelineTab = () => wrapper.findComponent(TimelineTab);
|
||||
|
||||
describe('empty state', () => {
|
||||
|
|
|
@ -62,8 +62,8 @@ describe('Sentry Error Stack Trace', () => {
|
|||
describe('loading', () => {
|
||||
it('should show spinner while loading', () => {
|
||||
mountComponent();
|
||||
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
|
||||
expect(wrapper.find(Stacktrace).exists()).toBe(false);
|
||||
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(Stacktrace).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -74,8 +74,8 @@ describe('Sentry Error Stack Trace', () => {
|
|||
|
||||
it('should show stacktrace', () => {
|
||||
mountComponent({ stubs: {} });
|
||||
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
|
||||
expect(wrapper.find(Stacktrace).exists()).toBe(true);
|
||||
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
|
||||
expect(wrapper.findComponent(Stacktrace).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,15 +21,15 @@ describe('JiraImportApp', () => {
|
|||
|
||||
const setupIllustration = 'setup-illustration.svg';
|
||||
|
||||
const getFormComponent = () => wrapper.find(JiraImportForm);
|
||||
const getFormComponent = () => wrapper.findComponent(JiraImportForm);
|
||||
|
||||
const getProgressComponent = () => wrapper.find(JiraImportProgress);
|
||||
const getProgressComponent = () => wrapper.findComponent(JiraImportProgress);
|
||||
|
||||
const getSetupComponent = () => wrapper.find(JiraImportSetup);
|
||||
const getSetupComponent = () => wrapper.findComponent(JiraImportSetup);
|
||||
|
||||
const getAlert = () => wrapper.find(GlAlert);
|
||||
const getAlert = () => wrapper.findComponent(GlAlert);
|
||||
|
||||
const getLoadingIcon = () => wrapper.find(GlLoadingIcon);
|
||||
const getLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
|
||||
|
||||
const mountComponent = ({
|
||||
isJiraConfigured = true,
|
||||
|
|
|
@ -289,7 +289,7 @@ describe('JiraImportForm', () => {
|
|||
|
||||
it('updates the user list', () => {
|
||||
expect(getUserDropdown().findAll(GlDropdownItem)).toHaveLength(1);
|
||||
expect(getUserDropdown().find(GlDropdownItem).text()).toContain(
|
||||
expect(getUserDropdown().findComponent(GlDropdownItem).text()).toContain(
|
||||
'fchopin (Frederic Chopin)',
|
||||
);
|
||||
});
|
||||
|
|
|
@ -8,7 +8,7 @@ describe('JiraImportProgress', () => {
|
|||
|
||||
const importProject = 'JIRAPROJECT';
|
||||
|
||||
const getGlEmptyStateProp = (attribute) => wrapper.find(GlEmptyState).props(attribute);
|
||||
const getGlEmptyStateProp = (attribute) => wrapper.findComponent(GlEmptyState).props(attribute);
|
||||
|
||||
const getParagraphText = () => wrapper.find('p').text();
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import { illustration, jiraIntegrationPath } from '../mock_data';
|
|||
describe('JiraImportSetup', () => {
|
||||
let wrapper;
|
||||
|
||||
const getGlEmptyStateProp = (attribute) => wrapper.find(GlEmptyState).props(attribute);
|
||||
const getGlEmptyStateProp = (attribute) => wrapper.findComponent(GlEmptyState).props(attribute);
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(JiraImportSetup, {
|
||||
|
|
|
@ -34,7 +34,7 @@ describe('~/labels/components/delete_label_modal', () => {
|
|||
wrapper.destroy();
|
||||
});
|
||||
|
||||
const findModal = () => wrapper.find(GlModal);
|
||||
const findModal = () => wrapper.findComponent(GlModal);
|
||||
const findPrimaryModalButton = () => wrapper.findByTestId('delete-button');
|
||||
|
||||
describe('template', () => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { GlAvatarLink, GlBadge } from '@gitlab/ui';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import UserAvatar from '~/members/components/avatars/user_avatar.vue';
|
||||
import { AVAILABILITY_STATUS } from '~/set_status_modal/utils';
|
||||
import { AVAILABILITY_STATUS } from '~/set_status_modal/constants';
|
||||
|
||||
import { member as memberMock, member2faEnabled, orphanedMember } from '../../mock_data';
|
||||
|
||||
|
|
|
@ -49,8 +49,8 @@ describe('Merge Conflict Resolver App', () => {
|
|||
extendedWrapper(w).findByTestId('interactive-button');
|
||||
const findFileInlineButton = (w = wrapper) => extendedWrapper(w).findByTestId('inline-button');
|
||||
const findSideBySideButton = () => wrapper.findByTestId('side-by-side');
|
||||
const findInlineConflictLines = (w = wrapper) => w.find(InlineConflictLines);
|
||||
const findParallelConflictLines = (w = wrapper) => w.find(ParallelConflictLines);
|
||||
const findInlineConflictLines = (w = wrapper) => w.findComponent(InlineConflictLines);
|
||||
const findParallelConflictLines = (w = wrapper) => w.findComponent(ParallelConflictLines);
|
||||
const findCommitMessageTextarea = () => wrapper.findByTestId('commit-message');
|
||||
|
||||
it('shows the amount of conflicts', () => {
|
||||
|
|
|
@ -96,9 +96,9 @@ describe('Milestone combobox component', () => {
|
|||
|
||||
const findNoResults = () => wrapper.find('[data-testid="milestone-combobox-no-results"]');
|
||||
|
||||
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
|
||||
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
|
||||
|
||||
const findSearchBox = () => wrapper.find(GlSearchBoxByType);
|
||||
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
|
||||
|
||||
const findProjectMilestonesSection = () =>
|
||||
wrapper.find('[data-testid="project-milestones-section"]');
|
||||
|
|
|
@ -25,7 +25,7 @@ describe('~/nav/components/top_nav_dropdown_menu.vue', () => {
|
|||
};
|
||||
|
||||
const findMenuItems = () => wrapper.findAllComponents(TopNavMenuItem);
|
||||
const findMenuSections = () => wrapper.find(TopNavMenuSections);
|
||||
const findMenuSections = () => wrapper.findComponent(TopNavMenuSections);
|
||||
const findMenuSidebar = () => wrapper.find('[data-testid="menu-sidebar"]');
|
||||
const findMenuSubview = () => wrapper.findComponent(KeepAliveSlots);
|
||||
const hasFullWidthMenuSidebar = () => findMenuSidebar().classes('gl-w-full');
|
||||
|
|
|
@ -26,7 +26,7 @@ describe('~/nav/components/top_nav_menu_item.vue', () => {
|
|||
});
|
||||
};
|
||||
|
||||
const findButton = () => wrapper.find(GlButton);
|
||||
const findButton = () => wrapper.findComponent(GlButton);
|
||||
const findButtonIcons = () =>
|
||||
findButton()
|
||||
.findAllComponents(GlIcon)
|
||||
|
|
|
@ -27,7 +27,7 @@ describe('LaTeX output cell', () => {
|
|||
${1} | ${false}
|
||||
`('sets `Prompt.show-output` to $expectation when index is $index', ({ index, expectation }) => {
|
||||
const wrapper = createComponent(inlineLatex, index);
|
||||
const prompt = wrapper.find(Prompt);
|
||||
const prompt = wrapper.findComponent(Prompt);
|
||||
|
||||
expect(prompt.props().count).toEqual(count);
|
||||
expect(prompt.props().showOutput).toEqual(expectation);
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue