Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
48d25238c3
commit
d2675fa4de
|
@ -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*/**/*'
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}`);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 || '';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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'),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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(() =>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -39,7 +39,7 @@ export default () => {
|
|||
return createElement(TerraformList, {
|
||||
props: {
|
||||
emptyStateImage,
|
||||
terraformAdmin: el.hasAttribute('data-terraform-admin'),
|
||||
terraformAdmin: Object.hasOwn(el.dataset, 'terraformAdmin'),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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',
|
||||
},
|
||||
];
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,9 @@
|
|||
query epicColor($fullPath: ID!, $iid: ID) {
|
||||
workspace: group(fullPath: $fullPath) {
|
||||
id
|
||||
issuable: epic(iid: $iid) {
|
||||
id
|
||||
color
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
mutation updateEpicColor($input: UpdateEpicInput!) {
|
||||
updateIssuableColor: updateEpic(input: $input) {
|
||||
issuable: epic {
|
||||
id
|
||||
color
|
||||
}
|
||||
errors
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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 });
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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}"
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
64d492cca82603147226c9b0e6f424d2d2ba7a17ea0fe022510fb376016028e1
|
|
@ -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);
|
||||
|
|
|
@ -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**.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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* -
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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 > General > 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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
||||
|
|
|
@ -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 };
|
||||
});
|
||||
|
|
|
@ -256,7 +256,7 @@ describe('Members Utils', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
el = document.createElement('div');
|
||||
el.setAttribute('data-members-data', dataAttribute);
|
||||
el.dataset.membersData = dataAttribute;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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});`);
|
||||
});
|
||||
});
|
|
@ -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.',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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([[]]);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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: [],
|
||||
},
|
||||
},
|
||||
};
|
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue