Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-06-09 15:09:21 +00:00
parent 48d25238c3
commit d2675fa4de
105 changed files with 1709 additions and 278 deletions

View File

@ -108,6 +108,8 @@ rules:
message: 'Migrate to GlSkeletonLoader, or import GlDeprecatedSkeletonLoading.'
# See https://gitlab.com/gitlab-org/gitlab/-/issues/360551
vue/multi-word-component-names: off
unicorn/prefer-dom-node-dataset:
- error
overrides:
- files:
- '{,ee/,jh/}spec/frontend*/**/*'

View File

@ -55,8 +55,8 @@ export function renderKroki(krokiImages) {
// A single Kroki image is processed multiple times for some reason,
// so this condition ensures we only create one alert per Kroki image
if (!parent.hasAttribute('data-kroki-processed')) {
parent.setAttribute('data-kroki-processed', 'true');
if (!Object.hasOwn(parent.dataset, 'krokiProcessed')) {
parent.dataset.krokiProcessed = 'true';
parent.after(createAlert(krokiImage));
}
});

View File

@ -112,7 +112,7 @@ class SafeMathRenderer {
try {
displayContainer.innerHTML = this.katex.renderToString(text, {
displayMode: el.getAttribute('data-math-style') === 'display',
displayMode: el.dataset.mathStyle === 'display',
throwOnError: true,
maxSize: 20,
maxExpand: 20,
@ -145,7 +145,7 @@ class SafeMathRenderer {
this.elements.forEach((el) => {
const placeholder = document.createElement('span');
placeholder.style.display = 'none';
placeholder.setAttribute('data-math-style', el.getAttribute('data-math-style'));
placeholder.dataset.mathStyle = el.dataset.mathStyle;
placeholder.textContent = el.textContent;
el.parentNode.replaceChild(placeholder, el);
this.queue.push(placeholder);

View File

@ -9,10 +9,11 @@ const updateLineNumbersOnBlobPermalinks = (linksToUpdate) => {
[].concat(Array.prototype.slice.call(linksToUpdate)).forEach((permalinkButton) => {
const baseHref =
permalinkButton.getAttribute('data-original-href') ||
permalinkButton.dataset.originalHref ||
(() => {
const href = permalinkButton.getAttribute('href');
permalinkButton.setAttribute('data-original-href', href);
// eslint-disable-next-line no-param-reassign
permalinkButton.dataset.originalHref = href;
return href;
})();
permalinkButton.setAttribute('href', `${baseHref}${hashUrlString}`);

View File

@ -36,19 +36,19 @@ const loadRichBlobViewer = (type) => {
const loadViewer = (viewerParam) => {
const viewer = viewerParam;
const url = viewer.getAttribute('data-url');
const { url } = viewer.dataset;
if (!url || viewer.getAttribute('data-loaded') || viewer.getAttribute('data-loading')) {
if (!url || viewer.dataset.loaded || viewer.dataset.loading) {
return Promise.resolve(viewer);
}
viewer.setAttribute('data-loading', 'true');
viewer.dataset.loading = 'true';
return axios.get(url).then(({ data }) => {
viewer.innerHTML = data.html;
window.requestIdleCallback(() => {
viewer.removeAttribute('data-loading');
delete viewer.dataset.loading;
});
return viewer;
@ -108,7 +108,7 @@ export class BlobViewer {
switchToInitialViewer() {
const initialViewer = this.$fileHolder[0].querySelector('.blob-viewer:not(.hidden)');
let initialViewerName = initialViewer.getAttribute('data-type');
let initialViewerName = initialViewer.dataset.type;
if (this.switcher && window.location.hash.indexOf('#L') === 0) {
initialViewerName = 'simple';
@ -138,12 +138,12 @@ export class BlobViewer {
e.preventDefault();
this.switchToViewer(target.getAttribute('data-viewer'));
this.switchToViewer(target.dataset.viewer);
}
toggleCopyButtonState() {
if (!this.copySourceBtn) return;
if (this.simpleViewer.getAttribute('data-loaded')) {
if (this.simpleViewer.dataset.loaded) {
this.copySourceBtnTooltip.setAttribute('title', __('Copy file contents'));
this.copySourceBtn.classList.remove('disabled');
} else if (this.activeViewer === this.simpleViewer) {
@ -199,7 +199,8 @@ export class BlobViewer {
this.$fileHolder.trigger('highlight:line');
handleLocationHash();
viewer.setAttribute('data-loaded', 'true');
// eslint-disable-next-line no-param-reassign
viewer.dataset.loaded = 'true';
this.toggleCopyButtonState();
eventHub.$emit('showBlobInteractionZones', viewer.dataset.path);
});

View File

@ -5,7 +5,7 @@ export const addTooltipToEl = (el) => {
if (textEl && textEl.scrollWidth > textEl.offsetWidth) {
el.setAttribute('title', el.textContent);
el.setAttribute('data-container', 'body');
el.dataset.container = 'body';
el.classList.add('has-tooltip');
}
};

View File

@ -32,8 +32,8 @@ export const addInteractionClass = ({ path, d, wrapTextNodes }) => {
});
if (el && !isTextNode(el)) {
el.setAttribute('data-char-index', d.start_char);
el.setAttribute('data-line-index', d.start_line);
el.dataset.charIndex = d.start_char;
el.dataset.lineIndex = d.start_line;
el.classList.add('cursor-pointer', 'code-navigation', 'js-code-navigation');
el.closest('.line').classList.add('code-navigation-line');
}

View File

@ -107,10 +107,10 @@ function createLink(data, selected, options, index) {
}
if (options.trackSuggestionClickedLabel) {
link.setAttribute('data-track-action', 'click_text');
link.setAttribute('data-track-label', options.trackSuggestionClickedLabel);
link.setAttribute('data-track-value', index);
link.setAttribute('data-track-property', slugify(data.category || 'no-category'));
link.dataset.trackAction = 'click_text';
link.dataset.trackLabel = options.trackSuggestionClickedLabel;
link.dataset.trackValue = index;
link.dataset.trackProperty = slugify(data.category || 'no-category');
}
link.classList.toggle('is-active', selected);

View File

@ -26,7 +26,7 @@ export default class Diff {
FilesCommentButton.init($diffFile);
const firstFile = $('.files').first().get(0);
const canCreateNote = firstFile && firstFile.hasAttribute('data-can-create-note');
const canCreateNote = firstFile && Object.hasOwn(firstFile.dataset, 'canCreateNote');
$diffFile.each((index, file) => initImageDiffHelper.initImageDiff(file, canCreateNote));
if (!isBound) {

View File

@ -197,10 +197,10 @@ export default class AvailableDropdownMappings {
}
getGroupId() {
return this.filteredSearchInput.getAttribute('data-group-id') || '';
return this.filteredSearchInput.dataset.groupId || '';
}
getProjectId() {
return this.filteredSearchInput.getAttribute('data-project-id') || '';
return this.filteredSearchInput.dataset.projectId || '';
}
}

View File

@ -25,9 +25,9 @@ export default class DropdownHint extends FilteredSearchDropdown {
const { selected } = e.detail;
if (selected.tagName === 'LI') {
if (selected.hasAttribute('data-value')) {
if (Object.hasOwn(selected.dataset, 'value')) {
this.dismissDropdown();
} else if (selected.getAttribute('data-action') === 'submit') {
} else if (selected.dataset.action === 'submit') {
this.dismissDropdown();
this.dispatchFormSubmitEvent();
} else {

View File

@ -23,7 +23,7 @@ export default class DropdownOperator extends FilteredSearchDropdown {
const { selected } = e.detail;
if (selected.tagName === 'LI') {
if (selected.hasAttribute('data-value')) {
if (Object.hasOwn(selected.dataset, 'value')) {
const name = FilteredSearchVisualTokens.getLastTokenPartial();
const operator = selected.dataset.value;

View File

@ -31,11 +31,11 @@ export default class DropdownUser extends DropdownAjaxFilter {
}
getGroupId() {
return this.input.getAttribute('data-group-id');
return this.input.dataset.groupId;
}
getProjectId() {
return this.input.getAttribute('data-project-id');
return this.input.dataset.projectId;
}
projectOrGroupId() {

View File

@ -87,6 +87,7 @@ export default class DropdownUtils {
}
static setDataValueIfSelected(filter, operator, selected) {
// eslint-disable-next-line unicorn/prefer-dom-node-dataset
const dataValue = selected.getAttribute('data-value');
if (dataValue) {
@ -96,6 +97,7 @@ export default class DropdownUtils {
tokenValue: dataValue,
clicked: true,
options: {
// eslint-disable-next-line unicorn/prefer-dom-node-dataset
capitalizeTokenValue: selected.hasAttribute('data-capitalize'),
},
});

View File

@ -165,8 +165,8 @@ class DropDown {
images.forEach((image) => {
const img = image;
img.src = img.getAttribute('data-src');
img.removeAttribute('data-src');
img.src = img.dataset.src;
delete img.dataset.src;
});
}
}

View File

@ -814,7 +814,7 @@ export default class FilteredSearchManager {
getUsernameParams() {
const usernamesById = {};
try {
const attribute = this.filteredSearchInput.getAttribute('data-username-params');
const attribute = this.filteredSearchInput.dataset.usernameParams;
JSON.parse(attribute).forEach((user) => {
usernamesById[user.id] = user.username;
});

View File

@ -6,7 +6,7 @@ export function setPositionDataAttribute(el, options) {
const positionObject = { ...JSON.parse(position), x, y, width, height };
el.setAttribute('data-position', JSON.stringify(positionObject));
el.dataset.position = JSON.stringify(positionObject);
}
export function updateDiscussionAvatarBadgeNumber(discussionEl, newBadgeNumber) {

View File

@ -82,10 +82,7 @@ export default class CreateMergeRequestDropdown {
this.init();
if (isConfidentialIssue()) {
this.createMergeRequestButton.setAttribute(
'data-dropdown-trigger',
'#create-merge-request-dropdown',
);
this.createMergeRequestButton.dataset.dropdownTrigger = '#create-merge-request-dropdown';
initConfidentialMergeRequest();
}
}

View File

@ -378,7 +378,7 @@ export default {
},
setActiveTask(el) {
const { parentElement } = el;
const lineNumbers = parentElement.getAttribute('data-sourcepos').match(/\b\d+(?=:)/g);
const lineNumbers = parentElement.dataset.sourcepos.match(/\b\d+(?=:)/g);
this.activeTask = {
title: parentElement.innerText,
lineNumberStart: lineNumbers[0],

View File

@ -127,7 +127,7 @@ export default class LazyLoader {
// Loading Images which are in the current viewport or close to them
this.lazyImages = this.lazyImages.filter((selectedImage) => {
if (selectedImage.getAttribute('data-src')) {
if (selectedImage.dataset.src) {
const imgBoundRect = selectedImage.getBoundingClientRect();
const imgTop = scrollTop + imgBoundRect.top;
const imgBound = imgTop + imgBoundRect.height;
@ -156,16 +156,17 @@ export default class LazyLoader {
}
static loadImage(img) {
if (img.getAttribute('data-src')) {
if (img.dataset.src) {
img.setAttribute('loading', 'lazy');
let imgUrl = img.getAttribute('data-src');
let imgUrl = img.dataset.src;
// Only adding width + height for avatars for now
if (imgUrl.indexOf('/avatar/') > -1 && imgUrl.indexOf('?') === -1) {
const targetWidth = img.getAttribute('width') || img.width;
imgUrl += `?width=${targetWidth}`;
}
img.setAttribute('src', imgUrl);
img.removeAttribute('data-src');
// eslint-disable-next-line no-param-reassign
delete img.dataset.src;
img.classList.remove('lazy');
img.classList.add('js-lazy-loaded');
img.classList.add('qa-js-lazy-loaded');

View File

@ -56,7 +56,7 @@ export function confirmAction(
export function confirmViaGlModal(message, element) {
const primaryBtnConfig = {};
const confirmBtnVariant = element.getAttribute('data-confirm-btn-variant');
const { confirmBtnVariant } = element.dataset;
if (confirmBtnVariant) {
primaryBtnConfig.primaryBtnVariant = confirmBtnVariant;

View File

@ -41,7 +41,7 @@ export default {
const dropdownToggle = this.$refs.glDropdown.$el.querySelector('.dropdown-toggle');
if (dropdownToggle) {
dropdownToggle.setAttribute('data-qa-selector', 'access_level_dropdown');
dropdownToggle.dataset.qaSelector = 'access_level_dropdown';
}
},
methods: {

View File

@ -1,6 +1,6 @@
function onSidebarLinkClick() {
const setDataTrackAction = (element, action) => {
element.setAttribute('data-track-action', action);
element.dataset.trackAction = action;
};
const setDataTrackExtra = (element, value) => {
@ -12,10 +12,10 @@ function onSidebarLinkClick() {
? SIDEBAR_COLLAPSED
: SIDEBAR_EXPANDED;
element.setAttribute(
'data-track-extra',
JSON.stringify({ sidebar_display: sidebarCollapsed, menu_display: value }),
);
element.dataset.trackExtra = JSON.stringify({
sidebar_display: sidebarCollapsed,
menu_display: value,
});
};
const EXPANDED = 'Expanded';

View File

@ -298,7 +298,7 @@ export default class ActivityCalendar {
.querySelector(this.activitiesContainer)
.querySelectorAll('.js-localtime')
.forEach((el) => {
el.setAttribute('title', formatDate(el.getAttribute('data-datetime')));
el.setAttribute('title', formatDate(el.dataset.datetime));
});
})
.catch(() =>

View File

@ -57,7 +57,7 @@ export default {
if (authorParam) {
commitsSearchInput.setAttribute('disabled', true);
commitsSearchInput.setAttribute('data-toggle', 'tooltip');
commitsSearchInput.dataset.toggle = 'tooltip';
commitsSearchInput.setAttribute('title', tooltipMessage);
this.currentAuthor = authorParam;
}

View File

@ -17,6 +17,7 @@ export default {
piplelinesTabEvent: 'p_analytics_ci_cd_pipelines',
deploymentFrequencyTabEvent: 'p_analytics_ci_cd_deployment_frequency',
leadTimeTabEvent: 'p_analytics_ci_cd_lead_time',
timeToRestoreServiceTabEvent: 'p_analytics_ci_cd_time_to_restore_service',
inject: {
shouldRenderDoraCharts: {
type: Boolean,
@ -37,7 +38,7 @@ export default {
const chartsToShow = ['pipelines'];
if (this.shouldRenderDoraCharts) {
chartsToShow.push('deployment-frequency', 'lead-time');
chartsToShow.push('deployment-frequency', 'lead-time', 'time-to-restore-service');
}
if (this.shouldRenderQualitySummary) {

View File

@ -119,7 +119,7 @@ function mountAssigneesComponentDeprecated(mediator) {
issuableIid: String(iid),
projectPath: fullPath,
field: el.dataset.field,
signedIn: el.hasAttribute('data-signed-in'),
signedIn: Object.hasOwn(el.dataset, 'signedIn'),
issuableType:
isInIssuePage() || isInIncidentPage() || isInDesignPage()
? IssuableType.Issue
@ -149,7 +149,7 @@ function mountAssigneesComponent() {
},
provide: {
canUpdate: editable,
directlyInviteMembers: el.hasAttribute('data-directly-invite-members'),
directlyInviteMembers: Object.hasOwn(el.dataset, 'directlyInviteMembers'),
},
render: (createElement) =>
createElement('sidebar-assignees-widget', {

View File

@ -39,7 +39,7 @@ export default () => {
return createElement(TerraformList, {
props: {
emptyStateImage,
terraformAdmin: el.hasAttribute('data-terraform-admin'),
terraformAdmin: Object.hasOwn(el.dataset, 'terraformAdmin'),
},
});
},

View File

@ -0,0 +1,25 @@
<script>
export default {
props: {
color: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
},
};
</script>
<template>
<div>
<span
class="dropdown-label-box gl-flex-shrink-0 gl-top-1 gl-mr-0"
data-testid="color-item"
:style="{ backgroundColor: color }"
></span>
<span class="hide-collapsed">{{ title }}</span>
</div>
</template>

View File

@ -0,0 +1,214 @@
<script>
import createFlash from '~/flash';
import { s__ } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import { DEFAULT_COLOR, COLOR_WIDGET_COLOR, DROPDOWN_VARIANT, ISSUABLE_COLORS } from './constants';
import DropdownContents from './dropdown_contents.vue';
import DropdownValue from './dropdown_value.vue';
import { isDropdownVariantSidebar, isDropdownVariantEmbedded } from './utils';
import epicColorQuery from './graphql/epic_color.query.graphql';
import updateEpicColorMutation from './graphql/epic_update_color.mutation.graphql';
export default {
i18n: {
assignColor: s__('ColorWidget|Assign epic color'),
dropdownButtonText: COLOR_WIDGET_COLOR,
fetchingError: s__('ColorWidget|Error fetching epic color.'),
updatingError: s__('ColorWidget|An error occurred while updating color.'),
widgetTitle: COLOR_WIDGET_COLOR,
},
components: {
DropdownValue,
DropdownContents,
SidebarEditableItem,
},
props: {
allowEdit: {
type: Boolean,
required: false,
default: false,
},
iid: {
type: String,
required: false,
default: '',
},
fullPath: {
type: String,
required: true,
},
variant: {
type: String,
required: false,
default: DROPDOWN_VARIANT.Sidebar,
},
dropdownButtonText: {
type: String,
required: false,
default: COLOR_WIDGET_COLOR,
},
dropdownTitle: {
type: String,
required: false,
default: s__('ColorWidget|Assign epic color'),
},
},
data() {
return {
issuableColor: {
color: '',
title: '',
},
colorUpdateInProgress: false,
oldIid: null,
sidebarExpandedOnClick: false,
};
},
apollo: {
issuableColor: {
query: epicColorQuery,
skip() {
return !isDropdownVariantSidebar(this.variant);
},
variables() {
return {
iid: this.iid,
fullPath: this.fullPath,
};
},
update(data) {
const issuableColor = data.workspace?.issuable?.color;
if (issuableColor) {
return ISSUABLE_COLORS.find((color) => color.color === issuableColor) ?? DEFAULT_COLOR;
}
return DEFAULT_COLOR;
},
error() {
createFlash({
message: this.$options.i18n.fetchingError,
captureError: true,
});
},
},
},
computed: {
isLoading() {
return this.colorUpdateInProgress || this.$apollo.queries.issuableColor.loading;
},
},
watch: {
iid(_, oldVal) {
this.oldIid = oldVal;
},
},
methods: {
handleDropdownClose(color) {
if (this.iid !== '') {
this.updateSelectedColor(this.getUpdateVariables(color));
} else {
this.$emit('updateSelectedColor', color);
}
this.collapseEditableItem();
},
collapseEditableItem() {
this.$refs.editable?.collapse();
if (this.sidebarExpandedOnClick) {
this.sidebarExpandedOnClick = false;
this.$emit('toggleCollapse');
}
},
getUpdateVariables(color) {
const currentIid = this.oldIid || this.iid;
return {
iid: currentIid,
groupPath: this.fullPath,
color: color.color,
};
},
updateSelectedColor(inputVariables) {
this.colorUpdateInProgress = true;
this.$apollo
.mutate({
mutation: updateEpicColorMutation,
variables: { input: inputVariables },
})
.then(({ data }) => {
if (data.updateIssuableColor?.errors?.length) {
throw new Error();
}
this.$emit('updateSelectedColor', {
id: data.updateIssuableColor?.issuable?.id,
color: data.updateIssuableColor?.issuable?.color,
});
})
.catch((error) =>
createFlash({
message: this.$options.i18n.updatingError,
captureError: true,
error,
}),
)
.finally(() => {
this.colorUpdateInProgress = false;
});
},
isDropdownVariantSidebar,
isDropdownVariantEmbedded,
},
};
</script>
<template>
<div
class="labels-select-wrapper gl-relative"
:class="{
'is-embedded': isDropdownVariantEmbedded(variant),
}"
>
<template v-if="isDropdownVariantSidebar(variant)">
<sidebar-editable-item
ref="editable"
:title="$options.i18n.widgetTitle"
:loading="isLoading"
:can-edit="allowEdit"
@open="oldIid = null"
>
<template #collapsed>
<dropdown-value :selected-color="issuableColor">
<slot></slot>
</dropdown-value>
</template>
<template #default="{ edit }">
<dropdown-value :selected-color="issuableColor" class="gl-mb-2">
<slot></slot>
</dropdown-value>
<dropdown-contents
ref="dropdownContents"
:dropdown-button-text="dropdownButtonText"
:dropdown-title="dropdownTitle"
:selected-color="issuableColor"
:variant="variant"
:is-visible="edit"
@setColor="handleDropdownClose"
@closeDropdown="collapseEditableItem"
/>
</template>
</sidebar-editable-item>
</template>
<dropdown-contents
v-else
ref="dropdownContents"
:dropdown-button-text="dropdownButtonText"
:dropdown-title="dropdownTitle"
:selected-color="issuableColor"
:variant="variant"
@setColor="handleDropdownClose"
/>
</div>
</template>

View File

@ -0,0 +1,30 @@
import { __, s__ } from '~/locale';
export const COLOR_WIDGET_COLOR = s__('ColorWidget|Color');
export const DROPDOWN_VARIANT = {
Sidebar: 'sidebar',
Embedded: 'embedded',
};
export const DEFAULT_COLOR = { title: __('SuggestedColors|Blue'), color: '#1068bf' };
export const ISSUABLE_COLORS = [
DEFAULT_COLOR,
{
title: s__('SuggestedColors|Green'),
color: '#217645',
},
{
title: s__('SuggestedColors|Red'),
color: '#c91c00',
},
{
title: s__('SuggestedColors|Orange'),
color: '#9e5400',
},
{
title: s__('SuggestedColors|Purple'),
color: '#694cc0',
},
];

View File

@ -0,0 +1,109 @@
<script>
import { GlDropdown } from '@gitlab/ui';
import DropdownContentsColorView from './dropdown_contents_color_view.vue';
import DropdownHeader from './dropdown_header.vue';
import { isDropdownVariantSidebar } from './utils';
export default {
components: {
DropdownContentsColorView,
DropdownHeader,
GlDropdown,
},
props: {
dropdownTitle: {
type: String,
required: true,
},
selectedColor: {
type: Object,
required: true,
},
dropdownButtonText: {
type: String,
required: true,
},
variant: {
type: String,
required: true,
},
isVisible: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
showDropdownContentsCreateView: false,
localSelectedColor: this.selectedColor,
isDirty: false,
};
},
computed: {
buttonText() {
if (!this.localSelectedColor?.title) {
return this.dropdownButtonText;
}
return this.localSelectedColor.title;
},
},
watch: {
localSelectedColor: {
handler() {
this.isDirty = true;
},
deep: true,
},
isVisible(newVal) {
if (newVal) {
this.$refs.dropdown.show();
this.isDirty = false;
this.localSelectedColor = this.selectedColor;
} else {
this.$refs.dropdown.hide();
this.setColor();
}
},
selectedColor(newVal) {
if (!this.isDirty) {
this.localSelectedColor = newVal;
}
},
},
methods: {
setColor() {
if (!this.isDirty) {
return;
}
this.$emit('setColor', this.localSelectedColor);
},
handleDropdownHide() {
this.$emit('closeDropdown');
if (!isDropdownVariantSidebar(this.variant)) {
this.setColor();
}
this.$refs.dropdown.hide();
},
},
};
</script>
<template>
<gl-dropdown ref="dropdown" :text="buttonText" class="gl-w-full" @hide="handleDropdownHide">
<template #header>
<dropdown-header
ref="header"
:dropdown-title="dropdownTitle"
@closeDropdown="handleDropdownHide"
/>
</template>
<template #default>
<dropdown-contents-color-view
v-model="localSelectedColor"
@closeDropdown="handleDropdownHide"
/>
</template>
</gl-dropdown>
</template>

View File

@ -0,0 +1,53 @@
<script>
import { GlDropdownForm, GlDropdownItem } from '@gitlab/ui';
import ColorItem from './color_item.vue';
import { ISSUABLE_COLORS } from './constants';
export default {
components: {
GlDropdownForm,
GlDropdownItem,
ColorItem,
},
model: {
prop: 'selectedColor',
},
props: {
selectedColor: {
type: Object,
required: true,
},
},
data() {
return {
colors: ISSUABLE_COLORS,
};
},
methods: {
isColorSelected(color) {
return this.selectedColor.color === color.color;
},
handleColorClick(color) {
this.$emit('input', color);
this.$emit('closeDropdown', this.selectedColor);
},
},
};
</script>
<template>
<gl-dropdown-form>
<div>
<gl-dropdown-item
v-for="color in colors"
:key="color.color"
:is-checked="isColorSelected(color)"
:is-check-centered="true"
:is-check-item="true"
@click.native.capture.stop="handleColorClick(color)"
>
<color-item :color="color.color" :title="color.title" />
</gl-dropdown-item>
</div>
</gl-dropdown-form>
</template>

View File

@ -0,0 +1,31 @@
<script>
import { GlButton } from '@gitlab/ui';
export default {
components: {
GlButton,
},
props: {
dropdownTitle: {
type: String,
required: true,
},
},
};
</script>
<template>
<div>
<div class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!">
<span class="gl-flex-grow-1">{{ dropdownTitle }}</span>
<gl-button
:aria-label="__('Close')"
variant="link"
size="small"
class="dropdown-header-button gl-p-0!"
icon="close"
@click="$emit('closeDropdown')"
/>
</div>
</div>
</template>

View File

@ -0,0 +1,43 @@
<script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { COLOR_WIDGET_COLOR } from './constants';
import ColorItem from './color_item.vue';
export default {
i18n: {
dropdownTitle: COLOR_WIDGET_COLOR,
},
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
GlIcon,
ColorItem,
},
props: {
selectedColor: {
type: Object,
required: true,
},
},
};
</script>
<template>
<div class="value js-value">
<div
v-gl-tooltip.left.viewport
:title="$options.i18n.dropdownTitle"
class="sidebar-collapsed-icon"
>
<gl-icon name="appearance" />
<color-item
:color="selectedColor.color"
:title="selectedColor.title"
class="gl-font-base gl-line-height-24"
/>
</div>
<color-item class="hide-collapsed" :color="selectedColor.color" :title="selectedColor.title" />
</div>
</template>

View File

@ -0,0 +1,9 @@
query epicColor($fullPath: ID!, $iid: ID) {
workspace: group(fullPath: $fullPath) {
id
issuable: epic(iid: $iid) {
id
color
}
}
}

View File

@ -0,0 +1,9 @@
mutation updateEpicColor($input: UpdateEpicInput!) {
updateIssuableColor: updateEpic(input: $input) {
issuable: epic {
id
color
}
errors
}
}

View File

@ -0,0 +1,15 @@
import { DROPDOWN_VARIANT } from './constants';
/**
* Returns boolean representing whether dropdown variant
* is `sidebar`
* @param {string} variant
*/
export const isDropdownVariantSidebar = (variant) => variant === DROPDOWN_VARIANT.Sidebar;
/**
* Returns boolean representing whether dropdown variant
* is `embedded`
* @param {string} variant
*/
export const isDropdownVariantEmbedded = (variant) => variant === DROPDOWN_VARIANT.Embedded;

View File

@ -33,7 +33,7 @@ export default {
this.fetchFreshItems();
const body = document.querySelector('body');
const namespaceId = body.getAttribute('data-namespace-id');
const { namespaceId } = body.dataset;
this.track('click_whats_new_drawer', { label: 'namespace_id', value: namespaceId });
},

View File

@ -1,6 +1,6 @@
export const STORAGE_KEY = 'display-whats-new-notification';
export const getVersionDigest = (appEl) => appEl.getAttribute('data-version-digest');
export const getVersionDigest = (appEl) => appEl.dataset.versionDigest;
export const setNotification = (appEl) => {
const versionDigest = getVersionDigest(appEl);

View File

@ -7,7 +7,7 @@ class Import::GiteaController < Import::GithubController
def new
if session[access_token_key].present? && provider_url.present?
redirect_to status_import_url
redirect_to status_import_url(namespace_id: params[:namespace_id])
end
end

View File

@ -432,8 +432,9 @@ class Group < Namespace
end
# Check if user is a last owner of the group.
# Excludes project_bots
def last_owner?(user)
has_owner?(user) && single_owner?
has_owner?(user) && all_owners_excluding_project_bots.size == 1
end
def member_last_owner?(member)
@ -442,8 +443,8 @@ class Group < Namespace
last_owner?(member.user)
end
def single_owner?
members_with_parents.owners.size == 1
def all_owners_excluding_project_bots
members_with_parents.owners.merge(User.without_project_bot)
end
def single_blocked_owner?

View File

@ -1,5 +1,6 @@
# frozen_string_literal: true
# Optimization class to fix group member n+1 queries
class LastGroupOwnerAssigner
def initialize(group, members)
@group = group
@ -39,6 +40,6 @@ class LastGroupOwnerAssigner
end
def owners
@owners ||= group.members_with_parents.owners.load
@owners ||= group.all_owners_excluding_project_bots.load
end
end

View File

@ -19,7 +19,7 @@
= f.label :max_pages_size, _('Maximum size of pages (MB)'), class: 'label-bold'
= f.number_field :max_pages_size, class: 'form-control gl-form-input'
.form-text.text-muted
- pages_link_url = help_page_path('administration/pages/index', anchor: 'set-global-maximum-pages-size-per-project')
- pages_link_url = help_page_path('administration/pages/index', anchor: 'set-maximum-size-of-gitlab-pages-site-in-a-project')
- pages_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: pages_link_url }
= s_('AdminSettings|Set the maximum size of GitLab Pages per project (0 for unlimited). %{link_start}Learn more.%{link_end}').html_safe % { link_start: pages_link_start, link_end: '</a>'.html_safe }
%h5

View File

@ -11,6 +11,7 @@
= _('To get started, please enter your Gitea Host URL and a %{link_to_personal_token}.').html_safe % { link_to_personal_token: link_to_personal_token }
= form_tag personal_access_token_import_gitea_path do
= hidden_field_tag(:namespace_id, params[:namespace_id])
.form-group.row
= label_tag :gitea_host_url, _('Gitea Host URL'), class: 'col-form-label col-sm-2'
.col-sm-4

View File

@ -3,4 +3,4 @@
= custom_icon('gitea_logo')
= _('Import Projects from Gitea')
= render 'import/githubish_status', provider: 'gitea'
= render 'import/githubish_status', provider: 'gitea', default_namespace: @namespace

View File

@ -53,7 +53,7 @@
- if gitea_import_enabled?
%div
= link_to new_import_gitea_path, class: 'gl-button btn-default btn import_gitea js-import-project-btn', data: { platform: 'gitea', **tracking_attrs_data(track_label, 'click_button', 'gitea') } do
= link_to new_import_gitea_path(namespace_id: namespace_id), class: 'gl-button btn-default btn import_gitea js-import-project-btn', data: { platform: 'gitea', **tracking_attrs_data(track_label, 'click_button', 'gitea') } do
.gl-button-icon
= custom_icon('gitea_logo')
Gitea

View File

@ -74,16 +74,12 @@ module ContainerRegistry
if repository.migration_state == 'pre_importing' &&
Feature.enabled?(:registry_migration_guard_dynamic_pre_import_timeout) &&
migration_start_timestamp(repository).before?(timeout.ago)
timeout = dynamic_pre_import_timeout_for(repository)
timeout = migration.dynamic_pre_import_timeout_for(repository)
end
migration_start_timestamp(repository).before?(timeout.ago)
end
def dynamic_pre_import_timeout_for(repository)
(repository.tags_count * migration.pre_import_tags_rate).seconds
end
def external_state_matches_migration_state?(repository)
status = repository.external_import_status

View File

@ -2,16 +2,17 @@
class DeleteContainerRepositoryWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
include ExclusiveLeaseGuard
data_consistency :always
sidekiq_options retry: 3
include ExclusiveLeaseGuard
queue_namespace :container_repository
feature_category :container_registry
LEASE_TIMEOUT = 1.hour
LEASE_TIMEOUT = 1.hour.freeze
FIXED_DELAY = 10.seconds.freeze
attr_reader :container_repository
@ -22,6 +23,16 @@ class DeleteContainerRepositoryWorker # rubocop:disable Scalability/IdempotentWo
return unless current_user && container_repository && project
if migration.delete_container_repository_worker_support? && migrating?
delay = migration_duration
self.class.perform_in(delay.from_now)
log_extra_metadata_on_done(:delete_postponed, delay)
return
end
# If a user accidentally attempts to delete the same container registry in quick succession,
# this can lead to orphaned tags.
try_obtain_lease do
@ -29,6 +40,28 @@ class DeleteContainerRepositoryWorker # rubocop:disable Scalability/IdempotentWo
end
end
private
def migrating?
!(container_repository.default? ||
container_repository.import_done? ||
container_repository.import_skipped?)
end
def migration_duration
duration = migration.import_timeout.seconds + FIXED_DELAY
if container_repository.pre_importing?
duration += migration.dynamic_pre_import_timeout_for(container_repository)
end
duration
end
def migration
ContainerRegistry::Migration
end
# For ExclusiveLeaseGuard concern
def lease_key
@lease_key ||= "container_repository:delete:#{container_repository.id}"

View File

@ -0,0 +1,8 @@
---
name: container_registry_migration_phase2_delete_container_repository_worker_support
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/88997
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350543
milestone: '15.1'
type: development
group: group::package
default_enabled: false

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class DropCiPipelinesConfigPipelineIdSequence < Gitlab::Database::Migration[2.0]
enable_lock_retries!
def up
drop_sequence(:ci_pipelines_config, :pipeline_id, :ci_pipelines_config_pipeline_id_seq)
end
def down
add_sequence(:ci_pipelines_config, :pipeline_id, :ci_pipelines_config_pipeline_id_seq, 1)
end
end

View File

@ -0,0 +1 @@
64d492cca82603147226c9b0e6f424d2d2ba7a17ea0fe022510fb376016028e1

View File

@ -12901,15 +12901,6 @@ CREATE TABLE ci_pipelines_config (
content text NOT NULL
);
CREATE SEQUENCE ci_pipelines_config_pipeline_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE ci_pipelines_config_pipeline_id_seq OWNED BY ci_pipelines_config.pipeline_id;
CREATE SEQUENCE ci_pipelines_id_seq
START WITH 1
INCREMENT BY 1
@ -22669,8 +22660,6 @@ ALTER TABLE ONLY ci_pipeline_variables ALTER COLUMN id SET DEFAULT nextval('ci_p
ALTER TABLE ONLY ci_pipelines ALTER COLUMN id SET DEFAULT nextval('ci_pipelines_id_seq'::regclass);
ALTER TABLE ONLY ci_pipelines_config ALTER COLUMN pipeline_id SET DEFAULT nextval('ci_pipelines_config_pipeline_id_seq'::regclass);
ALTER TABLE ONLY ci_platform_metrics ALTER COLUMN id SET DEFAULT nextval('ci_platform_metrics_id_seq'::regclass);
ALTER TABLE ONLY ci_project_mirrors ALTER COLUMN id SET DEFAULT nextval('ci_project_mirrors_id_seq'::regclass);

View File

@ -680,35 +680,44 @@ Follow the steps below to configure the proxy listener of GitLab Pages.
1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
## Set global maximum pages size per project **(FREE SELF)**
## Set global maximum size of each GitLab Pages site **(FREE SELF)**
Prerequisites:
- Only GitLab administrators can edit this setting.
To set the global maximum pages size for a project:
1. On the top bar, select **Menu > Admin**.
1. On the left sidebar, select **Settings > Preferences**.
1. Expand **Pages**.
1. Edit the **Maximum size of pages**.
1. Enter a value under **Maximum size of pages**.
1. Select **Save changes**.
## Override maximum pages size per project or group **(PREMIUM SELF)**
## Set maximum size of each GitLab Pages site in a group **(PREMIUM SELF)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/16610) in GitLab 12.7.
Prerequisites:
NOTE:
Only GitLab administrators are able to view and override the **Maximum size of Pages** setting.
- You must have at least the Maintainer role for the group.
To override the global maximum pages size for a specific project:
1. On the top bar, select **Menu > Projects** and find your project.
1. On the left sidebar, select **Settings > Pages**.
1. Enter a value under **Maximum size of pages** in MB.
1. Select **Save changes**.
To override the global maximum pages size for a specific group:
To set the maximum size of each GitLab Pages site in a group, overriding the inherited setting:
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Settings > General**.
1. Expand **Pages**.
1. Enter a value under **Maximum size** in MB.
1. Select **Save changes**.
## Set maximum size of GitLab Pages site in a project **(PREMIUM SELF)**
Prerequisites:
- You must have at least the Maintainer role for the project.
To set the maximum size of GitLab Pages site in a project, overriding the inherited setting:
1. On the top bar, select **Menu > Projects** and find your project.
1. On the left sidebar, select **Settings > Pages**.
1. Enter a value under **Maximum size of pages** in MB.
1. Select **Save changes**.

View File

@ -209,6 +209,7 @@ The exceptions to the [original dotenv rules](https://github.com/motdotla/dotenv
self-managed instances is 150, and can be changed by changing the
`dotenv_variables` [application limit](../../administration/instance_limits.md#limit-dotenv-variables).
- Variable substitution in the `.env` file is not supported.
- [Multiline values in the `.env` file](https://github.com/motdotla/dotenv#multiline-values) are not supported.
- The `.env` file can't have empty lines or comments (starting with `#`).
- Key values in the `env` file cannot have spaces or newline characters (`\n`), including when using single or double quotes.
- Quote escaping during parsing (`key = 'value'` -> `{key: "value"}`) is not supported.

View File

@ -3998,18 +3998,20 @@ In this example, the script:
The following keywords are deprecated.
### Globally-defined `types`
<!--- start_remove The following content will be removed on remove_date: '2022-08-22' -->
WARNING:
`types` is deprecated, and is [scheduled to be removed in GitLab 15.0](https://gitlab.com/gitlab-org/gitlab/-/issues/346823).
### Globally-defined `types` (removed)
The `types` keyword was deprecated in GitLab 9.0, and [removed in GitLab 15.0](https://gitlab.com/gitlab-org/gitlab/-/issues/346823).
Use [`stages`](#stages) instead.
### Job-defined `type`
### Job-defined `type` (removed)
WARNING:
`type` is deprecated, and is [scheduled to be removed in GitLab 15.0](https://gitlab.com/gitlab-org/gitlab/-/issues/346823).
The `type` keyword was deprecated in GitLab 9.0, and [removed in GitLab 15.0](https://gitlab.com/gitlab-org/gitlab/-/issues/346823).
Use [`stage`](#stage) instead.
<!--- end_remove -->
### Globally-defined `image`, `services`, `cache`, `before_script`, `after_script`
Defining `image`, `services`, `cache`, `before_script`, and

View File

@ -284,7 +284,7 @@ See [troubleshooting batched background migrations](../user/admin_area/monitorin
## Dealing with running CI/CD pipelines and jobs
If you upgrade your GitLab instance while the GitLab Runner is processing jobs, the trace updates fail. When GitLab is back online, the trace updates should self-heal. However, depending on the error, the GitLab Runner either retries or eventually terminates job handling.
If you upgrade your GitLab instance while the GitLab Runner is processing jobs, the trace updates fail. When GitLab is back online, the trace updates should self-heal. However, depending on the error, the GitLab Runner either retries, or eventually terminates, job handling.
As for the artifacts, the GitLab Runner attempts to upload them three times, after which the job eventually fails.
@ -419,7 +419,7 @@ possible.
## Version-specific upgrading instructions
Each month, major, minor or patch releases of GitLab are published along with a
Each month, major, minor, or patch releases of GitLab are published along with a
[release post](https://about.gitlab.com/releases/categories/releases/).
You should read the release posts for all versions you're passing over.
At the end of major and minor release posts, there are three sections to look for specifically:
@ -432,7 +432,7 @@ These include:
- Steps you need to perform as part of an upgrade.
For example [8.12](https://about.gitlab.com/releases/2016/09/22/gitlab-8-12-released/#upgrade-barometer)
required the Elasticsearch index to be recreated. Any older version of GitLab upgrading to 8.12 or higher would require this.
required the Elasticsearch index to be recreated. Any older version of GitLab upgrading to 8.12 or later would require this.
- Changes to the versions of software we support such as
[ceasing support for IE11 in GitLab 13](https://about.gitlab.com/releases/2020/03/22/gitlab-12-9-released/#ending-support-for-internet-explorer-11).
@ -446,17 +446,42 @@ NOTE:
Specific information that follow related to Ruby and Git versions do not apply to [Omnibus installations](https://docs.gitlab.com/omnibus/)
and [Helm Chart deployments](https://docs.gitlab.com/charts/). They come with appropriate Ruby and Git versions and are not using system binaries for Ruby and Git. There is no need to install Ruby or Git when utilizing these two approaches.
### 15.1.0
- If you run external PostgreSQL, particularly AWS RDS,
[check you have a PostgreSQL bug fix](#postgresql-segmentation-fault-issue)
to avoid the database crashing.
### 15.0.0
- Elasticsearch 6.8 [is no longer supported](../integration/elasticsearch.md#version-requirements). Before you upgrade to GitLab 15.0, [update Elasticsearch to any 7.x version](../integration/elasticsearch.md#upgrade-to-a-new-elasticsearch-major-version).
- If you run external PostgreSQL, particularly AWS RDS,
[check you have a PostgreSQL bug fix](#postgresql-segmentation-fault-issue)
to avoid the database crashing.
### 14.10.0
- Before upgrading to GitLab 14.10, you need to already have the latest 14.9.Z installed on your instance.
- Before upgrading to GitLab 14.10, you must already have the latest 14.9.Z installed on your instance.
The upgrade to GitLab 14.10 executes a [concurrent index drop](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84308) of unneeded
entries from the `ci_job_artifacts` database table. This could potentially run for multiple minutes, especially if the table has a lot of
traffic and the migration is unable to acquire a lock. It is advised to let this process finish as restarting may result in data loss.
- If you run external PostgreSQL, particularly AWS RDS,
[check you have a PostgreSQL bug fix](#postgresql-segmentation-fault-issue)
to avoid the database crashing.
- Upgrading to patch level 14.10.3 or later might encounter a one-hour timeout due to a long running database data change,
if it was not completed while running GitLab 14.9.
```plaintext
FATAL: Mixlib::ShellOut::CommandTimeout: rails_migration[gitlab-rails]
(gitlab::database_migrations line 51) had an error:
[..]
Mixlib::ShellOut::CommandTimeout: Command timed out after 3600s:
```
A workaround exists to [complete the data change and the upgrade manually](package/index.md#mixlibshelloutcommandtimeout-rails_migrationgitlab-rails--command-timed-out-after-3600s).
### 14.9.0
- Database changes made by the upgrade to GitLab 14.9 can take hours or days to complete on larger GitLab instances.
@ -464,11 +489,11 @@ and [Helm Chart deployments](https://docs.gitlab.com/charts/). They come with ap
records in `namespaces` table for each record in `projects` table.
After you update to 14.9.0 or a later 14.9 patch version,
[batched background migrations need to finish](#batched-background-migrations)
[batched background migrations must finish](#batched-background-migrations)
before you update to a later version.
If the migrations are not finished and you try to update to a later version,
you'll see an error like:
you see errors like:
```plaintext
Expected batched background migration for the given configuration to be marked as 'finished', but it is 'active':
@ -497,10 +522,14 @@ and [Helm Chart deployments](https://docs.gitlab.com/charts/). They come with ap
end
```
- If you run external PostgreSQL, particularly AWS RDS,
[check you have a PostgreSQL bug fix](#postgresql-segmentation-fault-issue)
to avoid the database crashing.
### 14.8.0
- If upgrading from a version earlier than 14.6.5, 14.7.4, or 14.8.2, please review the [Critical Security Release: 14.8.2, 14.7.4, and 14.6.5](https://about.gitlab.com/releases/2022/02/25/critical-security-release-gitlab-14-8-2-released/) blog post.
Updating to 14.8.2 or later will reset runner registration tokens for your groups and projects.
- If upgrading from a version earlier than 14.6.5, 14.7.4, or 14.8.2, review the [Critical Security Release: 14.8.2, 14.7.4, and 14.6.5](https://about.gitlab.com/releases/2022/02/25/critical-security-release-gitlab-14-8-2-released/) blog post.
Updating to 14.8.2 or later resets runner registration tokens for your groups and projects.
- The agent server for Kubernetes [is enabled by default](https://about.gitlab.com/releases/2022/02/22/gitlab-14-8-released/#the-agent-server-for-kubernetes-is-enabled-by-default)
on Omnibus installations. If you run GitLab at scale,
such as [the reference architectures](../administration/reference_architectures/index.md),
@ -539,12 +568,15 @@ that may remain stuck permanently in a **pending** state.
[batched migration](../user/admin_area/monitoring/background_migrations.md) named
`BackfillNamespaceIdForNamespaceRoute`. You can [ignore](https://gitlab.com/gitlab-org/gitlab/-/issues/357822)
this. Retry it after you upgrade to version 14.9.x.
- If you run external PostgreSQL, particularly AWS RDS,
[check you have a PostgreSQL bug fix](#postgresql-segmentation-fault-issue)
to avoid the database crashing.
### 14.7.0
- See [LFS objects import and mirror issue in GitLab 14.6.0 to 14.7.2](#lfs-objects-import-and-mirror-issue-in-gitlab-1460-to-1472).
- If upgrading from a version earlier than 14.6.5, 14.7.4, or 14.8.2, please review the [Critical Security Release: 14.8.2, 14.7.4, and 14.6.5](https://about.gitlab.com/releases/2022/02/25/critical-security-release-gitlab-14-8-2-released/) blog post.
Updating to 14.7.4 or later will reset runner registration tokens for your groups and projects.
- If upgrading from a version earlier than 14.6.5, 14.7.4, or 14.8.2, review the [Critical Security Release: 14.8.2, 14.7.4, and 14.6.5](https://about.gitlab.com/releases/2022/02/25/critical-security-release-gitlab-14-8-2-released/) blog post.
Updating to 14.7.4 or later resets runner registration tokens for your groups and projects.
- GitLab 14.7 introduced a change where Gitaly expects persistent files in the `/tmp` directory.
When using the `noatime` mount option on `/tmp` in a node running Gitaly, most Linux distributions
run into [an issue with Git server hooks getting deleted](https://gitlab.com/gitlab-org/gitaly/-/issues/4113).
@ -563,8 +595,8 @@ that may remain stuck permanently in a **pending** state.
### 14.6.0
- See [LFS objects import and mirror issue in GitLab 14.6.0 to 14.7.2](#lfs-objects-import-and-mirror-issue-in-gitlab-1460-to-1472).
- If upgrading from a version earlier than 14.6.5, 14.7.4, or 14.8.2, please review the [Critical Security Release: 14.8.2, 14.7.4, and 14.6.5](https://about.gitlab.com/releases/2022/02/25/critical-security-release-gitlab-14-8-2-released/) blog post.
Updating to 14.6.5 or later will reset runner registration tokens for your groups and projects.
- If upgrading from a version earlier than 14.6.5, 14.7.4, or 14.8.2, review the [Critical Security Release: 14.8.2, 14.7.4, and 14.6.5](https://about.gitlab.com/releases/2022/02/25/critical-security-release-gitlab-14-8-2-released/) blog post.
Updating to 14.6.5 or later resets runner registration tokens for your groups and projects.
### 14.5.0
@ -574,17 +606,17 @@ or [init scripts](upgrading_from_source.md#configure-sysv-init-script) by [follo
- Connections between Workhorse and Gitaly use the Gitaly `backchannel` protocol by default. If you deployed a gRPC proxy between Workhorse and Gitaly,
Workhorse can no longer connect. As a workaround, [disable the temporary `workhorse_use_sidechannel`](../administration/feature_flags.md#enable-or-disable-the-feature)
feature flag. If you need a proxy between Workhorse and Gitaly, use a TCP proxy. If you have feedback about this change, please go to [this issue](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/1301).
feature flag. If you need a proxy between Workhorse and Gitaly, use a TCP proxy. If you have feedback about this change, go to [this issue](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/1301).
- In 14.1 we introduced a background migration that changes how we store merge request diff commits
in order to significantly reduce the amount of storage needed.
- In 14.1 we introduced a background migration that changes how we store merge request diff commits,
to significantly reduce the amount of storage needed.
In 14.5 we introduce a set of migrations that wrap up this process by making sure
that all remaining jobs over the `merge_request_diff_commits` table are completed.
These jobs will have already been processed in most cases so that no extra time is necessary during an upgrade to 14.5.
These jobs have already been processed in most cases so that no extra time is necessary during an upgrade to 14.5.
However, if there are remaining jobs or you haven't already upgraded to 14.1,
the deployment may take multiple hours to complete.
All merge request diff commits will automatically incorporate these changes, and there are no
All merge request diff commits automatically incorporate these changes, and there are no
additional requirements to perform the upgrade.
Existing data in the `merge_request_diff_commits` table remains unpacked until you run `VACUUM FULL merge_request_diff_commits`.
But note that the `VACUUM FULL` operation locks and rewrites the entire `merge_request_diff_commits` table,
@ -606,10 +638,22 @@ or [init scripts](upgrading_from_source.md#configure-sysv-init-script) by [follo
end
```
- Upgrading to 14.5 (or later) [might encounter a one hour timeout](https://gitlab.com/gitlab-org/gitlab/-/issues/354211)
owing to a long running database data change.
```plaintext
FATAL: Mixlib::ShellOut::CommandTimeout: rails_migration[gitlab-rails]
(gitlab::database_migrations line 51) had an error:
[..]
Mixlib::ShellOut::CommandTimeout: Command timed out after 3600s:
```
[There is a workaround to complete the data change and the upgrade manually](package/index.md#mixlibshelloutcommandtimeout-rails_migrationgitlab-rails--command-timed-out-after-3600s)
### 14.4.4
- For [zero-downtime upgrades](zero_downtime.md) on a GitLab cluster with separate Web and API nodes, you need to enable the `paginated_tree_graphql_query` [feature flag](../administration/feature_flags.md#enable-or-disable-the-feature) _before_ upgrading GitLab Web nodes to 14.4.
This is because we [enabled `paginated_tree_graphql_query` by default in 14.4](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70913/diffs), so if GitLab UI is on 14.4 and its API is on 14.3, the frontend will have this feature enabled but the backend will have it disabled. This will result in the following error:
- For [zero-downtime upgrades](zero_downtime.md) on a GitLab cluster with separate Web and API nodes, you must enable the `paginated_tree_graphql_query` [feature flag](../administration/feature_flags.md#enable-or-disable-the-feature) _before_ upgrading GitLab Web nodes to 14.4.
This is because we [enabled `paginated_tree_graphql_query` by default in 14.4](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70913/diffs), so if GitLab UI is on 14.4 and its API is on 14.3, the frontend has this feature enabled but the backend has it disabled. This results in the following error:
```shell
bundle.esm.js:63 Uncaught (in promise) Error: GraphQL error: Field 'paginatedTree' doesn't exist on type 'Repository'
@ -708,7 +752,7 @@ for how to proceed.
- [Instances running 14.0.0 - 14.0.4 should not upgrade directly to GitLab 14.2 or later](#upgrading-to-later-14y-releases)
but can upgrade to 14.1.Z.
It is not required for instances already running 14.0.5 (or higher) to stop at 14.1.Z.
It is not required for instances already running 14.0.5 (or later) to stop at 14.1.Z.
14.1 is included on the upgrade path for the broadest compatibility
with self-managed installations, and ensure 14.0.0-14.0.4 installations do not
encounter issues with [batched background migrations](#batched-background-migrations).
@ -733,18 +777,18 @@ Prerequisites:
Long running batched background database migrations:
- Database changes made by the upgrade to GitLab 14.0 can take hours or days to complete on larger GitLab instances.
These [batched background migrations](#batched-background-migrations) update whole database tables to mitigate primary key overflow and must be finished before upgrading to GitLab 14.2 or higher.
These [batched background migrations](#batched-background-migrations) update whole database tables to mitigate primary key overflow and must be finished before upgrading to GitLab 14.2 or later.
- Due to an issue where `BatchedBackgroundMigrationWorkers` were
[not working](https://gitlab.com/gitlab-org/charts/gitlab/-/issues/2785#note_614738345)
for self-managed instances, a [fix was created](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65106)
that requires an update to at least 14.0.5. The fix was also released in [14.1.0](#1410).
After you update to 14.0.5 or a later 14.0 patch version,
[batched background migrations need to finish](#batched-background-migrations)
[batched background migrations must finish](#batched-background-migrations)
before you update to a later version.
If the migrations are not finished and you try to update to a later version,
you'll see an error like:
you see an error like:
```plaintext
Expected batched background migration for the given configuration to be marked as 'finished', but it is 'active':
@ -769,7 +813,7 @@ Other issues:
1. Upgrade first to either:
- 14.0.5 or a later 14.0.Z patch release.
- 14.1.0 or a later 14.1.Z patch release.
1. [Batched background migrations need to finish](#batched-background-migrations)
1. [Batched background migrations must finish](#batched-background-migrations)
before you update to a later version [and may take longer than usual](#1400).
### 13.12.0
@ -777,7 +821,7 @@ Other issues:
- See [Maintenance mode issue in GitLab 13.9 to 14.4](#maintenance-mode-issue-in-gitlab-139-to-144).
- Check the GitLab database [has no references to legacy storage](../administration/raketasks/storage.md#on-legacy-storage).
The GitLab 14.0 pre-install check will cause the package update to fail if there is unmigrated data:
The GitLab 14.0 pre-install check causes the package update to fail if unmigrated data exists:
```plaintext
Checking for unmigrated data on legacy storage
@ -799,7 +843,7 @@ Other issues:
To prevent this risk of data loss, you must remove the content of the `RescheduleArtifactExpiryBackfillAgain`
migration, which makes it a no-op migration. You can repeat the changes from the
[commit that makes the migration no-op in 14.9 and later](https://gitlab.com/gitlab-org/gitlab/-/blob/42c3dfc5a1c8181767bbb5c76e7c5fa6fefbbc2b/db/post_migrate/20210413132500_reschedule_artifact_expiry_backfill_again.rb).
For more information, please see [how to disable a data migration](../development/database/deleting_migrations.md#how-to-disable-a-data-migration).
For more information, see [how to disable a data migration](../development/database/deleting_migrations.md#how-to-disable-a-data-migration).
### 13.10.0
@ -885,7 +929,7 @@ DETAIL: Key (project_id, type)=(NNN, ServiceName) is duplicated.
Ruby 2.7.2 is required. GitLab does not start with Ruby 2.6.6 or older versions.
The required Git version is Git v2.29 or higher.
The required Git version is Git v2.29 or later.
GitLab 13.6 includes a
[background migration `BackfillJiraTrackerDeploymentType2`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46368)
@ -902,7 +946,7 @@ end
### 13.4.0
GitLab 13.4.0 includes a background migration to [move all remaining repositories in legacy storage to hashed storage](../administration/raketasks/storage.md#migrate-to-hashed-storage). There are [known issues with this migration](https://gitlab.com/gitlab-org/gitlab/-/issues/259605) which are fixed in GitLab 13.5.4 and later. If possible, skip 13.4.0 and upgrade to 13.5.4 or higher instead. Note that the migration can take quite a while to run, depending on how many repositories must be moved. Be sure to check that all background migrations have completed before upgrading further.
GitLab 13.4.0 includes a background migration to [move all remaining repositories in legacy storage to hashed storage](../administration/raketasks/storage.md#migrate-to-hashed-storage). There are [known issues with this migration](https://gitlab.com/gitlab-org/gitlab/-/issues/259605) which are fixed in GitLab 13.5.4 and later. If possible, skip 13.4.0 and upgrade to 13.5.4 or later instead. The migration can take quite a while to run, depending on how many repositories must be moved. Be sure to check that all background migrations have completed before upgrading further.
### 13.3.0
@ -977,7 +1021,7 @@ If you persist your own Rack Attack initializers between upgrades, you might
- [GitLab 13.0 requires PostgreSQL 11](https://about.gitlab.com/releases/2020/05/22/gitlab-13-0-released/#postgresql-11-is-now-the-minimum-required-version-to-install-gitlab).
- 12.10 is the final release that shipped with PostgreSQL 9.6, 10, and 11.
- You should make sure that your database is PostgreSQL 11 on GitLab 12.10 before upgrading to 13.0. This will require downtime.
- You should make sure that your database is PostgreSQL 11 on GitLab 12.10 before upgrading to 13.0. This upgrade requires downtime.
### 12.2.0
@ -1017,7 +1061,7 @@ for more information.
When [Maintenance mode](../administration/maintenance_mode/index.md) is enabled, users cannot sign in with SSO, SAML, or LDAP.
Users who were signed in before Maintenance mode was enabled will continue to be signed in. If the administrator who enabled Maintenance mode loses their session, then they will not be able to disable Maintenance mode via the UI. In that case, you can [disable Maintenance mode via the API or Rails console](../administration/maintenance_mode/#disable-maintenance-mode).
Users who were signed in before Maintenance mode was enabled, continue to be signed in. If the administrator who enabled Maintenance mode loses their session, then they can't disable Maintenance mode via the UI. In that case, you can [disable Maintenance mode via the API or Rails console](../administration/maintenance_mode/#disable-maintenance-mode).
[This bug](https://gitlab.com/gitlab-org/gitlab/-/issues/329261) was fixed in GitLab 14.5.0 and backported into 14.4.3 and 14.3.5.
@ -1027,6 +1071,20 @@ When Geo is enabled, LFS objects fail to be saved for imported or mirrored proje
[This bug](https://gitlab.com/gitlab-org/gitlab/-/issues/352368) was fixed in GitLab 14.8.0 and backported into 14.7.3.
### PostgreSQL segmentation fault issue
If you run GitLab with external PostgreSQL, particularly AWS RDS, ensure you upgrade PostgreSQL
to patch levels to a minimum of 12.10 or 13.3 before upgrading to GitLab 14.8 or later.
[In 14.8](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75511)
for GitLab Enterprise Edition and [in 15.1](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/87983)
for GitLab Community Edition a GitLab feature called Loose Foreign Keys was enabled.
After it was enabled, we have had reports of unplanned PostgreSQL restarts caused
by a database engine bug that causes a segmentation fault.
Read more [in the issue](https://gitlab.com/gitlab-org/gitlab/-/issues/364763).
## Miscellaneous
- [MySQL to PostgreSQL](mysql_to_postgresql.md) guides you through migrating

View File

@ -308,3 +308,32 @@ To update the GPG key of the GitLab packages server run:
curl --silent "https://packages.gitlab.com/gpg.key" | apt-key add -
apt-get update
```
### `Mixlib::ShellOut::CommandTimeout: rails_migration[gitlab-rails] [..] Command timed out after 3600s`
If database schema and data changes (database migrations) must take more than one hour to run,
upgrades fail with a `timed out` error:
```plaintext
FATAL: Mixlib::ShellOut::CommandTimeout: rails_migration[gitlab-rails] (gitlab::database_migrations line 51)
had an error: Mixlib::ShellOut::CommandTimeout: bash[migrate gitlab-rails database]
(/opt/gitlab/embedded/cookbooks/cache/cookbooks/gitlab/resources/rails_migration.rb line 16)
had an error: Mixlib::ShellOut::CommandTimeout: Command timed out after 3600s:
```
To fix this error:
1. Run the remaining database migrations:
```shell
sudo gitlab-rake db:migrate
```
This command may take a very long time to complete. Use `screen` or some other mechanism to ensure
the program is not interrupted if your SSH session drops.
1. Complete the upgrade:
```shell
sudo gitlab-ctl reconfigure
```

View File

@ -133,7 +133,7 @@ Below are the settings for [GitLab Pages](https://about.gitlab.com/stages-devops
| IP address | `35.185.44.232` | - |
| Custom domains support | **{check-circle}** Yes | **{dotted-circle}** No |
| TLS certificates support | **{check-circle}** Yes | **{dotted-circle}** No |
| [Maximum size](../../administration/pages/index.md#set-global-maximum-pages-size-per-project) (compressed) | 1 GB | 100 MB |
| [Maximum size](../../administration/pages/index.md#set-global-maximum-size-of-each-gitlab-pages-site) (compressed) | 1 GB | 100 MB |
The maximum size of your Pages site is also regulated by the artifacts maximum size,
which is part of [GitLab CI/CD](#gitlab-cicd).

View File

@ -19,7 +19,7 @@ and from merge requests:
- *When viewing a file, or the repository file list* -
1. In the upper right corner of the page, select **Open in Web IDE** if it is visible.
1. If **Open in Web IDE** is not visible:
1. Select the **(angle-down)** next to **Edit** or **Gitpod**, depending on your configuration.
1. Select the (**{chevron-down}**) next to **Edit** or **Gitpod**, depending on your configuration.
1. Select **Open in Web IDE** from the list to display it as the editing option.
1. Select **Open in Web IDE** to open the editor.
- *When viewing a merge request* -

View File

@ -114,7 +114,9 @@ module API
module_version: params[:module_version]
)
jwt_token = Gitlab::TerraformRegistryToken.from_token(token_from_namespace_inheritable).encoded
if token_from_namespace_inheritable
jwt_token = Gitlab::TerraformRegistryToken.from_token(token_from_namespace_inheritable).encoded
end
header 'X-Terraform-Get', module_file_path.sub(%r{module_version/file$}, "#{params[:module_version]}/file?token=#{jwt_token}&archive=tgz")
status :no_content

View File

@ -43,6 +43,10 @@ module ContainerRegistry
Feature.enabled?(:container_registry_migration_limit_gitlab_org)
end
def self.delete_container_repository_worker_support?
Feature.enabled?(:container_registry_migration_phase2_delete_container_repository_worker_support)
end
def self.enqueue_waiting_time
return 0 if Feature.enabled?(:container_registry_migration_phase2_enqueue_speed_fast)
return 165.minutes if Feature.enabled?(:container_registry_migration_phase2_enqueue_speed_slow)
@ -73,5 +77,9 @@ module ContainerRegistry
def self.all_plans?
Feature.enabled?(:container_registry_migration_phase2_all_plans)
end
def self.dynamic_pre_import_timeout_for(repository)
(repository.tags_count * pre_import_tags_rate).seconds
end
end
end

View File

@ -8,7 +8,6 @@ namespace :gitlab do
# These aren't used by anything so we can ignore these https://gitlab.com/gitlab-org/gitlab/-/issues/362984
EXCLUDED_SEQUENCES = %w[
ci_job_artifact_states_job_artifact_id_seq
ci_pipelines_config_pipeline_id_seq
].freeze
desc 'Bump all the CI tables sequences on the Main Database'

View File

@ -8987,6 +8987,18 @@ msgstr ""
msgid "Collector hostname"
msgstr ""
msgid "ColorWidget|An error occurred while updating color."
msgstr ""
msgid "ColorWidget|Assign epic color"
msgstr ""
msgid "ColorWidget|Color"
msgstr ""
msgid "ColorWidget|Error fetching epic color."
msgstr ""
msgid "Colorize messages"
msgstr ""
@ -11253,6 +11265,9 @@ msgstr ""
msgid "DORA4Metrics|Date"
msgstr ""
msgid "DORA4Metrics|Days for an open incident"
msgstr ""
msgid "DORA4Metrics|Days from merge to deploy"
msgstr ""
@ -11265,6 +11280,15 @@ msgstr ""
msgid "DORA4Metrics|Median (last %{days}d)"
msgstr ""
msgid "DORA4Metrics|Median time (last %{days}d)"
msgstr ""
msgid "DORA4Metrics|Median time an incident was open in a production environment over the given time period."
msgstr ""
msgid "DORA4Metrics|No incidents during this period"
msgstr ""
msgid "DORA4Metrics|No merge requests were deployed during this period"
msgstr ""
@ -11277,12 +11301,18 @@ msgstr ""
msgid "DORA4Metrics|Something went wrong while getting lead time data."
msgstr ""
msgid "DORA4Metrics|Something went wrong while getting time to restore service data."
msgstr ""
msgid "DORA4Metrics|The chart displays the frequency of deployments to production environment(s) that are based on the %{linkStart}deployment_tier%{linkEnd} value."
msgstr ""
msgid "DORA4Metrics|The chart displays the median time between a merge request being merged and deployed to production environment(s) that are based on the %{linkStart}deployment_tier%{linkEnd} value."
msgstr ""
msgid "DORA4Metrics|Time to restore service"
msgstr ""
msgid "DSN"
msgstr ""
@ -17256,6 +17286,9 @@ msgstr ""
msgid "GitLabPages|Are you sure?"
msgstr ""
msgid "GitLabPages|Can be overridden per project. For no limit, enter 0. To inherit the value, leave empty."
msgstr ""
msgid "GitLabPages|Certificate: %{subject}"
msgstr ""
@ -17277,7 +17310,7 @@ msgstr ""
msgid "GitLabPages|GitLab Pages are disabled for this project. You can enable them on your project's %{strong_start}Settings &gt; General &gt; Visibility%{strong_end} page."
msgstr ""
msgid "GitLabPages|Maximum size of pages (MB)"
msgid "GitLabPages|Maximum size (MB)"
msgstr ""
msgid "GitLabPages|New Domain"
@ -17313,9 +17346,6 @@ msgstr ""
msgid "GitLabPages|Support for domains and certificates is disabled. Ask your system's administrator to enable it."
msgstr ""
msgid "GitLabPages|The total size of deployed static content will be limited to this size. 0 for unlimited. Leave empty to inherit the global value."
msgstr ""
msgid "GitLabPages|Unverified"
msgstr ""
@ -18288,10 +18318,10 @@ msgstr ""
msgid "GroupSettings|Select the project that contains your custom Insights file."
msgstr ""
msgid "GroupSettings|Set the initial name and protections for the default branch of new repositories created in the group."
msgid "GroupSettings|Set a size limit for all content in each Pages site in this group. %{link_start}Learn more.%{link_end}"
msgstr ""
msgid "GroupSettings|Set the maximum size of GitLab Pages for this group. %{link_start}Learn more.%{link_end}"
msgid "GroupSettings|Set the initial name and protections for the default branch of new repositories created in the group."
msgstr ""
msgid "GroupSettings|The Auto DevOps pipeline runs if no alternative CI configuration file is found."
@ -34068,6 +34098,15 @@ msgstr ""
msgid "SecurityOrchestration|This group"
msgstr ""
msgid "SecurityOrchestration|This is a group-level policy"
msgstr ""
msgid "SecurityOrchestration|This is a project-level policy"
msgstr ""
msgid "SecurityOrchestration|This policy is inherited from %{namespace}"
msgstr ""
msgid "SecurityOrchestration|This policy is inherited from the %{linkStart}namespace%{linkEnd} and must be edited there"
msgstr ""
@ -36823,6 +36862,9 @@ msgstr ""
msgid "SuggestedColors|Gray"
msgstr ""
msgid "SuggestedColors|Green"
msgstr ""
msgid "SuggestedColors|Green screen"
msgstr ""
@ -36841,6 +36883,9 @@ msgstr ""
msgid "SuggestedColors|Orange"
msgstr ""
msgid "SuggestedColors|Purple"
msgstr ""
msgid "SuggestedColors|Red"
msgstr ""

View File

@ -6,10 +6,10 @@ module Gitlab
module Settings
class UsageQuotas < Chemlab::Page
# TODO: Supplant with data-qa-selectors
link :pipeline_tab, id: 'pipelines-quota'
link :storage_tab, id: 'storage-quota'
link :buy_ci_minutes, text: 'Buy additional minutes'
link :buy_storage, text: /Buy storage/
link :pipelines_tab
link :storage_tab
link :buy_ci_minutes
link :buy_storage
div :plan_ci_minutes
div :additional_ci_minutes
span :purchased_usage_total

View File

@ -26,7 +26,7 @@ module QA
def purchase_ci_minutes(quantity: 1)
Page::Group::Menu.perform(&:go_to_usage_quotas)
Gitlab::Page::Group::Settings::UsageQuotas.perform do |usage_quota|
usage_quota.pipeline_tab
usage_quota.pipelines_tab
usage_quota.buy_ci_minutes
end

View File

@ -305,11 +305,37 @@ RSpec.describe Groups::GroupMembersController do
group.add_owner(user)
end
it 'cannot removes himself from the group' do
it 'cannot remove user from the group' do
delete :leave, params: { group_id: group }
expect(response).to have_gitlab_http_status(:forbidden)
end
context 'and there is a group project bot owner' do
before do
create(:group_member, :owner, source: group, user: create(:user, :project_bot))
end
it 'cannot remove user from the group' do
delete :leave, params: { group_id: group }
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'and there is another owner' do
before do
create(:group_member, :owner, source: group)
end
it 'removes user from members', :aggregate_failures do
delete :leave, params: { group_id: group }
expect(controller).to set_flash.to "You left the \"#{group.name}\" group."
expect(response).to redirect_to(dashboard_groups_path)
expect(group.users).not_to include user
end
end
end
context 'and is a requester' do

View File

@ -96,19 +96,6 @@ RSpec.describe Import::GithubController do
describe "POST personal_access_token" do
it_behaves_like 'a GitHub-ish import controller: POST personal_access_token'
it 'passes namespace_id param as query param if it was present' do
namespace_id = 5
status_import_url = public_send("status_import_#{provider}_url", { namespace_id: namespace_id })
allow_next_instance_of(Gitlab::LegacyGithubImport::Client) do |client|
allow(client).to receive(:user).and_return(true)
end
post :personal_access_token, params: { personal_access_token: 'some-token', namespace_id: 5 }
expect(controller).to redirect_to(status_import_url)
end
end
describe "GET status" do

View File

@ -13,16 +13,16 @@ export default function initVueMRPage() {
const diffsAppProjectPath = 'testproject';
const mrEl = document.createElement('div');
mrEl.className = 'merge-request fixture-mr';
mrEl.setAttribute('data-mr-action', 'diffs');
mrEl.dataset.mrAction = 'diffs';
mrTestEl.appendChild(mrEl);
const mrDiscussionsEl = document.createElement('div');
mrDiscussionsEl.id = 'js-vue-mr-discussions';
mrDiscussionsEl.setAttribute('data-current-user-data', JSON.stringify(userDataMock));
mrDiscussionsEl.setAttribute('data-noteable-data', JSON.stringify(noteableDataMock));
mrDiscussionsEl.setAttribute('data-notes-data', JSON.stringify(notesDataMock));
mrDiscussionsEl.setAttribute('data-noteable-type', 'merge-request');
mrDiscussionsEl.setAttribute('data-is-locked', 'false');
mrDiscussionsEl.dataset.currentUserData = JSON.stringify(userDataMock);
mrDiscussionsEl.dataset.noteableData = JSON.stringify(noteableDataMock);
mrDiscussionsEl.dataset.notesData = JSON.stringify(notesDataMock);
mrDiscussionsEl.dataset.noteableType = 'merge-request';
mrDiscussionsEl.dataset.isLocked = 'false';
mrTestEl.appendChild(mrDiscussionsEl);
const discussionCounterEl = document.createElement('div');
@ -31,9 +31,9 @@ export default function initVueMRPage() {
const diffsAppEl = document.createElement('div');
diffsAppEl.id = 'js-diffs-app';
diffsAppEl.setAttribute('data-endpoint', diffsAppEndpoint);
diffsAppEl.setAttribute('data-project-path', diffsAppProjectPath);
diffsAppEl.setAttribute('data-current-user-data', JSON.stringify(userDataMock));
diffsAppEl.dataset.endpoint = diffsAppEndpoint;
diffsAppEl.dataset.projectPath = diffsAppProjectPath;
diffsAppEl.dataset.currentUserData = JSON.stringify(userDataMock);
mrTestEl.appendChild(diffsAppEl);
const mock = new MockAdapter(axios);

View File

@ -9,7 +9,7 @@ export const toHaveSpriteIcon = (element, iconName) => {
const iconReferences = [].slice.apply(element.querySelectorAll('svg use'));
const matchingIcon = iconReferences.find(
(reference) => reference.parentNode.getAttribute('data-testid') === `${iconName}-icon`,
(reference) => reference.parentNode.dataset.testid === `${iconName}-icon`,
);
const pass = Boolean(matchingIcon);

View File

@ -12,8 +12,8 @@ describe('initAdminUsersApp', () => {
beforeEach(() => {
el = document.createElement('div');
el.setAttribute('data-users', JSON.stringify(users));
el.setAttribute('data-paths', JSON.stringify(paths));
el.dataset.users = JSON.stringify(users);
el.dataset.paths = JSON.stringify(paths);
wrapper = createWrapper(initAdminUsersApp(el));
});
@ -40,8 +40,8 @@ describe('initAdminUserActions', () => {
beforeEach(() => {
el = document.createElement('div');
el.setAttribute('data-user', JSON.stringify(user));
el.setAttribute('data-paths', JSON.stringify(paths));
el.dataset.user = JSON.stringify(user);
el.dataset.paths = JSON.stringify(paths);
wrapper = createWrapper(initAdminUserActions(el));
});

View File

@ -15,8 +15,8 @@ describe('initRecoveryCodes', () => {
beforeEach(() => {
el = document.createElement('div');
el.setAttribute('class', 'js-2fa-recovery-codes');
el.setAttribute('data-codes', codesJsonString);
el.setAttribute('data-profile-account-path', profileAccountPath);
el.dataset.codes = codesJsonString;
el.dataset.profileAccountPath = profileAccountPath;
document.body.appendChild(el);
wrapper = createWrapper(initRecoveryCodes());

View File

@ -11,7 +11,7 @@ function createComponent() {
}
async function setLoaded(loaded) {
document.querySelector('.blob-viewer').setAttribute('data-loaded', loaded);
document.querySelector('.blob-viewer').dataset.loaded = loaded;
await nextTick();
}
@ -53,7 +53,7 @@ describe('Markdown table of contents component', () => {
it('does not show dropdown when viewing non-rich content', async () => {
createComponent();
document.querySelector('.blob-viewer').setAttribute('data-type', 'simple');
document.querySelector('.blob-viewer').dataset.type = 'simple';
await setLoaded(true);

View File

@ -80,9 +80,9 @@ describe('Blob viewer', () => {
return asyncClick()
.then(() => asyncClick())
.then(() => {
expect(
document.querySelector('.blob-viewer[data-type="simple"]').getAttribute('data-loaded'),
).toBe('true');
expect(document.querySelector('.blob-viewer[data-type="simple"]').dataset.loaded).toBe(
'true',
);
});
});

View File

@ -21,12 +21,12 @@ describe('LockPopovers', () => {
};
if (lockedByApplicationSetting) {
popoverMountEl.setAttribute('data-popover-data', JSON.stringify(popoverData));
popoverMountEl.dataset.popoverData = JSON.stringify(popoverData);
} else if (lockedByAncestor) {
popoverMountEl.setAttribute(
'data-popover-data',
JSON.stringify({ ...popoverData, ancestor_namespace: mockNamespace }),
);
popoverMountEl.dataset.popoverData = JSON.stringify({
...popoverData,
ancestor_namespace: mockNamespace,
});
}
document.body.appendChild(popoverMountEl);

View File

@ -195,8 +195,8 @@ describe('Code navigation actions', () => {
it('commits SET_CURRENT_DEFINITION with LSIF data', () => {
target.classList.add('js-code-navigation');
target.setAttribute('data-line-index', '0');
target.setAttribute('data-char-index', '0');
target.dataset.lineIndex = '0';
target.dataset.charIndex = '0';
return testAction(
actions.showDefinition,
@ -218,8 +218,8 @@ describe('Code navigation actions', () => {
it('adds hll class to target element', () => {
target.classList.add('js-code-navigation');
target.setAttribute('data-line-index', '0');
target.setAttribute('data-char-index', '0');
target.dataset.lineIndex = '0';
target.dataset.charIndex = '0';
return testAction(
actions.showDefinition,
@ -243,8 +243,8 @@ describe('Code navigation actions', () => {
it('caches current target element', () => {
target.classList.add('js-code-navigation');
target.setAttribute('data-line-index', '0');
target.setAttribute('data-char-index', '0');
target.dataset.lineIndex = '0';
target.dataset.charIndex = '0';
return testAction(
actions.showDefinition,

View File

@ -31,9 +31,9 @@ describe('ConfirmModal', () => {
buttons.forEach((x) => {
const button = document.createElement('button');
button.setAttribute('class', 'js-confirm-modal-button');
button.setAttribute('data-path', x.path);
button.setAttribute('data-method', x.method);
button.setAttribute('data-modal-attributes', JSON.stringify(x.modalAttributes));
button.dataset.path = x.path;
button.dataset.method = x.method;
button.dataset.modalAttributes = JSON.stringify(x.modalAttributes);
button.innerHTML = 'Action';
buttonContainer.appendChild(button);
});

View File

@ -59,9 +59,10 @@ describe('waitForCSSLoaded', () => {
<link href="two.css" data-startupcss="loading">
`);
const events = waitForCSSLoaded(mockedCallback);
document
.querySelectorAll('[data-startupcss="loading"]')
.forEach((elem) => elem.setAttribute('data-startupcss', 'loaded'));
document.querySelectorAll('[data-startupcss="loading"]').forEach((elem) => {
// eslint-disable-next-line no-param-reassign
elem.dataset.startupcss = 'loaded';
});
document.dispatchEvent(new CustomEvent('CSSStartupLinkLoaded'));
await events;

View File

@ -84,7 +84,7 @@ describe('CreateMergeRequestDropdown', () => {
});
it('enables when can create confidential issue', () => {
document.querySelector('.js-create-mr').setAttribute('data-is-confidential', 'true');
document.querySelector('.js-create-mr').dataset.isConfidential = 'true';
confidentialState.selectedProject = { name: 'test' };
dropdown.enable();
@ -93,7 +93,7 @@ describe('CreateMergeRequestDropdown', () => {
});
it('does not enable when can not create confidential issue', () => {
document.querySelector('.js-create-mr').setAttribute('data-is-confidential', 'true');
document.querySelector('.js-create-mr').dataset.isConfidential = 'true';
dropdown.enable();

View File

@ -25,11 +25,11 @@ describe('DeleteLabelModal', () => {
buttons.forEach((x) => {
const button = document.createElement('button');
button.setAttribute('class', 'js-delete-label-modal-button');
button.setAttribute('data-label-name', x.labelName);
button.setAttribute('data-destroy-path', x.destroyPath);
button.dataset.labelName = x.labelName;
button.dataset.destroyPath = x.destroyPath;
if (x.subjectName) {
button.setAttribute('data-subject-name', x.subjectName);
button.dataset.subjectName = x.subjectName;
}
button.innerHTML = 'Action';

View File

@ -27,7 +27,7 @@ describe('LazyLoader', () => {
const createLazyLoadImage = () => {
const newImg = document.createElement('img');
newImg.className = 'lazy';
newImg.setAttribute('data-src', TEST_PATH);
newImg.dataset.src = TEST_PATH;
document.body.appendChild(newImg);
triggerChildMutation();
@ -108,7 +108,7 @@ describe('LazyLoader', () => {
expect(LazyLoader.loadImage).toHaveBeenCalledWith(img);
expect(img.getAttribute('src')).toBe(TEST_PATH);
expect(img.getAttribute('data-src')).toBe(null);
expect(img.dataset.src).toBeUndefined();
expect(img).toHaveClass('js-lazy-loaded');
});

View File

@ -24,7 +24,7 @@ describe('initMembersApp', () => {
beforeEach(() => {
el = document.createElement('div');
el.setAttribute('data-members-data', dataAttribute);
el.dataset.membersData = dataAttribute;
window.gon = { current_user_id: 123 };
});

View File

@ -256,7 +256,7 @@ describe('Members Utils', () => {
beforeEach(() => {
el = document.createElement('div');
el.setAttribute('data-members-data', dataAttribute);
el.dataset.membersData = dataAttribute;
});
afterEach(() => {

View File

@ -78,8 +78,8 @@ describe('Markdown component', () => {
});
await nextTick();
expect(findLink().getAttribute('data-remote')).toBe(null);
expect(findLink().getAttribute('data-type')).toBe(null);
expect(findLink().dataset.remote).toBeUndefined();
expect(findLink().dataset.type).toBeUndefined();
});
describe('When parsing images', () => {

View File

@ -404,13 +404,13 @@ describe('Actions Notes Store', () => {
beforeEach(() => {
axiosMock.onDelete(endpoint).replyOnce(200, {});
document.body.setAttribute('data-page', '');
document.body.dataset.page = '';
});
afterEach(() => {
axiosMock.restore();
document.body.setAttribute('data-page', '');
document.body.dataset.page = '';
});
it('commits DELETE_NOTE and dispatches updateMergeRequestWidget', () => {
@ -440,7 +440,7 @@ describe('Actions Notes Store', () => {
it('dispatches removeDiscussionsFromDiff on merge request page', () => {
const note = { path: endpoint, id: 1 };
document.body.setAttribute('data-page', 'projects:merge_requests:show');
document.body.dataset.page = 'projects:merge_requests:show';
return testAction(
actions.removeNote,
@ -473,13 +473,13 @@ describe('Actions Notes Store', () => {
beforeEach(() => {
axiosMock.onDelete(endpoint).replyOnce(200, {});
document.body.setAttribute('data-page', '');
document.body.dataset.page = '';
});
afterEach(() => {
axiosMock.restore();
document.body.setAttribute('data-page', '');
document.body.dataset.page = '';
});
it('dispatches removeNote', () => {

View File

@ -17,11 +17,11 @@ describe('performance bar wrapper', () => {
performance.getEntriesByType = jest.fn().mockReturnValue([]);
peekWrapper.setAttribute('id', 'js-peek');
peekWrapper.setAttribute('data-env', 'development');
peekWrapper.setAttribute('data-request-id', '123');
peekWrapper.setAttribute('data-peek-url', '/-/peek/results');
peekWrapper.setAttribute('data-stats-url', 'https://log.gprd.gitlab.net/app/dashboards#/view/');
peekWrapper.setAttribute('data-profile-url', '?lineprofiler=true');
peekWrapper.dataset.env = 'development';
peekWrapper.dataset.requestId = '123';
peekWrapper.dataset.peekUrl = '/-/peek/results';
peekWrapper.dataset.statsUrl = 'https://log.gprd.gitlab.net/app/dashboards#/view/';
peekWrapper.dataset.profileUrl = '?lineprofiler=true';
mock = new MockAdapter(axios);

View File

@ -53,7 +53,7 @@ describe('Search autocomplete dropdown', () => {
};
const disableProjectIssues = () => {
document.querySelector('.js-search-project-options').setAttribute('data-issues-disabled', true);
document.querySelector('.js-search-project-options').dataset.issuesDisabled = true;
};
// Mock `gl` object in window for dashboard specific page. App code will need it.

View File

@ -22,7 +22,7 @@ describe('User Popovers', () => {
const link = document.createElement('a');
link.classList.add('js-user-link');
link.setAttribute('data-user', '1');
link.dataset.user = '1';
return link;
};

View File

@ -95,10 +95,10 @@ export const setAssignees = (...users) => {
const input = document.createElement('input');
input.name = 'merge_request[assignee_ids][]';
input.value = user.id.toString();
input.setAttribute('data-avatar-url', user.avatar_url);
input.setAttribute('data-name', user.name);
input.setAttribute('data-username', user.username);
input.setAttribute('data-can-merge', user.can_merge);
input.dataset.avatarUrl = user.avatar_url;
input.dataset.name = user.name;
input.dataset.username = user.username;
input.dataset.canMerge = user.can_merge;
return input;
}),
);

View File

@ -193,9 +193,7 @@ describe('MRWidgetMerged', () => {
it('shows button to copy commit SHA to clipboard', () => {
expect(selectors.copyMergeShaButton).not.toBe(null);
expect(selectors.copyMergeShaButton.getAttribute('data-clipboard-text')).toBe(
vm.mr.mergeCommitSha,
);
expect(selectors.copyMergeShaButton.dataset.clipboardText).toBe(vm.mr.mergeCommitSha);
});
it('hides button to copy commit SHA if SHA does not exist', async () => {

View File

@ -424,7 +424,7 @@ describe('MrWidgetOptions', () => {
beforeEach(() => {
const favicon = document.createElement('link');
favicon.setAttribute('id', 'favicon');
favicon.setAttribute('data-original-href', faviconDataUrl);
favicon.dataset.originalHref = faviconDataUrl;
document.body.appendChild(favicon);
faviconElement = document.getElementById('favicon');

View File

@ -0,0 +1,35 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { hexToRgb } from '~/lib/utils/color_utils';
import ColorItem from '~/vue_shared/components/color_select_dropdown/color_item.vue';
import { color } from './mock_data';
describe('ColorItem', () => {
let wrapper;
const propsData = color;
const createComponent = () => {
wrapper = shallowMountExtended(ColorItem, {
propsData,
});
};
const findColorItem = () => wrapper.findByTestId('color-item');
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('renders the correct title', () => {
expect(wrapper.text()).toBe(propsData.title);
});
it('renders the correct background color for the color item', () => {
const convertedColor = hexToRgb(propsData.color).join(', ');
expect(findColorItem().attributes('style')).toBe(`background-color: rgb(${convertedColor});`);
});
});

View File

@ -0,0 +1,192 @@
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import DropdownContents from '~/vue_shared/components/color_select_dropdown/dropdown_contents.vue';
import DropdownValue from '~/vue_shared/components/color_select_dropdown/dropdown_value.vue';
import epicColorQuery from '~/vue_shared/components/color_select_dropdown/graphql/epic_color.query.graphql';
import updateEpicColorMutation from '~/vue_shared/components/color_select_dropdown/graphql/epic_update_color.mutation.graphql';
import ColorSelectRoot from '~/vue_shared/components/color_select_dropdown/color_select_root.vue';
import { DROPDOWN_VARIANT } from '~/vue_shared/components/color_select_dropdown/constants';
import { colorQueryResponse, updateColorMutationResponse, color } from './mock_data';
jest.mock('~/flash');
Vue.use(VueApollo);
const successfulQueryHandler = jest.fn().mockResolvedValue(colorQueryResponse);
const successfulMutationHandler = jest.fn().mockResolvedValue(updateColorMutationResponse);
const errorQueryHandler = jest.fn().mockRejectedValue('Error fetching epic color.');
const errorMutationHandler = jest.fn().mockRejectedValue('An error occurred while updating color.');
const defaultProps = {
allowEdit: true,
iid: '1',
fullPath: 'workspace-1',
};
describe('LabelsSelectRoot', () => {
let wrapper;
const findSidebarEditableItem = () => wrapper.findComponent(SidebarEditableItem);
const findDropdownValue = () => wrapper.findComponent(DropdownValue);
const findDropdownContents = () => wrapper.findComponent(DropdownContents);
const createComponent = ({
queryHandler = successfulQueryHandler,
mutationHandler = successfulMutationHandler,
propsData,
} = {}) => {
const mockApollo = createMockApollo([
[epicColorQuery, queryHandler],
[updateEpicColorMutation, mutationHandler],
]);
wrapper = shallowMount(ColorSelectRoot, {
apolloProvider: mockApollo,
propsData: {
...defaultProps,
...propsData,
},
provide: {
canUpdate: true,
},
stubs: {
SidebarEditableItem,
},
});
};
afterEach(() => {
wrapper.destroy();
});
describe('template', () => {
const defaultClasses = ['labels-select-wrapper', 'gl-relative'];
it.each`
variant | cssClass
${'sidebar'} | ${defaultClasses}
${'embedded'} | ${[...defaultClasses, 'is-embedded']}
`(
'renders component root element with CSS class `$cssClass` when variant is "$variant"',
async ({ variant, cssClass }) => {
createComponent({
propsData: { variant },
});
expect(wrapper.classes()).toEqual(cssClass);
},
);
});
describe('if the variant is `sidebar`', () => {
beforeEach(() => {
createComponent();
});
it('renders SidebarEditableItem component', () => {
expect(findSidebarEditableItem().exists()).toBe(true);
});
it('renders correct props for the SidebarEditableItem component', () => {
expect(findSidebarEditableItem().props()).toMatchObject({
title: wrapper.vm.$options.i18n.widgetTitle,
canEdit: defaultProps.allowEdit,
loading: true,
});
});
describe('when colors are loaded', () => {
beforeEach(async () => {
createComponent();
await waitForPromises();
});
it('passes false `loading` prop to sidebar editable item', () => {
expect(findSidebarEditableItem().props('loading')).toBe(false);
});
it('renders dropdown value component when query colors is resolved', () => {
expect(findDropdownValue().props('selectedColor')).toMatchObject(color);
});
});
});
describe('if the variant is `embedded`', () => {
beforeEach(() => {
createComponent({ propsData: { iid: undefined, variant: DROPDOWN_VARIANT.Embedded } });
});
it('renders DropdownContents component', () => {
expect(findDropdownContents().exists()).toBe(true);
});
it('renders correct props for the DropdownContents component', () => {
expect(findDropdownContents().props()).toMatchObject({
variant: DROPDOWN_VARIANT.Embedded,
dropdownTitle: wrapper.vm.$options.i18n.assignColor,
dropdownButtonText: wrapper.vm.$options.i18n.dropdownButtonText,
});
});
it('handles DropdownContents setColor', () => {
findDropdownContents().vm.$emit('setColor', color);
expect(wrapper.emitted('updateSelectedColor')).toEqual([[color]]);
});
});
describe('when epicColorQuery errored', () => {
beforeEach(async () => {
createComponent({ queryHandler: errorQueryHandler });
await waitForPromises();
});
it('creates flash with error message', () => {
expect(createFlash).toHaveBeenCalledWith({
captureError: true,
message: 'Error fetching epic color.',
});
});
});
it('emits `updateSelectedColor` event on dropdown contents `setColor` event if iid is not set', () => {
createComponent({ propsData: { iid: undefined } });
findDropdownContents().vm.$emit('setColor', color);
expect(wrapper.emitted('updateSelectedColor')).toEqual([[color]]);
});
describe('when updating color for epic', () => {
beforeEach(() => {
createComponent();
findDropdownContents().vm.$emit('setColor', color);
});
it('sets the loading state', () => {
expect(findSidebarEditableItem().props('loading')).toBe(true);
});
it('updates color correctly after successful mutation', async () => {
await waitForPromises();
expect(findDropdownValue().props('selectedColor').color).toEqual(
updateColorMutationResponse.data.updateIssuableColor.issuable.color,
);
});
it('displays an error if mutation was rejected', async () => {
createComponent({ mutationHandler: errorMutationHandler });
findDropdownContents().vm.$emit('setColor', color);
await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({
captureError: true,
error: expect.anything(),
message: 'An error occurred while updating color.',
});
});
});
});

View File

@ -0,0 +1,43 @@
import { GlDropdownForm } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import DropdownContentsColorView from '~/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue';
import ColorItem from '~/vue_shared/components/color_select_dropdown/color_item.vue';
import { ISSUABLE_COLORS } from '~/vue_shared/components/color_select_dropdown/constants';
import { color as defaultColor } from './mock_data';
const propsData = {
selectedColor: defaultColor,
};
describe('DropdownContentsColorView', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMount(DropdownContentsColorView, {
propsData,
});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
const findColors = () => wrapper.findAllComponents(ColorItem);
const findColorList = () => wrapper.findComponent(GlDropdownForm);
it('renders color list', async () => {
expect(findColorList().exists()).toBe(true);
expect(findColors()).toHaveLength(ISSUABLE_COLORS.length);
});
it.each(ISSUABLE_COLORS)('emits an `input` event with %o on click on the option %#', (color) => {
const colorIndex = ISSUABLE_COLORS.indexOf(color);
findColors().at(colorIndex).trigger('click');
expect(wrapper.emitted('input')[0][0]).toMatchObject(color);
});
});

View File

@ -0,0 +1,113 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { DROPDOWN_VARIANT } from '~/vue_shared/components/color_select_dropdown/constants';
import DropdownContents from '~/vue_shared/components/color_select_dropdown/dropdown_contents.vue';
import DropdownContentsColorView from '~/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue';
import { color } from './mock_data';
const showDropdown = jest.fn();
const focusInput = jest.fn();
const defaultProps = {
dropdownTitle: '',
selectedColor: color,
dropdownButtonText: '',
variant: '',
isVisible: false,
};
const GlDropdownStub = {
template: `
<div>
<slot name="header"></slot>
<slot></slot>
</div>
`,
methods: {
show: showDropdown,
hide: jest.fn(),
},
};
const DropdownHeaderStub = {
template: `
<div>Hello, I am a header</div>
`,
methods: {
focusInput,
},
};
describe('DropdownContent', () => {
let wrapper;
const createComponent = ({ propsData = {} } = {}) => {
wrapper = shallowMount(DropdownContents, {
propsData: {
...defaultProps,
...propsData,
},
stubs: {
GlDropdown: GlDropdownStub,
DropdownHeader: DropdownHeaderStub,
},
});
};
afterEach(() => {
wrapper.destroy();
});
const findColorView = () => wrapper.findComponent(DropdownContentsColorView);
const findDropdownHeader = () => wrapper.findComponent(DropdownHeaderStub);
const findDropdown = () => wrapper.findComponent(GlDropdownStub);
it('calls dropdown `show` method on `isVisible` prop change', async () => {
createComponent();
await wrapper.setProps({
isVisible: true,
});
expect(showDropdown).toHaveBeenCalledTimes(1);
});
it('does not emit `setColor` event on dropdown hide if color did not change', () => {
createComponent();
findDropdown().vm.$emit('hide');
expect(wrapper.emitted('setColor')).toBeUndefined();
});
it('emits `setColor` event on dropdown hide if color changed on non-sidebar widget', async () => {
createComponent({ propsData: { variant: DROPDOWN_VARIANT.Embedded } });
const updatedColor = {
title: 'Blue-gray',
color: '#6699cc',
};
findColorView().vm.$emit('input', updatedColor);
await nextTick();
findDropdown().vm.$emit('hide');
expect(wrapper.emitted('setColor')).toEqual([[updatedColor]]);
});
it('emits `setColor` event on visibility change if color changed on sidebar widget', async () => {
createComponent({ propsData: { variant: DROPDOWN_VARIANT.Sidebar, isVisible: true } });
const updatedColor = {
title: 'Blue-gray',
color: '#6699cc',
};
findColorView().vm.$emit('input', updatedColor);
wrapper.setProps({ isVisible: false });
await nextTick();
expect(wrapper.emitted('setColor')).toEqual([[updatedColor]]);
});
it('renders header', () => {
createComponent();
expect(findDropdownHeader().exists()).toBe(true);
});
});

View File

@ -0,0 +1,40 @@
import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import DropdownHeader from '~/vue_shared/components/color_select_dropdown/dropdown_header.vue';
const propsData = {
dropdownTitle: 'Epic color',
};
describe('DropdownHeader', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMount(DropdownHeader, { propsData });
};
const findButton = () => wrapper.findComponent(GlButton);
afterEach(() => {
wrapper.destroy();
});
beforeEach(() => {
createComponent();
});
it('renders the correct title', () => {
expect(wrapper.text()).toBe(propsData.dropdownTitle);
});
it('renders a close button', () => {
expect(findButton().attributes('aria-label')).toBe('Close');
});
it('emits `closeDropdown` event on button click', () => {
expect(wrapper.emitted('closeDropdown')).toBeUndefined();
findButton().vm.$emit('click');
expect(wrapper.emitted('closeDropdown')).toEqual([[]]);
});
});

View File

@ -0,0 +1,46 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ColorItem from '~/vue_shared/components/color_select_dropdown/color_item.vue';
import DropdownValue from '~/vue_shared/components/color_select_dropdown/dropdown_value.vue';
import { color } from './mock_data';
const propsData = {
selectedColor: color,
};
describe('DropdownValue', () => {
let wrapper;
const findColorItems = () => wrapper.findAllComponents(ColorItem);
const createComponent = () => {
wrapper = shallowMountExtended(DropdownValue, { propsData });
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('when there is a color set', () => {
it('renders the color', () => {
expect(findColorItems()).toHaveLength(2);
});
it.each`
index | cssClass
${0} | ${['gl-font-base', 'gl-line-height-24']}
${1} | ${['hide-collapsed']}
`(
'passes correct props to the ColorItem with CSS class `$cssClass`',
async ({ index, cssClass }) => {
expect(findColorItems().at(index).props()).toMatchObject(propsData.selectedColor);
expect(findColorItems().at(index).classes()).toEqual(cssClass);
},
);
});
});

View File

@ -0,0 +1,30 @@
export const color = {
color: '#217645',
title: 'Green',
};
export const colorQueryResponse = {
data: {
workspace: {
id: 'gid://gitlab/Workspace/1',
issuable: {
__typename: 'Epic',
id: 'gid://gitlab/Epic/1',
color: '#217645',
},
},
},
};
export const updateColorMutationResponse = {
data: {
updateIssuableColor: {
issuable: {
__typename: 'Epic',
id: 'gid://gitlab/Epic/1',
color: '#217645',
},
errors: [],
},
},
};

View File

@ -46,14 +46,14 @@ export const findMonacoDiffEditor = () =>
export const findAndSetEditorValue = async (value) => {
const editor = await findMonacoEditor();
const uri = editor.getAttribute('data-uri');
const { uri } = editor.dataset;
monacoEditor.getModel(uri).setValue(value);
};
export const getEditorValue = async () => {
const editor = await findMonacoEditor();
const uri = editor.getAttribute('data-uri');
const { uri } = editor.dataset;
return monacoEditor.getModel(uri).getValue();
};

View File

@ -229,4 +229,31 @@ RSpec.describe ContainerRegistry::Migration do
it { is_expected.to eq(false) }
end
end
describe '.delete_container_repository_worker_support?' do
subject { described_class.delete_container_repository_worker_support? }
it { is_expected.to eq(true) }
context 'feature flag disabled' do
before do
stub_feature_flags(container_registry_migration_phase2_delete_container_repository_worker_support: false)
end
it { is_expected.to eq(false) }
end
end
describe '.dynamic_pre_import_timeout_for' do
let(:container_repository) { build(:container_repository) }
subject { described_class.dynamic_pre_import_timeout_for(container_repository) }
it 'returns the expected seconds' do
stub_application_setting(container_registry_pre_import_tags_rate: 0.6)
expect(container_repository).to receive(:tags_count).and_return(50)
expect(subject).to eq((0.6 * 50).seconds)
end
end
end

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